@@ -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'
2426import { 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
118140interface 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
127150function 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 ( )
0 commit comments