@tanstack/db 0.4.9 → 0.4.11
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 +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +6 -0
- 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 +1 -0
- 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/compiler/index.cjs +6 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +3 -2
- package/dist/cjs/query/compiler/joins.cjs +6 -3
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +2 -2
- package/dist/cjs/query/compiler/order-by.cjs +18 -4
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +2 -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 +43 -6
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +27 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +29 -0
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +1 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +2 -2
- 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 +6 -0
- package/dist/esm/errors.js +8 -0
- 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 +2 -1
- package/dist/esm/local-only.d.ts +2 -5
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +3 -2
- package/dist/esm/query/compiler/index.js +6 -2
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +2 -2
- package/dist/esm/query/compiler/joins.js +6 -3
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +2 -1
- package/dist/esm/query/compiler/order-by.js +18 -4
- package/dist/esm/query/compiler/order-by.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 +27 -1
- package/dist/esm/query/live/collection-config-builder.js +44 -7
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +1 -0
- package/dist/esm/query/live/collection-subscriber.js +29 -0
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +2 -2
- package/dist/esm/query/live-query-collection.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 +12 -0
- package/src/event-emitter.ts +118 -0
- package/src/local-only.ts +5 -12
- package/src/query/compiler/index.ts +9 -1
- package/src/query/compiler/joins.ts +7 -1
- package/src/query/compiler/order-by.ts +23 -2
- package/src/query/compiler/types.ts +5 -0
- package/src/query/index.ts +1 -0
- package/src/query/live/collection-config-builder.ts +76 -7
- package/src/query/live/collection-subscriber.ts +50 -0
- package/src/query/live-query-collection.ts +8 -4
- package/src/types.ts +93 -11
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
|
@@ -629,3 +629,15 @@ export class MissingAliasInputsError extends QueryCompilationError {
|
|
|
629
629
|
)
|
|
630
630
|
}
|
|
631
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
|
|
|
@@ -28,7 +28,9 @@ import type {
|
|
|
28
28
|
NamespacedAndKeyedStream,
|
|
29
29
|
ResultStream,
|
|
30
30
|
} from "../../types.js"
|
|
31
|
-
import type { QueryCache, QueryMapping } from "./types.js"
|
|
31
|
+
import type { QueryCache, QueryMapping, WindowOptions } from "./types.js"
|
|
32
|
+
|
|
33
|
+
export type { WindowOptions } from "./types.js"
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Result of query compilation including both the pipeline and source-specific WHERE clauses
|
|
@@ -87,6 +89,7 @@ export function compileQuery(
|
|
|
87
89
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
88
90
|
lazySources: Set<string>,
|
|
89
91
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
92
|
+
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
90
93
|
cache: QueryCache = new WeakMap(),
|
|
91
94
|
queryMapping: QueryMapping = new WeakMap()
|
|
92
95
|
): CompilationResult {
|
|
@@ -134,6 +137,7 @@ export function compileQuery(
|
|
|
134
137
|
callbacks,
|
|
135
138
|
lazySources,
|
|
136
139
|
optimizableOrderByCollections,
|
|
140
|
+
setWindowFn,
|
|
137
141
|
cache,
|
|
138
142
|
queryMapping,
|
|
139
143
|
aliasToCollectionId,
|
|
@@ -169,6 +173,7 @@ export function compileQuery(
|
|
|
169
173
|
callbacks,
|
|
170
174
|
lazySources,
|
|
171
175
|
optimizableOrderByCollections,
|
|
176
|
+
setWindowFn,
|
|
172
177
|
rawQuery,
|
|
173
178
|
compileQuery,
|
|
174
179
|
aliasToCollectionId,
|
|
@@ -311,6 +316,7 @@ export function compileQuery(
|
|
|
311
316
|
query.select || {},
|
|
312
317
|
collections[mainCollectionId]!,
|
|
313
318
|
optimizableOrderByCollections,
|
|
319
|
+
setWindowFn,
|
|
314
320
|
query.limit,
|
|
315
321
|
query.offset
|
|
316
322
|
)
|
|
@@ -381,6 +387,7 @@ function processFrom(
|
|
|
381
387
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
382
388
|
lazySources: Set<string>,
|
|
383
389
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
390
|
+
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
384
391
|
cache: QueryCache,
|
|
385
392
|
queryMapping: QueryMapping,
|
|
386
393
|
aliasToCollectionId: Record<string, string>,
|
|
@@ -412,6 +419,7 @@ function processFrom(
|
|
|
412
419
|
callbacks,
|
|
413
420
|
lazySources,
|
|
414
421
|
optimizableOrderByCollections,
|
|
422
|
+
setWindowFn,
|
|
415
423
|
cache,
|
|
416
424
|
queryMapping
|
|
417
425
|
)
|
|
@@ -31,7 +31,7 @@ import type {
|
|
|
31
31
|
NamespacedAndKeyedStream,
|
|
32
32
|
NamespacedRow,
|
|
33
33
|
} from "../../types.js"
|
|
34
|
-
import type { QueryCache, QueryMapping } from "./types.js"
|
|
34
|
+
import type { QueryCache, QueryMapping, WindowOptions } from "./types.js"
|
|
35
35
|
import type { CollectionSubscription } from "../../collection/subscription.js"
|
|
36
36
|
|
|
37
37
|
/** Function type for loading specific keys into a lazy collection */
|
|
@@ -61,6 +61,7 @@ export function processJoins(
|
|
|
61
61
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
62
62
|
lazySources: Set<string>,
|
|
63
63
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
64
|
+
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
64
65
|
rawQuery: QueryIR,
|
|
65
66
|
onCompileSubquery: CompileQueryFn,
|
|
66
67
|
aliasToCollectionId: Record<string, string>,
|
|
@@ -83,6 +84,7 @@ export function processJoins(
|
|
|
83
84
|
callbacks,
|
|
84
85
|
lazySources,
|
|
85
86
|
optimizableOrderByCollections,
|
|
87
|
+
setWindowFn,
|
|
86
88
|
rawQuery,
|
|
87
89
|
onCompileSubquery,
|
|
88
90
|
aliasToCollectionId,
|
|
@@ -111,6 +113,7 @@ function processJoin(
|
|
|
111
113
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
112
114
|
lazySources: Set<string>,
|
|
113
115
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
116
|
+
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
114
117
|
rawQuery: QueryIR,
|
|
115
118
|
onCompileSubquery: CompileQueryFn,
|
|
116
119
|
aliasToCollectionId: Record<string, string>,
|
|
@@ -131,6 +134,7 @@ function processJoin(
|
|
|
131
134
|
callbacks,
|
|
132
135
|
lazySources,
|
|
133
136
|
optimizableOrderByCollections,
|
|
137
|
+
setWindowFn,
|
|
134
138
|
cache,
|
|
135
139
|
queryMapping,
|
|
136
140
|
onCompileSubquery,
|
|
@@ -421,6 +425,7 @@ function processJoinSource(
|
|
|
421
425
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
422
426
|
lazySources: Set<string>,
|
|
423
427
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
428
|
+
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
424
429
|
cache: QueryCache,
|
|
425
430
|
queryMapping: QueryMapping,
|
|
426
431
|
onCompileSubquery: CompileQueryFn,
|
|
@@ -453,6 +458,7 @@ function processJoinSource(
|
|
|
453
458
|
callbacks,
|
|
454
459
|
lazySources,
|
|
455
460
|
optimizableOrderByCollections,
|
|
461
|
+
setWindowFn,
|
|
456
462
|
cache,
|
|
457
463
|
queryMapping
|
|
458
464
|
)
|
|
@@ -5,6 +5,7 @@ import { ensureIndexForField } from "../../indexes/auto-index.js"
|
|
|
5
5
|
import { findIndexForField } from "../../utils/index-optimization.js"
|
|
6
6
|
import { compileExpression } from "./evaluators.js"
|
|
7
7
|
import { replaceAggregatesByRefs } from "./group-by.js"
|
|
8
|
+
import type { WindowOptions } from "./types.js"
|
|
8
9
|
import type { CompiledSingleRowExpression } from "./evaluators.js"
|
|
9
10
|
import type { OrderBy, OrderByClause, QueryIR, Select } from "../ir.js"
|
|
10
11
|
import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
|
|
@@ -38,6 +39,7 @@ export function processOrderBy(
|
|
|
38
39
|
selectClause: Select,
|
|
39
40
|
collection: Collection,
|
|
40
41
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
42
|
+
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
41
43
|
limit?: number,
|
|
42
44
|
offset?: number
|
|
43
45
|
): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
|
|
@@ -107,6 +109,8 @@ export function processOrderBy(
|
|
|
107
109
|
|
|
108
110
|
let setSizeCallback: ((getSize: () => number) => void) | undefined
|
|
109
111
|
|
|
112
|
+
let orderByOptimizationInfo: OrderByOptimizationInfo | undefined
|
|
113
|
+
|
|
110
114
|
// Optimize the orderBy operator to lazily load elements
|
|
111
115
|
// by using the range index of the collection.
|
|
112
116
|
// Only for orderBy clause on a single column for now (no composite ordering)
|
|
@@ -161,7 +165,7 @@ export function processOrderBy(
|
|
|
161
165
|
? String(orderByExpression.path[0])
|
|
162
166
|
: rawQuery.from.alias
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
orderByOptimizationInfo = {
|
|
165
169
|
alias: orderByAlias,
|
|
166
170
|
offset: offset ?? 0,
|
|
167
171
|
limit,
|
|
@@ -179,7 +183,7 @@ export function processOrderBy(
|
|
|
179
183
|
...optimizableOrderByCollections[followRefCollection.id]!,
|
|
180
184
|
dataNeeded: () => {
|
|
181
185
|
const size = getSize()
|
|
182
|
-
return Math.max(0, limit - size)
|
|
186
|
+
return Math.max(0, orderByOptimizationInfo!.limit - size)
|
|
183
187
|
},
|
|
184
188
|
}
|
|
185
189
|
}
|
|
@@ -194,6 +198,23 @@ export function processOrderBy(
|
|
|
194
198
|
offset,
|
|
195
199
|
comparator: compare,
|
|
196
200
|
setSizeCallback,
|
|
201
|
+
setWindowFn: (
|
|
202
|
+
windowFn: (options: { offset?: number; limit?: number }) => void
|
|
203
|
+
) => {
|
|
204
|
+
setWindowFn(
|
|
205
|
+
// We wrap the move function such that we update the orderByOptimizationInfo
|
|
206
|
+
// because that is used by the `dataNeeded` callback to determine if we need to load more data
|
|
207
|
+
(options) => {
|
|
208
|
+
windowFn(options)
|
|
209
|
+
if (orderByOptimizationInfo) {
|
|
210
|
+
orderByOptimizationInfo.offset =
|
|
211
|
+
options.offset ?? orderByOptimizationInfo.offset
|
|
212
|
+
orderByOptimizationInfo.limit =
|
|
213
|
+
options.limit ?? orderByOptimizationInfo.limit
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
},
|
|
197
218
|
})
|
|
198
219
|
// orderByWithFractionalIndex returns [key, [value, index]] - we keep this format
|
|
199
220
|
)
|
|
@@ -10,3 +10,8 @@ export type QueryCache = WeakMap<QueryIR, CompilationResult>
|
|
|
10
10
|
* Mapping from optimized queries back to their original queries for caching
|
|
11
11
|
*/
|
|
12
12
|
export type QueryMapping = WeakMap<QueryIR, QueryIR>
|
|
13
|
+
|
|
14
|
+
export type WindowOptions = {
|
|
15
|
+
offset?: number
|
|
16
|
+
limit?: number
|
|
17
|
+
}
|
package/src/query/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { D2, output } from "@tanstack/db-ivm"
|
|
2
2
|
import { compileQuery } from "../compiler/index.js"
|
|
3
3
|
import { buildQuery, getQueryIR } from "../builder/index.js"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
MissingAliasInputsError,
|
|
6
|
+
SetWindowRequiresOrderByError,
|
|
7
|
+
} from "../../errors.js"
|
|
5
8
|
import { transactionScopedScheduler } from "../../scheduler.js"
|
|
6
9
|
import { getActiveTransaction } from "../../transactions.js"
|
|
7
10
|
import { CollectionSubscriber } from "./collection-subscriber.js"
|
|
8
11
|
import { getCollectionBuilder } from "./collection-registry.js"
|
|
12
|
+
import type { WindowOptions } from "../compiler/index.js"
|
|
9
13
|
import type { SchedulerContextId } from "../../scheduler.js"
|
|
10
14
|
import type { CollectionSubscription } from "../../collection/subscription.js"
|
|
11
15
|
import type { RootStreamBuilder } from "@tanstack/db-ivm"
|
|
@@ -32,6 +36,19 @@ import type { AllCollectionEvents } from "../../collection/events.js"
|
|
|
32
36
|
export type LiveQueryCollectionUtils = UtilsRecord & {
|
|
33
37
|
getRunCount: () => number
|
|
34
38
|
getBuilder: () => CollectionConfigBuilder<any, any>
|
|
39
|
+
/**
|
|
40
|
+
* Sets the offset and limit of an ordered query.
|
|
41
|
+
* Is a no-op if the query is not ordered.
|
|
42
|
+
*
|
|
43
|
+
* @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded
|
|
44
|
+
*/
|
|
45
|
+
setWindow: (options: WindowOptions) => true | Promise<void>
|
|
46
|
+
/**
|
|
47
|
+
* Gets the current window (offset and limit) for an ordered query.
|
|
48
|
+
*
|
|
49
|
+
* @returns The current window settings, or `undefined` if the query is not windowed
|
|
50
|
+
*/
|
|
51
|
+
getWindow: () => { offset: number; limit: number } | undefined
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
type PendingGraphRun = {
|
|
@@ -79,7 +96,12 @@ export class CollectionConfigBuilder<
|
|
|
79
96
|
private isInErrorState = false
|
|
80
97
|
|
|
81
98
|
// Reference to the live query collection for error state transitions
|
|
82
|
-
|
|
99
|
+
public liveQueryCollection?: Collection<TResult, any, any>
|
|
100
|
+
|
|
101
|
+
private windowFn: ((options: WindowOptions) => void) | undefined
|
|
102
|
+
private currentWindow: WindowOptions | undefined
|
|
103
|
+
|
|
104
|
+
private maybeRunGraphFn: (() => void) | undefined
|
|
83
105
|
|
|
84
106
|
private readonly aliasDependencies: Record<
|
|
85
107
|
string,
|
|
@@ -171,10 +193,52 @@ export class CollectionConfigBuilder<
|
|
|
171
193
|
utils: {
|
|
172
194
|
getRunCount: this.getRunCount.bind(this),
|
|
173
195
|
getBuilder: () => this,
|
|
196
|
+
setWindow: this.setWindow.bind(this),
|
|
197
|
+
getWindow: this.getWindow.bind(this),
|
|
174
198
|
},
|
|
175
199
|
}
|
|
176
200
|
}
|
|
177
201
|
|
|
202
|
+
setWindow(options: WindowOptions): true | Promise<void> {
|
|
203
|
+
if (!this.windowFn) {
|
|
204
|
+
throw new SetWindowRequiresOrderByError()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.currentWindow = options
|
|
208
|
+
this.windowFn(options)
|
|
209
|
+
this.maybeRunGraphFn?.()
|
|
210
|
+
|
|
211
|
+
// Check if loading a subset was triggered
|
|
212
|
+
if (this.liveQueryCollection?.isLoadingSubset) {
|
|
213
|
+
// Loading was triggered, return a promise that resolves when it completes
|
|
214
|
+
return new Promise<void>((resolve) => {
|
|
215
|
+
const unsubscribe = this.liveQueryCollection!.on(
|
|
216
|
+
`loadingSubset:change`,
|
|
217
|
+
(event) => {
|
|
218
|
+
if (!event.isLoadingSubset) {
|
|
219
|
+
unsubscribe()
|
|
220
|
+
resolve()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// No loading was triggered
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
getWindow(): { offset: number; limit: number } | undefined {
|
|
232
|
+
// Only return window if this is a windowed query (has orderBy and windowFn)
|
|
233
|
+
if (!this.windowFn || !this.currentWindow) {
|
|
234
|
+
return undefined
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
offset: this.currentWindow.offset ?? 0,
|
|
238
|
+
limit: this.currentWindow.limit ?? 0,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
178
242
|
/**
|
|
179
243
|
* Resolves a collection alias to its collection ID.
|
|
180
244
|
*
|
|
@@ -452,13 +516,15 @@ export class CollectionConfigBuilder<
|
|
|
452
516
|
}
|
|
453
517
|
)
|
|
454
518
|
|
|
455
|
-
const
|
|
519
|
+
const loadSubsetDataCallbacks = this.subscribeToAllCollections(
|
|
456
520
|
config,
|
|
457
521
|
fullSyncState
|
|
458
522
|
)
|
|
459
523
|
|
|
524
|
+
this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks)
|
|
525
|
+
|
|
460
526
|
// Initial run with callback to load more data if needed
|
|
461
|
-
this.scheduleGraphRun(
|
|
527
|
+
this.scheduleGraphRun(loadSubsetDataCallbacks)
|
|
462
528
|
|
|
463
529
|
// Return the unsubscribe function
|
|
464
530
|
return () => {
|
|
@@ -517,7 +583,10 @@ export class CollectionConfigBuilder<
|
|
|
517
583
|
this.subscriptions,
|
|
518
584
|
this.lazySourcesCallbacks,
|
|
519
585
|
this.lazySources,
|
|
520
|
-
this.optimizableOrderByCollections
|
|
586
|
+
this.optimizableOrderByCollections,
|
|
587
|
+
(windowFn: (options: WindowOptions) => void) => {
|
|
588
|
+
this.windowFn = windowFn
|
|
589
|
+
}
|
|
521
590
|
)
|
|
522
591
|
|
|
523
592
|
this.pipelineCache = compilation.pipeline
|
|
@@ -764,7 +833,7 @@ export class CollectionConfigBuilder<
|
|
|
764
833
|
// Combine all loaders into a single callback that initiates loading more data
|
|
765
834
|
// from any source that needs it. Returns true once all loaders have been called,
|
|
766
835
|
// but the actual async loading may still be in progress.
|
|
767
|
-
const
|
|
836
|
+
const loadSubsetDataCallbacks = () => {
|
|
768
837
|
loaders.map((loader) => loader())
|
|
769
838
|
return true
|
|
770
839
|
}
|
|
@@ -776,7 +845,7 @@ export class CollectionConfigBuilder<
|
|
|
776
845
|
// Initial status check after all subscriptions are set up
|
|
777
846
|
this.updateLiveQueryStatus(config)
|
|
778
847
|
|
|
779
|
-
return
|
|
848
|
+
return loadSubsetDataCallbacks
|
|
780
849
|
}
|
|
781
850
|
}
|
|
782
851
|
|