@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 +21 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/useLiveQuery.svelte.d.ts +175 -0
- package/dist/useLiveQuery.svelte.js +148 -0
- package/package.json +36 -31
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.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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.
|
|
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": "
|
|
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
|
+
}
|