@tanstack/db 0.4.15 → 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.
Files changed (52) hide show
  1. package/dist/cjs/index.cjs +8 -0
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +2 -0
  4. package/dist/cjs/indexes/auto-index.cjs +17 -8
  5. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  6. package/dist/cjs/paced-mutations.cjs +52 -0
  7. package/dist/cjs/paced-mutations.cjs.map +1 -0
  8. package/dist/cjs/paced-mutations.d.cts +81 -0
  9. package/dist/cjs/query/optimizer.cjs +17 -2
  10. package/dist/cjs/query/optimizer.cjs.map +1 -1
  11. package/dist/cjs/strategies/debounceStrategy.cjs +21 -0
  12. package/dist/cjs/strategies/debounceStrategy.cjs.map +1 -0
  13. package/dist/cjs/strategies/debounceStrategy.d.cts +22 -0
  14. package/dist/cjs/strategies/index.d.cts +4 -0
  15. package/dist/cjs/strategies/queueStrategy.cjs +33 -0
  16. package/dist/cjs/strategies/queueStrategy.cjs.map +1 -0
  17. package/dist/cjs/strategies/queueStrategy.d.cts +43 -0
  18. package/dist/cjs/strategies/throttleStrategy.cjs +21 -0
  19. package/dist/cjs/strategies/throttleStrategy.cjs.map +1 -0
  20. package/dist/cjs/strategies/throttleStrategy.d.cts +39 -0
  21. package/dist/cjs/strategies/types.d.cts +103 -0
  22. package/dist/esm/index.d.ts +2 -0
  23. package/dist/esm/index.js +8 -0
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/indexes/auto-index.js +17 -8
  26. package/dist/esm/indexes/auto-index.js.map +1 -1
  27. package/dist/esm/paced-mutations.d.ts +81 -0
  28. package/dist/esm/paced-mutations.js +52 -0
  29. package/dist/esm/paced-mutations.js.map +1 -0
  30. package/dist/esm/query/optimizer.js +17 -2
  31. package/dist/esm/query/optimizer.js.map +1 -1
  32. package/dist/esm/strategies/debounceStrategy.d.ts +22 -0
  33. package/dist/esm/strategies/debounceStrategy.js +21 -0
  34. package/dist/esm/strategies/debounceStrategy.js.map +1 -0
  35. package/dist/esm/strategies/index.d.ts +4 -0
  36. package/dist/esm/strategies/queueStrategy.d.ts +43 -0
  37. package/dist/esm/strategies/queueStrategy.js +33 -0
  38. package/dist/esm/strategies/queueStrategy.js.map +1 -0
  39. package/dist/esm/strategies/throttleStrategy.d.ts +39 -0
  40. package/dist/esm/strategies/throttleStrategy.js +21 -0
  41. package/dist/esm/strategies/throttleStrategy.js.map +1 -0
  42. package/dist/esm/strategies/types.d.ts +103 -0
  43. package/package.json +3 -1
  44. package/src/index.ts +2 -0
  45. package/src/indexes/auto-index.ts +23 -10
  46. package/src/paced-mutations.ts +169 -0
  47. package/src/query/optimizer.ts +31 -4
  48. package/src/strategies/debounceStrategy.ts +45 -0
  49. package/src/strategies/index.ts +17 -0
  50. package/src/strategies/queueStrategy.ts +75 -0
  51. package/src/strategies/throttleStrategy.ts +62 -0
  52. package/src/strategies/types.ts +130 -0
@@ -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
+ }
@@ -330,9 +330,22 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR {
330
330
  return query
331
331
  }
332
332
 
333
- // Skip optimization if there are no joins - predicate pushdown only benefits joins
334
- // Single-table queries don't benefit from this optimization
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
- // Only include WHERE clauses that weren't successfully optimized
696
- where: remainingWhereClauses.length > 0 ? remainingWhereClauses : [],
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
+ }
@@ -0,0 +1,130 @@
1
+ import type { Transaction } from "../transactions"
2
+
3
+ /**
4
+ * Base strategy interface that all strategy implementations must conform to
5
+ */
6
+ export interface BaseStrategy<TName extends string = string> {
7
+ /** Type discriminator for strategy identification */
8
+ _type: TName
9
+
10
+ /**
11
+ * Execute a function according to the strategy's timing rules
12
+ * @param fn - The function to execute
13
+ * @returns The result of the function execution (if applicable)
14
+ */
15
+ execute: <T extends object = Record<string, unknown>>(
16
+ fn: () => Transaction<T>
17
+ ) => void | Promise<void>
18
+
19
+ /**
20
+ * Clean up any resources held by the strategy
21
+ * Should be called when the strategy is no longer needed
22
+ */
23
+ cleanup: () => void
24
+ }
25
+
26
+ /**
27
+ * Options for debounce strategy
28
+ * Delays execution until after a period of inactivity
29
+ */
30
+ export interface DebounceStrategyOptions {
31
+ /** Wait time in milliseconds before execution */
32
+ wait: number
33
+ /** Execute immediately on the first call */
34
+ leading?: boolean
35
+ /** Execute after the wait period on the last call */
36
+ trailing?: boolean
37
+ }
38
+
39
+ /**
40
+ * Debounce strategy that delays execution until activity stops
41
+ */
42
+ export interface DebounceStrategy extends BaseStrategy<`debounce`> {
43
+ options: DebounceStrategyOptions
44
+ }
45
+
46
+ /**
47
+ * Options for queue strategy
48
+ * Processes all executions in order (FIFO/LIFO)
49
+ */
50
+ export interface QueueStrategyOptions {
51
+ /** Wait time between processing queue items (milliseconds) */
52
+ wait?: number
53
+ /** Maximum queue size (items are dropped if exceeded) */
54
+ maxSize?: number
55
+ /** Where to add new items in the queue */
56
+ addItemsTo?: `front` | `back`
57
+ /** Where to get items from when processing */
58
+ getItemsFrom?: `front` | `back`
59
+ }
60
+
61
+ /**
62
+ * Queue strategy that processes all executions in order
63
+ * FIFO: { addItemsTo: 'back', getItemsFrom: 'front' }
64
+ * LIFO: { addItemsTo: 'back', getItemsFrom: 'back' }
65
+ */
66
+ export interface QueueStrategy extends BaseStrategy<`queue`> {
67
+ options?: QueueStrategyOptions
68
+ }
69
+
70
+ /**
71
+ * Options for throttle strategy
72
+ * Ensures executions are evenly spaced over time
73
+ */
74
+ export interface ThrottleStrategyOptions {
75
+ /** Minimum wait time between executions (milliseconds) */
76
+ wait: number
77
+ /** Execute immediately on the first call */
78
+ leading?: boolean
79
+ /** Execute on the last call after wait period */
80
+ trailing?: boolean
81
+ }
82
+
83
+ /**
84
+ * Throttle strategy that spaces executions evenly over time
85
+ */
86
+ export interface ThrottleStrategy extends BaseStrategy<`throttle`> {
87
+ options: ThrottleStrategyOptions
88
+ }
89
+
90
+ /**
91
+ * Options for batch strategy
92
+ * Groups multiple executions together
93
+ */
94
+ export interface BatchStrategyOptions {
95
+ /** Maximum items per batch */
96
+ maxSize?: number
97
+ /** Maximum wait time before processing batch (milliseconds) */
98
+ wait?: number
99
+ /** Custom logic to determine when to execute batch */
100
+ getShouldExecute?: (items: Array<any>) => boolean
101
+ }
102
+
103
+ /**
104
+ * Batch strategy that groups multiple executions together
105
+ */
106
+ export interface BatchStrategy extends BaseStrategy<`batch`> {
107
+ options?: BatchStrategyOptions
108
+ }
109
+
110
+ /**
111
+ * Union type of all available strategies
112
+ */
113
+ export type Strategy =
114
+ | DebounceStrategy
115
+ | QueueStrategy
116
+ | ThrottleStrategy
117
+ | BatchStrategy
118
+
119
+ /**
120
+ * Extract the options type from a strategy
121
+ */
122
+ export type StrategyOptions<T extends Strategy> = T extends DebounceStrategy
123
+ ? DebounceStrategyOptions
124
+ : T extends QueueStrategy
125
+ ? QueueStrategyOptions
126
+ : T extends ThrottleStrategy
127
+ ? ThrottleStrategyOptions
128
+ : T extends BatchStrategy
129
+ ? BatchStrategyOptions
130
+ : never