Skip to content

Commit 947a325

Browse files
committed
feat: wait on server for data
1 parent 751a635 commit 947a325

File tree

8 files changed

+212
-35
lines changed

8 files changed

+212
-35
lines changed

playground/src/stores/counter.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { ref, computed } from 'vue'
22
import { acceptHMRUpdate, defineStore } from 'pinia'
33
import { doc, setDoc, updateDoc } from 'firebase/firestore'
4-
import { useFirestore } from '@/firebase'
5-
import { useDocument } from 'vuefire'
4+
import { useDocument, useFirestore } from 'vuefire'
65

76
export const useCounterStore = defineStore('counter', () => {
87
const count = ref(0)

src/app/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FirebaseApp, getApp } from 'firebase/app'
2-
import { getCurrentScope, inject, InjectionKey } from 'vue'
2+
import { getCurrentInstance, getCurrentScope, inject, InjectionKey } from 'vue'
33

44
// @internal
55
export const _FirebaseAppInjectionKey: InjectionKey<FirebaseApp> =
@@ -14,7 +14,7 @@ export const _FirebaseAppInjectionKey: InjectionKey<FirebaseApp> =
1414
export function useFirebaseApp(name?: string): FirebaseApp {
1515
// TODO: warn no current scope
1616
return (
17-
(getCurrentScope() &&
17+
(getCurrentInstance() &&
1818
inject(
1919
_FirebaseAppInjectionKey,
2020
// avoid the inject not found warning

src/firestore/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getCurrentScope,
1212
isRef,
1313
onScopeDispose,
14+
onServerPrefetch,
1415
ref,
1516
Ref,
1617
ShallowRef,
@@ -99,6 +100,7 @@ export function _useFirestoreRef(
99100
)
100101
}
101102

103+
// FIXME: force once on server
102104
_unbind = (isDocumentRef(docRefValue) ? bindDocument : bindCollection)(
103105
// @ts-expect-error: cannot type with the ternary
104106
data,
@@ -143,6 +145,9 @@ export function _useFirestoreRef(
143145
// TODO: warn else
144146
if (hasCurrentScope) {
145147
onScopeDispose(unbind)
148+
// wait for the promise during SSR
149+
// TODO: configurable
150+
onServerPrefetch(() => promise.value)
146151
}
147152

148153
// TODO: rename to stop

src/shared.ts

+36
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,42 @@ export function isFirestoreDataReference<T = unknown>(
124124
return isDocumentRef(source) || isCollectionRef(source)
125125
}
126126

127+
// The Firestore SDK has an undocumented _query
128+
// object that has a method to generate a hash for a query,
129+
// which we need for useObservable
130+
// https://github.com/firebase/firebase-js-sdk/blob/5beb23cd47312ffc415d3ce2ae309cc3a3fde39f/packages/firestore/src/core/query.ts#L221
131+
// @internal
132+
export interface _FirestoreQueryWithId<T = DocumentData>
133+
extends FirestoreQuery<T> {
134+
_query: {
135+
canonicalId(): string
136+
}
137+
}
138+
139+
export function isFirestoreQuery(
140+
source: unknown
141+
): source is _FirestoreQueryWithId<unknown> {
142+
return isObject(source) && source.type === 'query'
143+
}
144+
145+
export function getDataSourcePath(
146+
source:
147+
| DocumentReference<unknown>
148+
| FirestoreQuery<unknown>
149+
| CollectionReference<unknown>
150+
| DatabaseQuery
151+
): string | null {
152+
return isFirestoreDataReference(source)
153+
? source.path
154+
: isDatabaseReference(source)
155+
? // gets a path like /users/1?orderByKey=true
156+
source.toString()
157+
: isFirestoreQuery(source)
158+
? // internal id
159+
null // FIXME: find a way to get the canonicalId that no longer exists
160+
: null
161+
}
162+
127163
export function isDatabaseReference(
128164
source: any
129165
): source is DatabaseReference | DatabaseQuery {

src/ssr/plugin.ts

+20-28
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@ import {
55
DocumentReference,
66
Query as FirestoreQuery,
77
} from 'firebase/firestore'
8-
import type { App } from 'vue'
98
import { useFirebaseApp, _FirebaseAppInjectionKey } from '../app'
10-
import { isDatabaseReference, isFirestoreDataReference, noop } from '../shared'
9+
import { getDataSourcePath, noop } from '../shared'
1110

12-
export function VueFireSSR(app: App, firebaseApp: FirebaseApp) {
13-
app.provide(_FirebaseAppInjectionKey, firebaseApp)
14-
}
15-
16-
const appPendingPromises = new WeakMap<
11+
export const appPendingPromises = new WeakMap<
1712
FirebaseApp,
1813
Map<string, Promise<unknown>>
1914
>()
2015

16+
export function clearPendingPromises(app: FirebaseApp) {
17+
appPendingPromises.delete(app)
18+
}
19+
2120
export function addPendingPromise(
2221
promise: Promise<unknown>,
2322
// TODO: should this just be ssrKey? and let functions infer the path?
@@ -38,43 +37,36 @@ export function addPendingPromise(
3837
if (ssrKey) {
3938
pendingPromises.set(ssrKey, promise)
4039
} else {
41-
// TODO: warn if in SSR context
42-
// throw new Error('Could not get the path of the data source')
40+
// TODO: warn if in SSR context in other contexts than vite
41+
if (process.env.NODE_ENV !== 'production' /* && import.meta.env?.SSR */) {
42+
console.warn('[VueFire]: Could not get the path of the data source')
43+
}
4344
}
4445

4546
return ssrKey ? () => pendingPromises.delete(ssrKey!) : noop
4647
}
4748

48-
function getDataSourcePath(
49-
source:
50-
| DocumentReference<unknown>
51-
| FirestoreQuery<unknown>
52-
| CollectionReference<unknown>
53-
| DatabaseQuery
54-
): string | null {
55-
return isFirestoreDataReference(source)
56-
? source.path
57-
: isDatabaseReference(source)
58-
? source.toString()
59-
: null
60-
}
61-
6249
/**
6350
* Allows awaiting for all pending data sources. Useful to wait for SSR
6451
*
6552
* @param name - optional name of teh firebase app
6653
* @returns - a Promise that resolves with an array of all the resolved pending promises
6754
*/
68-
export function usePendingPromises(name?: string) {
69-
const app = useFirebaseApp(name)
55+
export function usePendingPromises(app?: FirebaseApp) {
56+
app = app || useFirebaseApp()
7057
const pendingPromises = appPendingPromises.get(app)
71-
return pendingPromises
58+
const p = pendingPromises
7259
? Promise.all(
7360
Array.from(pendingPromises).map(([key, promise]) =>
7461
promise.then((data) => [key, data] as const)
7562
)
7663
)
7764
: Promise.resolve([])
65+
66+
// consume the promises
67+
appPendingPromises.delete(app)
68+
69+
return p
7870
}
7971

8072
export function getInitialData(
@@ -84,13 +76,13 @@ export function getInitialData(
8476
const pendingPromises = appPendingPromises.get(app)
8577

8678
if (!pendingPromises) {
87-
if (__DEV__) {
79+
if (process.env.NODE_ENV !== 'production') {
8880
console.warn('[VueFire]: No initial data found.')
8981
}
9082
return Promise.resolve({})
9183
}
9284

93-
return usePendingPromises(app.name).then((keyData) =>
85+
return usePendingPromises(app).then((keyData) =>
9486
keyData.reduce((initialData, [key, data]) => {
9587
initialData[key] = data
9688
return initialData

tests/firestore/collection.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ describe(
316316
showFinished.value = null
317317

318318
const { data, promise } = factory({
319-
// @ts-expect-error
319+
// @ts-expect-error: this one is a query
320320
ref: listToDisplay,
321321
})
322322
await promise.value

tests/firestore/refs-in-documents.spec.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { mount } from '@vue/test-utils'
22
import { beforeEach, describe, it, expect, afterEach } from 'vitest'
33
import {
4-
CollectionReference,
54
doc as originalDoc,
65
DocumentData,
76
DocumentReference,
@@ -11,7 +10,6 @@ import { unref } from 'vue'
1110
import { _InferReferenceType, _RefFirestore } from '../../src/firestore'
1211
import {
1312
UseDocumentOptions,
14-
usePendingPromises,
1513
VueFirestoreQueryData,
1614
useDocument,
1715
} from '../../src'

tests/firestore/ssr.spec.ts

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { mount } from '@vue/test-utils'
5+
import { beforeEach, describe, it, expect, afterEach } from 'vitest'
6+
import {
7+
CollectionReference,
8+
doc as originalDoc,
9+
DocumentData,
10+
DocumentReference,
11+
} from 'firebase/firestore'
12+
import { setupFirestoreRefs, sleep, firebaseApp } from '../utils'
13+
import { onServerPrefetch, ShallowUnwrapRef, unref } from 'vue'
14+
import { _InferReferenceType, _RefFirestore } from '../../src/firestore'
15+
import {
16+
UseDocumentOptions,
17+
UseCollectionOptions,
18+
usePendingPromises,
19+
VueFirestoreQueryData,
20+
useDocument,
21+
useCollection,
22+
} from '../../src'
23+
import { _MaybeRef, _Nullable } from '../../src/shared'
24+
import { Component, createSSRApp, inject, ref, computed, customRef } from 'vue'
25+
import { renderToString, ssrInterpolate } from '@vue/server-renderer'
26+
import { clearPendingPromises, getInitialData } from '../../src/ssr/plugin'
27+
28+
describe('Firestore refs in documents', async () => {
29+
const { collection, query, addDoc, setDoc, updateDoc, deleteDoc, doc } =
30+
setupFirestoreRefs()
31+
32+
beforeEach(() => {
33+
clearPendingPromises(firebaseApp)
34+
})
35+
36+
function createMyApp<T>(
37+
setup: () => T,
38+
render: (ctx: ShallowUnwrapRef<Awaited<T>>) => unknown
39+
) {
40+
const App = {
41+
ssrRender(ctx: any, push: any, _parent: any) {
42+
push(`<p>${ssrInterpolate(render(ctx))}</p>`)
43+
},
44+
setup,
45+
}
46+
47+
const app = createSSRApp(App)
48+
49+
return { app }
50+
}
51+
52+
function factoryCollection<T = DocumentData>({
53+
options,
54+
ref = collection(),
55+
}: {
56+
options?: UseCollectionOptions
57+
ref?: _MaybeRef<_Nullable<CollectionReference<T>>>
58+
} = {}) {
59+
let data!: _RefFirestore<VueFirestoreQueryData<T>>
60+
61+
const wrapper = mount({
62+
template: 'no',
63+
setup() {
64+
// @ts-expect-error: generic forced
65+
data =
66+
// split for ts
67+
useCollection(ref, options)
68+
const { data: list, pending, error, promise, unbind } = data
69+
return { list, pending, error, promise, unbind }
70+
},
71+
})
72+
73+
return {
74+
wrapper,
75+
// to simplify types
76+
listRef: unref(ref)!,
77+
// non enumerable properties cannot be spread
78+
data: data.data,
79+
pending: data.pending,
80+
error: data.error,
81+
promise: data.promise,
82+
unbind: data.unbind,
83+
}
84+
}
85+
86+
function factoryDoc<T = DocumentData>({
87+
options,
88+
ref,
89+
}: {
90+
options?: UseDocumentOptions
91+
ref?: _MaybeRef<DocumentReference<T>>
92+
} = {}) {
93+
let data!: _RefFirestore<VueFirestoreQueryData<T>>
94+
95+
const wrapper = mount({
96+
template: 'no',
97+
setup() {
98+
// @ts-expect-error: generic forced
99+
data =
100+
// split for ts
101+
useDocument(ref, options)
102+
const { data: list, pending, error, promise, unbind } = data
103+
return { list, pending, error, promise, unbind }
104+
},
105+
})
106+
107+
return {
108+
wrapper,
109+
listRef: unref(ref),
110+
// non enumerable properties cannot be spread
111+
data: data.data,
112+
pending: data.pending,
113+
error: data.error,
114+
promise: data.promise,
115+
unbind: data.unbind,
116+
}
117+
}
118+
119+
it('can await within setup', async () => {
120+
const docRef = doc<{ name: string }>()
121+
await setDoc(docRef, { name: 'a' })
122+
const { app } = createMyApp(
123+
async () => {
124+
const { data, promise } = useDocument(docRef)
125+
await promise.value
126+
return { data }
127+
},
128+
({ data }) => data.name
129+
)
130+
131+
expect(await renderToString(app)).toBe(`<p>a</p>`)
132+
})
133+
134+
it('can await outside of setup', async () => {
135+
const docRef = doc<{ name: string }>()
136+
await setDoc(docRef, { name: 'hello' })
137+
const { app } = createMyApp(
138+
() => {
139+
const data = useDocument(docRef)
140+
return { data }
141+
},
142+
({ data }) => data.name
143+
)
144+
145+
expect(await renderToString(app)).toBe(`<p>hello</p>`)
146+
})
147+
})

0 commit comments

Comments
 (0)