@tanstack/solid-db 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @tanstack/solid-db
2
+
3
+ Solidjs hooks for TanStack DB. See [TanStack/db](https://github.com/TanStack/db) for more details.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@tanstack/solid-db",
3
+ "description": "Solid integration for @tanstack/db",
4
+ "version": "0.0.27",
5
+ "author": "Kyle Mathews",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/TanStack/db.git",
10
+ "directory": "packages/solid-db"
11
+ },
12
+ "homepage": "https://tanstack.com/db",
13
+ "keywords": [
14
+ "optimistic",
15
+ "solid",
16
+ "typescript"
17
+ ],
18
+ "packageManager": "pnpm@10.6.3",
19
+ "dependencies": {
20
+ "@solid-primitives/map": "^0.7.2",
21
+ "@tanstack/db": "workspace:*"
22
+ },
23
+ "devDependencies": {
24
+ "@electric-sql/client": "1.0.0",
25
+ "@solidjs/testing-library": "^0.8.10",
26
+ "@vitest/coverage-istanbul": "^3.0.9",
27
+ "jsdom": "^26.0.0",
28
+ "solid-js": "^1.9.7",
29
+ "vite-plugin-solid": "^2.11.7",
30
+ "vitest": "^3.0.9"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/esm/index.d.ts",
36
+ "default": "./dist/esm/index.js"
37
+ }
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src"
44
+ ],
45
+ "main": "dist/cjs/index.cjs",
46
+ "module": "dist/esm/index.js",
47
+ "peerDependencies": {
48
+ "solid-js": ">=1.9.0"
49
+ },
50
+ "scripts": {
51
+ "build": "vite build",
52
+ "dev": "vite build --watch",
53
+ "test": "vitest --run",
54
+ "lint": "eslint . --fix"
55
+ },
56
+ "sideEffects": false,
57
+ "type": "module",
58
+ "types": "dist/esm/index.d.ts"
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Re-export all public APIs
2
+ export * from "./useLiveQuery"
3
+
4
+ // Re-export everything from @tanstack/db
5
+ export * from "@tanstack/db"
6
+
7
+ // Re-export some stuff explicitly to ensure the type & value is exported
8
+ export type { Collection } from "@tanstack/db"
9
+ export { createTransaction } from "@tanstack/db"
@@ -0,0 +1,326 @@
1
+ import {
2
+ batch,
3
+ createComputed,
4
+ createMemo,
5
+ createResource,
6
+ createSignal,
7
+ onCleanup,
8
+ } from "solid-js"
9
+ import { ReactiveMap } from "@solid-primitives/map"
10
+ import { CollectionImpl, createLiveQueryCollection } from "@tanstack/db"
11
+ import { createStore, reconcile } from "solid-js/store"
12
+ import type { Accessor } from "solid-js"
13
+ import type {
14
+ ChangeMessage,
15
+ Collection,
16
+ CollectionStatus,
17
+ Context,
18
+ GetResult,
19
+ InitialQueryBuilder,
20
+ LiveQueryCollectionConfig,
21
+ QueryBuilder,
22
+ } from "@tanstack/db"
23
+
24
+ /**
25
+ * Create a live query using a query function
26
+ * @param queryFn - Query function that defines what data to fetch
27
+ * @returns Object with reactive data, state, and status information
28
+ * @example
29
+ * // Basic query with object syntax
30
+ * const todosQuery = useLiveQuery((q) =>
31
+ * q.from({ todos: todosCollection })
32
+ * .where(({ todos }) => eq(todos.completed, false))
33
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
34
+ * )
35
+ *
36
+ * @example
37
+ * // With dependencies that trigger re-execution
38
+ * const todosQuery = useLiveQuery(
39
+ * (q) => q.from({ todos: todosCollection })
40
+ * .where(({ todos }) => gt(todos.priority, minPriority())),
41
+ * )
42
+ *
43
+ * @example
44
+ * // Join pattern
45
+ * const personIssues = useLiveQuery((q) =>
46
+ * q.from({ issues: issueCollection })
47
+ * .join({ persons: personCollection }, ({ issues, persons }) =>
48
+ * eq(issues.userId, persons.id)
49
+ * )
50
+ * .select(({ issues, persons }) => ({
51
+ * id: issues.id,
52
+ * title: issues.title,
53
+ * userName: persons.name
54
+ * }))
55
+ * )
56
+ *
57
+ * @example
58
+ * // Handle loading and error states
59
+ * const todosQuery = useLiveQuery((q) =>
60
+ * q.from({ todos: todoCollection })
61
+ * )
62
+ *
63
+ * return (
64
+ * <Switch>
65
+ * <Match when={todosQuery.isLoading()}>
66
+ * <div>Loading...</div>
67
+ * </Match>
68
+ * <Match when={todosQuery.isError()}>
69
+ * <div>Error: {todosQuery.status()}</div>
70
+ * </Match>
71
+ * <Match when={todosQuery.isReady()}>
72
+ * <For each={todosQuery.data()}>
73
+ * {(todo) => <li key={todo.id}>{todo.text}</li>}
74
+ * </For>
75
+ * </Match>
76
+ * </Switch>
77
+ * )
78
+ */
79
+ // Overload 1: Accept just the query function
80
+ export function useLiveQuery<TContext extends Context>(
81
+ queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>
82
+ ): {
83
+ state: ReactiveMap<string | number, GetResult<TContext>>
84
+ data: Array<GetResult<TContext>>
85
+ collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>
86
+ status: Accessor<CollectionStatus>
87
+ isLoading: Accessor<boolean>
88
+ isReady: Accessor<boolean>
89
+ isIdle: Accessor<boolean>
90
+ isError: Accessor<boolean>
91
+ isCleanedUp: Accessor<boolean>
92
+ }
93
+
94
+ /**
95
+ * Create a live query using configuration object
96
+ * @param config - Configuration object with query and options
97
+ * @returns Object with reactive data, state, and status information
98
+ * @example
99
+ * // Basic config object usage
100
+ * const todosQuery = useLiveQuery(() => ({
101
+ * query: (q) => q.from({ todos: todosCollection }),
102
+ * gcTime: 60000
103
+ * }))
104
+ *
105
+ * @example
106
+ * // With query builder and options
107
+ * const queryBuilder = new Query()
108
+ * .from({ persons: collection })
109
+ * .where(({ persons }) => gt(persons.age, 30))
110
+ * .select(({ persons }) => ({ id: persons.id, name: persons.name }))
111
+ *
112
+ * const personsQuery = useLiveQuery(() => ({ query: queryBuilder }))
113
+ *
114
+ * @example
115
+ * // Handle all states uniformly
116
+ * const itemsQuery = useLiveQuery(() => ({
117
+ * query: (q) => q.from({ items: itemCollection })
118
+ * }))
119
+ *
120
+ * return (
121
+ * <Switch fallback={<div>{itemsQuery.data.length} items loaded</div>}>
122
+ * <Match when={itemsQuery.isLoading()}>
123
+ * <div>Loading...</div>
124
+ * </Match>
125
+ * <Match when={itemsQuery.isError()}>
126
+ * <div>Something went wrong</div>
127
+ * </Match>
128
+ * <Match when={!itemsQuery.isReady()}>
129
+ * <div>Preparing...</div>
130
+ * </Match>
131
+ * </Switch>
132
+ * )
133
+ */
134
+ // Overload 2: Accept config object
135
+ export function useLiveQuery<TContext extends Context>(
136
+ config: Accessor<LiveQueryCollectionConfig<TContext>>
137
+ ): {
138
+ state: ReactiveMap<string | number, GetResult<TContext>>
139
+ data: Array<GetResult<TContext>>
140
+ collection: Accessor<Collection<GetResult<TContext>, string | number, {}>>
141
+ status: Accessor<CollectionStatus>
142
+ isLoading: Accessor<boolean>
143
+ isReady: Accessor<boolean>
144
+ isIdle: Accessor<boolean>
145
+ isError: Accessor<boolean>
146
+ isCleanedUp: Accessor<boolean>
147
+ }
148
+
149
+ /**
150
+ * Subscribe to an existing live query collection
151
+ * @param liveQueryCollection - Pre-created live query collection to subscribe to
152
+ * @returns Object with reactive data, state, and status information
153
+ * @example
154
+ * // Using pre-created live query collection
155
+ * const myLiveQuery = createLiveQueryCollection((q) =>
156
+ * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))
157
+ * )
158
+ * const todosQuery = useLiveQuery(() => myLiveQuery)
159
+ *
160
+ * @example
161
+ * // Access collection methods directly
162
+ * const existingQuery = useLiveQuery(() => existingCollection)
163
+ *
164
+ * // Use collection for mutations
165
+ * const handleToggle = (id) => {
166
+ * existingQuery.collection().update(id, draft => { draft.completed = !draft.completed })
167
+ * }
168
+ *
169
+ * @example
170
+ * // Handle states consistently
171
+ * const sharedQuery = useLiveQuery(() => sharedCollection)
172
+ *
173
+ * return (
174
+ * <Switch fallback={<div><For each={sharedQuery.data()}>{(item) => <Item key={item.id} {...item} />}</For></div>}>
175
+ * <Match when={sharedQuery.isLoading()}>
176
+ * <div>Loading...</div>
177
+ * </Match>
178
+ * <Match when={sharedQuery.isError()}>
179
+ * <div>Error loading data</div>
180
+ * </Match>
181
+ * </Switch>
182
+ * )
183
+ */
184
+ // Overload 3: Accept pre-created live query collection
185
+ export function useLiveQuery<
186
+ TResult extends object,
187
+ TKey extends string | number,
188
+ TUtils extends Record<string, any>,
189
+ >(
190
+ liveQueryCollection: Accessor<Collection<TResult, TKey, TUtils>>
191
+ ): {
192
+ state: ReactiveMap<TKey, TResult>
193
+ data: Array<TResult>
194
+ collection: Accessor<Collection<TResult, TKey, TUtils>>
195
+ status: Accessor<CollectionStatus>
196
+ isLoading: Accessor<boolean>
197
+ isReady: Accessor<boolean>
198
+ isIdle: Accessor<boolean>
199
+ isError: Accessor<boolean>
200
+ isCleanedUp: Accessor<boolean>
201
+ }
202
+
203
+ // Implementation - use function overloads to infer the actual collection type
204
+ export function useLiveQuery(
205
+ configOrQueryOrCollection: (queryFn?: any) => any
206
+ ) {
207
+ const collection = createMemo(
208
+ () => {
209
+ if (configOrQueryOrCollection.length === 1) {
210
+ return createLiveQueryCollection({
211
+ query: configOrQueryOrCollection,
212
+ startSync: true,
213
+ })
214
+ }
215
+
216
+ const innerCollection = configOrQueryOrCollection()
217
+ if (innerCollection instanceof CollectionImpl) {
218
+ innerCollection.startSyncImmediate()
219
+ return innerCollection as Collection
220
+ }
221
+
222
+ return createLiveQueryCollection({
223
+ ...innerCollection,
224
+ startSync: true,
225
+ })
226
+ },
227
+ undefined,
228
+ { name: `TanstackDBCollectionMemo` }
229
+ )
230
+
231
+ // Reactive state that gets updated granularly through change events
232
+ const state = new ReactiveMap<string | number, any>()
233
+
234
+ // Reactive data array that maintains sorted order
235
+ const [data, setData] = createStore<Array<any>>([], {
236
+ name: `TanstackDBData`,
237
+ })
238
+
239
+ // Track collection status reactively
240
+ const [status, setStatus] = createSignal(collection().status, {
241
+ name: `TanstackDBStatus`,
242
+ })
243
+
244
+ // Helper to sync data array from collection in correct order
245
+ const syncDataFromCollection = (
246
+ currentCollection: Collection<any, any, any>
247
+ ) => {
248
+ setData((prev) =>
249
+ reconcile(Array.from(currentCollection.values()))(prev).filter(Boolean)
250
+ )
251
+ }
252
+
253
+ // Track current unsubscribe function
254
+ let currentUnsubscribe: (() => void) | null = null
255
+
256
+ createComputed(
257
+ () => {
258
+ const currentCollection = collection()
259
+
260
+ // Update status ref whenever the effect runs
261
+ setStatus(currentCollection.status)
262
+
263
+ // Initialize state with current collection data
264
+ state.clear()
265
+ for (const [key, value] of currentCollection.entries()) {
266
+ state.set(key, value)
267
+ }
268
+
269
+ // Initialize data array in correct order
270
+ syncDataFromCollection(currentCollection)
271
+
272
+ // Subscribe to collection changes with granular updates
273
+ currentUnsubscribe = currentCollection.subscribeChanges(
274
+ (changes: Array<ChangeMessage<any>>) => {
275
+ // Apply each change individually to the reactive state
276
+ batch(() => {
277
+ for (const change of changes) {
278
+ switch (change.type) {
279
+ case `insert`:
280
+ case `update`:
281
+ state.set(change.key, change.value)
282
+ break
283
+ case `delete`:
284
+ state.delete(change.key)
285
+ break
286
+ }
287
+ }
288
+ })
289
+
290
+ // Update the data array to maintain sorted order
291
+ syncDataFromCollection(currentCollection)
292
+
293
+ // Update status ref on every change
294
+ setStatus(currentCollection.status)
295
+ }
296
+ )
297
+
298
+ // Preload collection data if not already started
299
+ if (currentCollection.status === `idle`) {
300
+ createResource(() => currentCollection.preload())
301
+ }
302
+
303
+ // Cleanup when computed is invalidated
304
+ onCleanup(() => {
305
+ if (currentUnsubscribe) {
306
+ currentUnsubscribe()
307
+ currentUnsubscribe = null
308
+ }
309
+ })
310
+ },
311
+ undefined,
312
+ { name: `TanstackDBSyncComputed` }
313
+ )
314
+
315
+ return {
316
+ state,
317
+ data,
318
+ collection,
319
+ status,
320
+ isLoading: () => status() === `loading` || status() === `initialCommit`,
321
+ isReady: () => status() === `ready`,
322
+ isIdle: () => status() === `idle`,
323
+ isError: () => status() === `error`,
324
+ isCleanedUp: () => status() === `cleaned-up`,
325
+ }
326
+ }