Skip to content

Commit 0d2b01d

Browse files
Add Snapshot Listeners to firestore-exp (#3317)
1 parent 5b26aed commit 0d2b01d

File tree

7 files changed

+220
-48
lines changed

7 files changed

+220
-48
lines changed

.changeset/seven-crabs-join.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/firestore/exp/index.d.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ export function updateDoc(
362362
): Promise<void>;
363363
export function deleteDoc(reference: DocumentReference<unknown>): Promise<void>;
364364

365+
// TODO(firestoreexp): Update API Proposal to use FirestoreError in these
366+
// callbacks
365367
export function onSnapshot<T>(
366368
reference: DocumentReference<T>,
367369
observer: {
@@ -375,28 +377,28 @@ export function onSnapshot<T>(
375377
options: SnapshotListenOptions,
376378
observer: {
377379
next?: (snapshot: DocumentSnapshot<T>) => void;
378-
error?: (error: Error) => void;
380+
error?: (error: FirestoreError) => void;
379381
complete?: () => void;
380382
}
381383
): () => void;
382384
export function onSnapshot<T>(
383385
reference: DocumentReference<T>,
384386
onNext: (snapshot: DocumentSnapshot<T>) => void,
385-
onError?: (error: Error) => void,
387+
onError?: (error: FirestoreError) => void,
386388
onCompletion?: () => void
387389
): () => void;
388390
export function onSnapshot<T>(
389391
reference: DocumentReference<T>,
390392
options: SnapshotListenOptions,
391393
onNext: (snapshot: DocumentSnapshot<T>) => void,
392-
onError?: (error: Error) => void,
394+
onError?: (error: FirestoreError) => void,
393395
onCompletion?: () => void
394396
): () => void;
395397
export function onSnapshot<T>(
396398
query: Query<T>,
397399
observer: {
398400
next?: (snapshot: QuerySnapshot<T>) => void;
399-
error?: (error: Error) => void;
401+
error?: (error: FirestoreError) => void;
400402
complete?: () => void;
401403
}
402404
): () => void;
@@ -405,28 +407,28 @@ export function onSnapshot<T>(
405407
options: SnapshotListenOptions,
406408
observer: {
407409
next?: (snapshot: QuerySnapshot<T>) => void;
408-
error?: (error: Error) => void;
410+
error?: (error: FirestoreError) => void;
409411
complete?: () => void;
410412
}
411413
): () => void;
412414
export function onSnapshot<T>(
413415
query: Query<T>,
414416
onNext: (snapshot: QuerySnapshot<T>) => void,
415-
onError?: (error: Error) => void,
417+
onError?: (error: FirestoreError) => void,
416418
onCompletion?: () => void
417419
): () => void;
418420
export function onSnapshot<T>(
419421
query: Query<T>,
420422
options: SnapshotListenOptions,
421423
onNext: (snapshot: QuerySnapshot<T>) => void,
422-
onError?: (error: Error) => void,
424+
onError?: (error: FirestoreError) => void,
423425
onCompletion?: () => void
424426
): () => void;
425427
export function onSnapshotsInSync(
426428
firestore: FirebaseFirestore,
427429
observer: {
428430
next?: (value: void) => void;
429-
error?: (error: Error) => void;
431+
error?: (error: FirestoreError) => void;
430432
complete?: () => void;
431433
}
432434
): () => void;

packages/firestore/exp/index.node.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,16 @@ export {
4949

5050
export { runTransaction, Transaction } from '../lite/src/api/transaction';
5151

52-
export { getDoc, getDocFromCache, getDocFromServer } from './src/api/reference';
52+
export {
53+
getDoc,
54+
getDocFromCache,
55+
getDocFromServer,
56+
onSnapshot,
57+
setDoc,
58+
updateDoc,
59+
deleteDoc,
60+
addDoc
61+
} from './src/api/reference';
5362

5463
export {
5564
FieldValue,

packages/firestore/exp/src/api/reference.ts

Lines changed: 170 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ import { debugAssert } from '../../../src/util/assert';
2828
import { cast } from '../../../lite/src/api/util';
2929
import { DocumentSnapshot, QuerySnapshot } from './snapshot';
3030
import {
31+
addDocSnapshotListener,
32+
addQuerySnapshotListener,
3133
applyFirestoreDataConverter,
3234
getDocsViaSnapshotListener,
3335
getDocViaSnapshotListener,
34-
SnapshotMetadata
36+
SnapshotMetadata,
37+
validateHasExplicitOrderByForLimitToLast
3538
} from '../../../src/api/database';
3639
import { ViewSnapshot } from '../../../src/core/view_snapshot';
3740
import {
@@ -44,6 +47,14 @@ import {
4447
import { Document } from '../../../src/model/document';
4548
import { DeleteMutation, Precondition } from '../../../src/model/mutation';
4649
import { FieldPath } from '../../../src/api/field_path';
50+
import {
51+
CompleteFn,
52+
ErrorFn,
53+
isPartialObserver,
54+
NextFn,
55+
PartialObserver,
56+
Unsubscribe
57+
} from '../../../src/api/observer';
4758

4859
export function getDoc<T>(
4960
reference: firestore.DocumentReference<T>
@@ -101,17 +112,14 @@ export function getQuery<T>(
101112
): Promise<QuerySnapshot<T>> {
102113
const internalQuery = cast<Query<T>>(query, Query);
103114
const firestore = cast<Firestore>(query.firestore, Firestore);
115+
116+
validateHasExplicitOrderByForLimitToLast(internalQuery._query);
104117
return firestore._getFirestoreClient().then(async firestoreClient => {
105118
const snapshot = await getDocsViaSnapshotListener(
106119
firestoreClient,
107120
internalQuery._query
108121
);
109-
return new QuerySnapshot(
110-
firestore,
111-
internalQuery,
112-
snapshot,
113-
new SnapshotMetadata(snapshot.hasPendingWrites, snapshot.fromCache)
114-
);
122+
return new QuerySnapshot(firestore, internalQuery, snapshot);
115123
});
116124
}
117125

@@ -124,12 +132,7 @@ export function getQueryFromCache<T>(
124132
const snapshot = await firestoreClient.getDocumentsFromLocalCache(
125133
internalQuery._query
126134
);
127-
return new QuerySnapshot(
128-
firestore,
129-
internalQuery,
130-
snapshot,
131-
new SnapshotMetadata(snapshot.hasPendingWrites, /* fromCache= */ true)
132-
);
135+
return new QuerySnapshot(firestore, internalQuery, snapshot);
133136
});
134137
}
135138

@@ -144,12 +147,7 @@ export function getQueryFromServer<T>(
144147
internalQuery._query,
145148
{ source: 'server' }
146149
);
147-
return new QuerySnapshot(
148-
firestore,
149-
internalQuery,
150-
snapshot,
151-
new SnapshotMetadata(snapshot.hasPendingWrites, snapshot.fromCache)
152-
);
150+
return new QuerySnapshot(firestore, internalQuery, snapshot);
153151
});
154152
}
155153

@@ -280,6 +278,159 @@ export function addDoc<T>(
280278
.then(() => docRef);
281279
}
282280

281+
// TODO(firestorexp): Make sure these overloads are tested via the Firestore
282+
// integration tests
283+
export function onSnapshot<T>(
284+
reference: firestore.DocumentReference<T>,
285+
observer: {
286+
next?: (snapshot: firestore.DocumentSnapshot<T>) => void;
287+
error?: (error: firestore.FirestoreError) => void;
288+
complete?: () => void;
289+
}
290+
): Unsubscribe;
291+
export function onSnapshot<T>(
292+
reference: firestore.DocumentReference<T>,
293+
options: firestore.SnapshotListenOptions,
294+
observer: {
295+
next?: (snapshot: firestore.DocumentSnapshot<T>) => void;
296+
error?: (error: firestore.FirestoreError) => void;
297+
complete?: () => void;
298+
}
299+
): Unsubscribe;
300+
export function onSnapshot<T>(
301+
reference: firestore.DocumentReference<T>,
302+
onNext: (snapshot: firestore.DocumentSnapshot<T>) => void,
303+
onError?: (error: firestore.FirestoreError) => void,
304+
onCompletion?: () => void
305+
): Unsubscribe;
306+
export function onSnapshot<T>(
307+
reference: firestore.DocumentReference<T>,
308+
options: firestore.SnapshotListenOptions,
309+
onNext: (snapshot: firestore.DocumentSnapshot<T>) => void,
310+
onError?: (error: firestore.FirestoreError) => void,
311+
onCompletion?: () => void
312+
): Unsubscribe;
313+
export function onSnapshot<T>(
314+
query: firestore.Query<T>,
315+
observer: {
316+
next?: (snapshot: firestore.QuerySnapshot<T>) => void;
317+
error?: (error: firestore.FirestoreError) => void;
318+
complete?: () => void;
319+
}
320+
): Unsubscribe;
321+
export function onSnapshot<T>(
322+
query: firestore.Query<T>,
323+
options: firestore.SnapshotListenOptions,
324+
observer: {
325+
next?: (snapshot: firestore.QuerySnapshot<T>) => void;
326+
error?: (error: firestore.FirestoreError) => void;
327+
complete?: () => void;
328+
}
329+
): Unsubscribe;
330+
export function onSnapshot<T>(
331+
query: firestore.Query<T>,
332+
onNext: (snapshot: firestore.QuerySnapshot<T>) => void,
333+
onError?: (error: firestore.FirestoreError) => void,
334+
onCompletion?: () => void
335+
): Unsubscribe;
336+
export function onSnapshot<T>(
337+
query: firestore.Query<T>,
338+
options: firestore.SnapshotListenOptions,
339+
onNext: (snapshot: firestore.QuerySnapshot<T>) => void,
340+
onError?: (error: firestore.FirestoreError) => void,
341+
onCompletion?: () => void
342+
): Unsubscribe;
343+
export function onSnapshot<T>(
344+
ref: firestore.Query<T> | firestore.DocumentReference<T>,
345+
...args: unknown[]
346+
): Unsubscribe {
347+
let options: firestore.SnapshotListenOptions = {
348+
includeMetadataChanges: false
349+
};
350+
let currArg = 0;
351+
if (typeof args[currArg] === 'object' && !isPartialObserver(args[currArg])) {
352+
options = args[currArg] as firestore.SnapshotListenOptions;
353+
currArg++;
354+
}
355+
356+
const internalOptions = {
357+
includeMetadataChanges: options.includeMetadataChanges
358+
};
359+
360+
if (isPartialObserver(args[currArg])) {
361+
const userObserver = args[currArg] as PartialObserver<
362+
firestore.QuerySnapshot<T>
363+
>;
364+
args[currArg] = userObserver.next?.bind(userObserver);
365+
args[currArg + 1] = userObserver.error?.bind(userObserver);
366+
args[currArg + 2] = userObserver.complete?.bind(userObserver);
367+
}
368+
369+
let asyncObserver: Promise<Unsubscribe>;
370+
371+
if (ref instanceof DocumentReference) {
372+
const firestore = cast(ref.firestore, Firestore);
373+
374+
const observer: PartialObserver<ViewSnapshot> = {
375+
next: snapshot => {
376+
if (args[currArg]) {
377+
(args[currArg] as NextFn<firestore.DocumentSnapshot<T>>)(
378+
convertToDocSnapshot(firestore, ref, snapshot)
379+
);
380+
}
381+
},
382+
error: args[currArg + 1] as ErrorFn,
383+
complete: args[currArg + 2] as CompleteFn
384+
};
385+
386+
asyncObserver = firestore
387+
._getFirestoreClient()
388+
.then(firestoreClient =>
389+
addDocSnapshotListener(
390+
firestoreClient,
391+
ref._key,
392+
internalOptions,
393+
observer
394+
)
395+
);
396+
} else {
397+
const query = cast<Query<T>>(ref, Query);
398+
const firestore = cast(query, Firestore);
399+
400+
const observer: PartialObserver<ViewSnapshot> = {
401+
next: snapshot => {
402+
if (args[currArg]) {
403+
(args[currArg] as NextFn<firestore.QuerySnapshot<T>>)(
404+
new QuerySnapshot(firestore, query, snapshot)
405+
);
406+
}
407+
},
408+
error: args[currArg + 1] as ErrorFn,
409+
complete: args[currArg + 2] as CompleteFn
410+
};
411+
412+
validateHasExplicitOrderByForLimitToLast(query._query);
413+
414+
asyncObserver = firestore
415+
._getFirestoreClient()
416+
.then(firestoreClient =>
417+
addQuerySnapshotListener(
418+
firestoreClient,
419+
query._query,
420+
internalOptions,
421+
observer
422+
)
423+
);
424+
}
425+
426+
// TODO(firestorexp): Add test that verifies that we don't raise a snapshot if
427+
// unsubscribe is called before `asyncObserver` resolves.
428+
return () => {
429+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
430+
asyncObserver.then(unsubscribe => unsubscribe());
431+
};
432+
}
433+
283434
/**
284435
* Converts a ViewSnapshot that contains the single document specified by `ref`
285436
* to a DocumentSnapshot.

packages/firestore/exp/src/api/snapshot.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,21 @@ export class QueryDocumentSnapshot<T = firestore.DocumentData>
121121

122122
export class QuerySnapshot<T = firestore.DocumentData>
123123
implements firestore.QuerySnapshot<T> {
124+
readonly metadata: SnapshotMetadata;
125+
124126
private _cachedChanges?: Array<firestore.DocumentChange<T>>;
125127
private _cachedChangesIncludeMetadataChanges?: boolean;
126128

127129
constructor(
128130
readonly _firestore: Firestore,
129131
readonly query: Query<T>,
130-
readonly _snapshot: ViewSnapshot,
131-
readonly metadata: SnapshotMetadata
132-
) {}
132+
readonly _snapshot: ViewSnapshot
133+
) {
134+
this.metadata = new SnapshotMetadata(
135+
_snapshot.hasPendingWrites,
136+
_snapshot.fromCache
137+
);
138+
}
133139

134140
get docs(): Array<firestore.QueryDocumentSnapshot<T>> {
135141
const result: Array<firestore.QueryDocumentSnapshot<T>> = [];
@@ -154,7 +160,7 @@ export class QuerySnapshot<T = firestore.DocumentData>
154160
thisArg,
155161
this._convertToDocumentSnapshot(
156162
doc,
157-
this.metadata.fromCache,
163+
this._snapshot.fromCache,
158164
this._snapshot.mutatedKeys.has(doc.key)
159165
)
160166
);

packages/firestore/lite/src/api/reference.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ import { hardAssert } from '../../../src/util/assert';
4848
import { DeleteMutation, Precondition } from '../../../src/model/mutation';
4949
import {
5050
applyFirestoreDataConverter,
51-
BaseQuery
51+
BaseQuery,
52+
validateHasExplicitOrderByForLimitToLast
5253
} from '../../../src/api/database';
5354
import { FieldPath } from './field_path';
5455
import { cast } from './util';
@@ -417,6 +418,7 @@ export function getQuery<T>(
417418
query: firestore.Query<T>
418419
): Promise<firestore.QuerySnapshot<T>> {
419420
const internalQuery = cast<Query<T>>(query, Query);
421+
validateHasExplicitOrderByForLimitToLast(internalQuery._query);
420422
return internalQuery.firestore._getDatastore().then(async datastore => {
421423
const result = await invokeRunQueryRpc(datastore, internalQuery._query);
422424
const docs = result.map(

0 commit comments

Comments
 (0)