@tanstack/solid-db 0.2.45 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/useLiveQuery.d.ts +20 -3
- package/dist/esm/useLiveQuery.js +71 -8
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/useLiveQuery.ts +106 -10
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReactiveMap } from '@solid-primitives/map';
|
|
2
2
|
import { Accessor } from 'solid-js';
|
|
3
|
-
import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
|
|
3
|
+
import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryPersistedStatus, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
|
|
4
|
+
type InferConditionalResultType<TContext extends Context> = TContext extends SingleResult ? InferResultType<TContext> | [] : InferResultType<TContext>;
|
|
4
5
|
/**
|
|
5
6
|
* Create a live query using a query function
|
|
6
7
|
* @param queryFn - Query function that defines what data to fetch
|
|
@@ -81,21 +82,27 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
|
|
|
81
82
|
status: CollectionStatus;
|
|
82
83
|
isLoading: boolean;
|
|
83
84
|
isReady: boolean;
|
|
85
|
+
persistedStatus: LiveQueryPersistedStatus;
|
|
86
|
+
isPersistedReady: boolean;
|
|
87
|
+
persistedError: unknown | undefined;
|
|
84
88
|
isIdle: boolean;
|
|
85
89
|
isError: boolean;
|
|
86
90
|
isCleanedUp: boolean;
|
|
87
91
|
};
|
|
88
|
-
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): Accessor<
|
|
92
|
+
export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): Accessor<InferConditionalResultType<TContext>> & {
|
|
89
93
|
/**
|
|
90
94
|
* @deprecated use function result instead
|
|
91
95
|
* query.data -> query()
|
|
92
96
|
*/
|
|
93
|
-
data:
|
|
97
|
+
data: InferConditionalResultType<TContext>;
|
|
94
98
|
state: ReactiveMap<string | number, GetResult<TContext>>;
|
|
95
99
|
collection: Collection<GetResult<TContext>, string | number, {}> | null;
|
|
96
100
|
status: CollectionStatus | `disabled`;
|
|
97
101
|
isLoading: boolean;
|
|
98
102
|
isReady: boolean;
|
|
103
|
+
persistedStatus: LiveQueryPersistedStatus;
|
|
104
|
+
isPersistedReady: boolean;
|
|
105
|
+
persistedError: unknown | undefined;
|
|
99
106
|
isIdle: boolean;
|
|
100
107
|
isError: boolean;
|
|
101
108
|
isCleanedUp: boolean;
|
|
@@ -151,6 +158,9 @@ export declare function useLiveQuery<TContext extends Context>(config: Accessor<
|
|
|
151
158
|
status: CollectionStatus;
|
|
152
159
|
isLoading: boolean;
|
|
153
160
|
isReady: boolean;
|
|
161
|
+
persistedStatus: LiveQueryPersistedStatus;
|
|
162
|
+
isPersistedReady: boolean;
|
|
163
|
+
persistedError: unknown | undefined;
|
|
154
164
|
isIdle: boolean;
|
|
155
165
|
isError: boolean;
|
|
156
166
|
isCleanedUp: boolean;
|
|
@@ -201,6 +211,9 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
|
|
|
201
211
|
status: CollectionStatus;
|
|
202
212
|
isLoading: boolean;
|
|
203
213
|
isReady: boolean;
|
|
214
|
+
persistedStatus: LiveQueryPersistedStatus;
|
|
215
|
+
isPersistedReady: boolean;
|
|
216
|
+
persistedError: unknown | undefined;
|
|
204
217
|
isIdle: boolean;
|
|
205
218
|
isError: boolean;
|
|
206
219
|
isCleanedUp: boolean;
|
|
@@ -216,7 +229,11 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
|
|
|
216
229
|
status: CollectionStatus;
|
|
217
230
|
isLoading: boolean;
|
|
218
231
|
isReady: boolean;
|
|
232
|
+
persistedStatus: LiveQueryPersistedStatus;
|
|
233
|
+
isPersistedReady: boolean;
|
|
234
|
+
persistedError: unknown | undefined;
|
|
219
235
|
isIdle: boolean;
|
|
220
236
|
isError: boolean;
|
|
221
237
|
isCleanedUp: boolean;
|
|
222
238
|
};
|
|
239
|
+
export {};
|
package/dist/esm/useLiveQuery.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMemo, createSignal, createResource, batch, createEffect
|
|
1
|
+
import { createMemo, createSignal, onCleanup, createResource, batch, createEffect } from "solid-js";
|
|
2
2
|
import { ReactiveMap } from "@solid-primitives/map";
|
|
3
3
|
import { BaseQueryBuilder, createLiveQueryCollection, isCollection, createLiveQueryObserver, isSingleResultCollection } from "@tanstack/db";
|
|
4
4
|
import { createStore, reconcile } from "solid-js/store";
|
|
@@ -33,6 +33,8 @@ function useLiveQuery(configOrQueryOrCollection) {
|
|
|
33
33
|
{ name: `TanstackDBCollectionMemo` }
|
|
34
34
|
);
|
|
35
35
|
const state = new ReactiveMap();
|
|
36
|
+
const rowsByKey = /* @__PURE__ */ new Map();
|
|
37
|
+
let rowsCollection;
|
|
36
38
|
const [data, setData] = createStore([], {
|
|
37
39
|
name: `TanstackDBData`
|
|
38
40
|
});
|
|
@@ -42,24 +44,56 @@ function useLiveQuery(configOrQueryOrCollection) {
|
|
|
42
44
|
name: `TanstackDBStatus`
|
|
43
45
|
}
|
|
44
46
|
);
|
|
47
|
+
const [persistedStatus, setLocalStatus] = createSignal(`unavailable`);
|
|
48
|
+
const [persistedError, setLocalError] = createSignal(void 0);
|
|
45
49
|
const syncDataFromCollection = (currentCollection) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
50
|
+
const nextRows = [];
|
|
51
|
+
const retainedKeys = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const [key, value] of currentCollection.entries()) {
|
|
53
|
+
retainedKeys.add(key);
|
|
54
|
+
const existing = rowsByKey.get(key);
|
|
55
|
+
if (existing) {
|
|
56
|
+
existing.update(value);
|
|
57
|
+
nextRows.push(existing.value);
|
|
58
|
+
} else {
|
|
59
|
+
const [row, setRow] = createStore(value);
|
|
60
|
+
rowsByKey.set(key, {
|
|
61
|
+
value: row,
|
|
62
|
+
update: (nextValue) => setRow(reconcile(nextValue, { key: null }))
|
|
63
|
+
});
|
|
64
|
+
nextRows.push(row);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const key of rowsByKey.keys()) {
|
|
68
|
+
if (!retainedKeys.has(key)) rowsByKey.delete(key);
|
|
69
|
+
}
|
|
70
|
+
setData((previous) => reconcile(nextRows, { key: null })(previous));
|
|
49
71
|
};
|
|
50
72
|
let resourceGeneration = 0;
|
|
73
|
+
let localWaitObserver;
|
|
74
|
+
onCleanup(() => localWaitObserver?.dispose());
|
|
51
75
|
const [getDataResource] = createResource(
|
|
52
76
|
() => ({ currentCollection: collection() }),
|
|
53
77
|
async ({ currentCollection }) => {
|
|
54
78
|
const generation = ++resourceGeneration;
|
|
79
|
+
localWaitObserver?.dispose();
|
|
80
|
+
localWaitObserver = void 0;
|
|
55
81
|
if (!currentCollection) {
|
|
56
82
|
return [];
|
|
57
83
|
}
|
|
58
84
|
setStatus(currentCollection.status);
|
|
59
85
|
try {
|
|
60
|
-
|
|
86
|
+
const observer = createLiveQueryObserver(currentCollection);
|
|
87
|
+
localWaitObserver = observer;
|
|
88
|
+
try {
|
|
89
|
+
await observer.preloadForInitialRender();
|
|
90
|
+
} finally {
|
|
91
|
+
observer.dispose();
|
|
92
|
+
if (localWaitObserver === observer) localWaitObserver = void 0;
|
|
93
|
+
}
|
|
61
94
|
} catch (error) {
|
|
62
|
-
if (generation
|
|
95
|
+
if (generation !== resourceGeneration) return data;
|
|
96
|
+
setStatus(`error`);
|
|
63
97
|
throw error;
|
|
64
98
|
}
|
|
65
99
|
if (generation !== resourceGeneration) {
|
|
@@ -85,10 +119,18 @@ function useLiveQuery(configOrQueryOrCollection) {
|
|
|
85
119
|
const currentCollection = collection();
|
|
86
120
|
if (!currentCollection) {
|
|
87
121
|
setStatus(`disabled`);
|
|
122
|
+
setLocalStatus(`unavailable`);
|
|
123
|
+
setLocalError(void 0);
|
|
88
124
|
state.clear();
|
|
125
|
+
rowsByKey.clear();
|
|
126
|
+
rowsCollection = void 0;
|
|
89
127
|
setData([]);
|
|
90
128
|
return;
|
|
91
129
|
}
|
|
130
|
+
if (rowsCollection !== currentCollection) {
|
|
131
|
+
rowsByKey.clear();
|
|
132
|
+
rowsCollection = currentCollection;
|
|
133
|
+
}
|
|
92
134
|
const observer = createLiveQueryObserver(currentCollection);
|
|
93
135
|
state.clear();
|
|
94
136
|
const unsubscribe = observer.subscribe(
|
|
@@ -113,13 +155,19 @@ function useLiveQuery(configOrQueryOrCollection) {
|
|
|
113
155
|
}
|
|
114
156
|
}
|
|
115
157
|
syncDataFromCollection(currentCollection);
|
|
116
|
-
|
|
158
|
+
const snapshot = observer.getSnapshot();
|
|
159
|
+
setStatus(snapshot.status);
|
|
160
|
+
setLocalStatus(snapshot.persistedStatus);
|
|
161
|
+
setLocalError(snapshot.persistedError);
|
|
117
162
|
});
|
|
118
163
|
}
|
|
119
164
|
);
|
|
120
165
|
batch(() => {
|
|
121
166
|
syncDataFromCollection(currentCollection);
|
|
122
|
-
|
|
167
|
+
const snapshot = observer.getSnapshot();
|
|
168
|
+
setStatus(snapshot.status);
|
|
169
|
+
setLocalStatus(snapshot.persistedStatus);
|
|
170
|
+
setLocalError(snapshot.persistedError);
|
|
123
171
|
});
|
|
124
172
|
onCleanup(() => {
|
|
125
173
|
unsubscribe();
|
|
@@ -167,6 +215,21 @@ function useLiveQuery(configOrQueryOrCollection) {
|
|
|
167
215
|
return status() === `ready` || status() === `disabled`;
|
|
168
216
|
}
|
|
169
217
|
},
|
|
218
|
+
persistedStatus: {
|
|
219
|
+
get() {
|
|
220
|
+
return persistedStatus();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
isPersistedReady: {
|
|
224
|
+
get() {
|
|
225
|
+
return persistedStatus() === `ready`;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
persistedError: {
|
|
229
|
+
get() {
|
|
230
|
+
return persistedError();
|
|
231
|
+
}
|
|
232
|
+
},
|
|
170
233
|
isIdle: {
|
|
171
234
|
get() {
|
|
172
235
|
return status() === `idle`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n batch,\n createEffect,\n createMemo,\n createResource,\n createSignal,\n onCleanup,\n} from 'solid-js'\nimport { ReactiveMap } from '@solid-primitives/map'\nimport {\n BaseQueryBuilder,\n createLiveQueryCollection,\n createLiveQueryObserver,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport { createStore, reconcile } from 'solid-js/store'\nimport type { Accessor } from 'solid-js'\nimport type {\n ChangeMessage,\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\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 * @returns Accessor that returns data with Suspense support, with state and status information as properties\n * @example\n * // Basic query with object syntax\n * const todosQuery = 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 todosQuery = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority())),\n * )\n *\n * @example\n * // Join pattern\n * const personIssues = 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 todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * return (\n * <Switch>\n * <Match when={todosQuery.isLoading}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={todosQuery.isError}>\n * <div>Error: {todosQuery.status}</div>\n * </Match>\n * <Match when={todosQuery.isReady}>\n * <For each={todosQuery()}>\n * {(todo) => <li key={todo.id}>{todo.text}</li>}\n * </For>\n * </Match>\n * </Switch>\n * )\n *\n * @example\n * // Use Suspense boundaries\n * const todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <For each={todosQuery()}>\n * {(todo) => <li key={todo.id}>{todo.text}</li>}\n * </For>\n * </Suspense>\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): Accessor<InferResultType<TContext>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: InferResultType<TContext>\n state: ReactiveMap<string | number, 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// Overload 1b: 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): Accessor<InferResultType<TContext>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: InferResultType<TContext>\n state: ReactiveMap<string | number, GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}> | null\n status: CollectionStatus | `disabled`\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 * @returns Accessor that returns data with Suspense support, with state and status information as properties\n * @example\n * // Basic config object usage\n * const todosQuery = 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 personsQuery = useLiveQuery(() => ({ query: queryBuilder }))\n *\n * @example\n * // Handle all states uniformly\n * const itemsQuery = useLiveQuery(() => ({\n * query: (q) => q.from({ items: itemCollection })\n * }))\n *\n * return (\n * <Switch fallback={<div>{itemsQuery().length} items loaded</div>}>\n * <Match when={itemsQuery.isLoading}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={itemsQuery.isError}>\n * <div>Something went wrong</div>\n * </Match>\n * <Match when={!itemsQuery.isReady}>\n * <div>Preparing...</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: Accessor<LiveQueryCollectionConfig<TContext>>,\n): Accessor<InferResultType<TContext>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: InferResultType<TContext>\n state: ReactiveMap<string | number, 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 Accessor that returns data with Suspense support, with state and status information as properties\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 todosQuery = useLiveQuery(() => myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const existingQuery = useLiveQuery(() => existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * existingQuery.collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const sharedQuery = useLiveQuery(() => sharedCollection)\n *\n * return (\n * <Switch fallback={<div><For each={sharedQuery()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>\n * <Match when={sharedQuery.isLoading}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={sharedQuery.isError}>\n * <div>Error loading data</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 3: Accept pre-created live query collection (non-single result)\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<\n Collection<TResult, TKey, TUtils> & NonSingleResult\n >,\n): Accessor<Array<TResult>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: Array<TResult>\n state: ReactiveMap<TKey, 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// Overload 3b: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<\n Collection<TResult, TKey, TUtils> & SingleResult\n >,\n): Accessor<TResult | undefined> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: TResult | undefined\n state: ReactiveMap<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & SingleResult\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: (queryFn?: any) => any,\n) {\n const collection = createMemo(\n () => {\n if (configOrQueryOrCollection.length === 1) {\n // This is a query function - check if it returns null/undefined\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n })\n }\n\n const innerCollection = configOrQueryOrCollection()\n\n if (innerCollection === undefined || innerCollection === null) {\n // Disabled query - return null\n return null\n }\n\n if (isCollection(innerCollection)) {\n innerCollection.startSyncImmediate()\n return innerCollection as Collection\n }\n\n return createLiveQueryCollection({\n ...innerCollection,\n startSync: true,\n })\n },\n undefined,\n { name: `TanstackDBCollectionMemo` },\n )\n\n // Reactive state that gets updated granularly through change events\n const state = new ReactiveMap<string | number, any>()\n\n // Reactive data array that maintains sorted order\n const [data, setData] = createStore<Array<any>>([], {\n name: `TanstackDBData`,\n })\n\n // Track collection status reactively\n const [status, setStatus] = createSignal(\n collection() ? collection()!.status : (`disabled` as const),\n {\n name: `TanstackDBStatus`,\n },\n )\n\n // Helper to sync data array from collection in correct order\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>,\n ) => {\n setData((prev) =>\n reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean),\n )\n }\n\n // Generation guard for the resource's async continuations: Solid discards a\n // superseded fetch's *return value*, but the writes below are side effects\n // into hook-scoped state and would still run — resurrecting rows/status from\n // a collection that has already been replaced.\n let resourceGeneration = 0\n\n const [getDataResource] = createResource(\n () => ({ currentCollection: collection() }),\n async ({ currentCollection }) => {\n const generation = ++resourceGeneration\n if (!currentCollection) {\n return []\n }\n setStatus(currentCollection.status)\n try {\n await currentCollection.toArrayWhenReady()\n } catch (error) {\n if (generation === resourceGeneration) setStatus(`error`)\n throw error\n }\n if (generation !== resourceGeneration) {\n return data\n }\n // Initialize state with current collection data\n batch(() => {\n state.clear()\n for (const [key, value] of currentCollection.entries()) {\n state.set(key, value)\n }\n syncDataFromCollection(currentCollection)\n setStatus(currentCollection.status)\n })\n return data\n },\n {\n name: `TanstackDBData`,\n deferStream: false,\n initialValue: data,\n },\n )\n\n createEffect(() => {\n const currentCollection = collection()\n if (!currentCollection) {\n setStatus(`disabled` as const)\n state.clear()\n setData([])\n return\n }\n\n // The shared observer owns subscription, the ready-race, and status; Solid\n // materializes into its keyed ReactiveMap (granular) + reconciled store.\n const observer = createLiveQueryObserver(currentCollection)\n // Clear any keys carried over from a previous collection before the new\n // observer re-seeds via `includeInitialState` (which only inserts current\n // rows, never deletes stale ones). Without this, switching collections\n // leaves the dropped keys in `state` until the async resource reconciles.\n state.clear()\n const unsubscribe = observer.subscribe(\n (changes: Array<ChangeMessage<any>> | undefined) => {\n batch(() => {\n if (changes) {\n for (const change of changes) {\n switch (change.type) {\n case `insert`:\n case `update`:\n state.set(change.key, change.value)\n break\n case `delete`:\n state.delete(change.key)\n break\n }\n }\n } else {\n // Cleanup and other status-only publications carry no row deltas.\n // Rebuild the keyed view so it cannot diverge from ordered data.\n state.clear()\n for (const [key, value] of observer.getSnapshot().state ?? []) {\n state.set(key, value)\n }\n }\n syncDataFromCollection(currentCollection)\n setStatus(observer.getSnapshot().status)\n })\n },\n )\n // An already-ready empty collection produces no initial row batch. Bring\n // ordered data and status in line synchronously instead of waiting for the\n // resource continuation to correct the previous collection's rows.\n batch(() => {\n syncDataFromCollection(currentCollection)\n setStatus(observer.getSnapshot().status)\n })\n\n onCleanup(() => {\n unsubscribe()\n observer.dispose()\n })\n })\n\n // We have to remove getters from the resource function so we wrap it\n function getData() {\n const currentCollection = collection()\n if (currentCollection) {\n if (isSingleResultCollection(currentCollection)) {\n // Force resource tracking so Suspense works\n getDataResource()\n return data[0]\n }\n }\n return getDataResource()\n }\n\n Object.defineProperties(getData, {\n data: {\n get() {\n return getData()\n },\n },\n status: {\n get() {\n return status()\n },\n },\n collection: {\n get() {\n return collection()\n },\n },\n state: {\n get() {\n return state\n },\n },\n isLoading: {\n get() {\n return status() === `loading`\n },\n },\n isReady: {\n get() {\n return status() === `ready` || status() === `disabled`\n },\n },\n isIdle: {\n get() {\n return status() === `idle`\n },\n },\n isError: {\n get() {\n return status() === `error`\n },\n },\n isCleanedUp: {\n get() {\n return status() === `cleaned-up`\n },\n },\n })\n return getData\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n batch,\n createEffect,\n createMemo,\n createResource,\n createSignal,\n onCleanup,\n} from 'solid-js'\nimport { ReactiveMap } from '@solid-primitives/map'\nimport {\n BaseQueryBuilder,\n createLiveQueryCollection,\n createLiveQueryObserver,\n isCollection,\n isSingleResultCollection,\n} from '@tanstack/db'\nimport { createStore, reconcile } from 'solid-js/store'\nimport type { Accessor } from 'solid-js'\nimport type {\n ChangeMessage,\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\ntype InferConditionalResultType<TContext extends Context> =\n TContext extends SingleResult\n ? InferResultType<TContext> | []\n : InferResultType<TContext>\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @returns Accessor that returns data with Suspense support, with state and status information as properties\n * @example\n * // Basic query with object syntax\n * const todosQuery = 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 todosQuery = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority())),\n * )\n *\n * @example\n * // Join pattern\n * const personIssues = 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 todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * return (\n * <Switch>\n * <Match when={todosQuery.isLoading}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={todosQuery.isError}>\n * <div>Error: {todosQuery.status}</div>\n * </Match>\n * <Match when={todosQuery.isReady}>\n * <For each={todosQuery()}>\n * {(todo) => <li key={todo.id}>{todo.text}</li>}\n * </For>\n * </Match>\n * </Switch>\n * )\n *\n * @example\n * // Use Suspense boundaries\n * const todosQuery = useLiveQuery((q) =>\n * q.from({ todos: todoCollection })\n * )\n *\n * return (\n * <Suspense fallback={<div>Loading...</div>}>\n * <For each={todosQuery()}>\n * {(todo) => <li key={todo.id}>{todo.text}</li>}\n * </For>\n * </Suspense>\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): Accessor<InferResultType<TContext>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: InferResultType<TContext>\n state: ReactiveMap<string | number, GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n// Overload 1b: 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): Accessor<InferConditionalResultType<TContext>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: InferConditionalResultType<TContext>\n state: ReactiveMap<string | number, GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}> | null\n status: CollectionStatus | `disabled`\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @returns Accessor that returns data with Suspense support, with state and status information as properties\n * @example\n * // Basic config object usage\n * const todosQuery = 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 personsQuery = useLiveQuery(() => ({ query: queryBuilder }))\n *\n * @example\n * // Handle all states uniformly\n * const itemsQuery = useLiveQuery(() => ({\n * query: (q) => q.from({ items: itemCollection })\n * }))\n *\n * return (\n * <Switch fallback={<div>{itemsQuery().length} items loaded</div>}>\n * <Match when={itemsQuery.isLoading}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={itemsQuery.isError}>\n * <div>Something went wrong</div>\n * </Match>\n * <Match when={!itemsQuery.isReady}>\n * <div>Preparing...</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 2: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: Accessor<LiveQueryCollectionConfig<TContext>>,\n): Accessor<InferResultType<TContext>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: InferResultType<TContext>\n state: ReactiveMap<string | number, GetResult<TContext>>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Accessor that returns data with Suspense support, with state and status information as properties\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 todosQuery = useLiveQuery(() => myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const existingQuery = useLiveQuery(() => existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * existingQuery.collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const sharedQuery = useLiveQuery(() => sharedCollection)\n *\n * return (\n * <Switch fallback={<div><For each={sharedQuery()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>\n * <Match when={sharedQuery.isLoading}>\n * <div>Loading...</div>\n * </Match>\n * <Match when={sharedQuery.isError}>\n * <div>Error loading data</div>\n * </Match>\n * </Switch>\n * )\n */\n// Overload 3: Accept pre-created live query collection (non-single result)\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<\n Collection<TResult, TKey, TUtils> & NonSingleResult\n >,\n): Accessor<Array<TResult>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: Array<TResult>\n state: ReactiveMap<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n// Overload 3b: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<\n Collection<TResult, TKey, TUtils> & SingleResult\n >,\n): Accessor<TResult | undefined> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: TResult | undefined\n state: ReactiveMap<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: (queryFn?: any) => any,\n) {\n const collection = createMemo(\n () => {\n if (configOrQueryOrCollection.length === 1) {\n // This is a query function - check if it returns null/undefined\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Disabled query - return null\n return null\n }\n\n return createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n })\n }\n\n const innerCollection = configOrQueryOrCollection()\n\n if (innerCollection === undefined || innerCollection === null) {\n // Disabled query - return null\n return null\n }\n\n if (isCollection(innerCollection)) {\n innerCollection.startSyncImmediate()\n return innerCollection as Collection\n }\n\n return createLiveQueryCollection({\n ...innerCollection,\n startSync: true,\n })\n },\n undefined,\n { name: `TanstackDBCollectionMemo` },\n )\n\n // Reactive state that gets updated granularly through change events\n const state = new ReactiveMap<string | number, any>()\n\n // Keep the live Collection's result keys private while preserving one stable\n // Solid store per logical row. A row's public $key can belong to an upstream\n // Collection and is therefore not necessarily unique in this result.\n const rowsByKey = new Map<\n string | number,\n { value: any; update: (value: any) => void }\n >()\n let rowsCollection: Collection<any, any, any> | undefined\n const [data, setData] = createStore<Array<any>>([], {\n name: `TanstackDBData`,\n })\n\n // Track collection status reactively\n const [status, setStatus] = createSignal(\n collection() ? collection()!.status : (`disabled` as const),\n {\n name: `TanstackDBStatus`,\n },\n )\n const [persistedStatus, setLocalStatus] =\n createSignal<LiveQueryPersistedStatus>(`unavailable`)\n const [persistedError, setLocalError] = createSignal<unknown>(undefined)\n\n // Helper to sync data array from collection in correct order\n const syncDataFromCollection = (\n currentCollection: Collection<any, any, any>,\n ) => {\n const nextRows: Array<any> = []\n const retainedKeys = new Set<string | number>()\n\n for (const [key, value] of currentCollection.entries()) {\n retainedKeys.add(key)\n\n const existing = rowsByKey.get(key)\n if (existing) {\n existing.update(value)\n nextRows.push(existing.value)\n } else {\n const [row, setRow] = createStore(value)\n rowsByKey.set(key, {\n value: row,\n update: (nextValue) => setRow(reconcile(nextValue, { key: null })),\n })\n nextRows.push(row)\n }\n }\n\n for (const key of rowsByKey.keys()) {\n if (!retainedKeys.has(key)) rowsByKey.delete(key)\n }\n\n setData((previous) => reconcile(nextRows, { key: null })(previous))\n }\n\n // Generation guard for the resource's async continuations: Solid discards a\n // superseded fetch's *return value*, but the writes below are side effects\n // into hook-scoped state and would still run — resurrecting rows/status from\n // a collection that has already been replaced.\n let resourceGeneration = 0\n let localWaitObserver: LiveQueryObserver<any, any> | undefined\n onCleanup(() => localWaitObserver?.dispose())\n\n const [getDataResource] = createResource(\n () => ({ currentCollection: collection() }),\n async ({ currentCollection }) => {\n const generation = ++resourceGeneration\n localWaitObserver?.dispose()\n localWaitObserver = undefined\n if (!currentCollection) {\n return []\n }\n setStatus(currentCollection.status)\n try {\n const observer = createLiveQueryObserver(currentCollection)\n localWaitObserver = observer\n try {\n await observer.preloadForInitialRender()\n } finally {\n observer.dispose()\n if (localWaitObserver === observer) localWaitObserver = undefined\n }\n } catch (error) {\n if (generation !== resourceGeneration) return data\n setStatus(`error`)\n throw error\n }\n if (generation !== resourceGeneration) {\n return data\n }\n // Initialize state with current collection data\n batch(() => {\n state.clear()\n for (const [key, value] of currentCollection.entries()) {\n state.set(key, value)\n }\n syncDataFromCollection(currentCollection)\n setStatus(currentCollection.status)\n })\n return data\n },\n {\n name: `TanstackDBData`,\n deferStream: false,\n initialValue: data,\n },\n )\n\n createEffect(() => {\n const currentCollection = collection()\n if (!currentCollection) {\n setStatus(`disabled` as const)\n setLocalStatus(`unavailable`)\n setLocalError(undefined)\n state.clear()\n rowsByKey.clear()\n rowsCollection = undefined\n setData([])\n return\n }\n\n if (rowsCollection !== currentCollection) {\n rowsByKey.clear()\n rowsCollection = currentCollection\n }\n\n // The shared observer owns subscription, the ready-race, and status; Solid\n // materializes into its keyed ReactiveMap (granular) + reconciled store.\n const observer = createLiveQueryObserver(currentCollection)\n // Clear any keys carried over from a previous collection before the new\n // observer re-seeds via `includeInitialState` (which only inserts current\n // rows, never deletes stale ones). Without this, switching collections\n // leaves the dropped keys in `state` until the async resource reconciles.\n state.clear()\n const unsubscribe = observer.subscribe(\n (changes: Array<ChangeMessage<any>> | undefined) => {\n batch(() => {\n if (changes) {\n for (const change of changes) {\n switch (change.type) {\n case `insert`:\n case `update`:\n state.set(change.key, change.value)\n break\n case `delete`:\n state.delete(change.key)\n break\n }\n }\n } else {\n // Cleanup and other status-only publications carry no row deltas.\n // Rebuild the keyed view so it cannot diverge from ordered data.\n state.clear()\n for (const [key, value] of observer.getSnapshot().state ?? []) {\n state.set(key, value)\n }\n }\n syncDataFromCollection(currentCollection)\n const snapshot = observer.getSnapshot()\n setStatus(snapshot.status)\n setLocalStatus(snapshot.persistedStatus)\n setLocalError(snapshot.persistedError)\n })\n },\n )\n // An already-ready empty collection produces no initial row batch. Bring\n // ordered data and status in line synchronously instead of waiting for the\n // resource continuation to correct the previous collection's rows.\n batch(() => {\n syncDataFromCollection(currentCollection)\n const snapshot = observer.getSnapshot()\n setStatus(snapshot.status)\n setLocalStatus(snapshot.persistedStatus)\n setLocalError(snapshot.persistedError)\n })\n\n onCleanup(() => {\n unsubscribe()\n observer.dispose()\n })\n })\n\n // We have to remove getters from the resource function so we wrap it\n function getData() {\n const currentCollection = collection()\n if (currentCollection) {\n if (isSingleResultCollection(currentCollection)) {\n // Force resource tracking so Suspense works\n getDataResource()\n return data[0]\n }\n }\n return getDataResource()\n }\n\n Object.defineProperties(getData, {\n data: {\n get() {\n return getData()\n },\n },\n status: {\n get() {\n return status()\n },\n },\n collection: {\n get() {\n return collection()\n },\n },\n state: {\n get() {\n return state\n },\n },\n isLoading: {\n get() {\n return status() === `loading`\n },\n },\n isReady: {\n get() {\n return status() === `ready` || status() === `disabled`\n },\n },\n persistedStatus: {\n get() {\n return persistedStatus()\n },\n },\n isPersistedReady: {\n get() {\n return persistedStatus() === `ready`\n },\n },\n persistedError: {\n get() {\n return persistedError()\n },\n },\n isIdle: {\n get() {\n return status() === `idle`\n },\n },\n isError: {\n get() {\n return status() === `error`\n },\n },\n isCleanedUp: {\n get() {\n return status() === `cleaned-up`\n },\n },\n })\n return getData\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/solid-db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Solid integration for @tanstack/db",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@solid-primitives/map": "^0.7.2",
|
|
40
|
-
"@tanstack/db": "0.
|
|
40
|
+
"@tanstack/db": "0.11.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"solid-js": ">=1.9.0"
|
package/src/useLiveQuery.ts
CHANGED
|
@@ -25,11 +25,18 @@ import type {
|
|
|
25
25
|
InferResultType,
|
|
26
26
|
InitialQueryBuilder,
|
|
27
27
|
LiveQueryCollectionConfig,
|
|
28
|
+
LiveQueryObserver,
|
|
29
|
+
LiveQueryPersistedStatus,
|
|
28
30
|
NonSingleResult,
|
|
29
31
|
QueryBuilder,
|
|
30
32
|
SingleResult,
|
|
31
33
|
} from '@tanstack/db'
|
|
32
34
|
|
|
35
|
+
type InferConditionalResultType<TContext extends Context> =
|
|
36
|
+
TContext extends SingleResult
|
|
37
|
+
? InferResultType<TContext> | []
|
|
38
|
+
: InferResultType<TContext>
|
|
39
|
+
|
|
33
40
|
/**
|
|
34
41
|
* Create a live query using a query function
|
|
35
42
|
* @param queryFn - Query function that defines what data to fetch
|
|
@@ -113,6 +120,9 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
113
120
|
status: CollectionStatus
|
|
114
121
|
isLoading: boolean
|
|
115
122
|
isReady: boolean
|
|
123
|
+
persistedStatus: LiveQueryPersistedStatus
|
|
124
|
+
isPersistedReady: boolean
|
|
125
|
+
persistedError: unknown | undefined
|
|
116
126
|
isIdle: boolean
|
|
117
127
|
isError: boolean
|
|
118
128
|
isCleanedUp: boolean
|
|
@@ -123,17 +133,20 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
123
133
|
queryFn: (
|
|
124
134
|
q: InitialQueryBuilder,
|
|
125
135
|
) => QueryBuilder<TContext> | undefined | null,
|
|
126
|
-
): Accessor<
|
|
136
|
+
): Accessor<InferConditionalResultType<TContext>> & {
|
|
127
137
|
/**
|
|
128
138
|
* @deprecated use function result instead
|
|
129
139
|
* query.data -> query()
|
|
130
140
|
*/
|
|
131
|
-
data:
|
|
141
|
+
data: InferConditionalResultType<TContext>
|
|
132
142
|
state: ReactiveMap<string | number, GetResult<TContext>>
|
|
133
143
|
collection: Collection<GetResult<TContext>, string | number, {}> | null
|
|
134
144
|
status: CollectionStatus | `disabled`
|
|
135
145
|
isLoading: boolean
|
|
136
146
|
isReady: boolean
|
|
147
|
+
persistedStatus: LiveQueryPersistedStatus
|
|
148
|
+
isPersistedReady: boolean
|
|
149
|
+
persistedError: unknown | undefined
|
|
137
150
|
isIdle: boolean
|
|
138
151
|
isError: boolean
|
|
139
152
|
isCleanedUp: boolean
|
|
@@ -193,6 +206,9 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
193
206
|
status: CollectionStatus
|
|
194
207
|
isLoading: boolean
|
|
195
208
|
isReady: boolean
|
|
209
|
+
persistedStatus: LiveQueryPersistedStatus
|
|
210
|
+
isPersistedReady: boolean
|
|
211
|
+
persistedError: unknown | undefined
|
|
196
212
|
isIdle: boolean
|
|
197
213
|
isError: boolean
|
|
198
214
|
isCleanedUp: boolean
|
|
@@ -253,6 +269,9 @@ export function useLiveQuery<
|
|
|
253
269
|
status: CollectionStatus
|
|
254
270
|
isLoading: boolean
|
|
255
271
|
isReady: boolean
|
|
272
|
+
persistedStatus: LiveQueryPersistedStatus
|
|
273
|
+
isPersistedReady: boolean
|
|
274
|
+
persistedError: unknown | undefined
|
|
256
275
|
isIdle: boolean
|
|
257
276
|
isError: boolean
|
|
258
277
|
isCleanedUp: boolean
|
|
@@ -278,6 +297,9 @@ export function useLiveQuery<
|
|
|
278
297
|
status: CollectionStatus
|
|
279
298
|
isLoading: boolean
|
|
280
299
|
isReady: boolean
|
|
300
|
+
persistedStatus: LiveQueryPersistedStatus
|
|
301
|
+
isPersistedReady: boolean
|
|
302
|
+
persistedError: unknown | undefined
|
|
281
303
|
isIdle: boolean
|
|
282
304
|
isError: boolean
|
|
283
305
|
isCleanedUp: boolean
|
|
@@ -329,7 +351,14 @@ export function useLiveQuery(
|
|
|
329
351
|
// Reactive state that gets updated granularly through change events
|
|
330
352
|
const state = new ReactiveMap<string | number, any>()
|
|
331
353
|
|
|
332
|
-
//
|
|
354
|
+
// Keep the live Collection's result keys private while preserving one stable
|
|
355
|
+
// Solid store per logical row. A row's public $key can belong to an upstream
|
|
356
|
+
// Collection and is therefore not necessarily unique in this result.
|
|
357
|
+
const rowsByKey = new Map<
|
|
358
|
+
string | number,
|
|
359
|
+
{ value: any; update: (value: any) => void }
|
|
360
|
+
>()
|
|
361
|
+
let rowsCollection: Collection<any, any, any> | undefined
|
|
333
362
|
const [data, setData] = createStore<Array<any>>([], {
|
|
334
363
|
name: `TanstackDBData`,
|
|
335
364
|
})
|
|
@@ -341,14 +370,39 @@ export function useLiveQuery(
|
|
|
341
370
|
name: `TanstackDBStatus`,
|
|
342
371
|
},
|
|
343
372
|
)
|
|
373
|
+
const [persistedStatus, setLocalStatus] =
|
|
374
|
+
createSignal<LiveQueryPersistedStatus>(`unavailable`)
|
|
375
|
+
const [persistedError, setLocalError] = createSignal<unknown>(undefined)
|
|
344
376
|
|
|
345
377
|
// Helper to sync data array from collection in correct order
|
|
346
378
|
const syncDataFromCollection = (
|
|
347
379
|
currentCollection: Collection<any, any, any>,
|
|
348
380
|
) => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
381
|
+
const nextRows: Array<any> = []
|
|
382
|
+
const retainedKeys = new Set<string | number>()
|
|
383
|
+
|
|
384
|
+
for (const [key, value] of currentCollection.entries()) {
|
|
385
|
+
retainedKeys.add(key)
|
|
386
|
+
|
|
387
|
+
const existing = rowsByKey.get(key)
|
|
388
|
+
if (existing) {
|
|
389
|
+
existing.update(value)
|
|
390
|
+
nextRows.push(existing.value)
|
|
391
|
+
} else {
|
|
392
|
+
const [row, setRow] = createStore(value)
|
|
393
|
+
rowsByKey.set(key, {
|
|
394
|
+
value: row,
|
|
395
|
+
update: (nextValue) => setRow(reconcile(nextValue, { key: null })),
|
|
396
|
+
})
|
|
397
|
+
nextRows.push(row)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
for (const key of rowsByKey.keys()) {
|
|
402
|
+
if (!retainedKeys.has(key)) rowsByKey.delete(key)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
setData((previous) => reconcile(nextRows, { key: null })(previous))
|
|
352
406
|
}
|
|
353
407
|
|
|
354
408
|
// Generation guard for the resource's async continuations: Solid discards a
|
|
@@ -356,19 +410,31 @@ export function useLiveQuery(
|
|
|
356
410
|
// into hook-scoped state and would still run — resurrecting rows/status from
|
|
357
411
|
// a collection that has already been replaced.
|
|
358
412
|
let resourceGeneration = 0
|
|
413
|
+
let localWaitObserver: LiveQueryObserver<any, any> | undefined
|
|
414
|
+
onCleanup(() => localWaitObserver?.dispose())
|
|
359
415
|
|
|
360
416
|
const [getDataResource] = createResource(
|
|
361
417
|
() => ({ currentCollection: collection() }),
|
|
362
418
|
async ({ currentCollection }) => {
|
|
363
419
|
const generation = ++resourceGeneration
|
|
420
|
+
localWaitObserver?.dispose()
|
|
421
|
+
localWaitObserver = undefined
|
|
364
422
|
if (!currentCollection) {
|
|
365
423
|
return []
|
|
366
424
|
}
|
|
367
425
|
setStatus(currentCollection.status)
|
|
368
426
|
try {
|
|
369
|
-
|
|
427
|
+
const observer = createLiveQueryObserver(currentCollection)
|
|
428
|
+
localWaitObserver = observer
|
|
429
|
+
try {
|
|
430
|
+
await observer.preloadForInitialRender()
|
|
431
|
+
} finally {
|
|
432
|
+
observer.dispose()
|
|
433
|
+
if (localWaitObserver === observer) localWaitObserver = undefined
|
|
434
|
+
}
|
|
370
435
|
} catch (error) {
|
|
371
|
-
if (generation
|
|
436
|
+
if (generation !== resourceGeneration) return data
|
|
437
|
+
setStatus(`error`)
|
|
372
438
|
throw error
|
|
373
439
|
}
|
|
374
440
|
if (generation !== resourceGeneration) {
|
|
@@ -396,11 +462,20 @@ export function useLiveQuery(
|
|
|
396
462
|
const currentCollection = collection()
|
|
397
463
|
if (!currentCollection) {
|
|
398
464
|
setStatus(`disabled` as const)
|
|
465
|
+
setLocalStatus(`unavailable`)
|
|
466
|
+
setLocalError(undefined)
|
|
399
467
|
state.clear()
|
|
468
|
+
rowsByKey.clear()
|
|
469
|
+
rowsCollection = undefined
|
|
400
470
|
setData([])
|
|
401
471
|
return
|
|
402
472
|
}
|
|
403
473
|
|
|
474
|
+
if (rowsCollection !== currentCollection) {
|
|
475
|
+
rowsByKey.clear()
|
|
476
|
+
rowsCollection = currentCollection
|
|
477
|
+
}
|
|
478
|
+
|
|
404
479
|
// The shared observer owns subscription, the ready-race, and status; Solid
|
|
405
480
|
// materializes into its keyed ReactiveMap (granular) + reconciled store.
|
|
406
481
|
const observer = createLiveQueryObserver(currentCollection)
|
|
@@ -433,7 +508,10 @@ export function useLiveQuery(
|
|
|
433
508
|
}
|
|
434
509
|
}
|
|
435
510
|
syncDataFromCollection(currentCollection)
|
|
436
|
-
|
|
511
|
+
const snapshot = observer.getSnapshot()
|
|
512
|
+
setStatus(snapshot.status)
|
|
513
|
+
setLocalStatus(snapshot.persistedStatus)
|
|
514
|
+
setLocalError(snapshot.persistedError)
|
|
437
515
|
})
|
|
438
516
|
},
|
|
439
517
|
)
|
|
@@ -442,7 +520,10 @@ export function useLiveQuery(
|
|
|
442
520
|
// resource continuation to correct the previous collection's rows.
|
|
443
521
|
batch(() => {
|
|
444
522
|
syncDataFromCollection(currentCollection)
|
|
445
|
-
|
|
523
|
+
const snapshot = observer.getSnapshot()
|
|
524
|
+
setStatus(snapshot.status)
|
|
525
|
+
setLocalStatus(snapshot.persistedStatus)
|
|
526
|
+
setLocalError(snapshot.persistedError)
|
|
446
527
|
})
|
|
447
528
|
|
|
448
529
|
onCleanup(() => {
|
|
@@ -495,6 +576,21 @@ export function useLiveQuery(
|
|
|
495
576
|
return status() === `ready` || status() === `disabled`
|
|
496
577
|
},
|
|
497
578
|
},
|
|
579
|
+
persistedStatus: {
|
|
580
|
+
get() {
|
|
581
|
+
return persistedStatus()
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
isPersistedReady: {
|
|
585
|
+
get() {
|
|
586
|
+
return persistedStatus() === `ready`
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
persistedError: {
|
|
590
|
+
get() {
|
|
591
|
+
return persistedError()
|
|
592
|
+
},
|
|
593
|
+
},
|
|
498
594
|
isIdle: {
|
|
499
595
|
get() {
|
|
500
596
|
return status() === `idle`
|