Skip to content

Commit a5d9e10

Browse files
authored
Fix filtered get() cache (#6497)
Fix issue where `get()` incorrectly updated non-default query cache. Also fixes #6397
1 parent ac578e9 commit a5d9e10

File tree

7 files changed

+376
-201
lines changed

7 files changed

+376
-201
lines changed

.changeset/smart-crabs-warn.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@firebase/database": patch
3+
---
4+
5+
Fix issue with how get results for filtered queries are added to cache.
6+
Fix issue with events not getting propagated to listeners by get.

packages/database-compat/test/info.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@ describe('.info Tests', function () {
130130

131131
await ea.promise;
132132

133-
expect(typeof offsets[0]).to.equal('number');
134-
expect(offsets[0]).not.to.be.greaterThan(0);
133+
expect(offsets[0]).to.be.a('number');
135134

136135
// Make sure push still works
137136
ref.push();

packages/database/src/api/Reference_impl.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ import { parseRepoInfo } from '../core/util/libs/parser';
3939
import { nextPushId } from '../core/util/NextPushId';
4040
import {
4141
Path,
42-
pathChild,
4342
pathEquals,
4443
pathGetBack,
4544
pathGetFront,
46-
pathIsEmpty,
45+
pathChild,
4746
pathParent,
48-
pathToUrlEncodedString
47+
pathToUrlEncodedString,
48+
pathIsEmpty
4949
} from '../core/util/Path';
5050
import {
5151
fatal,
@@ -246,7 +246,6 @@ function validateLimit(params: QueryParams) {
246246
);
247247
}
248248
}
249-
250249
/**
251250
* @internal
252251
*/
@@ -465,6 +464,7 @@ export class DataSnapshot {
465464
return this._node.val();
466465
}
467466
}
467+
468468
/**
469469
*
470470
* Returns a `Reference` representing the location in the Database
@@ -525,7 +525,6 @@ export function refFromURL(db: Database, url: string): DatabaseReference {
525525

526526
return ref(db, parsedURL.path.toString());
527527
}
528-
529528
/**
530529
* Gets a `Reference` for the location at the specified relative path.
531530
*
@@ -811,15 +810,16 @@ export function update(ref: DatabaseReference, values: object): Promise<void> {
811810
*/
812811
export function get(query: Query): Promise<DataSnapshot> {
813812
query = getModularInstance(query) as QueryImpl;
814-
return repoGetValue(query._repo, query).then(node => {
813+
const callbackContext = new CallbackContext(() => {});
814+
const container = new ValueEventRegistration(callbackContext);
815+
return repoGetValue(query._repo, query, container).then(node => {
815816
return new DataSnapshot(
816817
node,
817818
new ReferenceImpl(query._repo, query._path),
818819
query._queryParams.getIndex()
819820
);
820821
});
821822
}
822-
823823
/**
824824
* Represents registration for 'value' events.
825825
*/

packages/database/src/core/Repo.ts

+52-21
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
stringify
2525
} from '@firebase/util';
2626

27+
import { ValueEventRegistration } from '../api/Reference_impl';
28+
2729
import { AppCheckTokenProvider } from './AppCheckTokenProvider';
2830
import { AuthTokenProvider } from './AuthTokenProvider';
2931
import { PersistentConnection } from './PersistentConnection';
@@ -61,7 +63,7 @@ import {
6163
syncTreeCalcCompleteEventCache,
6264
syncTreeGetServerValue,
6365
syncTreeRemoveEventRegistration,
64-
syncTreeRegisterQuery
66+
syncTreeTagForQuery
6567
} from './SyncTree';
6668
import { Indexable } from './util/misc';
6769
import {
@@ -452,14 +454,18 @@ function repoGetNextWriteId(repo: Repo): number {
452454
* belonging to active listeners. If they are found, such values
453455
* are considered to be the most up-to-date.
454456
*
455-
* If the client is not connected, this method will try to
456-
* establish a connection and request the value for `query`. If
457-
* the client is not able to retrieve the query result, it reports
458-
* an error.
457+
* If the client is not connected, this method will wait until the
458+
* repo has established a connection and then request the value for `query`.
459+
* If the client is not able to retrieve the query result for another reason,
460+
* it reports an error.
459461
*
460462
* @param query - The query to surface a value for.
461463
*/
462-
export function repoGetValue(repo: Repo, query: QueryContext): Promise<Node> {
464+
export function repoGetValue(
465+
repo: Repo,
466+
query: QueryContext,
467+
eventRegistration: ValueEventRegistration
468+
): Promise<Node> {
463469
// Only active queries are cached. There is no persisted cache.
464470
const cached = syncTreeGetServerValue(repo.serverSyncTree_, query);
465471
if (cached != null) {
@@ -470,32 +476,57 @@ export function repoGetValue(repo: Repo, query: QueryContext): Promise<Node> {
470476
const node = nodeFromJSON(payload).withIndex(
471477
query._queryParams.getIndex()
472478
);
473-
// if this is a filtered query, then overwrite at path
479+
/**
480+
* Below we simulate the actions of an `onlyOnce` `onValue()` event where:
481+
* Add an event registration,
482+
* Update data at the path,
483+
* Raise any events,
484+
* Cleanup the SyncTree
485+
*/
486+
syncTreeAddEventRegistration(
487+
repo.serverSyncTree_,
488+
query,
489+
eventRegistration,
490+
true
491+
);
492+
let events: Event[];
474493
if (query._queryParams.loadsAllData()) {
475-
syncTreeApplyServerOverwrite(repo.serverSyncTree_, query._path, node);
494+
events = syncTreeApplyServerOverwrite(
495+
repo.serverSyncTree_,
496+
query._path,
497+
node
498+
);
476499
} else {
477-
// Simulate `syncTreeAddEventRegistration` without events/listener setup.
478-
// We do this (along with the syncTreeRemoveEventRegistration` below) so that
479-
// `repoGetValue` results have the same cache effects as initial listener(s)
480-
// updates.
481-
const tag = syncTreeRegisterQuery(repo.serverSyncTree_, query);
482-
syncTreeApplyTaggedQueryOverwrite(
500+
const tag = syncTreeTagForQuery(repo.serverSyncTree_, query);
501+
events = syncTreeApplyTaggedQueryOverwrite(
483502
repo.serverSyncTree_,
484503
query._path,
485504
node,
486505
tag
487506
);
488-
// Call `syncTreeRemoveEventRegistration` with a null event registration, since there is none.
489-
// Note: The below code essentially unregisters the query and cleans up any views/syncpoints temporarily created above.
490507
}
491-
const cancels = syncTreeRemoveEventRegistration(
508+
/*
509+
* We need to raise events in the scenario where `get()` is called at a parent path, and
510+
* while the `get()` is pending, `onValue` is called at a child location. While get() is waiting
511+
* for the data, `onValue` will register a new event. Then, get() will come back, and update the syncTree
512+
* and its corresponding serverCache, including the child location where `onValue` is called. Then,
513+
* `onValue` will receive the event from the server, but look at the syncTree and see that the data received
514+
* from the server is already at the SyncPoint, and so the `onValue` callback will never get fired.
515+
* Calling `eventQueueRaiseEventsForChangedPath()` is the correct way to propagate the events and
516+
* ensure the corresponding child events will get fired.
517+
*/
518+
eventQueueRaiseEventsForChangedPath(
519+
repo.eventQueue_,
520+
query._path,
521+
events
522+
);
523+
syncTreeRemoveEventRegistration(
492524
repo.serverSyncTree_,
493525
query,
494-
null
526+
eventRegistration,
527+
null,
528+
true
495529
);
496-
if (cancels.length > 0) {
497-
repoLog(repo, 'unexpected cancel events in repoGetValue');
498-
}
499530
return node;
500531
},
501532
err => {

0 commit comments

Comments
 (0)