@tanstack/svelte-db 0.1.94 → 0.2.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/README.md +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/useLiveInfiniteQuery.svelte.d.ts +40 -0
- package/dist/useLiveInfiniteQuery.svelte.js +136 -0
- package/dist/useLiveQuery.svelte.js +41 -56
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source
|
|
4
|
+
media="(prefers-color-scheme: dark)"
|
|
5
|
+
srcset="https://tanstack.com/api/readme/db.png?framework=svelte&theme=dark"
|
|
6
|
+
/>
|
|
7
|
+
<source
|
|
8
|
+
media="(prefers-color-scheme: light)"
|
|
9
|
+
srcset="https://tanstack.com/api/readme/db.png?framework=svelte"
|
|
10
|
+
/>
|
|
11
|
+
<img
|
|
12
|
+
src="https://tanstack.com/api/readme/db.png?framework=svelte"
|
|
13
|
+
alt="TanStack Svelte DB"
|
|
14
|
+
width="900"
|
|
15
|
+
/>
|
|
16
|
+
</picture>
|
|
17
|
+
</div>
|
|
1
18
|
# @tanstack/svelte-db
|
|
2
19
|
|
|
3
20
|
Svelte helpers for TanStack DB. See [TanStack/db](https://github.com/TanStack/db) for more details.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { UseLiveQueryReturnWithCollection, useLiveQuery } from './useLiveQuery.svelte.js';
|
|
2
|
+
import type { Collection, Context, InferResultType, InitialQueryBuilder, NonSingleResult, QueryBuilder } from '@tanstack/db';
|
|
3
|
+
type MaybeGetter<T> = T | (() => T);
|
|
4
|
+
type InfiniteQueryOptions = {
|
|
5
|
+
pageSize?: number;
|
|
6
|
+
initialPageParam?: number;
|
|
7
|
+
};
|
|
8
|
+
export type LiveInfiniteQueryConfig<TRow> = InfiniteQueryOptions & {
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated Pagination uses the shared controller's peek-ahead strategy.
|
|
11
|
+
* This remains for compatibility with TanStack Query conventions.
|
|
12
|
+
*/
|
|
13
|
+
getNextPageParam?: (lastPage: Array<TRow>, allPages: Array<Array<TRow>>, lastPageParam: number, allPageParams: Array<number>) => number | undefined;
|
|
14
|
+
};
|
|
15
|
+
export type UseLiveInfiniteQueryConfig<TContext extends Context> = LiveInfiniteQueryConfig<InferResultType<TContext>[number]>;
|
|
16
|
+
export type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<ReturnType<typeof useLiveQuery<TContext>>, `data`> & {
|
|
17
|
+
data: InferResultType<TContext>;
|
|
18
|
+
pages: Array<Array<InferResultType<TContext>[number]>>;
|
|
19
|
+
pageParams: Array<number>;
|
|
20
|
+
fetchNextPage: () => Promise<void>;
|
|
21
|
+
hasNextPage: boolean;
|
|
22
|
+
isFetchingNextPage: boolean;
|
|
23
|
+
error: unknown;
|
|
24
|
+
};
|
|
25
|
+
export type UseLiveInfiniteQueryReturnWithCollection<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>> = Omit<UseLiveQueryReturnWithCollection<TResult, TKey, TUtils, Array<TResult>>, `data`> & {
|
|
26
|
+
data: Array<TResult>;
|
|
27
|
+
pages: Array<Array<TResult>>;
|
|
28
|
+
pageParams: Array<number>;
|
|
29
|
+
fetchNextPage: () => Promise<void>;
|
|
30
|
+
hasNextPage: boolean;
|
|
31
|
+
isFetchingNextPage: boolean;
|
|
32
|
+
error: unknown;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Create a Svelte-native reactive view over the shared live-query window
|
|
36
|
+
* controller. The query must include an `orderBy` clause.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useLiveInfiniteQuery<TResult extends object, TKey extends string | number, TUtils extends Record<string, any>>(liveQueryCollection: MaybeGetter<Collection<TResult, TKey, TUtils> & NonSingleResult>, config: LiveInfiniteQueryConfig<TResult>): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>;
|
|
39
|
+
export declare function useLiveInfiniteQuery<TContext extends Context>(queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>, config: UseLiveInfiniteQueryConfig<TContext>, deps?: Array<() => unknown>): UseLiveInfiniteQueryReturn<TContext>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { assertLiveQueryWindowManyResult, compareLiveQueryWindowDependencies, createLiveQueryCollection, createLiveQueryWindowController, fetchNextLiveQueryWindowPage, getLiveQueryWindowCollectionWarning, normalizeLiveQueryWindowPageSize, resolveLiveQueryWindowInput, shouldPreserveLiveQueryWindowPageCount, } from '@tanstack/db';
|
|
2
|
+
import { tick, untrack } from 'svelte';
|
|
3
|
+
const DEFAULT_GC_TIME_MS = 1;
|
|
4
|
+
export function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
|
|
5
|
+
let validatedCollection = null;
|
|
6
|
+
let previousController = null;
|
|
7
|
+
let previousInput = null;
|
|
8
|
+
let previousDependencies = null;
|
|
9
|
+
let previousPageSize = null;
|
|
10
|
+
let previousInitialPageParam = null;
|
|
11
|
+
const pageSize = $derived(normalizeLiveQueryWindowPageSize(config.pageSize));
|
|
12
|
+
const initialPageParam = $derived(config.initialPageParam ?? 0);
|
|
13
|
+
const controller = $derived.by(() => {
|
|
14
|
+
const dependencies = deps.map((dependency) => dependency());
|
|
15
|
+
const input = resolveLiveQueryWindowInput(queryFnOrCollection);
|
|
16
|
+
const dependencyComparison = compareLiveQueryWindowDependencies(previousDependencies, dependencies);
|
|
17
|
+
const dependenciesChanged = dependencyComparison.changed;
|
|
18
|
+
const dependenciesStructurallyEqual = dependencyComparison.structurallyEqual;
|
|
19
|
+
const pageShapeChanged = previousPageSize !== pageSize ||
|
|
20
|
+
previousInitialPageParam !== initialPageParam;
|
|
21
|
+
const sameCollection = input.kind === `collection` &&
|
|
22
|
+
previousInput?.kind === `collection` &&
|
|
23
|
+
previousInput.collection === input.collection;
|
|
24
|
+
const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({
|
|
25
|
+
hasPreviousController: previousController !== null,
|
|
26
|
+
previousInputKind: previousInput?.kind,
|
|
27
|
+
inputKind: input.kind,
|
|
28
|
+
sameCollection,
|
|
29
|
+
dependenciesChanged,
|
|
30
|
+
dependenciesStructurallyEqual,
|
|
31
|
+
pageShapeChanged,
|
|
32
|
+
});
|
|
33
|
+
const previousPageCount = previousController
|
|
34
|
+
? Math.max(1, previousController.getSnapshot().pages.length)
|
|
35
|
+
: 1;
|
|
36
|
+
const initialPageCount = canPreservePageCount ? previousPageCount : 1;
|
|
37
|
+
previousInput = input;
|
|
38
|
+
previousDependencies = [...dependencies];
|
|
39
|
+
previousPageSize = pageSize;
|
|
40
|
+
previousInitialPageParam = initialPageParam;
|
|
41
|
+
if (input.kind === `collection`) {
|
|
42
|
+
const collection = input.collection;
|
|
43
|
+
const warning = getLiveQueryWindowCollectionWarning(collection, pageSize + 1);
|
|
44
|
+
if (validatedCollection !== collection) {
|
|
45
|
+
validatedCollection = collection;
|
|
46
|
+
if (warning)
|
|
47
|
+
console.warn(warning);
|
|
48
|
+
}
|
|
49
|
+
const currentController = createLiveQueryWindowController(collection, {
|
|
50
|
+
pageSize,
|
|
51
|
+
initialPageParam,
|
|
52
|
+
initialPageCount,
|
|
53
|
+
});
|
|
54
|
+
previousController = currentController;
|
|
55
|
+
return currentController;
|
|
56
|
+
}
|
|
57
|
+
const collection = createLiveQueryCollection({
|
|
58
|
+
query: input.query.limit(pageSize + 1).offset(0),
|
|
59
|
+
startSync: false,
|
|
60
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
61
|
+
});
|
|
62
|
+
assertLiveQueryWindowManyResult(collection);
|
|
63
|
+
const currentController = createLiveQueryWindowController(collection, {
|
|
64
|
+
pageSize,
|
|
65
|
+
initialPageParam,
|
|
66
|
+
initialPageCount,
|
|
67
|
+
});
|
|
68
|
+
previousController = currentController;
|
|
69
|
+
return currentController;
|
|
70
|
+
});
|
|
71
|
+
let snapshot = $state.raw(untrack(() => controller.getSnapshot()));
|
|
72
|
+
$effect(() => {
|
|
73
|
+
const currentController = controller;
|
|
74
|
+
snapshot = currentController.getSnapshot();
|
|
75
|
+
const unsubscribe = currentController.subscribe(() => {
|
|
76
|
+
snapshot = currentController.getSnapshot();
|
|
77
|
+
});
|
|
78
|
+
snapshot = currentController.getSnapshot();
|
|
79
|
+
return () => {
|
|
80
|
+
unsubscribe();
|
|
81
|
+
currentController.dispose();
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
const fetchNextPage = async () => {
|
|
85
|
+
// A dependency can invalidate the derived controller before Svelte runs the
|
|
86
|
+
// effect that subscribes it. Queue the imperative call until that handoff
|
|
87
|
+
// has completed so it cannot target an inactive controller.
|
|
88
|
+
await tick();
|
|
89
|
+
await fetchNextLiveQueryWindowPage(controller);
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
get state() {
|
|
93
|
+
return snapshot.state;
|
|
94
|
+
},
|
|
95
|
+
get data() {
|
|
96
|
+
return snapshot.data;
|
|
97
|
+
},
|
|
98
|
+
get collection() {
|
|
99
|
+
return snapshot.collection;
|
|
100
|
+
},
|
|
101
|
+
get status() {
|
|
102
|
+
return snapshot.status;
|
|
103
|
+
},
|
|
104
|
+
get isLoading() {
|
|
105
|
+
return snapshot.isLoading;
|
|
106
|
+
},
|
|
107
|
+
get isReady() {
|
|
108
|
+
return snapshot.isReady;
|
|
109
|
+
},
|
|
110
|
+
get isIdle() {
|
|
111
|
+
return snapshot.isIdle;
|
|
112
|
+
},
|
|
113
|
+
get isError() {
|
|
114
|
+
return snapshot.isError;
|
|
115
|
+
},
|
|
116
|
+
get isCleanedUp() {
|
|
117
|
+
return snapshot.isCleanedUp;
|
|
118
|
+
},
|
|
119
|
+
get pages() {
|
|
120
|
+
return snapshot.pages;
|
|
121
|
+
},
|
|
122
|
+
get pageParams() {
|
|
123
|
+
return snapshot.pageParams;
|
|
124
|
+
},
|
|
125
|
+
get hasNextPage() {
|
|
126
|
+
return snapshot.hasNextPage;
|
|
127
|
+
},
|
|
128
|
+
get isFetchingNextPage() {
|
|
129
|
+
return snapshot.isFetchingNextPage;
|
|
130
|
+
},
|
|
131
|
+
get error() {
|
|
132
|
+
return snapshot.error;
|
|
133
|
+
},
|
|
134
|
+
fetchNextPage,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
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, isCollection, isSingleResultCollection, } from '@tanstack/db';
|
|
5
|
+
import { BaseQueryBuilder, createLiveQueryCollection, createLiveQueryObserver, isCollection, isSingleResultCollection, } from '@tanstack/db';
|
|
6
6
|
function toValue(value) {
|
|
7
7
|
if (typeof value === `function`) {
|
|
8
8
|
return value();
|
|
@@ -89,11 +89,19 @@ export function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
89
89
|
internalData.push(...Array.from(currentCollection.values()));
|
|
90
90
|
});
|
|
91
91
|
};
|
|
92
|
-
//
|
|
93
|
-
|
|
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
|
+
};
|
|
94
99
|
// Watch for collection changes and subscribe to updates
|
|
95
100
|
$effect(() => {
|
|
96
101
|
const currentCollection = collection;
|
|
102
|
+
// Tear down any previous observer.
|
|
103
|
+
currentObserver?.dispose();
|
|
104
|
+
currentObserver = null;
|
|
97
105
|
// Handle null collection (disabled query)
|
|
98
106
|
if (!currentCollection) {
|
|
99
107
|
status = `disabled`;
|
|
@@ -101,68 +109,45 @@ export function useLiveQuery(configOrQueryOrCollection, deps = []) {
|
|
|
101
109
|
state.clear();
|
|
102
110
|
internalData = [];
|
|
103
111
|
});
|
|
104
|
-
if (currentUnsubscribe) {
|
|
105
|
-
currentUnsubscribe();
|
|
106
|
-
currentUnsubscribe = null;
|
|
107
|
-
}
|
|
108
112
|
return;
|
|
109
113
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Initialize state with current collection data
|
|
117
|
-
untrack(() => {
|
|
118
|
-
state.clear();
|
|
119
|
-
for (const [key, value] of currentCollection.entries()) {
|
|
120
|
-
state.set(key, value);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
// Initialize data array in correct order
|
|
124
|
-
syncDataFromCollection(currentCollection);
|
|
125
|
-
// Listen for the first ready event to catch status transitions
|
|
126
|
-
// that might not trigger change events (fixes async status transition bug)
|
|
127
|
-
currentCollection.onFirstReady(() => {
|
|
128
|
-
// Update status directly - Svelte's reactivity system handles the update automatically
|
|
129
|
-
// Note: We cannot use flushSync here as it's disallowed inside effects in async mode
|
|
130
|
-
status = currentCollection.status;
|
|
131
|
-
});
|
|
132
|
-
// Subscribe to collection changes with granular updates
|
|
133
|
-
const subscription = currentCollection.subscribeChanges((changes) => {
|
|
134
|
-
// Apply each change individually to the reactive state
|
|
114
|
+
const observer = createLiveQueryObserver(currentCollection);
|
|
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());
|
|
119
|
+
const unsubscribe = observer.subscribe((changes) => {
|
|
135
120
|
untrack(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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);
|
|
145
140
|
}
|
|
146
141
|
}
|
|
147
142
|
});
|
|
148
|
-
|
|
149
|
-
syncDataFromCollection(currentCollection);
|
|
150
|
-
// Update status state on every change
|
|
151
|
-
status = currentCollection.status;
|
|
152
|
-
}, {
|
|
153
|
-
includeInitialState: true,
|
|
143
|
+
syncFromObserver(observer, currentCollection);
|
|
154
144
|
});
|
|
155
|
-
|
|
156
|
-
// Preload collection data if not already started
|
|
157
|
-
if (currentCollection.status === `idle`) {
|
|
158
|
-
currentCollection.preload().catch(console.error);
|
|
159
|
-
}
|
|
145
|
+
syncFromObserver(observer, currentCollection);
|
|
160
146
|
// Cleanup when effect is invalidated
|
|
161
147
|
return () => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
148
|
+
unsubscribe();
|
|
149
|
+
observer.dispose();
|
|
150
|
+
currentObserver = null;
|
|
166
151
|
};
|
|
167
152
|
});
|
|
168
153
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/svelte-db",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.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.7.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"svelte": "^5.0.0"
|