mrwade / pave

Paving the way to better state management.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Pave

Paving the way to better state management.

Try out pave in your browser.

Why?

Pave is inspired by Relay, GraphQL, Falcor and Om (next). Pave attempts to take the best pieces of each (subjectively) and expose a simple API that makes managing app state much easier.

Goals

  • Performance: The core of any data layer will have many hot paths and should run efficiently.

  • Schema-less: Unlike GraphQL, there is no requirement that your data is strictly typed.

  • POJO: All data is represented as JSON-friendly Plain Ol' JavaScript Objects so there is no need to worry about how to serialize X and how to deserialize Y.

  • Multiple Remotes: Create a client side router to use Pave with an existing REST API, implement a Pave router on the server or mix and match. Allowing multiple remotes both on the client and server makes integrating Pave into an existing project manageable.

Install

npm install pave

API

Router

new Router({maxQueryCost, routes: Object})

  • maxQueryCost (optional) is disabled by default. Use this property to limit the number of path parts that may be parsed in a single run.

    import {Router} from 'pave';
    
    const router = new Router({maxQueryCost: 10});
    
    // query cost is 3, no problem
    router.run({query: ['foo', 'bar', 'baz']});
    
    // query cost is 6, also no problem
    router.run({query: ['foo', [0, 1], 'bar']});
    
    // query cost is 12, will throw an error
    router.run({query: ['foo', [0, 1], ['bar', 'baz']});
  • routes is an object that maps route matchers to their handlers. The keys of this object can use one or a mix of different keywords.

    • $key matches exactly 1 scalar value

    • $keys matches n scalar values

    • $obj matches exactly 1 object

    • $objs matches n objects

    • * matches everything

    Handlers should return either a delta to be applied to the store cache or a promise.

    import {Router} from 'pave';
    
    const router = new Router({
      routes: {
    
        // A round that returns blog posts. The $obj argument will contain options
        // for listing these posts (in the example below, sort order). The $keys
        // argument will contain the indices of the items to return.
        'blogPosts.$obj.$keys':
        ({1: options, 2: range}) =>
          db('blogPosts')
            .select('*')
            .orderBy('createdAt', options.order || 'desc')
            .offset(range[0])
            .limit(range[range.length - 1] - range[0])
            .then(blogPosts => ({
              blogPostsById: {
                blogPosts.reduce((posts, post) => {
                  posts[post.id] = {$set: post};
                  return posts;
                }, {})
              },
              blogPosts: {
                [toKey(options)]: range.reduce((posts, n, i) => {
                  posts[n] = {$set: {$ref: ['blogPostsById', blogPosts[i].id]}};
                  return posts;
                }
              }
            })),
    
        // The catch-all matcher can be used to throw an error when no route
        // matches or proxy unmatched paths to another router.
        '*': ({paths}) => throw new Error(`No route found for ${paths}`)
      }
    });

router.run({query, context}) => SyncPromise

Run the specified query and pass the given context through to each route handler.

Store

new Store({cache: Object, router: Router})

  • cache (optional) is the initial value of the cache. Defaults to {}.

  • router (optional) is the router to be used when invoking run.

store.get(Array) => Any

Returns the fully-resolved graph starting at the given path.

import {Store} from 'pave';

const store = new Store({
  cache: {
    foo: {bar: {$ref: ['baz']}},
    baz: {name: 'Mr. Baz'}
  }
});

store.get([]); // => {foo: {bar: {name: 'Mr. Baz'}}, baz: {name: 'Mr. Baz'}}
store.get(['foo']); // => {bar: {name: 'Mr. Baz'}}
store.get(['foo', 'bar', 'name']); // => 'Mr. Baz'
store.get(['doesNotExist']); // => undefined

store.getRaw(Array) => Any

Returns the unresolved graph starting at the given path.

import {Store} from 'pave';

const store = new Store({
  cache: {
    foo: {bar: {$ref: ['baz']}},
    baz: {name: 'Mr. Baz'}
  }
});

store.getRaw([]); // => {foo: {bar: {$ref: ['baz']}}, baz: {name: 'Mr. Baz'}}
store.getRaw(['foo']); // => {bar: {$ref: ['baz']}}
store.getRaw(['foo', 'bar', 'name']); // => 'Mr. Baz'
store.getRaw(['doesNotExist']); // => undefined

store.resolve(Array) => Array

Resolves a path in the graph to its simplest form.

import {Store} from 'pave';

const store = new Store({
  cache: {
    foo: {bar: {$ref: ['baz']}},
    baz: {name: 'Mr. Baz'}
  }
});

store.resolve([]); // => []
store.resolve(['foo']); // => ['foo']
store.resolve(['foo', 'bar', 'name']); // => ['baz', 'name']
store.resolve(['doesNotExist']); // => ['doesNotExist']

store.update(Array or Object) => Store

Immutably updates the cache based on the delta directives given. Available directives are $set, $merge, $apply, $splice, $push, $pop, $shift and $unshift. The array methods also work on array-like objects.

import {Store} from 'pave';

const store = new Store({
  cache: {
    foo: {bar: {$ref: ['baz']}},
    baz: {name: 'Mr. Baz'}
  }
});

const {cache} = store;
store.update({baz: {name: {$set: 'Dr. Baz'}}});

// The original cache value is untouched. Store previous cache values to create
// an undo/redo stack.
cache === store.cache; // => false
store.get(['baz', 'name']); // => 'Dr. Baz'

store.update({whos: {$set: ['Cindy Lou', 'Augustus May']}});
store.get(['whos']); // => ['Cindy Lou', 'Augustus May']

store.update({whos: {$splice: [[0, 1, 'Martha May'], [2, 0, 'Betty Lou']]}});
store.get(['whos']); // => ['Martha May', 'Augustus May', 'Betty Lou']

store.run(router.run options) => SyncPromise

Passes the options to store.router.run and uses store to cache results. See router.run for run information.

store.update(Array or Object) => Store

Immutably updates the cache based on the delta directives given. Available directives are $set, $merge, $apply, $splice, $push, $pop, $shift and $unshift. The array methods also work on array-like objects.

import {Store} from 'pave';

const store = new Store({
  cache: {
    foo: {bar: {$ref: ['baz']}},
    baz: {name: 'Mr. Baz'}
  }
});

const {cache} = store;
store.update({baz: {name: {$set: 'Dr. Baz'}}});

// The original cache value is untouched. Store previous cache values to create
// an undo/redo stack.
cache === store.cache; // => false
store.get(['baz', 'name']); // => 'Dr. Baz'

store.update({whos: {$set: ['Cindy Lou', 'Augustus May']}});
store.get(['whos']); // => ['Cindy Lou', 'Augustus May']

store.update({whos: {$splice: [[0, 1, 'Martha May'], [2, 0, 'Betty Lou']]}});
store.get(['whos']); // => ['Martha May', 'Augustus May', 'Betty Lou']

store.watch(Array, Function) => Store

Watch a query for changes.

import {Store} from 'pave';

const store = new Store({cache: {foo: 123}});

const handler = prev => {
  // The first argument, `prev` in this case, is the previous state of the
  // cache.
  console.log('foo changed!');
};

store.watch(['foo'], handler);
store.update({foo: {$set: 456}}); // The handler above will fire.

store.unwatch(Function) => Store

Stop watching a query for changes.

toKey(Any) => String

Primarily useful for consistently serializing objects, toKey will take any value and convert it to it's Pave path segment representation.

import {toKey} from 'pave';

toKey('foo'); // => 'foo'
toKey(123); // => '123'
toKey({a: 1, b: 2}); // => '{"a":1,"b":2}'
toKey({b: 2, a: 1}); // => '{"a":1,"b":2}'

About

Paving the way to better state management.

License:MIT License


Languages

Language:JavaScript 98.7%Language:Makefile 1.3%