Skip to content

Commit

Permalink
feat(onUnhandled*): The set feature for unhandled paths.
Browse files Browse the repository at this point in the history
The set feature does all the proper optimizing of jsonGraph going into
the next dataSource.  There is an upcoming change where it will be
dataSource only.  For now the three methods will persist for a bit.
  • Loading branch information
ThePrimeagen committed Dec 15, 2015
1 parent 8948460 commit d48bbb1
Show file tree
Hide file tree
Showing 6 changed files with 565 additions and 129 deletions.
42 changes: 27 additions & 15 deletions src/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ var Keys = require('./Keys');
var parseTree = require('./parse-tree');
var matcher = require('./operations/matcher');
var recurseMatchAndExecute = require('./run/recurseMatchAndExecute');
var runSetAction = require('./run/set/runSetAction');
var runCallAction = require('./run/call/runCallAction');
var set = 'set';
var call = 'call';
var pathUtils = require('falcor-path-utils');
var collapse = pathUtils.collapse;
Expand Down Expand Up @@ -39,7 +37,7 @@ Router.prototype = {
/**
* Performs the get algorithm on the router.
* @param {PathSet[]} paths -
* @returns {JSONGraphEnvelope}
* @returns {Observable.<JSONGraphEnvelope>}
*/
get: require('./router/get'),

Expand All @@ -54,19 +52,33 @@ Router.prototype = {
this._unhandled.get = unhandled(this, unhandledHandler);
},

set: function(jsong) {
/**
* Takes in a jsonGraph and outputs a Observable.<jsonGraph>. The set
* method will use get until it evaluates the last key of the path inside
* of paths. At that point it will produce an intermediate structure that
* matches the path and has the value that is found in the jsonGraph env.
*
* One of the requirements for interaction with a dataSource is that the
* set message must be optimized to the best of the incoming sources
* knowledge.
*
* @param {JSONGraphEnvelope} jsonGraph -
* @returns {Observable.<JSONGraphEnvelope>}
*/
set: require('./router/set'),

var jsongCache = {};
var action = runSetAction(this, jsong, jsongCache);
return run(this._matcher, action, jsong.paths, set, this, jsongCache).

// Turn it(jsongGraph, invalidations, missing, etc.) into a
// jsonGraph envelope
map(function(details) {
return {
jsonGraph: details.jsonGraph
};
});
/**
* Takes in a function to call that has the same return inteface as any
* route that will be called in the event of "unhandledPaths" on a set.
*
* What will come into the set function will be the subset of the jsonGraph
* that represents the unhandledPaths of set.
*
* @param {Function} unhandledHandler -
* @returns {undefined}
*/
onUnhandledSet: function(unhandledHandler) {
this._unhandled.set = unhandled(this, unhandledHandler);
},

call: function(callPath, args, suffixes, paths) {
Expand Down
27 changes: 27 additions & 0 deletions src/operations/matcher/intersection/hasIntersectionWithTree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @param {PathSet} path - A simple path
* @param {Object} tree - The tree should have `null` leaves to denote a
* leaf node.
*/
module.exports = function hasIntersectionWithTree(path, tree) {
return _hasIntersection(path, tree, 0);
};

function _hasIntersection(path, node, depth) {

// Exit / base condition. We have reached the
// length of our path and we are at a node of null.
if (depth === path.length && node === null) {
return true;
}

var key = path[depth];
var next = node[key];

// If its not undefined, then its a branch.
if (node !== undefined) {
return _hasIntersection(path, next, depth + 1);
}

return false;
}
131 changes: 131 additions & 0 deletions src/router/set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
var set = 'set';
var recurseMatchAndExecute = require('./../run/recurseMatchAndExecute');
var runSetAction = require('./../run/set/runSetAction');
var materialize = require('../run/materialize');
var Observable = require('rx').Observable;
var spreadPaths = require('./../support/spreadPaths');
var pathValueMerge = require('./../cache/pathValueMerge');
var optimizePathSets = require('./../cache/optimizePathSets');
var hasIntersectionWithTree = require('./../operations/matcher/intersection/hasIntersectionWithTree');
var getValue = require('./../cache/getValue');
var normalizePathSets = require('../operations/ranges/normalizePathSets');
var pathUtils = require('falcor-path-utils');
var collapse = pathUtils.collapse;

/**
* @returns {Observable.<JSONGraph>}
* @private
*/
module.exports = function routerSet(jsonGraph) {
var jsongCache = {};
var router = this;
var action = runSetAction(router, jsonGraph, jsongCache);
jsonGraph.paths = normalizePathSets(jsonGraph.paths);
return recurseMatchAndExecute(router._matcher, action, jsonGraph.paths,
set, router, jsongCache).

// Takes the jsonGraphEnvelope and extra details that comes out of the
// recursive matching algorithm and either attempts the fallback
// options or returns the built jsonGraph.
flatMap(function(details) {
var out = {
jsonGraph: details.jsonGraph
};

// If there is an unhandler then we should call that method and
// provide the subset of jsonGraph that represents the missing
// routes.
if (details.unhandledPaths.length && router._unhandled.set) {
var unhandledPaths = details.unhandledPaths;
var jsonGraphFragment = {};

// PERFORMANCE: We know this is a potential performance downfall
// PERFORMANCE: but we want to see if its even a corner case.
// PERFORMANCE: Most likely this will not be hit, but if it does
// PERFORMANCE: then we can take care of it
// Set is interesting. This is what has to happen.
// 1. incoming paths are spread so that each one is simple.
// 2. incoming path, one at a time, are optimized by the
// incoming jsonGraph.
// 3. test intersection against incoming optimized path and
// unhandledPathSet
// 4. If 3 is true, build the jsonGraphFragment by using a
// pathValue of optimizedPath and vale from un-optimized
// path and original jsonGraphEnvelope.
var jsonGraphEnvelope = {jsonGraph: jsonGraphFragment};
var unhandledPathsTree = unhandledPaths.
reduce(function(acc, path) {
pathValueMerge(acc, {path: path, value: null});
return acc;
}, {});

// 1. Spread
var pathIntersection = spreadPaths(jsonGraph.paths).

// 2.1 Optimize. We know its one at a time therefore we
// just pluck [0] out.
map(function(path) {
return [
// full path
path,

// optimized path
optimizePathSets(details.jsonGraph, [path],
router.maxRefFollow)[0]]
}).

// 2.2 Remove all the optimized paths that were found in
// the cache.
filter(function(x) { return x[1]; }).

// 3.1 test intersection.
map(function(pathAndOPath) {
var oPath = pathAndOPath[1];
var hasIntersection = hasIntersectionWithTree(
oPath, unhandledPathsTree);

// Creates the pathValue if there are a path
// intersection
if (hasIntersection) {
var value =
getValue(jsonGraph.jsonGraph, pathAndOPath[0]);

return {
path: oPath,
value: value
};
}

return null;
}).

// 3.2 strip out nulls (the non-intersection paths).
filter(function(x) { return x !== null; });

// 4. build the optimized JSONGraph envelope.
pathIntersection.
reduce(function(acc, pathValue) {
pathValueMerge(acc, pathValue);
return acc;
}, jsonGraphFragment);

jsonGraphEnvelope.paths = collapse(
pathIntersection.map(function(pV) {
return pV.path;
}));

return router._unhandled.set(out, unhandledPaths,
jsonGraphEnvelope);
}

return Observable.return(out);
}).

// We will continue to materialize over the whole jsonGraph message.
// This makes sense if you think about pathValues and an API that
// if ask for a range of 10 and only 8 were returned, it would not
// materialize for you, instead, allow the router to do that.
map(function(jsonGraphEnvelope) {
return materialize(router, jsonGraph.paths, jsonGraphEnvelope);
});
};
114 changes: 114 additions & 0 deletions test/unit/functional/unhandled.get.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
var R = require('../../../src/Router');
var noOp = function() {};
var chai = require('chai');
var expect = chai.expect;
var sinon = require('sinon');
var pathValueMerge = require('./../../../src/cache/pathValueMerge');
var Observable = require('rx').Observable;
var $atom = require('./../../../src/support/types').$atom;

describe('#get', function() {
it('should return an empty Observable and just materialize values.', function(done) {
var router = new R([]);
var onUnhandledPaths = sinon.spy(function convert(paths) {
return Observable.empty();
});
router.onUnhandledGet(onUnhandledPaths);

var obs = router.
get([['videos', 'summary']]);
var onNext = sinon.spy();
obs.
doAction(onNext, noOp, function() {
expect(onNext.calledOnce).to.be.ok;
expect(onNext.getCall(0).args[0]).to.deep.equals({
jsonGraph: {
videos: {
summary: {$type: $atom}
}
}
});
expect(onUnhandledPaths.calledOnce).to.be.ok;
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
['videos', 'summary']
]);
}).
subscribe(noOp, done, done);
});
it('should call the onUnhandledGet when the route completely misses a route.', function(done) {
var router = new R([]);
var onUnhandledPaths = sinon.spy(function convert(paths) {
return paths.reduce(function(jsonGraph, path) {
pathValueMerge(jsonGraph.jsonGraph, {
path: path,
value: 'missing'
});
return jsonGraph;
}, {jsonGraph: {}});
});
router.onUnhandledGet(onUnhandledPaths);

var obs = router.
get([['videos', 'summary']]);
var onNext = sinon.spy();
obs.
doAction(onNext, noOp, function() {
expect(onNext.calledOnce).to.be.ok;
expect(onNext.getCall(0).args[0]).to.deep.equals({
jsonGraph: {
videos: {
summary: 'missing'
}
}
});
expect(onUnhandledPaths.calledOnce).to.be.ok;
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
['videos', 'summary']
]);
}).
subscribe(noOp, done, done);
});

it('should call the onUnhandledGet when the route partially misses a route.', function(done) {
var router = new R([{
route: 'videos.length',
get: function() {
return {
path: ['videos', 'length'],
value: 5
};
}
}]);
var onUnhandledPaths = sinon.spy(function convert(paths) {
return paths.reduce(function(jsonGraph, path) {
pathValueMerge(jsonGraph.jsonGraph, {
path: path,
value: 'missing'
});
return jsonGraph;
}, {jsonGraph: {}});
});
router.onUnhandledGet(onUnhandledPaths);

var obs = router.
get([['videos', ['length', 'summary']]]);
var onNext = sinon.spy();
obs.
doAction(onNext, noOp, function() {
expect(onNext.calledOnce).to.be.ok;
expect(onNext.getCall(0).args[0]).to.deep.equals({
jsonGraph: {
videos: {
summary: 'missing',
length: 5
}
}
});
expect(onUnhandledPaths.calledOnce).to.be.ok;
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
['videos', 'summary']
]);
}).
subscribe(noOp, done, done);
});
});
Loading

0 comments on commit d48bbb1

Please sign in to comment.