Skip to content

Commit 318af54

Browse files
Apply range-filters in get when probing active-listener cache (#4408)
* Apply range-filters in get when probing active-listener cache * Fix get() _again_ * cleanup * lint error and test feedback * increase test coverage * Remove unused var * chnageset * fix changeset * Fix misunderstanding of suggested fix. * Update packages/database/test/query.test.ts Co-authored-by: Sebastian Schmidt <[email protected]> * Update packages/database/test/query.test.ts Co-authored-by: Sebastian Schmidt <[email protected]> * add more tests for get * Update .changeset/small-icons-allow.md Co-authored-by: Sebastian Schmidt <[email protected]> * Update packages/database/src/core/view/View.ts Co-authored-by: Sebastian Schmidt <[email protected]> * Update packages/database/src/core/SyncPoint.ts Co-authored-by: Sebastian Schmidt <[email protected]> * Fix get with pending writes case Co-authored-by: Sebastian Schmidt <[email protected]>
1 parent f513922 commit 318af54

File tree

6 files changed

+260
-20
lines changed

6 files changed

+260
-20
lines changed

.changeset/small-icons-allow.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
'@firebase/database': patch
3+
---
4+
Fixed an issue with `Query.get()` where Query filters are not applied to data in some cases.

packages/database/src/core/Repo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,8 @@ export class Repo {
382382
*/
383383
getValue(query: Query): Promise<DataSnapshot> {
384384
// Only active queries are cached. There is no persisted cache.
385-
const cached = this.serverSyncTree_.calcCompleteEventCache(query.path);
386-
if (!cached.isEmpty()) {
385+
const cached = this.serverSyncTree_.getServerValue(query);
386+
if (cached != null) {
387387
return Promise.resolve(
388388
new DataSnapshot(
389389
cached,

packages/database/src/core/SyncPoint.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,22 +93,24 @@ export class SyncPoint {
9393
return events;
9494
}
9595
}
96-
96+
9797
/**
98-
* Add an event callback for the specified query.
98+
* Get a view for the specified query.
9999
*
100-
* @param serverCache Complete server cache, if we have it.
100+
* @param query The query to return a view for
101+
* @param writesCache
102+
* @param serverCache
103+
* @param serverCacheComplete
101104
* @return Events to raise.
102105
*/
103-
addEventRegistration(
106+
getView(
104107
query: Query,
105-
eventRegistration: EventRegistration,
106108
writesCache: WriteTreeRef,
107109
serverCache: Node | null,
108110
serverCacheComplete: boolean
109-
): Event[] {
111+
): View {
110112
const queryId = query.queryIdentifier();
111-
let view = this.views.get(queryId);
113+
const view = this.views.get(queryId);
112114
if (!view) {
113115
// TODO: make writesCache take flag for complete server node
114116
let eventCache = writesCache.calcCompleteEventCache(
@@ -128,10 +130,37 @@ export class SyncPoint {
128130
new CacheNode(eventCache, eventCacheComplete, false),
129131
new CacheNode(serverCache, serverCacheComplete, false)
130132
);
131-
view = new View(query, viewCache);
132-
this.views.set(queryId, view);
133+
return new View(query, viewCache);
133134
}
135+
return view;
136+
}
134137

138+
/**
139+
* Add an event callback for the specified query.
140+
*
141+
* @param query
142+
* @param eventRegistration
143+
* @param writesCache
144+
* @param serverCache Complete server cache, if we have it.
145+
* @param serverCacheComplete
146+
* @return Events to raise.
147+
*/
148+
addEventRegistration(
149+
query: Query,
150+
eventRegistration: EventRegistration,
151+
writesCache: WriteTreeRef,
152+
serverCache: Node | null,
153+
serverCacheComplete: boolean
154+
): Event[] {
155+
const view = this.getView(
156+
query,
157+
writesCache,
158+
serverCache,
159+
serverCacheComplete
160+
);
161+
if (!this.views.has(query.queryIdentifier())) {
162+
this.views.set(query.queryIdentifier(), view);
163+
}
135164
// This is guaranteed to exist now, we just created anything that was missing
136165
view.addEventRegistration(eventRegistration);
137166
return view.getInitialEvents(eventRegistration);

packages/database/src/core/SyncTree.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Node } from './snap/Node';
3232
import { Event } from './view/Event';
3333
import { EventRegistration } from './view/EventRegistration';
3434
import { View } from './view/View';
35+
import { CacheNode } from './view/CacheNode';
3536

3637
/**
3738
* @typedef {{
@@ -503,6 +504,38 @@ export class SyncTree {
503504
);
504505
}
505506

507+
getServerValue(query: Query): Node | null {
508+
const path = query.path;
509+
let serverCache: Node | null = null;
510+
// Any covering writes will necessarily be at the root, so really all we need to find is the server cache.
511+
// Consider optimizing this once there's a better understanding of what actual behavior will be.
512+
this.syncPointTree_.foreachOnPath(path, (pathToSyncPoint, sp) => {
513+
const relativePath = Path.relativePath(pathToSyncPoint, path);
514+
serverCache = serverCache || sp.getCompleteServerCache(relativePath);
515+
});
516+
let syncPoint = this.syncPointTree_.get(path);
517+
if (!syncPoint) {
518+
syncPoint = new SyncPoint();
519+
this.syncPointTree_ = this.syncPointTree_.set(path, syncPoint);
520+
} else {
521+
serverCache = serverCache || syncPoint.getCompleteServerCache(Path.Empty);
522+
}
523+
const serverCacheComplete = serverCache != null;
524+
const serverCacheNode: CacheNode | null = serverCacheComplete
525+
? new CacheNode(serverCache, true, false)
526+
: null;
527+
const writesCache: WriteTreeRef | null = this.pendingWriteTree_.childWrites(
528+
query.path
529+
);
530+
const view: View = syncPoint.getView(
531+
query,
532+
writesCache,
533+
serverCacheComplete ? serverCacheNode.getNode() : ChildrenNode.EMPTY_NODE,
534+
serverCacheComplete
535+
);
536+
return view.getCompleteNode();
537+
}
538+
506539
/**
507540
* This collapses multiple unfiltered views into a single view, since we only need a single
508541
* listener for them.

packages/database/src/core/view/View.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ export class View {
9292
return this.viewCache_.getServerCache().getNode();
9393
}
9494

95+
getCompleteNode(): Node | null {
96+
return this.viewCache_.getCompleteEventSnap();
97+
}
98+
9599
getCompleteServerCache(path: Path): Node | null {
96100
const cache = this.viewCache_.getCompleteServerSnap();
97101
if (cache) {

packages/database/test/query.test.ts

Lines changed: 179 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,40 @@ describe('Query Tests', () => {
13991399
expect(Object.values(snap.val())).to.deep.equal([snap.val()[childOne.key]]);
14001400
});
14011401

1402+
it('Ensure startAfter on key index works with overlapping listener', async () => {
1403+
const node = getRandomNode() as Reference;
1404+
const childOne = node.push();
1405+
const childTwo = node.push();
1406+
// Create a server synced and a latency-compensated write
1407+
await childOne.set(1);
1408+
childTwo.set(2);
1409+
const ea = EventAccumulatorFactory.waitsForCount(1);
1410+
node.on('value', snap => {
1411+
ea.addEvent(snap.val());
1412+
});
1413+
await ea.promise;
1414+
const snap = await node.orderByKey().startAfter(childOne.key).get();
1415+
expect(Object.keys(snap.val())).to.deep.equal([childTwo.key]);
1416+
expect(Object.values(snap.val())).to.deep.equal([snap.val()[childTwo.key]]);
1417+
});
1418+
1419+
it('Ensure endBefore on key index works with overlapping listener', async () => {
1420+
const node = getRandomNode() as Reference;
1421+
const childOne = node.push();
1422+
const childTwo = node.push();
1423+
// Create a server synced and a latency-compensated write
1424+
await childOne.set(1);
1425+
childTwo.set(2);
1426+
const ea = EventAccumulatorFactory.waitsForCount(1);
1427+
node.on('value', snap => {
1428+
ea.addEvent(snap.val());
1429+
});
1430+
await ea.promise;
1431+
const snap = await node.orderByKey().endBefore(childTwo.key).get();
1432+
expect(Object.keys(snap.val())).to.deep.equal([childOne.key]);
1433+
expect(Object.values(snap.val())).to.deep.equal([snap.val()[childOne.key]]);
1434+
});
1435+
14021436
it('Ensure startAt / endAt with priority works.', async () => {
14031437
const node = getRandomNode() as Reference;
14041438

@@ -3120,14 +3154,28 @@ describe('Query Tests', () => {
31203154
expect((await node.get()).val()).to.equal(null);
31213155
});
31223156

3123-
it('get at non-empty root returns correct value', async () => {
3157+
it('get at node returns correct value', async () => {
31243158
const node = getRandomNode() as Reference;
31253159
const expected = { foo: 'a', bar: 'b' };
31263160
await node.set(expected);
31273161
const snapshot = await node.get();
31283162
expect(snapshot.val()).to.deep.equal(expected);
31293163
});
31303164

3165+
it('get for child returns correct value', async () => {
3166+
const node = getRandomNode() as Reference;
3167+
await node.set({ foo: 'a', bar: 'b', baz: 'c' });
3168+
const snapshot = await node.child('baz').get();
3169+
expect(snapshot.val()).to.deep.equal('c');
3170+
});
3171+
3172+
it('get for parent returns correct value', async () => {
3173+
const node = getRandomNode() as Reference;
3174+
const child = node.child('child');
3175+
await child.set(1);
3176+
expect((await node.get()).val()).to.deep.equal({ child: 1 });
3177+
});
3178+
31313179
it('get for removed node returns correct value', async () => {
31323180
const node = getRandomNode() as Reference;
31333181
const expected = { foo: 'a', bar: 'b' };
@@ -3140,7 +3188,7 @@ describe('Query Tests', () => {
31403188
expect(snapshot.val()).to.be.null;
31413189
});
31423190

3143-
it('get while offline is rejected', async () => {
3191+
it('get for missing node while offline is rejected', async () => {
31443192
const node = getRandomNode() as Reference;
31453193
node.database.goOffline();
31463194
try {
@@ -3150,13 +3198,7 @@ describe('Query Tests', () => {
31503198
}
31513199
});
31523200

3153-
it('get returns the latest value', async () => {
3154-
const node = getRandomNode() as Reference;
3155-
await node.set({ foo: 'bar' });
3156-
expect((await node.get()).val()).to.deep.equal({ foo: 'bar' });
3157-
});
3158-
3159-
it('get reads from cache if database is not connected', async () => {
3201+
it('get reads node from cache when not connected', async () => {
31603202
const node = getRandomNode() as Reference;
31613203
const node2 = getFreshRepo(node.path);
31623204
try {
@@ -3178,6 +3220,134 @@ describe('Query Tests', () => {
31783220
}
31793221
});
31803222

3223+
it('get reads child node from cache when not connected', async () => {
3224+
const node = getRandomNode() as Reference;
3225+
const node2 = getFreshRepo(node.path);
3226+
try {
3227+
await node2.set({ foo: 'bar' });
3228+
const onSnapshot = await new Promise((resolve, _) => {
3229+
node.on('value', snap => {
3230+
resolve(snap);
3231+
});
3232+
});
3233+
node.database.goOffline();
3234+
const getSnapshot = await node.child('foo').get();
3235+
// node's cache dropped here.
3236+
node.off();
3237+
expect(getSnapshot.val()).to.deep.equal('bar');
3238+
} finally {
3239+
node.database.goOnline();
3240+
}
3241+
});
3242+
3243+
it('get reads parent node from cache when not connected', async () => {
3244+
const node = getRandomNode() as Reference;
3245+
const node2 = getFreshRepo(node.path);
3246+
try {
3247+
await node2.set({ foo: 'bar' });
3248+
await node2.child('baz').set(1);
3249+
const onSnapshot = await new Promise((resolve, _) => {
3250+
node.on('value', snap => {
3251+
resolve(snap);
3252+
});
3253+
});
3254+
node.database.goOffline();
3255+
const getSnapshot = await node.get();
3256+
// node's cache dropped here.
3257+
node.off();
3258+
expect(getSnapshot.val()).to.deep.equal({ foo: 'bar', baz: 1 });
3259+
} finally {
3260+
node.database.goOnline();
3261+
}
3262+
});
3263+
3264+
it('get with pending node writes when not connected', async () => {
3265+
const node = getRandomNode() as Reference;
3266+
const node2 = getFreshRepo(node.path);
3267+
try {
3268+
await node2.set({ foo: 'bar' });
3269+
const onSnapshot = await new Promise((resolve, _) => {
3270+
node.on('value', snap => {
3271+
resolve(snap);
3272+
});
3273+
});
3274+
node.database.goOffline();
3275+
node.set({ foo: 'baz' });
3276+
const getSnapshot = await node.get();
3277+
// node's cache dropped here.
3278+
node.off();
3279+
expect(getSnapshot.val()).to.deep.equal({ foo: 'baz' });
3280+
} finally {
3281+
node.database.goOnline();
3282+
}
3283+
});
3284+
3285+
it('get with pending child writes when not connected', async () => {
3286+
const node = getRandomNode() as Reference;
3287+
const node2 = getFreshRepo(node.path);
3288+
try {
3289+
await node2.set({ foo: 'bar' });
3290+
const onSnapshot = await new Promise((resolve, _) => {
3291+
node.on('value', snap => {
3292+
resolve(snap);
3293+
});
3294+
});
3295+
node.database.goOffline();
3296+
node.child('baz').set(true);
3297+
const getSnapshot = await node.get();
3298+
// node's cache dropped here.
3299+
node.off();
3300+
expect(getSnapshot.val()).to.deep.equal({ foo: 'bar', baz: true });
3301+
} finally {
3302+
node.database.goOnline();
3303+
}
3304+
});
3305+
3306+
it('get with pending parent writes when not connected', async () => {
3307+
const node = getRandomNode() as Reference;
3308+
const node2 = getFreshRepo(node.path);
3309+
try {
3310+
await node2.set({ foo: 'bar' });
3311+
const onSnapshot = await new Promise((resolve, _) => {
3312+
node.on('value', snap => {
3313+
resolve(snap);
3314+
});
3315+
});
3316+
node.database.goOffline();
3317+
node.set({ foo: 'baz' });
3318+
const getSnapshot = await node.child('foo').get();
3319+
// node's cache dropped here.
3320+
node.off();
3321+
expect(getSnapshot.val()).to.deep.equal('baz');
3322+
} finally {
3323+
node.database.goOnline();
3324+
}
3325+
});
3326+
3327+
it('get with pending writes', async () => {
3328+
const node = getRandomNode() as Reference;
3329+
node.database.goOffline();
3330+
try {
3331+
node.set({ foo: 'bar' });
3332+
const snap = await node.get();
3333+
expect(snap.val()).to.deep.equal({ foo: 'bar' });
3334+
} finally {
3335+
node.database.goOnline();
3336+
}
3337+
});
3338+
3339+
it('get child of pending writes', async () => {
3340+
const node = getRandomNode() as Reference;
3341+
node.database.goOffline();
3342+
try {
3343+
node.set({ foo: 'bar' });
3344+
const snap = await node.child('foo').get();
3345+
expect(snap.val()).to.deep.equal('bar');
3346+
} finally {
3347+
node.database.goOnline();
3348+
}
3349+
});
3350+
31813351
it('get does not cache sibling data', async () => {
31823352
const reader = getRandomNode() as Reference;
31833353
const writer = getFreshRepo(reader.path);

0 commit comments

Comments
 (0)