@tanstack/react-db 0.1.20 → 0.1.21
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/useLiveQuery.cjs +74 -35
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +52 -0
- package/dist/esm/useLiveQuery.d.ts +52 -0
- package/dist/esm/useLiveQuery.js +75 -36
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/useLiveQuery.ts +212 -55
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const react = require("react");
|
|
4
4
|
const db = require("@tanstack/db");
|
|
5
|
+
const DEFAULT_GC_TIME_MS = 1;
|
|
5
6
|
function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
6
7
|
const isCollection = configOrQueryOrCollection && typeof configOrQueryOrCollection === `object` && typeof configOrQueryOrCollection.subscribeChanges === `function` && typeof configOrQueryOrCollection.startSyncImmediate === `function` && typeof configOrQueryOrCollection.id === `string`;
|
|
7
8
|
const collectionRef = react.useRef(
|
|
@@ -9,6 +10,8 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
9
10
|
);
|
|
10
11
|
const depsRef = react.useRef(null);
|
|
11
12
|
const configRef = react.useRef(null);
|
|
13
|
+
const versionRef = react.useRef(0);
|
|
14
|
+
const snapshotRef = react.useRef(null);
|
|
12
15
|
const needsNewCollection = !collectionRef.current || isCollection && configRef.current !== configOrQueryOrCollection || !isCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]));
|
|
13
16
|
if (needsNewCollection) {
|
|
14
17
|
if (isCollection) {
|
|
@@ -17,25 +20,41 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
17
20
|
configRef.current = configOrQueryOrCollection;
|
|
18
21
|
} else {
|
|
19
22
|
if (typeof configOrQueryOrCollection === `function`) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const queryBuilder = new db.BaseQueryBuilder();
|
|
24
|
+
const result = configOrQueryOrCollection(queryBuilder);
|
|
25
|
+
if (result === void 0 || result === null) {
|
|
26
|
+
collectionRef.current = null;
|
|
27
|
+
} else if (result instanceof db.CollectionImpl) {
|
|
28
|
+
result.startSyncImmediate();
|
|
29
|
+
collectionRef.current = result;
|
|
30
|
+
} else if (result instanceof db.BaseQueryBuilder) {
|
|
31
|
+
collectionRef.current = db.createLiveQueryCollection({
|
|
32
|
+
query: configOrQueryOrCollection,
|
|
33
|
+
startSync: true,
|
|
34
|
+
gcTime: DEFAULT_GC_TIME_MS
|
|
35
|
+
});
|
|
36
|
+
} else if (result && typeof result === `object`) {
|
|
37
|
+
collectionRef.current = db.createLiveQueryCollection({
|
|
38
|
+
startSync: true,
|
|
39
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
40
|
+
...result
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
depsRef.current = [...deps];
|
|
26
48
|
} else {
|
|
27
49
|
collectionRef.current = db.createLiveQueryCollection({
|
|
28
50
|
startSync: true,
|
|
29
|
-
gcTime:
|
|
30
|
-
// Live queries created by useLiveQuery are cleaned up immediately
|
|
51
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
31
52
|
...configOrQueryOrCollection
|
|
32
53
|
});
|
|
54
|
+
depsRef.current = [...deps];
|
|
33
55
|
}
|
|
34
|
-
depsRef.current = [...deps];
|
|
35
56
|
}
|
|
36
57
|
}
|
|
37
|
-
const versionRef = react.useRef(0);
|
|
38
|
-
const snapshotRef = react.useRef(null);
|
|
39
58
|
if (needsNewCollection) {
|
|
40
59
|
versionRef.current = 0;
|
|
41
60
|
snapshotRef.current = null;
|
|
@@ -43,6 +62,10 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
43
62
|
const subscribeRef = react.useRef(null);
|
|
44
63
|
if (!subscribeRef.current || needsNewCollection) {
|
|
45
64
|
subscribeRef.current = (onStoreChange) => {
|
|
65
|
+
if (!collectionRef.current) {
|
|
66
|
+
return () => {
|
|
67
|
+
};
|
|
68
|
+
}
|
|
46
69
|
const unsubscribe = collectionRef.current.subscribeChanges(() => {
|
|
47
70
|
versionRef.current += 1;
|
|
48
71
|
onStoreChange();
|
|
@@ -77,30 +100,46 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
77
100
|
const returnedSnapshotRef = react.useRef(null);
|
|
78
101
|
const returnedRef = react.useRef(null);
|
|
79
102
|
if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
if (!snapshot.collection) {
|
|
104
|
+
returnedRef.current = {
|
|
105
|
+
state: void 0,
|
|
106
|
+
data: void 0,
|
|
107
|
+
collection: void 0,
|
|
108
|
+
status: `disabled`,
|
|
109
|
+
isLoading: false,
|
|
110
|
+
isReady: false,
|
|
111
|
+
isIdle: false,
|
|
112
|
+
isError: false,
|
|
113
|
+
isCleanedUp: false,
|
|
114
|
+
isEnabled: false
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
const entries = Array.from(snapshot.collection.entries());
|
|
118
|
+
let stateCache = null;
|
|
119
|
+
let dataCache = null;
|
|
120
|
+
returnedRef.current = {
|
|
121
|
+
get state() {
|
|
122
|
+
if (!stateCache) {
|
|
123
|
+
stateCache = new Map(entries);
|
|
124
|
+
}
|
|
125
|
+
return stateCache;
|
|
126
|
+
},
|
|
127
|
+
get data() {
|
|
128
|
+
if (!dataCache) {
|
|
129
|
+
dataCache = entries.map(([, value]) => value);
|
|
130
|
+
}
|
|
131
|
+
return dataCache;
|
|
132
|
+
},
|
|
133
|
+
collection: snapshot.collection,
|
|
134
|
+
status: snapshot.collection.status,
|
|
135
|
+
isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
|
|
136
|
+
isReady: snapshot.collection.status === `ready`,
|
|
137
|
+
isIdle: snapshot.collection.status === `idle`,
|
|
138
|
+
isError: snapshot.collection.status === `error`,
|
|
139
|
+
isCleanedUp: snapshot.collection.status === `cleaned-up`,
|
|
140
|
+
isEnabled: true
|
|
141
|
+
};
|
|
142
|
+
}
|
|
104
143
|
returnedSnapshotRef.current = snapshot;
|
|
105
144
|
}
|
|
106
145
|
return returnedRef.current;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport { createLiveQueryCollection } from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept just the query function\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 3: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Original logic for creating collections\n // Ensure we always start sync for React hooks\n if (typeof configOrQueryOrCollection === `function`) {\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n }) as unknown as Collection<object, string | number, {}>\n } else {\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n ...configOrQueryOrCollection,\n }) as unknown as Collection<object, string | number, {}>\n }\n depsRef.current = [...deps]\n }\n }\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n const unsubscribe = collectionRef.current!.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current!.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}>\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current!\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":["useRef","createLiveQueryCollection","useSyncExternalStore"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: Array<GetResult<TContext>> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (result instanceof CollectionImpl) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n // If no collection, return a no-op unsubscribe function\n if (!collectionRef.current) {\n return () => {}\n }\n\n const unsubscribe = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}> | null\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Handle null collection case (when callback returns undefined/null)\n if (!snapshot.collection) {\n returnedRef.current = {\n state: undefined,\n data: undefined,\n collection: undefined,\n status: `disabled`,\n isLoading: false,\n isReady: false,\n isIdle: false,\n isError: false,\n isCleanedUp: false,\n isEnabled: false,\n }\n } else {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n isEnabled: true,\n }\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":["useRef","BaseQueryBuilder","CollectionImpl","createLiveQueryCollection","useSyncExternalStore"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Collection, CollectionStatus, Context, GetResult, InitialQueryBuilder, LiveQueryCollectionConfig, QueryBuilder } from '@tanstack/db';
|
|
2
|
+
export type UseLiveQueryStatus = CollectionStatus | `disabled`;
|
|
2
3
|
/**
|
|
3
4
|
* Create a live query using a query function
|
|
4
5
|
* @param queryFn - Query function that defines what data to fetch
|
|
@@ -59,6 +60,55 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
|
|
|
59
60
|
isIdle: boolean;
|
|
60
61
|
isError: boolean;
|
|
61
62
|
isCleanedUp: boolean;
|
|
63
|
+
isEnabled: true;
|
|
64
|
+
};
|
|
65
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<unknown>): {
|
|
66
|
+
state: Map<string | number, GetResult<TContext>> | undefined;
|
|
67
|
+
data: Array<GetResult<TContext>> | undefined;
|
|
68
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
|
|
69
|
+
status: UseLiveQueryStatus;
|
|
70
|
+
isLoading: boolean;
|
|
71
|
+
isReady: boolean;
|
|
72
|
+
isIdle: boolean;
|
|
73
|
+
isError: boolean;
|
|
74
|
+
isCleanedUp: boolean;
|
|
75
|
+
isEnabled: boolean;
|
|
76
|
+
};
|
|
77
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => LiveQueryCollectionConfig<TContext> | undefined | null, deps?: Array<unknown>): {
|
|
78
|
+
state: Map<string | number, GetResult<TContext>> | undefined;
|
|
79
|
+
data: Array<GetResult<TContext>> | undefined;
|
|
80
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
|
|
81
|
+
status: UseLiveQueryStatus;
|
|
82
|
+
isLoading: boolean;
|
|
83
|
+
isReady: boolean;
|
|
84
|
+
isIdle: boolean;
|
|
85
|
+
isError: boolean;
|
|
86
|
+
isCleanedUp: boolean;
|
|
87
|
+
isEnabled: boolean;
|
|
88
|
+
};
|
|
89
|
+
export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
|
|
90
|
+
state: Map<TKey, TResult> | undefined;
|
|
91
|
+
data: Array<TResult> | undefined;
|
|
92
|
+
collection: Collection<TResult, TKey, TUtils> | undefined;
|
|
93
|
+
status: UseLiveQueryStatus;
|
|
94
|
+
isLoading: boolean;
|
|
95
|
+
isReady: boolean;
|
|
96
|
+
isIdle: boolean;
|
|
97
|
+
isError: boolean;
|
|
98
|
+
isCleanedUp: boolean;
|
|
99
|
+
isEnabled: boolean;
|
|
100
|
+
};
|
|
101
|
+
export declare function useLiveQuery<TContext extends Context, TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | LiveQueryCollectionConfig<TContext> | Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
|
|
102
|
+
state: Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined;
|
|
103
|
+
data: Array<GetResult<TContext>> | Array<TResult> | undefined;
|
|
104
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | Collection<TResult, TKey, TUtils> | undefined;
|
|
105
|
+
status: UseLiveQueryStatus;
|
|
106
|
+
isLoading: boolean;
|
|
107
|
+
isReady: boolean;
|
|
108
|
+
isIdle: boolean;
|
|
109
|
+
isError: boolean;
|
|
110
|
+
isCleanedUp: boolean;
|
|
111
|
+
isEnabled: boolean;
|
|
62
112
|
};
|
|
63
113
|
/**
|
|
64
114
|
* Create a live query using configuration object
|
|
@@ -103,6 +153,7 @@ export declare function useLiveQuery<TContext extends Context>(config: LiveQuery
|
|
|
103
153
|
isIdle: boolean;
|
|
104
154
|
isError: boolean;
|
|
105
155
|
isCleanedUp: boolean;
|
|
156
|
+
isEnabled: true;
|
|
106
157
|
};
|
|
107
158
|
/**
|
|
108
159
|
* Subscribe to an existing live query collection
|
|
@@ -143,4 +194,5 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
|
|
|
143
194
|
isIdle: boolean;
|
|
144
195
|
isError: boolean;
|
|
145
196
|
isCleanedUp: boolean;
|
|
197
|
+
isEnabled: true;
|
|
146
198
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Collection, CollectionStatus, Context, GetResult, InitialQueryBuilder, LiveQueryCollectionConfig, QueryBuilder } from '@tanstack/db';
|
|
2
|
+
export type UseLiveQueryStatus = CollectionStatus | `disabled`;
|
|
2
3
|
/**
|
|
3
4
|
* Create a live query using a query function
|
|
4
5
|
* @param queryFn - Query function that defines what data to fetch
|
|
@@ -59,6 +60,55 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
|
|
|
59
60
|
isIdle: boolean;
|
|
60
61
|
isError: boolean;
|
|
61
62
|
isCleanedUp: boolean;
|
|
63
|
+
isEnabled: true;
|
|
64
|
+
};
|
|
65
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null, deps?: Array<unknown>): {
|
|
66
|
+
state: Map<string | number, GetResult<TContext>> | undefined;
|
|
67
|
+
data: Array<GetResult<TContext>> | undefined;
|
|
68
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
|
|
69
|
+
status: UseLiveQueryStatus;
|
|
70
|
+
isLoading: boolean;
|
|
71
|
+
isReady: boolean;
|
|
72
|
+
isIdle: boolean;
|
|
73
|
+
isError: boolean;
|
|
74
|
+
isCleanedUp: boolean;
|
|
75
|
+
isEnabled: boolean;
|
|
76
|
+
};
|
|
77
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => LiveQueryCollectionConfig<TContext> | undefined | null, deps?: Array<unknown>): {
|
|
78
|
+
state: Map<string | number, GetResult<TContext>> | undefined;
|
|
79
|
+
data: Array<GetResult<TContext>> | undefined;
|
|
80
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | undefined;
|
|
81
|
+
status: UseLiveQueryStatus;
|
|
82
|
+
isLoading: boolean;
|
|
83
|
+
isReady: boolean;
|
|
84
|
+
isIdle: boolean;
|
|
85
|
+
isError: boolean;
|
|
86
|
+
isCleanedUp: boolean;
|
|
87
|
+
isEnabled: boolean;
|
|
88
|
+
};
|
|
89
|
+
export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
|
|
90
|
+
state: Map<TKey, TResult> | undefined;
|
|
91
|
+
data: Array<TResult> | undefined;
|
|
92
|
+
collection: Collection<TResult, TKey, TUtils> | undefined;
|
|
93
|
+
status: UseLiveQueryStatus;
|
|
94
|
+
isLoading: boolean;
|
|
95
|
+
isReady: boolean;
|
|
96
|
+
isIdle: boolean;
|
|
97
|
+
isError: boolean;
|
|
98
|
+
isCleanedUp: boolean;
|
|
99
|
+
isEnabled: boolean;
|
|
100
|
+
};
|
|
101
|
+
export declare function useLiveQuery<TContext extends Context, TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | LiveQueryCollectionConfig<TContext> | Collection<TResult, TKey, TUtils> | undefined | null, deps?: Array<unknown>): {
|
|
102
|
+
state: Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined;
|
|
103
|
+
data: Array<GetResult<TContext>> | Array<TResult> | undefined;
|
|
104
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | Collection<TResult, TKey, TUtils> | undefined;
|
|
105
|
+
status: UseLiveQueryStatus;
|
|
106
|
+
isLoading: boolean;
|
|
107
|
+
isReady: boolean;
|
|
108
|
+
isIdle: boolean;
|
|
109
|
+
isError: boolean;
|
|
110
|
+
isCleanedUp: boolean;
|
|
111
|
+
isEnabled: boolean;
|
|
62
112
|
};
|
|
63
113
|
/**
|
|
64
114
|
* Create a live query using configuration object
|
|
@@ -103,6 +153,7 @@ export declare function useLiveQuery<TContext extends Context>(config: LiveQuery
|
|
|
103
153
|
isIdle: boolean;
|
|
104
154
|
isError: boolean;
|
|
105
155
|
isCleanedUp: boolean;
|
|
156
|
+
isEnabled: true;
|
|
106
157
|
};
|
|
107
158
|
/**
|
|
108
159
|
* Subscribe to an existing live query collection
|
|
@@ -143,4 +194,5 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
|
|
|
143
194
|
isIdle: boolean;
|
|
144
195
|
isError: boolean;
|
|
145
196
|
isCleanedUp: boolean;
|
|
197
|
+
isEnabled: true;
|
|
146
198
|
};
|
package/dist/esm/useLiveQuery.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
-
import { createLiveQueryCollection } from "@tanstack/db";
|
|
2
|
+
import { BaseQueryBuilder, CollectionImpl, createLiveQueryCollection } from "@tanstack/db";
|
|
3
|
+
const DEFAULT_GC_TIME_MS = 1;
|
|
3
4
|
function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
4
5
|
const isCollection = configOrQueryOrCollection && typeof configOrQueryOrCollection === `object` && typeof configOrQueryOrCollection.subscribeChanges === `function` && typeof configOrQueryOrCollection.startSyncImmediate === `function` && typeof configOrQueryOrCollection.id === `string`;
|
|
5
6
|
const collectionRef = useRef(
|
|
@@ -7,6 +8,8 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
7
8
|
);
|
|
8
9
|
const depsRef = useRef(null);
|
|
9
10
|
const configRef = useRef(null);
|
|
11
|
+
const versionRef = useRef(0);
|
|
12
|
+
const snapshotRef = useRef(null);
|
|
10
13
|
const needsNewCollection = !collectionRef.current || isCollection && configRef.current !== configOrQueryOrCollection || !isCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]));
|
|
11
14
|
if (needsNewCollection) {
|
|
12
15
|
if (isCollection) {
|
|
@@ -15,25 +18,41 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
15
18
|
configRef.current = configOrQueryOrCollection;
|
|
16
19
|
} else {
|
|
17
20
|
if (typeof configOrQueryOrCollection === `function`) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const queryBuilder = new BaseQueryBuilder();
|
|
22
|
+
const result = configOrQueryOrCollection(queryBuilder);
|
|
23
|
+
if (result === void 0 || result === null) {
|
|
24
|
+
collectionRef.current = null;
|
|
25
|
+
} else if (result instanceof CollectionImpl) {
|
|
26
|
+
result.startSyncImmediate();
|
|
27
|
+
collectionRef.current = result;
|
|
28
|
+
} else if (result instanceof BaseQueryBuilder) {
|
|
29
|
+
collectionRef.current = createLiveQueryCollection({
|
|
30
|
+
query: configOrQueryOrCollection,
|
|
31
|
+
startSync: true,
|
|
32
|
+
gcTime: DEFAULT_GC_TIME_MS
|
|
33
|
+
});
|
|
34
|
+
} else if (result && typeof result === `object`) {
|
|
35
|
+
collectionRef.current = createLiveQueryCollection({
|
|
36
|
+
startSync: true,
|
|
37
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
38
|
+
...result
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
depsRef.current = [...deps];
|
|
24
46
|
} else {
|
|
25
47
|
collectionRef.current = createLiveQueryCollection({
|
|
26
48
|
startSync: true,
|
|
27
|
-
gcTime:
|
|
28
|
-
// Live queries created by useLiveQuery are cleaned up immediately
|
|
49
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
29
50
|
...configOrQueryOrCollection
|
|
30
51
|
});
|
|
52
|
+
depsRef.current = [...deps];
|
|
31
53
|
}
|
|
32
|
-
depsRef.current = [...deps];
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
|
-
const versionRef = useRef(0);
|
|
36
|
-
const snapshotRef = useRef(null);
|
|
37
56
|
if (needsNewCollection) {
|
|
38
57
|
versionRef.current = 0;
|
|
39
58
|
snapshotRef.current = null;
|
|
@@ -41,6 +60,10 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
41
60
|
const subscribeRef = useRef(null);
|
|
42
61
|
if (!subscribeRef.current || needsNewCollection) {
|
|
43
62
|
subscribeRef.current = (onStoreChange) => {
|
|
63
|
+
if (!collectionRef.current) {
|
|
64
|
+
return () => {
|
|
65
|
+
};
|
|
66
|
+
}
|
|
44
67
|
const unsubscribe = collectionRef.current.subscribeChanges(() => {
|
|
45
68
|
versionRef.current += 1;
|
|
46
69
|
onStoreChange();
|
|
@@ -75,30 +98,46 @@ function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
75
98
|
const returnedSnapshotRef = useRef(null);
|
|
76
99
|
const returnedRef = useRef(null);
|
|
77
100
|
if (!returnedSnapshotRef.current || returnedSnapshotRef.current.version !== snapshot.version || returnedSnapshotRef.current.collection !== snapshot.collection) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
if (!snapshot.collection) {
|
|
102
|
+
returnedRef.current = {
|
|
103
|
+
state: void 0,
|
|
104
|
+
data: void 0,
|
|
105
|
+
collection: void 0,
|
|
106
|
+
status: `disabled`,
|
|
107
|
+
isLoading: false,
|
|
108
|
+
isReady: false,
|
|
109
|
+
isIdle: false,
|
|
110
|
+
isError: false,
|
|
111
|
+
isCleanedUp: false,
|
|
112
|
+
isEnabled: false
|
|
113
|
+
};
|
|
114
|
+
} else {
|
|
115
|
+
const entries = Array.from(snapshot.collection.entries());
|
|
116
|
+
let stateCache = null;
|
|
117
|
+
let dataCache = null;
|
|
118
|
+
returnedRef.current = {
|
|
119
|
+
get state() {
|
|
120
|
+
if (!stateCache) {
|
|
121
|
+
stateCache = new Map(entries);
|
|
122
|
+
}
|
|
123
|
+
return stateCache;
|
|
124
|
+
},
|
|
125
|
+
get data() {
|
|
126
|
+
if (!dataCache) {
|
|
127
|
+
dataCache = entries.map(([, value]) => value);
|
|
128
|
+
}
|
|
129
|
+
return dataCache;
|
|
130
|
+
},
|
|
131
|
+
collection: snapshot.collection,
|
|
132
|
+
status: snapshot.collection.status,
|
|
133
|
+
isLoading: snapshot.collection.status === `loading` || snapshot.collection.status === `initialCommit`,
|
|
134
|
+
isReady: snapshot.collection.status === `ready`,
|
|
135
|
+
isIdle: snapshot.collection.status === `idle`,
|
|
136
|
+
isError: snapshot.collection.status === `error`,
|
|
137
|
+
isCleanedUp: snapshot.collection.status === `cleaned-up`,
|
|
138
|
+
isEnabled: true
|
|
139
|
+
};
|
|
140
|
+
}
|
|
102
141
|
returnedSnapshotRef.current = snapshot;
|
|
103
142
|
}
|
|
104
143
|
return returnedRef.current;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport { createLiveQueryCollection } from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept just the query function\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 3: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Original logic for creating collections\n // Ensure we always start sync for React hooks\n if (typeof configOrQueryOrCollection === `function`) {\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n }) as unknown as Collection<object, string | number, {}>\n } else {\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: 0, // Live queries created by useLiveQuery are cleaned up immediately\n ...configOrQueryOrCollection,\n }) as unknown as Collection<object, string | number, {}>\n }\n depsRef.current = [...deps]\n }\n }\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n const unsubscribe = collectionRef.current!.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current!.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}>\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current!\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}>\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from \"react\"\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\n} from \"@tanstack/db\"\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from \"@tanstack/db\"\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * )\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: Array<GetResult<TContext>> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>\n): {\n state:\n | Map<string | number, GetResult<TContext>>\n | Map<TKey, TResult>\n | undefined\n data: Array<GetResult<TContext>> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({ query: queryBuilder })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>\n): {\n state: Map<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 7: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils>\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps: Array<unknown> = []\n) {\n // Check if it's already a collection by checking for specific collection methods\n const isCollection =\n configOrQueryOrCollection &&\n typeof configOrQueryOrCollection === `object` &&\n typeof configOrQueryOrCollection.subscribeChanges === `function` &&\n typeof configOrQueryOrCollection.startSyncImmediate === `function` &&\n typeof configOrQueryOrCollection.id === `string`\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // Use refs to track version and memoized snapshot\n const versionRef = useRef(0)\n const snapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (isCollection && configRef.current !== configOrQueryOrCollection) ||\n (!isCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\n\n if (needsNewCollection) {\n if (isCollection) {\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (result instanceof CollectionImpl) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\n }\n\n // Reset refs when collection changes\n if (needsNewCollection) {\n versionRef.current = 0\n snapshotRef.current = null\n }\n\n // Create stable subscribe function using ref\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n // If no collection, return a no-op unsubscribe function\n if (!collectionRef.current) {\n return () => {}\n }\n\n const unsubscribe = collectionRef.current.subscribeChanges(() => {\n // Bump version on any change; getSnapshot will rebuild next time\n versionRef.current += 1\n onStoreChange()\n })\n // Collection may be ready and will not receive initial `subscribeChanges()`\n if (collectionRef.current.status === `ready`) {\n versionRef.current += 1\n onStoreChange()\n }\n return () => {\n unsubscribe()\n }\n }\n }\n\n // Create stable getSnapshot function using ref\n const getSnapshotRef = useRef<\n | (() => {\n collection: Collection<object, string | number, {}> | null\n version: number\n })\n | null\n >(null)\n if (!getSnapshotRef.current || needsNewCollection) {\n getSnapshotRef.current = () => {\n const currentVersion = versionRef.current\n const currentCollection = collectionRef.current\n\n // Recreate snapshot object only if version/collection changed\n if (\n !snapshotRef.current ||\n snapshotRef.current.version !== currentVersion ||\n snapshotRef.current.collection !== currentCollection\n ) {\n snapshotRef.current = {\n collection: currentCollection,\n version: currentVersion,\n }\n }\n\n return snapshotRef.current\n }\n }\n\n // Use useSyncExternalStore to subscribe to collection changes\n const snapshot = useSyncExternalStore(\n subscribeRef.current,\n getSnapshotRef.current\n )\n\n // Track last snapshot (from useSyncExternalStore) and the returned value separately\n const returnedSnapshotRef = useRef<{\n collection: Collection<object, string | number, {}> | null\n version: number\n } | null>(null)\n // Keep implementation return loose to satisfy overload signatures\n const returnedRef = useRef<any>(null)\n\n // Rebuild returned object only when the snapshot changes (version or collection identity)\n if (\n !returnedSnapshotRef.current ||\n returnedSnapshotRef.current.version !== snapshot.version ||\n returnedSnapshotRef.current.collection !== snapshot.collection\n ) {\n // Handle null collection case (when callback returns undefined/null)\n if (!snapshot.collection) {\n returnedRef.current = {\n state: undefined,\n data: undefined,\n collection: undefined,\n status: `disabled`,\n isLoading: false,\n isReady: false,\n isIdle: false,\n isError: false,\n isCleanedUp: false,\n isEnabled: false,\n }\n } else {\n // Capture a stable view of entries for this snapshot to avoid tearing\n const entries = Array.from(snapshot.collection.entries())\n let stateCache: Map<string | number, unknown> | null = null\n let dataCache: Array<unknown> | null = null\n\n returnedRef.current = {\n get state() {\n if (!stateCache) {\n stateCache = new Map(entries)\n }\n return stateCache\n },\n get data() {\n if (!dataCache) {\n dataCache = entries.map(([, value]) => value)\n }\n return dataCache\n },\n collection: snapshot.collection,\n status: snapshot.collection.status,\n isLoading:\n snapshot.collection.status === `loading` ||\n snapshot.collection.status === `initialCommit`,\n isReady: snapshot.collection.status === `ready`,\n isIdle: snapshot.collection.status === `idle`,\n isError: snapshot.collection.status === `error`,\n isCleanedUp: snapshot.collection.status === `cleaned-up`,\n isEnabled: true,\n }\n }\n\n // Remember the snapshot that produced this returned value\n returnedSnapshotRef.current = snapshot\n }\n\n return returnedRef.current!\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-db",
|
|
3
3
|
"description": "React integration for @tanstack/db",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.21",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"use-sync-external-store": "^1.5.0",
|
|
20
|
-
"@tanstack/db": "0.3.
|
|
20
|
+
"@tanstack/db": "0.3.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@electric-sql/client": "1.0.0",
|
package/src/useLiveQuery.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useRef, useSyncExternalStore } from "react"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BaseQueryBuilder,
|
|
4
|
+
CollectionImpl,
|
|
5
|
+
createLiveQueryCollection,
|
|
6
|
+
} from "@tanstack/db"
|
|
3
7
|
import type {
|
|
4
8
|
Collection,
|
|
5
9
|
CollectionStatus,
|
|
@@ -10,6 +14,10 @@ import type {
|
|
|
10
14
|
QueryBuilder,
|
|
11
15
|
} from "@tanstack/db"
|
|
12
16
|
|
|
17
|
+
const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)
|
|
18
|
+
|
|
19
|
+
export type UseLiveQueryStatus = CollectionStatus | `disabled`
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* Create a live query using a query function
|
|
15
23
|
* @param queryFn - Query function that defines what data to fetch
|
|
@@ -60,7 +68,7 @@ import type {
|
|
|
60
68
|
* </ul>
|
|
61
69
|
* )
|
|
62
70
|
*/
|
|
63
|
-
// Overload 1: Accept
|
|
71
|
+
// Overload 1: Accept query function that always returns QueryBuilder
|
|
64
72
|
export function useLiveQuery<TContext extends Context>(
|
|
65
73
|
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
|
|
66
74
|
deps?: Array<unknown>
|
|
@@ -68,12 +76,109 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
68
76
|
state: Map<string | number, GetResult<TContext>>
|
|
69
77
|
data: Array<GetResult<TContext>>
|
|
70
78
|
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
71
|
-
status: CollectionStatus
|
|
79
|
+
status: CollectionStatus // Can't be disabled if always returns QueryBuilder
|
|
80
|
+
isLoading: boolean
|
|
81
|
+
isReady: boolean
|
|
82
|
+
isIdle: boolean
|
|
83
|
+
isError: boolean
|
|
84
|
+
isCleanedUp: boolean
|
|
85
|
+
isEnabled: true // Always true if always returns QueryBuilder
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Overload 2: Accept query function that can return undefined/null
|
|
89
|
+
export function useLiveQuery<TContext extends Context>(
|
|
90
|
+
queryFn: (
|
|
91
|
+
q: InitialQueryBuilder
|
|
92
|
+
) => QueryBuilder<TContext> | undefined | null,
|
|
93
|
+
deps?: Array<unknown>
|
|
94
|
+
): {
|
|
95
|
+
state: Map<string | number, GetResult<TContext>> | undefined
|
|
96
|
+
data: Array<GetResult<TContext>> | undefined
|
|
97
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | undefined
|
|
98
|
+
status: UseLiveQueryStatus
|
|
99
|
+
isLoading: boolean
|
|
100
|
+
isReady: boolean
|
|
101
|
+
isIdle: boolean
|
|
102
|
+
isError: boolean
|
|
103
|
+
isCleanedUp: boolean
|
|
104
|
+
isEnabled: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Overload 3: Accept query function that can return LiveQueryCollectionConfig
|
|
108
|
+
export function useLiveQuery<TContext extends Context>(
|
|
109
|
+
queryFn: (
|
|
110
|
+
q: InitialQueryBuilder
|
|
111
|
+
) => LiveQueryCollectionConfig<TContext> | undefined | null,
|
|
112
|
+
deps?: Array<unknown>
|
|
113
|
+
): {
|
|
114
|
+
state: Map<string | number, GetResult<TContext>> | undefined
|
|
115
|
+
data: Array<GetResult<TContext>> | undefined
|
|
116
|
+
collection: Collection<GetResult<TContext>, string | number, {}> | undefined
|
|
117
|
+
status: UseLiveQueryStatus
|
|
118
|
+
isLoading: boolean
|
|
119
|
+
isReady: boolean
|
|
120
|
+
isIdle: boolean
|
|
121
|
+
isError: boolean
|
|
122
|
+
isCleanedUp: boolean
|
|
123
|
+
isEnabled: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Overload 4: Accept query function that can return Collection
|
|
127
|
+
export function useLiveQuery<
|
|
128
|
+
TResult extends object,
|
|
129
|
+
TKey extends string | number,
|
|
130
|
+
TUtils extends Record<string, any>,
|
|
131
|
+
>(
|
|
132
|
+
queryFn: (
|
|
133
|
+
q: InitialQueryBuilder
|
|
134
|
+
) => Collection<TResult, TKey, TUtils> | undefined | null,
|
|
135
|
+
deps?: Array<unknown>
|
|
136
|
+
): {
|
|
137
|
+
state: Map<TKey, TResult> | undefined
|
|
138
|
+
data: Array<TResult> | undefined
|
|
139
|
+
collection: Collection<TResult, TKey, TUtils> | undefined
|
|
140
|
+
status: UseLiveQueryStatus
|
|
72
141
|
isLoading: boolean
|
|
73
142
|
isReady: boolean
|
|
74
143
|
isIdle: boolean
|
|
75
144
|
isError: boolean
|
|
76
145
|
isCleanedUp: boolean
|
|
146
|
+
isEnabled: boolean
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Overload 5: Accept query function that can return all types
|
|
150
|
+
export function useLiveQuery<
|
|
151
|
+
TContext extends Context,
|
|
152
|
+
TResult extends object,
|
|
153
|
+
TKey extends string | number,
|
|
154
|
+
TUtils extends Record<string, any>,
|
|
155
|
+
>(
|
|
156
|
+
queryFn: (
|
|
157
|
+
q: InitialQueryBuilder
|
|
158
|
+
) =>
|
|
159
|
+
| QueryBuilder<TContext>
|
|
160
|
+
| LiveQueryCollectionConfig<TContext>
|
|
161
|
+
| Collection<TResult, TKey, TUtils>
|
|
162
|
+
| undefined
|
|
163
|
+
| null,
|
|
164
|
+
deps?: Array<unknown>
|
|
165
|
+
): {
|
|
166
|
+
state:
|
|
167
|
+
| Map<string | number, GetResult<TContext>>
|
|
168
|
+
| Map<TKey, TResult>
|
|
169
|
+
| undefined
|
|
170
|
+
data: Array<GetResult<TContext>> | Array<TResult> | undefined
|
|
171
|
+
collection:
|
|
172
|
+
| Collection<GetResult<TContext>, string | number, {}>
|
|
173
|
+
| Collection<TResult, TKey, TUtils>
|
|
174
|
+
| undefined
|
|
175
|
+
status: UseLiveQueryStatus
|
|
176
|
+
isLoading: boolean
|
|
177
|
+
isReady: boolean
|
|
178
|
+
isIdle: boolean
|
|
179
|
+
isError: boolean
|
|
180
|
+
isCleanedUp: boolean
|
|
181
|
+
isEnabled: boolean
|
|
77
182
|
}
|
|
78
183
|
|
|
79
184
|
/**
|
|
@@ -109,7 +214,7 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
109
214
|
*
|
|
110
215
|
* return <div>{data.length} items loaded</div>
|
|
111
216
|
*/
|
|
112
|
-
// Overload
|
|
217
|
+
// Overload 6: Accept config object
|
|
113
218
|
export function useLiveQuery<TContext extends Context>(
|
|
114
219
|
config: LiveQueryCollectionConfig<TContext>,
|
|
115
220
|
deps?: Array<unknown>
|
|
@@ -117,12 +222,13 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
117
222
|
state: Map<string | number, GetResult<TContext>>
|
|
118
223
|
data: Array<GetResult<TContext>>
|
|
119
224
|
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
120
|
-
status: CollectionStatus
|
|
225
|
+
status: CollectionStatus // Can't be disabled for config objects
|
|
121
226
|
isLoading: boolean
|
|
122
227
|
isReady: boolean
|
|
123
228
|
isIdle: boolean
|
|
124
229
|
isError: boolean
|
|
125
230
|
isCleanedUp: boolean
|
|
231
|
+
isEnabled: true // Always true for config objects
|
|
126
232
|
}
|
|
127
233
|
|
|
128
234
|
/**
|
|
@@ -154,7 +260,7 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
154
260
|
*
|
|
155
261
|
* return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>
|
|
156
262
|
*/
|
|
157
|
-
// Overload
|
|
263
|
+
// Overload 7: Accept pre-created live query collection
|
|
158
264
|
export function useLiveQuery<
|
|
159
265
|
TResult extends object,
|
|
160
266
|
TKey extends string | number,
|
|
@@ -165,12 +271,13 @@ export function useLiveQuery<
|
|
|
165
271
|
state: Map<TKey, TResult>
|
|
166
272
|
data: Array<TResult>
|
|
167
273
|
collection: Collection<TResult, TKey, TUtils>
|
|
168
|
-
status: CollectionStatus
|
|
274
|
+
status: CollectionStatus // Can't be disabled for pre-created live query collections
|
|
169
275
|
isLoading: boolean
|
|
170
276
|
isReady: boolean
|
|
171
277
|
isIdle: boolean
|
|
172
278
|
isError: boolean
|
|
173
279
|
isCleanedUp: boolean
|
|
280
|
+
isEnabled: true // Always true for pre-created live query collections
|
|
174
281
|
}
|
|
175
282
|
|
|
176
283
|
// Implementation - use function overloads to infer the actual collection type
|
|
@@ -193,6 +300,13 @@ export function useLiveQuery(
|
|
|
193
300
|
const depsRef = useRef<Array<unknown> | null>(null)
|
|
194
301
|
const configRef = useRef<unknown>(null)
|
|
195
302
|
|
|
303
|
+
// Use refs to track version and memoized snapshot
|
|
304
|
+
const versionRef = useRef(0)
|
|
305
|
+
const snapshotRef = useRef<{
|
|
306
|
+
collection: Collection<object, string | number, {}> | null
|
|
307
|
+
version: number
|
|
308
|
+
} | null>(null)
|
|
309
|
+
|
|
196
310
|
// Check if we need to create/recreate the collection
|
|
197
311
|
const needsNewCollection =
|
|
198
312
|
!collectionRef.current ||
|
|
@@ -209,32 +323,53 @@ export function useLiveQuery(
|
|
|
209
323
|
collectionRef.current = configOrQueryOrCollection
|
|
210
324
|
configRef.current = configOrQueryOrCollection
|
|
211
325
|
} else {
|
|
212
|
-
//
|
|
213
|
-
// Ensure we always start sync for React hooks
|
|
326
|
+
// Handle different callback return types
|
|
214
327
|
if (typeof configOrQueryOrCollection === `function`) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
328
|
+
// Call the function with a query builder to see what it returns
|
|
329
|
+
const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder
|
|
330
|
+
const result = configOrQueryOrCollection(queryBuilder)
|
|
331
|
+
|
|
332
|
+
if (result === undefined || result === null) {
|
|
333
|
+
// Callback returned undefined/null - disabled query
|
|
334
|
+
collectionRef.current = null
|
|
335
|
+
} else if (result instanceof CollectionImpl) {
|
|
336
|
+
// Callback returned a Collection instance - use it directly
|
|
337
|
+
result.startSyncImmediate()
|
|
338
|
+
collectionRef.current = result
|
|
339
|
+
} else if (result instanceof BaseQueryBuilder) {
|
|
340
|
+
// Callback returned QueryBuilder - create live query collection using the original callback
|
|
341
|
+
// (not the result, since the result might be from a different query builder instance)
|
|
342
|
+
collectionRef.current = createLiveQueryCollection({
|
|
343
|
+
query: configOrQueryOrCollection,
|
|
344
|
+
startSync: true,
|
|
345
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
346
|
+
})
|
|
347
|
+
} else if (result && typeof result === `object`) {
|
|
348
|
+
// Assume it's a LiveQueryCollectionConfig
|
|
349
|
+
collectionRef.current = createLiveQueryCollection({
|
|
350
|
+
startSync: true,
|
|
351
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
352
|
+
...result,
|
|
353
|
+
})
|
|
354
|
+
} else {
|
|
355
|
+
// Unexpected return type
|
|
356
|
+
throw new Error(
|
|
357
|
+
`useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
depsRef.current = [...deps]
|
|
220
361
|
} else {
|
|
362
|
+
// Original logic for config objects
|
|
221
363
|
collectionRef.current = createLiveQueryCollection({
|
|
222
364
|
startSync: true,
|
|
223
|
-
gcTime:
|
|
365
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
224
366
|
...configOrQueryOrCollection,
|
|
225
|
-
})
|
|
367
|
+
})
|
|
368
|
+
depsRef.current = [...deps]
|
|
226
369
|
}
|
|
227
|
-
depsRef.current = [...deps]
|
|
228
370
|
}
|
|
229
371
|
}
|
|
230
372
|
|
|
231
|
-
// Use refs to track version and memoized snapshot
|
|
232
|
-
const versionRef = useRef(0)
|
|
233
|
-
const snapshotRef = useRef<{
|
|
234
|
-
collection: Collection<object, string | number, {}>
|
|
235
|
-
version: number
|
|
236
|
-
} | null>(null)
|
|
237
|
-
|
|
238
373
|
// Reset refs when collection changes
|
|
239
374
|
if (needsNewCollection) {
|
|
240
375
|
versionRef.current = 0
|
|
@@ -247,13 +382,18 @@ export function useLiveQuery(
|
|
|
247
382
|
>(null)
|
|
248
383
|
if (!subscribeRef.current || needsNewCollection) {
|
|
249
384
|
subscribeRef.current = (onStoreChange: () => void) => {
|
|
250
|
-
|
|
385
|
+
// If no collection, return a no-op unsubscribe function
|
|
386
|
+
if (!collectionRef.current) {
|
|
387
|
+
return () => {}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const unsubscribe = collectionRef.current.subscribeChanges(() => {
|
|
251
391
|
// Bump version on any change; getSnapshot will rebuild next time
|
|
252
392
|
versionRef.current += 1
|
|
253
393
|
onStoreChange()
|
|
254
394
|
})
|
|
255
395
|
// Collection may be ready and will not receive initial `subscribeChanges()`
|
|
256
|
-
if (collectionRef.current
|
|
396
|
+
if (collectionRef.current.status === `ready`) {
|
|
257
397
|
versionRef.current += 1
|
|
258
398
|
onStoreChange()
|
|
259
399
|
}
|
|
@@ -266,7 +406,7 @@ export function useLiveQuery(
|
|
|
266
406
|
// Create stable getSnapshot function using ref
|
|
267
407
|
const getSnapshotRef = useRef<
|
|
268
408
|
| (() => {
|
|
269
|
-
collection: Collection<object, string | number, {}>
|
|
409
|
+
collection: Collection<object, string | number, {}> | null
|
|
270
410
|
version: number
|
|
271
411
|
})
|
|
272
412
|
| null
|
|
@@ -274,7 +414,7 @@ export function useLiveQuery(
|
|
|
274
414
|
if (!getSnapshotRef.current || needsNewCollection) {
|
|
275
415
|
getSnapshotRef.current = () => {
|
|
276
416
|
const currentVersion = versionRef.current
|
|
277
|
-
const currentCollection = collectionRef.current
|
|
417
|
+
const currentCollection = collectionRef.current
|
|
278
418
|
|
|
279
419
|
// Recreate snapshot object only if version/collection changed
|
|
280
420
|
if (
|
|
@@ -300,7 +440,7 @@ export function useLiveQuery(
|
|
|
300
440
|
|
|
301
441
|
// Track last snapshot (from useSyncExternalStore) and the returned value separately
|
|
302
442
|
const returnedSnapshotRef = useRef<{
|
|
303
|
-
collection: Collection<object, string | number, {}>
|
|
443
|
+
collection: Collection<object, string | number, {}> | null
|
|
304
444
|
version: number
|
|
305
445
|
} | null>(null)
|
|
306
446
|
// Keep implementation return loose to satisfy overload signatures
|
|
@@ -312,33 +452,50 @@ export function useLiveQuery(
|
|
|
312
452
|
returnedSnapshotRef.current.version !== snapshot.version ||
|
|
313
453
|
returnedSnapshotRef.current.collection !== snapshot.collection
|
|
314
454
|
) {
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
455
|
+
// Handle null collection case (when callback returns undefined/null)
|
|
456
|
+
if (!snapshot.collection) {
|
|
457
|
+
returnedRef.current = {
|
|
458
|
+
state: undefined,
|
|
459
|
+
data: undefined,
|
|
460
|
+
collection: undefined,
|
|
461
|
+
status: `disabled`,
|
|
462
|
+
isLoading: false,
|
|
463
|
+
isReady: false,
|
|
464
|
+
isIdle: false,
|
|
465
|
+
isError: false,
|
|
466
|
+
isCleanedUp: false,
|
|
467
|
+
isEnabled: false,
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
// Capture a stable view of entries for this snapshot to avoid tearing
|
|
471
|
+
const entries = Array.from(snapshot.collection.entries())
|
|
472
|
+
let stateCache: Map<string | number, unknown> | null = null
|
|
473
|
+
let dataCache: Array<unknown> | null = null
|
|
319
474
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
475
|
+
returnedRef.current = {
|
|
476
|
+
get state() {
|
|
477
|
+
if (!stateCache) {
|
|
478
|
+
stateCache = new Map(entries)
|
|
479
|
+
}
|
|
480
|
+
return stateCache
|
|
481
|
+
},
|
|
482
|
+
get data() {
|
|
483
|
+
if (!dataCache) {
|
|
484
|
+
dataCache = entries.map(([, value]) => value)
|
|
485
|
+
}
|
|
486
|
+
return dataCache
|
|
487
|
+
},
|
|
488
|
+
collection: snapshot.collection,
|
|
489
|
+
status: snapshot.collection.status,
|
|
490
|
+
isLoading:
|
|
491
|
+
snapshot.collection.status === `loading` ||
|
|
492
|
+
snapshot.collection.status === `initialCommit`,
|
|
493
|
+
isReady: snapshot.collection.status === `ready`,
|
|
494
|
+
isIdle: snapshot.collection.status === `idle`,
|
|
495
|
+
isError: snapshot.collection.status === `error`,
|
|
496
|
+
isCleanedUp: snapshot.collection.status === `cleaned-up`,
|
|
497
|
+
isEnabled: true,
|
|
498
|
+
}
|
|
342
499
|
}
|
|
343
500
|
|
|
344
501
|
// Remember the snapshot that produced this returned value
|