@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.
@@ -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<InferResultType<TContext>> & {
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: InferResultType<TContext>;
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 {};
@@ -1,4 +1,4 @@
1
- import { createMemo, createSignal, createResource, batch, createEffect, onCleanup } from "solid-js";
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
- setData(
47
- (prev) => reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean)
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
- await currentCollection.toArrayWhenReady();
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 === resourceGeneration) setStatus(`error`);
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
- setStatus(observer.getSnapshot().status);
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
- setStatus(observer.getSnapshot().status);
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.2.45",
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.9.2"
40
+ "@tanstack/db": "0.11.1"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "solid-js": ">=1.9.0"
@@ -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<InferResultType<TContext>> & {
136
+ ): Accessor<InferConditionalResultType<TContext>> & {
127
137
  /**
128
138
  * @deprecated use function result instead
129
139
  * query.data -> query()
130
140
  */
131
- data: InferResultType<TContext>
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
- // Reactive data array that maintains sorted order
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
- setData((prev) =>
350
- reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean),
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
- await currentCollection.toArrayWhenReady()
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 === resourceGeneration) setStatus(`error`)
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
- setStatus(observer.getSnapshot().status)
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
- setStatus(observer.getSnapshot().status)
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`