Skip to content

Commit 998fe38

Browse files
committed
fix(firestore): correctly set pending when hydrating and during racing conditions
1 parent 90bd7f5 commit 998fe38

File tree

6 files changed

+93
-20
lines changed

6 files changed

+93
-20
lines changed

src/database/bind.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
_MaybeRef,
1414
_ResolveRejectFn,
1515
} from '../shared'
16-
import { ref, Ref, unref } from 'vue-demi'
16+
import { Ref, unref } from 'vue-demi'
1717
import {
1818
onValue,
1919
onChildAdded,

src/database/useDatabaseRef.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function _useDatabaseRef(
4141
reference: _MaybeRef<_Nullable<DatabaseReference | Query>>,
4242
localOptions: UseDatabaseRefOptions = {}
4343
): _RefDatabase<unknown> {
44-
let unbind!: UnbindWithReset
44+
let unbind: UnbindWithReset = noop
4545
const options = Object.assign({}, globalDatabaseOptions, localOptions)
4646
const initialSourceValue = unref(reference)
4747
const data = options.target || ref<unknown | null>()
@@ -84,6 +84,7 @@ export function _useDatabaseRef(
8484
const referenceValue = unref(reference)
8585

8686
const newPromise = new Promise<unknown | null>((resolve, reject) => {
87+
unbind(options.reset)
8788
if (!referenceValue) {
8889
unbind = noop
8990
// resolve to avoid an ever pending promise

src/firestore/useFirestoreRef.ts

+28-16
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,17 @@ export function _useFirestoreRef(
8181
}
8282

8383
// set the initial value from SSR even if the ref comes from outside
84-
// TODO: allow passing firebase app name
8584
data.value = getInitialValue(
8685
initialSourceValue,
8786
options.ssrKey,
8887
data.value,
8988
useFirebaseApp()
9089
)
9190

92-
const pending = ref(true)
91+
// if no initial value is found (ssr), we should set pending to true
92+
let shouldStartAsPending = data.value === undefined // no initial value
93+
94+
const pending = ref(false)
9395
const error = ref<FirestoreError>()
9496
// force the type since its value is set right after and undefined isn't possible
9597
const promise = shallowRef() as ShallowRef<Promise<unknown | null>>
@@ -99,7 +101,7 @@ export function _useFirestoreRef(
99101
function bindFirestoreRef() {
100102
let docRefValue = unref(docOrCollectionRef)
101103

102-
const p = new Promise<unknown | null>((resolve, reject) => {
104+
const newPromise = new Promise<unknown | null>((resolve, reject) => {
103105
// stop the previous subscription
104106
unbind(options.reset)
105107
// skip if the ref is null or undefined
@@ -110,6 +112,11 @@ export function _useFirestoreRef(
110112
return resolve(null)
111113
}
112114

115+
pending.value = shouldStartAsPending
116+
// the very first time we bind, if we hydrated the value, we don't set loading to true
117+
// this way we ensure, all subsequent calls to bindDatabaseRef will set pending to true
118+
shouldStartAsPending = true
119+
113120
if (!docRefValue.converter) {
114121
docRefValue = docRefValue.withConverter(
115122
// @ts-expect-error: seems like a ts error
@@ -128,25 +135,30 @@ export function _useFirestoreRef(
128135
options
129136
)
130137
})
131-
132-
promise.value = p
133-
134-
p.catch((reason: FirestoreError) => {
135-
error.value = reason
136-
}).finally(() => {
137-
pending.value = false
138-
})
138+
.catch((reason) => {
139+
if (promise.value === newPromise) {
140+
error.value = reason
141+
}
142+
return Promise.reject(reason) // propagate the error
143+
})
144+
.finally(() => {
145+
// ensure the current promise is still valid
146+
if (promise.value === newPromise) {
147+
pending.value = false
148+
}
149+
})
150+
151+
// we set the promise here to ensure that pending is set right after if the user awaits the promise
152+
promise.value = newPromise
139153
}
140154

141155
let stopWatcher = noop
142156
if (isRef(docOrCollectionRef)) {
143-
stopWatcher = watch(docOrCollectionRef, bindFirestoreRef, {
144-
immediate: true,
145-
})
146-
} else {
147-
bindFirestoreRef()
157+
stopWatcher = watch(docOrCollectionRef, bindFirestoreRef)
148158
}
149159

160+
bindFirestoreRef()
161+
150162
// only add the first promise to the pending ones
151163
if (initialSourceValue) {
152164
removePendingPromise = addPendingPromise(

tests/database/ssr.spec.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import { useDatabaseObject } from '../../src'
88
import { createSSRApp } from 'vue'
99
import { renderToString, ssrInterpolate } from '@vue/server-renderer'
1010
import { clearPendingPromises } from '../../src/ssr/plugin'
11+
import { _initialStatesMap } from '../../src/ssr/initialState'
1112

1213
describe('Database SSR', async () => {
1314
const { databaseRef, set } = setupDatabaseRefs()
1415

1516
beforeEach(() => {
1617
clearPendingPromises(firebaseApp)
18+
// delete any ssr state
19+
_initialStatesMap.delete(firebaseApp)
1720
})
1821

1922
function createMyApp<T>(

tests/firestore/document.spec.ts

+56-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { mount } from '@vue/test-utils'
2-
import { describe, expect, it } from 'vitest'
2+
import { beforeEach, describe, expect, it } from 'vitest'
33
import {
44
addDoc,
55
doc as originalDoc,
66
DocumentData,
77
DocumentReference,
88
FirestoreError,
99
} from 'firebase/firestore'
10-
import { expectType, setupFirestoreRefs, tds, firestore } from '../utils'
10+
import {
11+
expectType,
12+
setupFirestoreRefs,
13+
tds,
14+
firestore,
15+
firebaseApp,
16+
} from '../utils'
1117
import { nextTick, ref, shallowRef, unref, type Ref } from 'vue'
1218
import { isPOJO, _MaybeRef, _Nullable } from '../../src/shared'
1319
import {
@@ -17,6 +23,10 @@ import {
1723
_RefFirestore,
1824
} from '../../src'
1925
import { mockWarn } from '../vitest-mock-warn'
26+
import {
27+
useSSRInitialState,
28+
_initialStatesMap,
29+
} from '../../src/ssr/initialState'
2030

2131
describe(
2232
'Firestore documents',
@@ -57,6 +67,11 @@ describe(
5767
}
5868
}
5969

70+
beforeEach(() => {
71+
// delete any ssr state
72+
_initialStatesMap.delete(firebaseApp)
73+
})
74+
6075
it('binds a document', async () => {
6176
const { wrapper, itemRef, data } = factory()
6277

@@ -118,6 +133,45 @@ describe(
118133
expect(data.value!.id).toBe(itemRef.id)
119134
})
120135

136+
it('sets pending while loading', async () => {
137+
const itemRef = shallowRef(doc('a'))
138+
const { pending, promise } = factory({ ref: itemRef })
139+
140+
expect(pending.value).toBe(true)
141+
await promise.value
142+
expect(pending.value).toBe(false)
143+
144+
// set the target to a new ref so it can be loaded again
145+
itemRef.value = doc('b')
146+
147+
await nextTick() // for the watcher to trigger
148+
expect(pending.value).toBe(true)
149+
await promise.value
150+
expect(pending.value).toBe(false)
151+
})
152+
153+
it('sets pending to false if there is an initial value (ssr)', async () => {
154+
const itemRef = shallowRef(doc())
155+
useSSRInitialState({ f: { a: 1 }, r: {}, s: {}, u: {} }, firebaseApp)
156+
const { pending, promise } = factory({
157+
ref: itemRef,
158+
options: { ssrKey: 'a' },
159+
})
160+
161+
expect(pending.value).toBe(false)
162+
await promise.value
163+
expect(pending.value).toBe(false)
164+
})
165+
166+
it('skips setting pending if the object is an empty ref', async () => {
167+
const itemRef = shallowRef()
168+
const { pending, promise } = factory({ ref: itemRef })
169+
170+
expect(pending.value).toBe(false)
171+
await promise.value
172+
expect(pending.value).toBe(false)
173+
})
174+
121175
it('manually unbinds a document', async () => {
122176
const { itemRef, data, stop: unbind } = factory()
123177

tests/firestore/ssr.spec.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import { useDocument } from '../../src'
88
import { createSSRApp } from 'vue'
99
import { renderToString, ssrInterpolate } from '@vue/server-renderer'
1010
import { clearPendingPromises } from '../../src/ssr/plugin'
11+
import { _initialStatesMap } from '../../src/ssr/initialState'
1112

1213
describe('Firestore SSR', async () => {
1314
const { collection, query, addDoc, setDoc, updateDoc, deleteDoc, doc } =
1415
setupFirestoreRefs()
1516

1617
beforeEach(() => {
1718
clearPendingPromises(firebaseApp)
19+
// delete any ssr state
20+
_initialStatesMap.delete(firebaseApp)
1821
})
1922

2023
function createMyApp<T>(

0 commit comments

Comments
 (0)