-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FalcorJSON $__status is 'resolved' when model progressively returns partial response from cache #15
Comments
Interesting, I can put together a failing test case, if helpful. |
@jameslaneconkling you're intuition about I thought I had test coverage, but it looks like I only have tests covering the |
@jameslaneconkling ok, I think I fixed the issue. I just published v2.9.7 to npm, let me know if this doesn't solve the problem for you. |
So, the above issue was failing two of my tests with // fails 2.9.6 but passes 2.9.7 when recycleJSON is false
test('Should emit progressively for query in local cache and remote service', (t) => {
t.plan(2);
const {
stream: change$,
handler: graphChange
} = createEventHandler();
const model = new Model({
source: {
get: () => Observable.of({
paths: [['items', 1, 'title']],
jsonGraph: {
items: {
1: { $type: 'ref', value: ['item', 'b'] }
},
item: {
b: { title: 'Item B' }
}
}
}).delay(100)
},
cache: {
items: {
0: { $type: 'ref', value: ['item', 'a'] }
},
item: {
a: { title: 'Item A' }
}
},
recycleJSON: RECYCLEJSON,
onChange: graphChange
});
const paths = () => [['items', [0, 1], 'title']];
const expectedResults = [
{
graphFragment: { json: { items: { 0: { title: 'Item A' } } } },
graphFragmentStatus: 'next',
id: 2
},
{
graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' } } } },
graphFragmentStatus: 'complete',
id: 2
}
];
withGraphFragment(paths, model, change$)(Observable.of({ id: 2 }))
.subscribe(tapeResultObserver(t, RECYCLEJSON)(expectedResults));
});
# Should emit progressively for query in local cache and remote service
✖ emission 2 should match expected output
operator: deepEqual
expected: |-
{ graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' } } } }, graphFragmentStatus: 'complete', id: 2 }
actual: |-
{ id: 2, graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' } } } }, graphFragmentStatus: 'next' } // fails both 2.9.6 and 2.9.7 when recycleJSON is false
test('Should emit next when props change updates path', (t) => {
t.plan(4);
const {
stream: change$,
handler: graphChange
} = createEventHandler();
let i = 0;
const model = new Model({
source: {
get: () => {
i += 1;
if (i === 1) {
return Observable.of({
paths: [['items', { to: 1 }, 'title'],['items', 'length']],
jsonGraph: {
items: {
0: { $type: 'ref', value: ['item', 'a'] },
1: { $type: 'ref', value: ['item', 'b'] },
length: 50
},
item: {
a: { title: 'Item A' },
b: { title: 'Item B' }
}
}
}).delay(100);
} else if (i === 2) {
return Observable.of({
paths: [['items', 2, 'title']],
jsonGraph: {
items: {
2: { $type: 'ref', value: ['item', 'c'] }
},
item: {
c: { title: 'Item C' }
}
}
}).delay(100);
}
return Observable.throw('Shouldn\'t run');
}
},
recycleJSON: RECYCLEJSON,
onChange: graphChange
});
const paths = ({ range }) => [['items', range, 'title'], ['items', 'length']];
const expectedResults = [
{
graphFragment: {},
graphFragmentStatus: 'next',
range: { to: 1 }
},
{
graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' }, length: 50 } } },
graphFragmentStatus: 'complete',
range: { to: 1 }
},
{
graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' }, length: 50 } } },
graphFragmentStatus: 'next',
range: { to: 2 }
},
{
graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' }, 2: { title: 'Item C' }, length: 50 } } },
graphFragmentStatus: 'complete',
range: { to: 2 }
},
];
withGraphFragment(paths, model, change$)(
Observable.of({ range: { to: 1 } })
.concat(Observable.of({ range: { to: 2 } }).delay(200))
)
.subscribe(tapeResultObserver(t, RECYCLEJSON)(expectedResults));
});
✖ emission 3 should match expected output
operator: deepEqual
expected: |-
{ graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' }, length: 50 } } }, graphFragmentStatus: 'next', range: { to: 2 } }
actual: |-
{ range: { to: 2 }, graphFragment: { json: { items: { 0: { title: 'Item A' }, 1: { title: 'Item B' }, length: 50 } } }, graphFragmentStatus: 'complete' } There's some of my own application code in there. If helpful, I can rewrite the tests to just use the model so output is more comprehensible. The translation bt/ the model logic and my app is pretty light, though: |
@jameslaneconkling it looks like the second test may be expecting the wrong first value? I wouldn't expect progressive get to emit an empty JSON branch when none of the requested paths are in the cache, only when at least one is. I have a demo in esnextb.in that I think replicates your test, and the output looks right to me: const m = new Model({ source: getDataSource() });
const get1 = Observable.from(m.get(['items', 'length'], ['items', {to: 1}, 'title']).progressively());
const get2 = Observable.from(m.get(['items', 'length'], ['items', {to: 2}, 'title']).progressively());
Observable.concat(
get1.do((x) => console.log(`get1:`, x.json.$__status, x.json.toString())),
get2.do((x) => console.log(`get2:`, x.json.$__status, x.json.toString()))
)
.subscribe();
/*
> dataSource get 0
> get1: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
> get2: pending {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
> dataSource get 1
> get2: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"2":{"title":"Item C"},"length":50}}
*/ |
@trxcllnt my bad. Yeah, the leading emit is application logic on my end: the You're test does demonstrate what I'd expect ( |
OK, have can recreate the issue: const get1 = Observable.from(m.get(['items', {to: 1}, 'title'], ['items', 'length']).progressively());
const get2 = Observable.from(m.get(['items', {to: 2}, 'title'], ['items', 'length']).progressively());
/*
get1: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
get2: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
get2: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"2":{"title":"Item C"},"length":50}}
*/ Notice, the paths are switched compared to your version ( |
@jameslaneconkling OH duh. I know exactly what's going wrong. Will work on a fix today. Thanks again 🥇 ! |
@jameslaneconkling after investigating this a bit, I think I know why I punted on making this work for the old mode. The commit 1571eeb will fix it for the case you've encountered here, but not for all scenarios. Specifically, it's only fixed when the first path is at least partially in the cache. If the first path is entirely missing, but the second path isn't, you'll see the behavior in this issue again. To recap, this now works: const get1 = m.get(['items', {to: 1}, 'title'], ['items', 'length']).progressively();
const get2 = m.get(['items', {to: 2}, 'title'], ['items', 'length']).progressively();
/*
> dataSource get 0
> get1: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
> get2: pending {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
> dataSource get 1
> get2: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"2":{"title":"Item C"},"length":50}}
*/ But if we change the first path of const get1 = m.get(['items', {to: 1}, 'title'], ['items', 'length']).progressively();
const get2 = m.get(['items', 2, 'title'], ['items', 'length']).progressively();
/*
> dataSource get 0
> get1: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
> get2: resolved {"items":{"length":50}} // <-- uh-oh
> dataSource get 1
> get2: resolved {"items":{"2":{"title":"Item C"},"length":50}}
*/ The reason why is we try to avoid allocations during The recycleJSON version works because we collapse requested paths into a single path tree (so all paths are represented in the same data structure). This is necessary for computing unique hashcodes, but is more expensive up-front. This isn't a problem in the real world because we can memoize the collapse, but this optimization isn't something the base falcor library has enough information to do internally. In the degenerate case that we're calling I see two possible solutions to the problem of the general case:
For reference, here's the output of the example from this issue in materialized mode: const m = new Model({ source: getDataSource(), materialized: true });
const get1 = m.get(['items', {to: 1}, 'title'], ['items', 'length']).progressively();
const get2 = m.get(['items', 2, 'title'], ['items', 'length']).progressively();
/*
> get1: pending {"items":{"0":{},"1":{}}}
> dataSource get 0
> get1: resolved {"items":{"0":{"title":"Item A"},"1":{"title":"Item B"},"length":50}}
> get2: pending {"items":{"2":{},"length":50}}
> dataSource get 1
> get2: resolved {"items":{"2":{"title":"Item C"},"length":50}}
*/ |
OK, I can confirm that the fix does pass my test case, but does not pass the case outlined above where the first path in the second query is completely missing from the cache. So as I understand it, for paths that don't hit the cache, there is no way to preserve the information that there was a cache miss when the entire query emits (when recycleJSON and materialized are false), yeah? On my end, I could just punt on this fix and start using Anyhow, I don't have a good answer to the above. guaranteed consistent behavior for |
Is the
FalcorJSON.$__status
property supposed to reliably determine whether or not a model query is done emitting?I'm running into the following issue:
model cache is populated with some results, e.g.
[search, 1, { to: 9 }, title]
query model that emits progressively for paths, some of which can resolve in the local cache
resolved
statusIs this a correct usage of
$__status
?The text was updated successfully, but these errors were encountered: