Skip to content

Commit d48bbb1

Browse files
committed
feat(onUnhandled*): The set feature for unhandled paths.
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.
1 parent 8948460 commit d48bbb1

File tree

6 files changed

+565
-129
lines changed

6 files changed

+565
-129
lines changed

src/Router.js

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ var Keys = require('./Keys');
22
var parseTree = require('./parse-tree');
33
var matcher = require('./operations/matcher');
44
var recurseMatchAndExecute = require('./run/recurseMatchAndExecute');
5-
var runSetAction = require('./run/set/runSetAction');
65
var runCallAction = require('./run/call/runCallAction');
7-
var set = 'set';
86
var call = 'call';
97
var pathUtils = require('falcor-path-utils');
108
var collapse = pathUtils.collapse;
@@ -39,7 +37,7 @@ Router.prototype = {
3937
/**
4038
* Performs the get algorithm on the router.
4139
* @param {PathSet[]} paths -
42-
* @returns {JSONGraphEnvelope}
40+
* @returns {Observable.<JSONGraphEnvelope>}
4341
*/
4442
get: require('./router/get'),
4543

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

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

59-
var jsongCache = {};
60-
var action = runSetAction(this, jsong, jsongCache);
61-
return run(this._matcher, action, jsong.paths, set, this, jsongCache).
62-
63-
// Turn it(jsongGraph, invalidations, missing, etc.) into a
64-
// jsonGraph envelope
65-
map(function(details) {
66-
return {
67-
jsonGraph: details.jsonGraph
68-
};
69-
});
70+
/**
71+
* Takes in a function to call that has the same return inteface as any
72+
* route that will be called in the event of "unhandledPaths" on a set.
73+
*
74+
* What will come into the set function will be the subset of the jsonGraph
75+
* that represents the unhandledPaths of set.
76+
*
77+
* @param {Function} unhandledHandler -
78+
* @returns {undefined}
79+
*/
80+
onUnhandledSet: function(unhandledHandler) {
81+
this._unhandled.set = unhandled(this, unhandledHandler);
7082
},
7183

7284
call: function(callPath, args, suffixes, paths) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @param {PathSet} path - A simple path
3+
* @param {Object} tree - The tree should have `null` leaves to denote a
4+
* leaf node.
5+
*/
6+
module.exports = function hasIntersectionWithTree(path, tree) {
7+
return _hasIntersection(path, tree, 0);
8+
};
9+
10+
function _hasIntersection(path, node, depth) {
11+
12+
// Exit / base condition. We have reached the
13+
// length of our path and we are at a node of null.
14+
if (depth === path.length && node === null) {
15+
return true;
16+
}
17+
18+
var key = path[depth];
19+
var next = node[key];
20+
21+
// If its not undefined, then its a branch.
22+
if (node !== undefined) {
23+
return _hasIntersection(path, next, depth + 1);
24+
}
25+
26+
return false;
27+
}

src/router/set.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
var set = 'set';
2+
var recurseMatchAndExecute = require('./../run/recurseMatchAndExecute');
3+
var runSetAction = require('./../run/set/runSetAction');
4+
var materialize = require('../run/materialize');
5+
var Observable = require('rx').Observable;
6+
var spreadPaths = require('./../support/spreadPaths');
7+
var pathValueMerge = require('./../cache/pathValueMerge');
8+
var optimizePathSets = require('./../cache/optimizePathSets');
9+
var hasIntersectionWithTree = require('./../operations/matcher/intersection/hasIntersectionWithTree');
10+
var getValue = require('./../cache/getValue');
11+
var normalizePathSets = require('../operations/ranges/normalizePathSets');
12+
var pathUtils = require('falcor-path-utils');
13+
var collapse = pathUtils.collapse;
14+
15+
/**
16+
* @returns {Observable.<JSONGraph>}
17+
* @private
18+
*/
19+
module.exports = function routerSet(jsonGraph) {
20+
var jsongCache = {};
21+
var router = this;
22+
var action = runSetAction(router, jsonGraph, jsongCache);
23+
jsonGraph.paths = normalizePathSets(jsonGraph.paths);
24+
return recurseMatchAndExecute(router._matcher, action, jsonGraph.paths,
25+
set, router, jsongCache).
26+
27+
// Takes the jsonGraphEnvelope and extra details that comes out of the
28+
// recursive matching algorithm and either attempts the fallback
29+
// options or returns the built jsonGraph.
30+
flatMap(function(details) {
31+
var out = {
32+
jsonGraph: details.jsonGraph
33+
};
34+
35+
// If there is an unhandler then we should call that method and
36+
// provide the subset of jsonGraph that represents the missing
37+
// routes.
38+
if (details.unhandledPaths.length && router._unhandled.set) {
39+
var unhandledPaths = details.unhandledPaths;
40+
var jsonGraphFragment = {};
41+
42+
// PERFORMANCE: We know this is a potential performance downfall
43+
// PERFORMANCE: but we want to see if its even a corner case.
44+
// PERFORMANCE: Most likely this will not be hit, but if it does
45+
// PERFORMANCE: then we can take care of it
46+
// Set is interesting. This is what has to happen.
47+
// 1. incoming paths are spread so that each one is simple.
48+
// 2. incoming path, one at a time, are optimized by the
49+
// incoming jsonGraph.
50+
// 3. test intersection against incoming optimized path and
51+
// unhandledPathSet
52+
// 4. If 3 is true, build the jsonGraphFragment by using a
53+
// pathValue of optimizedPath and vale from un-optimized
54+
// path and original jsonGraphEnvelope.
55+
var jsonGraphEnvelope = {jsonGraph: jsonGraphFragment};
56+
var unhandledPathsTree = unhandledPaths.
57+
reduce(function(acc, path) {
58+
pathValueMerge(acc, {path: path, value: null});
59+
return acc;
60+
}, {});
61+
62+
// 1. Spread
63+
var pathIntersection = spreadPaths(jsonGraph.paths).
64+
65+
// 2.1 Optimize. We know its one at a time therefore we
66+
// just pluck [0] out.
67+
map(function(path) {
68+
return [
69+
// full path
70+
path,
71+
72+
// optimized path
73+
optimizePathSets(details.jsonGraph, [path],
74+
router.maxRefFollow)[0]]
75+
}).
76+
77+
// 2.2 Remove all the optimized paths that were found in
78+
// the cache.
79+
filter(function(x) { return x[1]; }).
80+
81+
// 3.1 test intersection.
82+
map(function(pathAndOPath) {
83+
var oPath = pathAndOPath[1];
84+
var hasIntersection = hasIntersectionWithTree(
85+
oPath, unhandledPathsTree);
86+
87+
// Creates the pathValue if there are a path
88+
// intersection
89+
if (hasIntersection) {
90+
var value =
91+
getValue(jsonGraph.jsonGraph, pathAndOPath[0]);
92+
93+
return {
94+
path: oPath,
95+
value: value
96+
};
97+
}
98+
99+
return null;
100+
}).
101+
102+
// 3.2 strip out nulls (the non-intersection paths).
103+
filter(function(x) { return x !== null; });
104+
105+
// 4. build the optimized JSONGraph envelope.
106+
pathIntersection.
107+
reduce(function(acc, pathValue) {
108+
pathValueMerge(acc, pathValue);
109+
return acc;
110+
}, jsonGraphFragment);
111+
112+
jsonGraphEnvelope.paths = collapse(
113+
pathIntersection.map(function(pV) {
114+
return pV.path;
115+
}));
116+
117+
return router._unhandled.set(out, unhandledPaths,
118+
jsonGraphEnvelope);
119+
}
120+
121+
return Observable.return(out);
122+
}).
123+
124+
// We will continue to materialize over the whole jsonGraph message.
125+
// This makes sense if you think about pathValues and an API that
126+
// if ask for a range of 10 and only 8 were returned, it would not
127+
// materialize for you, instead, allow the router to do that.
128+
map(function(jsonGraphEnvelope) {
129+
return materialize(router, jsonGraph.paths, jsonGraphEnvelope);
130+
});
131+
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
var R = require('../../../src/Router');
2+
var noOp = function() {};
3+
var chai = require('chai');
4+
var expect = chai.expect;
5+
var sinon = require('sinon');
6+
var pathValueMerge = require('./../../../src/cache/pathValueMerge');
7+
var Observable = require('rx').Observable;
8+
var $atom = require('./../../../src/support/types').$atom;
9+
10+
describe('#get', function() {
11+
it('should return an empty Observable and just materialize values.', function(done) {
12+
var router = new R([]);
13+
var onUnhandledPaths = sinon.spy(function convert(paths) {
14+
return Observable.empty();
15+
});
16+
router.onUnhandledGet(onUnhandledPaths);
17+
18+
var obs = router.
19+
get([['videos', 'summary']]);
20+
var onNext = sinon.spy();
21+
obs.
22+
doAction(onNext, noOp, function() {
23+
expect(onNext.calledOnce).to.be.ok;
24+
expect(onNext.getCall(0).args[0]).to.deep.equals({
25+
jsonGraph: {
26+
videos: {
27+
summary: {$type: $atom}
28+
}
29+
}
30+
});
31+
expect(onUnhandledPaths.calledOnce).to.be.ok;
32+
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
33+
['videos', 'summary']
34+
]);
35+
}).
36+
subscribe(noOp, done, done);
37+
});
38+
it('should call the onUnhandledGet when the route completely misses a route.', function(done) {
39+
var router = new R([]);
40+
var onUnhandledPaths = sinon.spy(function convert(paths) {
41+
return paths.reduce(function(jsonGraph, path) {
42+
pathValueMerge(jsonGraph.jsonGraph, {
43+
path: path,
44+
value: 'missing'
45+
});
46+
return jsonGraph;
47+
}, {jsonGraph: {}});
48+
});
49+
router.onUnhandledGet(onUnhandledPaths);
50+
51+
var obs = router.
52+
get([['videos', 'summary']]);
53+
var onNext = sinon.spy();
54+
obs.
55+
doAction(onNext, noOp, function() {
56+
expect(onNext.calledOnce).to.be.ok;
57+
expect(onNext.getCall(0).args[0]).to.deep.equals({
58+
jsonGraph: {
59+
videos: {
60+
summary: 'missing'
61+
}
62+
}
63+
});
64+
expect(onUnhandledPaths.calledOnce).to.be.ok;
65+
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
66+
['videos', 'summary']
67+
]);
68+
}).
69+
subscribe(noOp, done, done);
70+
});
71+
72+
it('should call the onUnhandledGet when the route partially misses a route.', function(done) {
73+
var router = new R([{
74+
route: 'videos.length',
75+
get: function() {
76+
return {
77+
path: ['videos', 'length'],
78+
value: 5
79+
};
80+
}
81+
}]);
82+
var onUnhandledPaths = sinon.spy(function convert(paths) {
83+
return paths.reduce(function(jsonGraph, path) {
84+
pathValueMerge(jsonGraph.jsonGraph, {
85+
path: path,
86+
value: 'missing'
87+
});
88+
return jsonGraph;
89+
}, {jsonGraph: {}});
90+
});
91+
router.onUnhandledGet(onUnhandledPaths);
92+
93+
var obs = router.
94+
get([['videos', ['length', 'summary']]]);
95+
var onNext = sinon.spy();
96+
obs.
97+
doAction(onNext, noOp, function() {
98+
expect(onNext.calledOnce).to.be.ok;
99+
expect(onNext.getCall(0).args[0]).to.deep.equals({
100+
jsonGraph: {
101+
videos: {
102+
summary: 'missing',
103+
length: 5
104+
}
105+
}
106+
});
107+
expect(onUnhandledPaths.calledOnce).to.be.ok;
108+
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
109+
['videos', 'summary']
110+
]);
111+
}).
112+
subscribe(noOp, done, done);
113+
});
114+
});

0 commit comments

Comments
 (0)