@tanstack/svelte-db 0.2.1 → 0.3.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.
@@ -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: LiveQueryCollectionConfig<TContext>, deps?: Array<() => unknown>): UseLiveQueryReturn<GetResult<TContext>, InferResultType<TContext>>;
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 collection = $derived.by(() => {
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 unwrappedParam;
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.forEach((dep) => toValue(dep));
52
- // Ensure we always start sync for Svelte helpers
53
- if (typeof unwrappedParam === `function`) {
54
- // Check if query function returns null/undefined (disabled query)
55
- const queryBuilder = new BaseQueryBuilder();
56
- const result = unwrappedParam(queryBuilder);
57
- if (result === undefined || result === null) {
58
- // Disabled query - return null
59
- return null;
60
- }
61
- return createLiveQueryCollection({
62
- query: unwrappedParam,
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
- // A reactive getter (or param-driven query fn) can resolve to null/undefined
68
- // to mean "disabled". `toValue` above already called it, so guard here —
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(collection ? collection.status : `disabled`);
85
- // Helper to sync data array from collection in correct order
86
- const syncDataFromCollection = (currentCollection) => {
125
+ let status = $state(initialSnapshot.status);
126
+ const syncFromObserver = (observer, changes) => {
127
+ const snapshot = observer.getSnapshot();
128
+ status = snapshot.status;
87
129
  untrack(() => {
88
- internalData = [];
89
- internalData.push(...Array.from(currentCollection.values()));
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 currentCollection = collection;
102
- // Tear down any previous observer.
103
- currentObserver?.dispose();
104
- currentObserver = null;
105
- // Handle null collection (disabled query)
106
- if (!currentCollection) {
107
- status = `disabled`;
108
- untrack(() => {
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
- return;
163
+ syncFromObserver(currentObserver);
113
164
  }
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());
165
+ const observer = currentObserver;
119
166
  const unsubscribe = observer.subscribe((changes) => {
120
- untrack(() => {
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
- syncFromObserver(observer, currentCollection);
169
+ currentResolved.resumeDeferredCollections();
170
+ syncFromObserver(observer);
146
171
  // Cleanup when effect is invalidated
147
172
  return () => {
148
173
  unsubscribe();
149
- observer.dispose();
150
- currentObserver = null;
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.2.1",
3
+ "version": "0.3.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.7.1"
38
+ "@tanstack/db": "0.8.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "svelte": "^5.0.0"
@@ -10,7 +10,7 @@ description: >
10
10
  type: framework
11
11
  library: db
12
12
  framework: svelte
13
- library_version: '0.6.0'
13
+ library_version: '0.6.17'
14
14
  requires:
15
15
  - db-core
16
16
  sources: