Skip to content

Commit aef5468

Browse files
authored
Abort Firestore listeners on terminate. (#8399)
* Abort onSnapshotListeners on terminate. * Pretty * Fix race condition
1 parent 6bb2e89 commit aef5468

File tree

4 files changed

+56
-10
lines changed

4 files changed

+56
-10
lines changed

packages/firestore/src/core/component_provider.ts

+1
Original file line numberDiff line numberDiff line change
@@ -485,5 +485,6 @@ export class OnlineComponentProvider {
485485
async terminate(): Promise<void> {
486486
await remoteStoreShutdown(this.remoteStore);
487487
this.datastore?.terminate();
488+
this.eventManager?.terminate();
488489
}
489490
}

packages/firestore/src/core/event_manager.ts

+35-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import { debugAssert, debugCast } from '../util/assert';
1919
import { wrapInUserErrorIfRecoverable } from '../util/async_queue';
20-
import { FirestoreError } from '../util/error';
20+
import { Code, FirestoreError } from '../util/error';
2121
import { EventHandler } from '../util/misc';
2222
import { ObjectMap } from '../util/obj_map';
2323

@@ -64,19 +64,17 @@ export interface EventManager {
6464
onUnlisten?: (query: Query, disableRemoteListen: boolean) => Promise<void>;
6565
onFirstRemoteStoreListen?: (query: Query) => Promise<void>;
6666
onLastRemoteStoreUnlisten?: (query: Query) => Promise<void>;
67+
terminate(): void;
6768
}
6869

6970
export function newEventManager(): EventManager {
7071
return new EventManagerImpl();
7172
}
7273

7374
export class EventManagerImpl implements EventManager {
74-
queries = new ObjectMap<Query, QueryListenersInfo>(
75-
q => canonifyQuery(q),
76-
queryEquals
77-
);
75+
queries: ObjectMap<Query, QueryListenersInfo> = newQueriesObjectMap();
7876

79-
onlineState = OnlineState.Unknown;
77+
onlineState: OnlineState = OnlineState.Unknown;
8078

8179
snapshotsInSyncListeners: Set<Observer<void>> = new Set();
8280

@@ -98,6 +96,20 @@ export class EventManagerImpl implements EventManager {
9896
* still listening to the cache.
9997
*/
10098
onLastRemoteStoreUnlisten?: (query: Query) => Promise<void>;
99+
100+
terminate(): void {
101+
errorAllTargets(
102+
this,
103+
new FirestoreError(Code.ABORTED, 'Firestore shutting down')
104+
);
105+
}
106+
}
107+
108+
function newQueriesObjectMap(): ObjectMap<Query, QueryListenersInfo> {
109+
return new ObjectMap<Query, QueryListenersInfo>(
110+
q => canonifyQuery(q),
111+
queryEquals
112+
);
101113
}
102114

103115
function validateEventManager(eventManagerImpl: EventManagerImpl): void {
@@ -334,6 +346,23 @@ export function removeSnapshotsInSyncListener(
334346
eventManagerImpl.snapshotsInSyncListeners.delete(observer);
335347
}
336348

349+
function errorAllTargets(
350+
eventManager: EventManager,
351+
error: FirestoreError
352+
): void {
353+
const eventManagerImpl = debugCast(eventManager, EventManagerImpl);
354+
const queries = eventManagerImpl.queries;
355+
356+
// Prevent further access by clearing ObjectMap.
357+
eventManagerImpl.queries = newQueriesObjectMap();
358+
359+
queries.forEach((_, queryInfo) => {
360+
for (const listener of queryInfo.listeners) {
361+
listener.onError(error);
362+
}
363+
});
364+
}
365+
337366
// Call all global snapshot listeners that have been set.
338367
function raiseSnapshotsInSyncEvent(eventManagerImpl: EventManagerImpl): void {
339368
eventManagerImpl.snapshotsInSyncListeners.forEach(observer => {

packages/firestore/test/integration/api/batch_writes.test.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,11 @@ apiDescribe('Database batch writes', persistence => {
155155
);
156156
return accumulator
157157
.awaitEvent()
158-
.then(initialSnap => {
158+
.then(async initialSnap => {
159159
expect(initialSnap.docs.length).to.equal(0);
160160

161161
// Atomically write two documents.
162-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
163-
writeBatch(db).set(docA, { a: 1 }).set(docB, { b: 2 }).commit();
162+
await writeBatch(db).set(docA, { a: 1 }).set(docB, { b: 2 }).commit();
164163

165164
return accumulator.awaitEvent();
166165
})

packages/firestore/test/integration/api/database.test.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ import {
6363
FieldPath,
6464
newTestFirestore,
6565
SnapshotOptions,
66-
newTestApp
66+
newTestApp,
67+
FirestoreError
6768
} from '../util/firebase_export';
6869
import {
6970
apiDescribe,
@@ -1442,6 +1443,22 @@ apiDescribe('Database', persistence => {
14421443
});
14431444
});
14441445

1446+
it('query listener throws error on termination', async () => {
1447+
return withTestDoc(persistence, async (docRef, firestore) => {
1448+
const deferred: Deferred<FirestoreError> = new Deferred();
1449+
const unsubscribe = onSnapshot(docRef, snapshot => {}, deferred.resolve);
1450+
1451+
await terminate(firestore);
1452+
1453+
await expect(deferred.promise)
1454+
.to.eventually.haveOwnProperty('message')
1455+
.equal('Firestore shutting down');
1456+
1457+
// Call should proceed without error.
1458+
unsubscribe();
1459+
});
1460+
});
1461+
14451462
it('can wait for pending writes', async () => {
14461463
await withTestDoc(persistence, async (docRef, firestore) => {
14471464
// Prevent pending writes receiving acknowledgement.

0 commit comments

Comments
 (0)