@tanstack/db 0.4.9 → 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 +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 +32 -6
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +13 -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 +13 -1
- package/dist/esm/query/live/collection-config-builder.js +33 -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 +56 -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/events.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventEmitter } from "../event-emitter.js"
|
|
1
2
|
import type { Collection } from "./index.js"
|
|
2
3
|
import type { CollectionStatus } from "../types.js"
|
|
3
4
|
|
|
@@ -31,9 +32,21 @@ export interface CollectionSubscribersChangeEvent {
|
|
|
31
32
|
subscriberCount: number
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Event emitted when the collection's loading more state changes
|
|
37
|
+
*/
|
|
38
|
+
export interface CollectionLoadingSubsetChangeEvent {
|
|
39
|
+
type: `loadingSubset:change`
|
|
40
|
+
collection: Collection<any, any, any, any, any>
|
|
41
|
+
isLoadingSubset: boolean
|
|
42
|
+
previousIsLoadingSubset: boolean
|
|
43
|
+
loadingSubsetTransition: `start` | `end`
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
export type AllCollectionEvents = {
|
|
35
47
|
"status:change": CollectionStatusChangeEvent
|
|
36
48
|
"subscribers:change": CollectionSubscribersChangeEvent
|
|
49
|
+
"loadingSubset:change": CollectionLoadingSubsetChangeEvent
|
|
37
50
|
} & {
|
|
38
51
|
[K in CollectionStatus as `status:${K}`]: CollectionStatusEvent<K>
|
|
39
52
|
}
|
|
@@ -42,94 +55,32 @@ export type CollectionEvent =
|
|
|
42
55
|
| AllCollectionEvents[keyof AllCollectionEvents]
|
|
43
56
|
| CollectionStatusChangeEvent
|
|
44
57
|
| CollectionSubscribersChangeEvent
|
|
58
|
+
| CollectionLoadingSubsetChangeEvent
|
|
45
59
|
|
|
46
60
|
export type CollectionEventHandler<T extends keyof AllCollectionEvents> = (
|
|
47
61
|
event: AllCollectionEvents[T]
|
|
48
62
|
) => void
|
|
49
63
|
|
|
50
|
-
export class CollectionEventsManager {
|
|
64
|
+
export class CollectionEventsManager extends EventEmitter<AllCollectionEvents> {
|
|
51
65
|
private collection!: Collection<any, any, any, any, any>
|
|
52
|
-
private listeners = new Map<
|
|
53
|
-
keyof AllCollectionEvents,
|
|
54
|
-
Set<CollectionEventHandler<any>>
|
|
55
|
-
>()
|
|
56
66
|
|
|
57
|
-
constructor() {
|
|
67
|
+
constructor() {
|
|
68
|
+
super()
|
|
69
|
+
}
|
|
58
70
|
|
|
59
71
|
setDeps(deps: { collection: Collection<any, any, any, any, any> }) {
|
|
60
72
|
this.collection = deps.collection
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!this.listeners.has(event)) {
|
|
68
|
-
this.listeners.set(event, new Set())
|
|
69
|
-
}
|
|
70
|
-
this.listeners.get(event)!.add(callback)
|
|
71
|
-
|
|
72
|
-
return () => {
|
|
73
|
-
this.listeners.get(event)?.delete(callback)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
once<T extends keyof AllCollectionEvents>(
|
|
78
|
-
event: T,
|
|
79
|
-
callback: CollectionEventHandler<T>
|
|
80
|
-
) {
|
|
81
|
-
const unsubscribe = this.on(event, (eventPayload) => {
|
|
82
|
-
callback(eventPayload)
|
|
83
|
-
unsubscribe()
|
|
84
|
-
})
|
|
85
|
-
return unsubscribe
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
off<T extends keyof AllCollectionEvents>(
|
|
89
|
-
event: T,
|
|
90
|
-
callback: CollectionEventHandler<T>
|
|
91
|
-
) {
|
|
92
|
-
this.listeners.get(event)?.delete(callback)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
waitFor<T extends keyof AllCollectionEvents>(
|
|
96
|
-
event: T,
|
|
97
|
-
timeout?: number
|
|
98
|
-
): Promise<AllCollectionEvents[T]> {
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
let timeoutId: NodeJS.Timeout | undefined
|
|
101
|
-
const unsubscribe = this.on(event, (eventPayload) => {
|
|
102
|
-
if (timeoutId) {
|
|
103
|
-
clearTimeout(timeoutId)
|
|
104
|
-
timeoutId = undefined
|
|
105
|
-
}
|
|
106
|
-
resolve(eventPayload)
|
|
107
|
-
unsubscribe()
|
|
108
|
-
})
|
|
109
|
-
if (timeout) {
|
|
110
|
-
timeoutId = setTimeout(() => {
|
|
111
|
-
timeoutId = undefined
|
|
112
|
-
unsubscribe()
|
|
113
|
-
reject(new Error(`Timeout waiting for event ${event}`))
|
|
114
|
-
}, timeout)
|
|
115
|
-
}
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Emit an event to all listeners
|
|
77
|
+
* Public API for emitting collection events
|
|
78
|
+
*/
|
|
119
79
|
emit<T extends keyof AllCollectionEvents>(
|
|
120
80
|
event: T,
|
|
121
81
|
eventPayload: AllCollectionEvents[T]
|
|
122
|
-
) {
|
|
123
|
-
this.
|
|
124
|
-
try {
|
|
125
|
-
listener(eventPayload)
|
|
126
|
-
} catch (error) {
|
|
127
|
-
// Re-throw in a microtask to surface the error
|
|
128
|
-
queueMicrotask(() => {
|
|
129
|
-
throw error
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
})
|
|
82
|
+
): void {
|
|
83
|
+
this.emitInner(event, eventPayload)
|
|
133
84
|
}
|
|
134
85
|
|
|
135
86
|
emitStatusChange<T extends CollectionStatus>(
|
|
@@ -166,6 +117,6 @@ export class CollectionEventsManager {
|
|
|
166
117
|
}
|
|
167
118
|
|
|
168
119
|
cleanup() {
|
|
169
|
-
this.
|
|
120
|
+
this.clearListeners()
|
|
170
121
|
}
|
|
171
122
|
}
|
package/src/collection/index.ts
CHANGED
|
@@ -25,7 +25,6 @@ import type {
|
|
|
25
25
|
InferSchemaOutput,
|
|
26
26
|
InsertConfig,
|
|
27
27
|
NonSingleResult,
|
|
28
|
-
OnLoadMoreOptions,
|
|
29
28
|
OperationConfig,
|
|
30
29
|
SingleResult,
|
|
31
30
|
SubscribeChangesOptions,
|
|
@@ -48,7 +47,7 @@ import type { IndexProxy } from "../indexes/lazy-index.js"
|
|
|
48
47
|
export interface Collection<
|
|
49
48
|
T extends object = Record<string, unknown>,
|
|
50
49
|
TKey extends string | number = string | number,
|
|
51
|
-
TUtils extends UtilsRecord =
|
|
50
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
52
51
|
TSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
53
52
|
TInsertInput extends object = T,
|
|
54
53
|
> extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {
|
|
@@ -131,7 +130,7 @@ export interface Collection<
|
|
|
131
130
|
export function createCollection<
|
|
132
131
|
T extends StandardSchemaV1,
|
|
133
132
|
TKey extends string | number = string | number,
|
|
134
|
-
TUtils extends UtilsRecord =
|
|
133
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
135
134
|
>(
|
|
136
135
|
options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
|
|
137
136
|
schema: T
|
|
@@ -144,7 +143,7 @@ export function createCollection<
|
|
|
144
143
|
export function createCollection<
|
|
145
144
|
T extends StandardSchemaV1,
|
|
146
145
|
TKey extends string | number = string | number,
|
|
147
|
-
TUtils extends UtilsRecord =
|
|
146
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
148
147
|
>(
|
|
149
148
|
options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
|
|
150
149
|
schema: T
|
|
@@ -158,7 +157,7 @@ export function createCollection<
|
|
|
158
157
|
export function createCollection<
|
|
159
158
|
T extends object,
|
|
160
159
|
TKey extends string | number = string | number,
|
|
161
|
-
TUtils extends UtilsRecord =
|
|
160
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
162
161
|
>(
|
|
163
162
|
options: CollectionConfig<T, TKey, never> & {
|
|
164
163
|
schema?: never // prohibit schema if an explicit type is provided
|
|
@@ -171,7 +170,7 @@ export function createCollection<
|
|
|
171
170
|
export function createCollection<
|
|
172
171
|
T extends object,
|
|
173
172
|
TKey extends string | number = string | number,
|
|
174
|
-
TUtils extends UtilsRecord =
|
|
173
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
175
174
|
>(
|
|
176
175
|
options: CollectionConfig<T, TKey, never> & {
|
|
177
176
|
schema?: never // prohibit schema if an explicit type is provided
|
|
@@ -218,7 +217,7 @@ export class CollectionImpl<
|
|
|
218
217
|
private _events: CollectionEventsManager
|
|
219
218
|
private _changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
|
|
220
219
|
public _lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
|
|
221
|
-
|
|
220
|
+
public _sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
|
|
222
221
|
private _indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
|
|
223
222
|
private _mutations: CollectionMutationsManager<
|
|
224
223
|
TOutput,
|
|
@@ -303,6 +302,7 @@ export class CollectionImpl<
|
|
|
303
302
|
collection: this, // Required for passing to config.sync callback
|
|
304
303
|
state: this._state,
|
|
305
304
|
lifecycle: this._lifecycle,
|
|
305
|
+
events: this._events,
|
|
306
306
|
})
|
|
307
307
|
|
|
308
308
|
// Only start sync immediately if explicitly enabled
|
|
@@ -356,23 +356,19 @@ export class CollectionImpl<
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
359
|
+
* Check if the collection is currently loading more data
|
|
360
|
+
* @returns true if the collection has pending load more operations, false otherwise
|
|
361
361
|
*/
|
|
362
|
-
public
|
|
363
|
-
this._sync.
|
|
362
|
+
public get isLoadingSubset(): boolean {
|
|
363
|
+
return this._sync.isLoadingSubset
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
|
|
370
|
-
* If data loading is synchronous, the data is loaded when the method returns.
|
|
367
|
+
* Start sync immediately - internal method for compiled queries
|
|
368
|
+
* This bypasses lazy loading for special cases like live query results
|
|
371
369
|
*/
|
|
372
|
-
public
|
|
373
|
-
|
|
374
|
-
return this._sync.syncOnLoadMoreFn(options)
|
|
375
|
-
}
|
|
370
|
+
public startSyncImmediate(): void {
|
|
371
|
+
this._sync.startSync()
|
|
376
372
|
}
|
|
377
373
|
|
|
378
374
|
/**
|
|
@@ -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
|
@@ -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
|
+
}
|