@tanstack/react-db 0.1.95 → 0.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/cjs/useLiveInfiniteQuery.cjs +80 -108
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +1 -0
- package/dist/cjs/useLiveQuery.cjs +10 -86
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/esm/useLiveInfiniteQuery.d.ts +1 -0
- package/dist/esm/useLiveInfiniteQuery.js +82 -110
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.js +11 -87
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/useLiveInfiniteQuery.ts +147 -171
- package/src/useLiveQuery.ts +31 -138
|
@@ -1,23 +1,39 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
CollectionImpl,
|
|
4
|
+
createLiveQueryCollection,
|
|
5
|
+
createLiveQueryWindowController,
|
|
6
|
+
deepEquals,
|
|
7
|
+
} from '@tanstack/db'
|
|
8
|
+
// Type-only: used in `ReturnType<typeof useLiveQuery>` in UseLiveInfiniteQueryReturn.
|
|
9
|
+
import type { useLiveQuery } from './useLiveQuery'
|
|
4
10
|
import type {
|
|
5
11
|
Collection,
|
|
6
12
|
Context,
|
|
7
13
|
InferResultType,
|
|
8
14
|
InitialQueryBuilder,
|
|
9
|
-
|
|
15
|
+
LiveQueryWindowController,
|
|
10
16
|
NonSingleResult,
|
|
11
17
|
QueryBuilder,
|
|
12
18
|
} from '@tanstack/db'
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
utils:
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// Live queries created here are cleaned up immediately (0 disables GC).
|
|
21
|
+
const DEFAULT_GC_TIME_MS = 1
|
|
22
|
+
|
|
23
|
+
type WindowedCollection = Collection<any, any, any> & {
|
|
24
|
+
utils: {
|
|
25
|
+
setWindow: (options: {
|
|
26
|
+
offset: number
|
|
27
|
+
limit: number
|
|
28
|
+
}) => true | Promise<void>
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Type guard: does this collection expose `setWindow` (i.e. has an orderBy)? */
|
|
33
|
+
function hasSetWindow(
|
|
34
|
+
collection: Collection<any, any, any>,
|
|
35
|
+
): collection is WindowedCollection {
|
|
36
|
+
return typeof collection.utils?.setWindow === `function`
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
|
|
@@ -46,8 +62,13 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<
|
|
|
46
62
|
fetchNextPage: () => void
|
|
47
63
|
hasNextPage: boolean
|
|
48
64
|
isFetchingNextPage: boolean
|
|
65
|
+
error: unknown
|
|
49
66
|
}
|
|
50
67
|
|
|
68
|
+
type EnabledLiveQueryReturn<TContext extends Context> = ReturnType<
|
|
69
|
+
typeof useLiveQuery<TContext>
|
|
70
|
+
>
|
|
71
|
+
|
|
51
72
|
/**
|
|
52
73
|
* Create an infinite query using a query function with live updates
|
|
53
74
|
*
|
|
@@ -151,180 +172,135 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
151
172
|
)
|
|
152
173
|
}
|
|
153
174
|
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
const collectionRef = useRef<Collection<any, any, any> | null>(null)
|
|
176
|
+
const controllerRef = useRef<LiveQueryWindowController<any, any> | null>(null)
|
|
177
|
+
const configRef = useRef<unknown>(null)
|
|
178
|
+
const depsRef = useRef<Array<unknown> | null>(null)
|
|
179
|
+
const pageSizeRef = useRef(pageSize)
|
|
180
|
+
const initialPageParamRef = useRef(initialPageParam)
|
|
181
|
+
const validatedCollectionRef = useRef<unknown>(null)
|
|
182
|
+
const inputKind = isCollection ? `collection` : `query`
|
|
183
|
+
const inputKindRef = useRef<typeof inputKind | null>(null)
|
|
184
|
+
const previousInputKind = inputKindRef.current
|
|
185
|
+
|
|
186
|
+
const dependenciesChanged =
|
|
187
|
+
!isCollection &&
|
|
188
|
+
(depsRef.current === null ||
|
|
189
|
+
depsRef.current.length !== deps.length ||
|
|
190
|
+
depsRef.current.some((dep, index) => dep !== deps[index]))
|
|
191
|
+
const dependenciesStructurallyEqual =
|
|
192
|
+
!isCollection &&
|
|
193
|
+
depsRef.current !== null &&
|
|
194
|
+
deepEquals(depsRef.current, deps)
|
|
195
|
+
const needsNewCollection =
|
|
196
|
+
!collectionRef.current ||
|
|
197
|
+
inputKindRef.current !== inputKind ||
|
|
198
|
+
(isCollection && configRef.current !== queryFnOrCollection) ||
|
|
199
|
+
dependenciesChanged
|
|
200
|
+
const pageShapeChanged =
|
|
201
|
+
pageSizeRef.current !== pageSize ||
|
|
202
|
+
initialPageParamRef.current !== initialPageParam
|
|
203
|
+
const needsNewController =
|
|
204
|
+
!controllerRef.current || needsNewCollection || pageShapeChanged
|
|
205
|
+
|
|
206
|
+
if (needsNewCollection) {
|
|
207
|
+
inputKindRef.current = inputKind
|
|
178
208
|
if (isCollection) {
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
collectionRef.current = queryFnOrCollection
|
|
182
|
-
hasValidatedCollectionRef.current = false
|
|
183
|
-
shouldReset = true
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
// Reset if deps changed (for query functions)
|
|
187
|
-
if (prevDepsKeyRef.current !== depsKey) {
|
|
188
|
-
prevDepsKeyRef.current = depsKey
|
|
189
|
-
shouldReset = true
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (shouldReset) {
|
|
194
|
-
setLoadedPageCount(1)
|
|
195
|
-
}
|
|
196
|
-
}, [isCollection, queryFnOrCollection, depsKey])
|
|
197
|
-
|
|
198
|
-
// Create a live query with initial limit and offset
|
|
199
|
-
// Either pass collection directly or wrap query function
|
|
200
|
-
// Use pageSize + 1 for peek-ahead detection (to know if there are more pages)
|
|
201
|
-
const queryResult = isCollection
|
|
202
|
-
? useLiveQuery(queryFnOrCollection)
|
|
203
|
-
: useLiveQuery(
|
|
204
|
-
(q) =>
|
|
205
|
-
queryFnOrCollection(q)
|
|
206
|
-
.limit(pageSize + 1)
|
|
207
|
-
.offset(0),
|
|
208
|
-
deps,
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
// Adjust window when pagination changes
|
|
212
|
-
useEffect(() => {
|
|
213
|
-
const utils = queryResult.collection.utils
|
|
214
|
-
const expectedOffset = 0
|
|
215
|
-
const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead
|
|
216
|
-
|
|
217
|
-
// Check if collection has orderBy (required for setWindow)
|
|
218
|
-
if (!isLiveQueryCollectionUtils(utils)) {
|
|
219
|
-
// For pre-created collections, throw an error if no orderBy
|
|
220
|
-
if (isCollection) {
|
|
209
|
+
const collection = queryFnOrCollection as Collection<any, any, any>
|
|
210
|
+
if (!hasSetWindow(collection)) {
|
|
221
211
|
throw new Error(
|
|
222
212
|
`useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +
|
|
223
213
|
`Please add .orderBy() to your createLiveQueryCollection query.`,
|
|
224
214
|
)
|
|
225
215
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
216
|
+
// Warn once per collection instance if its current window doesn't match
|
|
217
|
+
// the first page the hook is about to enforce.
|
|
218
|
+
if (validatedCollectionRef.current !== collection) {
|
|
219
|
+
validatedCollectionRef.current = collection
|
|
220
|
+
const currentWindow = collection.utils.getWindow?.()
|
|
221
|
+
if (
|
|
222
|
+
currentWindow &&
|
|
223
|
+
(currentWindow.offset !== 0 || currentWindow.limit !== pageSize + 1)
|
|
224
|
+
) {
|
|
225
|
+
console.warn(
|
|
226
|
+
`useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +
|
|
227
|
+
`but the hook expects {offset: 0, limit: ${pageSize + 1}}. Adjusting window now.`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
241
230
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// For query functions, wait until collection is ready
|
|
246
|
-
if (!isCollection && !queryResult.isReady) return
|
|
247
|
-
|
|
248
|
-
// Adjust the window
|
|
249
|
-
let cancelled = false
|
|
250
|
-
const result = utils.setWindow({
|
|
251
|
-
offset: expectedOffset,
|
|
252
|
-
limit: expectedLimit,
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
if (result !== true) {
|
|
256
|
-
setIsFetchingNextPage(true)
|
|
257
|
-
result
|
|
258
|
-
.catch((error: unknown) => {
|
|
259
|
-
if (!cancelled)
|
|
260
|
-
console.error(`useLiveInfiniteQuery: setWindow failed:`, error)
|
|
261
|
-
})
|
|
262
|
-
.finally(() => {
|
|
263
|
-
if (!cancelled) setIsFetchingNextPage(false)
|
|
264
|
-
})
|
|
231
|
+
collectionRef.current = collection
|
|
232
|
+
configRef.current = queryFnOrCollection
|
|
265
233
|
} else {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// Split the data array into pages and determine if there's a next page
|
|
281
|
-
const { pages, pageParams, hasNextPage, flatData } = useMemo(() => {
|
|
282
|
-
const dataArray = (
|
|
283
|
-
Array.isArray(queryResult.data) ? queryResult.data : []
|
|
284
|
-
) as InferResultType<TContext>
|
|
285
|
-
const totalItemsRequested = loadedPageCount * pageSize
|
|
286
|
-
|
|
287
|
-
// Check if we have more data than requested (the peek ahead item)
|
|
288
|
-
const hasMore = dataArray.length > totalItemsRequested
|
|
289
|
-
|
|
290
|
-
// Build pages array (without the peek ahead item)
|
|
291
|
-
const pagesResult: Array<Array<InferResultType<TContext>[number]>> = []
|
|
292
|
-
const pageParamsResult: Array<number> = []
|
|
293
|
-
|
|
294
|
-
for (let i = 0; i < loadedPageCount; i++) {
|
|
295
|
-
const pageData = dataArray.slice(i * pageSize, (i + 1) * pageSize)
|
|
296
|
-
pagesResult.push(pageData)
|
|
297
|
-
pageParamsResult.push(initialPageParam + i)
|
|
234
|
+
// Wrap the query with the first page's peek-ahead window; the controller
|
|
235
|
+
// grows the limit from here via setWindow.
|
|
236
|
+
collectionRef.current = createLiveQueryCollection({
|
|
237
|
+
query: (q: InitialQueryBuilder) =>
|
|
238
|
+
queryFnOrCollection(q)
|
|
239
|
+
.limit(pageSize + 1)
|
|
240
|
+
.offset(0),
|
|
241
|
+
// Construction happens during render. Synchronization starts only when
|
|
242
|
+
// useSyncExternalStore commits the controller subscription.
|
|
243
|
+
startSync: false,
|
|
244
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
245
|
+
})
|
|
246
|
+
depsRef.current = [...deps]
|
|
298
247
|
}
|
|
248
|
+
}
|
|
299
249
|
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
250
|
+
if (needsNewController) {
|
|
251
|
+
const previousController = controllerRef.current
|
|
252
|
+
const canPreservePageCount =
|
|
253
|
+
previousController !== null &&
|
|
254
|
+
(!needsNewCollection ||
|
|
255
|
+
(previousInputKind === `query` && dependenciesStructurallyEqual))
|
|
256
|
+
const initialPageCount = canPreservePageCount
|
|
257
|
+
? Math.max(1, previousController.getSnapshot().pages.length)
|
|
258
|
+
: 1
|
|
259
|
+
pageSizeRef.current = pageSize
|
|
260
|
+
initialPageParamRef.current = initialPageParam
|
|
261
|
+
controllerRef.current = createLiveQueryWindowController(
|
|
262
|
+
collectionRef.current,
|
|
263
|
+
{
|
|
264
|
+
pageSize,
|
|
265
|
+
initialPageParam,
|
|
266
|
+
initialPageCount,
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
const controller = controllerRef.current!
|
|
305
271
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}, [queryResult.data, loadedPageCount, pageSize, initialPageParam])
|
|
272
|
+
const subscribe = useCallback(
|
|
273
|
+
(onStoreChange: () => void) => controller.subscribe(onStoreChange),
|
|
274
|
+
[controller],
|
|
275
|
+
)
|
|
276
|
+
const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])
|
|
277
|
+
const snapshot = useSyncExternalStore(subscribe, getSnapshot)
|
|
313
278
|
|
|
314
|
-
// Fetch next page
|
|
315
279
|
const fetchNextPage = useCallback(() => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
280
|
+
void controller.fetchNextPage().catch(() => {
|
|
281
|
+
// Pagination errors are exposed through the controller snapshot. The
|
|
282
|
+
// hook's void callback has no promise error channel, so consume it here.
|
|
283
|
+
})
|
|
284
|
+
}, [controller])
|
|
320
285
|
|
|
321
286
|
return {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
287
|
+
data: snapshot.data as InferResultType<TContext>,
|
|
288
|
+
state: snapshot.state as EnabledLiveQueryReturn<TContext>[`state`],
|
|
289
|
+
status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],
|
|
290
|
+
isLoading: snapshot.isLoading,
|
|
291
|
+
isReady: snapshot.isReady,
|
|
292
|
+
isIdle: snapshot.isIdle,
|
|
293
|
+
isError: snapshot.isError,
|
|
294
|
+
isCleanedUp: snapshot.isCleanedUp,
|
|
295
|
+
collection:
|
|
296
|
+
snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],
|
|
297
|
+
isEnabled:
|
|
298
|
+
snapshot.isEnabled as EnabledLiveQueryReturn<TContext>[`isEnabled`],
|
|
299
|
+
pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,
|
|
300
|
+
pageParams: snapshot.pageParams as Array<number>,
|
|
326
301
|
fetchNextPage,
|
|
327
|
-
hasNextPage,
|
|
328
|
-
isFetchingNextPage,
|
|
329
|
-
|
|
302
|
+
hasNextPage: snapshot.hasNextPage,
|
|
303
|
+
isFetchingNextPage: snapshot.isFetchingNextPage,
|
|
304
|
+
error: snapshot.error,
|
|
305
|
+
}
|
|
330
306
|
}
|
package/src/useLiveQuery.ts
CHANGED
|
@@ -2,9 +2,8 @@ import { useRef, useSyncExternalStore } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
BaseQueryBuilder,
|
|
4
4
|
createLiveQueryCollection,
|
|
5
|
-
|
|
5
|
+
createLiveQueryObserver,
|
|
6
6
|
isCollection,
|
|
7
|
-
isSingleResultCollection,
|
|
8
7
|
} from '@tanstack/db'
|
|
9
8
|
import type {
|
|
10
9
|
Collection,
|
|
@@ -14,6 +13,7 @@ import type {
|
|
|
14
13
|
InferResultType,
|
|
15
14
|
InitialQueryBuilder,
|
|
16
15
|
LiveQueryCollectionConfig,
|
|
16
|
+
LiveQueryObserver,
|
|
17
17
|
NonSingleResult,
|
|
18
18
|
QueryBuilder,
|
|
19
19
|
SingleResult,
|
|
@@ -328,12 +328,10 @@ export function useLiveQuery(
|
|
|
328
328
|
const depsRef = useRef<Array<unknown> | null>(null)
|
|
329
329
|
const configRef = useRef<unknown>(null)
|
|
330
330
|
|
|
331
|
-
//
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
version: number
|
|
336
|
-
} | null>(null)
|
|
331
|
+
// The shared observer owns subscription, the ready-race, and the snapshot.
|
|
332
|
+
const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(
|
|
333
|
+
null,
|
|
334
|
+
)
|
|
337
335
|
|
|
338
336
|
// Check if we need to create/recreate the collection
|
|
339
337
|
const needsNewCollection =
|
|
@@ -413,143 +411,38 @@ export function useLiveQuery(
|
|
|
413
411
|
}
|
|
414
412
|
}
|
|
415
413
|
|
|
416
|
-
//
|
|
414
|
+
// Recreate the observer when the underlying collection changes. The observer
|
|
415
|
+
// is not disposed explicitly here or on unmount: `useSyncExternalStore`
|
|
416
|
+
// unsubscribes it when the subscribe changes or the component unmounts, which
|
|
417
|
+
// detaches the collection subscription; the observer is then GC'd. (An unmount
|
|
418
|
+
// effect that disposed it would misfire under StrictMode/offscreen effect
|
|
419
|
+
// replay, leaving a disposed observer in the ref.)
|
|
417
420
|
if (needsNewCollection) {
|
|
418
|
-
|
|
419
|
-
|
|
421
|
+
// Defer the initial notify: useSyncExternalStore must not be notified
|
|
422
|
+
// synchronously during subscribe.
|
|
423
|
+
// Wholesale mode: React re-reads getSnapshot() on notify, keeps the
|
|
424
|
+
// hook's pre-observer loading policy, and — because wholesale delivers
|
|
425
|
+
// nothing synchronously during subscribe — never notifies
|
|
426
|
+
// useSyncExternalStore inside its own subscribe call.
|
|
427
|
+
observerRef.current = createLiveQueryObserver(collectionRef.current, {
|
|
428
|
+
mode: `wholesale`,
|
|
429
|
+
})
|
|
420
430
|
}
|
|
431
|
+
const observer = observerRef.current!
|
|
421
432
|
|
|
422
|
-
//
|
|
433
|
+
// Stable subscribe bound to the current observer; the observer owns the
|
|
434
|
+
// subscription, ready-race, and disposal.
|
|
423
435
|
const subscribeRef = useRef<
|
|
424
436
|
((onStoreChange: () => void) => () => void) | null
|
|
425
437
|
>(null)
|
|
426
438
|
if (!subscribeRef.current || needsNewCollection) {
|
|
427
|
-
subscribeRef.current = (onStoreChange
|
|
428
|
-
|
|
429
|
-
if (!collectionRef.current) {
|
|
430
|
-
return () => {}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
let unsubscribed = false
|
|
434
|
-
|
|
435
|
-
const subscription = collectionRef.current.subscribeChanges(() => {
|
|
436
|
-
// Drop late notifies that race with unsubscribe.
|
|
437
|
-
if (unsubscribed) return
|
|
438
|
-
// Bump version on any change; getSnapshot will rebuild next time
|
|
439
|
-
versionRef.current += 1
|
|
440
|
-
onStoreChange()
|
|
441
|
-
})
|
|
442
|
-
// Already-ready collections won't emit an initial change. Notify React
|
|
443
|
-
// ourselves, but defer to a microtask — calling onStoreChange synchronously
|
|
444
|
-
// here lands during the render-to-commit window and trips React's
|
|
445
|
-
// "state update on a component that hasn't mounted yet" warning.
|
|
446
|
-
if (collectionRef.current.status === `ready`) {
|
|
447
|
-
queueMicrotask(() => {
|
|
448
|
-
if (unsubscribed) return
|
|
449
|
-
versionRef.current += 1
|
|
450
|
-
onStoreChange()
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
return () => {
|
|
454
|
-
unsubscribed = true
|
|
455
|
-
subscription.unsubscribe()
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Create stable getSnapshot function using ref
|
|
461
|
-
const getSnapshotRef = useRef<
|
|
462
|
-
| (() => {
|
|
463
|
-
collection: Collection<object, string | number, {}> | null
|
|
464
|
-
version: number
|
|
465
|
-
})
|
|
466
|
-
| null
|
|
467
|
-
>(null)
|
|
468
|
-
if (!getSnapshotRef.current || needsNewCollection) {
|
|
469
|
-
getSnapshotRef.current = () => {
|
|
470
|
-
const currentVersion = versionRef.current
|
|
471
|
-
const currentCollection = collectionRef.current
|
|
472
|
-
|
|
473
|
-
// Recreate snapshot object only if version/collection changed
|
|
474
|
-
if (
|
|
475
|
-
!snapshotRef.current ||
|
|
476
|
-
snapshotRef.current.version !== currentVersion ||
|
|
477
|
-
snapshotRef.current.collection !== currentCollection
|
|
478
|
-
) {
|
|
479
|
-
snapshotRef.current = {
|
|
480
|
-
collection: currentCollection,
|
|
481
|
-
version: currentVersion,
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return snapshotRef.current
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Use useSyncExternalStore to subscribe to collection changes
|
|
490
|
-
const snapshot = useSyncExternalStore(
|
|
491
|
-
subscribeRef.current,
|
|
492
|
-
getSnapshotRef.current,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
// Track last snapshot (from useSyncExternalStore) and the returned value separately
|
|
496
|
-
const returnedSnapshotRef = useRef<{
|
|
497
|
-
collection: Collection<object, string | number, {}> | null
|
|
498
|
-
version: number
|
|
499
|
-
} | null>(null)
|
|
500
|
-
// Keep implementation return loose to satisfy overload signatures
|
|
501
|
-
const returnedRef = useRef<any>(null)
|
|
502
|
-
|
|
503
|
-
// Rebuild returned object only when the snapshot changes (version or collection identity)
|
|
504
|
-
if (
|
|
505
|
-
!returnedSnapshotRef.current ||
|
|
506
|
-
returnedSnapshotRef.current.version !== snapshot.version ||
|
|
507
|
-
returnedSnapshotRef.current.collection !== snapshot.collection
|
|
508
|
-
) {
|
|
509
|
-
// Handle null collection case (when callback returns undefined/null)
|
|
510
|
-
if (!snapshot.collection) {
|
|
511
|
-
returnedRef.current = {
|
|
512
|
-
state: undefined,
|
|
513
|
-
data: undefined,
|
|
514
|
-
collection: undefined,
|
|
515
|
-
status: `disabled`,
|
|
516
|
-
isLoading: false,
|
|
517
|
-
isReady: true,
|
|
518
|
-
isIdle: false,
|
|
519
|
-
isError: false,
|
|
520
|
-
isCleanedUp: false,
|
|
521
|
-
isEnabled: false,
|
|
522
|
-
}
|
|
523
|
-
} else {
|
|
524
|
-
// Capture a stable view of entries for this snapshot to avoid tearing
|
|
525
|
-
const entries = Array.from(snapshot.collection.entries())
|
|
526
|
-
const singleResult = isSingleResultCollection(snapshot.collection)
|
|
527
|
-
let stateCache: Map<string | number, unknown> | null = null
|
|
528
|
-
let dataCache: Array<unknown> | null = null
|
|
529
|
-
|
|
530
|
-
returnedRef.current = {
|
|
531
|
-
get state() {
|
|
532
|
-
if (!stateCache) {
|
|
533
|
-
stateCache = new Map(entries)
|
|
534
|
-
}
|
|
535
|
-
return stateCache
|
|
536
|
-
},
|
|
537
|
-
get data() {
|
|
538
|
-
if (!dataCache) {
|
|
539
|
-
dataCache = entries.map(([, value]) => value)
|
|
540
|
-
}
|
|
541
|
-
return singleResult ? dataCache[0] : dataCache
|
|
542
|
-
},
|
|
543
|
-
collection: snapshot.collection,
|
|
544
|
-
status: snapshot.collection.status,
|
|
545
|
-
...getLiveQueryStatusFlags(snapshot.collection.status),
|
|
546
|
-
isEnabled: true,
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Remember the snapshot that produced this returned value
|
|
551
|
-
returnedSnapshotRef.current = snapshot
|
|
439
|
+
subscribeRef.current = (onStoreChange) =>
|
|
440
|
+
observer.subscribe(() => onStoreChange())
|
|
552
441
|
}
|
|
553
442
|
|
|
554
|
-
return
|
|
443
|
+
// The observer returns a stable snapshot per revision, which is the return
|
|
444
|
+
// shape this hook exposes. Keep the return loose to satisfy the overloads.
|
|
445
|
+
return useSyncExternalStore(subscribeRef.current, () =>
|
|
446
|
+
observer.getSnapshot(),
|
|
447
|
+
) as any
|
|
555
448
|
}
|