@tanstack/svelte-db 0.0.0 → 0.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kyle Mathews
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ export * from "./useLiveQuery.svelte.js";
2
+ export * from "@tanstack/db";
3
+ export type { Collection } from "@tanstack/db";
4
+ export { createTransaction } from "@tanstack/db";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Re-export all public APIs
2
+ export * from "./useLiveQuery.svelte.js";
3
+ // Re-export everything from @tanstack/db
4
+ export * from "@tanstack/db";
5
+ export { createTransaction } from "@tanstack/db";
@@ -0,0 +1,175 @@
1
+ import type { Collection, CollectionStatus, Context, GetResult, InitialQueryBuilder, LiveQueryCollectionConfig, QueryBuilder } from "@tanstack/db";
2
+ /**
3
+ * Return type for useLiveQuery hook
4
+ * @property state - Reactive Map of query results (key → item)
5
+ * @property data - Reactive array of query results in order
6
+ * @property collection - The underlying query collection instance
7
+ * @property status - Current query status
8
+ * @property isLoading - True while initial query data is loading
9
+ * @property isReady - True when query has received first data and is ready
10
+ * @property isIdle - True when query hasn't started yet
11
+ * @property isError - True when query encountered an error
12
+ * @property isCleanedUp - True when query has been cleaned up
13
+ */
14
+ export interface UseLiveQueryReturn<T extends object> {
15
+ state: Map<string | number, T>;
16
+ data: Array<T>;
17
+ collection: Collection<T, string | number, {}>;
18
+ status: CollectionStatus;
19
+ isLoading: boolean;
20
+ isReady: boolean;
21
+ isIdle: boolean;
22
+ isError: boolean;
23
+ isCleanedUp: boolean;
24
+ }
25
+ export interface UseLiveQueryReturnWithCollection<T extends object, TKey extends string | number, TUtils extends Record<string, any>> {
26
+ state: Map<TKey, T>;
27
+ data: Array<T>;
28
+ collection: Collection<T, TKey, TUtils>;
29
+ status: CollectionStatus;
30
+ isLoading: boolean;
31
+ isReady: boolean;
32
+ isIdle: boolean;
33
+ isError: boolean;
34
+ isCleanedUp: boolean;
35
+ }
36
+ type MaybeGetter<T> = T | (() => T);
37
+ /**
38
+ * Create a live query using a query function
39
+ * @param queryFn - Query function that defines what data to fetch
40
+ * @param deps - Array of reactive dependencies that trigger query re-execution when changed
41
+ * @returns Reactive object with query data, state, and status information
42
+ * @example
43
+ * // Basic query with object syntax
44
+ * const todosQuery = useLiveQuery((q) =>
45
+ * q.from({ todos: todosCollection })
46
+ * .where(({ todos }) => eq(todos.completed, false))
47
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
48
+ * )
49
+ *
50
+ * @example
51
+ * // With reactive dependencies
52
+ * let minPriority = $state(5)
53
+ * const todosQuery = useLiveQuery(
54
+ * (q) => q.from({ todos: todosCollection })
55
+ * .where(({ todos }) => gt(todos.priority, minPriority)),
56
+ * [() => minPriority] // Re-run when minPriority changes
57
+ * )
58
+ *
59
+ * @example
60
+ * // Join pattern
61
+ * const issuesQuery = useLiveQuery((q) =>
62
+ * q.from({ issues: issueCollection })
63
+ * .join({ persons: personCollection }, ({ issues, persons }) =>
64
+ * eq(issues.userId, persons.id)
65
+ * )
66
+ * .select(({ issues, persons }) => ({
67
+ * id: issues.id,
68
+ * title: issues.title,
69
+ * userName: persons.name
70
+ * }))
71
+ * )
72
+ *
73
+ * @example
74
+ * // Handle loading and error states in template
75
+ * const todosQuery = useLiveQuery((q) =>
76
+ * q.from({ todos: todoCollection })
77
+ * )
78
+ *
79
+ * // In template:
80
+ * // {#if todosQuery.isLoading}
81
+ * // <div>Loading...</div>
82
+ * // {:else if todosQuery.isError}
83
+ * // <div>Error: {todosQuery.status}</div>
84
+ * // {:else}
85
+ * // <ul>
86
+ * // {#each todosQuery.data as todo (todo.id)}
87
+ * // <li>{todo.text}</li>
88
+ * // {/each}
89
+ * // </ul>
90
+ * // {/if}
91
+ */
92
+ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, deps?: Array<() => unknown>): UseLiveQueryReturn<GetResult<TContext>>;
93
+ /**
94
+ * Create a live query using configuration object
95
+ * @param config - Configuration object with query and options
96
+ * @param deps - Array of reactive dependencies that trigger query re-execution when changed
97
+ * @returns Reactive object with query 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 reactive dependencies
107
+ * let filter = $state('active')
108
+ * const todosQuery = useLiveQuery({
109
+ * query: (q) => q.from({ todos: todosCollection })
110
+ * .where(({ todos }) => eq(todos.status, filter))
111
+ * }, [() => filter])
112
+ *
113
+ * @example
114
+ * // Handle all states uniformly
115
+ * const itemsQuery = useLiveQuery({
116
+ * query: (q) => q.from({ items: itemCollection })
117
+ * })
118
+ *
119
+ * // In template:
120
+ * // {#if itemsQuery.isLoading}
121
+ * // <div>Loading...</div>
122
+ * // {:else if itemsQuery.isError}
123
+ * // <div>Something went wrong</div>
124
+ * // {:else if !itemsQuery.isReady}
125
+ * // <div>Preparing...</div>
126
+ * // {:else}
127
+ * // <div>{itemsQuery.data.length} items loaded</div>
128
+ * // {/if}
129
+ */
130
+ export declare function useLiveQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>, deps?: Array<() => unknown>): UseLiveQueryReturn<GetResult<TContext>>;
131
+ /**
132
+ * Subscribe to an existing query collection (can be reactive)
133
+ * @param liveQueryCollection - Pre-created query collection to subscribe to (can be a getter)
134
+ * @returns Reactive object with query data, state, and status information
135
+ * @example
136
+ * // Using pre-created query collection
137
+ * const myLiveQuery = createLiveQueryCollection((q) =>
138
+ * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))
139
+ * )
140
+ * const queryResult = useLiveQuery(myLiveQuery)
141
+ *
142
+ * @example
143
+ * // Reactive query collection reference
144
+ * let selectedQuery = $state(todosQuery)
145
+ * const queryResult = useLiveQuery(() => selectedQuery)
146
+ *
147
+ * // Switch queries reactively
148
+ * selectedQuery = archiveQuery
149
+ *
150
+ * @example
151
+ * // Access query collection methods directly
152
+ * const queryResult = useLiveQuery(existingQuery)
153
+ *
154
+ * // Use underlying collection for mutations
155
+ * const handleToggle = (id) => {
156
+ * queryResult.collection.update(id, draft => { draft.completed = !draft.completed })
157
+ * }
158
+ *
159
+ * @example
160
+ * // Handle states consistently
161
+ * const queryResult = useLiveQuery(sharedQuery)
162
+ *
163
+ * // In template:
164
+ * // {#if queryResult.isLoading}
165
+ * // <div>Loading...</div>
166
+ * // {:else if queryResult.isError}
167
+ * // <div>Error loading data</div>
168
+ * // {:else}
169
+ * // {#each queryResult.data as item (item.id)}
170
+ * // <Item {...item} />
171
+ * // {/each}
172
+ * // {/if}
173
+ */
174
+ export declare function useLiveQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: MaybeGetter<Collection<TResult, TKey, TUtils>>): UseLiveQueryReturnWithCollection<TResult, TKey, TUtils>;
175
+ export {};
@@ -0,0 +1,148 @@
1
+ import { untrack } from "svelte";
2
+ import { createLiveQueryCollection } from "@tanstack/db";
3
+ import { SvelteMap } from "svelte/reactivity";
4
+ function toValue(value) {
5
+ if (typeof value === `function`) {
6
+ return value();
7
+ }
8
+ return value;
9
+ }
10
+ // Implementation
11
+ export function useLiveQuery(configOrQueryOrCollection, deps = []) {
12
+ const collection = $derived.by(() => {
13
+ // First check if the original parameter might be a getter
14
+ // by seeing if toValue returns something different than the original
15
+ let unwrappedParam = configOrQueryOrCollection;
16
+ try {
17
+ const potentiallyUnwrapped = toValue(configOrQueryOrCollection);
18
+ if (potentiallyUnwrapped !== configOrQueryOrCollection) {
19
+ unwrappedParam = potentiallyUnwrapped;
20
+ }
21
+ }
22
+ catch {
23
+ // If toValue fails, use original parameter
24
+ unwrappedParam = configOrQueryOrCollection;
25
+ }
26
+ // Check if it's already a collection by checking for specific collection methods
27
+ const isCollection = unwrappedParam &&
28
+ typeof unwrappedParam === `object` &&
29
+ typeof unwrappedParam.subscribeChanges === `function` &&
30
+ typeof unwrappedParam.startSyncImmediate === `function` &&
31
+ typeof unwrappedParam.id === `string`;
32
+ if (isCollection) {
33
+ // It's already a collection, ensure sync is started for Svelte helpers
34
+ unwrappedParam.startSyncImmediate();
35
+ return unwrappedParam;
36
+ }
37
+ // Reference deps to make computed reactive to them
38
+ deps.forEach((dep) => toValue(dep));
39
+ // Ensure we always start sync for Svelte helpers
40
+ if (typeof unwrappedParam === `function`) {
41
+ return createLiveQueryCollection({
42
+ query: unwrappedParam,
43
+ startSync: true,
44
+ });
45
+ }
46
+ else {
47
+ return createLiveQueryCollection({
48
+ ...unwrappedParam,
49
+ startSync: true,
50
+ });
51
+ }
52
+ });
53
+ // Reactive state that gets updated granularly through change events
54
+ const state = new SvelteMap();
55
+ // Reactive data array that maintains sorted order
56
+ let internalData = $state([]);
57
+ // Track collection status reactively
58
+ let status = $state(collection.status);
59
+ // Helper to sync data array from collection in correct order
60
+ const syncDataFromCollection = (currentCollection) => {
61
+ untrack(() => {
62
+ internalData = [];
63
+ internalData.push(...Array.from(currentCollection.values()));
64
+ });
65
+ };
66
+ // Track current unsubscribe function
67
+ let currentUnsubscribe = null;
68
+ // Watch for collection changes and subscribe to updates
69
+ $effect(() => {
70
+ const currentCollection = collection;
71
+ // Update status state whenever the effect runs
72
+ status = currentCollection.status;
73
+ // Clean up previous subscription
74
+ if (currentUnsubscribe) {
75
+ currentUnsubscribe();
76
+ }
77
+ // Initialize state with current collection data
78
+ untrack(() => {
79
+ state.clear();
80
+ for (const [key, value] of currentCollection.entries()) {
81
+ state.set(key, value);
82
+ }
83
+ });
84
+ // Initialize data array in correct order
85
+ syncDataFromCollection(currentCollection);
86
+ // Subscribe to collection changes with granular updates
87
+ currentUnsubscribe = currentCollection.subscribeChanges((changes) => {
88
+ // Apply each change individually to the reactive state
89
+ untrack(() => {
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;
99
+ }
100
+ }
101
+ });
102
+ // Update the data array to maintain sorted order
103
+ syncDataFromCollection(currentCollection);
104
+ // Update status state on every change
105
+ status = currentCollection.status;
106
+ });
107
+ // Preload collection data if not already started
108
+ if (currentCollection.status === `idle`) {
109
+ currentCollection.preload().catch(console.error);
110
+ }
111
+ // Cleanup when effect is invalidated
112
+ return () => {
113
+ if (currentUnsubscribe) {
114
+ currentUnsubscribe();
115
+ currentUnsubscribe = null;
116
+ }
117
+ };
118
+ });
119
+ return {
120
+ get state() {
121
+ return state;
122
+ },
123
+ get data() {
124
+ return internalData;
125
+ },
126
+ get collection() {
127
+ return collection;
128
+ },
129
+ get status() {
130
+ return status;
131
+ },
132
+ get isLoading() {
133
+ return status === `loading` || status === `initialCommit`;
134
+ },
135
+ get isReady() {
136
+ return status === `ready`;
137
+ },
138
+ get isIdle() {
139
+ return status === `idle`;
140
+ },
141
+ get isError() {
142
+ return status === `error`;
143
+ },
144
+ get isCleanedUp() {
145
+ return status === `cleaned-up`;
146
+ },
147
+ };
148
+ }
package/package.json CHANGED
@@ -1,47 +1,52 @@
1
1
  {
2
2
  "name": "@tanstack/svelte-db",
3
3
  "description": "Svelte integration for @tanstack/db",
4
- "version": "0.0.0",
5
- "scripts": {
6
- "build": "svelte-package --input ./src --output ./dist --tsconfig ./tsconfig.build.json",
7
- "test": "npx vitest --run",
8
- "test:types": "svelte-check --tsconfig ./tsconfig.json",
9
- "lint": "eslint . --fix"
10
- },
11
- "files": [
12
- "dist",
13
- "!dist/**/*.test.*",
14
- "!dist/**/*.spec.*"
15
- ],
16
- "sideEffects": [
17
- "**/*.css"
18
- ],
19
- "svelte": "./dist/index.js",
20
- "types": "./dist/index.d.ts",
21
- "type": "module",
22
- "exports": {
23
- ".": {
24
- "types": "./dist/index.d.ts",
25
- "svelte": "./dist/index.js"
26
- }
27
- },
4
+ "version": "0.0.3",
28
5
  "dependencies": {
29
- "@tanstack/db": "workspace:*"
30
- },
31
- "peerDependencies": {
32
- "svelte": "^5.0.0"
6
+ "@tanstack/db": "0.0.33"
33
7
  },
34
8
  "devDependencies": {
35
9
  "@sveltejs/package": "^2.4.0",
36
- "@vitest/coverage-istanbul": "^3.0.9",
37
10
  "@sveltejs/vite-plugin-svelte": "^6.1.0",
11
+ "@vitest/coverage-istanbul": "^3.0.9",
38
12
  "publint": "^0.3.2",
39
13
  "svelte": "^5.28.6",
40
14
  "svelte-check": "^4.3.0"
41
15
  },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "svelte": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/**/*.test.*",
25
+ "!dist/**/*.spec.*"
26
+ ],
42
27
  "keywords": [
43
28
  "optimistic",
44
29
  "svelte",
45
30
  "typescript"
46
- ]
47
- }
31
+ ],
32
+ "peerDependencies": {
33
+ "svelte": "^5.0.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/TanStack/db.git",
38
+ "directory": "packages/svelte-db"
39
+ },
40
+ "sideEffects": [
41
+ "**/*.css"
42
+ ],
43
+ "svelte": "./dist/index.js",
44
+ "type": "module",
45
+ "types": "./dist/index.d.ts",
46
+ "scripts": {
47
+ "build": "svelte-package --input ./src --output ./dist --tsconfig ./tsconfig.build.json",
48
+ "lint": "eslint . --fix",
49
+ "test": "npx vitest --run",
50
+ "test:types": "svelte-check --tsconfig ./tsconfig.json"
51
+ }
52
+ }