@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.
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +2 -1
- package/dist/cjs/collection/lifecycle.cjs +2 -3
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/state.cjs +22 -33
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +6 -2
- package/dist/cjs/collection/sync.cjs +4 -3
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/errors.cjs +51 -17
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +38 -8
- package/dist/cjs/index.cjs +8 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.cjs +0 -3
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +42 -19
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +33 -8
- package/dist/cjs/query/compiler/joins.cjs +88 -66
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +5 -2
- package/dist/cjs/query/compiler/order-by.cjs +2 -0
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +322 -46
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +98 -7
- package/dist/cjs/query/live/collection-registry.cjs +16 -0
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
- package/dist/cjs/query/live/collection-registry.d.cts +26 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +57 -58
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
- package/dist/cjs/query/live-query-collection.cjs +11 -5
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +10 -3
- package/dist/cjs/query/optimizer.cjs +44 -7
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +4 -4
- package/dist/cjs/scheduler.cjs +137 -0
- package/dist/cjs/scheduler.cjs.map +1 -0
- package/dist/cjs/scheduler.d.cts +56 -0
- package/dist/cjs/transactions.cjs +7 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +3 -5
- package/dist/esm/collection/index.d.ts +2 -1
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/lifecycle.js +2 -3
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/state.d.ts +6 -2
- package/dist/esm/collection/state.js +22 -33
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/sync.js +4 -3
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +38 -8
- package/dist/esm/errors.js +52 -18
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +9 -5
- package/dist/esm/indexes/auto-index.js +0 -3
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +33 -8
- package/dist/esm/query/compiler/index.js +42 -19
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +5 -2
- package/dist/esm/query/compiler/joins.js +90 -68
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -0
- package/dist/esm/query/compiler/order-by.js +2 -0
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +98 -7
- package/dist/esm/query/live/collection-config-builder.js +322 -46
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.d.ts +26 -0
- package/dist/esm/query/live/collection-registry.js +16 -0
- package/dist/esm/query/live/collection-registry.js.map +1 -0
- package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
- package/dist/esm/query/live/collection-subscriber.js +57 -58
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +10 -3
- package/dist/esm/query/live-query-collection.js +11 -5
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +4 -4
- package/dist/esm/query/optimizer.js +44 -7
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/scheduler.d.ts +56 -0
- package/dist/esm/scheduler.js +137 -0
- package/dist/esm/scheduler.js.map +1 -0
- package/dist/esm/transactions.js +7 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +3 -5
- package/package.json +2 -2
- package/src/collection/index.ts +1 -1
- package/src/collection/lifecycle.ts +3 -4
- package/src/collection/state.ts +52 -48
- package/src/collection/sync.ts +7 -6
- package/src/errors.ts +79 -13
- package/src/indexes/auto-index.ts +0 -8
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +115 -32
- package/src/query/compiler/joins.ts +180 -127
- package/src/query/compiler/order-by.ts +7 -0
- package/src/query/compiler/select.ts +2 -3
- package/src/query/live/collection-config-builder.ts +542 -71
- package/src/query/live/collection-registry.ts +47 -0
- package/src/query/live/collection-subscriber.ts +87 -105
- package/src/query/live-query-collection.ts +39 -14
- package/src/query/optimizer.ts +85 -15
- package/src/scheduler.ts +198 -0
- package/src/transactions.ts +12 -1
- package/src/types.ts +3 -5
package/src/scheduler.ts
ADDED
|
@@ -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()
|
package/src/transactions.ts
CHANGED
|
@@ -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
|
-
|
|
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 →
|
|
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
|
|
307
|
+
/** Sync has started and is loading data */
|
|
308
308
|
| `loading`
|
|
309
|
-
/** Collection
|
|
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`
|