@tanstack/db 0.4.14 → 0.4.16
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/index.cjs +8 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/indexes/auto-index.cjs +17 -8
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/local-storage.cjs +21 -18
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +9 -0
- package/dist/cjs/paced-mutations.cjs +52 -0
- package/dist/cjs/paced-mutations.cjs.map +1 -0
- package/dist/cjs/paced-mutations.d.cts +81 -0
- package/dist/cjs/query/optimizer.cjs +17 -2
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/strategies/debounceStrategy.cjs +21 -0
- package/dist/cjs/strategies/debounceStrategy.cjs.map +1 -0
- package/dist/cjs/strategies/debounceStrategy.d.cts +22 -0
- package/dist/cjs/strategies/index.d.cts +4 -0
- package/dist/cjs/strategies/queueStrategy.cjs +33 -0
- package/dist/cjs/strategies/queueStrategy.cjs.map +1 -0
- package/dist/cjs/strategies/queueStrategy.d.cts +43 -0
- package/dist/cjs/strategies/throttleStrategy.cjs +21 -0
- package/dist/cjs/strategies/throttleStrategy.cjs.map +1 -0
- package/dist/cjs/strategies/throttleStrategy.d.cts +39 -0
- package/dist/cjs/strategies/types.d.cts +103 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +17 -8
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/local-storage.d.ts +9 -0
- package/dist/esm/local-storage.js +21 -18
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/paced-mutations.d.ts +81 -0
- package/dist/esm/paced-mutations.js +52 -0
- package/dist/esm/paced-mutations.js.map +1 -0
- package/dist/esm/query/optimizer.js +17 -2
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/strategies/debounceStrategy.d.ts +22 -0
- package/dist/esm/strategies/debounceStrategy.js +21 -0
- package/dist/esm/strategies/debounceStrategy.js.map +1 -0
- package/dist/esm/strategies/index.d.ts +4 -0
- package/dist/esm/strategies/queueStrategy.d.ts +43 -0
- package/dist/esm/strategies/queueStrategy.js +33 -0
- package/dist/esm/strategies/queueStrategy.js.map +1 -0
- package/dist/esm/strategies/throttleStrategy.d.ts +39 -0
- package/dist/esm/strategies/throttleStrategy.js +21 -0
- package/dist/esm/strategies/throttleStrategy.js.map +1 -0
- package/dist/esm/strategies/types.d.ts +103 -0
- package/package.json +4 -1
- package/src/index.ts +2 -0
- package/src/indexes/auto-index.ts +23 -10
- package/src/local-storage.ts +41 -17
- package/src/paced-mutations.ts +169 -0
- package/src/query/optimizer.ts +31 -4
- package/src/strategies/debounceStrategy.ts +45 -0
- package/src/strategies/index.ts +17 -0
- package/src/strategies/queueStrategy.ts +75 -0
- package/src/strategies/throttleStrategy.ts +62 -0
- package/src/strategies/types.ts +130 -0
|
@@ -44,14 +44,25 @@ export function ensureIndexForField<
|
|
|
44
44
|
|
|
45
45
|
// Create a new index for this field using the collection's createIndex method
|
|
46
46
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// Use the proxy-based approach to create the proper accessor for nested paths
|
|
48
|
+
collection.createIndex(
|
|
49
|
+
(row) => {
|
|
50
|
+
// Navigate through the field path
|
|
51
|
+
let current: any = row
|
|
52
|
+
for (const part of fieldPath) {
|
|
53
|
+
current = current[part]
|
|
54
|
+
}
|
|
55
|
+
return current
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: `auto:${fieldPath.join(`.`)}`,
|
|
59
|
+
indexType: BTreeIndex,
|
|
60
|
+
options: compareFn ? { compareFn, compareOptions } : {},
|
|
61
|
+
}
|
|
62
|
+
)
|
|
52
63
|
} catch (error) {
|
|
53
64
|
console.warn(
|
|
54
|
-
`${collection.id ? `[${collection.id}] ` : ``}Failed to create auto-index for field "${
|
|
65
|
+
`${collection.id ? `[${collection.id}] ` : ``}Failed to create auto-index for field path "${fieldPath.join(`.`)}":`,
|
|
55
66
|
error
|
|
56
67
|
)
|
|
57
68
|
}
|
|
@@ -108,7 +119,7 @@ function extractIndexableExpressions(
|
|
|
108
119
|
return
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
// Check if the first argument is a property reference
|
|
122
|
+
// Check if the first argument is a property reference
|
|
112
123
|
if (func.args.length < 1 || func.args[0].type !== `ref`) {
|
|
113
124
|
return
|
|
114
125
|
}
|
|
@@ -116,12 +127,14 @@ function extractIndexableExpressions(
|
|
|
116
127
|
const fieldRef = func.args[0]
|
|
117
128
|
const fieldPath = fieldRef.path
|
|
118
129
|
|
|
119
|
-
// Skip if
|
|
120
|
-
if (fieldPath.length
|
|
130
|
+
// Skip if the path is empty
|
|
131
|
+
if (fieldPath.length === 0) {
|
|
121
132
|
return
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
|
|
135
|
+
// For nested paths, use the full path joined with underscores as the field name
|
|
136
|
+
// For simple paths, use the first (and only) element
|
|
137
|
+
const fieldName = fieldPath.join(`_`)
|
|
125
138
|
results.push({ fieldName, fieldPath })
|
|
126
139
|
}
|
|
127
140
|
|
package/src/local-storage.ts
CHANGED
|
@@ -44,6 +44,11 @@ interface StoredItem<T> {
|
|
|
44
44
|
data: T
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export interface Parser {
|
|
48
|
+
parse: (data: string) => unknown
|
|
49
|
+
stringify: (data: unknown) => string
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
/**
|
|
48
53
|
* Configuration interface for localStorage collection options
|
|
49
54
|
* @template T - The type of items in the collection
|
|
@@ -71,6 +76,12 @@ export interface LocalStorageCollectionConfig<
|
|
|
71
76
|
* Can be any object that implements addEventListener/removeEventListener for storage events
|
|
72
77
|
*/
|
|
73
78
|
storageEventApi?: StorageEventApi
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parser to use for serializing and deserializing data to and from storage
|
|
82
|
+
* Defaults to JSON
|
|
83
|
+
*/
|
|
84
|
+
parser?: Parser
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
/**
|
|
@@ -113,13 +124,18 @@ export interface LocalStorageCollectionUtils extends UtilsRecord {
|
|
|
113
124
|
|
|
114
125
|
/**
|
|
115
126
|
* Validates that a value can be JSON serialized
|
|
127
|
+
* @param parser - The parser to use for serialization
|
|
116
128
|
* @param value - The value to validate for JSON serialization
|
|
117
129
|
* @param operation - The operation type being performed (for error messages)
|
|
118
130
|
* @throws Error if the value cannot be JSON serialized
|
|
119
131
|
*/
|
|
120
|
-
function validateJsonSerializable(
|
|
132
|
+
function validateJsonSerializable(
|
|
133
|
+
parser: Parser,
|
|
134
|
+
value: any,
|
|
135
|
+
operation: string
|
|
136
|
+
): void {
|
|
121
137
|
try {
|
|
122
|
-
|
|
138
|
+
parser.stringify(value)
|
|
123
139
|
} catch (error) {
|
|
124
140
|
throw new SerializationError(
|
|
125
141
|
operation,
|
|
@@ -314,6 +330,9 @@ export function localStorageCollectionOptions(
|
|
|
314
330
|
(typeof window !== `undefined` ? window : null) ||
|
|
315
331
|
createNoOpStorageEventApi()
|
|
316
332
|
|
|
333
|
+
// Default to JSON parser if no parser is provided
|
|
334
|
+
const parser = config.parser || JSON
|
|
335
|
+
|
|
317
336
|
// Track the last known state to detect changes
|
|
318
337
|
const lastKnownData = new Map<string | number, StoredItem<any>>()
|
|
319
338
|
|
|
@@ -322,6 +341,7 @@ export function localStorageCollectionOptions(
|
|
|
322
341
|
config.storageKey,
|
|
323
342
|
storage,
|
|
324
343
|
storageEventApi,
|
|
344
|
+
parser,
|
|
325
345
|
config.getKey,
|
|
326
346
|
lastKnownData
|
|
327
347
|
)
|
|
@@ -349,7 +369,7 @@ export function localStorageCollectionOptions(
|
|
|
349
369
|
dataMap.forEach((storedItem, key) => {
|
|
350
370
|
objectData[String(key)] = storedItem
|
|
351
371
|
})
|
|
352
|
-
const serialized =
|
|
372
|
+
const serialized = parser.stringify(objectData)
|
|
353
373
|
storage.setItem(config.storageKey, serialized)
|
|
354
374
|
} catch (error) {
|
|
355
375
|
console.error(
|
|
@@ -383,7 +403,7 @@ export function localStorageCollectionOptions(
|
|
|
383
403
|
const wrappedOnInsert = async (params: InsertMutationFnParams<any>) => {
|
|
384
404
|
// Validate that all values in the transaction can be JSON serialized
|
|
385
405
|
params.transaction.mutations.forEach((mutation) => {
|
|
386
|
-
validateJsonSerializable(mutation.modified, `insert`)
|
|
406
|
+
validateJsonSerializable(parser, mutation.modified, `insert`)
|
|
387
407
|
})
|
|
388
408
|
|
|
389
409
|
// Call the user handler BEFORE persisting changes (if provided)
|
|
@@ -394,7 +414,7 @@ export function localStorageCollectionOptions(
|
|
|
394
414
|
|
|
395
415
|
// Always persist to storage
|
|
396
416
|
// Load current data from storage
|
|
397
|
-
const currentData = loadFromStorage<any>(config.storageKey, storage)
|
|
417
|
+
const currentData = loadFromStorage<any>(config.storageKey, storage, parser)
|
|
398
418
|
|
|
399
419
|
// Add new items with version keys
|
|
400
420
|
params.transaction.mutations.forEach((mutation) => {
|
|
@@ -418,7 +438,7 @@ export function localStorageCollectionOptions(
|
|
|
418
438
|
const wrappedOnUpdate = async (params: UpdateMutationFnParams<any>) => {
|
|
419
439
|
// Validate that all values in the transaction can be JSON serialized
|
|
420
440
|
params.transaction.mutations.forEach((mutation) => {
|
|
421
|
-
validateJsonSerializable(mutation.modified, `update`)
|
|
441
|
+
validateJsonSerializable(parser, mutation.modified, `update`)
|
|
422
442
|
})
|
|
423
443
|
|
|
424
444
|
// Call the user handler BEFORE persisting changes (if provided)
|
|
@@ -429,7 +449,7 @@ export function localStorageCollectionOptions(
|
|
|
429
449
|
|
|
430
450
|
// Always persist to storage
|
|
431
451
|
// Load current data from storage
|
|
432
|
-
const currentData = loadFromStorage<any>(config.storageKey, storage)
|
|
452
|
+
const currentData = loadFromStorage<any>(config.storageKey, storage, parser)
|
|
433
453
|
|
|
434
454
|
// Update items with new version keys
|
|
435
455
|
params.transaction.mutations.forEach((mutation) => {
|
|
@@ -459,7 +479,7 @@ export function localStorageCollectionOptions(
|
|
|
459
479
|
|
|
460
480
|
// Always persist to storage
|
|
461
481
|
// Load current data from storage
|
|
462
|
-
const currentData = loadFromStorage<any>(config.storageKey, storage)
|
|
482
|
+
const currentData = loadFromStorage<any>(config.storageKey, storage, parser)
|
|
463
483
|
|
|
464
484
|
// Remove items
|
|
465
485
|
params.transaction.mutations.forEach((mutation) => {
|
|
@@ -518,10 +538,10 @@ export function localStorageCollectionOptions(
|
|
|
518
538
|
switch (mutation.type) {
|
|
519
539
|
case `insert`:
|
|
520
540
|
case `update`:
|
|
521
|
-
validateJsonSerializable(mutation.modified, mutation.type)
|
|
541
|
+
validateJsonSerializable(parser, mutation.modified, mutation.type)
|
|
522
542
|
break
|
|
523
543
|
case `delete`:
|
|
524
|
-
validateJsonSerializable(mutation.original, mutation.type)
|
|
544
|
+
validateJsonSerializable(parser, mutation.original, mutation.type)
|
|
525
545
|
break
|
|
526
546
|
}
|
|
527
547
|
}
|
|
@@ -529,7 +549,8 @@ export function localStorageCollectionOptions(
|
|
|
529
549
|
// Load current data from storage
|
|
530
550
|
const currentData = loadFromStorage<Record<string, unknown>>(
|
|
531
551
|
config.storageKey,
|
|
532
|
-
storage
|
|
552
|
+
storage,
|
|
553
|
+
parser
|
|
533
554
|
)
|
|
534
555
|
|
|
535
556
|
// Apply each mutation
|
|
@@ -579,13 +600,15 @@ export function localStorageCollectionOptions(
|
|
|
579
600
|
|
|
580
601
|
/**
|
|
581
602
|
* Load data from storage and return as a Map
|
|
603
|
+
* @param parser - The parser to use for deserializing the data
|
|
582
604
|
* @param storageKey - The key used to store data in the storage API
|
|
583
605
|
* @param storage - The storage API to load from (localStorage, sessionStorage, etc.)
|
|
584
606
|
* @returns Map of stored items with version tracking, or empty Map if loading fails
|
|
585
607
|
*/
|
|
586
608
|
function loadFromStorage<T extends object>(
|
|
587
609
|
storageKey: string,
|
|
588
|
-
storage: StorageApi
|
|
610
|
+
storage: StorageApi,
|
|
611
|
+
parser: Parser
|
|
589
612
|
): Map<string | number, StoredItem<T>> {
|
|
590
613
|
try {
|
|
591
614
|
const rawData = storage.getItem(storageKey)
|
|
@@ -593,7 +616,7 @@ function loadFromStorage<T extends object>(
|
|
|
593
616
|
return new Map()
|
|
594
617
|
}
|
|
595
618
|
|
|
596
|
-
const parsed =
|
|
619
|
+
const parsed = parser.parse(rawData)
|
|
597
620
|
const dataMap = new Map<string | number, StoredItem<T>>()
|
|
598
621
|
|
|
599
622
|
// Handle object format where keys map to StoredItem values
|
|
@@ -644,6 +667,7 @@ function createLocalStorageSync<T extends object>(
|
|
|
644
667
|
storageKey: string,
|
|
645
668
|
storage: StorageApi,
|
|
646
669
|
storageEventApi: StorageEventApi,
|
|
670
|
+
parser: Parser,
|
|
647
671
|
_getKey: (item: T) => string | number,
|
|
648
672
|
lastKnownData: Map<string | number, StoredItem<T>>
|
|
649
673
|
): SyncConfig<T> & {
|
|
@@ -704,7 +728,7 @@ function createLocalStorageSync<T extends object>(
|
|
|
704
728
|
const { begin, write, commit } = syncParams
|
|
705
729
|
|
|
706
730
|
// Load the new data
|
|
707
|
-
const newData = loadFromStorage<T>(storageKey, storage)
|
|
731
|
+
const newData = loadFromStorage<T>(storageKey, storage, parser)
|
|
708
732
|
|
|
709
733
|
// Find the specific changes
|
|
710
734
|
const changes = findChanges(lastKnownData, newData)
|
|
@@ -713,7 +737,7 @@ function createLocalStorageSync<T extends object>(
|
|
|
713
737
|
begin()
|
|
714
738
|
changes.forEach(({ type, value }) => {
|
|
715
739
|
if (value) {
|
|
716
|
-
validateJsonSerializable(value, type)
|
|
740
|
+
validateJsonSerializable(parser, value, type)
|
|
717
741
|
write({ type, value })
|
|
718
742
|
}
|
|
719
743
|
})
|
|
@@ -739,11 +763,11 @@ function createLocalStorageSync<T extends object>(
|
|
|
739
763
|
collection = params.collection
|
|
740
764
|
|
|
741
765
|
// Initial load
|
|
742
|
-
const initialData = loadFromStorage<T>(storageKey, storage)
|
|
766
|
+
const initialData = loadFromStorage<T>(storageKey, storage, parser)
|
|
743
767
|
if (initialData.size > 0) {
|
|
744
768
|
begin()
|
|
745
769
|
initialData.forEach((storedItem) => {
|
|
746
|
-
validateJsonSerializable(storedItem.data, `load`)
|
|
770
|
+
validateJsonSerializable(parser, storedItem.data, `load`)
|
|
747
771
|
write({ type: `insert`, value: storedItem.data })
|
|
748
772
|
})
|
|
749
773
|
commit()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createTransaction } from "./transactions"
|
|
2
|
+
import type { MutationFn, Transaction } from "./types"
|
|
3
|
+
import type { Strategy } from "./strategies/types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for creating a paced mutations manager
|
|
7
|
+
*/
|
|
8
|
+
export interface PacedMutationsConfig<
|
|
9
|
+
TVariables = unknown,
|
|
10
|
+
T extends object = Record<string, unknown>,
|
|
11
|
+
> {
|
|
12
|
+
/**
|
|
13
|
+
* Callback to apply optimistic updates immediately.
|
|
14
|
+
* Receives the variables passed to the mutate function.
|
|
15
|
+
*/
|
|
16
|
+
onMutate: (variables: TVariables) => void
|
|
17
|
+
/**
|
|
18
|
+
* Function to execute the mutation on the server.
|
|
19
|
+
* Receives the transaction parameters containing all merged mutations.
|
|
20
|
+
*/
|
|
21
|
+
mutationFn: MutationFn<T>
|
|
22
|
+
/**
|
|
23
|
+
* Strategy for controlling mutation execution timing
|
|
24
|
+
* Examples: debounceStrategy, queueStrategy, throttleStrategy
|
|
25
|
+
*/
|
|
26
|
+
strategy: Strategy
|
|
27
|
+
/**
|
|
28
|
+
* Custom metadata to associate with transactions
|
|
29
|
+
*/
|
|
30
|
+
metadata?: Record<string, unknown>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a paced mutations manager with pluggable timing strategies.
|
|
35
|
+
*
|
|
36
|
+
* This function provides a way to control when and how optimistic mutations
|
|
37
|
+
* are persisted to the backend, using strategies like debouncing, queuing,
|
|
38
|
+
* or throttling. The optimistic updates are applied immediately via `onMutate`,
|
|
39
|
+
* and the actual persistence is controlled by the strategy.
|
|
40
|
+
*
|
|
41
|
+
* The returned function accepts variables of type TVariables and returns a
|
|
42
|
+
* Transaction object that can be awaited to know when persistence completes
|
|
43
|
+
* or to handle errors.
|
|
44
|
+
*
|
|
45
|
+
* @param config - Configuration including onMutate, mutationFn and strategy
|
|
46
|
+
* @returns A function that accepts variables and returns a Transaction
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* // Debounced mutations for auto-save
|
|
51
|
+
* const updateTodo = createPacedMutations<string>({
|
|
52
|
+
* onMutate: (text) => {
|
|
53
|
+
* // Apply optimistic update immediately
|
|
54
|
+
* collection.update(id, draft => { draft.text = text })
|
|
55
|
+
* },
|
|
56
|
+
* mutationFn: async (text, { transaction }) => {
|
|
57
|
+
* await api.save(transaction.mutations)
|
|
58
|
+
* },
|
|
59
|
+
* strategy: debounceStrategy({ wait: 500 })
|
|
60
|
+
* })
|
|
61
|
+
*
|
|
62
|
+
* // Call with variables, returns a transaction
|
|
63
|
+
* const tx = updateTodo('New text')
|
|
64
|
+
*
|
|
65
|
+
* // Await persistence or handle errors
|
|
66
|
+
* await tx.isPersisted.promise
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* // Queue strategy for sequential processing
|
|
72
|
+
* const addTodo = createPacedMutations<{ text: string }>({
|
|
73
|
+
* onMutate: ({ text }) => {
|
|
74
|
+
* collection.insert({ id: uuid(), text, completed: false })
|
|
75
|
+
* },
|
|
76
|
+
* mutationFn: async ({ text }, { transaction }) => {
|
|
77
|
+
* await api.save(transaction.mutations)
|
|
78
|
+
* },
|
|
79
|
+
* strategy: queueStrategy({
|
|
80
|
+
* wait: 200,
|
|
81
|
+
* addItemsTo: 'back',
|
|
82
|
+
* getItemsFrom: 'front'
|
|
83
|
+
* })
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function createPacedMutations<
|
|
88
|
+
TVariables = unknown,
|
|
89
|
+
T extends object = Record<string, unknown>,
|
|
90
|
+
>(
|
|
91
|
+
config: PacedMutationsConfig<TVariables, T>
|
|
92
|
+
): (variables: TVariables) => Transaction<T> {
|
|
93
|
+
const { onMutate, mutationFn, strategy, ...transactionConfig } = config
|
|
94
|
+
|
|
95
|
+
// The currently active transaction (pending, not yet persisting)
|
|
96
|
+
let activeTransaction: Transaction<T> | null = null
|
|
97
|
+
|
|
98
|
+
// Commit callback that the strategy will call when it's time to persist
|
|
99
|
+
const commitCallback = () => {
|
|
100
|
+
if (!activeTransaction) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Strategy callback called but no active transaction exists. This indicates a bug in the strategy implementation.`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (activeTransaction.state !== `pending`) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Strategy callback called but active transaction is in state "${activeTransaction.state}". Expected "pending".`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const txToCommit = activeTransaction
|
|
113
|
+
|
|
114
|
+
// Clear active transaction reference before committing
|
|
115
|
+
activeTransaction = null
|
|
116
|
+
|
|
117
|
+
// Commit the transaction
|
|
118
|
+
txToCommit.commit().catch(() => {
|
|
119
|
+
// Errors are handled via transaction.isPersisted.promise
|
|
120
|
+
// This catch prevents unhandled promise rejections
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return txToCommit
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Executes a mutation with the given variables. Creates a new transaction if none is active,
|
|
128
|
+
* or adds to the existing active transaction. The strategy controls when
|
|
129
|
+
* the transaction is actually committed.
|
|
130
|
+
*/
|
|
131
|
+
function mutate(variables: TVariables): Transaction<T> {
|
|
132
|
+
// Create a new transaction if we don't have an active one
|
|
133
|
+
if (!activeTransaction || activeTransaction.state !== `pending`) {
|
|
134
|
+
activeTransaction = createTransaction<T>({
|
|
135
|
+
...transactionConfig,
|
|
136
|
+
mutationFn,
|
|
137
|
+
autoCommit: false,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Execute onMutate with variables to apply optimistic updates
|
|
142
|
+
activeTransaction.mutate(() => {
|
|
143
|
+
onMutate(variables)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Save reference before calling strategy.execute
|
|
147
|
+
const txToReturn = activeTransaction
|
|
148
|
+
|
|
149
|
+
// For queue strategy, pass a function that commits the captured transaction
|
|
150
|
+
// This prevents the error when commitCallback tries to access the cleared activeTransaction
|
|
151
|
+
if (strategy._type === `queue`) {
|
|
152
|
+
const capturedTx = activeTransaction
|
|
153
|
+
activeTransaction = null // Clear so next mutation creates a new transaction
|
|
154
|
+
strategy.execute(() => {
|
|
155
|
+
capturedTx.commit().catch(() => {
|
|
156
|
+
// Errors are handled via transaction.isPersisted.promise
|
|
157
|
+
})
|
|
158
|
+
return capturedTx
|
|
159
|
+
})
|
|
160
|
+
} else {
|
|
161
|
+
// For debounce/throttle, use commitCallback which manages activeTransaction
|
|
162
|
+
strategy.execute(commitCallback)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return txToReturn
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return mutate
|
|
169
|
+
}
|
package/src/query/optimizer.ts
CHANGED
|
@@ -330,9 +330,22 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR {
|
|
|
330
330
|
return query
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
//
|
|
334
|
-
//
|
|
333
|
+
// For queries without joins, combine multiple WHERE clauses into a single clause
|
|
334
|
+
// to avoid creating multiple filter operators in the pipeline
|
|
335
335
|
if (!query.join || query.join.length === 0) {
|
|
336
|
+
// Only optimize if there are multiple WHERE clauses to combine
|
|
337
|
+
if (query.where.length > 1) {
|
|
338
|
+
// Combine multiple WHERE clauses into a single AND expression
|
|
339
|
+
const splitWhereClauses = splitAndClauses(query.where)
|
|
340
|
+
const combinedWhere = combineWithAnd(splitWhereClauses)
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
...query,
|
|
344
|
+
where: [combinedWhere],
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// For single WHERE clauses, no optimization needed
|
|
336
349
|
return query
|
|
337
350
|
}
|
|
338
351
|
|
|
@@ -674,6 +687,20 @@ function applyOptimizations(
|
|
|
674
687
|
// If optimized and no outer JOINs - don't keep (original behavior)
|
|
675
688
|
}
|
|
676
689
|
|
|
690
|
+
// Combine multiple remaining WHERE clauses into a single clause to avoid
|
|
691
|
+
// multiple filter operations in the pipeline (performance optimization)
|
|
692
|
+
// First flatten any nested AND expressions to avoid and(and(...), ...)
|
|
693
|
+
const finalWhere: Array<Where> =
|
|
694
|
+
remainingWhereClauses.length > 1
|
|
695
|
+
? [
|
|
696
|
+
combineWithAnd(
|
|
697
|
+
remainingWhereClauses.flatMap((clause) =>
|
|
698
|
+
splitAndClausesRecursive(getWhereExpression(clause))
|
|
699
|
+
)
|
|
700
|
+
),
|
|
701
|
+
]
|
|
702
|
+
: remainingWhereClauses
|
|
703
|
+
|
|
677
704
|
// Create a completely new query object to ensure immutability
|
|
678
705
|
const optimizedQuery: QueryIR = {
|
|
679
706
|
// Copy all non-optimized fields as-is
|
|
@@ -692,8 +719,8 @@ function applyOptimizations(
|
|
|
692
719
|
from: optimizedFrom,
|
|
693
720
|
join: optimizedJoins,
|
|
694
721
|
|
|
695
|
-
//
|
|
696
|
-
where:
|
|
722
|
+
// Include combined WHERE clauses
|
|
723
|
+
where: finalWhere.length > 0 ? finalWhere : [],
|
|
697
724
|
}
|
|
698
725
|
|
|
699
726
|
return optimizedQuery
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Debouncer } from "@tanstack/pacer/debouncer"
|
|
2
|
+
import type { DebounceStrategy, DebounceStrategyOptions } from "./types"
|
|
3
|
+
import type { Transaction } from "../transactions"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a debounce strategy that delays transaction execution until after
|
|
7
|
+
* a period of inactivity.
|
|
8
|
+
*
|
|
9
|
+
* Ideal for scenarios like search inputs or auto-save fields where you want
|
|
10
|
+
* to wait for the user to stop typing before persisting changes.
|
|
11
|
+
*
|
|
12
|
+
* @param options - Configuration for the debounce behavior
|
|
13
|
+
* @returns A debounce strategy instance
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const mutate = useSerializedTransaction({
|
|
18
|
+
* mutationFn: async ({ transaction }) => {
|
|
19
|
+
* await api.save(transaction.mutations)
|
|
20
|
+
* },
|
|
21
|
+
* strategy: debounceStrategy({ wait: 500 })
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function debounceStrategy(
|
|
26
|
+
options: DebounceStrategyOptions
|
|
27
|
+
): DebounceStrategy {
|
|
28
|
+
const debouncer = new Debouncer(
|
|
29
|
+
(callback: () => Transaction) => callback(),
|
|
30
|
+
options
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
_type: `debounce`,
|
|
35
|
+
options,
|
|
36
|
+
execute: <T extends object = Record<string, unknown>>(
|
|
37
|
+
fn: () => Transaction<T>
|
|
38
|
+
) => {
|
|
39
|
+
debouncer.maybeExecute(fn as () => Transaction)
|
|
40
|
+
},
|
|
41
|
+
cleanup: () => {
|
|
42
|
+
debouncer.cancel()
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Export all strategy factories
|
|
2
|
+
export { debounceStrategy } from "./debounceStrategy"
|
|
3
|
+
export { queueStrategy } from "./queueStrategy"
|
|
4
|
+
export { throttleStrategy } from "./throttleStrategy"
|
|
5
|
+
|
|
6
|
+
// Export strategy types
|
|
7
|
+
export type {
|
|
8
|
+
Strategy,
|
|
9
|
+
BaseStrategy,
|
|
10
|
+
DebounceStrategy,
|
|
11
|
+
DebounceStrategyOptions,
|
|
12
|
+
QueueStrategy,
|
|
13
|
+
QueueStrategyOptions,
|
|
14
|
+
ThrottleStrategy,
|
|
15
|
+
ThrottleStrategyOptions,
|
|
16
|
+
StrategyOptions,
|
|
17
|
+
} from "./types"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AsyncQueuer } from "@tanstack/pacer/async-queuer"
|
|
2
|
+
import type { QueueStrategy, QueueStrategyOptions } from "./types"
|
|
3
|
+
import type { Transaction } from "../transactions"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a queue strategy that processes all mutations in order with proper serialization.
|
|
7
|
+
*
|
|
8
|
+
* Unlike other strategies that may drop executions, queue ensures every
|
|
9
|
+
* mutation is processed sequentially. Each transaction commit completes before
|
|
10
|
+
* the next one starts. Useful when data consistency is critical and
|
|
11
|
+
* every operation must complete in order.
|
|
12
|
+
*
|
|
13
|
+
* @param options - Configuration for queue behavior (FIFO/LIFO, timing, size limits)
|
|
14
|
+
* @returns A queue strategy instance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // FIFO queue - process in order received
|
|
19
|
+
* const mutate = usePacedMutations({
|
|
20
|
+
* mutationFn: async ({ transaction }) => {
|
|
21
|
+
* await api.save(transaction.mutations)
|
|
22
|
+
* },
|
|
23
|
+
* strategy: queueStrategy({
|
|
24
|
+
* wait: 200,
|
|
25
|
+
* addItemsTo: 'back',
|
|
26
|
+
* getItemsFrom: 'front'
|
|
27
|
+
* })
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* // LIFO queue - process most recent first
|
|
34
|
+
* const mutate = usePacedMutations({
|
|
35
|
+
* mutationFn: async ({ transaction }) => {
|
|
36
|
+
* await api.save(transaction.mutations)
|
|
37
|
+
* },
|
|
38
|
+
* strategy: queueStrategy({
|
|
39
|
+
* wait: 200,
|
|
40
|
+
* addItemsTo: 'back',
|
|
41
|
+
* getItemsFrom: 'back'
|
|
42
|
+
* })
|
|
43
|
+
* })
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function queueStrategy(options?: QueueStrategyOptions): QueueStrategy {
|
|
47
|
+
const queuer = new AsyncQueuer<void>({
|
|
48
|
+
concurrency: 1, // Process one at a time to ensure serialization
|
|
49
|
+
wait: options?.wait,
|
|
50
|
+
maxSize: options?.maxSize,
|
|
51
|
+
addItemsTo: options?.addItemsTo ?? `back`, // Default FIFO: add to back
|
|
52
|
+
getItemsFrom: options?.getItemsFrom ?? `front`, // Default FIFO: get from front
|
|
53
|
+
started: true, // Start processing immediately
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
_type: `queue`,
|
|
58
|
+
options,
|
|
59
|
+
execute: <T extends object = Record<string, unknown>>(
|
|
60
|
+
fn: () => Transaction<T>
|
|
61
|
+
) => {
|
|
62
|
+
// Wrap the callback in an async function that waits for persistence
|
|
63
|
+
queuer.addItem(async () => {
|
|
64
|
+
const transaction = fn()
|
|
65
|
+
// Wait for the transaction to be persisted before processing next item
|
|
66
|
+
// Note: fn() already calls commit(), we just wait for it to complete
|
|
67
|
+
await transaction.isPersisted.promise
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
cleanup: () => {
|
|
71
|
+
queuer.stop()
|
|
72
|
+
queuer.clear()
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Throttler } from "@tanstack/pacer/throttler"
|
|
2
|
+
import type { ThrottleStrategy, ThrottleStrategyOptions } from "./types"
|
|
3
|
+
import type { Transaction } from "../transactions"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a throttle strategy that ensures transactions are evenly spaced
|
|
7
|
+
* over time.
|
|
8
|
+
*
|
|
9
|
+
* Provides smooth, controlled execution patterns ideal for UI updates like
|
|
10
|
+
* sliders, progress bars, or scroll handlers where you want consistent
|
|
11
|
+
* execution timing.
|
|
12
|
+
*
|
|
13
|
+
* @param options - Configuration for throttle behavior
|
|
14
|
+
* @returns A throttle strategy instance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Throttle slider updates to every 200ms
|
|
19
|
+
* const mutate = useSerializedTransaction({
|
|
20
|
+
* mutationFn: async ({ transaction }) => {
|
|
21
|
+
* await api.updateVolume(transaction.mutations)
|
|
22
|
+
* },
|
|
23
|
+
* strategy: throttleStrategy({ wait: 200 })
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // Throttle with leading and trailing execution
|
|
30
|
+
* const mutate = useSerializedTransaction({
|
|
31
|
+
* mutationFn: async ({ transaction }) => {
|
|
32
|
+
* await api.save(transaction.mutations)
|
|
33
|
+
* },
|
|
34
|
+
* strategy: throttleStrategy({
|
|
35
|
+
* wait: 500,
|
|
36
|
+
* leading: true,
|
|
37
|
+
* trailing: true
|
|
38
|
+
* })
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function throttleStrategy(
|
|
43
|
+
options: ThrottleStrategyOptions
|
|
44
|
+
): ThrottleStrategy {
|
|
45
|
+
const throttler = new Throttler(
|
|
46
|
+
(callback: () => Transaction) => callback(),
|
|
47
|
+
options
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
_type: `throttle`,
|
|
52
|
+
options,
|
|
53
|
+
execute: <T extends object = Record<string, unknown>>(
|
|
54
|
+
fn: () => Transaction<T>
|
|
55
|
+
) => {
|
|
56
|
+
throttler.maybeExecute(fn as () => Transaction)
|
|
57
|
+
},
|
|
58
|
+
cleanup: () => {
|
|
59
|
+
throttler.cancel()
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|