@tanstack/db 0.4.7 → 0.4.9

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 (115) hide show
  1. package/dist/cjs/collection/index.cjs.map +1 -1
  2. package/dist/cjs/collection/index.d.cts +2 -1
  3. package/dist/cjs/collection/lifecycle.cjs +2 -3
  4. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  5. package/dist/cjs/collection/state.cjs +22 -33
  6. package/dist/cjs/collection/state.cjs.map +1 -1
  7. package/dist/cjs/collection/state.d.cts +6 -2
  8. package/dist/cjs/collection/sync.cjs +4 -3
  9. package/dist/cjs/collection/sync.cjs.map +1 -1
  10. package/dist/cjs/errors.cjs +51 -17
  11. package/dist/cjs/errors.cjs.map +1 -1
  12. package/dist/cjs/errors.d.cts +38 -8
  13. package/dist/cjs/index.cjs +8 -4
  14. package/dist/cjs/index.cjs.map +1 -1
  15. package/dist/cjs/indexes/auto-index.cjs +0 -3
  16. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  17. package/dist/cjs/query/builder/types.d.cts +1 -1
  18. package/dist/cjs/query/compiler/index.cjs +42 -19
  19. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/index.d.cts +33 -8
  21. package/dist/cjs/query/compiler/joins.cjs +88 -66
  22. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  23. package/dist/cjs/query/compiler/joins.d.cts +5 -2
  24. package/dist/cjs/query/compiler/order-by.cjs +2 -0
  25. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/order-by.d.cts +1 -0
  27. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  28. package/dist/cjs/query/live/collection-config-builder.cjs +322 -46
  29. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  30. package/dist/cjs/query/live/collection-config-builder.d.cts +98 -7
  31. package/dist/cjs/query/live/collection-registry.cjs +16 -0
  32. package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
  33. package/dist/cjs/query/live/collection-registry.d.cts +26 -0
  34. package/dist/cjs/query/live/collection-subscriber.cjs +57 -58
  35. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  36. package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
  37. package/dist/cjs/query/live-query-collection.cjs +11 -5
  38. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  39. package/dist/cjs/query/live-query-collection.d.cts +10 -3
  40. package/dist/cjs/query/optimizer.cjs +44 -7
  41. package/dist/cjs/query/optimizer.cjs.map +1 -1
  42. package/dist/cjs/query/optimizer.d.cts +4 -4
  43. package/dist/cjs/scheduler.cjs +137 -0
  44. package/dist/cjs/scheduler.cjs.map +1 -0
  45. package/dist/cjs/scheduler.d.cts +56 -0
  46. package/dist/cjs/transactions.cjs +7 -1
  47. package/dist/cjs/transactions.cjs.map +1 -1
  48. package/dist/cjs/types.d.cts +3 -5
  49. package/dist/esm/collection/index.d.ts +2 -1
  50. package/dist/esm/collection/index.js.map +1 -1
  51. package/dist/esm/collection/lifecycle.js +2 -3
  52. package/dist/esm/collection/lifecycle.js.map +1 -1
  53. package/dist/esm/collection/state.d.ts +6 -2
  54. package/dist/esm/collection/state.js +22 -33
  55. package/dist/esm/collection/state.js.map +1 -1
  56. package/dist/esm/collection/sync.js +4 -3
  57. package/dist/esm/collection/sync.js.map +1 -1
  58. package/dist/esm/errors.d.ts +38 -8
  59. package/dist/esm/errors.js +52 -18
  60. package/dist/esm/errors.js.map +1 -1
  61. package/dist/esm/index.js +9 -5
  62. package/dist/esm/indexes/auto-index.js +0 -3
  63. package/dist/esm/indexes/auto-index.js.map +1 -1
  64. package/dist/esm/query/builder/types.d.ts +1 -1
  65. package/dist/esm/query/compiler/index.d.ts +33 -8
  66. package/dist/esm/query/compiler/index.js +42 -19
  67. package/dist/esm/query/compiler/index.js.map +1 -1
  68. package/dist/esm/query/compiler/joins.d.ts +5 -2
  69. package/dist/esm/query/compiler/joins.js +90 -68
  70. package/dist/esm/query/compiler/joins.js.map +1 -1
  71. package/dist/esm/query/compiler/order-by.d.ts +1 -0
  72. package/dist/esm/query/compiler/order-by.js +2 -0
  73. package/dist/esm/query/compiler/order-by.js.map +1 -1
  74. package/dist/esm/query/compiler/select.js.map +1 -1
  75. package/dist/esm/query/live/collection-config-builder.d.ts +98 -7
  76. package/dist/esm/query/live/collection-config-builder.js +322 -46
  77. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  78. package/dist/esm/query/live/collection-registry.d.ts +26 -0
  79. package/dist/esm/query/live/collection-registry.js +16 -0
  80. package/dist/esm/query/live/collection-registry.js.map +1 -0
  81. package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
  82. package/dist/esm/query/live/collection-subscriber.js +57 -58
  83. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  84. package/dist/esm/query/live-query-collection.d.ts +10 -3
  85. package/dist/esm/query/live-query-collection.js +11 -5
  86. package/dist/esm/query/live-query-collection.js.map +1 -1
  87. package/dist/esm/query/optimizer.d.ts +4 -4
  88. package/dist/esm/query/optimizer.js +44 -7
  89. package/dist/esm/query/optimizer.js.map +1 -1
  90. package/dist/esm/scheduler.d.ts +56 -0
  91. package/dist/esm/scheduler.js +137 -0
  92. package/dist/esm/scheduler.js.map +1 -0
  93. package/dist/esm/transactions.js +7 -1
  94. package/dist/esm/transactions.js.map +1 -1
  95. package/dist/esm/types.d.ts +3 -5
  96. package/package.json +2 -2
  97. package/src/collection/index.ts +1 -1
  98. package/src/collection/lifecycle.ts +3 -4
  99. package/src/collection/state.ts +52 -48
  100. package/src/collection/sync.ts +7 -6
  101. package/src/errors.ts +79 -13
  102. package/src/indexes/auto-index.ts +0 -8
  103. package/src/query/builder/types.ts +1 -1
  104. package/src/query/compiler/index.ts +115 -32
  105. package/src/query/compiler/joins.ts +180 -127
  106. package/src/query/compiler/order-by.ts +7 -0
  107. package/src/query/compiler/select.ts +2 -3
  108. package/src/query/live/collection-config-builder.ts +542 -71
  109. package/src/query/live/collection-registry.ts +47 -0
  110. package/src/query/live/collection-subscriber.ts +87 -105
  111. package/src/query/live-query-collection.ts +39 -14
  112. package/src/query/optimizer.ts +85 -15
  113. package/src/scheduler.ts +198 -0
  114. package/src/transactions.ts +12 -1
  115. package/src/types.ts +3 -5
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Identifier used to scope scheduled work. Maps to a transaction id for live queries.
3
+ */
4
+ export type SchedulerContextId = string | symbol
5
+
6
+ /**
7
+ * Options for {@link Scheduler.schedule}. Jobs are identified by `jobId` within a context
8
+ * and may declare dependencies.
9
+ */
10
+ interface ScheduleOptions {
11
+ contextId?: SchedulerContextId
12
+ jobId: unknown
13
+ dependencies?: Iterable<unknown>
14
+ run: () => void
15
+ }
16
+
17
+ /**
18
+ * State per context. Queue preserves order, jobs hold run functions, dependencies track
19
+ * prerequisites, and completed records which jobs have run during the current flush.
20
+ */
21
+ interface SchedulerContextState {
22
+ queue: Array<unknown>
23
+ jobs: Map<unknown, () => void>
24
+ dependencies: Map<unknown, Set<unknown>>
25
+ completed: Set<unknown>
26
+ }
27
+
28
+ /**
29
+ * Scoped scheduler that coalesces work by context and job.
30
+ *
31
+ * - **context** (e.g. transaction id) defines the batching boundary; work is queued until flushed.
32
+ * - **job id** deduplicates work within a context; scheduling the same job replaces the previous run function.
33
+ * - Without a context id, work executes immediately.
34
+ *
35
+ * Callers manage their own state; the scheduler only orchestrates execution order.
36
+ */
37
+ export class Scheduler {
38
+ private contexts = new Map<SchedulerContextId, SchedulerContextState>()
39
+ private clearListeners = new Set<(contextId: SchedulerContextId) => void>()
40
+
41
+ /**
42
+ * Get or create the state bucket for a context.
43
+ */
44
+ private getOrCreateContext(
45
+ contextId: SchedulerContextId
46
+ ): SchedulerContextState {
47
+ let context = this.contexts.get(contextId)
48
+ if (!context) {
49
+ context = {
50
+ queue: [],
51
+ jobs: new Map(),
52
+ dependencies: new Map(),
53
+ completed: new Set(),
54
+ }
55
+ this.contexts.set(contextId, context)
56
+ }
57
+ return context
58
+ }
59
+
60
+ /**
61
+ * Schedule work. Without a context id, executes immediately.
62
+ * Otherwise queues the job to be flushed once dependencies are satisfied.
63
+ * Scheduling the same jobId again replaces the previous run function.
64
+ */
65
+ schedule({ contextId, jobId, dependencies, run }: ScheduleOptions): void {
66
+ if (typeof contextId === `undefined`) {
67
+ run()
68
+ return
69
+ }
70
+
71
+ const context = this.getOrCreateContext(contextId)
72
+
73
+ // If this is a new job, add it to the queue
74
+ if (!context.jobs.has(jobId)) {
75
+ context.queue.push(jobId)
76
+ }
77
+
78
+ // Store or replace the run function
79
+ context.jobs.set(jobId, run)
80
+
81
+ // Update dependencies
82
+ if (dependencies) {
83
+ const depSet = new Set<unknown>(dependencies)
84
+ depSet.delete(jobId)
85
+ context.dependencies.set(jobId, depSet)
86
+ } else if (!context.dependencies.has(jobId)) {
87
+ context.dependencies.set(jobId, new Set())
88
+ }
89
+
90
+ // Clear completion status since we're rescheduling
91
+ context.completed.delete(jobId)
92
+ }
93
+
94
+ /**
95
+ * Flush all queued work for a context. Jobs with unmet dependencies are retried.
96
+ * Throws if a pass completes without running any job (dependency cycle).
97
+ */
98
+ flush(contextId: SchedulerContextId): void {
99
+ const context = this.contexts.get(contextId)
100
+ if (!context) return
101
+
102
+ const { queue, jobs, dependencies, completed } = context
103
+
104
+ while (queue.length > 0) {
105
+ let ranThisPass = false
106
+ const jobsThisPass = queue.length
107
+
108
+ for (let i = 0; i < jobsThisPass; i++) {
109
+ const jobId = queue.shift()!
110
+ const run = jobs.get(jobId)
111
+ if (!run) {
112
+ dependencies.delete(jobId)
113
+ completed.delete(jobId)
114
+ continue
115
+ }
116
+
117
+ const deps = dependencies.get(jobId)
118
+ let ready = !deps
119
+ if (deps) {
120
+ ready = true
121
+ for (const dep of deps) {
122
+ if (dep !== jobId && !completed.has(dep)) {
123
+ ready = false
124
+ break
125
+ }
126
+ }
127
+ }
128
+
129
+ if (ready) {
130
+ jobs.delete(jobId)
131
+ dependencies.delete(jobId)
132
+ // Run the job. If it throws, we don't mark it complete, allowing the
133
+ // error to propagate while maintaining scheduler state consistency.
134
+ run()
135
+ completed.add(jobId)
136
+ ranThisPass = true
137
+ } else {
138
+ queue.push(jobId)
139
+ }
140
+ }
141
+
142
+ if (!ranThisPass) {
143
+ throw new Error(
144
+ `Scheduler detected unresolved dependencies for context ${String(
145
+ contextId
146
+ )}.`
147
+ )
148
+ }
149
+ }
150
+
151
+ this.contexts.delete(contextId)
152
+ }
153
+
154
+ /**
155
+ * Flush all contexts with pending work. Useful during tear-down.
156
+ */
157
+ flushAll(): void {
158
+ for (const contextId of Array.from(this.contexts.keys())) {
159
+ this.flush(contextId)
160
+ }
161
+ }
162
+
163
+ /** Clear all scheduled jobs for a context. */
164
+ clear(contextId: SchedulerContextId): void {
165
+ this.contexts.delete(contextId)
166
+ // Notify listeners that this context was cleared
167
+ this.clearListeners.forEach((listener) => listener(contextId))
168
+ }
169
+
170
+ /** Register a listener to be notified when a context is cleared. */
171
+ onClear(listener: (contextId: SchedulerContextId) => void): () => void {
172
+ this.clearListeners.add(listener)
173
+ return () => this.clearListeners.delete(listener)
174
+ }
175
+
176
+ /** Check if a context has pending jobs. */
177
+ hasPendingJobs(contextId: SchedulerContextId): boolean {
178
+ const context = this.contexts.get(contextId)
179
+ return !!context && context.jobs.size > 0
180
+ }
181
+
182
+ /** Remove a single job from a context and clean up its dependencies. */
183
+ clearJob(contextId: SchedulerContextId, jobId: unknown): void {
184
+ const context = this.contexts.get(contextId)
185
+ if (!context) return
186
+
187
+ context.jobs.delete(jobId)
188
+ context.dependencies.delete(jobId)
189
+ context.completed.delete(jobId)
190
+ context.queue = context.queue.filter((id) => id !== jobId)
191
+
192
+ if (context.jobs.size === 0) {
193
+ this.contexts.delete(contextId)
194
+ }
195
+ }
196
+ }
197
+
198
+ export const transactionScopedScheduler = new Scheduler()
@@ -5,6 +5,7 @@ import {
5
5
  TransactionNotPendingCommitError,
6
6
  TransactionNotPendingMutateError,
7
7
  } from "./errors"
8
+ import { transactionScopedScheduler } from "./scheduler.js"
8
9
  import type { Deferred } from "./deferred"
9
10
  import type {
10
11
  MutationFn,
@@ -179,11 +180,21 @@ export function getActiveTransaction(): Transaction | undefined {
179
180
  }
180
181
 
181
182
  function registerTransaction(tx: Transaction<any>) {
183
+ // Clear any stale work that may have been left behind if a previous mutate
184
+ // scope aborted before we could flush.
185
+ transactionScopedScheduler.clear(tx.id)
182
186
  transactionStack.push(tx)
183
187
  }
184
188
 
185
189
  function unregisterTransaction(tx: Transaction<any>) {
186
- transactionStack = transactionStack.filter((t) => t.id !== tx.id)
190
+ // Always flush pending work for this transaction before removing it from
191
+ // the ambient stack – this runs even if the mutate callback throws.
192
+ // If flush throws (e.g., due to a job error), we still clean up the stack.
193
+ try {
194
+ transactionScopedScheduler.flush(tx.id)
195
+ } finally {
196
+ transactionStack = transactionStack.filter((t) => t.id !== tx.id)
197
+ }
187
198
  }
188
199
 
189
200
  function removeFromPendingList(tx: Transaction<any>) {
package/src/types.ts CHANGED
@@ -298,17 +298,15 @@ export type DeleteMutationFn<
298
298
  *
299
299
  * @example
300
300
  * // Status transitions
301
- * // idle → loading → initialCommit ready
301
+ * // idle → loading → ready (when markReady() is called)
302
302
  * // Any status can transition to → error or cleaned-up
303
303
  */
304
304
  export type CollectionStatus =
305
305
  /** Collection is created but sync hasn't started yet (when startSync config is false) */
306
306
  | `idle`
307
- /** Sync has started but hasn't received the first commit yet */
307
+ /** Sync has started and is loading data */
308
308
  | `loading`
309
- /** Collection is in the process of committing its first transaction */
310
- | `initialCommit`
311
- /** Collection has received at least one commit and is ready for use */
309
+ /** Collection has been explicitly marked ready via markReady() */
312
310
  | `ready`
313
311
  /** An error occurred during sync initialization */
314
312
  | `error`