@tanstack/db 0.5.3 → 0.5.4
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/sync.cjs +5 -0
- package/dist/cjs/collection/sync.cjs.map +1 -1
- 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/esm/collection/sync.js +5 -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/package.json +2 -2
- package/src/collection/sync.ts +10 -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
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LiteQueuer } from "@tanstack/pacer-lite/lite-queuer";
|
|
2
2
|
function queueStrategy(options) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
let processingChain = Promise.resolve();
|
|
4
|
+
const queuer = new LiteQueuer(
|
|
5
|
+
(fn) => {
|
|
6
|
+
processingChain = processingChain.then(async () => {
|
|
7
|
+
const transaction = fn();
|
|
8
|
+
await transaction.isPersisted.promise;
|
|
9
|
+
}).catch(() => {
|
|
10
|
+
});
|
|
7
11
|
},
|
|
8
12
|
{
|
|
9
|
-
|
|
10
|
-
// Process one at a time to ensure serialization
|
|
11
|
-
wait: options?.wait,
|
|
13
|
+
wait: options?.wait ?? 0,
|
|
12
14
|
maxSize: options?.maxSize,
|
|
13
15
|
addItemsTo: options?.addItemsTo ?? `back`,
|
|
14
16
|
// Default FIFO: add to back
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queueStrategy.js","sources":["../../../src/strategies/queueStrategy.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"queueStrategy.js","sources":["../../../src/strategies/queueStrategy.ts"],"sourcesContent":["import { LiteQueuer } from \"@tanstack/pacer-lite/lite-queuer\"\nimport type { QueueStrategy, QueueStrategyOptions } from \"./types\"\nimport type { Transaction } from \"../transactions\"\n\n/**\n * Creates a queue strategy that processes all mutations in order with proper serialization.\n *\n * Unlike other strategies that may drop executions, queue ensures every\n * mutation is processed sequentially. Each transaction commit completes before\n * the next one starts. Useful when data consistency is critical and\n * every operation must complete in order.\n *\n * @param options - Configuration for queue behavior (FIFO/LIFO, timing, size limits)\n * @returns A queue strategy instance\n *\n * @example\n * ```ts\n * // FIFO queue - process in order received\n * const mutate = usePacedMutations({\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: queueStrategy({\n * wait: 200,\n * addItemsTo: 'back',\n * getItemsFrom: 'front'\n * })\n * })\n * ```\n *\n * @example\n * ```ts\n * // LIFO queue - process most recent first\n * const mutate = usePacedMutations({\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: queueStrategy({\n * wait: 200,\n * addItemsTo: 'back',\n * getItemsFrom: 'back'\n * })\n * })\n * ```\n */\nexport function queueStrategy(options?: QueueStrategyOptions): QueueStrategy {\n // Manual promise chaining to ensure async serialization\n // LiteQueuer (unlike AsyncQueuer from @tanstack/pacer) lacks built-in async queue\n // primitives and concurrency control. We compensate by manually chaining promises\n // to ensure each transaction completes before the next one starts.\n let processingChain = Promise.resolve()\n\n const queuer = new LiteQueuer<() => Transaction>(\n (fn) => {\n // Chain each transaction to the previous one's completion\n processingChain = processingChain\n .then(async () => {\n const transaction = fn()\n // Wait for the transaction to be persisted before processing next item\n await transaction.isPersisted.promise\n })\n .catch(() => {\n // Errors are handled via transaction.isPersisted.promise and surfaced there.\n // This catch prevents unhandled promise rejections from breaking the chain,\n // ensuring subsequent transactions can still execute even if one fails.\n })\n },\n {\n wait: options?.wait ?? 0,\n maxSize: options?.maxSize,\n addItemsTo: options?.addItemsTo ?? `back`, // Default FIFO: add to back\n getItemsFrom: options?.getItemsFrom ?? `front`, // Default FIFO: get from front\n started: true, // Start processing immediately\n }\n )\n\n return {\n _type: `queue`,\n options,\n execute: <T extends object = Record<string, unknown>>(\n fn: () => Transaction<T>\n ) => {\n // Add the transaction-creating function to the queue\n queuer.addItem(fn as () => Transaction)\n },\n cleanup: () => {\n queuer.stop()\n queuer.clear()\n },\n }\n}\n"],"names":[],"mappings":";AA6CO,SAAS,cAAc,SAA+C;AAK3E,MAAI,kBAAkB,QAAQ,QAAA;AAE9B,QAAM,SAAS,IAAI;AAAA,IACjB,CAAC,OAAO;AAEN,wBAAkB,gBACf,KAAK,YAAY;AAChB,cAAM,cAAc,GAAA;AAEpB,cAAM,YAAY,YAAY;AAAA,MAChC,CAAC,EACA,MAAM,MAAM;AAAA,MAIb,CAAC;AAAA,IACL;AAAA,IACA;AAAA,MACE,MAAM,SAAS,QAAQ;AAAA,MACvB,SAAS,SAAS;AAAA,MAClB,YAAY,SAAS,cAAc;AAAA;AAAA,MACnC,cAAc,SAAS,gBAAgB;AAAA;AAAA,MACvC,SAAS;AAAA;AAAA,IAAA;AAAA,EACX;AAGF,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,SAAS,CACP,OACG;AAEH,aAAO,QAAQ,EAAuB;AAAA,IACxC;AAAA,IACA,SAAS,MAAM;AACb,aAAO,KAAA;AACP,aAAO,MAAA;AAAA,IACT;AAAA,EAAA;AAEJ;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LiteThrottler } from "@tanstack/pacer-lite/lite-throttler";
|
|
2
2
|
function throttleStrategy(options) {
|
|
3
|
-
const throttler = new
|
|
3
|
+
const throttler = new LiteThrottler(
|
|
4
4
|
(callback) => callback(),
|
|
5
5
|
options
|
|
6
6
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"throttleStrategy.js","sources":["../../../src/strategies/throttleStrategy.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"throttleStrategy.js","sources":["../../../src/strategies/throttleStrategy.ts"],"sourcesContent":["import { LiteThrottler } from \"@tanstack/pacer-lite/lite-throttler\"\nimport type { ThrottleStrategy, ThrottleStrategyOptions } from \"./types\"\nimport type { Transaction } from \"../transactions\"\n\n/**\n * Creates a throttle strategy that ensures transactions are evenly spaced\n * over time.\n *\n * Provides smooth, controlled execution patterns ideal for UI updates like\n * sliders, progress bars, or scroll handlers where you want consistent\n * execution timing.\n *\n * @param options - Configuration for throttle behavior\n * @returns A throttle strategy instance\n *\n * @example\n * ```ts\n * // Throttle slider updates to every 200ms\n * const mutate = usePacedMutations({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => { draft.value = volume })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n * ```\n *\n * @example\n * ```ts\n * // Throttle with leading and trailing execution\n * const mutate = usePacedMutations({\n * onMutate: (data) => {\n * collection.update(id, draft => { Object.assign(draft, data) })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: throttleStrategy({\n * wait: 500,\n * leading: true,\n * trailing: true\n * })\n * })\n * ```\n */\nexport function throttleStrategy(\n options: ThrottleStrategyOptions\n): ThrottleStrategy {\n const throttler = new LiteThrottler(\n (callback: () => Transaction) => callback(),\n options\n )\n\n return {\n _type: `throttle`,\n options,\n execute: <T extends object = Record<string, unknown>>(\n fn: () => Transaction<T>\n ) => {\n throttler.maybeExecute(fn as () => Transaction)\n },\n cleanup: () => {\n throttler.cancel()\n },\n }\n}\n"],"names":[],"mappings":";AA+CO,SAAS,iBACd,SACkB;AAClB,QAAM,YAAY,IAAI;AAAA,IACpB,CAAC,aAAgC,SAAA;AAAA,IACjC;AAAA,EAAA;AAGF,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,SAAS,CACP,OACG;AACH,gBAAU,aAAa,EAAuB;AAAA,IAChD;AAAA,IACA,SAAS,MAAM;AACb,gBAAU,OAAA;AAAA,IACZ;AAAA,EAAA;AAEJ;"}
|
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.4",
|
|
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": {
|
package/src/collection/sync.ts
CHANGED
|
@@ -231,6 +231,16 @@ export class CollectionSyncManager<
|
|
|
231
231
|
return this.preloadPromise
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Warn when calling preload on an on-demand collection
|
|
235
|
+
if (this.syncMode === `on-demand`) {
|
|
236
|
+
console.warn(
|
|
237
|
+
`${this.id ? `[${this.id}] ` : ``}Calling .preload() on a collection with syncMode "on-demand" is a no-op. ` +
|
|
238
|
+
`In on-demand mode, data is only loaded when queries request it. ` +
|
|
239
|
+
`Instead, create a live query and call .preload() on that to load the specific data you need. ` +
|
|
240
|
+
`See https://tanstack.com/blog/tanstack-db-0.5-query-driven-sync for more details.`
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
234
244
|
this.preloadPromise = new Promise<void>((resolve, reject) => {
|
|
235
245
|
if (this.lifecycle.status === `ready`) {
|
|
236
246
|
resolve()
|
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
|
)
|