@tanstack/react-db 0.5.11 → 1.0.0-rc.1

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.
Files changed (40) hide show
  1. package/dist/cjs/live-query-internals.cjs.map +1 -1
  2. package/dist/cjs/live-query-internals.d.cts +3 -1
  3. package/dist/cjs/source-id-bindings.cjs +9 -42
  4. package/dist/cjs/source-id-bindings.cjs.map +1 -1
  5. package/dist/cjs/source-id-bindings.d.cts +6 -17
  6. package/dist/cjs/useLiveInfiniteQuery.cjs +93 -173
  7. package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
  8. package/dist/cjs/useLiveInfiniteQuery.d.cts +3 -14
  9. package/dist/cjs/useLiveQuery.cjs +187 -224
  10. package/dist/cjs/useLiveQuery.cjs.map +1 -1
  11. package/dist/cjs/useLiveQuery.d.cts +37 -136
  12. package/dist/cjs/useLiveSuspenseQuery.cjs +71 -64
  13. package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
  14. package/dist/cjs/useLiveSuspenseQuery.d.cts +10 -9
  15. package/dist/cjs/usePacedMutations.cjs.map +1 -1
  16. package/dist/cjs/usePacedMutations.d.cts +1 -1
  17. package/dist/esm/live-query-internals.d.ts +3 -1
  18. package/dist/esm/live-query-internals.js.map +1 -1
  19. package/dist/esm/source-id-bindings.d.ts +6 -17
  20. package/dist/esm/source-id-bindings.js +9 -40
  21. package/dist/esm/source-id-bindings.js.map +1 -1
  22. package/dist/esm/useLiveInfiniteQuery.d.ts +3 -14
  23. package/dist/esm/useLiveInfiniteQuery.js +95 -175
  24. package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
  25. package/dist/esm/useLiveQuery.d.ts +37 -136
  26. package/dist/esm/useLiveQuery.js +189 -226
  27. package/dist/esm/useLiveQuery.js.map +1 -1
  28. package/dist/esm/useLiveSuspenseQuery.d.ts +10 -9
  29. package/dist/esm/useLiveSuspenseQuery.js +71 -64
  30. package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
  31. package/dist/esm/usePacedMutations.d.ts +1 -1
  32. package/dist/esm/usePacedMutations.js.map +1 -1
  33. package/package.json +3 -3
  34. package/skills/react-db/SKILL.md +24 -15
  35. package/src/live-query-internals.ts +3 -0
  36. package/src/source-id-bindings.ts +16 -62
  37. package/src/useLiveInfiniteQuery.ts +138 -296
  38. package/src/useLiveQuery.ts +329 -495
  39. package/src/useLiveSuspenseQuery.ts +122 -132
  40. package/src/usePacedMutations.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"live-query-internals.cjs","sources":["../../src/live-query-internals.ts"],"sourcesContent":["import type {\n DbClient,\n LiveQueryObserver,\n UnhashableQueryIRError,\n} from '@tanstack/db'\n\nconst liveQueryResultInfo = Symbol(`liveQueryResultInfo`)\n\nexport type LiveQueryResultInfo = {\n client: DbClient | undefined\n queryHash: string | undefined\n identityError: UnhashableQueryIRError | undefined\n observer: LiveQueryObserver<object, string | number>\n}\n\ntype ResultWithInfo = {\n [liveQueryResultInfo]?: LiveQueryResultInfo\n}\n\nexport function setLiveQueryResultInfo(\n result: object,\n info: LiveQueryResultInfo,\n): void {\n Object.defineProperty(result, liveQueryResultInfo, {\n configurable: true,\n value: info,\n })\n}\n\nexport function getLiveQueryResultInfo(result: object): LiveQueryResultInfo {\n const info = (result as ResultWithInfo)[liveQueryResultInfo]\n if (!info) {\n throw new Error(`Missing internal live query result information.`)\n }\n return info\n}\n"],"names":[],"mappings":";;AAMA,MAAM,6CAA6B,qBAAqB;AAajD,SAAS,uBACd,QACA,MACM;AACN,SAAO,eAAe,QAAQ,qBAAqB;AAAA,IACjD,cAAc;AAAA,IACd,OAAO;AAAA,EAAA,CACR;AACH;AAEO,SAAS,uBAAuB,QAAqC;AAC1E,QAAM,OAAQ,OAA0B,mBAAmB;AAC3D,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,SAAO;AACT;;;"}
1
+ {"version":3,"file":"live-query-internals.cjs","sources":["../../src/live-query-internals.ts"],"sourcesContent":["import type {\n DbClient,\n DeferredLiveQueryCollections,\n LiveQueryObserver,\n UnhashableQueryIRError,\n} from '@tanstack/db'\n\nconst liveQueryResultInfo = Symbol(`liveQueryResultInfo`)\n\nexport type LiveQueryResultInfo = {\n client: DbClient | undefined\n queryHash: string | undefined\n identityError: UnhashableQueryIRError | undefined\n observer: LiveQueryObserver<object, string | number>\n /** Sync starts this render deferred, released if the render throws. */\n deferredCollections: DeferredLiveQueryCollections\n}\n\ntype ResultWithInfo = {\n [liveQueryResultInfo]?: LiveQueryResultInfo\n}\n\nexport function setLiveQueryResultInfo(\n result: object,\n info: LiveQueryResultInfo,\n): void {\n Object.defineProperty(result, liveQueryResultInfo, {\n configurable: true,\n value: info,\n })\n}\n\nexport function getLiveQueryResultInfo(result: object): LiveQueryResultInfo {\n const info = (result as ResultWithInfo)[liveQueryResultInfo]\n if (!info) {\n throw new Error(`Missing internal live query result information.`)\n }\n return info\n}\n"],"names":[],"mappings":";;AAOA,MAAM,6CAA6B,qBAAqB;AAejD,SAAS,uBACd,QACA,MACM;AACN,SAAO,eAAe,QAAQ,qBAAqB;AAAA,IACjD,cAAc;AAAA,IACd,OAAO;AAAA,EAAA,CACR;AACH;AAEO,SAAS,uBAAuB,QAAqC;AAC1E,QAAM,OAAQ,OAA0B,mBAAmB;AAC3D,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,SAAO;AACT;;;"}
@@ -1,9 +1,11 @@
1
- import { DbClient, LiveQueryObserver, UnhashableQueryIRError } from '@tanstack/db';
1
+ import { DbClient, DeferredLiveQueryCollections, LiveQueryObserver, UnhashableQueryIRError } from '@tanstack/db';
2
2
  export type LiveQueryResultInfo = {
3
3
  client: DbClient | undefined;
4
4
  queryHash: string | undefined;
5
5
  identityError: UnhashableQueryIRError | undefined;
6
6
  observer: LiveQueryObserver<object, string | number>;
7
+ /** Sync starts this render deferred, released if the render throws. */
8
+ deferredCollections: DeferredLiveQueryCollections;
7
9
  };
8
10
  export declare function setLiveQueryResultInfo(result: object, info: LiveQueryResultInfo): void;
9
11
  export declare function getLiveQueryResultInfo(result: object): LiveQueryResultInfo;
@@ -1,53 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
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;
4
+ function getUnboundDescriptorReporter(client, hookName) {
5
+ if (client) return void 0;
6
+ return (alias) => {
7
+ throw new Error(
8
+ `[${hookName}] Collection descriptor "${alias}" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`
9
+ );
10
+ };
13
11
  }
14
12
  function getPreparedSources(preparedValue, client, hookName) {
15
13
  return db.getPreparedLiveQuerySources(
16
14
  preparedValue,
17
- !client && hookName ? (alias) => {
18
- throw new Error(
19
- `[${hookName}] Collection descriptor "${alias}" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`
20
- );
21
- } : void 0
15
+ getUnboundDescriptorReporter(client, hookName)
22
16
  );
23
17
  }
24
- function createSourceIdBindings() {
25
- return { unscoped: /* @__PURE__ */ new Map(), byClient: /* @__PURE__ */ new WeakMap() };
26
- }
27
- function claimSourceIds(bindings, preparedValue, client, hookName, release) {
28
- const prior = client ? bindings.byClient.get(client) : bindings.unscoped;
29
- const seen = /* @__PURE__ */ new Map();
30
- for (const source of getPreparedSources(preparedValue, client, hookName)) {
31
- const token = getSourceObjectToken(source);
32
- const previous = seen.get(source.id) ?? prior?.get(source.id);
33
- if (previous !== void 0 && previous !== token) {
34
- release();
35
- throw new Error(
36
- `[${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.`
37
- );
38
- }
39
- seen.set(source.id, token);
40
- }
41
- const claimed = prior ?? /* @__PURE__ */ new Map();
42
- for (const [id, token] of seen) claimed.set(id, token);
43
- if (client) bindings.byClient.set(client, claimed);
44
- }
45
- Object.defineProperty(exports, "resumeSyncStarts", {
46
- enumerable: true,
47
- get: () => db.resumeDeferredLiveQueryCollections
48
- });
49
- exports.claimSourceIds = claimSourceIds;
50
- exports.createSourceIdBindings = createSourceIdBindings;
51
18
  exports.getPreparedSources = getPreparedSources;
52
- exports.getSourceObjectToken = getSourceObjectToken;
19
+ exports.getUnboundDescriptorReporter = getUnboundDescriptorReporter;
53
20
  //# sourceMappingURL=source-id-bindings.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"source-id-bindings.cjs","sources":["../../src/source-id-bindings.ts"],"sourcesContent":["import { getPreparedLiveQuerySources } from '@tanstack/db'\nimport type { DbClient } from '@tanstack/db'\n\nexport { resumeDeferredLiveQueryCollections as resumeSyncStarts } 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 client?: DbClient,\n hookName?: string,\n): Array<{ id: string }> {\n return getPreparedLiveQuerySources(\n preparedValue,\n !client && hookName\n ? (alias) => {\n throw new Error(\n `[${hookName}] Collection descriptor \"${alias}\" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`,\n )\n }\n : undefined,\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, client, hookName)) {\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"],"names":["getPreparedLiveQuerySources"],"mappings":";;;AAKA,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,eACA,QACA,UACuB;AACvB,SAAOA,GAAAA;AAAAA,IACL;AAAA,IACA,CAAC,UAAU,WACP,CAAC,UAAU;AACT,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,4BAA4B,KAAK;AAAA,MAAA;AAAA,IAEjD,IACA;AAAA,EAAA;AAER;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,eAAe,QAAQ,QAAQ,GAAG;AACxE,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;;;;;;;;;"}
1
+ {"version":3,"file":"source-id-bindings.cjs","sources":["../../src/source-id-bindings.ts"],"sourcesContent":["import { getPreparedLiveQuerySources } from '@tanstack/db'\nimport type { DbClient } from '@tanstack/db'\n\n/**\n * Reports a descriptor source that no DbClient bound, naming the provider a\n * React tree needs. Returns `undefined` when a client can bind descriptors.\n */\nexport function getUnboundDescriptorReporter(\n client: DbClient | undefined,\n hookName: string,\n): ((alias: string) => never) | undefined {\n if (client) return undefined\n return (alias) => {\n throw new Error(\n `[${hookName}] Collection descriptor \"${alias}\" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`,\n )\n }\n}\n\nexport function getPreparedSources(\n preparedValue: unknown,\n client: DbClient | undefined,\n hookName: string,\n): Array<{ id: string }> {\n return getPreparedLiveQuerySources(\n preparedValue,\n getUnboundDescriptorReporter(client, hookName),\n )\n}\n"],"names":["getPreparedLiveQuerySources"],"mappings":";;;AAOO,SAAS,6BACd,QACA,UACwC;AACxC,MAAI,OAAQ,QAAO;AACnB,SAAO,CAAC,UAAU;AAChB,UAAM,IAAI;AAAA,MACR,IAAI,QAAQ,4BAA4B,KAAK;AAAA,IAAA;AAAA,EAEjD;AACF;AAEO,SAAS,mBACd,eACA,QACA,UACuB;AACvB,SAAOA,GAAAA;AAAAA,IACL;AAAA,IACA,6BAA6B,QAAQ,QAAQ;AAAA,EAAA;AAEjD;;;"}
@@ -1,20 +1,9 @@
1
1
  import { DbClient } from '@tanstack/db';
2
- export { resumeDeferredLiveQueryCollections as resumeSyncStarts } from '@tanstack/db';
3
- export declare function getSourceObjectToken(source: object): number;
4
- export declare function getPreparedSources(preparedValue: unknown, client?: DbClient, hookName?: string): Array<{
5
- id: string;
6
- }>;
7
- /** The source object each Collection ID named while one hook is mounted. */
8
- export type SourceIdBindings = {
9
- unscoped: Map<string, number>;
10
- byClient: WeakMap<DbClient, Map<string, number>>;
11
- };
12
- export declare function createSourceIdBindings(): SourceIdBindings;
13
2
  /**
14
- * Derived query identity names each source by its Collection ID. While a hook
15
- * is mounted, an ID must keep naming one source object in each client scope,
16
- * or the hook could reuse a live query that reads the old source. Claims the
17
- * prepared value's sources, or calls `release` and throws on a collision so a
18
- * rejected render keeps no sync-start deferral.
3
+ * Reports a descriptor source that no DbClient bound, naming the provider a
4
+ * React tree needs. Returns `undefined` when a client can bind descriptors.
19
5
  */
20
- export declare function claimSourceIds(bindings: SourceIdBindings, preparedValue: unknown, client: DbClient | undefined, hookName: string, release: () => void): void;
6
+ export declare function getUnboundDescriptorReporter(client: DbClient | undefined, hookName: string): ((alias: string) => never) | undefined;
7
+ export declare function getPreparedSources(preparedValue: unknown, client: DbClient | undefined, hookName: string): Array<{
8
+ id: string;
9
+ }>;
@@ -4,205 +4,126 @@ 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");
8
7
  const useLiveQuery = require("./useLiveQuery.cjs");
9
- const DEFAULT_GC_TIME_MS = 1;
10
8
  const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
11
- function queryIdentityMatches(comparison, sameClient, usesLegacyDeps) {
12
- return sameClient && (usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual);
13
- }
14
- function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
15
- if (`getNextPageParam` in config) {
16
- throw new Error(
17
- `getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.`
18
- );
19
- }
20
- const pageSize = db.normalizeLiveQueryWindowPageSize(config.pageSize);
21
- const initialPageParam = config.initialPageParam ?? 0;
9
+ function useLiveInfiniteQuery(queryFnOrCollection, config) {
10
+ const pageShape = db.readLiveQueryWindowPageShape(config);
22
11
  const contextDbClient = DbProvider.useOptionalDbClient();
23
12
  const dbClient = config.client ?? contextDbClient;
24
- const inputIsCollection = db.getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`;
13
+ const inputIsCollection = db.isCollection(queryFnOrCollection);
25
14
  const committedRef = react.useRef(null);
26
15
  const committed = committedRef.current;
27
- const renderedCollectionRef = react.useRef(null);
16
+ const renderedRef = react.useRef(null);
28
17
  const sourceIdsRef = react.useRef(null);
29
- const inputKind = inputIsCollection ? `collection` : `query`;
30
18
  const derivedIdentityProfilerRef = react.useRef({
31
19
  renderCount: 0,
32
20
  totalMs: 0,
33
21
  maxMs: 0,
34
22
  warned: false
35
23
  });
36
- const legacyUnhashableIdentityRef = react.useRef([
37
- `legacy-unhashable`
38
- ]);
39
24
  const deferredCollections = /* @__PURE__ */ new Set();
25
+ const identityScope = () => useLiveQuery.getIdentityScope(
26
+ sourceIdsRef.current ??= db.createLiveQuerySourceIdClaims(),
27
+ dbClient,
28
+ `useLiveInfiniteQuery`
29
+ );
30
+ const queryKey = db.getLiveQueryKey(config);
40
31
  let preparedQueryValue = unpreparedQueryValue;
41
- let identityDeps = [];
42
- let identityMode = `collection`;
43
- if (!inputIsCollection) {
44
- if (config.queryKey !== void 0) {
45
- identityMode = `queryKey`;
46
- identityDeps = config.queryKey;
47
- } else if (deps !== void 0) {
48
- identityMode = `legacyDeps`;
49
- identityDeps = deps;
50
- useLiveQuery.warnDeprecatedDepsArray(`useLiveInfiniteQuery`);
51
- } else if (committed?.identityMode === `derived` && committed.inputQuery === queryFnOrCollection && committed.client === dbClient) {
52
- identityMode = `derived`;
53
- identityDeps = committed.dependencies ?? [];
54
- } else {
55
- identityMode = `derived`;
56
- const preparation = useLiveQuery.prepareDerivedQuery(
57
- queryFnOrCollection,
58
- dbClient,
59
- derivedIdentityProfilerRef.current,
60
- deferredCollections
61
- );
62
- preparedQueryValue = preparation.value;
63
- sourceIdBindings.claimSourceIds(
64
- sourceIdsRef.current ??= sourceIdBindings.createSourceIdBindings(),
65
- preparation.value,
66
- dbClient,
67
- `useLiveInfiniteQuery`,
68
- () => db.resumeDeferredLiveQueryCollections(deferredCollections)
69
- );
70
- if (preparation.status === `hashable`) {
71
- identityDeps = preparation.identityDeps;
32
+ let dependencies;
33
+ let derivedQuery = void 0;
34
+ const rendered = renderedRef.current;
35
+ let binding;
36
+ try {
37
+ if (!inputIsCollection) {
38
+ if (queryKey !== void 0) {
39
+ dependencies = queryKey;
40
+ } else if (committed !== null && committed.derivedQuery === queryFnOrCollection && committed.binding.scope === dbClient) {
41
+ derivedQuery = queryFnOrCollection;
42
+ dependencies = committed.binding.dependencies;
72
43
  } else {
73
- useLiveQuery.warnUnhashableDerivedIdentity(preparation.error);
74
- identityDeps = legacyUnhashableIdentityRef.current;
44
+ const preparation = useLiveQuery.prepareDerivedQuery(
45
+ queryFnOrCollection,
46
+ derivedIdentityProfilerRef.current,
47
+ identityScope(),
48
+ deferredCollections
49
+ );
50
+ preparedQueryValue = preparation.value;
51
+ derivedQuery = queryFnOrCollection;
52
+ dependencies = preparation.identityDeps;
75
53
  }
76
54
  }
77
- }
78
- const usesLegacyDeps = !inputIsCollection && config.queryKey === void 0 && deps !== void 0;
79
- const dependencyComparison = db.compareLiveQueryWindowDependencies(
80
- committed?.dependencies,
81
- identityDeps
82
- );
83
- const sameClient = committed?.client === dbClient;
84
- const dependenciesChanged = !inputIsCollection && !queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps);
85
- const dependenciesStructurallyEqual = usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual;
86
- const needsNewCollection = committed === null || committed.inputKind !== inputKind || inputIsCollection && committed.inputCollection !== queryFnOrCollection || dependenciesChanged;
87
- const pageShapeChanged = committed === null || committed.pageSize !== pageSize || committed.initialPageParam !== initialPageParam;
88
- const needsNewController = committed === null || needsNewCollection || pageShapeChanged;
89
- let renderState = committed;
90
- if (needsNewController) {
91
- let collection = committed?.collection;
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;
108
- if (needsNewCollection) {
109
- let inputValue = queryFnOrCollection;
110
- if (!inputIsCollection) {
111
- if (preparedQueryValue === unpreparedQueryValue) {
112
- preparedQueryValue = useLiveQuery.prepareQueryValue(
113
- queryFnOrCollection,
114
- dbClient,
115
- deferredCollections
116
- );
117
- }
118
- inputValue = () => preparedQueryValue;
119
- }
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
- }
133
- } else {
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
- }
55
+ const compileQuery = () => {
56
+ if (preparedQueryValue === unpreparedQueryValue) {
57
+ preparedQueryValue = useLiveQuery.prepareQueryValue(
58
+ queryFnOrCollection,
59
+ dbClient,
60
+ deferredCollections
61
+ );
147
62
  }
148
- }
149
- if (!collection) {
150
- throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`);
151
- }
152
- if (inputIsCollection) {
153
- warning = db.getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null;
154
- } else {
155
- db.assertLiveQueryWindowManyResult(collection);
156
- }
157
- if (suppliedCollection) {
158
- startInRender = db.liveQueryWindowMatches(collection, requiredLimit);
159
- }
160
- if (startInRender) collection.startSyncImmediate();
161
- if (createdCollection) {
162
- renderedCollectionRef.current = {
63
+ return preparedQueryValue;
64
+ };
65
+ binding = db.bindLiveQueryWindow(
66
+ committed?.binding ?? null,
67
+ {
68
+ ...pageShape,
69
+ input: typeof queryFnOrCollection === `function` ? compileQuery : queryFnOrCollection,
70
+ dependencies,
71
+ scope: dbClient,
163
72
  client: dbClient,
164
- dependencies: [...identityDeps],
165
- collection,
166
- deferredCollections
167
- };
73
+ queryKey,
74
+ recompile: `identity`
75
+ },
76
+ rendered?.binding
77
+ );
78
+ } catch (error) {
79
+ db.rejectLiveQueryInput(deferredCollections, error);
80
+ }
81
+ let currentRenderState = committed;
82
+ if (binding !== committed?.binding) {
83
+ const newCollection = binding.collection !== committed?.binding.collection;
84
+ if (newCollection && binding.collection === rendered?.binding.collection) {
85
+ for (const deferred of rendered.deferredCollections) {
86
+ deferredCollections.add(deferred);
87
+ }
168
88
  }
169
- renderState = {
170
- inputKind,
171
- inputCollection: inputIsCollection ? collection : null,
172
- inputQuery: inputIsCollection ? null : queryFnOrCollection,
173
- client: dbClient,
174
- identityMode,
175
- dependencies: inputIsCollection ? null : [...identityDeps],
176
- pageSize,
177
- initialPageParam,
178
- collection,
179
- controller: db.createLiveQueryWindowController(collection, {
180
- pageSize,
181
- initialPageParam,
182
- initialPageCount
183
- }),
184
- warning,
185
- warned: false,
186
- deferredCollections
89
+ const reusesRendered = newCollection && binding.collection === rendered?.binding.collection;
90
+ currentRenderState = {
91
+ binding,
92
+ derivedQuery,
93
+ deferredCollections,
94
+ // A render that never commits releases its deferred sync starts when
95
+ // garbage collection reclaims the window collection it built. A reused
96
+ // collection keeps the release its first render registered.
97
+ cancelReclaimRelease: reusesRendered ? rendered.cancelReclaimRelease : newCollection && binding.ownsCollection ? db.releaseDeferredOnReclaim(binding.collection, deferredCollections) : void 0,
98
+ warned: false
187
99
  };
100
+ if (binding.supplied) renderedRef.current = null;
101
+ else if (newCollection && binding.ownsCollection) {
102
+ renderedRef.current = currentRenderState;
103
+ }
188
104
  }
189
- const currentRenderState = renderState;
190
- const controller = currentRenderState.controller;
105
+ const renderState = currentRenderState;
106
+ const controller = renderState.binding.controller;
191
107
  const subscribe = react.useCallback(
192
108
  (onStoreChange) => {
193
109
  const unsubscribe = controller.subscribe(onStoreChange);
194
- committedRef.current = currentRenderState;
195
- if (currentRenderState.warning && !currentRenderState.warned) {
196
- currentRenderState.warned = true;
197
- console.warn(currentRenderState.warning);
110
+ committedRef.current = renderState;
111
+ if (renderState.binding.warning && !renderState.warned) {
112
+ renderState.warned = true;
113
+ console.warn(renderState.binding.warning);
198
114
  }
199
- db.resumeDeferredLiveQueryCollections(currentRenderState.deferredCollections);
115
+ renderState.cancelReclaimRelease?.();
116
+ db.resumeDeferredLiveQueryCollections(renderState.deferredCollections);
200
117
  return unsubscribe;
201
118
  },
202
- [controller, currentRenderState]
119
+ [controller, renderState]
203
120
  );
204
121
  const getSnapshot = react.useCallback(() => controller.getSnapshot(), [controller]);
205
- const snapshot = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
122
+ const snapshot = react.useSyncExternalStore(
123
+ subscribe,
124
+ getSnapshot,
125
+ () => controller.getServerSnapshot()
126
+ );
206
127
  const fetchNextPage = react.useCallback(
207
128
  () => db.fetchNextLiveQueryWindowPage(controller),
208
129
  [controller]
@@ -215,12 +136,11 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
215
136
  status: snapshot.status,
216
137
  isLoading: snapshot.isLoading,
217
138
  isReady: snapshot.isReady,
218
- persistedStatus: snapshot.persistedStatus,
219
- isPersistedReady: snapshot.isPersistedReady,
220
- persistedError: snapshot.persistedError,
221
139
  isIdle: snapshot.isIdle,
222
- isError: snapshot.isError,
140
+ isLoadingError: snapshot.isLoadingError,
141
+ isCrashed: snapshot.isCrashed,
223
142
  isCleanedUp: snapshot.isCleanedUp,
143
+ isSyncError: snapshot.isSyncError,
224
144
  collection: snapshot.collection,
225
145
  isEnabled: snapshot.isEnabled,
226
146
  pages: snapshot.pages,
@@ -228,7 +148,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
228
148
  fetchNextPage,
229
149
  hasNextPage: snapshot.hasNextPage,
230
150
  isFetchingNextPage: snapshot.isFetchingNextPage,
231
- error: snapshot.error
151
+ health: snapshot.health
232
152
  };
233
153
  }
234
154
  exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
@@ -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 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;;"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.cjs","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n bindLiveQueryWindow,\n createLiveQuerySourceIdClaims,\n fetchNextLiveQueryWindowPage,\n getLiveQueryKey,\n isCollection,\n readLiveQueryWindowPageShape,\n rejectLiveQueryInput,\n releaseDeferredOnReclaim,\n resumeDeferredLiveQueryCollections,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n getIdentityScope,\n prepareDerivedQuery,\n prepareQueryValue,\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 LiveQuerySourceIdClaims,\n LiveQueryStatusFlags,\n LiveQueryWindowBinding,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\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}\n\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = LiveQueryStatusFlags & {\n data: Array<TResult>\n state: Map<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & NonSingleResult\n status: CollectionStatus\n isEnabled: true\n pages: Array<Array<TResult>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\n/** One render's window binding and what its commit still owes. */\ntype InfiniteQueryRender = {\n binding: LiveQueryWindowBinding\n /** The query callback whose identity this render derived, if any. */\n derivedQuery: unknown\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n /** Drops the reclaim release when this render commits. */\n cancelReclaimRelease: (() => void) | undefined\n warned: boolean\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 * @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): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n): UseLiveInfiniteQueryReturn<TContext> {\n const pageShape = readLiveQueryWindowPageShape(config)\n const contextDbClient = useOptionalDbClient()\n const dbClient = config.client ?? contextDbClient\n const inputIsCollection = isCollection(queryFnOrCollection)\n\n const committedRef = useRef<InfiniteQueryRender | null>(null)\n const committed = committedRef.current\n // Like useLiveQuery's instance memo, keep the latest render whose binding\n // built a window collection. A second render before commit, such as a\n // StrictMode double render, reuses that collection instead of starting\n // another. Each render still builds its own controller and page count from\n // the committed binding, which an uncommitted render never changes.\n const renderedRef = useRef<InfiniteQueryRender | null>(null)\n const sourceIdsRef = useRef<LiveQuerySourceIdClaims | null>(null)\n const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const deferredCollections = new Set<\n CollectionImplType<any, string | number, any, any, any>\n >()\n const identityScope = () =>\n getIdentityScope(\n (sourceIdsRef.current ??= createLiveQuerySourceIdClaims()),\n dbClient,\n `useLiveInfiniteQuery`,\n )\n\n const queryKey = getLiveQueryKey(config)\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let dependencies: ReadonlyArray<unknown> | undefined\n let derivedQuery: unknown = undefined\n\n const rendered = renderedRef.current\n let binding: LiveQueryWindowBinding\n // The render boundary: a render that throws anywhere after preparation,\n // such as an unhashable, single-result, or uncompilable query, never\n // commits, so it releases the sync starts it deferred here.\n try {\n if (!inputIsCollection) {\n if (queryKey !== undefined) {\n dependencies = queryKey\n } else if (\n committed !== null &&\n committed.derivedQuery === queryFnOrCollection &&\n committed.binding.scope === dbClient\n ) {\n // The same callback in the same client scope keeps its identity.\n derivedQuery = queryFnOrCollection\n dependencies = committed.binding.dependencies\n } else {\n const preparation = prepareDerivedQuery(\n queryFnOrCollection,\n derivedIdentityProfilerRef.current,\n identityScope(),\n deferredCollections,\n )\n preparedQueryValue = preparation.value\n derivedQuery = queryFnOrCollection\n dependencies = preparation.identityDeps\n }\n }\n\n // The binding calls this only when it compiles a new window collection.\n const compileQuery = () => {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n queryFnOrCollection,\n dbClient,\n deferredCollections,\n )\n }\n return preparedQueryValue\n }\n\n binding = bindLiveQueryWindow(\n committed?.binding ?? null,\n {\n ...pageShape,\n input:\n typeof queryFnOrCollection === `function`\n ? compileQuery\n : queryFnOrCollection,\n dependencies,\n scope: dbClient,\n client: dbClient,\n queryKey,\n recompile: `identity`,\n },\n rendered?.binding,\n )\n } catch (error) {\n rejectLiveQueryInput(deferredCollections, error)\n }\n\n let currentRenderState = committed\n if (binding !== committed?.binding) {\n const newCollection = binding.collection !== committed?.binding.collection\n if (newCollection && binding.collection === rendered?.binding.collection) {\n // Sources the earlier render deferred still resume at this commit.\n for (const deferred of rendered.deferredCollections) {\n deferredCollections.add(deferred)\n }\n }\n const reusesRendered =\n newCollection && binding.collection === rendered?.binding.collection\n currentRenderState = {\n binding,\n derivedQuery,\n deferredCollections,\n // A render that never commits releases its deferred sync starts when\n // garbage collection reclaims the window collection it built. A reused\n // collection keeps the release its first render registered.\n cancelReclaimRelease: reusesRendered\n ? rendered.cancelReclaimRelease\n : newCollection && binding.ownsCollection\n ? releaseDeferredOnReclaim(binding.collection, deferredCollections)\n : undefined,\n warned: false,\n }\n // Keep a created collection for the next render only once it validated\n // and started, so a retry after a startup error builds a new one.\n if (binding.supplied) renderedRef.current = null\n else if (newCollection && binding.ownsCollection) {\n renderedRef.current = currentRenderState\n }\n }\n const renderState = currentRenderState!\n const controller = renderState.binding.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = renderState\n if (renderState.binding.warning && !renderState.warned) {\n renderState.warned = true\n console.warn(renderState.binding.warning)\n }\n // A commit releases the starts itself, so it drops the reclaim release.\n renderState.cancelReclaimRelease?.()\n resumeDeferredLiveQueryCollections(renderState.deferredCollections)\n return unsubscribe\n },\n [controller, renderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, () =>\n controller.getServerSnapshot(),\n )\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 isIdle: snapshot.isIdle,\n isLoadingError: snapshot.isLoadingError,\n isCrashed: snapshot.isCrashed,\n isCleanedUp: snapshot.isCleanedUp,\n isSyncError: snapshot.isSyncError,\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 health: snapshot.health,\n }\n}\n"],"names":["getIdentityScope","prepareDerivedQuery","prepareQueryValue","bindLiveQueryWindow","rejectLiveQueryInput","useCallback","resumeDeferredLiveQueryCollections","useSyncExternalStore"],"mappings":";;;;;;;AAwCA;AA2FO;AAIL;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAGA;AACEA;AAC4B;AAC1B;AACA;AAGJ;AACA;AAEA;AACA;AAEA;AACA;AAIA;AACE;AACE;AACE;AAAe;AAOf;AACA;AAAiC;AAEjC;AAAoBC;AAClB;AAC2B;AAC3B;AACA;AAEF;AACA;AACA;AAA2B;AAC7B;AAIF;AACE;AACE;AAAqBC;AACnB;AACA;AACA;AAAA;AAGJ;AAAO;AAGT;AAAUC;AACc;AACtB;AACK;AAIG;AACN;AACO;AACC;AACR;AACW;AAAA;AAEH;AAAA;AAGZC;AAA+C;AAGjD;AACA;AACE;AACA;AAEE;AACE;AAAgC;AAClC;AAEF;AAEA;AAAqB;AACnB;AACA;AACA;AAAA;AAAA;AAAA;AAQM;AACE;AAIV;AAA4C;AAE1C;AAAsB;AACxB;AAEF;AACA;AAEA;AAAkBC;AAEd;AACA;AACA;AACE;AACA;AAAwC;AAG1C;AACAC;AACA;AAAO;AACT;AACwB;AAE1B;AACA;AAAiBC;AAAqB;AAAW;AACpC;AAGb;AAAsBF;AACyB;AAClC;AAGb;AAAO;AACU;AAEb;AAAgB;AAClB;AACiB;AACG;AACF;AACD;AACQ;AACL;AACE;AACA;AAEX;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACZ;AAErB;;"}
@@ -1,5 +1,5 @@
1
1
  import { LiveQueryKey, useLiveQuery } from './useLiveQuery.cjs';
2
- import { Collection, CollectionStatus, Context, DbClient, InferResultType, InitialQueryBuilder, LiveQueryPersistedStatus, NonSingleResult, QueryBuilder } from '@tanstack/db';
2
+ import { Collection, CollectionStatus, Context, DbClient, InferResultType, InitialQueryBuilder, LiveQueryStatusFlags, NonSingleResult, QueryBuilder } from '@tanstack/db';
3
3
  export type UseLiveInfiniteQueryConfig<_TContext extends Context> = {
4
4
  /**
5
5
  * Explicit identity for queries that contain opaque functional variants or
@@ -20,28 +20,18 @@ export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnTy
20
20
  fetchNextPage: () => Promise<void>;
21
21
  hasNextPage: boolean;
22
22
  isFetchingNextPage: boolean;
23
- error: unknown;
24
23
  };
25
- export type UseLiveInfiniteQueryReturnWithCollection<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>> = {
24
+ export type UseLiveInfiniteQueryReturnWithCollection<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>> = LiveQueryStatusFlags & {
26
25
  data: Array<TResult>;
27
26
  state: Map<TKey, TResult>;
28
27
  collection: Collection<TResult, TKey, TUtils> & NonSingleResult;
29
28
  status: CollectionStatus;
30
- isLoading: boolean;
31
- isReady: boolean;
32
- persistedStatus: LiveQueryPersistedStatus;
33
- isPersistedReady: boolean;
34
- persistedError: unknown | undefined;
35
- isIdle: boolean;
36
- isError: boolean;
37
- isCleanedUp: boolean;
38
29
  isEnabled: true;
39
30
  pages: Array<Array<TResult>>;
40
31
  pageParams: Array<number>;
41
32
  fetchNextPage: () => Promise<void>;
42
33
  hasNextPage: boolean;
43
34
  isFetchingNextPage: boolean;
44
- error: unknown;
45
35
  };
46
36
  /**
47
37
  * Create an infinite query using a query function with live updates.
@@ -51,8 +41,7 @@ export type UseLiveInfiniteQueryReturnWithCollection<TResult extends object, TKe
51
41
  *
52
42
  * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.
53
43
  * @param config - Configuration including pageSize and an optional initial page label
54
- * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
55
44
  * @returns Object with pages, data, and pagination controls
56
45
  */
57
46
  export declare function useLiveInfiniteQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult, config: UseLiveInfiniteQueryConfig<any>): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>;
58
- export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<unknown>): UseLiveInfiniteQueryReturn<TContext>;
47
+ export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>): UseLiveInfiniteQueryReturn<TContext>;