@veams/status-quo-query 0.10.0 → 0.12.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 +212 -10
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +2 -0
- package/dist/provider.js.map +1 -1
- package/dist/query.d.ts +26 -15
- package/dist/query.js +40 -31
- package/dist/query.js.map +1 -1
- package/dist/react/hooks/index.d.ts +1 -0
- package/dist/react/hooks/index.js +2 -0
- package/dist/react/hooks/index.js.map +1 -0
- package/dist/react/hooks/use-query-handle.d.ts +2 -0
- package/dist/react/hooks/use-query-handle.js +71 -0
- package/dist/react/hooks/use-query-handle.js.map +1 -0
- package/dist/react/hooks/use-query-subscription.d.ts +1 -0
- package/dist/react/hooks/use-query-subscription.js +2 -0
- package/dist/react/hooks/use-query-subscription.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/eslint.config.mjs +13 -2
- package/jest.config.cjs +2 -2
- package/package.json +21 -10
- package/src/__tests__/provider.spec.ts +8 -0
- package/src/index.ts +0 -2
- package/src/provider.ts +6 -2
- package/src/query.ts +84 -68
- package/src/react/__tests__/use-query-handle.spec.tsx +104 -0
- package/src/react/hooks/index.ts +1 -0
- package/src/react/hooks/use-query-handle.ts +96 -0
- package/src/react/index.ts +1 -0
- package/tsconfig.eslint.json +2 -1
- package/tsconfig.json +5 -2
- package/dist/query-registry.d.ts +0 -9
- package/dist/query-registry.js +0 -28
- package/dist/query-registry.js.map +0 -1
- package/src/__tests__/query-registry.spec.ts +0 -101
- package/src/query-registry.ts +0 -52
package/src/query.ts
CHANGED
|
@@ -36,9 +36,9 @@ export type QueryFetchStatus = FetchStatus;
|
|
|
36
36
|
export type QueryStatus = TanstackQueryStatus;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Represents a stable snapshot of
|
|
39
|
+
* Represents a stable snapshot of one query handle's state.
|
|
40
40
|
*/
|
|
41
|
-
export interface
|
|
41
|
+
export interface QueryHandleSnapshot<TData, TError> {
|
|
42
42
|
// The data retrieved from a successful query.
|
|
43
43
|
data: TData | undefined;
|
|
44
44
|
// The error object if the query failed.
|
|
@@ -57,6 +57,14 @@ export interface QueryServiceSnapshot<TData, TError> {
|
|
|
57
57
|
isSuccess: boolean;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Represents the lightweight data/error read model for one query handle.
|
|
62
|
+
*/
|
|
63
|
+
export interface QueryHandleData<TData, TError> {
|
|
64
|
+
data: TData | undefined;
|
|
65
|
+
error: TError | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
/**
|
|
61
69
|
* Defines a subset of query state containing only the status and fetch status.
|
|
62
70
|
*/
|
|
@@ -66,15 +74,15 @@ export interface QueryMetaState {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
69
|
-
* Defines the public API for a query
|
|
77
|
+
* Defines the public API for a query handle.
|
|
70
78
|
*/
|
|
71
|
-
export interface
|
|
79
|
+
export interface QueryHandle<TData, TError> {
|
|
72
80
|
// Returns the current state snapshot of the query.
|
|
73
|
-
getSnapshot: () =>
|
|
81
|
+
getSnapshot: () => QueryHandleSnapshot<TData, TError>;
|
|
74
82
|
// Subscribes a listener to state changes; returns an unsubscribe function.
|
|
75
|
-
subscribe: (listener: (snapshot:
|
|
83
|
+
subscribe: (listener: (snapshot: QueryHandleSnapshot<TData, TError>) => void) => () => void;
|
|
76
84
|
// Manually triggers a refetch of this query.
|
|
77
|
-
refetch: (options?: RefetchOptions) => Promise<
|
|
85
|
+
refetch: (options?: RefetchOptions) => Promise<QueryHandleSnapshot<TData, TError>>;
|
|
78
86
|
// Marks this specific query as invalid in the cache to trigger a refetch if active.
|
|
79
87
|
invalidate: (options?: QueryInvalidateOptions) => Promise<void>;
|
|
80
88
|
// Escape hatch: provides direct access to the underlying Tanstack Query observer result.
|
|
@@ -93,14 +101,14 @@ type QueryDependencyDerivedOptions<TQueryKey extends QueryKey = QueryKey> = {
|
|
|
93
101
|
queryKey?: TQueryKey;
|
|
94
102
|
};
|
|
95
103
|
|
|
96
|
-
type
|
|
104
|
+
type QueryHandleRuntimeOptions<
|
|
97
105
|
TQueryFnData = unknown,
|
|
98
106
|
TError = Error,
|
|
99
107
|
TData = TQueryFnData,
|
|
100
108
|
TQueryData = TQueryFnData,
|
|
101
109
|
TQueryKey extends QueryKey = QueryKey,
|
|
102
110
|
> = Omit<
|
|
103
|
-
|
|
111
|
+
QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
|
|
104
112
|
'dependsOn'
|
|
105
113
|
>;
|
|
106
114
|
|
|
@@ -108,9 +116,9 @@ export type QueryDependencyTuple<
|
|
|
108
116
|
TSources extends readonly unknown[],
|
|
109
117
|
TQueryKey extends QueryKey = QueryKey,
|
|
110
118
|
> = readonly [
|
|
111
|
-
sources: { readonly [K in keyof TSources]:
|
|
119
|
+
sources: { readonly [K in keyof TSources]: QueryHandle<TSources[K], Error> },
|
|
112
120
|
deriveOptions: (
|
|
113
|
-
sourceSnapshots: { readonly [K in keyof TSources]:
|
|
121
|
+
sourceSnapshots: { readonly [K in keyof TSources]: QueryHandleSnapshot<TSources[K], Error> }
|
|
114
122
|
) => QueryDependencyDerivedOptions<TQueryKey>,
|
|
115
123
|
];
|
|
116
124
|
|
|
@@ -131,15 +139,15 @@ export interface CreateUntrackedQuery {
|
|
|
131
139
|
// The asynchronous function that performs the data fetch.
|
|
132
140
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
133
141
|
// Optional configuration for behavior like staleness, retry, and refetching.
|
|
134
|
-
options?:
|
|
135
|
-
):
|
|
142
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
143
|
+
): QueryHandle<TData, TError>;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
/**
|
|
139
147
|
* Function signature for the default query factory that derives dependencies from the final
|
|
140
148
|
* query-key segment.
|
|
141
149
|
*
|
|
142
|
-
* The tracked query handle deliberately stays API-compatible with the normal query
|
|
150
|
+
* The tracked query handle deliberately stays API-compatible with the normal query handle.
|
|
143
151
|
* The only extra behavior is invisible: dependency registration and on-demand re-registration.
|
|
144
152
|
*/
|
|
145
153
|
export interface CreateQuery {
|
|
@@ -154,14 +162,14 @@ export interface CreateQuery {
|
|
|
154
162
|
>(
|
|
155
163
|
queryKey: TQueryKey,
|
|
156
164
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
157
|
-
options?:
|
|
158
|
-
):
|
|
165
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
166
|
+
): QueryHandle<TData, TError>;
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
/**
|
|
162
|
-
* Configuration options for creating a query
|
|
170
|
+
* Configuration options for creating a query handle, excluding function and key.
|
|
163
171
|
*/
|
|
164
|
-
export type
|
|
172
|
+
export type QueryHandleOptions<
|
|
165
173
|
TQueryFnData = unknown,
|
|
166
174
|
TError = Error,
|
|
167
175
|
TData = TQueryFnData,
|
|
@@ -179,7 +187,7 @@ export type QueryServiceOptions<
|
|
|
179
187
|
* Extracts and maps status and fetchStatus to our QueryMetaState interface.
|
|
180
188
|
*/
|
|
181
189
|
export function toQueryMetaState<TData, TError>(
|
|
182
|
-
snapshot: Pick<
|
|
190
|
+
snapshot: Pick<QueryHandleSnapshot<TData, TError>, 'fetchStatus' | 'status'>
|
|
183
191
|
): QueryMetaState {
|
|
184
192
|
// Return a simplified state object for UI or other services.
|
|
185
193
|
return {
|
|
@@ -188,6 +196,18 @@ export function toQueryMetaState<TData, TError>(
|
|
|
188
196
|
};
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Extracts only data and error from a query snapshot.
|
|
201
|
+
*/
|
|
202
|
+
export function toQueryHandleData<TData, TError>(
|
|
203
|
+
snapshot: Pick<QueryHandleSnapshot<TData, TError>, 'data' | 'error'>
|
|
204
|
+
): QueryHandleData<TData, TError> {
|
|
205
|
+
return {
|
|
206
|
+
data: snapshot.data,
|
|
207
|
+
error: snapshot.error,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
/**
|
|
192
212
|
* Helper function to check if the query is in its initial loading state.
|
|
193
213
|
*/
|
|
@@ -200,7 +220,7 @@ export function isQueryLoading(query: QueryMetaState): boolean {
|
|
|
200
220
|
* Prepares the query factory by binding it to a specific QueryClient instance.
|
|
201
221
|
*/
|
|
202
222
|
export function setupQuery(queryClient: QueryClient): CreateUntrackedQuery {
|
|
203
|
-
// Returns the actual factory function for creating individual query
|
|
223
|
+
// Returns the actual factory function for creating individual query handles.
|
|
204
224
|
return function createQuery<
|
|
205
225
|
TSources extends readonly unknown[] = [],
|
|
206
226
|
TQueryFnData = unknown,
|
|
@@ -211,16 +231,16 @@ export function setupQuery(queryClient: QueryClient): CreateUntrackedQuery {
|
|
|
211
231
|
>(
|
|
212
232
|
queryKey: TQueryKey,
|
|
213
233
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
214
|
-
options?:
|
|
215
|
-
):
|
|
216
|
-
const { dependsOn, runtimeOptions } =
|
|
217
|
-
const
|
|
234
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
235
|
+
): QueryHandle<TData, TError> {
|
|
236
|
+
const { dependsOn, runtimeOptions } = splitQueryHandleOptions(options);
|
|
237
|
+
const handle = createQueryHandle(queryClient, queryKey, queryFn, runtimeOptions);
|
|
218
238
|
|
|
219
239
|
if (!dependsOn) {
|
|
220
|
-
return
|
|
240
|
+
return handle.handle;
|
|
221
241
|
}
|
|
222
242
|
|
|
223
|
-
return bindQueryDependencies(
|
|
243
|
+
return bindQueryDependencies(handle, queryKey, dependsOn);
|
|
224
244
|
};
|
|
225
245
|
}
|
|
226
246
|
|
|
@@ -249,33 +269,33 @@ export function setupTrackedQuery(
|
|
|
249
269
|
>(
|
|
250
270
|
queryKey: TQueryKey,
|
|
251
271
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
252
|
-
options?:
|
|
253
|
-
):
|
|
254
|
-
const { dependsOn, runtimeOptions } =
|
|
255
|
-
// Reuse the same core query
|
|
256
|
-
const
|
|
272
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
273
|
+
): QueryHandle<TData, TError> {
|
|
274
|
+
const { dependsOn, runtimeOptions } = splitQueryHandleOptions(options);
|
|
275
|
+
// Reuse the same core query-handle implementation as the untracked API.
|
|
276
|
+
const handle = createQueryHandle(queryClient, queryKey, queryFn, runtimeOptions);
|
|
257
277
|
// We only need re-registration on the transition from zero to one subscribers.
|
|
258
278
|
let subscriberCount = 0;
|
|
259
279
|
|
|
260
280
|
// Register the current query hash immediately so future tracked mutations can find it.
|
|
261
281
|
trackingRegistry.register(
|
|
262
|
-
|
|
263
|
-
extractTrackedDependencies(
|
|
282
|
+
handle.observer.getCurrentQuery().queryHash,
|
|
283
|
+
extractTrackedDependencies(handle.getCurrentQueryKey())
|
|
264
284
|
);
|
|
265
285
|
|
|
266
286
|
const applyTrackedDerivedState = (derivedOptions: QueryDependencyDerivedOptions<TQueryKey>) => {
|
|
267
|
-
const previousQueryHash =
|
|
287
|
+
const previousQueryHash = handle.observer.getCurrentQuery().queryHash;
|
|
268
288
|
|
|
269
|
-
|
|
289
|
+
handle.setDerivedState(derivedOptions);
|
|
270
290
|
|
|
271
|
-
const nextQueryHash =
|
|
291
|
+
const nextQueryHash = handle.observer.getCurrentQuery().queryHash;
|
|
272
292
|
|
|
273
293
|
if (nextQueryHash === previousQueryHash) {
|
|
274
294
|
return;
|
|
275
295
|
}
|
|
276
296
|
|
|
277
297
|
trackingRegistry.unregister(previousQueryHash);
|
|
278
|
-
trackingRegistry.register(nextQueryHash, extractTrackedDependencies(
|
|
298
|
+
trackingRegistry.register(nextQueryHash, extractTrackedDependencies(handle.getCurrentQueryKey()));
|
|
279
299
|
};
|
|
280
300
|
|
|
281
301
|
const dependencyController = dependsOn
|
|
@@ -291,9 +311,9 @@ export function setupTrackedQuery(
|
|
|
291
311
|
// the same mechanism TanStack uses internally when a query gets recreated after GC.
|
|
292
312
|
const liveQuery = queryClient.getQueryCache().build(
|
|
293
313
|
queryClient,
|
|
294
|
-
|
|
314
|
+
handle.getCurrentObserverOptions()
|
|
295
315
|
);
|
|
296
|
-
const liveDependencies = extractTrackedDependencies(
|
|
316
|
+
const liveDependencies = extractTrackedDependencies(handle.getCurrentQueryKey());
|
|
297
317
|
|
|
298
318
|
// Re-register only when TanStack has recreated the query and the registry has already
|
|
299
319
|
// cleaned up the previous hash. This keeps the edge-case handling cheap in the common case.
|
|
@@ -303,12 +323,12 @@ export function setupTrackedQuery(
|
|
|
303
323
|
};
|
|
304
324
|
|
|
305
325
|
return {
|
|
306
|
-
...
|
|
326
|
+
...handle.handle,
|
|
307
327
|
refetch: async (refetchOptions) => {
|
|
308
328
|
await dependencyController?.evaluateForRefetch();
|
|
309
329
|
// Refetch is one of the two explicit reactivation paths agreed on in the design.
|
|
310
330
|
ensureRegistered();
|
|
311
|
-
return
|
|
331
|
+
return handle.handle.refetch(refetchOptions);
|
|
312
332
|
},
|
|
313
333
|
subscribe: (listener) => {
|
|
314
334
|
// The first active subscriber is the other reactivation path. Re-running registration
|
|
@@ -320,7 +340,7 @@ export function setupTrackedQuery(
|
|
|
320
340
|
|
|
321
341
|
subscriberCount += 1;
|
|
322
342
|
|
|
323
|
-
const unsubscribe =
|
|
343
|
+
const unsubscribe = handle.handle.subscribe(listener);
|
|
324
344
|
|
|
325
345
|
return () => {
|
|
326
346
|
// Keep the counter bounded so accidental double-unsubscribe cannot push it negative.
|
|
@@ -338,10 +358,10 @@ export function setupTrackedQuery(
|
|
|
338
358
|
/**
|
|
339
359
|
* Internal helper to transform a raw Tanstack query result into our public snapshot format.
|
|
340
360
|
*/
|
|
341
|
-
function
|
|
361
|
+
function toQueryHandleSnapshot<TData, TError>(
|
|
342
362
|
result: QueryObserverResult<TData, TError>
|
|
343
|
-
):
|
|
344
|
-
// Extract and return the relevant fields for the UI or other
|
|
363
|
+
): QueryHandleSnapshot<TData, TError> {
|
|
364
|
+
// Extract and return the relevant fields for the UI or other handle consumers.
|
|
345
365
|
return {
|
|
346
366
|
data: result.data,
|
|
347
367
|
error: result.error,
|
|
@@ -354,7 +374,7 @@ function toQueryServiceSnapshot<TData, TError>(
|
|
|
354
374
|
};
|
|
355
375
|
}
|
|
356
376
|
|
|
357
|
-
function
|
|
377
|
+
function createQueryHandle<
|
|
358
378
|
TQueryFnData = unknown,
|
|
359
379
|
TError = Error,
|
|
360
380
|
TData = TQueryFnData,
|
|
@@ -364,12 +384,12 @@ function createQueryService<
|
|
|
364
384
|
queryClient: QueryClient,
|
|
365
385
|
queryKey: TQueryKey,
|
|
366
386
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
367
|
-
options?:
|
|
387
|
+
options?: QueryHandleRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
368
388
|
): {
|
|
369
389
|
// Expose the observer internally so tracked queries can access the current query hash.
|
|
370
390
|
observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
|
|
371
|
-
// Preserve the public query-
|
|
372
|
-
|
|
391
|
+
// Preserve the public query-handle shape for all callers.
|
|
392
|
+
handle: QueryHandle<TData, TError>;
|
|
373
393
|
getCurrentObserverOptions: () => QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
|
|
374
394
|
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>;
|
|
375
395
|
getCurrentQueryKey: () => TQueryKey;
|
|
@@ -401,14 +421,14 @@ function createQueryService<
|
|
|
401
421
|
getCurrentObserverOptions,
|
|
402
422
|
getCurrentQueryKey: () => resolvedQueryKey,
|
|
403
423
|
setDerivedState,
|
|
404
|
-
|
|
405
|
-
getSnapshot: () =>
|
|
424
|
+
handle: {
|
|
425
|
+
getSnapshot: () => toQueryHandleSnapshot(observer.getCurrentResult()),
|
|
406
426
|
subscribe: (listener) =>
|
|
407
427
|
observer.subscribe((result) => {
|
|
408
|
-
listener(
|
|
428
|
+
listener(toQueryHandleSnapshot(result));
|
|
409
429
|
}),
|
|
410
430
|
refetch: async (refetchOptions) =>
|
|
411
|
-
|
|
431
|
+
toQueryHandleSnapshot(await observer.refetch(refetchOptions)),
|
|
412
432
|
invalidate: (invalidateOptions) =>
|
|
413
433
|
queryClient.invalidateQueries(
|
|
414
434
|
{
|
|
@@ -433,24 +453,24 @@ function bindQueryDependencies<
|
|
|
433
453
|
TQueryData = TQueryFnData,
|
|
434
454
|
TQueryKey extends QueryKey = QueryKey,
|
|
435
455
|
>(
|
|
436
|
-
|
|
437
|
-
typeof
|
|
456
|
+
queryHandle: ReturnType<
|
|
457
|
+
typeof createQueryHandle<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
438
458
|
>,
|
|
439
459
|
queryKey: TQueryKey,
|
|
440
460
|
dependsOn: QueryDependencyTuple<TSources, TQueryKey>
|
|
441
|
-
):
|
|
461
|
+
): QueryHandle<TData, TError> {
|
|
442
462
|
const dependencyController = createDependencyController(
|
|
443
463
|
queryKey,
|
|
444
|
-
|
|
464
|
+
queryHandle.setDerivedState,
|
|
445
465
|
dependsOn
|
|
446
466
|
);
|
|
447
467
|
let subscriberCount = 0;
|
|
448
468
|
|
|
449
469
|
return {
|
|
450
|
-
...
|
|
470
|
+
...queryHandle.handle,
|
|
451
471
|
refetch: async (refetchOptions) => {
|
|
452
472
|
await dependencyController.evaluateForRefetch();
|
|
453
|
-
return
|
|
473
|
+
return queryHandle.handle.refetch(refetchOptions);
|
|
454
474
|
},
|
|
455
475
|
subscribe: (listener) => {
|
|
456
476
|
if (subscriberCount === 0) {
|
|
@@ -459,7 +479,7 @@ function bindQueryDependencies<
|
|
|
459
479
|
|
|
460
480
|
subscriberCount += 1;
|
|
461
481
|
|
|
462
|
-
const unsubscribe =
|
|
482
|
+
const unsubscribe = queryHandle.handle.subscribe(listener);
|
|
463
483
|
|
|
464
484
|
return () => {
|
|
465
485
|
subscriberCount = Math.max(0, subscriberCount - 1);
|
|
@@ -474,10 +494,6 @@ function bindQueryDependencies<
|
|
|
474
494
|
|
|
475
495
|
function createDependencyController<
|
|
476
496
|
TSources extends readonly unknown[] = [],
|
|
477
|
-
TQueryFnData = unknown,
|
|
478
|
-
TError = Error,
|
|
479
|
-
TData = TQueryFnData,
|
|
480
|
-
TQueryData = TQueryFnData,
|
|
481
497
|
TQueryKey extends QueryKey = QueryKey,
|
|
482
498
|
>(
|
|
483
499
|
baseQueryKey: TQueryKey,
|
|
@@ -555,7 +571,7 @@ function createDependencyController<
|
|
|
555
571
|
};
|
|
556
572
|
}
|
|
557
573
|
|
|
558
|
-
function
|
|
574
|
+
function splitQueryHandleOptions<
|
|
559
575
|
TQueryFnData = unknown,
|
|
560
576
|
TError = Error,
|
|
561
577
|
TData = TQueryFnData,
|
|
@@ -563,10 +579,10 @@ function splitQueryServiceOptions<
|
|
|
563
579
|
TQueryKey extends QueryKey = QueryKey,
|
|
564
580
|
TSources extends readonly unknown[] = [],
|
|
565
581
|
>(
|
|
566
|
-
options?:
|
|
582
|
+
options?: QueryHandleOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TSources>
|
|
567
583
|
): {
|
|
568
584
|
dependsOn?: QueryDependencyTuple<TSources, TQueryKey>;
|
|
569
|
-
runtimeOptions:
|
|
585
|
+
runtimeOptions: QueryHandleRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> | undefined;
|
|
570
586
|
} {
|
|
571
587
|
if (options === undefined) {
|
|
572
588
|
return {
|
|
@@ -592,7 +608,7 @@ function toQueryOptions<
|
|
|
592
608
|
>(
|
|
593
609
|
queryKey: TQueryKey,
|
|
594
610
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
|
595
|
-
options?:
|
|
611
|
+
options?: QueryHandleRuntimeOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
|
596
612
|
): QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey> &
|
|
597
613
|
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
|
|
598
614
|
// Centralize option assembly so both normal queries and tracked queries build observers and
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { act } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
4
|
+
|
|
5
|
+
import { setupQuery } from '../../query.js';
|
|
6
|
+
import { useQueryHandle } from '../hooks/use-query-handle.js';
|
|
7
|
+
|
|
8
|
+
import type { QueryHandle, QueryHandleSnapshot } from '../../query.js';
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('useQueryHandle', () => {
|
|
15
|
+
let container: HTMLDivElement;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
container = document.createElement('div');
|
|
23
|
+
document.body.appendChild(container);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
container.remove();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders the latest query snapshot', async () => {
|
|
31
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0 } } });
|
|
32
|
+
const createQuery = setupQuery(queryClient);
|
|
33
|
+
const query = createQuery(['product', 1], jest.fn().mockResolvedValue({ name: 'Ada' }), {
|
|
34
|
+
enabled: false,
|
|
35
|
+
});
|
|
36
|
+
const renderStates: Array<string | undefined> = [];
|
|
37
|
+
|
|
38
|
+
const Consumer = () => {
|
|
39
|
+
const snapshot = useQueryHandle(query);
|
|
40
|
+
renderStates.push(snapshot.data?.name);
|
|
41
|
+
|
|
42
|
+
return <span>{snapshot.data?.name ?? 'pending'}</span>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const root = createRoot(container);
|
|
46
|
+
|
|
47
|
+
await act(async () => {
|
|
48
|
+
root.render(<Consumer />);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(container.textContent).toBe('pending');
|
|
52
|
+
|
|
53
|
+
await act(async () => {
|
|
54
|
+
await query.refetch();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(container.textContent).toBe('Ada');
|
|
58
|
+
expect(renderStates).toContain(undefined);
|
|
59
|
+
expect(renderStates).toContain('Ada');
|
|
60
|
+
|
|
61
|
+
await act(async () => {
|
|
62
|
+
root.unmount();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it('cleans up the store subscription on unmount', async () => {
|
|
66
|
+
const snapshot: QueryHandleSnapshot<{ name: string }, Error> = {
|
|
67
|
+
data: { name: 'Ada' },
|
|
68
|
+
error: null,
|
|
69
|
+
fetchStatus: 'idle',
|
|
70
|
+
status: 'success',
|
|
71
|
+
isError: false,
|
|
72
|
+
isFetching: false,
|
|
73
|
+
isPending: false,
|
|
74
|
+
isSuccess: true,
|
|
75
|
+
};
|
|
76
|
+
const unsubscribe = jest.fn();
|
|
77
|
+
const query: QueryHandle<{ name: string }, Error> = {
|
|
78
|
+
getSnapshot: () => snapshot,
|
|
79
|
+
subscribe: jest.fn(() => unsubscribe),
|
|
80
|
+
refetch: jest.fn(async () => snapshot),
|
|
81
|
+
invalidate: jest.fn(async () => undefined),
|
|
82
|
+
unsafe_getResult: jest.fn(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const root = createRoot(container);
|
|
86
|
+
|
|
87
|
+
const Consumer = () => {
|
|
88
|
+
useQueryHandle(query);
|
|
89
|
+
return <span>ready</span>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await act(async () => {
|
|
93
|
+
root.render(<Consumer />);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(query.subscribe).toHaveBeenCalledTimes(1);
|
|
97
|
+
|
|
98
|
+
await act(async () => {
|
|
99
|
+
root.unmount();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useQueryHandle } from './use-query-handle.js';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Import the React hooks needed to memoize callbacks, hold mutable cache state,
|
|
2
|
+
// and connect an external store to React rendering.
|
|
3
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
// Import the query-handle contract and the stable snapshot shape the hook returns.
|
|
6
|
+
import type { QueryHandle, QueryHandleSnapshot } from '../../query.js';
|
|
7
|
+
// Describe the no-argument listener shape expected by useSyncExternalStore.
|
|
8
|
+
type Listener = () => void;
|
|
9
|
+
|
|
10
|
+
// Store one cached snapshot together with the store version it belongs to.
|
|
11
|
+
type SnapshotCacheEntry<TData, TError> = {
|
|
12
|
+
// Hold the snapshot returned by the query handle for this version.
|
|
13
|
+
snapshot: QueryHandleSnapshot<TData, TError>;
|
|
14
|
+
// Track which subscription version produced the cached snapshot.
|
|
15
|
+
version: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Subscribe a React component to a QueryHandle and return its latest snapshot.
|
|
19
|
+
export function useQueryHandle<TData, TError>(
|
|
20
|
+
// Receive the external query handle instance to subscribe to.
|
|
21
|
+
queryHandle: QueryHandle<TData, TError>
|
|
22
|
+
) {
|
|
23
|
+
// Count store notifications so we can tell when our cached snapshot is stale.
|
|
24
|
+
const snapshotVersionRef = useRef(0);
|
|
25
|
+
// Cache one client-side snapshot per observed version to keep getSnapshot stable.
|
|
26
|
+
const snapshotCacheRef = useRef<SnapshotCacheEntry<TData, TError> | null>(null);
|
|
27
|
+
// Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
|
|
28
|
+
const serverSnapshotCacheRef = useRef<QueryHandleSnapshot<TData, TError> | null>(null);
|
|
29
|
+
|
|
30
|
+
// Create the subscribe function expected by useSyncExternalStore.
|
|
31
|
+
const subscribe = useCallback(
|
|
32
|
+
// React passes a listener that must run whenever the external store changes.
|
|
33
|
+
(listener: Listener) =>
|
|
34
|
+
// Forward the subscription to the query handle.
|
|
35
|
+
queryHandle.subscribe(() => {
|
|
36
|
+
// Bump the version so later reads know the previous cache is outdated.
|
|
37
|
+
snapshotVersionRef.current += 1;
|
|
38
|
+
// Drop the cached client snapshot because the store just changed.
|
|
39
|
+
snapshotCacheRef.current = null;
|
|
40
|
+
// Drop the cached server snapshot for the same reason.
|
|
41
|
+
serverSnapshotCacheRef.current = null;
|
|
42
|
+
// Notify React that it should read a fresh snapshot.
|
|
43
|
+
listener();
|
|
44
|
+
}),
|
|
45
|
+
// Recreate the subscription function only when the handle instance changes.
|
|
46
|
+
[queryHandle]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Read the current client snapshot in a referentially stable way for React.
|
|
50
|
+
const getSnapshot = useCallback(() => {
|
|
51
|
+
// Read the latest store version number.
|
|
52
|
+
const version = snapshotVersionRef.current;
|
|
53
|
+
// Read the last cached client snapshot, if there is one.
|
|
54
|
+
const cachedSnapshot = snapshotCacheRef.current;
|
|
55
|
+
|
|
56
|
+
// Reuse the cached snapshot when it was produced for the current version.
|
|
57
|
+
if (cachedSnapshot && cachedSnapshot.version === version) {
|
|
58
|
+
// Return the cached snapshot so repeated reads in the same render stay stable.
|
|
59
|
+
return cachedSnapshot.snapshot;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Ask the query handle for the latest snapshot because the cache is empty or stale.
|
|
63
|
+
const snapshot = queryHandle.getSnapshot();
|
|
64
|
+
// Store the new snapshot together with the version it belongs to.
|
|
65
|
+
snapshotCacheRef.current = {
|
|
66
|
+
snapshot,
|
|
67
|
+
version,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Return the freshly read snapshot to React.
|
|
71
|
+
return snapshot;
|
|
72
|
+
}, [queryHandle]);
|
|
73
|
+
|
|
74
|
+
// Read the server snapshot used by React during SSR or hydration fallback paths.
|
|
75
|
+
const getServerSnapshot = useCallback(() => {
|
|
76
|
+
// Read the cached server snapshot, if one was stored earlier.
|
|
77
|
+
const cachedSnapshot = serverSnapshotCacheRef.current;
|
|
78
|
+
|
|
79
|
+
// Reuse the cached server snapshot to keep server reads stable.
|
|
80
|
+
if (cachedSnapshot) {
|
|
81
|
+
// Return the cached server snapshot directly.
|
|
82
|
+
return cachedSnapshot;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ask the query handle for a snapshot because no server cache exists yet.
|
|
86
|
+
const snapshot = queryHandle.getSnapshot();
|
|
87
|
+
// Cache that snapshot for the next server read.
|
|
88
|
+
serverSnapshotCacheRef.current = snapshot;
|
|
89
|
+
|
|
90
|
+
// Return the freshly read server snapshot.
|
|
91
|
+
return snapshot;
|
|
92
|
+
}, [queryHandle]);
|
|
93
|
+
|
|
94
|
+
// Let React subscribe to the external store and read snapshots through the callbacks above.
|
|
95
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useQueryHandle } from './hooks/index.js';
|
package/tsconfig.eslint.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
"declaration": true,
|
|
5
5
|
"declarationDir": "dist",
|
|
6
6
|
"esModuleInterop": true,
|
|
7
|
+
"jsx": "react",
|
|
7
8
|
"lib": [
|
|
8
|
-
"es2022"
|
|
9
|
+
"es2022",
|
|
10
|
+
"dom"
|
|
9
11
|
],
|
|
10
12
|
"module": "es2022",
|
|
11
13
|
"moduleResolution": "bundler",
|
|
@@ -19,7 +21,8 @@
|
|
|
19
21
|
]
|
|
20
22
|
},
|
|
21
23
|
"include": [
|
|
22
|
-
"src/**/*.ts"
|
|
24
|
+
"src/**/*.ts",
|
|
25
|
+
"src/**/*.tsx"
|
|
23
26
|
],
|
|
24
27
|
"exclude": [
|
|
25
28
|
"coverage",
|
package/dist/query-registry.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { QueryService } from './query.js';
|
|
2
|
-
export interface QueryRegistry<TParams, TKey extends readonly unknown[]> {
|
|
3
|
-
clear: () => void;
|
|
4
|
-
getKey: (params: TParams) => TKey;
|
|
5
|
-
name: string;
|
|
6
|
-
resolve: <TData, TError = Error>(params: TParams, create: (queryKey: TKey) => QueryService<TData, TError>) => QueryService<TData, TError>;
|
|
7
|
-
}
|
|
8
|
-
export declare function createQueryRegistry<TParams, TKey extends readonly unknown[]>(name: string, createKey: (params: TParams) => TKey): QueryRegistry<TParams, TKey>;
|
|
9
|
-
export declare function serializeQueryKey(queryKey: readonly unknown[]): string;
|
package/dist/query-registry.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { hashKey } from '@tanstack/query-core';
|
|
2
|
-
export function createQueryRegistry(name, createKey) {
|
|
3
|
-
const entries = new Map();
|
|
4
|
-
return {
|
|
5
|
-
name,
|
|
6
|
-
clear() {
|
|
7
|
-
entries.clear();
|
|
8
|
-
},
|
|
9
|
-
getKey(params) {
|
|
10
|
-
return createKey(params);
|
|
11
|
-
},
|
|
12
|
-
resolve(params, create) {
|
|
13
|
-
const queryKey = createKey(params);
|
|
14
|
-
const cacheKey = serializeQueryKey(queryKey);
|
|
15
|
-
const existingEntry = entries.get(cacheKey);
|
|
16
|
-
if (existingEntry) {
|
|
17
|
-
return existingEntry;
|
|
18
|
-
}
|
|
19
|
-
const nextEntry = create(queryKey);
|
|
20
|
-
entries.set(cacheKey, nextEntry);
|
|
21
|
-
return nextEntry;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
export function serializeQueryKey(queryKey) {
|
|
26
|
-
return hashKey(queryKey);
|
|
27
|
-
}
|
|
28
|
-
//# sourceMappingURL=query-registry.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"query-registry.js","sourceRoot":"","sources":["../src/query-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAc/C,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,SAAoC;IAEpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0C,CAAC;IAElE,OAAO;QACL,IAAI;QACJ,KAAK;YACH,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;QACD,MAAM,CAAC,MAAM;YACX,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,CACL,MAAe,EACf,MAAuD;YAEvD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;YAC7C,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAA4C,CAAC;YAEvF,IAAI,aAAa,EAAE,CAAC;gBAClB,OAAO,aAAa,CAAC;YACvB,CAAC;YAED,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;YAEnC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,SAA2C,CAAC,CAAC;YAEnE,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAA4B;IAC5D,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC3B,CAAC"}
|