@tanstack/react-db 0.5.5 → 0.5.7
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/dist/cjs/source-id-bindings.cjs +60 -0
- package/dist/cjs/source-id-bindings.cjs.map +1 -0
- package/dist/cjs/source-id-bindings.d.cts +26 -0
- package/dist/cjs/useLiveInfiniteQuery.cjs +68 -26
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.cjs +18 -52
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/esm/source-id-bindings.d.ts +26 -0
- package/dist/esm/source-id-bindings.js +60 -0
- package/dist/esm/source-id-bindings.js.map +1 -0
- package/dist/esm/useLiveInfiniteQuery.js +69 -27
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.js +18 -52
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/source-id-bindings.ts +96 -0
- package/src/useLiveInfiniteQuery.ts +129 -34
- package/src/useLiveQuery.ts +31 -68
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const db = require("@tanstack/db");
|
|
4
|
+
const sourceObjectTokens = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
let nextSourceObjectToken = 0;
|
|
6
|
+
function getSourceObjectToken(source) {
|
|
7
|
+
let id = sourceObjectTokens.get(source);
|
|
8
|
+
if (id === void 0) {
|
|
9
|
+
id = ++nextSourceObjectToken;
|
|
10
|
+
sourceObjectTokens.set(source, id);
|
|
11
|
+
}
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
function getPreparedSources(preparedValue) {
|
|
15
|
+
if (db.isCollection(preparedValue)) return [preparedValue];
|
|
16
|
+
const query = preparedValue instanceof db.BaseQueryBuilder ? preparedValue : preparedValue && typeof preparedValue === `object` && `query` in preparedValue && preparedValue.query instanceof db.BaseQueryBuilder ? preparedValue.query : void 0;
|
|
17
|
+
return query ? db.IR.collectCollectionSources(query._getQuery()).map(
|
|
18
|
+
({ collection }) => collection
|
|
19
|
+
) : [];
|
|
20
|
+
}
|
|
21
|
+
function createSourceIdBindings() {
|
|
22
|
+
return { unscoped: /* @__PURE__ */ new Map(), byClient: /* @__PURE__ */ new WeakMap() };
|
|
23
|
+
}
|
|
24
|
+
function claimSourceIds(bindings, preparedValue, client, hookName, release) {
|
|
25
|
+
const prior = client ? bindings.byClient.get(client) : bindings.unscoped;
|
|
26
|
+
const seen = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const source of getPreparedSources(preparedValue)) {
|
|
28
|
+
const token = getSourceObjectToken(source);
|
|
29
|
+
const previous = seen.get(source.id) ?? prior?.get(source.id);
|
|
30
|
+
if (previous !== void 0 && previous !== token) {
|
|
31
|
+
release();
|
|
32
|
+
throw new Error(
|
|
33
|
+
`[${hookName}] Source Collection "${source.id}" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
seen.set(source.id, token);
|
|
37
|
+
}
|
|
38
|
+
const claimed = prior ?? /* @__PURE__ */ new Map();
|
|
39
|
+
for (const [id, token] of seen) claimed.set(id, token);
|
|
40
|
+
if (client) bindings.byClient.set(client, claimed);
|
|
41
|
+
}
|
|
42
|
+
function resumeSyncStarts(collections) {
|
|
43
|
+
const pending = Array.from(collections);
|
|
44
|
+
collections.clear();
|
|
45
|
+
const errors = [];
|
|
46
|
+
for (const collection of pending) {
|
|
47
|
+
try {
|
|
48
|
+
collection._resumeSyncStart();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
errors.push(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (errors.length) throw errors[0];
|
|
54
|
+
}
|
|
55
|
+
exports.claimSourceIds = claimSourceIds;
|
|
56
|
+
exports.createSourceIdBindings = createSourceIdBindings;
|
|
57
|
+
exports.getPreparedSources = getPreparedSources;
|
|
58
|
+
exports.getSourceObjectToken = getSourceObjectToken;
|
|
59
|
+
exports.resumeSyncStarts = resumeSyncStarts;
|
|
60
|
+
//# sourceMappingURL=source-id-bindings.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-id-bindings.cjs","sources":["../../src/source-id-bindings.ts"],"sourcesContent":["import { BaseQueryBuilder, IR, isCollection } from '@tanstack/db'\nimport type { DbClient } from '@tanstack/db'\n\nconst sourceObjectTokens = new WeakMap<object, number>()\nlet nextSourceObjectToken = 0\n\nexport function getSourceObjectToken(source: object): number {\n let id = sourceObjectTokens.get(source)\n if (id === undefined) {\n id = ++nextSourceObjectToken\n sourceObjectTokens.set(source, id)\n }\n return id\n}\n\nexport function getPreparedSources(\n preparedValue: unknown,\n): Array<{ id: string }> {\n if (isCollection(preparedValue)) return [preparedValue]\n const query =\n preparedValue instanceof BaseQueryBuilder\n ? preparedValue\n : preparedValue &&\n typeof preparedValue === `object` &&\n `query` in preparedValue &&\n preparedValue.query instanceof BaseQueryBuilder\n ? preparedValue.query\n : undefined\n return query\n ? IR.collectCollectionSources(query._getQuery()).map(\n ({ collection }) => collection,\n )\n : []\n}\n\n/** The source object each Collection ID named while one hook is mounted. */\nexport type SourceIdBindings = {\n unscoped: Map<string, number>\n byClient: WeakMap<DbClient, Map<string, number>>\n}\n\nexport function createSourceIdBindings(): SourceIdBindings {\n return { unscoped: new Map(), byClient: new WeakMap() }\n}\n\n/**\n * Derived query identity names each source by its Collection ID. While a hook\n * is mounted, an ID must keep naming one source object in each client scope,\n * or the hook could reuse a live query that reads the old source. Claims the\n * prepared value's sources, or calls `release` and throws on a collision so a\n * rejected render keeps no sync-start deferral.\n */\nexport function claimSourceIds(\n bindings: SourceIdBindings,\n preparedValue: unknown,\n client: DbClient | undefined,\n hookName: string,\n release: () => void,\n): void {\n const prior = client ? bindings.byClient.get(client) : bindings.unscoped\n const seen = new Map<string, number>()\n for (const source of getPreparedSources(preparedValue)) {\n const token = getSourceObjectToken(source)\n const previous = seen.get(source.id) ?? prior?.get(source.id)\n if (previous !== undefined && previous !== token) {\n release()\n throw new Error(\n `[${hookName}] Source Collection \"${source.id}\" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,\n )\n }\n seen.set(source.id, token)\n }\n const claimed = prior ?? new Map<string, number>()\n for (const [id, token] of seen) claimed.set(id, token)\n if (client) bindings.byClient.set(client, claimed)\n}\n\n/**\n * Resumes every deferred sync start, then rethrows the first startup error, so\n * one failing source cannot keep a later shared source deferred.\n */\nexport function resumeSyncStarts(\n collections: Set<{ _resumeSyncStart: () => void }>,\n): void {\n const pending = Array.from(collections)\n collections.clear()\n const errors: Array<unknown> = []\n for (const collection of pending) {\n try {\n collection._resumeSyncStart()\n } catch (error) {\n errors.push(error)\n }\n }\n if (errors.length) throw errors[0]\n}\n"],"names":["isCollection","BaseQueryBuilder","IR"],"mappings":";;;AAGA,MAAM,yCAAyB,QAAA;AAC/B,IAAI,wBAAwB;AAErB,SAAS,qBAAqB,QAAwB;AAC3D,MAAI,KAAK,mBAAmB,IAAI,MAAM;AACtC,MAAI,OAAO,QAAW;AACpB,SAAK,EAAE;AACP,uBAAmB,IAAI,QAAQ,EAAE;AAAA,EACnC;AACA,SAAO;AACT;AAEO,SAAS,mBACd,eACuB;AACvB,MAAIA,gBAAa,aAAa,EAAG,QAAO,CAAC,aAAa;AACtD,QAAM,QACJ,yBAAyBC,GAAAA,mBACrB,gBACA,iBACE,OAAO,kBAAkB,YACzB,WAAW,iBACX,cAAc,iBAAiBA,sBAC/B,cAAc,QACd;AACR,SAAO,QACHC,GAAAA,GAAG,yBAAyB,MAAM,UAAA,CAAW,EAAE;AAAA,IAC7C,CAAC,EAAE,WAAA,MAAiB;AAAA,EAAA,IAEtB,CAAA;AACN;AAQO,SAAS,yBAA2C;AACzD,SAAO,EAAE,UAAU,oBAAI,IAAA,GAAO,UAAU,oBAAI,UAAQ;AACtD;AASO,SAAS,eACd,UACA,eACA,QACA,UACA,SACM;AACN,QAAM,QAAQ,SAAS,SAAS,SAAS,IAAI,MAAM,IAAI,SAAS;AAChE,QAAM,2BAAW,IAAA;AACjB,aAAW,UAAU,mBAAmB,aAAa,GAAG;AACtD,UAAM,QAAQ,qBAAqB,MAAM;AACzC,UAAM,WAAW,KAAK,IAAI,OAAO,EAAE,KAAK,OAAO,IAAI,OAAO,EAAE;AAC5D,QAAI,aAAa,UAAa,aAAa,OAAO;AAChD,cAAA;AACA,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,wBAAwB,OAAO,EAAE;AAAA,MAAA;AAAA,IAEjD;AACA,SAAK,IAAI,OAAO,IAAI,KAAK;AAAA,EAC3B;AACA,QAAM,UAAU,SAAS,oBAAI,IAAA;AAC7B,aAAW,CAAC,IAAI,KAAK,KAAK,KAAM,SAAQ,IAAI,IAAI,KAAK;AACrD,MAAI,OAAQ,UAAS,SAAS,IAAI,QAAQ,OAAO;AACnD;AAMO,SAAS,iBACd,aACM;AACN,QAAM,UAAU,MAAM,KAAK,WAAW;AACtC,cAAY,MAAA;AACZ,QAAM,SAAyB,CAAA;AAC/B,aAAW,cAAc,SAAS;AAChC,QAAI;AACF,iBAAW,iBAAA;AAAA,IACb,SAAS,OAAO;AACd,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AACA,MAAI,OAAO,OAAQ,OAAM,OAAO,CAAC;AACnC;;;;;;"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DbClient } from '@tanstack/db';
|
|
2
|
+
export declare function getSourceObjectToken(source: object): number;
|
|
3
|
+
export declare function getPreparedSources(preparedValue: unknown): Array<{
|
|
4
|
+
id: string;
|
|
5
|
+
}>;
|
|
6
|
+
/** The source object each Collection ID named while one hook is mounted. */
|
|
7
|
+
export type SourceIdBindings = {
|
|
8
|
+
unscoped: Map<string, number>;
|
|
9
|
+
byClient: WeakMap<DbClient, Map<string, number>>;
|
|
10
|
+
};
|
|
11
|
+
export declare function createSourceIdBindings(): SourceIdBindings;
|
|
12
|
+
/**
|
|
13
|
+
* Derived query identity names each source by its Collection ID. While a hook
|
|
14
|
+
* is mounted, an ID must keep naming one source object in each client scope,
|
|
15
|
+
* or the hook could reuse a live query that reads the old source. Claims the
|
|
16
|
+
* prepared value's sources, or calls `release` and throws on a collision so a
|
|
17
|
+
* rejected render keeps no sync-start deferral.
|
|
18
|
+
*/
|
|
19
|
+
export declare function claimSourceIds(bindings: SourceIdBindings, preparedValue: unknown, client: DbClient | undefined, hookName: string, release: () => void): void;
|
|
20
|
+
/**
|
|
21
|
+
* Resumes every deferred sync start, then rethrows the first startup error, so
|
|
22
|
+
* one failing source cannot keep a later shared source deferred.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resumeSyncStarts(collections: Set<{
|
|
25
|
+
_resumeSyncStart: () => void;
|
|
26
|
+
}>): void;
|
|
@@ -4,9 +4,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
4
4
|
const react = require("react");
|
|
5
5
|
const db = require("@tanstack/db");
|
|
6
6
|
const DbProvider = require("./DbProvider.cjs");
|
|
7
|
+
const sourceIdBindings = require("./source-id-bindings.cjs");
|
|
7
8
|
const useLiveQuery = require("./useLiveQuery.cjs");
|
|
8
9
|
const DEFAULT_GC_TIME_MS = 1;
|
|
9
10
|
const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
|
|
11
|
+
function queryIdentityMatches(comparison, sameClient, usesLegacyDeps) {
|
|
12
|
+
return sameClient && (usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual);
|
|
13
|
+
}
|
|
10
14
|
function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
11
15
|
if (`getNextPageParam` in config) {
|
|
12
16
|
throw new Error(
|
|
@@ -20,6 +24,8 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
|
20
24
|
const inputIsCollection = db.getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`;
|
|
21
25
|
const committedRef = react.useRef(null);
|
|
22
26
|
const committed = committedRef.current;
|
|
27
|
+
const renderedCollectionRef = react.useRef(null);
|
|
28
|
+
const sourceIdsRef = react.useRef(null);
|
|
23
29
|
const inputKind = inputIsCollection ? `collection` : `query`;
|
|
24
30
|
const derivedIdentityProfilerRef = react.useRef({
|
|
25
31
|
renderCount: 0,
|
|
@@ -54,6 +60,13 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
|
54
60
|
deferredCollections
|
|
55
61
|
);
|
|
56
62
|
preparedQueryValue = preparation.value;
|
|
63
|
+
sourceIdBindings.claimSourceIds(
|
|
64
|
+
sourceIdsRef.current ??= sourceIdBindings.createSourceIdBindings(),
|
|
65
|
+
preparation.value,
|
|
66
|
+
dbClient,
|
|
67
|
+
`useLiveInfiniteQuery`,
|
|
68
|
+
() => sourceIdBindings.resumeSyncStarts(deferredCollections)
|
|
69
|
+
);
|
|
57
70
|
if (preparation.status === `hashable`) {
|
|
58
71
|
identityDeps = preparation.identityDeps;
|
|
59
72
|
} else {
|
|
@@ -68,7 +81,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
|
68
81
|
identityDeps
|
|
69
82
|
);
|
|
70
83
|
const sameClient = committed?.client === dbClient;
|
|
71
|
-
const dependenciesChanged = !inputIsCollection &&
|
|
84
|
+
const dependenciesChanged = !inputIsCollection && !queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps);
|
|
72
85
|
const dependenciesStructurallyEqual = usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual;
|
|
73
86
|
const needsNewCollection = committed === null || committed.inputKind !== inputKind || inputIsCollection && committed.inputCollection !== queryFnOrCollection || dependenciesChanged;
|
|
74
87
|
const pageShapeChanged = committed === null || committed.pageSize !== pageSize || committed.initialPageParam !== initialPageParam;
|
|
@@ -77,6 +90,21 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
|
77
90
|
if (needsNewController) {
|
|
78
91
|
let collection = committed?.collection;
|
|
79
92
|
let warning = null;
|
|
93
|
+
let startInRender = false;
|
|
94
|
+
let suppliedCollection = false;
|
|
95
|
+
let createdCollection = false;
|
|
96
|
+
const canPreservePageCount = db.shouldPreserveLiveQueryWindowPageCount({
|
|
97
|
+
hasPreviousController: committed !== null,
|
|
98
|
+
previousInputKind: committed?.inputKind,
|
|
99
|
+
inputKind,
|
|
100
|
+
sameCollection: inputIsCollection && committed?.inputCollection === queryFnOrCollection,
|
|
101
|
+
dependenciesChanged,
|
|
102
|
+
dependenciesStructurallyEqual,
|
|
103
|
+
pageShapeChanged
|
|
104
|
+
});
|
|
105
|
+
const previousPageCount = committed ? Math.max(1, committed.controller.getSnapshot().pages.length) : 1;
|
|
106
|
+
const initialPageCount = canPreservePageCount ? previousPageCount : 1;
|
|
107
|
+
const requiredLimit = initialPageCount * pageSize + 1;
|
|
80
108
|
if (needsNewCollection) {
|
|
81
109
|
let inputValue = queryFnOrCollection;
|
|
82
110
|
if (!inputIsCollection) {
|
|
@@ -89,17 +117,33 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
|
89
117
|
}
|
|
90
118
|
inputValue = () => preparedQueryValue;
|
|
91
119
|
}
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
120
|
+
const rendered = renderedCollectionRef.current;
|
|
121
|
+
if (!inputIsCollection && rendered !== null && queryIdentityMatches(
|
|
122
|
+
db.compareLiveQueryWindowDependencies(
|
|
123
|
+
rendered.dependencies,
|
|
124
|
+
identityDeps
|
|
125
|
+
),
|
|
126
|
+
rendered.client === dbClient,
|
|
127
|
+
usesLegacyDeps
|
|
128
|
+
) && rendered.collection.status !== `cleaned-up` && rendered.collection.status !== `error` && db.liveQueryWindowMatches(rendered.collection, requiredLimit)) {
|
|
129
|
+
collection = rendered.collection;
|
|
130
|
+
for (const deferred of rendered.deferredCollections) {
|
|
131
|
+
deferredCollections.add(deferred);
|
|
132
|
+
}
|
|
95
133
|
} else {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
134
|
+
const input = db.resolveLiveQueryWindowInput(inputValue);
|
|
135
|
+
if (input.kind === `collection`) {
|
|
136
|
+
collection = input.collection;
|
|
137
|
+
suppliedCollection = true;
|
|
138
|
+
renderedCollectionRef.current = null;
|
|
139
|
+
} else {
|
|
140
|
+
collection = db.createLiveQueryCollection({
|
|
141
|
+
query: input.query.limit(requiredLimit).offset(0),
|
|
142
|
+
gcTime: DEFAULT_GC_TIME_MS
|
|
143
|
+
});
|
|
144
|
+
startInRender = true;
|
|
145
|
+
createdCollection = true;
|
|
146
|
+
}
|
|
103
147
|
}
|
|
104
148
|
}
|
|
105
149
|
if (!collection) {
|
|
@@ -110,17 +154,18 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
|
110
154
|
} else {
|
|
111
155
|
db.assertLiveQueryWindowManyResult(collection);
|
|
112
156
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
157
|
+
if (suppliedCollection) {
|
|
158
|
+
startInRender = db.liveQueryWindowMatches(collection, requiredLimit);
|
|
159
|
+
}
|
|
160
|
+
if (startInRender) collection.startSyncImmediate();
|
|
161
|
+
if (createdCollection) {
|
|
162
|
+
renderedCollectionRef.current = {
|
|
163
|
+
client: dbClient,
|
|
164
|
+
dependencies: [...identityDeps],
|
|
165
|
+
collection,
|
|
166
|
+
deferredCollections
|
|
167
|
+
};
|
|
168
|
+
}
|
|
124
169
|
renderState = {
|
|
125
170
|
inputKind,
|
|
126
171
|
inputCollection: inputIsCollection ? collection : null,
|
|
@@ -151,10 +196,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
|
|
|
151
196
|
currentRenderState.warned = true;
|
|
152
197
|
console.warn(currentRenderState.warning);
|
|
153
198
|
}
|
|
154
|
-
|
|
155
|
-
collection._resumeSyncStart();
|
|
156
|
-
}
|
|
157
|
-
currentRenderState.deferredCollections.clear();
|
|
199
|
+
sourceIdBindings.resumeSyncStarts(currentRenderState.deferredCollections);
|
|
158
200
|
return unsubscribe;
|
|
159
201
|
},
|
|
160
202
|
[controller, currentRenderState]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n assertLiveQueryWindowManyResult,\n compareLiveQueryWindowDependencies,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n fetchNextLiveQueryWindowPage,\n getLiveQueryWindowCollectionWarning,\n getLiveQueryWindowInputKind,\n normalizeLiveQueryWindowPageSize,\n resolveLiveQueryWindowInput,\n shouldPreserveLiveQueryWindowPageCount,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n prepareDerivedQuery,\n prepareQueryValue,\n warnDeprecatedDepsArray,\n warnUnhashableDerivedIdentity,\n} from './useLiveQuery'\nimport type {\n DerivedIdentityProfiler,\n LiveQueryKey,\n useLiveQuery,\n} from './useLiveQuery'\nimport type {\n Collection,\n CollectionImpl as CollectionImplType,\n CollectionStatus,\n Context,\n DbClient,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryPersistedStatus,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\n// Keep the generic parameter for existing typed config wrappers.\nexport type UseLiveInfiniteQueryConfig<_TContext extends Context> = {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n pageSize?: number\n /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = {\n data: Array<TResult>\n state: Map<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & NonSingleResult\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true\n pages: Array<Array<TResult>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\ntype InfiniteQueryRenderState = {\n inputKind: `collection` | `query`\n inputCollection: Collection<any, any, any> | null\n inputQuery: unknown\n client: DbClient | undefined\n identityMode: `collection` | `queryKey` | `legacyDeps` | `derived`\n dependencies: Array<unknown> | null\n pageSize: number\n initialPageParam: number\n collection: Collection<any, any, any>\n controller: LiveQueryWindowController<any, any>\n warning: string | null\n warned: boolean\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n}\n\n/**\n * Create an infinite query using a query function with live updates.\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and an optional initial page label\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext> {\n if (`getNextPageParam` in config) {\n throw new Error(\n `getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.`,\n )\n }\n const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize)\n const initialPageParam = config.initialPageParam ?? 0\n const contextDbClient = useOptionalDbClient()\n const dbClient = config.client ?? contextDbClient\n\n const inputIsCollection =\n getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`\n\n const committedRef = useRef<InfiniteQueryRenderState | null>(null)\n const committed = committedRef.current\n const inputKind = inputIsCollection ? `collection` : `query`\n const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const legacyUnhashableIdentityRef = useRef<Array<unknown>>([\n `legacy-unhashable`,\n ])\n const deferredCollections = new Set<\n CollectionImplType<any, string | number, any, any, any>\n >()\n\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown> = []\n let identityMode: InfiniteQueryRenderState[`identityMode`] = `collection`\n\n if (!inputIsCollection) {\n if (config.queryKey !== undefined) {\n identityMode = `queryKey`\n identityDeps = config.queryKey\n } else if (deps !== undefined) {\n identityMode = `legacyDeps`\n identityDeps = deps\n warnDeprecatedDepsArray(`useLiveInfiniteQuery`)\n } else if (\n committed?.identityMode === `derived` &&\n committed.inputQuery === queryFnOrCollection &&\n committed.client === dbClient\n ) {\n identityMode = `derived`\n identityDeps = committed.dependencies ?? []\n } else {\n identityMode = `derived`\n const preparation = prepareDerivedQuery(\n queryFnOrCollection,\n dbClient,\n derivedIdentityProfilerRef.current,\n deferredCollections,\n )\n preparedQueryValue = preparation.value\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = legacyUnhashableIdentityRef.current\n }\n }\n }\n\n const usesLegacyDeps =\n !inputIsCollection && config.queryKey === undefined && deps !== undefined\n const dependencyComparison = compareLiveQueryWindowDependencies(\n committed?.dependencies,\n identityDeps,\n )\n const sameClient = committed?.client === dbClient\n const dependenciesChanged =\n !inputIsCollection &&\n (!sameClient ||\n (usesLegacyDeps\n ? dependencyComparison.changed\n : !dependencyComparison.structurallyEqual))\n const dependenciesStructurallyEqual =\n usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual\n const needsNewCollection =\n committed === null ||\n committed.inputKind !== inputKind ||\n (inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n committed === null ||\n committed.pageSize !== pageSize ||\n committed.initialPageParam !== initialPageParam\n const needsNewController =\n committed === null || needsNewCollection || pageShapeChanged\n\n let renderState = committed\n if (needsNewController) {\n let collection = committed?.collection\n let warning: string | null = null\n\n if (needsNewCollection) {\n let inputValue = queryFnOrCollection\n if (!inputIsCollection) {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n queryFnOrCollection,\n dbClient,\n deferredCollections,\n )\n }\n inputValue = () => preparedQueryValue\n }\n const input = resolveLiveQueryWindowInput<TContext>(inputValue)\n if (input.kind === `collection`) {\n collection = input.collection\n } else {\n // Wrap the query with the first page's peek-ahead window; the controller\n // grows the limit from here via setWindow.\n collection = createLiveQueryCollection({\n query: input.query.limit(pageSize + 1).offset(0),\n // Construction happens during render. Synchronization starts only when\n // useSyncExternalStore commits the controller subscription.\n startSync: false,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n }\n }\n\n if (!collection) {\n throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`)\n }\n\n if (inputIsCollection) {\n warning =\n getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null\n } else {\n assertLiveQueryWindowManyResult(collection)\n }\n\n const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({\n hasPreviousController: committed !== null,\n previousInputKind: committed?.inputKind,\n inputKind,\n sameCollection:\n inputIsCollection && committed?.inputCollection === collection,\n dependenciesChanged,\n dependenciesStructurallyEqual,\n pageShapeChanged,\n })\n const previousPageCount = committed\n ? Math.max(1, committed.controller.getSnapshot().pages.length)\n : 1\n const initialPageCount = canPreservePageCount ? previousPageCount : 1\n renderState = {\n inputKind,\n inputCollection: inputIsCollection ? collection : null,\n inputQuery: inputIsCollection ? null : queryFnOrCollection,\n client: dbClient,\n identityMode,\n dependencies: inputIsCollection ? null : [...identityDeps],\n pageSize,\n initialPageParam,\n collection,\n controller: createLiveQueryWindowController(collection, {\n pageSize,\n initialPageParam,\n initialPageCount,\n }),\n warning,\n warned: false,\n deferredCollections,\n }\n }\n const currentRenderState = renderState!\n const controller = currentRenderState.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = currentRenderState\n if (currentRenderState.warning && !currentRenderState.warned) {\n currentRenderState.warned = true\n console.warn(currentRenderState.warning)\n }\n for (const collection of currentRenderState.deferredCollections) {\n collection._resumeSyncStart()\n }\n currentRenderState.deferredCollections.clear()\n return unsubscribe\n },\n [controller, currentRenderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n get state() {\n return snapshot.state as EnabledLiveQueryReturn<TContext>[`state`]\n },\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n persistedStatus: snapshot.persistedStatus,\n isPersistedReady: snapshot.isPersistedReady,\n persistedError: snapshot.persistedError,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled: snapshot.isEnabled,\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":["warnDeprecatedDepsArray","prepareDerivedQuery","warnUnhashableDerivedIdentity","compareLiveQueryWindowDependencies","prepareQueryValue","assertLiveQueryWindowManyResult","useCallback"],"mappings":";;;;;;;AA0CA;AACA;AA4GO;AAKL;AACE;AAAU;AACR;AAAA;AAGJ;AACA;AACA;AACA;AAEA;AAGA;AACA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAA2D;AACzD;AAEF;AAIA;AAEA;AACA;AAEA;AACE;AACE;AACA;AAAsB;AAEtB;AACA;AACAA;AAA8C;AAM9C;AACA;AAAyC;AAEzC;AACA;AAAoBC;AAClB;AACA;AAC2B;AAC3B;AAEF;AACA;AACE;AAA2B;AAE3BC;AACA;AAA2C;AAC7C;AACF;AAGF;AAEA;AAA6BC;AAChB;AACX;AAEF;AACA;AAMA;AAEA;AAKA;AAIA;AAGA;AACA;AACE;AACA;AAEA;AACE;AACA;AACE;AACE;AAAqBC;AACnB;AACA;AACA;AAAA;AAGJ;AAAmB;AAErB;AACA;AACE;AAAmB;AAInB;AAAuC;AACU;AAAA;AAAA;AAGpC;AACH;AACT;AACH;AAGF;AACE;AAAsE;AAGxE;AACE;AACmE;AAEnEC;AAA0C;AAG5C;AAAoE;AAC7B;AACP;AAC9B;AAEsD;AACtD;AACA;AACA;AAEF;AAGA;AACA;AAAc;AACZ;AACkD;AACX;AAC/B;AACR;AACyD;AACzD;AACA;AACA;AACwD;AACtD;AACA;AACA;AACD;AACD;AACQ;AACR;AAAA;AAGJ;AACA;AAEA;AAAkBC;AAEd;AACA;AACA;AACE;AACA;AAAuC;AAEzC;AACE;AAAW;AAEb;AACA;AAAO;AACT;AAC+B;AAEjC;AACA;AAEA;AAAsBA;AACyB;AAClC;AAGb;AAAO;AACU;AAEb;AAAgB;AAClB;AACiB;AACG;AACF;AACQ;AACC;AACF;AACR;AACC;AACI;AAEX;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACb;AAEpB;;"}
|
|
1
|
+
{"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n assertLiveQueryWindowManyResult,\n compareLiveQueryWindowDependencies,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n fetchNextLiveQueryWindowPage,\n getLiveQueryWindowCollectionWarning,\n getLiveQueryWindowInputKind,\n liveQueryWindowMatches,\n normalizeLiveQueryWindowPageSize,\n resolveLiveQueryWindowInput,\n shouldPreserveLiveQueryWindowPageCount,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport {\n prepareDerivedQuery,\n prepareQueryValue,\n warnDeprecatedDepsArray,\n warnUnhashableDerivedIdentity,\n} from './useLiveQuery'\nimport type { SourceIdBindings } from './source-id-bindings'\nimport type {\n DerivedIdentityProfiler,\n LiveQueryKey,\n useLiveQuery,\n} from './useLiveQuery'\nimport type {\n Collection,\n CollectionImpl as CollectionImplType,\n CollectionStatus,\n Context,\n DbClient,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryPersistedStatus,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\n// Keep the generic parameter for existing typed config wrappers.\nexport type UseLiveInfiniteQueryConfig<_TContext extends Context> = {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n pageSize?: number\n /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = {\n data: Array<TResult>\n state: Map<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & NonSingleResult\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true\n pages: Array<Array<TResult>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\ntype InfiniteQueryRenderState = {\n inputKind: `collection` | `query`\n inputCollection: Collection<any, any, any> | null\n inputQuery: unknown\n client: DbClient | undefined\n identityMode: `collection` | `queryKey` | `legacyDeps` | `derived`\n dependencies: Array<unknown> | null\n pageSize: number\n initialPageParam: number\n collection: Collection<any, any, any>\n controller: LiveQueryWindowController<any, any>\n warning: string | null\n warned: boolean\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n}\n\ntype RenderedWindowCollection = {\n client: DbClient | undefined\n dependencies: Array<unknown>\n collection: Collection<any, any, any>\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n}\n\n/**\n * Whether a recorded query identity matches the current one. Legacy deps\n * compare by reference. Derived identities and query keys compare by structure.\n */\nfunction queryIdentityMatches(\n comparison: { changed: boolean; structurallyEqual: boolean },\n sameClient: boolean,\n usesLegacyDeps: boolean,\n): boolean {\n return (\n sameClient &&\n (usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual)\n )\n}\n\n/**\n * Create an infinite query using a query function with live updates.\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and an optional initial page label\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext> {\n if (`getNextPageParam` in config) {\n throw new Error(\n `getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.`,\n )\n }\n const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize)\n const initialPageParam = config.initialPageParam ?? 0\n const contextDbClient = useOptionalDbClient()\n const dbClient = config.client ?? contextDbClient\n\n const inputIsCollection =\n getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`\n\n const committedRef = useRef<InfiniteQueryRenderState | null>(null)\n const committed = committedRef.current\n // Like useLiveQuery's instance memo, keep the window collection built by the\n // latest render. A second render before commit, such as a StrictMode double\n // render, reuses it instead of starting another one. Only the collection is\n // kept. Each render still builds its own controller and page count from the\n // committed state, which an uncommitted render never changes.\n const renderedCollectionRef = useRef<RenderedWindowCollection | null>(null)\n const sourceIdsRef = useRef<SourceIdBindings | null>(null)\n const inputKind = inputIsCollection ? `collection` : `query`\n const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const legacyUnhashableIdentityRef = useRef<Array<unknown>>([\n `legacy-unhashable`,\n ])\n const deferredCollections = new Set<\n CollectionImplType<any, string | number, any, any, any>\n >()\n\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown> = []\n let identityMode: InfiniteQueryRenderState[`identityMode`] = `collection`\n\n if (!inputIsCollection) {\n if (config.queryKey !== undefined) {\n identityMode = `queryKey`\n identityDeps = config.queryKey\n } else if (deps !== undefined) {\n identityMode = `legacyDeps`\n identityDeps = deps\n warnDeprecatedDepsArray(`useLiveInfiniteQuery`)\n } else if (\n committed?.identityMode === `derived` &&\n committed.inputQuery === queryFnOrCollection &&\n committed.client === dbClient\n ) {\n identityMode = `derived`\n identityDeps = committed.dependencies ?? []\n } else {\n identityMode = `derived`\n const preparation = prepareDerivedQuery(\n queryFnOrCollection,\n dbClient,\n derivedIdentityProfilerRef.current,\n deferredCollections,\n )\n preparedQueryValue = preparation.value\n claimSourceIds(\n (sourceIdsRef.current ??= createSourceIdBindings()),\n preparation.value,\n dbClient,\n `useLiveInfiniteQuery`,\n () => resumeSyncStarts(deferredCollections),\n )\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = legacyUnhashableIdentityRef.current\n }\n }\n }\n\n const usesLegacyDeps =\n !inputIsCollection && config.queryKey === undefined && deps !== undefined\n const dependencyComparison = compareLiveQueryWindowDependencies(\n committed?.dependencies,\n identityDeps,\n )\n const sameClient = committed?.client === dbClient\n const dependenciesChanged =\n !inputIsCollection &&\n !queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps)\n const dependenciesStructurallyEqual =\n usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual\n const needsNewCollection =\n committed === null ||\n committed.inputKind !== inputKind ||\n (inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n committed === null ||\n committed.pageSize !== pageSize ||\n committed.initialPageParam !== initialPageParam\n const needsNewController =\n committed === null || needsNewCollection || pageShapeChanged\n\n let renderState = committed\n if (needsNewController) {\n let collection = committed?.collection\n let warning: string | null = null\n let startInRender = false\n let suppliedCollection = false\n let createdCollection = false\n\n const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({\n hasPreviousController: committed !== null,\n previousInputKind: committed?.inputKind,\n inputKind,\n sameCollection:\n inputIsCollection && committed?.inputCollection === queryFnOrCollection,\n dependenciesChanged,\n dependenciesStructurallyEqual,\n pageShapeChanged,\n })\n const previousPageCount = committed\n ? Math.max(1, committed.controller.getSnapshot().pages.length)\n : 1\n const initialPageCount = canPreservePageCount ? previousPageCount : 1\n // The peek-ahead window for every retained page.\n const requiredLimit = initialPageCount * pageSize + 1\n\n if (needsNewCollection) {\n let inputValue = queryFnOrCollection\n if (!inputIsCollection) {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n queryFnOrCollection,\n dbClient,\n deferredCollections,\n )\n }\n inputValue = () => preparedQueryValue\n }\n const rendered = renderedCollectionRef.current\n if (\n !inputIsCollection &&\n rendered !== null &&\n queryIdentityMatches(\n compareLiveQueryWindowDependencies(\n rendered.dependencies,\n identityDeps,\n ),\n rendered.client === dbClient,\n usesLegacyDeps,\n ) &&\n rendered.collection.status !== `cleaned-up` &&\n rendered.collection.status !== `error` &&\n liveQueryWindowMatches(rendered.collection, requiredLimit)\n ) {\n // An earlier render built this collection and nothing has committed\n // since. Its window is exactly the retained pages, so its first\n // rows are correct. Sources it deferred still resume at commit. A\n // collection in terminal error, such as after its source restarted,\n // cannot serve the query again, so a new one replaces it.\n collection = rendered.collection\n for (const deferred of rendered.deferredCollections) {\n deferredCollections.add(deferred)\n }\n } else {\n const input = resolveLiveQueryWindowInput<TContext>(inputValue)\n if (input.kind === `collection`) {\n collection = input.collection\n suppliedCollection = true\n // A supplied collection is never reused, so release the last one.\n renderedCollectionRef.current = null\n } else {\n // Wrap the query with the peek-ahead window for every retained page,\n // so a collection that starts syncing now never publishes fewer rows\n // than the controller's pages. The controller grows the limit via\n // setWindow.\n collection = createLiveQueryCollection({\n query: input.query.limit(requiredLimit).offset(0),\n gcTime: DEFAULT_GC_TIME_MS,\n })\n startInRender = true\n createdCollection = true\n }\n }\n }\n\n if (!collection) {\n throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`)\n }\n\n if (inputIsCollection) {\n warning =\n getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null\n } else {\n assertLiveQueryWindowManyResult(collection)\n }\n // Like useLiveQuery, start sync during render once the input is valid. A\n // render reads only rows the sources already hold, such as a preloaded\n // collection's, and sends no request until the subscription commits. GC\n // reclaims a render that never commits. A supplied window that the\n // controller must still adjust waits for the subscription.\n if (suppliedCollection) {\n startInRender = liveQueryWindowMatches(collection, requiredLimit)\n }\n if (startInRender) collection.startSyncImmediate()\n // Keep a created collection for the next render only once it validated and\n // started, so a retry after a startup error builds a new one and throws.\n if (createdCollection) {\n renderedCollectionRef.current = {\n client: dbClient,\n dependencies: [...identityDeps],\n collection,\n deferredCollections,\n }\n }\n\n renderState = {\n inputKind,\n inputCollection: inputIsCollection ? collection : null,\n inputQuery: inputIsCollection ? null : queryFnOrCollection,\n client: dbClient,\n identityMode,\n dependencies: inputIsCollection ? null : [...identityDeps],\n pageSize,\n initialPageParam,\n collection,\n controller: createLiveQueryWindowController(collection, {\n pageSize,\n initialPageParam,\n initialPageCount,\n }),\n warning,\n warned: false,\n deferredCollections,\n }\n }\n const currentRenderState = renderState!\n const controller = currentRenderState.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = currentRenderState\n if (currentRenderState.warning && !currentRenderState.warned) {\n currentRenderState.warned = true\n console.warn(currentRenderState.warning)\n }\n resumeSyncStarts(currentRenderState.deferredCollections)\n return unsubscribe\n },\n [controller, currentRenderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n get state() {\n return snapshot.state as EnabledLiveQueryReturn<TContext>[`state`]\n },\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n persistedStatus: snapshot.persistedStatus,\n isPersistedReady: snapshot.isPersistedReady,\n persistedError: snapshot.persistedError,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled: snapshot.isEnabled,\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":["warnDeprecatedDepsArray","prepareDerivedQuery","claimSourceIds","warnUnhashableDerivedIdentity","compareLiveQueryWindowDependencies","prepareQueryValue","assertLiveQueryWindowManyResult","useCallback","resumeSyncStarts"],"mappings":";;;;;;;;AAiDA;AACA;AA2FA;AAKE;AAIF;AAgCO;AAKL;AACE;AAAU;AACR;AAAA;AAGJ;AACA;AACA;AACA;AAEA;AAGA;AACA;AAMA;AACA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAA2D;AACzD;AAEF;AAIA;AAEA;AACA;AAEA;AACE;AACE;AACA;AAAsB;AAEtB;AACA;AACAA;AAA8C;AAM9C;AACA;AAAyC;AAEzC;AACA;AAAoBC;AAClB;AACA;AAC2B;AAC3B;AAEF;AACAC;AAAAA;AAC4B;AACd;AACZ;AACA;AAC0C;AAE5C;AACE;AAA2B;AAE3BC;AACA;AAA2C;AAC7C;AACF;AAGF;AAEA;AAA6BC;AAChB;AACX;AAEF;AACA;AAGA;AAEA;AAKA;AAIA;AAGA;AACA;AACE;AACA;AACA;AACA;AACA;AAEA;AAAoE;AAC7B;AACP;AAC9B;AAEsD;AACtD;AACA;AACA;AAEF;AAGA;AAEA;AAEA;AACE;AACA;AACE;AACE;AAAqBC;AACnB;AACA;AACA;AAAA;AAGJ;AAAmB;AAErB;AACA;AAGE;AACED;AACW;AACT;AAAA;AAEkB;AACpB;AAWF;AACA;AACE;AAAgC;AAClC;AAEA;AACA;AACE;AACA;AAEA;AAAgC;AAMhC;AAAuC;AACW;AACxC;AAEV;AACA;AAAoB;AACtB;AACF;AAGF;AACE;AAAsE;AAGxE;AACE;AACmE;AAEnEE;AAA0C;AAO5C;AACE;AAAgE;AAElE;AAGA;AACE;AAAgC;AACtB;AACsB;AAC9B;AACA;AAAA;AAIJ;AAAc;AACZ;AACkD;AACX;AAC/B;AACR;AACyD;AACzD;AACA;AACA;AACwD;AACtD;AACA;AACA;AACD;AACD;AACQ;AACR;AAAA;AAGJ;AACA;AAEA;AAAkBC;AAEd;AACA;AACA;AACE;AACA;AAAuC;AAEzCC;AACA;AAAO;AACT;AAC+B;AAEjC;AACA;AAEA;AAAsBD;AACyB;AAClC;AAGb;AAAO;AACU;AAEb;AAAgB;AAClB;AACiB;AACG;AACF;AACQ;AACC;AACF;AACR;AACC;AACI;AAEX;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACb;AAEpB;;"}
|
|
@@ -6,6 +6,7 @@ const db = require("@tanstack/db");
|
|
|
6
6
|
const DbProvider = require("./DbProvider.cjs");
|
|
7
7
|
const liveQueryInternals = require("./live-query-internals.cjs");
|
|
8
8
|
const development = require("./development.cjs");
|
|
9
|
+
const sourceIdBindings = require("./source-id-bindings.cjs");
|
|
9
10
|
const DEFAULT_GC_TIME_MS = 1;
|
|
10
11
|
const DEFAULT_SUSPENSE_GC_TIME_MS = 5e3;
|
|
11
12
|
const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16;
|
|
@@ -17,25 +18,8 @@ const warnedUnhashableIdentityCallsites = /* @__PURE__ */ new Set();
|
|
|
17
18
|
const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
|
|
18
19
|
const unscopedSuspenseCollections = /* @__PURE__ */ new Map();
|
|
19
20
|
const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap();
|
|
20
|
-
const sourceObjectTokens = /* @__PURE__ */ new WeakMap();
|
|
21
|
-
let nextSourceObjectToken = 0;
|
|
22
|
-
function getSourceObjectToken(source) {
|
|
23
|
-
let id = sourceObjectTokens.get(source);
|
|
24
|
-
if (id === void 0) {
|
|
25
|
-
id = ++nextSourceObjectToken;
|
|
26
|
-
sourceObjectTokens.set(source, id);
|
|
27
|
-
}
|
|
28
|
-
return id;
|
|
29
|
-
}
|
|
30
|
-
function getPreparedSources(preparedValue) {
|
|
31
|
-
if (db.isCollection(preparedValue)) return [preparedValue];
|
|
32
|
-
const query = preparedValue instanceof db.BaseQueryBuilder ? preparedValue : preparedValue && typeof preparedValue === `object` && `query` in preparedValue && preparedValue.query instanceof db.BaseQueryBuilder ? preparedValue.query : void 0;
|
|
33
|
-
return query ? db.IR.collectCollectionSources(query._getQuery()).map(
|
|
34
|
-
({ collection }) => collection
|
|
35
|
-
) : [];
|
|
36
|
-
}
|
|
37
21
|
function getSourceQualifiedSuspenseKey(preparedValue, queryHash) {
|
|
38
|
-
const sourceIds = getPreparedSources(preparedValue).map(getSourceObjectToken);
|
|
22
|
+
const sourceIds = sourceIdBindings.getPreparedSources(preparedValue).map(sourceIdBindings.getSourceObjectToken);
|
|
39
23
|
if (sourceIds.length === 0) return queryHash;
|
|
40
24
|
return `${sourceIds.join(`,`)}:${queryHash}`;
|
|
41
25
|
}
|
|
@@ -171,10 +155,7 @@ function createHookInstance(dbClient) {
|
|
|
171
155
|
warned: false
|
|
172
156
|
},
|
|
173
157
|
deferredCollections: /* @__PURE__ */ new Set(),
|
|
174
|
-
sourceIds:
|
|
175
|
-
unscoped: /* @__PURE__ */ new Map(),
|
|
176
|
-
byClient: /* @__PURE__ */ new WeakMap()
|
|
177
|
-
},
|
|
158
|
+
sourceIds: sourceIdBindings.createSourceIdBindings(),
|
|
178
159
|
observer: null,
|
|
179
160
|
queryHash: void 0,
|
|
180
161
|
suspenseKey: void 0,
|
|
@@ -189,19 +170,7 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
|
|
|
189
170
|
const resolvedDeps = deps ?? [];
|
|
190
171
|
const instanceRef = react.useRef(null);
|
|
191
172
|
const instance = instanceRef.current ??= createHookInstance(dbClient);
|
|
192
|
-
const resumeDeferredCollections = () =>
|
|
193
|
-
const collections = Array.from(instance.deferredCollections);
|
|
194
|
-
instance.deferredCollections.clear();
|
|
195
|
-
const errors = [];
|
|
196
|
-
for (const collection of collections) {
|
|
197
|
-
try {
|
|
198
|
-
collection._resumeSyncStart();
|
|
199
|
-
} catch (error) {
|
|
200
|
-
errors.push(error);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (errors.length) throw errors[0];
|
|
204
|
-
};
|
|
173
|
+
const resumeDeferredCollections = () => sourceIdBindings.resumeSyncStarts(instance.deferredCollections);
|
|
205
174
|
const queryKey = !inputIsCollection ? getExplicitQueryKey(configOrQueryOrCollection) : void 0;
|
|
206
175
|
let preparedQueryValue = unpreparedQueryValue;
|
|
207
176
|
let identityDeps;
|
|
@@ -266,22 +235,13 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
|
|
|
266
235
|
warnDeprecatedDepsArray();
|
|
267
236
|
}
|
|
268
237
|
if (queryKey === void 0 && deps === void 0 && preparedQueryValue !== unpreparedQueryValue) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
throw new Error(
|
|
277
|
-
`[useLiveQuery] Source Collection "${source.id}" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
seen.set(source.id, token);
|
|
281
|
-
}
|
|
282
|
-
const bindings = prior ?? /* @__PURE__ */ new Map();
|
|
283
|
-
for (const [id, token] of seen) bindings.set(id, token);
|
|
284
|
-
if (dbClient) instance.sourceIds.byClient.set(dbClient, bindings);
|
|
238
|
+
sourceIdBindings.claimSourceIds(
|
|
239
|
+
instance.sourceIds,
|
|
240
|
+
preparedQueryValue,
|
|
241
|
+
dbClient,
|
|
242
|
+
`useLiveQuery`,
|
|
243
|
+
resumeDeferredCollections
|
|
244
|
+
);
|
|
285
245
|
}
|
|
286
246
|
const canReuseSuspenseKey = forSuspense && !inputIsCollection && queryHash !== void 0 && instance.collection !== null && instance.client === dbClient && instance.queryHash === queryHash && instance.suspenseKey !== void 0;
|
|
287
247
|
if (forSuspense && !inputIsCollection && queryHash && !canReuseSuspenseKey && preparedQueryValue === unpreparedQueryValue) {
|
|
@@ -327,7 +287,9 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
|
|
|
327
287
|
instance.collection = db.resolveLiveQueryValue(preparedQueryValue, {
|
|
328
288
|
gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS,
|
|
329
289
|
// Hydration and Suspense key the live-query Collection by identity.
|
|
330
|
-
pool: !forSuspense && !dbClient
|
|
290
|
+
pool: !forSuspense && !dbClient,
|
|
291
|
+
// Without a DbClient, Suspense starts it through a preload below.
|
|
292
|
+
startSync: !forSuspense || dbClient !== void 0
|
|
331
293
|
});
|
|
332
294
|
if (suspenseCollections && suspenseKey && instance.collection) {
|
|
333
295
|
const collection = instance.collection;
|
|
@@ -376,6 +338,10 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
|
|
|
376
338
|
});
|
|
377
339
|
}
|
|
378
340
|
const observer = instance.observer;
|
|
341
|
+
if (forSuspense && !dbClient && (instance.collection?.status === `idle` || instance.collection?.status === `cleaned-up`)) {
|
|
342
|
+
observer.preloadForInitialRender().catch(() => {
|
|
343
|
+
});
|
|
344
|
+
}
|
|
379
345
|
if (!instance.subscribe || needsNewCollection) {
|
|
380
346
|
instance.subscribe = (onStoreChange) => {
|
|
381
347
|
const unsubscribe = observer.subscribe(onStoreChange);
|