@tanstack/svelte-db 0.1.94 → 0.2.0
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 +143 -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,143 @@
|
|
|
1
|
+
import { createLiveQueryCollection, createLiveQueryWindowController, isCollection, } from '@tanstack/db';
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
4
|
+
const DEFAULT_GC_TIME_MS = 1;
|
|
5
|
+
function hasSetWindow(collection) {
|
|
6
|
+
return typeof collection.utils.setWindow === `function`;
|
|
7
|
+
}
|
|
8
|
+
function normalizePageSize(pageSize) {
|
|
9
|
+
if (pageSize === undefined ||
|
|
10
|
+
!Number.isSafeInteger(pageSize) ||
|
|
11
|
+
pageSize <= 0) {
|
|
12
|
+
return DEFAULT_PAGE_SIZE;
|
|
13
|
+
}
|
|
14
|
+
return pageSize;
|
|
15
|
+
}
|
|
16
|
+
function resolveInput(input) {
|
|
17
|
+
if (isCollection(input)) {
|
|
18
|
+
return { kind: `collection`, collection: input };
|
|
19
|
+
}
|
|
20
|
+
if (typeof input !== `function`) {
|
|
21
|
+
throw new Error(`useLiveInfiniteQuery: First argument must be either a pre-created live query collection or a query function. ` +
|
|
22
|
+
`Received: ${typeof input}`);
|
|
23
|
+
}
|
|
24
|
+
// A zero-argument function can be a reactive collection getter. Query
|
|
25
|
+
// callbacks conventionally accept the builder, so avoid probing those.
|
|
26
|
+
if (input.length === 0) {
|
|
27
|
+
try {
|
|
28
|
+
const collection = input();
|
|
29
|
+
if (isCollection(collection)) {
|
|
30
|
+
return { kind: `collection`, collection };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Query callbacks that close over their builder can still have arity 0.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
kind: `query`,
|
|
39
|
+
query: input,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function useLiveInfiniteQuery(queryFnOrCollection, config, deps = []) {
|
|
43
|
+
let validatedCollection = null;
|
|
44
|
+
const pageSize = $derived(normalizePageSize(config.pageSize));
|
|
45
|
+
const initialPageParam = $derived(config.initialPageParam ?? 0);
|
|
46
|
+
const controller = $derived.by(() => {
|
|
47
|
+
for (const dependency of deps)
|
|
48
|
+
dependency();
|
|
49
|
+
const input = resolveInput(queryFnOrCollection);
|
|
50
|
+
if (input.kind === `collection`) {
|
|
51
|
+
const collection = input.collection;
|
|
52
|
+
if (!hasSetWindow(collection)) {
|
|
53
|
+
throw new Error(`useLiveInfiniteQuery: Pre-created live query collection must have an orderBy clause for infinite pagination to work. ` +
|
|
54
|
+
`Please add .orderBy() to your createLiveQueryCollection query.`);
|
|
55
|
+
}
|
|
56
|
+
if (validatedCollection !== collection) {
|
|
57
|
+
validatedCollection = collection;
|
|
58
|
+
const currentWindow = collection.utils.getWindow?.();
|
|
59
|
+
const expectedLimit = pageSize + 1;
|
|
60
|
+
if (currentWindow &&
|
|
61
|
+
(currentWindow.offset !== 0 || currentWindow.limit !== expectedLimit)) {
|
|
62
|
+
console.warn(`useLiveInfiniteQuery: Pre-created collection has window {offset: ${currentWindow.offset}, limit: ${currentWindow.limit}} ` +
|
|
63
|
+
`but the hook expects {offset: 0, limit: ${expectedLimit}}. Adjusting window now.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return createLiveQueryWindowController(collection, {
|
|
67
|
+
pageSize,
|
|
68
|
+
initialPageParam,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const collection = createLiveQueryCollection({
|
|
72
|
+
query: (q) => input
|
|
73
|
+
.query(q)
|
|
74
|
+
.limit(pageSize + 1)
|
|
75
|
+
.offset(0),
|
|
76
|
+
startSync: false,
|
|
77
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
78
|
+
});
|
|
79
|
+
return createLiveQueryWindowController(collection, {
|
|
80
|
+
pageSize,
|
|
81
|
+
initialPageParam,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
let snapshot = $state.raw(untrack(() => controller.getSnapshot()));
|
|
85
|
+
$effect(() => {
|
|
86
|
+
const currentController = controller;
|
|
87
|
+
snapshot = currentController.getSnapshot();
|
|
88
|
+
const unsubscribe = currentController.subscribe(() => {
|
|
89
|
+
snapshot = currentController.getSnapshot();
|
|
90
|
+
});
|
|
91
|
+
snapshot = currentController.getSnapshot();
|
|
92
|
+
return () => {
|
|
93
|
+
unsubscribe();
|
|
94
|
+
currentController.dispose();
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
const fetchNextPage = () => controller.fetchNextPage();
|
|
98
|
+
return {
|
|
99
|
+
get state() {
|
|
100
|
+
return snapshot.state;
|
|
101
|
+
},
|
|
102
|
+
get data() {
|
|
103
|
+
return snapshot.data;
|
|
104
|
+
},
|
|
105
|
+
get collection() {
|
|
106
|
+
return snapshot.collection;
|
|
107
|
+
},
|
|
108
|
+
get status() {
|
|
109
|
+
return snapshot.status;
|
|
110
|
+
},
|
|
111
|
+
get isLoading() {
|
|
112
|
+
return snapshot.isLoading;
|
|
113
|
+
},
|
|
114
|
+
get isReady() {
|
|
115
|
+
return snapshot.isReady;
|
|
116
|
+
},
|
|
117
|
+
get isIdle() {
|
|
118
|
+
return snapshot.isIdle;
|
|
119
|
+
},
|
|
120
|
+
get isError() {
|
|
121
|
+
return snapshot.isError;
|
|
122
|
+
},
|
|
123
|
+
get isCleanedUp() {
|
|
124
|
+
return snapshot.isCleanedUp;
|
|
125
|
+
},
|
|
126
|
+
get pages() {
|
|
127
|
+
return snapshot.pages;
|
|
128
|
+
},
|
|
129
|
+
get pageParams() {
|
|
130
|
+
return snapshot.pageParams;
|
|
131
|
+
},
|
|
132
|
+
get hasNextPage() {
|
|
133
|
+
return snapshot.hasNextPage;
|
|
134
|
+
},
|
|
135
|
+
get isFetchingNextPage() {
|
|
136
|
+
return snapshot.isFetchingNextPage;
|
|
137
|
+
},
|
|
138
|
+
get error() {
|
|
139
|
+
return snapshot.error;
|
|
140
|
+
},
|
|
141
|
+
fetchNextPage,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -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.
|
|
3
|
+
"version": "0.2.0",
|
|
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.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"svelte": "^5.0.0"
|