Skip to content

Commit 094f6a5

Browse files
committed
feat(firestore): fetch once option
1 parent 80879d1 commit 094f6a5

File tree

3 files changed

+199
-95
lines changed

3 files changed

+199
-95
lines changed

src/firestore/subscribe.ts

+167-95
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import type {
1818
DocumentSnapshot,
1919
FirestoreDataConverter,
2020
Query,
21+
QuerySnapshot,
2122
SnapshotListenOptions,
2223
SnapshotOptions,
2324
} from 'firebase/firestore'
25+
import { getDoc, getDocs } from 'firebase/firestore'
2426
import { onSnapshot } from 'firebase/firestore'
2527

2628
/**
@@ -33,6 +35,15 @@ export interface FirestoreRefOptions extends _DataSourceOptions {
3335
*/
3436
maxRefDepth?: number
3537

38+
/**
39+
* Should the data be fetched once rather than subscribing to changes.
40+
* @experimental Still under development
41+
*/
42+
once?: boolean
43+
44+
/**
45+
* @inheritDoc {SnapshotOptions}
46+
*/
3647
snapshotOptions?: SnapshotOptions
3748

3849
/**
@@ -102,7 +113,8 @@ function updateDataFromDocumentSnapshot<T>(
102113
subs: Record<string, FirestoreSubscription>,
103114
ops: OperationsType,
104115
depth: number,
105-
resolve: _ResolveRejectFn
116+
resolve: _ResolveRejectFn,
117+
reject: _ResolveRejectFn
106118
) {
107119
const [data, refs] = extractRefs(
108120
// @ts-expect-error: FIXME: use better types
@@ -112,40 +124,84 @@ function updateDataFromDocumentSnapshot<T>(
112124
subs
113125
)
114126
ops.set(target, path, data)
115-
subscribeToRefs(options, target, path, subs, refs, ops, depth, resolve)
127+
subscribeToRefs(
128+
options,
129+
target,
130+
path,
131+
subs,
132+
refs,
133+
ops,
134+
depth,
135+
resolve,
136+
reject
137+
)
116138
}
117139

118140
interface SubscribeToDocumentParameter {
119141
target: Ref<unknown>
120142
path: string
121143
depth: number
122144
resolve: () => void
145+
reject: _ResolveRejectFn
123146
ops: OperationsType
124147
ref: DocumentReference
125148
}
126149

127150
function subscribeToDocument(
128-
{ ref, target, path, depth, resolve, ops }: SubscribeToDocumentParameter,
151+
{
152+
ref,
153+
target,
154+
path,
155+
depth,
156+
resolve,
157+
reject,
158+
ops,
159+
}: SubscribeToDocumentParameter,
129160
options: _DefaultsFirestoreRefOptions
130161
) {
131162
const subs = Object.create(null)
132-
const unbind = onSnapshot(ref, (snapshot) => {
133-
if (snapshot.exists()) {
134-
updateDataFromDocumentSnapshot(
135-
options,
136-
target,
137-
path,
138-
snapshot,
139-
subs,
140-
ops,
141-
depth,
142-
resolve
143-
)
144-
} else {
145-
ops.set(target, path, null)
146-
resolve()
147-
}
148-
})
163+
let unbind = noop
164+
165+
if (options.once) {
166+
getDoc(ref).then((snapshot) => {
167+
if (snapshot.exists()) {
168+
updateDataFromDocumentSnapshot(
169+
options,
170+
target,
171+
path,
172+
snapshot,
173+
subs,
174+
ops,
175+
depth,
176+
resolve,
177+
reject
178+
)
179+
} else {
180+
ops.set(target, path, null)
181+
resolve()
182+
}
183+
})
184+
// TODO: catch?
185+
} else {
186+
unbind = onSnapshot(ref, (snapshot) => {
187+
if (snapshot.exists()) {
188+
updateDataFromDocumentSnapshot(
189+
options,
190+
target,
191+
path,
192+
snapshot,
193+
subs,
194+
ops,
195+
depth,
196+
resolve,
197+
reject
198+
)
199+
} else {
200+
ops.set(target, path, null)
201+
resolve()
202+
}
203+
})
204+
}
149205

150206
return () => {
151207
unbind()
@@ -164,7 +220,8 @@ function subscribeToRefs(
164220
refs: Record<string, DocumentReference>,
165221
ops: OperationsType,
166222
depth: number,
167-
resolve: _ResolveRejectFn
223+
resolve: _ResolveRejectFn,
224+
reject: _ResolveRejectFn
168225
) {
169226
const refKeys = Object.keys(refs)
170227
const missingKeys = Object.keys(subs).filter(
@@ -210,6 +267,7 @@ function subscribeToRefs(
210267
depth,
211268
ops,
212269
resolve: deepResolve.bind(null, docPath),
270+
reject,
213271
},
214272
options
215273
),
@@ -236,6 +294,7 @@ export function bindCollection<T = unknown>(
236294
let arrayRef = ref(wait ? [] : target[key])
237295
const originalResolve = resolve
238296
let isResolved: boolean
297+
let stopOnSnapshot = noop
239298

240299
// contain ref subscriptions of objects
241300
// arraySubs is a mirror of array
@@ -260,15 +319,20 @@ export function bindCollection<T = unknown>(
260319
refs,
261320
ops,
262321
0,
263-
resolve.bind(null, doc)
322+
resolve.bind(null, doc),
323+
reject
264324
)
265325
},
266326
modified: ({ oldIndex, newIndex, doc }: DocumentChange<T>) => {
267327
const array = unref(arrayRef)
268328
const subs = arraySubs[oldIndex]
269329
const oldData = array[oldIndex]
270-
// @ts-expect-error: FIXME: Better types
271-
const [data, refs] = extractRefs(doc.data(snapshotOptions), oldData, subs)
330+
const [data, refs] = extractRefs(
331+
// @ts-expect-error: FIXME: Better types
332+
doc.data(snapshotOptions),
333+
oldData,
334+
subs
335+
)
272336
// only move things around after extracting refs
273337
// only move things around after extracting refs
274338
arraySubs.splice(newIndex, 0, subs)
@@ -282,7 +346,8 @@ export function bindCollection<T = unknown>(
282346
refs,
283347
ops,
284348
0,
285-
resolve
349+
resolve,
350+
reject
286351
)
287352
},
288353
removed: ({ oldIndex }: DocumentChange<T>) => {
@@ -292,61 +357,63 @@ export function bindCollection<T = unknown>(
292357
},
293358
}
294359

295-
const stopOnSnapshot = onSnapshot(
296-
collection,
297-
(snapshot) => {
298-
// console.log('pending', metadata.hasPendingWrites)
299-
// docs.forEach(d => console.log('doc', d, '\n', 'data', d.data()))
300-
// NOTE: this will only be triggered once and it will be with all the documents
301-
// from the query appearing as added
302-
// (https://firebase.google.com/docs/firestore/query-data/listen#view_changes_between_snapshots)
303-
304-
const docChanges = snapshot.docChanges(snapshotListenOptions)
305-
306-
if (!isResolved && docChanges.length) {
307-
// isResolved is only meant to make sure we do the check only once
308-
isResolved = true
309-
let count = 0
310-
const expectedItems = docChanges.length
311-
const validDocs = Object.create(null)
312-
for (let i = 0; i < expectedItems; i++) {
313-
validDocs[docChanges[i].doc.id] = true
314-
}
360+
function onSnapshotCallback(snapshot: QuerySnapshot<T>) {
361+
// console.log('pending', metadata.hasPendingWrites)
362+
// docs.forEach(d => console.log('doc', d, '\n', 'data', d.data()))
363+
// NOTE: this will only be triggered once and it will be with all the documents
364+
// from the query appearing as added
365+
// (https://firebase.google.com/docs/firestore/query-data/listen#view_changes_between_snapshots)
366+
367+
const docChanges = snapshot.docChanges(snapshotListenOptions)
368+
369+
if (!isResolved && docChanges.length) {
370+
// isResolved is only meant to make sure we do the check only once
371+
isResolved = true
372+
let count = 0
373+
const expectedItems = docChanges.length
374+
const validDocs = Object.create(null)
375+
for (let i = 0; i < expectedItems; i++) {
376+
validDocs[docChanges[i].doc.id] = true
377+
}
315378

316-
resolve = (data) => {
317-
if (data && (data as any).id in validDocs) {
318-
if (++count >= expectedItems) {
319-
// if wait is true, finally set the array
320-
if (options.wait) {
321-
ops.set(target, key, unref(arrayRef))
322-
// use the proxy object
323-
// arrayRef = target.value
324-
}
325-
originalResolve(unref(arrayRef))
326-
// reset resolve to noop
327-
resolve = noop
379+
resolve = (data) => {
380+
if (data && (data as any).id in validDocs) {
381+
if (++count >= expectedItems) {
382+
// if wait is true, finally set the array
383+
if (options.wait) {
384+
ops.set(target, key, unref(arrayRef))
385+
// use the proxy object
386+
// arrayRef = target.value
328387
}
388+
originalResolve(unref(arrayRef))
389+
// reset resolve to noop
390+
resolve = noop
329391
}
330392
}
331393
}
332-
docChanges.forEach((c) => {
333-
change[c.type](c)
334-
})
335-
336-
// resolves when array is empty
337-
// since this can only happen once, there is no need to guard against it
338-
// being called multiple times
339-
if (!docChanges.length) {
340-
if (options.wait) {
341-
ops.set(target, key, unref(arrayRef))
342-
// use the proxy object
343-
// arrayRef = target.value
344-
}
345-
resolve(unref(arrayRef))
394+
}
395+
docChanges.forEach((c) => {
396+
change[c.type](c)
397+
})
398+
399+
// resolves when array is empty
400+
// since this can only happen once, there is no need to guard against it
401+
// being called multiple times
402+
if (!docChanges.length) {
403+
if (options.wait) {
404+
ops.set(target, key, unref(arrayRef))
405+
// use the proxy object
406+
// arrayRef = target.value
346407
}
347-
},
348-
reject
349-
)
408+
resolve(unref(arrayRef))
409+
}
410+
}
411+
412+
if (options.once) {
413+
getDocs(collection).then(onSnapshotCallback).catch(reject)
414+
} else {
415+
stopOnSnapshot = onSnapshot(collection, onSnapshotCallback, reject)
416+
}
350417

351418
return (reset?: FirestoreRefOptions['reset']) => {
352419
stopOnSnapshot()
@@ -378,27 +445,32 @@ export function bindDocument<T>(
378445
// bind here the function so it can be resolved anywhere
379446
// this is specially useful for refs
380447
resolve = callOnceWithArg(resolve, () => walkGet(target, key))
381-
const stopOnSnapshot = onSnapshot(
382-
document,
383-
(snapshot) => {
384-
if (snapshot.exists()) {
385-
updateDataFromDocumentSnapshot(
386-
options,
387-
target,
388-
key,
389-
snapshot,
390-
subs,
391-
ops,
392-
0,
393-
resolve
394-
)
395-
} else {
396-
ops.set(target, key, null)
397-
resolve(null)
398-
}
399-
},
400-
reject
401-
)
448+
let stopOnSnapshot = noop
449+
450+
function onSnapshotCallback(snapshot: DocumentSnapshot<T>) {
451+
if (snapshot.exists()) {
452+
updateDataFromDocumentSnapshot(
453+
options,
454+
target,
455+
key,
456+
snapshot,
457+
subs,
458+
ops,
459+
0,
460+
resolve,
461+
reject
462+
)
463+
} else {
464+
ops.set(target, key, null)
465+
resolve(null)
466+
}
467+
}
468+
469+
if (options.once) {
470+
getDoc(document).then(onSnapshotCallback).catch(reject)
471+
} else {
472+
stopOnSnapshot = onSnapshot(document, onSnapshotCallback, reject)
473+
}
402474

403475
return (reset?: FirestoreRefOptions['reset']) => {
404476
stopOnSnapshot()

tests/firestore/collection.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,22 @@ describe(
155155
expect(wrapper.vm.list).toContainEqual({ name: 'cc' })
156156
})
157157

158+
it('fetches once', async () => {
159+
const listRef = collection<{ name: string }>()
160+
await addDoc(listRef, { name: 'a' })
161+
const { wrapper, promise, data } = factory<{ name: string }>({
162+
ref: listRef,
163+
options: { once: true },
164+
})
165+
166+
await promise.value
167+
168+
expect(wrapper.vm.list).toEqual([{ name: 'a' }])
169+
await addDoc(listRef, { name: 'd' })
170+
expect(wrapper.vm.list).toEqual([{ name: 'a' }])
171+
expect(data.value).toEqual([{ name: 'a' }])
172+
})
173+
158174
it('can add an array with null to the collection', async () => {
159175
const { wrapper, listRef, data } = factory<{
160176
list: Array<number | null>

0 commit comments

Comments
 (0)