npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

syme

v1.4.0

Published

A model set for allowing you to share data contracts between your view and the rest of your application, take your props on the go

Downloads

79

Readme

syme

data layer tools for universal web apps

Overview

This package takes the stance that data contracts should live on data instances and not on the interfaces that consume them. The impact of changes to these contracts is then easily audited by searching for instances of their definition, leading to easier to maintain code. The core of this system is the Model class. Model sets up a living data contract between frontend services, transforming and validating data, sending events on changes, and setting up rules for data hierarchy and initialization. Built on top of this are two helper classes. ModelListener is a wrapper around React.Component that takes over the duty of storing information on the view layer and keeps it in the model layer, setting up binding routes on the way. Any time a model updates, the view updates, and the view can send information to the model to allow it to modify the underlying data model. Setting all this up removes the siloing of data structure definition in the view (props) and allows that structure to be used in services, api calls, application logic, etc. Cache is an easy way to persist your data layer (whether it be in memory, local storage, or session storage) that also bundles up request contexts for you on the server, saving your site from making those extra api calls just to reload state.

Table of Contents

Model

Model has a simple interface, all documented in the source code, but sometimes examples are easier to parse. The most basic constructor looks like this:

class MyFirstModel extends Model {
    constructor(data) {
        super({
            id: 0,
            name: ''
        });

        this.fill(data);
    }
}

This example creates a model called MyFirstModel that defines it having the fields id and name with default values and hydrates based on it's constructor parameters.

hierarchy

You can add hierarchy to models by including the class name as a default parameter:

class Name extends Model {
    constructor(data) {
        super({
            first: '',
            last: ''
        });

        this.fill(data);
    }

    fullName() {
        return this.first + ' ' + this.last;
    }
}

class Permission extends Model {
    constructor(data) {
        super({
            id: 0,
            code: '',
            name: ''
        });

        this.fill(data);
    }
}

class AuthUser extends Model {
    constructor(data) {
        super({
            name: Name,
            perms: [ Permission ]
        });

        this.fill(data);
    }

    can(perm) {
        for (var i = 0; i < this.perms.length; i++) {
            if (this.perms[i].code === perm) {
                return true;
            }
        }

        return false;
    }
}

var MyAuth = new AuthUser({
    name: {
        first: 'super',
        last: 'dev'
    },
    perms: [{
        id: 12,
        code: 'delete',
        name: 'User can delete posts'
    }, {
        id: 64,
        code: 'edit',
        name: 'User can edit posts'
    }]
});

console.log(MyAuth.can('edit')); // outputs true
console.log(MyAuth.name.fullName()); // outputs 'super dev'

inheritance

If you want to inherit from a model, you can use the extend function:

class Animal extends Model {
    constructor(data) {
        super({
            species: '',
            name: ''
        });

        this.fill(data);
    }
}

class LoudAnimal extends Animal {
    constructor(data) {
        super().extend({
            sound: '',
            level: 0
        });

        this.fill(data);
    }
}

transformation

Sometimes data doesn't always come in clean. Sometimes you want to represent your data differently inside of your application logic than outside. There are even times when you have to take a data format and tear it to a million pieces to make it work. You can do this by overwriting the model's fill function. The out function works exactly the same, but in reverse.

class MyFirstTransform extends Model {
    constructor(data) {
        super({
            name: '',
            itemCount: 0
        });

        this.fill(data);
    }

    fill(data) {
        if (!data) {
            return this;
        }

        if (data.hasOwnProperty('items')) {
            data.itemCount = data.items.length;
            delete data.items;
        }

        super.fill(data);
        return this;
    }
}

ModelListener

The point of the ModelListener is to be as transparent as possible. If you want to reuse your data layer across your application, you need only to extend from ModelListener instead of React.Component and pass that data contract as prop model. This will instantly send all model updates to the view using the model's internal dirty flag and rate limiter. Lets look at a simple example:

class MyComponent extends ModelListener {
    static defaultProps = {
        model: MyFirstModel
    };

    render() {
        return <div>{ this.model.name }</div>
    }
}

React.createElement(<MyComponent model={ { name: 'super dev' } } />);

A two way street

The ModelListener also provides a way to update your model from the view through the update function. Here we are pumping the value from the input field into the model's name field.

class MyComponent extends ModelListener {
    static defaultProps = {
        model: MyFirstModel
    };

    update(field, evt) {
        super.update(field, evt.target.value);
    }

    render() {
        return (
            <input defaultValue={ this.model.name }
                onChange={ this.update.bind(this, 'name') } />
        );
    }
}

Cache

A cache is used whenever you need a centralized place within your application to maintain data. As long as any two instances reference the same cache definition, they should be referencing the same data. There's some nice features in here like adding an expiration to the data and being able to subscribe to changes in the cache. To create a cache, just make a definition:

class MyCache extends Cache {
    constructor() {
        super({
            key: 'todos',
            channel: 'local',
            expiration: 5 * 60 * 1000
        });
    }
}

now, whenever you fetch data (say on a button click), just populate the cache:

function fetch() {
    return new Promise((fulfill, reject) => {
        const cache = new MyCache();

        if (cache.cached) {
            fulfill(cache.cached);
        }

        apiRequest('//url')
            .then(resp) {
                cache.populate(resp.data);

                fulfill(cache.cached);
            }
    });
}

and if you want something else in your application to keep in step with the changes to your cache:

class MyViewClass {
    constructor() {
        const cache = new MyCache();

        cache.watch((data) => {
            this.update(data);
        });
    }
}

You can define a cache as existing in memory (channel: 'memory'), localStorage (channel: 'local'), or sessionStorage (channel: 'session'). If the interface is not available for the code's environment, it rolls back in persistance until it hits the memory layer.

Server Side Caches

Server side caches default to being in memory. You don't have to change any of your client side code for it to do this. They use a super sweet project called 'continuation-local-storage' to create a request focused namespace dedicated to your cache. To enable this, add middleware before you start building a request:

// in express
import { createNamespace } from 'continuation-local-storage';
import express from 'express';

const app = express();
app.use((req, resp, next) => {
    createNamespace('ServerState', () => {
        next();
    });
});

// the rest of your application

building the server's state is particularly nice with the react-resolver package as it's not bound to routes, which helps in reusability of components:

import { resolve } from 'react-resolver';

class MyComponent extends React.Component {
    render() {
        return <h1>{ this.props.model.name }</h1>
    }
}

export default resolve({
    model: () => {
        return new Promise((fulfill, reject) => {
            const cache = new MyCache();

            if (cache.cached) {
                fulfill(cache.cached);
            } else {
                apiRequest()
                    .then((resp) => {
                        cache.populate(resp.data);
                        fulfill(cache.cached);
                    });
            }
        });
    }
})(MyComponent);

follow that project's instructions for rendering on the server, and then make sure to output the server's state to the client so that no additional fetch is required to build the client state. You can do this by pumping the string rendered from StorageController.out() into an empty script tag on the page:

import StorageController from 'syme/dist/internal/storage-controller';

app.use((req, resp) => {
    match({
        routes: routes,
        location: req.url
    }, (error, redirectLocation, renderProps) => {
        Resolver
            .resolve(() => (
                <RouterContext { ...renderProps } />
            ))
            .then(({ Resolved }) => {
                resp.end(
                    [
                        '<!DOCTYPE html>',
                        '<html>',
                            '<head>',
                                `<script>${ StorageController.out() }</script>`,
                            '</head>',
                            '<body>',
                                '<div id="page">',
                                    React.renderToString(<Resolved />),
                                '</div>',
                            '</body>',
                        '</html>'
                    ].join('')
                );
            });
    });
});