Paving the way to better state management.
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.
-
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 deserializeY
. -
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.
npm install pave
-
maxQueryCost
(optional) is disabled by default. Use this property to limit the number of path parts that may be parsed in a singlerun
.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
matchesn
scalar values -
$obj
matches exactly 1 object -
$objs
matchesn
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}`) } });
-
Run the specified query and pass the given context through to each route handler.
-
cache
(optional) is the initial value of the cache. Defaults to{}
. -
router
(optional) is the router to be used when invokingrun
.
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
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
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']
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']
Passes the options to store.router.run
and uses store
to cache results. See
router.run
for run
information.
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']
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.
Stop watching a query for changes.
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}'