@tanstack/db 0.5.3 → 0.5.5
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 +10 -10
- package/dist/cjs/collection/subscription.cjs +17 -6
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +5 -0
- package/dist/cjs/collection/sync.cjs +16 -0
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +6 -0
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs +18 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/strategies/debounceStrategy.cjs +4 -4
- package/dist/cjs/strategies/debounceStrategy.cjs.map +1 -1
- package/dist/cjs/strategies/queueStrategy.cjs +10 -8
- package/dist/cjs/strategies/queueStrategy.cjs.map +1 -1
- package/dist/cjs/strategies/throttleStrategy.cjs +4 -4
- package/dist/cjs/strategies/throttleStrategy.cjs.map +1 -1
- package/dist/cjs/types.d.cts +2 -0
- package/dist/esm/collection/subscription.d.ts +5 -0
- package/dist/esm/collection/subscription.js +17 -6
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +6 -0
- package/dist/esm/collection/sync.js +16 -0
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/query/builder/index.js +19 -3
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/strategies/debounceStrategy.js +2 -2
- package/dist/esm/strategies/debounceStrategy.js.map +1 -1
- package/dist/esm/strategies/queueStrategy.js +10 -8
- package/dist/esm/strategies/queueStrategy.js.map +1 -1
- package/dist/esm/strategies/throttleStrategy.js +2 -2
- package/dist/esm/strategies/throttleStrategy.js.map +1 -1
- package/dist/esm/types.d.ts +2 -0
- package/package.json +2 -2
- package/src/collection/subscription.ts +32 -6
- package/src/collection/sync.ts +25 -0
- package/src/errors.ts +9 -0
- package/src/query/builder/index.ts +28 -2
- package/src/strategies/debounceStrategy.ts +2 -2
- package/src/strategies/queueStrategy.ts +22 -9
- package/src/strategies/throttleStrategy.ts +2 -2
- package/src/types.ts +3 -0
package/dist/esm/types.d.ts
CHANGED
|
@@ -198,10 +198,12 @@ export type LoadSubsetOptions = {
|
|
|
198
198
|
subscription?: Subscription;
|
|
199
199
|
};
|
|
200
200
|
export type LoadSubsetFn = (options: LoadSubsetOptions) => true | Promise<void>;
|
|
201
|
+
export type UnloadSubsetFn = (options: LoadSubsetOptions) => void;
|
|
201
202
|
export type CleanupFn = () => void;
|
|
202
203
|
export type SyncConfigRes = {
|
|
203
204
|
cleanup?: CleanupFn;
|
|
204
205
|
loadSubset?: LoadSubsetFn;
|
|
206
|
+
unloadSubset?: UnloadSubsetFn;
|
|
205
207
|
};
|
|
206
208
|
export interface SyncConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
|
|
207
209
|
sync: (params: {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
3
|
"description": "A reactive client store for building super fast apps on sync",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.5",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@standard-schema/spec": "^1.0.0",
|
|
7
|
-
"@tanstack/pacer": "^0.
|
|
7
|
+
"@tanstack/pacer-lite": "^0.1.0",
|
|
8
8
|
"@tanstack/db-ivm": "0.1.13"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
@@ -10,6 +10,7 @@ import type { BasicExpression, OrderBy } from "../query/ir.js"
|
|
|
10
10
|
import type { IndexInterface } from "../indexes/base-index.js"
|
|
11
11
|
import type {
|
|
12
12
|
ChangeMessage,
|
|
13
|
+
LoadSubsetOptions,
|
|
13
14
|
Subscription,
|
|
14
15
|
SubscriptionEvents,
|
|
15
16
|
SubscriptionStatus,
|
|
@@ -47,6 +48,12 @@ export class CollectionSubscription
|
|
|
47
48
|
// While `snapshotSent` is false we filter out all changes from subscription to the collection.
|
|
48
49
|
private snapshotSent = false
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Track all loadSubset calls made by this subscription so we can unload them on cleanup.
|
|
53
|
+
* We store the exact LoadSubsetOptions we passed to loadSubset to ensure symmetric unload.
|
|
54
|
+
*/
|
|
55
|
+
private loadedSubsets: Array<LoadSubsetOptions> = []
|
|
56
|
+
|
|
50
57
|
// Keep track of the keys we've sent (needed for join and orderBy optimizations)
|
|
51
58
|
private sentKeys = new Set<string | number>()
|
|
52
59
|
|
|
@@ -193,10 +200,14 @@ export class CollectionSubscription
|
|
|
193
200
|
|
|
194
201
|
// Request the sync layer to load more data
|
|
195
202
|
// don't await it, we will load the data into the collection when it comes in
|
|
196
|
-
const
|
|
203
|
+
const loadOptions: LoadSubsetOptions = {
|
|
197
204
|
where: stateOpts.where,
|
|
198
205
|
subscription: this,
|
|
199
|
-
}
|
|
206
|
+
}
|
|
207
|
+
const syncResult = this.collection._sync.loadSubset(loadOptions)
|
|
208
|
+
|
|
209
|
+
// Track this loadSubset call so we can unload it later
|
|
210
|
+
this.loadedSubsets.push(loadOptions)
|
|
200
211
|
|
|
201
212
|
const trackLoadSubsetPromise = opts?.trackLoadSubsetPromise ?? true
|
|
202
213
|
if (trackLoadSubsetPromise) {
|
|
@@ -333,12 +344,16 @@ export class CollectionSubscription
|
|
|
333
344
|
|
|
334
345
|
// Request the sync layer to load more data
|
|
335
346
|
// don't await it, we will load the data into the collection when it comes in
|
|
336
|
-
const
|
|
347
|
+
const loadOptions1: LoadSubsetOptions = {
|
|
337
348
|
where: whereWithValueFilter,
|
|
338
349
|
limit,
|
|
339
350
|
orderBy,
|
|
340
351
|
subscription: this,
|
|
341
|
-
}
|
|
352
|
+
}
|
|
353
|
+
const syncResult = this.collection._sync.loadSubset(loadOptions1)
|
|
354
|
+
|
|
355
|
+
// Track this loadSubset call
|
|
356
|
+
this.loadedSubsets.push(loadOptions1)
|
|
342
357
|
|
|
343
358
|
// Make parallel loadSubset calls for values equal to minValue and values greater than minValue
|
|
344
359
|
const promises: Array<Promise<void>> = []
|
|
@@ -348,10 +363,14 @@ export class CollectionSubscription
|
|
|
348
363
|
const { expression } = orderBy[0]!
|
|
349
364
|
const exactValueFilter = eq(expression, new Value(minValue))
|
|
350
365
|
|
|
351
|
-
const
|
|
366
|
+
const loadOptions2: LoadSubsetOptions = {
|
|
352
367
|
where: exactValueFilter,
|
|
353
368
|
subscription: this,
|
|
354
|
-
}
|
|
369
|
+
}
|
|
370
|
+
const equalValueResult = this.collection._sync.loadSubset(loadOptions2)
|
|
371
|
+
|
|
372
|
+
// Track this loadSubset call
|
|
373
|
+
this.loadedSubsets.push(loadOptions2)
|
|
355
374
|
|
|
356
375
|
if (equalValueResult instanceof Promise) {
|
|
357
376
|
promises.push(equalValueResult)
|
|
@@ -417,6 +436,13 @@ export class CollectionSubscription
|
|
|
417
436
|
}
|
|
418
437
|
|
|
419
438
|
unsubscribe() {
|
|
439
|
+
// Unload all subsets that this subscription loaded
|
|
440
|
+
// We pass the exact same LoadSubsetOptions we used for loadSubset
|
|
441
|
+
for (const options of this.loadedSubsets) {
|
|
442
|
+
this.collection._sync.unloadSubset(options)
|
|
443
|
+
}
|
|
444
|
+
this.loadedSubsets = []
|
|
445
|
+
|
|
420
446
|
this.emitInner(`unsubscribed`, {
|
|
421
447
|
type: `unsubscribed`,
|
|
422
448
|
subscription: this,
|
package/src/collection/sync.ts
CHANGED
|
@@ -43,6 +43,8 @@ export class CollectionSyncManager<
|
|
|
43
43
|
public syncLoadSubsetFn:
|
|
44
44
|
| ((options: LoadSubsetOptions) => true | Promise<void>)
|
|
45
45
|
| null = null
|
|
46
|
+
public syncUnloadSubsetFn: ((options: LoadSubsetOptions) => void) | null =
|
|
47
|
+
null
|
|
46
48
|
|
|
47
49
|
private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()
|
|
48
50
|
|
|
@@ -209,6 +211,9 @@ export class CollectionSyncManager<
|
|
|
209
211
|
// Store loadSubset function if provided
|
|
210
212
|
this.syncLoadSubsetFn = syncRes?.loadSubset ?? null
|
|
211
213
|
|
|
214
|
+
// Store unloadSubset function if provided
|
|
215
|
+
this.syncUnloadSubsetFn = syncRes?.unloadSubset ?? null
|
|
216
|
+
|
|
212
217
|
// Validate: on-demand mode requires a loadSubset function
|
|
213
218
|
if (this.syncMode === `on-demand` && !this.syncLoadSubsetFn) {
|
|
214
219
|
throw new CollectionConfigurationError(
|
|
@@ -231,6 +236,16 @@ export class CollectionSyncManager<
|
|
|
231
236
|
return this.preloadPromise
|
|
232
237
|
}
|
|
233
238
|
|
|
239
|
+
// Warn when calling preload on an on-demand collection
|
|
240
|
+
if (this.syncMode === `on-demand`) {
|
|
241
|
+
console.warn(
|
|
242
|
+
`${this.id ? `[${this.id}] ` : ``}Calling .preload() on a collection with syncMode "on-demand" is a no-op. ` +
|
|
243
|
+
`In on-demand mode, data is only loaded when queries request it. ` +
|
|
244
|
+
`Instead, create a live query and call .preload() on that to load the specific data you need. ` +
|
|
245
|
+
`See https://tanstack.com/blog/tanstack-db-0.5-query-driven-sync for more details.`
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
234
249
|
this.preloadPromise = new Promise<void>((resolve, reject) => {
|
|
235
250
|
if (this.lifecycle.status === `ready`) {
|
|
236
251
|
resolve()
|
|
@@ -331,6 +346,16 @@ export class CollectionSyncManager<
|
|
|
331
346
|
return true
|
|
332
347
|
}
|
|
333
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Notifies the sync layer that a subset is no longer needed.
|
|
351
|
+
* @param options Options that identify what data is being unloaded
|
|
352
|
+
*/
|
|
353
|
+
public unloadSubset(options: LoadSubsetOptions): void {
|
|
354
|
+
if (this.syncUnloadSubsetFn) {
|
|
355
|
+
this.syncUnloadSubsetFn(options)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
334
359
|
public cleanup(): void {
|
|
335
360
|
try {
|
|
336
361
|
if (this.syncCleanupFn) {
|
package/src/errors.ts
CHANGED
|
@@ -360,6 +360,15 @@ export class InvalidSourceError extends QueryBuilderError {
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
export class InvalidSourceTypeError extends QueryBuilderError {
|
|
364
|
+
constructor(context: string, type: string) {
|
|
365
|
+
super(
|
|
366
|
+
`Invalid source for ${context}: Expected an object with a single key-value pair like { alias: collection }. ` +
|
|
367
|
+
`For example: .from({ todos: todosCollection }). Got: ${type}`
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
363
372
|
export class JoinConditionMustBeEqualityError extends QueryBuilderError {
|
|
364
373
|
constructor() {
|
|
365
374
|
super(`Join condition must be an equality expression`)
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../ir.js"
|
|
11
11
|
import {
|
|
12
12
|
InvalidSourceError,
|
|
13
|
+
InvalidSourceTypeError,
|
|
13
14
|
JoinConditionMustBeEqualityError,
|
|
14
15
|
OnlyOneSourceAllowedError,
|
|
15
16
|
QueryMustHaveFromClauseError,
|
|
@@ -60,13 +61,38 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
|
|
|
60
61
|
source: TSource,
|
|
61
62
|
context: string
|
|
62
63
|
): [string, CollectionRef | QueryRef] {
|
|
63
|
-
|
|
64
|
+
// Validate source is a plain object (not null, array, string, etc.)
|
|
65
|
+
// We use try-catch to handle null/undefined gracefully
|
|
66
|
+
let keys: Array<string>
|
|
67
|
+
try {
|
|
68
|
+
keys = Object.keys(source)
|
|
69
|
+
} catch {
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
71
|
+
const type = source === null ? `null` : `undefined`
|
|
72
|
+
throw new InvalidSourceTypeError(context, type)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if it's an array (arrays pass Object.keys but aren't valid sources)
|
|
76
|
+
if (Array.isArray(source)) {
|
|
77
|
+
throw new InvalidSourceTypeError(context, `array`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate exactly one key
|
|
81
|
+
if (keys.length !== 1) {
|
|
82
|
+
if (keys.length === 0) {
|
|
83
|
+
throw new InvalidSourceTypeError(context, `empty object`)
|
|
84
|
+
}
|
|
85
|
+
// Check if it looks like a string was passed (has numeric keys)
|
|
86
|
+
if (keys.every((k) => !isNaN(Number(k)))) {
|
|
87
|
+
throw new InvalidSourceTypeError(context, `string`)
|
|
88
|
+
}
|
|
64
89
|
throw new OnlyOneSourceAllowedError(context)
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
const alias =
|
|
92
|
+
const alias = keys[0]!
|
|
68
93
|
const sourceValue = source[alias]
|
|
69
94
|
|
|
95
|
+
// Validate the value is a Collection or QueryBuilder
|
|
70
96
|
let ref: CollectionRef | QueryRef
|
|
71
97
|
|
|
72
98
|
if (sourceValue instanceof CollectionImpl) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LiteDebouncer } from "@tanstack/pacer-lite/lite-debouncer"
|
|
2
2
|
import type { DebounceStrategy, DebounceStrategyOptions } from "./types"
|
|
3
3
|
import type { Transaction } from "../transactions"
|
|
4
4
|
|
|
@@ -28,7 +28,7 @@ import type { Transaction } from "../transactions"
|
|
|
28
28
|
export function debounceStrategy(
|
|
29
29
|
options: DebounceStrategyOptions
|
|
30
30
|
): DebounceStrategy {
|
|
31
|
-
const debouncer = new
|
|
31
|
+
const debouncer = new LiteDebouncer(
|
|
32
32
|
(callback: () => Transaction) => callback(),
|
|
33
33
|
options
|
|
34
34
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LiteQueuer } from "@tanstack/pacer-lite/lite-queuer"
|
|
2
2
|
import type { QueueStrategy, QueueStrategyOptions } from "./types"
|
|
3
3
|
import type { Transaction } from "../transactions"
|
|
4
4
|
|
|
@@ -44,16 +44,29 @@ import type { Transaction } from "../transactions"
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
export function queueStrategy(options?: QueueStrategyOptions): QueueStrategy {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// Manual promise chaining to ensure async serialization
|
|
48
|
+
// LiteQueuer (unlike AsyncQueuer from @tanstack/pacer) lacks built-in async queue
|
|
49
|
+
// primitives and concurrency control. We compensate by manually chaining promises
|
|
50
|
+
// to ensure each transaction completes before the next one starts.
|
|
51
|
+
let processingChain = Promise.resolve()
|
|
52
|
+
|
|
53
|
+
const queuer = new LiteQueuer<() => Transaction>(
|
|
54
|
+
(fn) => {
|
|
55
|
+
// Chain each transaction to the previous one's completion
|
|
56
|
+
processingChain = processingChain
|
|
57
|
+
.then(async () => {
|
|
58
|
+
const transaction = fn()
|
|
59
|
+
// Wait for the transaction to be persisted before processing next item
|
|
60
|
+
await transaction.isPersisted.promise
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {
|
|
63
|
+
// Errors are handled via transaction.isPersisted.promise and surfaced there.
|
|
64
|
+
// This catch prevents unhandled promise rejections from breaking the chain,
|
|
65
|
+
// ensuring subsequent transactions can still execute even if one fails.
|
|
66
|
+
})
|
|
53
67
|
},
|
|
54
68
|
{
|
|
55
|
-
|
|
56
|
-
wait: options?.wait,
|
|
69
|
+
wait: options?.wait ?? 0,
|
|
57
70
|
maxSize: options?.maxSize,
|
|
58
71
|
addItemsTo: options?.addItemsTo ?? `back`, // Default FIFO: add to back
|
|
59
72
|
getItemsFrom: options?.getItemsFrom ?? `front`, // Default FIFO: get from front
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LiteThrottler } from "@tanstack/pacer-lite/lite-throttler"
|
|
2
2
|
import type { ThrottleStrategy, ThrottleStrategyOptions } from "./types"
|
|
3
3
|
import type { Transaction } from "../transactions"
|
|
4
4
|
|
|
@@ -48,7 +48,7 @@ import type { Transaction } from "../transactions"
|
|
|
48
48
|
export function throttleStrategy(
|
|
49
49
|
options: ThrottleStrategyOptions
|
|
50
50
|
): ThrottleStrategy {
|
|
51
|
-
const throttler = new
|
|
51
|
+
const throttler = new LiteThrottler(
|
|
52
52
|
(callback: () => Transaction) => callback(),
|
|
53
53
|
options
|
|
54
54
|
)
|
package/src/types.ts
CHANGED
|
@@ -273,11 +273,14 @@ export type LoadSubsetOptions = {
|
|
|
273
273
|
|
|
274
274
|
export type LoadSubsetFn = (options: LoadSubsetOptions) => true | Promise<void>
|
|
275
275
|
|
|
276
|
+
export type UnloadSubsetFn = (options: LoadSubsetOptions) => void
|
|
277
|
+
|
|
276
278
|
export type CleanupFn = () => void
|
|
277
279
|
|
|
278
280
|
export type SyncConfigRes = {
|
|
279
281
|
cleanup?: CleanupFn
|
|
280
282
|
loadSubset?: LoadSubsetFn
|
|
283
|
+
unloadSubset?: UnloadSubsetFn
|
|
281
284
|
}
|
|
282
285
|
export interface SyncConfig<
|
|
283
286
|
T extends object = Record<string, unknown>,
|