@tanstack/db 0.4.8 → 0.4.10
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/events.cjs +9 -51
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +18 -7
- package/dist/cjs/collection/index.cjs +9 -12
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +13 -14
- package/dist/cjs/collection/subscription.cjs +62 -6
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +16 -3
- package/dist/cjs/collection/sync.cjs +58 -6
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +18 -4
- package/dist/cjs/errors.cjs +59 -17
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +44 -8
- package/dist/cjs/event-emitter.cjs +94 -0
- package/dist/cjs/event-emitter.cjs.map +1 -0
- package/dist/cjs/event-emitter.d.cts +45 -0
- package/dist/cjs/index.cjs +9 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/local-only.d.cts +2 -5
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +46 -19
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +35 -9
- package/dist/cjs/query/compiler/joins.cjs +91 -66
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +6 -3
- package/dist/cjs/query/compiler/order-by.cjs +20 -4
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +3 -1
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/compiler/types.d.cts +4 -0
- package/dist/cjs/query/index.d.cts +1 -0
- package/dist/cjs/query/live/collection-config-builder.cjs +306 -46
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +97 -9
- 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 +86 -58
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +5 -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 +12 -5
- 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 +82 -11
- package/dist/esm/collection/events.d.ts +18 -7
- package/dist/esm/collection/events.js +9 -51
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +13 -14
- package/dist/esm/collection/index.js +9 -12
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +16 -3
- package/dist/esm/collection/subscription.js +62 -6
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +18 -4
- package/dist/esm/collection/sync.js +59 -7
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +44 -8
- package/dist/esm/errors.js +60 -18
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/event-emitter.d.ts +45 -0
- package/dist/esm/event-emitter.js +94 -0
- package/dist/esm/event-emitter.js.map +1 -0
- package/dist/esm/index.js +10 -5
- package/dist/esm/local-only.d.ts +2 -5
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +35 -9
- package/dist/esm/query/compiler/index.js +46 -19
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +6 -3
- package/dist/esm/query/compiler/joins.js +93 -68
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +3 -1
- package/dist/esm/query/compiler/order-by.js +20 -4
- 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/compiler/types.d.ts +4 -0
- package/dist/esm/query/index.d.ts +1 -0
- package/dist/esm/query/live/collection-config-builder.d.ts +97 -9
- package/dist/esm/query/live/collection-config-builder.js +306 -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 +5 -7
- package/dist/esm/query/live/collection-subscriber.js +86 -58
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +12 -5
- 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 +82 -11
- package/package.json +2 -2
- package/src/collection/events.ts +25 -74
- package/src/collection/index.ts +15 -19
- package/src/collection/subscription.ts +88 -6
- package/src/collection/sync.ts +81 -9
- package/src/errors.ts +91 -13
- package/src/event-emitter.ts +118 -0
- package/src/local-only.ts +5 -12
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +124 -33
- package/src/query/compiler/joins.ts +187 -128
- package/src/query/compiler/order-by.ts +30 -2
- package/src/query/compiler/select.ts +2 -3
- package/src/query/compiler/types.ts +5 -0
- package/src/query/index.ts +1 -0
- package/src/query/live/collection-config-builder.ts +501 -60
- package/src/query/live/collection-registry.ts +47 -0
- package/src/query/live/collection-subscriber.ts +137 -105
- package/src/query/live-query-collection.ts +47 -18
- package/src/query/optimizer.ts +85 -15
- package/src/scheduler.ts +198 -0
- package/src/transactions.ts +12 -1
- package/src/types.ts +93 -11
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { ensureIndexForExpression } from "../indexes/auto-index.js"
|
|
2
2
|
import { and, gt, lt } from "../query/builder/functions.js"
|
|
3
3
|
import { Value } from "../query/ir.js"
|
|
4
|
+
import { EventEmitter } from "../event-emitter.js"
|
|
4
5
|
import {
|
|
5
6
|
createFilterFunctionFromExpression,
|
|
6
7
|
createFilteredCallback,
|
|
7
8
|
} from "./change-events.js"
|
|
8
9
|
import type { BasicExpression, OrderBy } from "../query/ir.js"
|
|
9
10
|
import type { IndexInterface } from "../indexes/base-index.js"
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
ChangeMessage,
|
|
13
|
+
Subscription,
|
|
14
|
+
SubscriptionEvents,
|
|
15
|
+
SubscriptionStatus,
|
|
16
|
+
SubscriptionUnsubscribedEvent,
|
|
17
|
+
} from "../types.js"
|
|
11
18
|
import type { CollectionImpl } from "./index.js"
|
|
12
19
|
|
|
13
20
|
type RequestSnapshotOptions = {
|
|
@@ -22,13 +29,17 @@ type RequestLimitedSnapshotOptions = {
|
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
type CollectionSubscriptionOptions = {
|
|
32
|
+
includeInitialState?: boolean
|
|
25
33
|
/** Pre-compiled expression for filtering changes */
|
|
26
34
|
whereExpression?: BasicExpression<boolean>
|
|
27
35
|
/** Callback to call when the subscription is unsubscribed */
|
|
28
|
-
onUnsubscribe?: () => void
|
|
36
|
+
onUnsubscribe?: (event: SubscriptionUnsubscribedEvent) => void
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
export class CollectionSubscription
|
|
39
|
+
export class CollectionSubscription
|
|
40
|
+
extends EventEmitter<SubscriptionEvents>
|
|
41
|
+
implements Subscription
|
|
42
|
+
{
|
|
32
43
|
private loadedInitialState = false
|
|
33
44
|
|
|
34
45
|
// Flag to indicate that we have sent at least 1 snapshot.
|
|
@@ -42,11 +53,24 @@ export class CollectionSubscription {
|
|
|
42
53
|
|
|
43
54
|
private orderByIndex: IndexInterface<string | number> | undefined
|
|
44
55
|
|
|
56
|
+
// Status tracking
|
|
57
|
+
private _status: SubscriptionStatus = `ready`
|
|
58
|
+
private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()
|
|
59
|
+
|
|
60
|
+
public get status(): SubscriptionStatus {
|
|
61
|
+
return this._status
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
constructor(
|
|
46
65
|
private collection: CollectionImpl<any, any, any, any, any>,
|
|
47
66
|
private callback: (changes: Array<ChangeMessage<any, any>>) => void,
|
|
48
67
|
private options: CollectionSubscriptionOptions
|
|
49
68
|
) {
|
|
69
|
+
super()
|
|
70
|
+
if (options.onUnsubscribe) {
|
|
71
|
+
this.on(`unsubscribed`, (event) => options.onUnsubscribe!(event))
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
// Auto-index for where expressions if enabled
|
|
51
75
|
if (options.whereExpression) {
|
|
52
76
|
ensureIndexForExpression(options.whereExpression, this.collection)
|
|
@@ -71,6 +95,53 @@ export class CollectionSubscription {
|
|
|
71
95
|
this.orderByIndex = index
|
|
72
96
|
}
|
|
73
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Set subscription status and emit events if changed
|
|
100
|
+
*/
|
|
101
|
+
private setStatus(newStatus: SubscriptionStatus) {
|
|
102
|
+
if (this._status === newStatus) {
|
|
103
|
+
return // No change
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const previousStatus = this._status
|
|
107
|
+
this._status = newStatus
|
|
108
|
+
|
|
109
|
+
// Emit status:change event
|
|
110
|
+
this.emitInner(`status:change`, {
|
|
111
|
+
type: `status:change`,
|
|
112
|
+
subscription: this,
|
|
113
|
+
previousStatus,
|
|
114
|
+
status: newStatus,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Emit specific status event
|
|
118
|
+
const eventKey: `status:${SubscriptionStatus}` = `status:${newStatus}`
|
|
119
|
+
this.emitInner(eventKey, {
|
|
120
|
+
type: eventKey,
|
|
121
|
+
subscription: this,
|
|
122
|
+
previousStatus,
|
|
123
|
+
status: newStatus,
|
|
124
|
+
} as SubscriptionEvents[typeof eventKey])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Track a loadSubset promise and manage loading status
|
|
129
|
+
*/
|
|
130
|
+
private trackLoadSubsetPromise(syncResult: Promise<void> | true) {
|
|
131
|
+
// Track the promise if it's actually a promise (async work)
|
|
132
|
+
if (syncResult instanceof Promise) {
|
|
133
|
+
this.pendingLoadSubsetPromises.add(syncResult)
|
|
134
|
+
this.setStatus(`loadingSubset`)
|
|
135
|
+
|
|
136
|
+
syncResult.finally(() => {
|
|
137
|
+
this.pendingLoadSubsetPromises.delete(syncResult)
|
|
138
|
+
if (this.pendingLoadSubsetPromises.size === 0) {
|
|
139
|
+
this.setStatus(`ready`)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
74
145
|
hasLoadedInitialState() {
|
|
75
146
|
return this.loadedInitialState
|
|
76
147
|
}
|
|
@@ -121,10 +192,13 @@ export class CollectionSubscription {
|
|
|
121
192
|
|
|
122
193
|
// Request the sync layer to load more data
|
|
123
194
|
// don't await it, we will load the data into the collection when it comes in
|
|
124
|
-
this.collection.
|
|
195
|
+
const syncResult = this.collection._sync.loadSubset({
|
|
125
196
|
where: stateOpts.where,
|
|
197
|
+
subscription: this,
|
|
126
198
|
})
|
|
127
199
|
|
|
200
|
+
this.trackLoadSubsetPromise(syncResult)
|
|
201
|
+
|
|
128
202
|
// Also load data immediately from the collection
|
|
129
203
|
const snapshot = this.collection.currentStateAsChanges(stateOpts)
|
|
130
204
|
|
|
@@ -215,11 +289,14 @@ export class CollectionSubscription {
|
|
|
215
289
|
|
|
216
290
|
// Request the sync layer to load more data
|
|
217
291
|
// don't await it, we will load the data into the collection when it comes in
|
|
218
|
-
this.collection.
|
|
292
|
+
const syncResult = this.collection._sync.loadSubset({
|
|
219
293
|
where: whereWithValueFilter,
|
|
220
294
|
limit,
|
|
221
295
|
orderBy,
|
|
296
|
+
subscription: this,
|
|
222
297
|
})
|
|
298
|
+
|
|
299
|
+
this.trackLoadSubsetPromise(syncResult)
|
|
223
300
|
}
|
|
224
301
|
|
|
225
302
|
/**
|
|
@@ -264,6 +341,11 @@ export class CollectionSubscription {
|
|
|
264
341
|
}
|
|
265
342
|
|
|
266
343
|
unsubscribe() {
|
|
267
|
-
this.
|
|
344
|
+
this.emitInner(`unsubscribed`, {
|
|
345
|
+
type: `unsubscribed`,
|
|
346
|
+
subscription: this,
|
|
347
|
+
})
|
|
348
|
+
// Clear all event listeners to prevent memory leaks
|
|
349
|
+
this.clearListeners()
|
|
268
350
|
}
|
|
269
351
|
}
|
package/src/collection/sync.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CollectionConfigurationError,
|
|
2
3
|
CollectionIsInErrorStateError,
|
|
3
4
|
DuplicateKeySyncError,
|
|
4
5
|
NoPendingSyncTransactionCommitError,
|
|
@@ -13,12 +14,13 @@ import type {
|
|
|
13
14
|
ChangeMessage,
|
|
14
15
|
CleanupFn,
|
|
15
16
|
CollectionConfig,
|
|
16
|
-
|
|
17
|
+
LoadSubsetOptions,
|
|
17
18
|
SyncConfigRes,
|
|
18
19
|
} from "../types"
|
|
19
20
|
import type { CollectionImpl } from "./index.js"
|
|
20
21
|
import type { CollectionStateManager } from "./state"
|
|
21
22
|
import type { CollectionLifecycleManager } from "./lifecycle"
|
|
23
|
+
import type { CollectionEventsManager } from "./events.js"
|
|
22
24
|
|
|
23
25
|
export class CollectionSyncManager<
|
|
24
26
|
TOutput extends object = Record<string, unknown>,
|
|
@@ -29,31 +31,38 @@ export class CollectionSyncManager<
|
|
|
29
31
|
private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
30
32
|
private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
|
|
31
33
|
private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
|
|
34
|
+
private _events!: CollectionEventsManager
|
|
32
35
|
private config!: CollectionConfig<TOutput, TKey, TSchema>
|
|
33
36
|
private id: string
|
|
37
|
+
private syncMode: `eager` | `on-demand`
|
|
34
38
|
|
|
35
39
|
public preloadPromise: Promise<void> | null = null
|
|
36
40
|
public syncCleanupFn: (() => void) | null = null
|
|
37
|
-
public
|
|
38
|
-
| ((options:
|
|
41
|
+
public syncLoadSubsetFn:
|
|
42
|
+
| ((options: LoadSubsetOptions) => true | Promise<void>)
|
|
39
43
|
| null = null
|
|
40
44
|
|
|
45
|
+
private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()
|
|
46
|
+
|
|
41
47
|
/**
|
|
42
48
|
* Creates a new CollectionSyncManager instance
|
|
43
49
|
*/
|
|
44
50
|
constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {
|
|
45
51
|
this.config = config
|
|
46
52
|
this.id = id
|
|
53
|
+
this.syncMode = config.syncMode ?? `eager`
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
setDeps(deps: {
|
|
50
57
|
collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
51
58
|
state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
|
|
52
59
|
lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
|
|
60
|
+
events: CollectionEventsManager
|
|
53
61
|
}) {
|
|
54
62
|
this.collection = deps.collection
|
|
55
63
|
this.state = deps.state
|
|
56
64
|
this.lifecycle = deps.lifecycle
|
|
65
|
+
this._events = deps.events
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -189,8 +198,16 @@ export class CollectionSyncManager<
|
|
|
189
198
|
// Store cleanup function if provided
|
|
190
199
|
this.syncCleanupFn = syncRes?.cleanup ?? null
|
|
191
200
|
|
|
192
|
-
// Store
|
|
193
|
-
this.
|
|
201
|
+
// Store loadSubset function if provided
|
|
202
|
+
this.syncLoadSubsetFn = syncRes?.loadSubset ?? null
|
|
203
|
+
|
|
204
|
+
// Validate: on-demand mode requires a loadSubset function
|
|
205
|
+
if (this.syncMode === `on-demand` && !this.syncLoadSubsetFn) {
|
|
206
|
+
throw new CollectionConfigurationError(
|
|
207
|
+
`Collection "${this.id}" is configured with syncMode "on-demand" but the sync function did not return a loadSubset handler. ` +
|
|
208
|
+
`Either provide a loadSubset handler or use syncMode "eager".`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
194
211
|
} catch (error) {
|
|
195
212
|
this.lifecycle.setStatus(`error`)
|
|
196
213
|
throw error
|
|
@@ -239,16 +256,71 @@ export class CollectionSyncManager<
|
|
|
239
256
|
return this.preloadPromise
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Gets whether the collection is currently loading more data
|
|
261
|
+
*/
|
|
262
|
+
public get isLoadingSubset(): boolean {
|
|
263
|
+
return this.pendingLoadSubsetPromises.size > 0
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Tracks a load promise for isLoadingSubset state.
|
|
268
|
+
* @internal This is for internal coordination (e.g., live-query glue code), not for general use.
|
|
269
|
+
*/
|
|
270
|
+
public trackLoadPromise(promise: Promise<void>): void {
|
|
271
|
+
const loadingStarting = !this.isLoadingSubset
|
|
272
|
+
this.pendingLoadSubsetPromises.add(promise)
|
|
273
|
+
|
|
274
|
+
if (loadingStarting) {
|
|
275
|
+
this._events.emit(`loadingSubset:change`, {
|
|
276
|
+
type: `loadingSubset:change`,
|
|
277
|
+
collection: this.collection,
|
|
278
|
+
isLoadingSubset: true,
|
|
279
|
+
previousIsLoadingSubset: false,
|
|
280
|
+
loadingSubsetTransition: `start`,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
promise.finally(() => {
|
|
285
|
+
const loadingEnding =
|
|
286
|
+
this.pendingLoadSubsetPromises.size === 1 &&
|
|
287
|
+
this.pendingLoadSubsetPromises.has(promise)
|
|
288
|
+
this.pendingLoadSubsetPromises.delete(promise)
|
|
289
|
+
|
|
290
|
+
if (loadingEnding) {
|
|
291
|
+
this._events.emit(`loadingSubset:change`, {
|
|
292
|
+
type: `loadingSubset:change`,
|
|
293
|
+
collection: this.collection,
|
|
294
|
+
isLoadingSubset: false,
|
|
295
|
+
previousIsLoadingSubset: true,
|
|
296
|
+
loadingSubsetTransition: `end`,
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
242
302
|
/**
|
|
243
303
|
* Requests the sync layer to load more data.
|
|
244
304
|
* @param options Options to control what data is being loaded
|
|
245
305
|
* @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
|
|
246
|
-
*
|
|
306
|
+
* Returns true if no sync function is configured, if syncMode is 'eager', or if there is no work to do.
|
|
247
307
|
*/
|
|
248
|
-
public
|
|
249
|
-
|
|
250
|
-
|
|
308
|
+
public loadSubset(options: LoadSubsetOptions): Promise<void> | true {
|
|
309
|
+
// Bypass loadSubset when syncMode is 'eager'
|
|
310
|
+
if (this.syncMode === `eager`) {
|
|
311
|
+
return true
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (this.syncLoadSubsetFn) {
|
|
315
|
+
const result = this.syncLoadSubsetFn(options)
|
|
316
|
+
// If the result is a promise, track it
|
|
317
|
+
if (result instanceof Promise) {
|
|
318
|
+
this.trackLoadPromise(result)
|
|
319
|
+
return result
|
|
320
|
+
}
|
|
251
321
|
}
|
|
322
|
+
|
|
323
|
+
return true
|
|
252
324
|
}
|
|
253
325
|
|
|
254
326
|
public cleanup(): void {
|
package/src/errors.ts
CHANGED
|
@@ -349,9 +349,23 @@ export class LimitOffsetRequireOrderByError extends QueryCompilationError {
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Error thrown when a collection input stream is not found during query compilation.
|
|
354
|
+
* In self-joins, each alias (e.g., 'employee', 'manager') requires its own input stream.
|
|
355
|
+
*/
|
|
352
356
|
export class CollectionInputNotFoundError extends QueryCompilationError {
|
|
353
|
-
constructor(
|
|
354
|
-
|
|
357
|
+
constructor(
|
|
358
|
+
alias: string,
|
|
359
|
+
collectionId?: string,
|
|
360
|
+
availableKeys?: Array<string>
|
|
361
|
+
) {
|
|
362
|
+
const details = collectionId
|
|
363
|
+
? `alias "${alias}" (collection "${collectionId}")`
|
|
364
|
+
: `collection "${alias}"`
|
|
365
|
+
const availableKeysMsg = availableKeys?.length
|
|
366
|
+
? `. Available keys: ${availableKeys.join(`, `)}`
|
|
367
|
+
: ``
|
|
368
|
+
super(`Input for ${details} not found in inputs map${availableKeysMsg}`)
|
|
355
369
|
}
|
|
356
370
|
}
|
|
357
371
|
|
|
@@ -399,32 +413,32 @@ export class UnsupportedJoinTypeError extends JoinError {
|
|
|
399
413
|
}
|
|
400
414
|
}
|
|
401
415
|
|
|
402
|
-
export class
|
|
403
|
-
constructor(
|
|
416
|
+
export class InvalidJoinConditionSameSourceError extends JoinError {
|
|
417
|
+
constructor(sourceAlias: string) {
|
|
404
418
|
super(
|
|
405
|
-
`Invalid join condition: both expressions refer to the same
|
|
419
|
+
`Invalid join condition: both expressions refer to the same source "${sourceAlias}"`
|
|
406
420
|
)
|
|
407
421
|
}
|
|
408
422
|
}
|
|
409
423
|
|
|
410
|
-
export class
|
|
424
|
+
export class InvalidJoinConditionSourceMismatchError extends JoinError {
|
|
411
425
|
constructor() {
|
|
412
|
-
super(`Invalid join condition: expressions must reference
|
|
426
|
+
super(`Invalid join condition: expressions must reference source aliases`)
|
|
413
427
|
}
|
|
414
428
|
}
|
|
415
429
|
|
|
416
|
-
export class
|
|
417
|
-
constructor(
|
|
430
|
+
export class InvalidJoinConditionLeftSourceError extends JoinError {
|
|
431
|
+
constructor(sourceAlias: string) {
|
|
418
432
|
super(
|
|
419
|
-
`Invalid join condition: left expression refers to an unavailable
|
|
433
|
+
`Invalid join condition: left expression refers to an unavailable source "${sourceAlias}"`
|
|
420
434
|
)
|
|
421
435
|
}
|
|
422
436
|
}
|
|
423
437
|
|
|
424
|
-
export class
|
|
425
|
-
constructor(
|
|
438
|
+
export class InvalidJoinConditionRightSourceError extends JoinError {
|
|
439
|
+
constructor(sourceAlias: string) {
|
|
426
440
|
super(
|
|
427
|
-
`Invalid join condition: right expression does not refer to the joined
|
|
441
|
+
`Invalid join condition: right expression does not refer to the joined source "${sourceAlias}"`
|
|
428
442
|
)
|
|
429
443
|
}
|
|
430
444
|
}
|
|
@@ -563,3 +577,67 @@ export class CannotCombineEmptyExpressionListError extends QueryOptimizerError {
|
|
|
563
577
|
super(`Cannot combine empty expression list`)
|
|
564
578
|
}
|
|
565
579
|
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Internal error when the query optimizer fails to convert a WHERE clause to a collection filter.
|
|
583
|
+
*/
|
|
584
|
+
export class WhereClauseConversionError extends QueryOptimizerError {
|
|
585
|
+
constructor(collectionId: string, alias: string) {
|
|
586
|
+
super(
|
|
587
|
+
`Failed to convert WHERE clause to collection filter for collection '${collectionId}' alias '${alias}'. This indicates a bug in the query optimization logic.`
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Error when a subscription cannot be found during lazy join processing.
|
|
594
|
+
* For subqueries, aliases may be remapped (e.g., 'activeUser' → 'user').
|
|
595
|
+
*/
|
|
596
|
+
export class SubscriptionNotFoundError extends QueryCompilationError {
|
|
597
|
+
constructor(
|
|
598
|
+
resolvedAlias: string,
|
|
599
|
+
originalAlias: string,
|
|
600
|
+
collectionId: string,
|
|
601
|
+
availableAliases: Array<string>
|
|
602
|
+
) {
|
|
603
|
+
super(
|
|
604
|
+
`Internal error: subscription for alias '${resolvedAlias}' (remapped from '${originalAlias}', collection '${collectionId}') is missing in join pipeline. Available aliases: ${availableAliases.join(`, `)}. This indicates a bug in alias tracking.`
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Error thrown when aggregate expressions are used outside of a GROUP BY context.
|
|
611
|
+
*/
|
|
612
|
+
export class AggregateNotSupportedError extends QueryCompilationError {
|
|
613
|
+
constructor() {
|
|
614
|
+
super(
|
|
615
|
+
`Aggregate expressions are not supported in this context. Use GROUP BY clause for aggregates.`
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Internal error when the compiler returns aliases that don't have corresponding input streams.
|
|
622
|
+
* This should never happen since all aliases come from user declarations.
|
|
623
|
+
*/
|
|
624
|
+
export class MissingAliasInputsError extends QueryCompilationError {
|
|
625
|
+
constructor(missingAliases: Array<string>) {
|
|
626
|
+
super(
|
|
627
|
+
`Internal error: compiler returned aliases without inputs: ${missingAliases.join(`, `)}. ` +
|
|
628
|
+
`This indicates a bug in query compilation. Please report this issue.`
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Error thrown when setWindow is called on a collection without an ORDER BY clause.
|
|
635
|
+
*/
|
|
636
|
+
export class SetWindowRequiresOrderByError extends QueryCompilationError {
|
|
637
|
+
constructor() {
|
|
638
|
+
super(
|
|
639
|
+
`setWindow() can only be called on collections with an ORDER BY clause. ` +
|
|
640
|
+
`Add .orderBy() to your query to enable window movement.`
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic type-safe event emitter
|
|
3
|
+
* @template TEvents - Record of event names to event payload types
|
|
4
|
+
*/
|
|
5
|
+
export class EventEmitter<TEvents extends Record<string, any>> {
|
|
6
|
+
private listeners = new Map<
|
|
7
|
+
keyof TEvents,
|
|
8
|
+
Set<(event: TEvents[keyof TEvents]) => void>
|
|
9
|
+
>()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to an event
|
|
13
|
+
* @param event - Event name to listen for
|
|
14
|
+
* @param callback - Function to call when event is emitted
|
|
15
|
+
* @returns Unsubscribe function
|
|
16
|
+
*/
|
|
17
|
+
on<T extends keyof TEvents>(
|
|
18
|
+
event: T,
|
|
19
|
+
callback: (event: TEvents[T]) => void
|
|
20
|
+
): () => void {
|
|
21
|
+
if (!this.listeners.has(event)) {
|
|
22
|
+
this.listeners.set(event, new Set())
|
|
23
|
+
}
|
|
24
|
+
this.listeners.get(event)!.add(callback as (event: any) => void)
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
this.listeners.get(event)?.delete(callback as (event: any) => void)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to an event once (automatically unsubscribes after first emission)
|
|
33
|
+
* @param event - Event name to listen for
|
|
34
|
+
* @param callback - Function to call when event is emitted
|
|
35
|
+
* @returns Unsubscribe function
|
|
36
|
+
*/
|
|
37
|
+
once<T extends keyof TEvents>(
|
|
38
|
+
event: T,
|
|
39
|
+
callback: (event: TEvents[T]) => void
|
|
40
|
+
): () => void {
|
|
41
|
+
const unsubscribe = this.on(event, (eventPayload) => {
|
|
42
|
+
callback(eventPayload)
|
|
43
|
+
unsubscribe()
|
|
44
|
+
})
|
|
45
|
+
return unsubscribe
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unsubscribe from an event
|
|
50
|
+
* @param event - Event name to stop listening for
|
|
51
|
+
* @param callback - Function to remove
|
|
52
|
+
*/
|
|
53
|
+
off<T extends keyof TEvents>(
|
|
54
|
+
event: T,
|
|
55
|
+
callback: (event: TEvents[T]) => void
|
|
56
|
+
): void {
|
|
57
|
+
this.listeners.get(event)?.delete(callback as (event: any) => void)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wait for an event to be emitted
|
|
62
|
+
* @param event - Event name to wait for
|
|
63
|
+
* @param timeout - Optional timeout in milliseconds
|
|
64
|
+
* @returns Promise that resolves with the event payload
|
|
65
|
+
*/
|
|
66
|
+
waitFor<T extends keyof TEvents>(
|
|
67
|
+
event: T,
|
|
68
|
+
timeout?: number
|
|
69
|
+
): Promise<TEvents[T]> {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
let timeoutId: NodeJS.Timeout | undefined
|
|
72
|
+
const unsubscribe = this.on(event, (eventPayload) => {
|
|
73
|
+
if (timeoutId) {
|
|
74
|
+
clearTimeout(timeoutId)
|
|
75
|
+
timeoutId = undefined
|
|
76
|
+
}
|
|
77
|
+
resolve(eventPayload)
|
|
78
|
+
unsubscribe()
|
|
79
|
+
})
|
|
80
|
+
if (timeout) {
|
|
81
|
+
timeoutId = setTimeout(() => {
|
|
82
|
+
timeoutId = undefined
|
|
83
|
+
unsubscribe()
|
|
84
|
+
reject(new Error(`Timeout waiting for event ${String(event)}`))
|
|
85
|
+
}, timeout)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Emit an event to all listeners
|
|
92
|
+
* @param event - Event name to emit
|
|
93
|
+
* @param eventPayload - Event payload
|
|
94
|
+
* @internal For use by subclasses - subclasses should wrap this with a public emit if needed
|
|
95
|
+
*/
|
|
96
|
+
protected emitInner<T extends keyof TEvents>(
|
|
97
|
+
event: T,
|
|
98
|
+
eventPayload: TEvents[T]
|
|
99
|
+
): void {
|
|
100
|
+
this.listeners.get(event)?.forEach((listener) => {
|
|
101
|
+
try {
|
|
102
|
+
listener(eventPayload)
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Re-throw in a microtask to surface the error
|
|
105
|
+
queueMicrotask(() => {
|
|
106
|
+
throw error
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Clear all listeners
|
|
114
|
+
*/
|
|
115
|
+
protected clearListeners(): void {
|
|
116
|
+
this.listeners.clear()
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/local-only.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BaseCollectionConfig,
|
|
3
3
|
CollectionConfig,
|
|
4
|
-
DeleteMutationFn,
|
|
5
4
|
DeleteMutationFnParams,
|
|
6
5
|
InferSchemaOutput,
|
|
7
|
-
InsertMutationFn,
|
|
8
6
|
InsertMutationFnParams,
|
|
9
7
|
OperationType,
|
|
10
8
|
PendingMutation,
|
|
11
9
|
SyncConfig,
|
|
12
|
-
UpdateMutationFn,
|
|
13
10
|
UpdateMutationFnParams,
|
|
14
11
|
UtilsRecord,
|
|
15
12
|
} from "./types"
|
|
@@ -67,13 +64,7 @@ type LocalOnlyCollectionOptionsResult<
|
|
|
67
64
|
T extends object,
|
|
68
65
|
TKey extends string | number,
|
|
69
66
|
TSchema extends StandardSchemaV1 | never = never,
|
|
70
|
-
> =
|
|
71
|
-
CollectionConfig<T, TKey, TSchema>,
|
|
72
|
-
`onInsert` | `onUpdate` | `onDelete`
|
|
73
|
-
> & {
|
|
74
|
-
onInsert?: InsertMutationFn<T, TKey, LocalOnlyCollectionUtils>
|
|
75
|
-
onUpdate?: UpdateMutationFn<T, TKey, LocalOnlyCollectionUtils>
|
|
76
|
-
onDelete?: DeleteMutationFn<T, TKey, LocalOnlyCollectionUtils>
|
|
67
|
+
> = CollectionConfig<T, TKey, TSchema> & {
|
|
77
68
|
utils: LocalOnlyCollectionUtils
|
|
78
69
|
}
|
|
79
70
|
|
|
@@ -191,7 +182,7 @@ export function localOnlyCollectionOptions<
|
|
|
191
182
|
const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config
|
|
192
183
|
|
|
193
184
|
// Create the sync configuration with transaction confirmation capability
|
|
194
|
-
const syncResult = createLocalOnlySync(initialData)
|
|
185
|
+
const syncResult = createLocalOnlySync<T, TKey>(initialData)
|
|
195
186
|
|
|
196
187
|
/**
|
|
197
188
|
* Create wrapper handlers that call user handlers first, then confirm transactions
|
|
@@ -279,9 +270,11 @@ export function localOnlyCollectionOptions<
|
|
|
279
270
|
onDelete: wrappedOnDelete,
|
|
280
271
|
utils: {
|
|
281
272
|
acceptMutations,
|
|
282
|
-
}
|
|
273
|
+
},
|
|
283
274
|
startSync: true,
|
|
284
275
|
gcTime: 0,
|
|
276
|
+
} as LocalOnlyCollectionOptionsResult<T, TKey, TSchema> & {
|
|
277
|
+
schema?: StandardSchemaV1
|
|
285
278
|
}
|
|
286
279
|
}
|
|
287
280
|
|
|
@@ -107,7 +107,7 @@ export type SchemaFromSource<T extends Source> = Prettify<{
|
|
|
107
107
|
* GetAliases - Extracts all table aliases available in a query context
|
|
108
108
|
*
|
|
109
109
|
* Simple utility type that returns the keys of the schema, representing
|
|
110
|
-
* all table/
|
|
110
|
+
* all table/source aliases that can be referenced in the current query.
|
|
111
111
|
*/
|
|
112
112
|
export type GetAliases<TContext extends Context> = keyof TContext[`schema`]
|
|
113
113
|
|