@tanstack/react-db 0.2.0 → 0.3.0
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 +12 -0
- package/dist/cjs/DbProvider.cjs +23 -0
- package/dist/cjs/DbProvider.cjs.map +1 -0
- package/dist/cjs/DbProvider.d.cts +9 -0
- package/dist/cjs/HydrationBoundary.cjs +16 -0
- package/dist/cjs/HydrationBoundary.cjs.map +1 -0
- package/dist/cjs/HydrationBoundary.d.cts +7 -0
- package/dist/cjs/index.cjs +6 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +4 -1
- package/dist/cjs/live-query-internals.cjs +19 -0
- package/dist/cjs/live-query-internals.cjs.map +1 -0
- package/dist/cjs/live-query-internals.d.cts +9 -0
- package/dist/cjs/useLiveInfiniteQuery.cjs +76 -8
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +12 -58
- package/dist/cjs/useLiveQuery.cjs +263 -41
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +79 -33
- package/dist/cjs/useLiveSuspenseQuery.cjs +19 -5
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
- package/dist/cjs/useLiveSuspenseQuery.d.cts +22 -19
- package/dist/esm/DbProvider.d.ts +9 -0
- package/dist/esm/DbProvider.js +23 -0
- package/dist/esm/DbProvider.js.map +1 -0
- package/dist/esm/HydrationBoundary.d.ts +7 -0
- package/dist/esm/HydrationBoundary.js +16 -0
- package/dist/esm/HydrationBoundary.js.map +1 -0
- package/dist/esm/index.d.ts +4 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query-internals.d.ts +9 -0
- package/dist/esm/live-query-internals.js +19 -0
- package/dist/esm/live-query-internals.js.map +1 -0
- package/dist/esm/useLiveInfiniteQuery.d.ts +12 -58
- package/dist/esm/useLiveInfiniteQuery.js +76 -8
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.d.ts +79 -33
- package/dist/esm/useLiveQuery.js +265 -43
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.d.ts +22 -19
- package/dist/esm/useLiveSuspenseQuery.js +19 -5
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
- package/package.json +2 -2
- package/skills/react-db/SKILL.md +119 -90
- package/src/DbProvider.tsx +32 -0
- package/src/HydrationBoundary.tsx +25 -0
- package/src/index.ts +8 -1
- package/src/live-query-internals.ts +36 -0
- package/src/useLiveInfiniteQuery.ts +119 -65
- package/src/useLiveQuery.ts +441 -92
- package/src/useLiveSuspenseQuery.ts +56 -29
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DbClient,
|
|
3
|
+
LiveQueryObserver,
|
|
4
|
+
UnhashableQueryIRError,
|
|
5
|
+
} from '@tanstack/db'
|
|
6
|
+
|
|
7
|
+
const liveQueryResultInfo = Symbol(`liveQueryResultInfo`)
|
|
8
|
+
|
|
9
|
+
export type LiveQueryResultInfo = {
|
|
10
|
+
client: DbClient | undefined
|
|
11
|
+
queryHash: string | undefined
|
|
12
|
+
identityError: UnhashableQueryIRError | undefined
|
|
13
|
+
observer: LiveQueryObserver<object, string | number>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ResultWithInfo = {
|
|
17
|
+
[liveQueryResultInfo]?: LiveQueryResultInfo
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setLiveQueryResultInfo(
|
|
21
|
+
result: object,
|
|
22
|
+
info: LiveQueryResultInfo,
|
|
23
|
+
): void {
|
|
24
|
+
Object.defineProperty(result, liveQueryResultInfo, {
|
|
25
|
+
configurable: true,
|
|
26
|
+
value: info,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getLiveQueryResultInfo(result: object): LiveQueryResultInfo {
|
|
31
|
+
const info = (result as ResultWithInfo)[liveQueryResultInfo]
|
|
32
|
+
if (!info) {
|
|
33
|
+
throw new Error(`Missing internal live query result information.`)
|
|
34
|
+
}
|
|
35
|
+
return info
|
|
36
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
1
3
|
import { useCallback, useRef, useSyncExternalStore } from 'react'
|
|
2
4
|
import {
|
|
3
5
|
assertLiveQueryWindowManyResult,
|
|
@@ -11,11 +13,23 @@ import {
|
|
|
11
13
|
resolveLiveQueryWindowInput,
|
|
12
14
|
shouldPreserveLiveQueryWindowPageCount,
|
|
13
15
|
} from '@tanstack/db'
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
+
import { useOptionalDbClient } from './DbProvider'
|
|
17
|
+
import {
|
|
18
|
+
prepareDerivedQuery,
|
|
19
|
+
prepareQueryValue,
|
|
20
|
+
warnDeprecatedDepsArray,
|
|
21
|
+
warnUnhashableDerivedIdentity,
|
|
22
|
+
} from './useLiveQuery'
|
|
23
|
+
import type {
|
|
24
|
+
DerivedIdentityProfiler,
|
|
25
|
+
LiveQueryKey,
|
|
26
|
+
useLiveQuery,
|
|
27
|
+
} from './useLiveQuery'
|
|
16
28
|
import type {
|
|
17
29
|
Collection,
|
|
30
|
+
CollectionImpl as CollectionImplType,
|
|
18
31
|
Context,
|
|
32
|
+
DbClient,
|
|
19
33
|
InferResultType,
|
|
20
34
|
InitialQueryBuilder,
|
|
21
35
|
LiveQueryWindowController,
|
|
@@ -25,8 +39,17 @@ import type {
|
|
|
25
39
|
|
|
26
40
|
// Live queries created here are cleaned up immediately (0 disables GC).
|
|
27
41
|
const DEFAULT_GC_TIME_MS = 1
|
|
42
|
+
const unpreparedQueryValue = Symbol(`unpreparedQueryValue`)
|
|
28
43
|
|
|
29
44
|
export type UseLiveInfiniteQueryConfig<TContext extends Context> = {
|
|
45
|
+
/**
|
|
46
|
+
* Explicit identity for queries that contain opaque functional variants or
|
|
47
|
+
* are hot enough that deriving identity from structured IR is too expensive.
|
|
48
|
+
* Structured queries should omit this so DB can derive identity directly.
|
|
49
|
+
*/
|
|
50
|
+
queryKey?: LiveQueryKey
|
|
51
|
+
/** Override the nearest DbProvider for this query. */
|
|
52
|
+
client?: DbClient
|
|
30
53
|
pageSize?: number
|
|
31
54
|
initialPageParam?: number
|
|
32
55
|
/**
|
|
@@ -62,6 +85,9 @@ type EnabledLiveQueryReturn<TContext extends Context> = ReturnType<
|
|
|
62
85
|
type InfiniteQueryRenderState = {
|
|
63
86
|
inputKind: `collection` | `query`
|
|
64
87
|
inputCollection: Collection<any, any, any> | null
|
|
88
|
+
inputQuery: unknown
|
|
89
|
+
client: DbClient | undefined
|
|
90
|
+
identityMode: `collection` | `queryKey` | `legacyDeps` | `derived`
|
|
65
91
|
dependencies: Array<unknown> | null
|
|
66
92
|
pageSize: number
|
|
67
93
|
initialPageParam: number
|
|
@@ -69,72 +95,21 @@ type InfiniteQueryRenderState = {
|
|
|
69
95
|
controller: LiveQueryWindowController<any, any>
|
|
70
96
|
warning: string | null
|
|
71
97
|
warned: boolean
|
|
98
|
+
deferredCollections: Set<
|
|
99
|
+
CollectionImplType<any, string | number, any, any, any>
|
|
100
|
+
>
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
/**
|
|
75
|
-
* Create an infinite query using a query function with live updates
|
|
104
|
+
* Create an infinite query using a query function with live updates.
|
|
76
105
|
*
|
|
77
106
|
* Uses `utils.setWindow()` to dynamically adjust the limit/offset window
|
|
78
107
|
* without recreating the live query collection on each page change.
|
|
79
108
|
*
|
|
80
109
|
* @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.
|
|
81
110
|
* @param config - Configuration including pageSize and getNextPageParam
|
|
82
|
-
* @param deps -
|
|
111
|
+
* @param deps - Deprecated array of dependencies that trigger query re-execution when changed
|
|
83
112
|
* @returns Object with pages, data, and pagination controls
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* // Basic infinite query
|
|
87
|
-
* const { data, pages, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
|
|
88
|
-
* (q) => q
|
|
89
|
-
* .from({ posts: postsCollection })
|
|
90
|
-
* .orderBy(({ posts }) => posts.createdAt, 'desc')
|
|
91
|
-
* .select(({ posts }) => ({
|
|
92
|
-
* id: posts.id,
|
|
93
|
-
* title: posts.title
|
|
94
|
-
* })),
|
|
95
|
-
* {
|
|
96
|
-
* pageSize: 20,
|
|
97
|
-
* getNextPageParam: (lastPage, allPages) =>
|
|
98
|
-
* lastPage.length === 20 ? allPages.length : undefined
|
|
99
|
-
* }
|
|
100
|
-
* )
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* // With dependencies
|
|
104
|
-
* const { pages, fetchNextPage } = useLiveInfiniteQuery(
|
|
105
|
-
* (q) => q
|
|
106
|
-
* .from({ posts: postsCollection })
|
|
107
|
-
* .where(({ posts }) => eq(posts.category, category))
|
|
108
|
-
* .orderBy(({ posts }) => posts.createdAt, 'desc'),
|
|
109
|
-
* {
|
|
110
|
-
* pageSize: 10,
|
|
111
|
-
* getNextPageParam: (lastPage) =>
|
|
112
|
-
* lastPage.length === 10 ? lastPage.length : undefined
|
|
113
|
-
* },
|
|
114
|
-
* [category]
|
|
115
|
-
* )
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* // Router loader pattern with pre-created collection
|
|
119
|
-
* // In loader:
|
|
120
|
-
* const postsQuery = createLiveQueryCollection({
|
|
121
|
-
* query: (q) => q
|
|
122
|
-
* .from({ posts: postsCollection })
|
|
123
|
-
* .orderBy(({ posts }) => posts.createdAt, 'desc')
|
|
124
|
-
* .limit(20)
|
|
125
|
-
* })
|
|
126
|
-
* await postsQuery.preload()
|
|
127
|
-
* return { postsQuery }
|
|
128
|
-
*
|
|
129
|
-
* // In component:
|
|
130
|
-
* const { postsQuery } = useLoaderData()
|
|
131
|
-
* const { data, fetchNextPage, hasNextPage } = useLiveInfiniteQuery(
|
|
132
|
-
* postsQuery,
|
|
133
|
-
* {
|
|
134
|
-
* pageSize: 20,
|
|
135
|
-
* getNextPageParam: (lastPage) => lastPage.length === 20 ? lastPage.length : undefined
|
|
136
|
-
* }
|
|
137
|
-
* )
|
|
138
113
|
*/
|
|
139
114
|
|
|
140
115
|
// Overload for pre-created collection (non-single result)
|
|
@@ -158,10 +133,12 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
158
133
|
export function useLiveInfiniteQuery<TContext extends Context>(
|
|
159
134
|
queryFnOrCollection: any,
|
|
160
135
|
config: UseLiveInfiniteQueryConfig<TContext>,
|
|
161
|
-
deps
|
|
136
|
+
deps?: Array<unknown>,
|
|
162
137
|
): UseLiveInfiniteQueryReturn<TContext> {
|
|
163
138
|
const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize)
|
|
164
139
|
const initialPageParam = config.initialPageParam ?? 0
|
|
140
|
+
const contextDbClient = useOptionalDbClient()
|
|
141
|
+
const dbClient = config.client ?? contextDbClient
|
|
165
142
|
|
|
166
143
|
const inputIsCollection =
|
|
167
144
|
getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`
|
|
@@ -169,14 +146,72 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
169
146
|
const committedRef = useRef<InfiniteQueryRenderState | null>(null)
|
|
170
147
|
const committed = committedRef.current
|
|
171
148
|
const inputKind = inputIsCollection ? `collection` : `query`
|
|
149
|
+
const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({
|
|
150
|
+
renderCount: 0,
|
|
151
|
+
totalMs: 0,
|
|
152
|
+
maxMs: 0,
|
|
153
|
+
warned: false,
|
|
154
|
+
})
|
|
155
|
+
const legacyUnhashableIdentityRef = useRef<Array<unknown>>([
|
|
156
|
+
`legacy-unhashable`,
|
|
157
|
+
])
|
|
158
|
+
const deferredCollections = new Set<
|
|
159
|
+
CollectionImplType<any, string | number, any, any, any>
|
|
160
|
+
>()
|
|
172
161
|
|
|
162
|
+
let preparedQueryValue: unknown | typeof unpreparedQueryValue =
|
|
163
|
+
unpreparedQueryValue
|
|
164
|
+
let identityDeps: ReadonlyArray<unknown> = []
|
|
165
|
+
let identityMode: InfiniteQueryRenderState[`identityMode`] = `collection`
|
|
166
|
+
|
|
167
|
+
if (!inputIsCollection) {
|
|
168
|
+
if (config.queryKey !== undefined) {
|
|
169
|
+
identityMode = `queryKey`
|
|
170
|
+
identityDeps = config.queryKey
|
|
171
|
+
} else if (deps !== undefined) {
|
|
172
|
+
identityMode = `legacyDeps`
|
|
173
|
+
identityDeps = deps
|
|
174
|
+
warnDeprecatedDepsArray(`useLiveInfiniteQuery`)
|
|
175
|
+
} else if (
|
|
176
|
+
committed?.identityMode === `derived` &&
|
|
177
|
+
committed.inputQuery === queryFnOrCollection &&
|
|
178
|
+
committed.client === dbClient
|
|
179
|
+
) {
|
|
180
|
+
identityMode = `derived`
|
|
181
|
+
identityDeps = committed.dependencies ?? []
|
|
182
|
+
} else {
|
|
183
|
+
identityMode = `derived`
|
|
184
|
+
const preparation = prepareDerivedQuery(
|
|
185
|
+
queryFnOrCollection,
|
|
186
|
+
dbClient,
|
|
187
|
+
derivedIdentityProfilerRef.current,
|
|
188
|
+
deferredCollections,
|
|
189
|
+
)
|
|
190
|
+
preparedQueryValue = preparation.value
|
|
191
|
+
if (preparation.status === `hashable`) {
|
|
192
|
+
identityDeps = preparation.identityDeps
|
|
193
|
+
} else {
|
|
194
|
+
warnUnhashableDerivedIdentity(preparation.error)
|
|
195
|
+
identityDeps = legacyUnhashableIdentityRef.current
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const usesLegacyDeps =
|
|
201
|
+
!inputIsCollection && config.queryKey === undefined && deps !== undefined
|
|
173
202
|
const dependencyComparison = compareLiveQueryWindowDependencies(
|
|
174
203
|
committed?.dependencies,
|
|
175
|
-
|
|
204
|
+
identityDeps,
|
|
176
205
|
)
|
|
177
|
-
const
|
|
206
|
+
const sameClient = committed?.client === dbClient
|
|
207
|
+
const dependenciesChanged =
|
|
208
|
+
!inputIsCollection &&
|
|
209
|
+
(!sameClient ||
|
|
210
|
+
(usesLegacyDeps
|
|
211
|
+
? dependencyComparison.changed
|
|
212
|
+
: !dependencyComparison.structurallyEqual))
|
|
178
213
|
const dependenciesStructurallyEqual =
|
|
179
|
-
|
|
214
|
+
usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual
|
|
180
215
|
const needsNewCollection =
|
|
181
216
|
committed === null ||
|
|
182
217
|
committed.inputKind !== inputKind ||
|
|
@@ -195,7 +230,18 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
195
230
|
let warning: string | null = null
|
|
196
231
|
|
|
197
232
|
if (needsNewCollection) {
|
|
198
|
-
|
|
233
|
+
let inputValue = queryFnOrCollection
|
|
234
|
+
if (!inputIsCollection) {
|
|
235
|
+
if (preparedQueryValue === unpreparedQueryValue) {
|
|
236
|
+
preparedQueryValue = prepareQueryValue(
|
|
237
|
+
queryFnOrCollection,
|
|
238
|
+
dbClient,
|
|
239
|
+
deferredCollections,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
inputValue = () => preparedQueryValue
|
|
243
|
+
}
|
|
244
|
+
const input = resolveLiveQueryWindowInput<TContext>(inputValue)
|
|
199
245
|
if (input.kind === `collection`) {
|
|
200
246
|
collection = input.collection
|
|
201
247
|
} else {
|
|
@@ -239,7 +285,10 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
239
285
|
renderState = {
|
|
240
286
|
inputKind,
|
|
241
287
|
inputCollection: inputIsCollection ? collection : null,
|
|
242
|
-
|
|
288
|
+
inputQuery: inputIsCollection ? null : queryFnOrCollection,
|
|
289
|
+
client: dbClient,
|
|
290
|
+
identityMode,
|
|
291
|
+
dependencies: inputIsCollection ? null : [...identityDeps],
|
|
243
292
|
pageSize,
|
|
244
293
|
initialPageParam,
|
|
245
294
|
collection,
|
|
@@ -250,6 +299,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
250
299
|
}),
|
|
251
300
|
warning,
|
|
252
301
|
warned: false,
|
|
302
|
+
deferredCollections,
|
|
253
303
|
}
|
|
254
304
|
}
|
|
255
305
|
const currentRenderState = renderState!
|
|
@@ -263,12 +313,16 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
263
313
|
currentRenderState.warned = true
|
|
264
314
|
console.warn(currentRenderState.warning)
|
|
265
315
|
}
|
|
316
|
+
for (const collection of currentRenderState.deferredCollections) {
|
|
317
|
+
collection._resumeSyncStart()
|
|
318
|
+
}
|
|
319
|
+
currentRenderState.deferredCollections.clear()
|
|
266
320
|
return unsubscribe
|
|
267
321
|
},
|
|
268
322
|
[controller, currentRenderState],
|
|
269
323
|
)
|
|
270
324
|
const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])
|
|
271
|
-
const snapshot = useSyncExternalStore(subscribe, getSnapshot)
|
|
325
|
+
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
272
326
|
|
|
273
327
|
const fetchNextPage = useCallback(
|
|
274
328
|
() => fetchNextLiveQueryWindowPage(controller),
|