@tanstack/solid-db 0.1.61 → 0.2.0

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.
@@ -4,7 +4,7 @@ import { Collection, CollectionStatus, Context, GetResult, InitialQueryBuilder,
4
4
  /**
5
5
  * Create a live query using a query function
6
6
  * @param queryFn - Query function that defines what data to fetch
7
- * @returns Object with reactive data, state, and status information
7
+ * @returns Accessor that returns data with Suspense support, with state and status information as properties
8
8
  * @example
9
9
  * // Basic query with object syntax
10
10
  * const todosQuery = useLiveQuery((q) =>
@@ -42,46 +42,68 @@ import { Collection, CollectionStatus, Context, GetResult, InitialQueryBuilder,
42
42
  *
43
43
  * return (
44
44
  * <Switch>
45
- * <Match when={todosQuery.isLoading()}>
45
+ * <Match when={todosQuery.isLoading}>
46
46
  * <div>Loading...</div>
47
47
  * </Match>
48
- * <Match when={todosQuery.isError()}>
49
- * <div>Error: {todosQuery.status()}</div>
48
+ * <Match when={todosQuery.isError}>
49
+ * <div>Error: {todosQuery.status}</div>
50
50
  * </Match>
51
- * <Match when={todosQuery.isReady()}>
52
- * <For each={todosQuery.data()}>
51
+ * <Match when={todosQuery.isReady}>
52
+ * <For each={todosQuery()}>
53
53
  * {(todo) => <li key={todo.id}>{todo.text}</li>}
54
54
  * </For>
55
55
  * </Match>
56
56
  * </Switch>
57
57
  * )
58
+ *
59
+ * @example
60
+ * // Use Suspense boundaries
61
+ * const todosQuery = useLiveQuery((q) =>
62
+ * q.from({ todos: todoCollection })
63
+ * )
64
+ *
65
+ * return (
66
+ * <Suspense fallback={<div>Loading...</div>}>
67
+ * <For each={todosQuery()}>
68
+ * {(todo) => <li key={todo.id}>{todo.text}</li>}
69
+ * </For>
70
+ * </Suspense>
71
+ * )
58
72
  */
59
- export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>): {
60
- state: ReactiveMap<string | number, GetResult<TContext>>;
73
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>): Accessor<Array<GetResult<TContext>>> & {
74
+ /**
75
+ * @deprecated use function result instead
76
+ * query.data -> query()
77
+ */
61
78
  data: Array<GetResult<TContext>>;
62
- collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>;
63
- status: Accessor<CollectionStatus>;
64
- isLoading: Accessor<boolean>;
65
- isReady: Accessor<boolean>;
66
- isIdle: Accessor<boolean>;
67
- isError: Accessor<boolean>;
68
- isCleanedUp: Accessor<boolean>;
69
- };
70
- export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): {
71
79
  state: ReactiveMap<string | number, GetResult<TContext>>;
80
+ collection: Collection<GetResult<TContext>, string | number, {}>;
81
+ status: CollectionStatus;
82
+ isLoading: boolean;
83
+ isReady: boolean;
84
+ isIdle: boolean;
85
+ isError: boolean;
86
+ isCleanedUp: boolean;
87
+ };
88
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext> | undefined | null): Accessor<Array<GetResult<TContext>>> & {
89
+ /**
90
+ * @deprecated use function result instead
91
+ * query.data -> query()
92
+ */
72
93
  data: Array<GetResult<TContext>>;
73
- collection: Accessor<Collection<GetResult<TContext>, string | number, {}> | null>;
74
- status: Accessor<CollectionStatus | `disabled`>;
75
- isLoading: Accessor<boolean>;
76
- isReady: Accessor<boolean>;
77
- isIdle: Accessor<boolean>;
78
- isError: Accessor<boolean>;
79
- isCleanedUp: Accessor<boolean>;
94
+ state: ReactiveMap<string | number, GetResult<TContext>>;
95
+ collection: Collection<GetResult<TContext>, string | number, {}> | null;
96
+ status: CollectionStatus | `disabled`;
97
+ isLoading: boolean;
98
+ isReady: boolean;
99
+ isIdle: boolean;
100
+ isError: boolean;
101
+ isCleanedUp: boolean;
80
102
  };
81
103
  /**
82
104
  * Create a live query using configuration object
83
105
  * @param config - Configuration object with query and options
84
- * @returns Object with reactive data, state, and status information
106
+ * @returns Accessor that returns data with Suspense support, with state and status information as properties
85
107
  * @example
86
108
  * // Basic config object usage
87
109
  * const todosQuery = useLiveQuery(() => ({
@@ -105,34 +127,38 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
105
127
  * }))
106
128
  *
107
129
  * return (
108
- * <Switch fallback={<div>{itemsQuery.data.length} items loaded</div>}>
109
- * <Match when={itemsQuery.isLoading()}>
130
+ * <Switch fallback={<div>{itemsQuery().length} items loaded</div>}>
131
+ * <Match when={itemsQuery.isLoading}>
110
132
  * <div>Loading...</div>
111
133
  * </Match>
112
- * <Match when={itemsQuery.isError()}>
134
+ * <Match when={itemsQuery.isError}>
113
135
  * <div>Something went wrong</div>
114
136
  * </Match>
115
- * <Match when={!itemsQuery.isReady()}>
137
+ * <Match when={!itemsQuery.isReady}>
116
138
  * <div>Preparing...</div>
117
139
  * </Match>
118
140
  * </Switch>
119
141
  * )
120
142
  */
121
- export declare function useLiveQuery<TContext extends Context>(config: Accessor<LiveQueryCollectionConfig<TContext>>): {
122
- state: ReactiveMap<string | number, GetResult<TContext>>;
143
+ export declare function useLiveQuery<TContext extends Context>(config: Accessor<LiveQueryCollectionConfig<TContext>>): Accessor<Array<GetResult<TContext>>> & {
144
+ /**
145
+ * @deprecated use function result instead
146
+ * query.data -> query()
147
+ */
123
148
  data: Array<GetResult<TContext>>;
124
- collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>;
125
- status: Accessor<CollectionStatus>;
126
- isLoading: Accessor<boolean>;
127
- isReady: Accessor<boolean>;
128
- isIdle: Accessor<boolean>;
129
- isError: Accessor<boolean>;
130
- isCleanedUp: Accessor<boolean>;
149
+ state: ReactiveMap<string | number, GetResult<TContext>>;
150
+ collection: Collection<GetResult<TContext>, string | number, {}>;
151
+ status: CollectionStatus;
152
+ isLoading: boolean;
153
+ isReady: boolean;
154
+ isIdle: boolean;
155
+ isError: boolean;
156
+ isCleanedUp: boolean;
131
157
  };
132
158
  /**
133
159
  * Subscribe to an existing live query collection
134
160
  * @param liveQueryCollection - Pre-created live query collection to subscribe to
135
- * @returns Object with reactive data, state, and status information
161
+ * @returns Accessor that returns data with Suspense support, with state and status information as properties
136
162
  * @example
137
163
  * // Using pre-created live query collection
138
164
  * const myLiveQuery = createLiveQueryCollection((q) =>
@@ -146,7 +172,7 @@ export declare function useLiveQuery<TContext extends Context>(config: Accessor<
146
172
  *
147
173
  * // Use collection for mutations
148
174
  * const handleToggle = (id) => {
149
- * existingQuery.collection().update(id, draft => { draft.completed = !draft.completed })
175
+ * existingQuery.collection.update(id, draft => { draft.completed = !draft.completed })
150
176
  * }
151
177
  *
152
178
  * @example
@@ -154,24 +180,28 @@ export declare function useLiveQuery<TContext extends Context>(config: Accessor<
154
180
  * const sharedQuery = useLiveQuery(() => sharedCollection)
155
181
  *
156
182
  * return (
157
- * <Switch fallback={<div><For each={sharedQuery.data()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>
158
- * <Match when={sharedQuery.isLoading()}>
183
+ * <Switch fallback={<div><For each={sharedQuery()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>
184
+ * <Match when={sharedQuery.isLoading}>
159
185
  * <div>Loading...</div>
160
186
  * </Match>
161
- * <Match when={sharedQuery.isError()}>
187
+ * <Match when={sharedQuery.isError}>
162
188
  * <div>Error loading data</div>
163
189
  * </Match>
164
190
  * </Switch>
165
191
  * )
166
192
  */
167
- export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>): {
168
- state: ReactiveMap<TKey, TResult>;
193
+ export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>): Accessor<Array<TResult>> & {
194
+ /**
195
+ * @deprecated use function result instead
196
+ * query.data -> query()
197
+ */
169
198
  data: Array<TResult>;
170
- collection: Accessor<Collection<TResult, TKey, TUtils>>;
171
- status: Accessor<CollectionStatus>;
172
- isLoading: Accessor<boolean>;
173
- isReady: Accessor<boolean>;
174
- isIdle: Accessor<boolean>;
175
- isError: Accessor<boolean>;
176
- isCleanedUp: Accessor<boolean>;
199
+ state: ReactiveMap<TKey, TResult>;
200
+ collection: Collection<TResult, TKey, TUtils>;
201
+ status: CollectionStatus;
202
+ isLoading: boolean;
203
+ isReady: boolean;
204
+ isIdle: boolean;
205
+ isError: boolean;
206
+ isCleanedUp: boolean;
177
207
  };
@@ -1,4 +1,4 @@
1
- import { createMemo, createSignal, createComputed, batch, createResource, onCleanup } from "solid-js";
1
+ import { createMemo, createSignal, createResource, batch, createEffect, onCleanup } from "solid-js";
2
2
  import { ReactiveMap } from "@solid-primitives/map";
3
3
  import { BaseQueryBuilder, createLiveQueryCollection, CollectionImpl } from "@tanstack/db";
4
4
  import { createStore, reconcile } from "solid-js/store";
@@ -47,72 +47,121 @@ function useLiveQuery(configOrQueryOrCollection) {
47
47
  (prev) => reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean)
48
48
  );
49
49
  };
50
- let currentUnsubscribe = null;
51
- createComputed(
52
- () => {
53
- const currentCollection = collection();
50
+ const [getDataResource] = createResource(
51
+ () => ({ currentCollection: collection() }),
52
+ async ({ currentCollection }) => {
54
53
  if (!currentCollection) {
55
- setStatus(`disabled`);
56
- state.clear();
57
- setData([]);
58
- if (currentUnsubscribe) {
59
- currentUnsubscribe();
60
- currentUnsubscribe = null;
61
- }
62
- return;
54
+ return [];
63
55
  }
64
56
  setStatus(currentCollection.status);
65
- state.clear();
66
- for (const [key, value] of currentCollection.entries()) {
67
- state.set(key, value);
57
+ try {
58
+ await currentCollection.toArrayWhenReady();
59
+ } catch (error) {
60
+ setStatus(`error`);
61
+ throw error;
68
62
  }
69
- const subscription = currentCollection.subscribeChanges(
70
- (changes) => {
71
- batch(() => {
72
- for (const change of changes) {
73
- switch (change.type) {
74
- case `insert`:
75
- case `update`:
76
- state.set(change.key, change.value);
77
- break;
78
- case `delete`:
79
- state.delete(change.key);
80
- break;
81
- }
63
+ batch(() => {
64
+ state.clear();
65
+ for (const [key, value] of currentCollection.entries()) {
66
+ state.set(key, value);
67
+ }
68
+ syncDataFromCollection(currentCollection);
69
+ setStatus(currentCollection.status);
70
+ });
71
+ return data;
72
+ },
73
+ {
74
+ name: `TanstackDBData`,
75
+ deferStream: false,
76
+ initialValue: data
77
+ }
78
+ );
79
+ createEffect(() => {
80
+ const currentCollection = collection();
81
+ if (!currentCollection) {
82
+ setStatus(`disabled`);
83
+ state.clear();
84
+ setData([]);
85
+ return;
86
+ }
87
+ const subscription = currentCollection.subscribeChanges(
88
+ (changes) => {
89
+ batch(() => {
90
+ for (const change of changes) {
91
+ switch (change.type) {
92
+ case `insert`:
93
+ case `update`:
94
+ state.set(change.key, change.value);
95
+ break;
96
+ case `delete`:
97
+ state.delete(change.key);
98
+ break;
82
99
  }
83
- });
100
+ }
84
101
  syncDataFromCollection(currentCollection);
85
102
  setStatus(currentCollection.status);
86
- },
87
- {
88
- includeInitialState: true
89
- }
90
- );
91
- currentUnsubscribe = subscription.unsubscribe.bind(subscription);
92
- if (currentCollection.status === `idle`) {
93
- createResource(() => currentCollection.preload());
103
+ });
104
+ },
105
+ {
106
+ // Include initial state to ensure immediate population for pre-created collections
107
+ includeInitialState: true
108
+ }
109
+ );
110
+ onCleanup(() => {
111
+ subscription.unsubscribe();
112
+ });
113
+ });
114
+ function getData() {
115
+ return getDataResource();
116
+ }
117
+ Object.defineProperties(getData, {
118
+ data: {
119
+ get() {
120
+ return getData();
94
121
  }
95
- onCleanup(() => {
96
- if (currentUnsubscribe) {
97
- currentUnsubscribe();
98
- currentUnsubscribe = null;
99
- }
100
- });
101
122
  },
102
- void 0,
103
- { name: `TanstackDBSyncComputed` }
104
- );
105
- return {
106
- state,
107
- data,
108
- collection,
109
- status,
110
- isLoading: () => status() === `loading`,
111
- isReady: () => status() === `ready` || status() === `disabled`,
112
- isIdle: () => status() === `idle`,
113
- isError: () => status() === `error`,
114
- isCleanedUp: () => status() === `cleaned-up`
115
- };
123
+ status: {
124
+ get() {
125
+ return status();
126
+ }
127
+ },
128
+ collection: {
129
+ get() {
130
+ return collection();
131
+ }
132
+ },
133
+ state: {
134
+ get() {
135
+ return state;
136
+ }
137
+ },
138
+ isLoading: {
139
+ get() {
140
+ return status() === `loading`;
141
+ }
142
+ },
143
+ isReady: {
144
+ get() {
145
+ return status() === `ready` || status() === `disabled`;
146
+ }
147
+ },
148
+ isIdle: {
149
+ get() {
150
+ return status() === `idle`;
151
+ }
152
+ },
153
+ isError: {
154
+ get() {
155
+ return status() === `error`;
156
+ }
157
+ },
158
+ isCleanedUp: {
159
+ get() {
160
+ return status() === `cleaned-up`;
161
+ }
162
+ }
163
+ });
164
+ return getData;
116
165
  }
117
166
  export {
118
167
  useLiveQuery
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import {\n batch,\n createComputed,\n createMemo,\n createResource,\n createSignal,\n onCleanup,\n} from 'solid-js'\nimport { ReactiveMap } from '@solid-primitives/map'\nimport {\n BaseQueryBuilder,\n CollectionImpl,\n createLiveQueryCollection,\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 InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @returns Object with reactive data, state, and status information\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.data()}>\n * {(todo) => <li key={todo.id}>{todo.text}</li>}\n * </For>\n * </Match>\n * </Switch>\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): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<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): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<\n GetResult<TContext>,\n string | number,\n {}\n > | null>\n status: Accessor<CollectionStatus | `disabled`>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @returns Object with reactive data, state, and status information\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.data.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): {\n state: ReactiveMap<string | number, GetResult<TContext>>\n data: Array<GetResult<TContext>>\n collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<boolean>\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const 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.data()}>{(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\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>,\n): {\n state: ReactiveMap<TKey, TResult>\n data: Array<TResult>\n collection: Accessor<Collection<TResult, TKey, TUtils>>\n status: Accessor<CollectionStatus>\n isLoading: Accessor<boolean>\n isReady: Accessor<boolean>\n isIdle: Accessor<boolean>\n isError: Accessor<boolean>\n isCleanedUp: Accessor<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 (innerCollection instanceof CollectionImpl) {\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 // Track current unsubscribe function\n let currentUnsubscribe: (() => void) | null = null\n\n createComputed(\n () => {\n const currentCollection = collection()\n\n // Handle null collection (disabled query)\n if (!currentCollection) {\n setStatus(`disabled` as const)\n state.clear()\n setData([])\n if (currentUnsubscribe) {\n currentUnsubscribe()\n currentUnsubscribe = null\n }\n return\n }\n\n // Update status ref whenever the effect runs\n setStatus(currentCollection.status)\n\n // Initialize state with current collection data\n state.clear()\n for (const [key, value] of currentCollection.entries()) {\n state.set(key, value)\n }\n\n // Subscribe to collection changes with granular updates\n const subscription = currentCollection.subscribeChanges(\n (changes: Array<ChangeMessage<any>>) => {\n // Apply each change individually to the reactive state\n batch(() => {\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 })\n\n // Update the data array to maintain sorted order\n syncDataFromCollection(currentCollection)\n\n // Update status ref on every change\n setStatus(currentCollection.status)\n },\n {\n includeInitialState: true,\n },\n )\n\n currentUnsubscribe = subscription.unsubscribe.bind(subscription)\n\n // Preload collection data if not already started\n if (currentCollection.status === `idle`) {\n createResource(() => currentCollection.preload())\n }\n\n // Cleanup when computed is invalidated\n onCleanup(() => {\n if (currentUnsubscribe) {\n currentUnsubscribe()\n currentUnsubscribe = null\n }\n })\n },\n undefined,\n { name: `TanstackDBSyncComputed` },\n )\n\n return {\n state,\n data,\n collection,\n status,\n isLoading: () => status() === `loading`,\n isReady: () => status() === `ready` || status() === `disabled`,\n isIdle: () => status() === `idle`,\n isError: () => status() === `error`,\n isCleanedUp: () => status() === `cleaned-up`,\n }\n}\n"],"names":[],"mappings":";;;;AAoOO,SAAS,aACd,2BACA;AACA,QAAM,aAAa;AAAA,IACjB,MAAM;AACJ,UAAI,0BAA0B,WAAW,GAAG;AAE1C,cAAM,eAAe,IAAI,iBAAA;AACzB,cAAM,SAAS,0BAA0B,YAAY;AAErD,YAAI,WAAW,UAAa,WAAW,MAAM;AAE3C,iBAAO;AAAA,QACT;AAEA,eAAO,0BAA0B;AAAA,UAC/B,OAAO;AAAA,UACP,WAAW;AAAA,QAAA,CACZ;AAAA,MACH;AAEA,YAAM,kBAAkB,0BAAA;AAExB,UAAI,oBAAoB,UAAa,oBAAoB,MAAM;AAE7D,eAAO;AAAA,MACT;AAEA,UAAI,2BAA2B,gBAAgB;AAC7C,wBAAgB,mBAAA;AAChB,eAAO;AAAA,MACT;AAEA,aAAO,0BAA0B;AAAA,QAC/B,GAAG;AAAA,QACH,WAAW;AAAA,MAAA,CACZ;AAAA,IACH;AAAA,IACA;AAAA,IACA,EAAE,MAAM,2BAAA;AAAA,EAA2B;AAIrC,QAAM,QAAQ,IAAI,YAAA;AAGlB,QAAM,CAAC,MAAM,OAAO,IAAI,YAAwB,CAAA,GAAI;AAAA,IAClD,MAAM;AAAA,EAAA,CACP;AAGD,QAAM,CAAC,QAAQ,SAAS,IAAI;AAAA,IAC1B,WAAA,IAAe,WAAA,EAAc,SAAU;AAAA,IACvC;AAAA,MACE,MAAM;AAAA,IAAA;AAAA,EACR;AAIF,QAAM,yBAAyB,CAC7B,sBACG;AACH;AAAA,MAAQ,CAAC,SACP,UAAU,MAAM,KAAK,kBAAkB,OAAA,CAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,OAAO;AAAA,IAAA;AAAA,EAE1E;AAGA,MAAI,qBAA0C;AAE9C;AAAA,IACE,MAAM;AACJ,YAAM,oBAAoB,WAAA;AAG1B,UAAI,CAAC,mBAAmB;AACtB,kBAAU,UAAmB;AAC7B,cAAM,MAAA;AACN,gBAAQ,CAAA,CAAE;AACV,YAAI,oBAAoB;AACtB,6BAAA;AACA,+BAAqB;AAAA,QACvB;AACA;AAAA,MACF;AAGA,gBAAU,kBAAkB,MAAM;AAGlC,YAAM,MAAA;AACN,iBAAW,CAAC,KAAK,KAAK,KAAK,kBAAkB,WAAW;AACtD,cAAM,IAAI,KAAK,KAAK;AAAA,MACtB;AAGA,YAAM,eAAe,kBAAkB;AAAA,QACrC,CAAC,YAAuC;AAEtC,gBAAM,MAAM;AACV,uBAAW,UAAU,SAAS;AAC5B,sBAAQ,OAAO,MAAA;AAAA,gBACb,KAAK;AAAA,gBACL,KAAK;AACH,wBAAM,IAAI,OAAO,KAAK,OAAO,KAAK;AAClC;AAAA,gBACF,KAAK;AACH,wBAAM,OAAO,OAAO,GAAG;AACvB;AAAA,cAAA;AAAA,YAEN;AAAA,UACF,CAAC;AAGD,iCAAuB,iBAAiB;AAGxC,oBAAU,kBAAkB,MAAM;AAAA,QACpC;AAAA,QACA;AAAA,UACE,qBAAqB;AAAA,QAAA;AAAA,MACvB;AAGF,2BAAqB,aAAa,YAAY,KAAK,YAAY;AAG/D,UAAI,kBAAkB,WAAW,QAAQ;AACvC,uBAAe,MAAM,kBAAkB,SAAS;AAAA,MAClD;AAGA,gBAAU,MAAM;AACd,YAAI,oBAAoB;AACtB,6BAAA;AACA,+BAAqB;AAAA,QACvB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA;AAAA,IACA,EAAE,MAAM,yBAAA;AAAA,EAAyB;AAGnC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,MAAM,aAAa;AAAA,IAC9B,SAAS,MAAM,aAAa,WAAW,aAAa;AAAA,IACpD,QAAQ,MAAM,aAAa;AAAA,IAC3B,SAAS,MAAM,aAAa;AAAA,IAC5B,aAAa,MAAM,OAAA,MAAa;AAAA,EAAA;AAEpC;"}
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 CollectionImpl,\n createLiveQueryCollection,\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 InitialQueryBuilder,\n LiveQueryCollectionConfig,\n QueryBuilder,\n} from '@tanstack/db'\n\n/**\n * Create a live query using a query function\n * @param queryFn - Query function that defines what data to fetch\n * @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<Array<GetResult<TContext>>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: Array<GetResult<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<Array<GetResult<TContext>>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: Array<GetResult<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<Array<GetResult<TContext>>> & {\n /**\n * @deprecated use function result instead\n * query.data -> query()\n */\n data: Array<GetResult<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\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>,\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// 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 (innerCollection instanceof CollectionImpl) {\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 const [getDataResource] = createResource(\n () => ({ currentCollection: collection() }),\n async ({ currentCollection }) => {\n if (!currentCollection) {\n return []\n }\n setStatus(currentCollection.status)\n try {\n await currentCollection.toArrayWhenReady()\n } catch (error) {\n setStatus(`error`)\n throw error\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 const subscription = currentCollection.subscribeChanges(\n (changes: Array<ChangeMessage<any>>) => {\n // Apply each change individually to the reactive state\n batch(() => {\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\n syncDataFromCollection(currentCollection)\n\n // Update status ref on every change\n setStatus(currentCollection.status)\n })\n },\n {\n // Include initial state to ensure immediate population for pre-created collections\n includeInitialState: true,\n },\n )\n\n onCleanup(() => {\n subscription.unsubscribe()\n })\n })\n\n // We have to remove getters from the resource function so we wrap it\n function getData() {\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/solid-db",
3
- "version": "0.1.61",
3
+ "version": "0.2.0",
4
4
  "description": "Solid integration for @tanstack/db",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@solid-primitives/map": "^0.7.2",
38
- "@tanstack/db": "0.5.18"
38
+ "@tanstack/db": "0.5.20"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "solid-js": ">=1.9.0"
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  batch,
3
- createComputed,
3
+ createEffect,
4
4
  createMemo,
5
5
  createResource,
6
6
  createSignal,
@@ -28,7 +28,7 @@ import type {
28
28
  /**
29
29
  * Create a live query using a query function
30
30
  * @param queryFn - Query function that defines what data to fetch
31
- * @returns Object with reactive data, state, and status information
31
+ * @returns Accessor that returns data with Suspense support, with state and status information as properties
32
32
  * @example
33
33
  * // Basic query with object syntax
34
34
  * const todosQuery = useLiveQuery((q) =>
@@ -66,33 +66,51 @@ import type {
66
66
  *
67
67
  * return (
68
68
  * <Switch>
69
- * <Match when={todosQuery.isLoading()}>
69
+ * <Match when={todosQuery.isLoading}>
70
70
  * <div>Loading...</div>
71
71
  * </Match>
72
- * <Match when={todosQuery.isError()}>
73
- * <div>Error: {todosQuery.status()}</div>
72
+ * <Match when={todosQuery.isError}>
73
+ * <div>Error: {todosQuery.status}</div>
74
74
  * </Match>
75
- * <Match when={todosQuery.isReady()}>
76
- * <For each={todosQuery.data()}>
75
+ * <Match when={todosQuery.isReady}>
76
+ * <For each={todosQuery()}>
77
77
  * {(todo) => <li key={todo.id}>{todo.text}</li>}
78
78
  * </For>
79
79
  * </Match>
80
80
  * </Switch>
81
81
  * )
82
+ *
83
+ * @example
84
+ * // Use Suspense boundaries
85
+ * const todosQuery = useLiveQuery((q) =>
86
+ * q.from({ todos: todoCollection })
87
+ * )
88
+ *
89
+ * return (
90
+ * <Suspense fallback={<div>Loading...</div>}>
91
+ * <For each={todosQuery()}>
92
+ * {(todo) => <li key={todo.id}>{todo.text}</li>}
93
+ * </For>
94
+ * </Suspense>
95
+ * )
82
96
  */
83
97
  // Overload 1: Accept query function that always returns QueryBuilder
84
98
  export function useLiveQuery<TContext extends Context>(
85
99
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
86
- ): {
87
- state: ReactiveMap<string | number, GetResult<TContext>>
100
+ ): Accessor<Array<GetResult<TContext>>> & {
101
+ /**
102
+ * @deprecated use function result instead
103
+ * query.data -> query()
104
+ */
88
105
  data: Array<GetResult<TContext>>
89
- collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>
90
- status: Accessor<CollectionStatus>
91
- isLoading: Accessor<boolean>
92
- isReady: Accessor<boolean>
93
- isIdle: Accessor<boolean>
94
- isError: Accessor<boolean>
95
- isCleanedUp: Accessor<boolean>
106
+ state: ReactiveMap<string | number, GetResult<TContext>>
107
+ collection: Collection<GetResult<TContext>, string | number, {}>
108
+ status: CollectionStatus
109
+ isLoading: boolean
110
+ isReady: boolean
111
+ isIdle: boolean
112
+ isError: boolean
113
+ isCleanedUp: boolean
96
114
  }
97
115
 
98
116
  // Overload 1b: Accept query function that can return undefined/null
@@ -100,26 +118,26 @@ export function useLiveQuery<TContext extends Context>(
100
118
  queryFn: (
101
119
  q: InitialQueryBuilder,
102
120
  ) => QueryBuilder<TContext> | undefined | null,
103
- ): {
104
- state: ReactiveMap<string | number, GetResult<TContext>>
121
+ ): Accessor<Array<GetResult<TContext>>> & {
122
+ /**
123
+ * @deprecated use function result instead
124
+ * query.data -> query()
125
+ */
105
126
  data: Array<GetResult<TContext>>
106
- collection: Accessor<Collection<
107
- GetResult<TContext>,
108
- string | number,
109
- {}
110
- > | null>
111
- status: Accessor<CollectionStatus | `disabled`>
112
- isLoading: Accessor<boolean>
113
- isReady: Accessor<boolean>
114
- isIdle: Accessor<boolean>
115
- isError: Accessor<boolean>
116
- isCleanedUp: Accessor<boolean>
127
+ state: ReactiveMap<string | number, GetResult<TContext>>
128
+ collection: Collection<GetResult<TContext>, string | number, {}> | null
129
+ status: CollectionStatus | `disabled`
130
+ isLoading: boolean
131
+ isReady: boolean
132
+ isIdle: boolean
133
+ isError: boolean
134
+ isCleanedUp: boolean
117
135
  }
118
136
 
119
137
  /**
120
138
  * Create a live query using configuration object
121
139
  * @param config - Configuration object with query and options
122
- * @returns Object with reactive data, state, and status information
140
+ * @returns Accessor that returns data with Suspense support, with state and status information as properties
123
141
  * @example
124
142
  * // Basic config object usage
125
143
  * const todosQuery = useLiveQuery(() => ({
@@ -143,14 +161,14 @@ export function useLiveQuery<TContext extends Context>(
143
161
  * }))
144
162
  *
145
163
  * return (
146
- * <Switch fallback={<div>{itemsQuery.data.length} items loaded</div>}>
147
- * <Match when={itemsQuery.isLoading()}>
164
+ * <Switch fallback={<div>{itemsQuery().length} items loaded</div>}>
165
+ * <Match when={itemsQuery.isLoading}>
148
166
  * <div>Loading...</div>
149
167
  * </Match>
150
- * <Match when={itemsQuery.isError()}>
168
+ * <Match when={itemsQuery.isError}>
151
169
  * <div>Something went wrong</div>
152
170
  * </Match>
153
- * <Match when={!itemsQuery.isReady()}>
171
+ * <Match when={!itemsQuery.isReady}>
154
172
  * <div>Preparing...</div>
155
173
  * </Match>
156
174
  * </Switch>
@@ -159,22 +177,26 @@ export function useLiveQuery<TContext extends Context>(
159
177
  // Overload 2: Accept config object
160
178
  export function useLiveQuery<TContext extends Context>(
161
179
  config: Accessor<LiveQueryCollectionConfig<TContext>>,
162
- ): {
163
- state: ReactiveMap<string | number, GetResult<TContext>>
180
+ ): Accessor<Array<GetResult<TContext>>> & {
181
+ /**
182
+ * @deprecated use function result instead
183
+ * query.data -> query()
184
+ */
164
185
  data: Array<GetResult<TContext>>
165
- collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>
166
- status: Accessor<CollectionStatus>
167
- isLoading: Accessor<boolean>
168
- isReady: Accessor<boolean>
169
- isIdle: Accessor<boolean>
170
- isError: Accessor<boolean>
171
- isCleanedUp: Accessor<boolean>
186
+ state: ReactiveMap<string | number, GetResult<TContext>>
187
+ collection: Collection<GetResult<TContext>, string | number, {}>
188
+ status: CollectionStatus
189
+ isLoading: boolean
190
+ isReady: boolean
191
+ isIdle: boolean
192
+ isError: boolean
193
+ isCleanedUp: boolean
172
194
  }
173
195
 
174
196
  /**
175
197
  * Subscribe to an existing live query collection
176
198
  * @param liveQueryCollection - Pre-created live query collection to subscribe to
177
- * @returns Object with reactive data, state, and status information
199
+ * @returns Accessor that returns data with Suspense support, with state and status information as properties
178
200
  * @example
179
201
  * // Using pre-created live query collection
180
202
  * const myLiveQuery = createLiveQueryCollection((q) =>
@@ -188,7 +210,7 @@ export function useLiveQuery<TContext extends Context>(
188
210
  *
189
211
  * // Use collection for mutations
190
212
  * const handleToggle = (id) => {
191
- * existingQuery.collection().update(id, draft => { draft.completed = !draft.completed })
213
+ * existingQuery.collection.update(id, draft => { draft.completed = !draft.completed })
192
214
  * }
193
215
  *
194
216
  * @example
@@ -196,11 +218,11 @@ export function useLiveQuery<TContext extends Context>(
196
218
  * const sharedQuery = useLiveQuery(() => sharedCollection)
197
219
  *
198
220
  * return (
199
- * <Switch fallback={<div><For each={sharedQuery.data()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>
200
- * <Match when={sharedQuery.isLoading()}>
221
+ * <Switch fallback={<div><For each={sharedQuery()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>
222
+ * <Match when={sharedQuery.isLoading}>
201
223
  * <div>Loading...</div>
202
224
  * </Match>
203
- * <Match when={sharedQuery.isError()}>
225
+ * <Match when={sharedQuery.isError}>
204
226
  * <div>Error loading data</div>
205
227
  * </Match>
206
228
  * </Switch>
@@ -213,16 +235,20 @@ export function useLiveQuery<
213
235
  TUtils extends Record<string, any>,
214
236
  >(
215
237
  liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>,
216
- ): {
217
- state: ReactiveMap<TKey, TResult>
238
+ ): Accessor<Array<TResult>> & {
239
+ /**
240
+ * @deprecated use function result instead
241
+ * query.data -> query()
242
+ */
218
243
  data: Array<TResult>
219
- collection: Accessor<Collection<TResult, TKey, TUtils>>
220
- status: Accessor<CollectionStatus>
221
- isLoading: Accessor<boolean>
222
- isReady: Accessor<boolean>
223
- isIdle: Accessor<boolean>
224
- isError: Accessor<boolean>
225
- isCleanedUp: Accessor<boolean>
244
+ state: ReactiveMap<TKey, TResult>
245
+ collection: Collection<TResult, TKey, TUtils>
246
+ status: CollectionStatus
247
+ isLoading: boolean
248
+ isReady: boolean
249
+ isIdle: boolean
250
+ isError: boolean
251
+ isCleanedUp: boolean
226
252
  }
227
253
 
228
254
  // Implementation - use function overloads to infer the actual collection type
@@ -293,91 +319,129 @@ export function useLiveQuery(
293
319
  )
294
320
  }
295
321
 
296
- // Track current unsubscribe function
297
- let currentUnsubscribe: (() => void) | null = null
298
-
299
- createComputed(
300
- () => {
301
- const currentCollection = collection()
302
-
303
- // Handle null collection (disabled query)
322
+ const [getDataResource] = createResource(
323
+ () => ({ currentCollection: collection() }),
324
+ async ({ currentCollection }) => {
304
325
  if (!currentCollection) {
305
- setStatus(`disabled` as const)
306
- state.clear()
307
- setData([])
308
- if (currentUnsubscribe) {
309
- currentUnsubscribe()
310
- currentUnsubscribe = null
311
- }
312
- return
326
+ return []
313
327
  }
314
-
315
- // Update status ref whenever the effect runs
316
328
  setStatus(currentCollection.status)
317
-
318
- // Initialize state with current collection data
319
- state.clear()
320
- for (const [key, value] of currentCollection.entries()) {
321
- state.set(key, value)
329
+ try {
330
+ await currentCollection.toArrayWhenReady()
331
+ } catch (error) {
332
+ setStatus(`error`)
333
+ throw error
322
334
  }
335
+ // Initialize state with current collection data
336
+ batch(() => {
337
+ state.clear()
338
+ for (const [key, value] of currentCollection.entries()) {
339
+ state.set(key, value)
340
+ }
341
+ syncDataFromCollection(currentCollection)
342
+ setStatus(currentCollection.status)
343
+ })
344
+ return data
345
+ },
346
+ {
347
+ name: `TanstackDBData`,
348
+ deferStream: false,
349
+ initialValue: data,
350
+ },
351
+ )
323
352
 
324
- // Subscribe to collection changes with granular updates
325
- const subscription = currentCollection.subscribeChanges(
326
- (changes: Array<ChangeMessage<any>>) => {
327
- // Apply each change individually to the reactive state
328
- batch(() => {
329
- for (const change of changes) {
330
- switch (change.type) {
331
- case `insert`:
332
- case `update`:
333
- state.set(change.key, change.value)
334
- break
335
- case `delete`:
336
- state.delete(change.key)
337
- break
338
- }
353
+ createEffect(() => {
354
+ const currentCollection = collection()
355
+ if (!currentCollection) {
356
+ setStatus(`disabled` as const)
357
+ state.clear()
358
+ setData([])
359
+ return
360
+ }
361
+ const subscription = currentCollection.subscribeChanges(
362
+ (changes: Array<ChangeMessage<any>>) => {
363
+ // Apply each change individually to the reactive state
364
+ batch(() => {
365
+ for (const change of changes) {
366
+ switch (change.type) {
367
+ case `insert`:
368
+ case `update`:
369
+ state.set(change.key, change.value)
370
+ break
371
+ case `delete`:
372
+ state.delete(change.key)
373
+ break
339
374
  }
340
- })
375
+ }
341
376
 
342
- // Update the data array to maintain sorted order
343
377
  syncDataFromCollection(currentCollection)
344
378
 
345
379
  // Update status ref on every change
346
380
  setStatus(currentCollection.status)
347
- },
348
- {
349
- includeInitialState: true,
350
- },
351
- )
381
+ })
382
+ },
383
+ {
384
+ // Include initial state to ensure immediate population for pre-created collections
385
+ includeInitialState: true,
386
+ },
387
+ )
352
388
 
353
- currentUnsubscribe = subscription.unsubscribe.bind(subscription)
389
+ onCleanup(() => {
390
+ subscription.unsubscribe()
391
+ })
392
+ })
354
393
 
355
- // Preload collection data if not already started
356
- if (currentCollection.status === `idle`) {
357
- createResource(() => currentCollection.preload())
358
- }
394
+ // We have to remove getters from the resource function so we wrap it
395
+ function getData() {
396
+ return getDataResource()
397
+ }
359
398
 
360
- // Cleanup when computed is invalidated
361
- onCleanup(() => {
362
- if (currentUnsubscribe) {
363
- currentUnsubscribe()
364
- currentUnsubscribe = null
365
- }
366
- })
399
+ Object.defineProperties(getData, {
400
+ data: {
401
+ get() {
402
+ return getData()
403
+ },
367
404
  },
368
- undefined,
369
- { name: `TanstackDBSyncComputed` },
370
- )
371
-
372
- return {
373
- state,
374
- data,
375
- collection,
376
- status,
377
- isLoading: () => status() === `loading`,
378
- isReady: () => status() === `ready` || status() === `disabled`,
379
- isIdle: () => status() === `idle`,
380
- isError: () => status() === `error`,
381
- isCleanedUp: () => status() === `cleaned-up`,
382
- }
405
+ status: {
406
+ get() {
407
+ return status()
408
+ },
409
+ },
410
+ collection: {
411
+ get() {
412
+ return collection()
413
+ },
414
+ },
415
+ state: {
416
+ get() {
417
+ return state
418
+ },
419
+ },
420
+ isLoading: {
421
+ get() {
422
+ return status() === `loading`
423
+ },
424
+ },
425
+ isReady: {
426
+ get() {
427
+ return status() === `ready` || status() === `disabled`
428
+ },
429
+ },
430
+ isIdle: {
431
+ get() {
432
+ return status() === `idle`
433
+ },
434
+ },
435
+ isError: {
436
+ get() {
437
+ return status() === `error`
438
+ },
439
+ },
440
+ isCleanedUp: {
441
+ get() {
442
+ return status() === `cleaned-up`
443
+ },
444
+ },
445
+ })
446
+ return getData
383
447
  }