@tanstack/db 0.5.20 → 0.5.22
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/dist/cjs/collection/lifecycle.cjs +1 -1
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/state.cjs +9 -4
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +6 -0
- package/dist/cjs/collection/sync.cjs +5 -3
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/errors.cjs +5 -1
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +1 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +12 -23
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.d.cts +5 -10
- package/dist/cjs/query/live/collection-config-builder.cjs +2 -1
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/internal.cjs.map +1 -1
- package/dist/cjs/query/live/internal.d.cts +1 -0
- package/dist/cjs/strategies/queueStrategy.cjs.map +1 -1
- package/dist/cjs/strategies/queueStrategy.d.cts +8 -2
- package/dist/cjs/types.d.cts +8 -1
- package/dist/esm/collection/lifecycle.js +1 -1
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/state.d.ts +6 -0
- package/dist/esm/collection/state.js +9 -4
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/sync.js +5 -3
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +1 -0
- package/dist/esm/errors.js +5 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +1 -1
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/compiler/group-by.d.ts +5 -10
- package/dist/esm/query/compiler/group-by.js +12 -23
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.js +2 -1
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/internal.d.ts +1 -0
- package/dist/esm/query/live/internal.js.map +1 -1
- package/dist/esm/strategies/queueStrategy.d.ts +8 -2
- package/dist/esm/strategies/queueStrategy.js.map +1 -1
- package/dist/esm/types.d.ts +8 -1
- package/package.json +2 -2
- package/src/collection/lifecycle.ts +4 -2
- package/src/collection/state.ts +23 -2
- package/src/collection/sync.ts +3 -1
- package/src/errors.ts +19 -3
- package/src/query/builder/functions.ts +3 -3
- package/src/query/compiler/group-by.ts +27 -54
- package/src/query/live/collection-config-builder.ts +1 -0
- package/src/query/live/internal.ts +1 -0
- package/src/strategies/queueStrategy.ts +8 -2
- package/src/types.ts +6 -1
package/dist/esm/types.d.ts
CHANGED
|
@@ -247,7 +247,14 @@ export type SyncConfigRes = {
|
|
|
247
247
|
export interface SyncConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
|
|
248
248
|
sync: (params: {
|
|
249
249
|
collection: Collection<T, TKey, any, any, any>;
|
|
250
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Begin a new sync transaction.
|
|
252
|
+
* @param options.immediate - When true, the transaction will be processed immediately
|
|
253
|
+
* even if there are persisting user transactions. Used by manual write operations.
|
|
254
|
+
*/
|
|
255
|
+
begin: (options?: {
|
|
256
|
+
immediate?: boolean;
|
|
257
|
+
}) => void;
|
|
251
258
|
write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void;
|
|
252
259
|
commit: () => void;
|
|
253
260
|
markReady: () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.22",
|
|
4
4
|
"description": "A reactive client store for building super fast apps on sync",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@standard-schema/spec": "^1.1.0",
|
|
41
41
|
"@tanstack/pacer-lite": "^0.2.0",
|
|
42
|
-
"@tanstack/db-ivm": "0.1.
|
|
42
|
+
"@tanstack/db-ivm": "0.1.17"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"typescript": ">=4.7"
|
|
@@ -180,8 +180,10 @@ export class CollectionLifecycleManager<
|
|
|
180
180
|
|
|
181
181
|
const gcTime = this.config.gcTime ?? 300000 // 5 minutes default
|
|
182
182
|
|
|
183
|
-
// If gcTime is 0, GC is disabled
|
|
184
|
-
|
|
183
|
+
// If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled.
|
|
184
|
+
// Note: setTimeout with Infinity coerces to 0 via ToInt32, causing immediate GC,
|
|
185
|
+
// so we must explicitly check for non-finite values here.
|
|
186
|
+
if (gcTime <= 0 || !Number.isFinite(gcTime)) {
|
|
185
187
|
return
|
|
186
188
|
}
|
|
187
189
|
|
package/src/collection/state.ts
CHANGED
|
@@ -25,6 +25,12 @@ interface PendingSyncedTransaction<
|
|
|
25
25
|
upserts: Map<TKey, T>
|
|
26
26
|
deletes: Set<TKey>
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* When true, this transaction should be processed immediately even if there
|
|
30
|
+
* are persisting user transactions. Used by manual write operations (writeInsert,
|
|
31
|
+
* writeUpdate, writeDelete, writeUpsert) which need synchronous updates to syncedData.
|
|
32
|
+
*/
|
|
33
|
+
immediate?: boolean
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export class CollectionStateManager<
|
|
@@ -437,13 +443,17 @@ export class CollectionStateManager<
|
|
|
437
443
|
committedSyncedTransactions,
|
|
438
444
|
uncommittedSyncedTransactions,
|
|
439
445
|
hasTruncateSync,
|
|
446
|
+
hasImmediateSync,
|
|
440
447
|
} = this.pendingSyncedTransactions.reduce(
|
|
441
448
|
(acc, t) => {
|
|
442
449
|
if (t.committed) {
|
|
443
450
|
acc.committedSyncedTransactions.push(t)
|
|
444
|
-
if (t.truncate
|
|
451
|
+
if (t.truncate) {
|
|
445
452
|
acc.hasTruncateSync = true
|
|
446
453
|
}
|
|
454
|
+
if (t.immediate) {
|
|
455
|
+
acc.hasImmediateSync = true
|
|
456
|
+
}
|
|
447
457
|
} else {
|
|
448
458
|
acc.uncommittedSyncedTransactions.push(t)
|
|
449
459
|
}
|
|
@@ -457,10 +467,21 @@ export class CollectionStateManager<
|
|
|
457
467
|
PendingSyncedTransaction<TOutput, TKey>
|
|
458
468
|
>,
|
|
459
469
|
hasTruncateSync: false,
|
|
470
|
+
hasImmediateSync: false,
|
|
460
471
|
},
|
|
461
472
|
)
|
|
462
473
|
|
|
463
|
-
|
|
474
|
+
// Process committed transactions if:
|
|
475
|
+
// 1. No persisting user transaction (normal sync flow), OR
|
|
476
|
+
// 2. There's a truncate operation (must be processed immediately), OR
|
|
477
|
+
// 3. There's an immediate transaction (manual writes must be processed synchronously)
|
|
478
|
+
//
|
|
479
|
+
// Note: When hasImmediateSync or hasTruncateSync is true, we process ALL committed
|
|
480
|
+
// sync transactions (not just the immediate/truncate ones). This is intentional for
|
|
481
|
+
// ordering correctness: if we only processed the immediate transaction, earlier
|
|
482
|
+
// non-immediate transactions would be applied later and could overwrite newer state.
|
|
483
|
+
// Processing all committed transactions together preserves causal ordering.
|
|
484
|
+
if (!hasPersistingTransaction || hasTruncateSync || hasImmediateSync) {
|
|
464
485
|
// Set flag to prevent redundant optimistic state recalculations
|
|
465
486
|
this.isCommittingSyncTransactions = true
|
|
466
487
|
|
package/src/collection/sync.ts
CHANGED
|
@@ -88,11 +88,12 @@ export class CollectionSyncManager<
|
|
|
88
88
|
const syncRes = normalizeSyncFnResult(
|
|
89
89
|
this.config.sync.sync({
|
|
90
90
|
collection: this.collection,
|
|
91
|
-
begin: () => {
|
|
91
|
+
begin: (options?: { immediate?: boolean }) => {
|
|
92
92
|
this.state.pendingSyncedTransactions.push({
|
|
93
93
|
committed: false,
|
|
94
94
|
operations: [],
|
|
95
95
|
deletedKeys: new Set(),
|
|
96
|
+
immediate: options?.immediate,
|
|
96
97
|
})
|
|
97
98
|
},
|
|
98
99
|
write: (
|
|
@@ -149,6 +150,7 @@ export class CollectionSyncManager<
|
|
|
149
150
|
throw new DuplicateKeySyncError(key, this.id, {
|
|
150
151
|
hasCustomGetKey: internal?.hasCustomGetKey ?? false,
|
|
151
152
|
hasJoins: internal?.hasJoins ?? false,
|
|
153
|
+
hasDistinct: internal?.hasDistinct ?? false,
|
|
152
154
|
})
|
|
153
155
|
}
|
|
154
156
|
}
|
package/src/errors.ts
CHANGED
|
@@ -172,12 +172,28 @@ export class DuplicateKeySyncError extends CollectionOperationError {
|
|
|
172
172
|
constructor(
|
|
173
173
|
key: string | number,
|
|
174
174
|
collectionId: string,
|
|
175
|
-
options?: {
|
|
175
|
+
options?: {
|
|
176
|
+
hasCustomGetKey?: boolean
|
|
177
|
+
hasJoins?: boolean
|
|
178
|
+
hasDistinct?: boolean
|
|
179
|
+
},
|
|
176
180
|
) {
|
|
177
181
|
const baseMessage = `Cannot insert document with key "${key}" from sync because it already exists in the collection "${collectionId}"`
|
|
178
182
|
|
|
179
|
-
// Provide enhanced guidance when custom getKey is used with
|
|
180
|
-
if (options?.hasCustomGetKey && options.
|
|
183
|
+
// Provide enhanced guidance when custom getKey is used with distinct
|
|
184
|
+
if (options?.hasCustomGetKey && options.hasDistinct) {
|
|
185
|
+
super(
|
|
186
|
+
`${baseMessage}. ` +
|
|
187
|
+
`This collection uses a custom getKey with .distinct(). ` +
|
|
188
|
+
`The .distinct() operator deduplicates by the ENTIRE selected object (standard SQL behavior), ` +
|
|
189
|
+
`but your custom getKey extracts only a subset of fields. This causes multiple distinct rows ` +
|
|
190
|
+
`(with different values in non-key fields) to receive the same key. ` +
|
|
191
|
+
`To fix this, either: (1) ensure your SELECT only includes fields that uniquely identify each row, ` +
|
|
192
|
+
`(2) use .groupBy() with min()/max() aggregates to select one value per group, or ` +
|
|
193
|
+
`(3) remove the custom getKey to use the default key behavior.`,
|
|
194
|
+
)
|
|
195
|
+
} else if (options?.hasCustomGetKey && options.hasJoins) {
|
|
196
|
+
// Provide enhanced guidance when custom getKey is used with joins
|
|
181
197
|
super(
|
|
182
198
|
`${baseMessage}. ` +
|
|
183
199
|
`This collection uses a custom getKey with joined queries. ` +
|
|
@@ -53,10 +53,10 @@ type ExtractType<T> =
|
|
|
53
53
|
// Helper type to determine aggregate return type based on input nullability
|
|
54
54
|
type AggregateReturnType<T> =
|
|
55
55
|
ExtractType<T> extends infer U
|
|
56
|
-
? U extends number | undefined | null | Date | bigint
|
|
56
|
+
? U extends number | undefined | null | Date | bigint | string
|
|
57
57
|
? Aggregate<U>
|
|
58
|
-
: Aggregate<number | undefined | null | Date | bigint>
|
|
59
|
-
: Aggregate<number | undefined | null | Date | bigint>
|
|
58
|
+
: Aggregate<number | undefined | null | Date | bigint | string>
|
|
59
|
+
: Aggregate<number | undefined | null | Date | bigint | string>
|
|
60
60
|
|
|
61
61
|
// Helper type to determine string function return type based on input nullability
|
|
62
62
|
type StringFunctionReturnType<T> =
|
|
@@ -353,20 +353,28 @@ function getAggregateFunction(aggExpr: Aggregate) {
|
|
|
353
353
|
const valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {
|
|
354
354
|
const value = compiledExpr(namespacedRow)
|
|
355
355
|
// Ensure we return a number for numeric aggregate functions
|
|
356
|
-
|
|
356
|
+
if (typeof value === `number`) {
|
|
357
|
+
return value
|
|
358
|
+
}
|
|
359
|
+
return value != null ? Number(value) : 0
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
// Create a value extractor function for
|
|
360
|
-
const
|
|
362
|
+
// Create a value extractor function for min/max that preserves comparable types
|
|
363
|
+
const valueExtractorForMinMax = ([, namespacedRow]: [
|
|
361
364
|
string,
|
|
362
365
|
NamespacedRow,
|
|
363
366
|
]) => {
|
|
364
367
|
const value = compiledExpr(namespacedRow)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
// Preserve strings, numbers, Dates, and bigints for comparison
|
|
369
|
+
if (
|
|
370
|
+
typeof value === `number` ||
|
|
371
|
+
typeof value === `string` ||
|
|
372
|
+
typeof value === `bigint` ||
|
|
373
|
+
value instanceof Date
|
|
374
|
+
) {
|
|
375
|
+
return value
|
|
376
|
+
}
|
|
377
|
+
return value != null ? Number(value) : 0
|
|
370
378
|
}
|
|
371
379
|
|
|
372
380
|
// Create a raw value extractor function for the expression to aggregate
|
|
@@ -383,9 +391,9 @@ function getAggregateFunction(aggExpr: Aggregate) {
|
|
|
383
391
|
case `avg`:
|
|
384
392
|
return avg(valueExtractor)
|
|
385
393
|
case `min`:
|
|
386
|
-
return min(
|
|
394
|
+
return min(valueExtractorForMinMax)
|
|
387
395
|
case `max`:
|
|
388
|
-
return max(
|
|
396
|
+
return max(valueExtractorForMinMax)
|
|
389
397
|
default:
|
|
390
398
|
throw new UnsupportedAggregateFunctionError(aggExpr.name)
|
|
391
399
|
}
|
|
@@ -394,20 +402,15 @@ function getAggregateFunction(aggExpr: Aggregate) {
|
|
|
394
402
|
/**
|
|
395
403
|
* Transforms expressions to replace aggregate functions with references to computed values.
|
|
396
404
|
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* 2. SELECT field references via $selected namespace (e.g., `$selected.latestActivity`) - validates and passes through unchanged
|
|
400
|
-
*
|
|
401
|
-
* For aggregate expressions, it finds matching aggregates in the SELECT clause and replaces them with
|
|
402
|
-
* PropRef([resultAlias, alias]) to reference the computed aggregate value.
|
|
405
|
+
* For aggregate expressions, finds matching aggregates in the SELECT clause and replaces them
|
|
406
|
+
* with PropRef([resultAlias, alias]) to reference the computed aggregate value.
|
|
403
407
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* are passed through unchanged (treating them as table column references).
|
|
408
|
+
* Ref expressions (table columns and $selected fields) and value expressions are passed through unchanged.
|
|
409
|
+
* Function expressions are recursively transformed.
|
|
407
410
|
*
|
|
408
411
|
* @param havingExpr - The expression to transform (can be aggregate, ref, func, or val)
|
|
409
412
|
* @param selectClause - The SELECT clause containing aliases and aggregate definitions
|
|
410
|
-
* @param resultAlias - The namespace alias for SELECT results (default: '$selected'
|
|
413
|
+
* @param resultAlias - The namespace alias for SELECT results (default: '$selected')
|
|
411
414
|
* @returns A transformed BasicExpression that references computed values instead of raw expressions
|
|
412
415
|
*/
|
|
413
416
|
export function replaceAggregatesByRefs(
|
|
@@ -439,41 +442,11 @@ export function replaceAggregatesByRefs(
|
|
|
439
442
|
return new Func(funcExpr.name, transformedArgs)
|
|
440
443
|
}
|
|
441
444
|
|
|
442
|
-
case `ref`:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (path.length === 0) {
|
|
447
|
-
// Empty path - pass through
|
|
448
|
-
return havingExpr as BasicExpression
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Check if this is a $selected reference
|
|
452
|
-
if (path.length > 0 && path[0] === `$selected`) {
|
|
453
|
-
// Extract the field path after $selected
|
|
454
|
-
const fieldPath = path.slice(1)
|
|
455
|
-
|
|
456
|
-
if (fieldPath.length === 0) {
|
|
457
|
-
// Just $selected without a field - pass through unchanged
|
|
458
|
-
return havingExpr as BasicExpression
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Verify the field exists in SELECT clause
|
|
462
|
-
const alias = fieldPath.join(`.`)
|
|
463
|
-
if (alias in selectClause) {
|
|
464
|
-
// Pass through unchanged - $selected is already the correct namespace
|
|
465
|
-
return havingExpr as BasicExpression
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Field doesn't exist in SELECT - this is an error, but we'll pass through for now
|
|
469
|
-
// (Could throw an error here in the future)
|
|
470
|
-
return havingExpr as BasicExpression
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Not a $selected reference - this is a table column reference, pass through unchanged
|
|
474
|
-
// SELECT fields should only be accessed via $selected namespace
|
|
445
|
+
case `ref`:
|
|
446
|
+
// Ref expressions are passed through unchanged - they reference either:
|
|
447
|
+
// - $selected fields (which are already in the correct namespace)
|
|
448
|
+
// - Table column references (which remain valid)
|
|
475
449
|
return havingExpr as BasicExpression
|
|
476
|
-
}
|
|
477
450
|
|
|
478
451
|
case `val`:
|
|
479
452
|
// Return as-is
|
|
@@ -6,9 +6,15 @@ import type { Transaction } from '../transactions'
|
|
|
6
6
|
* Creates a queue strategy that processes all mutations in order with proper serialization.
|
|
7
7
|
*
|
|
8
8
|
* Unlike other strategies that may drop executions, queue ensures every
|
|
9
|
-
* mutation is
|
|
9
|
+
* mutation is attempted sequentially. Each transaction commit completes before
|
|
10
10
|
* the next one starts. Useful when data consistency is critical and
|
|
11
|
-
* every operation must
|
|
11
|
+
* every operation must be attempted in order.
|
|
12
|
+
*
|
|
13
|
+
* **Error handling behavior:**
|
|
14
|
+
* - If a mutation fails, it is NOT automatically retried - the transaction transitions to "failed" state
|
|
15
|
+
* - Failed mutations surface their error via `transaction.isPersisted.promise` (which will reject)
|
|
16
|
+
* - Subsequent mutations continue processing - a single failure does not block the queue
|
|
17
|
+
* - Each mutation is independent; there is no all-or-nothing transaction semantics
|
|
12
18
|
*
|
|
13
19
|
* @param options - Configuration for queue behavior (FIFO/LIFO, timing, size limits)
|
|
14
20
|
* @returns A queue strategy instance
|
package/src/types.ts
CHANGED
|
@@ -328,7 +328,12 @@ export interface SyncConfig<
|
|
|
328
328
|
> {
|
|
329
329
|
sync: (params: {
|
|
330
330
|
collection: Collection<T, TKey, any, any, any>
|
|
331
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Begin a new sync transaction.
|
|
333
|
+
* @param options.immediate - When true, the transaction will be processed immediately
|
|
334
|
+
* even if there are persisting user transactions. Used by manual write operations.
|
|
335
|
+
*/
|
|
336
|
+
begin: (options?: { immediate?: boolean }) => void
|
|
332
337
|
write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void
|
|
333
338
|
commit: () => void
|
|
334
339
|
markReady: () => void
|