@tanstack/svelte-db 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DbProvider.svelte +17 -0
- package/dist/DbProvider.svelte.d.ts +9 -0
- package/dist/db-context.d.ts +6 -0
- package/dist/db-context.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/useLiveQuery.svelte.d.ts +6 -2
- package/dist/useLiveQuery.svelte.js +107 -82
- package/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setDbClientContext } from './db-context.js'
|
|
3
|
+
import type { DbClient } from '@tanstack/db'
|
|
4
|
+
import type { Snippet } from 'svelte'
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
client,
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
client: DbClient
|
|
11
|
+
children?: Snippet
|
|
12
|
+
} = $props()
|
|
13
|
+
|
|
14
|
+
setDbClientContext(() => client)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{@render children?.()}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DbClient } from '@tanstack/db';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
client: DbClient;
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const DbProvider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type DbProvider = ReturnType<typeof DbProvider>;
|
|
9
|
+
export default DbProvider;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DbClient } from '@tanstack/db';
|
|
2
|
+
type DbClientContext = () => DbClient;
|
|
3
|
+
export declare function setDbClientContext(client: DbClientContext): DbClientContext;
|
|
4
|
+
export declare function useDbClient(): DbClient;
|
|
5
|
+
export declare function useOptionalDbClient(): DbClient | undefined;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const dbClientContext = Symbol.for(`@tanstack/svelte-db.DbClient`);
|
|
3
|
+
export function setDbClientContext(client) {
|
|
4
|
+
return setContext(dbClientContext, client);
|
|
5
|
+
}
|
|
6
|
+
export function useDbClient() {
|
|
7
|
+
const client = useOptionalDbClient();
|
|
8
|
+
if (!client) {
|
|
9
|
+
throw new Error(`useDbClient must be used within a DbProvider.`);
|
|
10
|
+
}
|
|
11
|
+
return client;
|
|
12
|
+
}
|
|
13
|
+
export function useOptionalDbClient() {
|
|
14
|
+
try {
|
|
15
|
+
return getContext(dbClientContext)?.();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Legacy helpers may be called from a rune root rather than a component.
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './useLiveQuery.svelte.js';
|
|
2
2
|
export * from './useLiveInfiniteQuery.svelte.js';
|
|
3
|
+
export { useDbClient, useOptionalDbClient } from './db-context.js';
|
|
4
|
+
export { default as DbProvider } from './DbProvider.svelte';
|
|
3
5
|
export * from '@tanstack/db';
|
|
4
6
|
export type { Collection } from '@tanstack/db';
|
|
5
7
|
export { createTransaction } from '@tanstack/db';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Re-export all public APIs
|
|
2
2
|
export * from './useLiveQuery.svelte.js';
|
|
3
3
|
export * from './useLiveInfiniteQuery.svelte.js';
|
|
4
|
+
export { useDbClient, useOptionalDbClient } from './db-context.js';
|
|
5
|
+
export { default as DbProvider } from './DbProvider.svelte';
|
|
4
6
|
// Re-export everything from @tanstack/db
|
|
5
7
|
export * from '@tanstack/db';
|
|
6
8
|
export { createTransaction } from '@tanstack/db';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
|
|
1
|
+
import type { Collection, CollectionStatus, Context, DbClient, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, LiveQueryKey, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
|
|
2
2
|
/**
|
|
3
3
|
* Return type for useLiveQuery hook
|
|
4
4
|
* @property state - Reactive Map of query results (key → item)
|
|
@@ -34,6 +34,10 @@ export interface UseLiveQueryReturnWithCollection<T extends object, TKey extends
|
|
|
34
34
|
isCleanedUp: boolean;
|
|
35
35
|
}
|
|
36
36
|
type MaybeGetter<T> = T | (() => T);
|
|
37
|
+
export type UseLiveQueryConfig<TContext extends Context> = LiveQueryCollectionConfig<TContext> & {
|
|
38
|
+
queryKey?: MaybeGetter<LiveQueryKey>;
|
|
39
|
+
client?: DbClient;
|
|
40
|
+
};
|
|
37
41
|
/**
|
|
38
42
|
* Create a live query using a query function
|
|
39
43
|
* @param queryFn - Query function that defines what data to fetch
|
|
@@ -161,7 +165,7 @@ export declare function useLiveQuery<TContext extends Context>(queryFn: (q: Init
|
|
|
161
165
|
* // <div>{itemsQuery.data.length} items loaded</div>
|
|
162
166
|
* // {/if}
|
|
163
167
|
*/
|
|
164
|
-
export declare function useLiveQuery<TContext extends Context>(config:
|
|
168
|
+
export declare function useLiveQuery<TContext extends Context>(config: UseLiveQueryConfig<TContext>, deps?: Array<() => unknown>): UseLiveQueryReturn<GetResult<TContext>, InferResultType<TContext>>;
|
|
165
169
|
/**
|
|
166
170
|
* Subscribe to an existing query collection (can be reactive)
|
|
167
171
|
* @param liveQueryCollection - Pre-created query collection to subscribe to (can be a getter)
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
// eslint-disable-next-line import/no-duplicates -- See https://github.com/un-ts/eslint-plugin-import-x/issues/308
|
|
4
4
|
import { SvelteMap } from 'svelte/reactivity';
|
|
5
|
-
import { BaseQueryBuilder, createLiveQueryCollection, createLiveQueryObserver, isCollection, isSingleResultCollection, } from '@tanstack/db';
|
|
5
|
+
import { BaseQueryBuilder, UnhashableQueryIRError, createLiveQueryCollection, createLiveQueryObserver, getLiveQueryHash, getStableValueHash, isCollection, isSingleResultCollection, prepareLiveQueryValue, } from '@tanstack/db';
|
|
6
|
+
import { useOptionalDbClient } from './db-context.js';
|
|
6
7
|
function toValue(value) {
|
|
7
8
|
if (typeof value === `function`) {
|
|
8
9
|
return value();
|
|
@@ -11,7 +12,8 @@ function toValue(value) {
|
|
|
11
12
|
}
|
|
12
13
|
// Implementation
|
|
13
14
|
export function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
14
|
-
const
|
|
15
|
+
const contextDbClient = useOptionalDbClient();
|
|
16
|
+
const resolved = $derived.by(() => {
|
|
15
17
|
// First check if the original parameter might be a getter
|
|
16
18
|
// by seeing if toValue returns something different than the original
|
|
17
19
|
let unwrappedParam = configOrQueryOrCollection;
|
|
@@ -27,6 +29,10 @@ export function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
27
29
|
}
|
|
28
30
|
// Check if it's already a collection by checking for specific collection methods
|
|
29
31
|
const inputIsCollection = isCollection(unwrappedParam);
|
|
32
|
+
const dbClient = inputIsCollection
|
|
33
|
+
? contextDbClient
|
|
34
|
+
: (unwrappedParam?.client ??
|
|
35
|
+
contextDbClient);
|
|
30
36
|
if (inputIsCollection) {
|
|
31
37
|
// Warn when passing a collection directly with on-demand sync mode
|
|
32
38
|
// In on-demand mode, data is only loaded when queries with predicates request it
|
|
@@ -45,109 +51,128 @@ export function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
45
51
|
if (unwrappedParam.status === `idle`) {
|
|
46
52
|
unwrappedParam.startSyncImmediate();
|
|
47
53
|
}
|
|
48
|
-
return
|
|
54
|
+
return {
|
|
55
|
+
collection: unwrappedParam,
|
|
56
|
+
client: dbClient,
|
|
57
|
+
queryHash: getStableValueHash([`collection`, unwrappedParam.id], `queryKey`),
|
|
58
|
+
resumeDeferredCollections: () => { },
|
|
59
|
+
};
|
|
49
60
|
}
|
|
50
61
|
// Reference deps to make computed reactive to them
|
|
51
|
-
deps.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
const dependencyValues = deps.map((dep) => toValue(dep));
|
|
63
|
+
const deferredCollections = new Set();
|
|
64
|
+
const preparedValue = prepareLiveQueryValue(unwrappedParam, dbClient, deferredCollections);
|
|
65
|
+
const configuredQueryKey = unwrappedParam?.queryKey;
|
|
66
|
+
const queryKey = configuredQueryKey
|
|
67
|
+
? toValue(configuredQueryKey)
|
|
68
|
+
: undefined;
|
|
69
|
+
let queryHash;
|
|
70
|
+
try {
|
|
71
|
+
queryHash =
|
|
72
|
+
deps.length > 0 && !queryKey
|
|
73
|
+
? getStableValueHash([`deps`, dependencyValues, getLiveQueryHash(preparedValue)], `queryKey`)
|
|
74
|
+
: getLiveQueryHash(preparedValue, queryKey);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!(error instanceof UnhashableQueryIRError))
|
|
78
|
+
throw error;
|
|
79
|
+
if (queryKey !== undefined)
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
let collection;
|
|
83
|
+
if (preparedValue === undefined || preparedValue === null) {
|
|
84
|
+
collection = null;
|
|
85
|
+
}
|
|
86
|
+
else if (isCollection(preparedValue)) {
|
|
87
|
+
collection = preparedValue;
|
|
88
|
+
}
|
|
89
|
+
else if (preparedValue instanceof BaseQueryBuilder) {
|
|
90
|
+
collection = createLiveQueryCollection({
|
|
91
|
+
query: preparedValue,
|
|
63
92
|
startSync: true,
|
|
64
93
|
});
|
|
65
94
|
}
|
|
66
95
|
else {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// otherwise `{ ...null }` reaches createLiveQueryCollection and throws.
|
|
70
|
-
if (unwrappedParam === undefined || unwrappedParam === null) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
return createLiveQueryCollection({
|
|
74
|
-
...unwrappedParam,
|
|
96
|
+
collection = createLiveQueryCollection({
|
|
97
|
+
...preparedValue,
|
|
75
98
|
startSync: true,
|
|
76
99
|
});
|
|
77
100
|
}
|
|
101
|
+
return {
|
|
102
|
+
collection,
|
|
103
|
+
client: dbClient,
|
|
104
|
+
queryHash,
|
|
105
|
+
resumeDeferredCollections: () => {
|
|
106
|
+
for (const deferredCollection of deferredCollections) {
|
|
107
|
+
deferredCollection._resumeSyncStart();
|
|
108
|
+
}
|
|
109
|
+
deferredCollections.clear();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
78
112
|
});
|
|
113
|
+
let currentResolved = untrack(() => resolved);
|
|
114
|
+
let currentObserver = createLiveQueryObserver(currentResolved.collection, {
|
|
115
|
+
client: currentResolved.client,
|
|
116
|
+
queryHash: currentResolved.queryHash,
|
|
117
|
+
onPreload: currentResolved.resumeDeferredCollections,
|
|
118
|
+
});
|
|
119
|
+
const initialSnapshot = currentObserver.getServerSnapshot();
|
|
79
120
|
// Reactive state that gets updated granularly through change events
|
|
80
|
-
const state = new SvelteMap();
|
|
121
|
+
const state = new SvelteMap(initialSnapshot.state ?? []);
|
|
81
122
|
// Reactive data array that maintains sorted order
|
|
82
|
-
let internalData = $state([]);
|
|
123
|
+
let internalData = $state(Array.from(initialSnapshot.state?.values() ?? []));
|
|
83
124
|
// Track collection status reactively
|
|
84
|
-
let status = $state(
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
let status = $state(initialSnapshot.status);
|
|
126
|
+
const syncFromObserver = (observer, changes) => {
|
|
127
|
+
const snapshot = observer.getSnapshot();
|
|
128
|
+
status = snapshot.status;
|
|
87
129
|
untrack(() => {
|
|
88
|
-
|
|
89
|
-
|
|
130
|
+
if (changes && changes.length > 0) {
|
|
131
|
+
for (const change of changes) {
|
|
132
|
+
switch (change.type) {
|
|
133
|
+
case `insert`:
|
|
134
|
+
case `update`:
|
|
135
|
+
state.set(change.key, change.value);
|
|
136
|
+
break;
|
|
137
|
+
case `delete`:
|
|
138
|
+
state.delete(change.key);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
state.clear();
|
|
145
|
+
for (const [key, value] of snapshot.state ?? []) {
|
|
146
|
+
state.set(key, value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
internalData = Array.from(snapshot.state?.values() ?? []);
|
|
90
150
|
});
|
|
91
151
|
};
|
|
92
|
-
// The shared observer owns subscription, the ready-race, and status; Svelte
|
|
93
|
-
// materializes into its own rune-backed map (granular) + ordered array.
|
|
94
|
-
let currentObserver = null;
|
|
95
|
-
const syncFromObserver = (observer, currentCollection) => {
|
|
96
|
-
status = observer.getSnapshot().status;
|
|
97
|
-
syncDataFromCollection(currentCollection);
|
|
98
|
-
};
|
|
99
152
|
// Watch for collection changes and subscribe to updates
|
|
100
153
|
$effect(() => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
state.clear();
|
|
110
|
-
internalData = [];
|
|
154
|
+
const nextResolved = resolved;
|
|
155
|
+
if (nextResolved !== currentResolved) {
|
|
156
|
+
currentObserver.dispose();
|
|
157
|
+
currentResolved = nextResolved;
|
|
158
|
+
currentObserver = createLiveQueryObserver(nextResolved.collection, {
|
|
159
|
+
client: nextResolved.client,
|
|
160
|
+
queryHash: nextResolved.queryHash,
|
|
161
|
+
onPreload: nextResolved.resumeDeferredCollections,
|
|
111
162
|
});
|
|
112
|
-
|
|
163
|
+
syncFromObserver(currentObserver);
|
|
113
164
|
}
|
|
114
|
-
const observer =
|
|
115
|
-
currentObserver = observer;
|
|
116
|
-
// Initial rows arrive as the observer's first delta (includeInitialState);
|
|
117
|
-
// apply them and every subsequent delta granularly to the rune-backed map.
|
|
118
|
-
untrack(() => state.clear());
|
|
165
|
+
const observer = currentObserver;
|
|
119
166
|
const unsubscribe = observer.subscribe((changes) => {
|
|
120
|
-
|
|
121
|
-
if (changes) {
|
|
122
|
-
for (const change of changes) {
|
|
123
|
-
switch (change.type) {
|
|
124
|
-
case `insert`:
|
|
125
|
-
case `update`:
|
|
126
|
-
state.set(change.key, change.value);
|
|
127
|
-
break;
|
|
128
|
-
case `delete`:
|
|
129
|
-
state.delete(change.key);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
// Cleanup and other status-only publications carry no row deltas.
|
|
136
|
-
// Rebuild the keyed view so it cannot diverge from ordered data.
|
|
137
|
-
state.clear();
|
|
138
|
-
for (const [key, value] of observer.getSnapshot().state ?? []) {
|
|
139
|
-
state.set(key, value);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
syncFromObserver(observer, currentCollection);
|
|
167
|
+
syncFromObserver(observer, changes);
|
|
144
168
|
});
|
|
145
|
-
|
|
169
|
+
currentResolved.resumeDeferredCollections();
|
|
170
|
+
syncFromObserver(observer);
|
|
146
171
|
// Cleanup when effect is invalidated
|
|
147
172
|
return () => {
|
|
148
173
|
unsubscribe();
|
|
149
|
-
observer
|
|
150
|
-
|
|
174
|
+
if (observer === currentObserver)
|
|
175
|
+
observer.dispose();
|
|
151
176
|
};
|
|
152
177
|
});
|
|
153
178
|
return {
|
|
@@ -155,14 +180,14 @@ export function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
155
180
|
return state;
|
|
156
181
|
},
|
|
157
182
|
get data() {
|
|
158
|
-
const currentCollection = collection;
|
|
183
|
+
const currentCollection = resolved.collection;
|
|
159
184
|
if (currentCollection && isSingleResultCollection(currentCollection)) {
|
|
160
185
|
return internalData[0];
|
|
161
186
|
}
|
|
162
187
|
return internalData;
|
|
163
188
|
},
|
|
164
189
|
get collection() {
|
|
165
|
-
return collection;
|
|
190
|
+
return resolved.collection;
|
|
166
191
|
},
|
|
167
192
|
get status() {
|
|
168
193
|
return status;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/svelte-db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Svelte integration for @tanstack/db",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"skills"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@tanstack/db": "0.
|
|
38
|
+
"@tanstack/db": "0.8.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"svelte": "^5.0.0"
|