@tanstack/db 0.5.11 → 0.5.13
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/SortedMap.cjs +40 -26
- package/dist/cjs/SortedMap.cjs.map +1 -1
- package/dist/cjs/SortedMap.d.cts +10 -15
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/changes.cjs +2 -0
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +12 -4
- package/dist/cjs/collection/index.cjs +2 -1
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/indexes.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/mutations.cjs +5 -2
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/collection/state.cjs +6 -5
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +4 -1
- package/dist/cjs/collection/subscription.cjs +91 -57
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +26 -4
- package/dist/cjs/collection/sync.cjs +11 -6
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/errors.cjs +9 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/event-emitter.cjs.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.cjs +8 -6
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/lazy-index.cjs.map +1 -1
- package/dist/cjs/indexes/reverse-index.cjs.map +1 -1
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/optimistic-action.cjs.map +1 -1
- package/dist/cjs/paced-mutations.cjs.map +1 -1
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +91 -38
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +6 -2
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/expression-helpers.cjs.map +1 -1
- package/dist/cjs/query/index.d.cts +1 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +30 -15
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/internal.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/predicate-utils.cjs +19 -2
- package/dist/cjs/query/predicate-utils.cjs.map +1 -1
- package/dist/cjs/query/predicate-utils.d.cts +32 -1
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
- package/dist/cjs/scheduler.cjs.map +1 -1
- package/dist/cjs/strategies/debounceStrategy.cjs.map +1 -1
- package/dist/cjs/strategies/queueStrategy.cjs.map +1 -1
- package/dist/cjs/strategies/throttleStrategy.cjs.map +1 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +53 -8
- package/dist/cjs/utils/browser-polyfills.cjs.map +1 -1
- package/dist/cjs/utils/btree.cjs.map +1 -1
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/cursor.cjs +39 -0
- package/dist/cjs/utils/cursor.cjs.map +1 -0
- package/dist/cjs/utils/cursor.d.cts +18 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/SortedMap.d.ts +10 -15
- package/dist/esm/SortedMap.js +40 -26
- package/dist/esm/SortedMap.js.map +1 -1
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/changes.js +2 -0
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/events.d.ts +12 -4
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.js +2 -1
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/indexes.js.map +1 -1
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/mutations.js +6 -3
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/collection/state.d.ts +4 -1
- package/dist/esm/collection/state.js +6 -5
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +26 -4
- package/dist/esm/collection/subscription.js +92 -58
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.js +11 -6
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +9 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/event-emitter.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.js +8 -6
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/lazy-index.js.map +1 -1
- package/dist/esm/indexes/reverse-index.js.map +1 -1
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/optimistic-action.js.map +1 -1
- package/dist/esm/paced-mutations.js.map +1 -1
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +6 -2
- package/dist/esm/query/compiler/order-by.js +91 -38
- 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/expression-helpers.js.map +1 -1
- package/dist/esm/query/index.d.ts +1 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.js +30 -15
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live/internal.js.map +1 -1
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/query/predicate-utils.d.ts +32 -1
- package/dist/esm/query/predicate-utils.js +19 -2
- package/dist/esm/query/predicate-utils.js.map +1 -1
- package/dist/esm/query/subset-dedupe.js.map +1 -1
- package/dist/esm/scheduler.js.map +1 -1
- package/dist/esm/strategies/debounceStrategy.js.map +1 -1
- package/dist/esm/strategies/queueStrategy.js.map +1 -1
- package/dist/esm/strategies/throttleStrategy.js.map +1 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +53 -8
- package/dist/esm/utils/browser-polyfills.js.map +1 -1
- package/dist/esm/utils/btree.js.map +1 -1
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/cursor.d.ts +18 -0
- package/dist/esm/utils/cursor.js +39 -0
- package/dist/esm/utils/cursor.js.map +1 -0
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +30 -28
- package/src/SortedMap.ts +50 -31
- package/src/collection/change-events.ts +20 -20
- package/src/collection/changes.ts +16 -12
- package/src/collection/events.ts +20 -10
- package/src/collection/index.ts +47 -46
- package/src/collection/indexes.ts +14 -14
- package/src/collection/lifecycle.ts +16 -16
- package/src/collection/mutations.ts +25 -20
- package/src/collection/state.ts +43 -36
- package/src/collection/subscription.ts +171 -90
- package/src/collection/sync.ts +34 -22
- package/src/duplicate-instance-check.ts +1 -1
- package/src/errors.ts +49 -40
- package/src/event-emitter.ts +5 -5
- package/src/index.ts +21 -21
- package/src/indexes/auto-index.ts +11 -11
- package/src/indexes/base-index.ts +13 -13
- package/src/indexes/btree-index.ts +21 -17
- package/src/indexes/index-options.ts +3 -3
- package/src/indexes/lazy-index.ts +8 -8
- package/src/indexes/reverse-index.ts +5 -5
- package/src/local-only.ts +12 -12
- package/src/local-storage.ts +17 -17
- package/src/optimistic-action.ts +5 -5
- package/src/paced-mutations.ts +6 -6
- package/src/proxy.ts +43 -43
- package/src/query/builder/functions.ts +28 -28
- package/src/query/builder/index.ts +22 -22
- package/src/query/builder/ref-proxy.ts +4 -4
- package/src/query/builder/types.ts +8 -8
- package/src/query/compiler/evaluators.ts +9 -9
- package/src/query/compiler/expressions.ts +6 -6
- package/src/query/compiler/group-by.ts +24 -24
- package/src/query/compiler/index.ts +44 -44
- package/src/query/compiler/joins.ts +37 -37
- package/src/query/compiler/order-by.ts +170 -77
- package/src/query/compiler/select.ts +13 -13
- package/src/query/compiler/types.ts +2 -2
- package/src/query/expression-helpers.ts +16 -16
- package/src/query/index.ts +10 -9
- package/src/query/ir.ts +13 -13
- package/src/query/live/collection-config-builder.ts +53 -53
- package/src/query/live/collection-registry.ts +6 -6
- package/src/query/live/collection-subscriber.ts +87 -48
- package/src/query/live/internal.ts +1 -1
- package/src/query/live/types.ts +4 -4
- package/src/query/live-query-collection.ts +15 -15
- package/src/query/optimizer.ts +29 -29
- package/src/query/predicate-utils.ts +105 -50
- package/src/query/subset-dedupe.ts +6 -6
- package/src/scheduler.ts +3 -3
- package/src/strategies/debounceStrategy.ts +6 -6
- package/src/strategies/index.ts +4 -4
- package/src/strategies/queueStrategy.ts +5 -5
- package/src/strategies/throttleStrategy.ts +6 -6
- package/src/strategies/types.ts +2 -2
- package/src/transactions.ts +9 -9
- package/src/types.ts +76 -18
- package/src/utils/array-utils.ts +1 -1
- package/src/utils/browser-polyfills.ts +2 -2
- package/src/utils/btree.ts +22 -22
- package/src/utils/comparison.ts +3 -3
- package/src/utils/cursor.ts +78 -0
- package/src/utils/index-optimization.ts +14 -14
- package/src/utils.ts +4 -4
package/src/collection/state.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { deepEquals } from
|
|
2
|
-
import { SortedMap } from
|
|
3
|
-
import type { Transaction } from
|
|
4
|
-
import type { StandardSchemaV1 } from
|
|
1
|
+
import { deepEquals } from '../utils'
|
|
2
|
+
import { SortedMap } from '../SortedMap'
|
|
3
|
+
import type { Transaction } from '../transactions'
|
|
4
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
5
5
|
import type {
|
|
6
6
|
ChangeMessage,
|
|
7
7
|
CollectionConfig,
|
|
8
8
|
OptimisticChangeMessage,
|
|
9
|
-
} from
|
|
10
|
-
import type { CollectionImpl } from
|
|
11
|
-
import type { CollectionLifecycleManager } from
|
|
12
|
-
import type { CollectionChangesManager } from
|
|
13
|
-
import type { CollectionIndexesManager } from
|
|
9
|
+
} from '../types'
|
|
10
|
+
import type { CollectionImpl } from './index.js'
|
|
11
|
+
import type { CollectionLifecycleManager } from './lifecycle'
|
|
12
|
+
import type { CollectionChangesManager } from './changes'
|
|
13
|
+
import type { CollectionIndexesManager } from './indexes'
|
|
14
|
+
import type { CollectionEventsManager } from './events'
|
|
14
15
|
|
|
15
16
|
interface PendingSyncedTransaction<
|
|
16
17
|
T extends object = Record<string, unknown>,
|
|
@@ -37,13 +38,14 @@ export class CollectionStateManager<
|
|
|
37
38
|
public lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
|
|
38
39
|
public changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
|
|
39
40
|
public indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
|
|
41
|
+
private _events!: CollectionEventsManager
|
|
40
42
|
|
|
41
43
|
// Core state - make public for testing
|
|
42
44
|
public transactions: SortedMap<string, Transaction<any>>
|
|
43
45
|
public pendingSyncedTransactions: Array<
|
|
44
46
|
PendingSyncedTransaction<TOutput, TKey>
|
|
45
47
|
> = []
|
|
46
|
-
public syncedData:
|
|
48
|
+
public syncedData: SortedMap<TKey, TOutput>
|
|
47
49
|
public syncedMetadata = new Map<TKey, unknown>()
|
|
48
50
|
|
|
49
51
|
// Optimistic state tracking - make public for testing
|
|
@@ -66,15 +68,12 @@ export class CollectionStateManager<
|
|
|
66
68
|
constructor(config: CollectionConfig<TOutput, TKey, TSchema>) {
|
|
67
69
|
this.config = config
|
|
68
70
|
this.transactions = new SortedMap<string, Transaction<any>>((a, b) =>
|
|
69
|
-
a.compareCreatedAt(b)
|
|
71
|
+
a.compareCreatedAt(b),
|
|
70
72
|
)
|
|
71
73
|
|
|
72
|
-
// Set up data storage
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} else {
|
|
76
|
-
this.syncedData = new Map<TKey, TOutput>()
|
|
77
|
-
}
|
|
74
|
+
// Set up data storage - always use SortedMap for deterministic iteration.
|
|
75
|
+
// If a custom compare function is provided, use it; otherwise entries are sorted by key only.
|
|
76
|
+
this.syncedData = new SortedMap<TKey, TOutput>(config.compare)
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
setDeps(deps: {
|
|
@@ -82,11 +81,13 @@ export class CollectionStateManager<
|
|
|
82
81
|
lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
|
|
83
82
|
changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
|
|
84
83
|
indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
|
|
84
|
+
events: CollectionEventsManager
|
|
85
85
|
}) {
|
|
86
86
|
this.collection = deps.collection
|
|
87
87
|
this.lifecycle = deps.lifecycle
|
|
88
88
|
this.changes = deps.changes
|
|
89
89
|
this.indexes = deps.indexes
|
|
90
|
+
this._events = deps.events
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
/**
|
|
@@ -185,7 +186,7 @@ export class CollectionStateManager<
|
|
|
185
186
|
* Execute a callback for each entry in the collection
|
|
186
187
|
*/
|
|
187
188
|
public forEach(
|
|
188
|
-
callbackfn: (value: TOutput, key: TKey, index: number) => void
|
|
189
|
+
callbackfn: (value: TOutput, key: TKey, index: number) => void,
|
|
189
190
|
): void {
|
|
190
191
|
let index = 0
|
|
191
192
|
for (const [key, value] of this.entries()) {
|
|
@@ -197,7 +198,7 @@ export class CollectionStateManager<
|
|
|
197
198
|
* Create a new array with the results of calling a function for each entry in the collection
|
|
198
199
|
*/
|
|
199
200
|
public map<U>(
|
|
200
|
-
callbackfn: (value: TOutput, key: TKey, index: number) => U
|
|
201
|
+
callbackfn: (value: TOutput, key: TKey, index: number) => U,
|
|
201
202
|
): Array<U> {
|
|
202
203
|
const result: Array<U> = []
|
|
203
204
|
let index = 0
|
|
@@ -213,7 +214,7 @@ export class CollectionStateManager<
|
|
|
213
214
|
* @returns True if the given collection is this collection, false otherwise
|
|
214
215
|
*/
|
|
215
216
|
private isThisCollection(
|
|
216
|
-
collection: CollectionImpl<any, any, any, any, any
|
|
217
|
+
collection: CollectionImpl<any, any, any, any, any>,
|
|
217
218
|
): boolean {
|
|
218
219
|
return collection === this.collection
|
|
219
220
|
}
|
|
@@ -222,7 +223,7 @@ export class CollectionStateManager<
|
|
|
222
223
|
* Recompute optimistic state from active transactions
|
|
223
224
|
*/
|
|
224
225
|
public recomputeOptimisticState(
|
|
225
|
-
triggeredByUserAction: boolean = false
|
|
226
|
+
triggeredByUserAction: boolean = false,
|
|
226
227
|
): void {
|
|
227
228
|
// Skip redundant recalculations when we're in the middle of committing sync transactions
|
|
228
229
|
// While the sync pipeline is replaying a large batch we still want to honour
|
|
@@ -257,7 +258,7 @@ export class CollectionStateManager<
|
|
|
257
258
|
case `update`:
|
|
258
259
|
this.optimisticUpserts.set(
|
|
259
260
|
mutation.key,
|
|
260
|
-
mutation.modified as TOutput
|
|
261
|
+
mutation.modified as TOutput,
|
|
261
262
|
)
|
|
262
263
|
this.optimisticDeletes.delete(mutation.key)
|
|
263
264
|
break
|
|
@@ -316,8 +317,8 @@ export class CollectionStateManager<
|
|
|
316
317
|
// We can infer this by checking if we have no remaining optimistic mutations for this key
|
|
317
318
|
const hasActiveOptimisticMutation = activeTransactions.some((tx) =>
|
|
318
319
|
tx.mutations.some(
|
|
319
|
-
(m) => this.isThisCollection(m.collection) && m.key === event.key
|
|
320
|
-
)
|
|
320
|
+
(m) => this.isThisCollection(m.collection) && m.key === event.key,
|
|
321
|
+
),
|
|
321
322
|
)
|
|
322
323
|
|
|
323
324
|
if (!hasActiveOptimisticMutation) {
|
|
@@ -348,10 +349,10 @@ export class CollectionStateManager<
|
|
|
348
349
|
private calculateSize(): number {
|
|
349
350
|
const syncedSize = this.syncedData.size
|
|
350
351
|
const deletesFromSynced = Array.from(this.optimisticDeletes).filter(
|
|
351
|
-
(key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key)
|
|
352
|
+
(key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key),
|
|
352
353
|
).length
|
|
353
354
|
const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter(
|
|
354
|
-
(key) => !this.syncedData.has(key)
|
|
355
|
+
(key) => !this.syncedData.has(key),
|
|
355
356
|
).length
|
|
356
357
|
|
|
357
358
|
return syncedSize - deletesFromSynced + upsertsNotInSynced
|
|
@@ -363,7 +364,7 @@ export class CollectionStateManager<
|
|
|
363
364
|
private collectOptimisticChanges(
|
|
364
365
|
previousUpserts: Map<TKey, TOutput>,
|
|
365
366
|
previousDeletes: Set<TKey>,
|
|
366
|
-
events: Array<ChangeMessage<TOutput, TKey
|
|
367
|
+
events: Array<ChangeMessage<TOutput, TKey>>,
|
|
367
368
|
): void {
|
|
368
369
|
const allKeys = new Set([
|
|
369
370
|
...previousUpserts.keys(),
|
|
@@ -377,7 +378,7 @@ export class CollectionStateManager<
|
|
|
377
378
|
const previousValue = this.getPreviousValue(
|
|
378
379
|
key,
|
|
379
380
|
previousUpserts,
|
|
380
|
-
previousDeletes
|
|
381
|
+
previousDeletes,
|
|
381
382
|
)
|
|
382
383
|
|
|
383
384
|
if (previousValue !== undefined && currentValue === undefined) {
|
|
@@ -405,7 +406,7 @@ export class CollectionStateManager<
|
|
|
405
406
|
private getPreviousValue(
|
|
406
407
|
key: TKey,
|
|
407
408
|
previousUpserts: Map<TKey, TOutput>,
|
|
408
|
-
previousDeletes: Set<TKey
|
|
409
|
+
previousDeletes: Set<TKey>,
|
|
409
410
|
): TOutput | undefined {
|
|
410
411
|
if (previousDeletes.has(key)) {
|
|
411
412
|
return undefined
|
|
@@ -456,7 +457,7 @@ export class CollectionStateManager<
|
|
|
456
457
|
PendingSyncedTransaction<TOutput, TKey>
|
|
457
458
|
>,
|
|
458
459
|
hasTruncateSync: false,
|
|
459
|
-
}
|
|
460
|
+
},
|
|
460
461
|
)
|
|
461
462
|
|
|
462
463
|
if (!hasPersistingTransaction || hasTruncateSync) {
|
|
@@ -528,6 +529,12 @@ export class CollectionStateManager<
|
|
|
528
529
|
for (const key of changedKeys) {
|
|
529
530
|
currentVisibleState.delete(key)
|
|
530
531
|
}
|
|
532
|
+
|
|
533
|
+
// 4) Emit truncate event so subscriptions can reset their cursor tracking state
|
|
534
|
+
this._events.emit(`truncate`, {
|
|
535
|
+
type: `truncate`,
|
|
536
|
+
collection: this.collection,
|
|
537
|
+
})
|
|
531
538
|
}
|
|
532
539
|
|
|
533
540
|
for (const operation of transaction.operations) {
|
|
@@ -545,8 +552,8 @@ export class CollectionStateManager<
|
|
|
545
552
|
Object.assign(
|
|
546
553
|
{},
|
|
547
554
|
this.syncedMetadata.get(key),
|
|
548
|
-
operation.metadata
|
|
549
|
-
)
|
|
555
|
+
operation.metadata,
|
|
556
|
+
),
|
|
550
557
|
)
|
|
551
558
|
break
|
|
552
559
|
case `delete`:
|
|
@@ -564,7 +571,7 @@ export class CollectionStateManager<
|
|
|
564
571
|
const updatedValue = Object.assign(
|
|
565
572
|
{},
|
|
566
573
|
this.syncedData.get(key),
|
|
567
|
-
operation.value
|
|
574
|
+
operation.value,
|
|
568
575
|
)
|
|
569
576
|
this.syncedData.set(key, updatedValue)
|
|
570
577
|
} else {
|
|
@@ -597,10 +604,10 @@ export class CollectionStateManager<
|
|
|
597
604
|
// Build re-apply sets from the snapshot taken at the start of this function.
|
|
598
605
|
// This prevents losing optimistic state if transactions complete during truncate processing.
|
|
599
606
|
const reapplyUpserts = new Map<TKey, TOutput>(
|
|
600
|
-
truncateOptimisticSnapshot!.upserts
|
|
607
|
+
truncateOptimisticSnapshot!.upserts,
|
|
601
608
|
)
|
|
602
609
|
const reapplyDeletes = new Set<TKey>(
|
|
603
|
-
truncateOptimisticSnapshot!.deletes
|
|
610
|
+
truncateOptimisticSnapshot!.deletes,
|
|
604
611
|
)
|
|
605
612
|
|
|
606
613
|
// Emit inserts for re-applied upserts, skipping any keys that have an optimistic delete.
|
|
@@ -679,7 +686,7 @@ export class CollectionStateManager<
|
|
|
679
686
|
case `update`:
|
|
680
687
|
this.optimisticUpserts.set(
|
|
681
688
|
mutation.key,
|
|
682
|
-
mutation.modified as TOutput
|
|
689
|
+
mutation.modified as TOutput,
|
|
683
690
|
)
|
|
684
691
|
this.optimisticDeletes.delete(mutation.key)
|
|
685
692
|
break
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { ensureIndexForExpression } from
|
|
2
|
-
import { and, eq,
|
|
3
|
-
import { Value } from
|
|
4
|
-
import { EventEmitter } from
|
|
1
|
+
import { ensureIndexForExpression } from '../indexes/auto-index.js'
|
|
2
|
+
import { and, eq, gte, lt } from '../query/builder/functions.js'
|
|
3
|
+
import { Value } from '../query/ir.js'
|
|
4
|
+
import { EventEmitter } from '../event-emitter.js'
|
|
5
|
+
import { buildCursor } from '../utils/cursor.js'
|
|
5
6
|
import {
|
|
6
7
|
createFilterFunctionFromExpression,
|
|
7
8
|
createFilteredCallback,
|
|
8
|
-
} from
|
|
9
|
-
import type { BasicExpression, OrderBy } from
|
|
10
|
-
import type { IndexInterface } from
|
|
9
|
+
} from './change-events.js'
|
|
10
|
+
import type { BasicExpression, OrderBy } from '../query/ir.js'
|
|
11
|
+
import type { IndexInterface } from '../indexes/base-index.js'
|
|
11
12
|
import type {
|
|
12
13
|
ChangeMessage,
|
|
13
14
|
LoadSubsetOptions,
|
|
@@ -15,19 +16,26 @@ import type {
|
|
|
15
16
|
SubscriptionEvents,
|
|
16
17
|
SubscriptionStatus,
|
|
17
18
|
SubscriptionUnsubscribedEvent,
|
|
18
|
-
} from
|
|
19
|
-
import type { CollectionImpl } from
|
|
19
|
+
} from '../types.js'
|
|
20
|
+
import type { CollectionImpl } from './index.js'
|
|
20
21
|
|
|
21
22
|
type RequestSnapshotOptions = {
|
|
22
23
|
where?: BasicExpression<boolean>
|
|
23
24
|
optimizedOnly?: boolean
|
|
24
25
|
trackLoadSubsetPromise?: boolean
|
|
26
|
+
/** Optional orderBy to pass to loadSubset for backend optimization */
|
|
27
|
+
orderBy?: OrderBy
|
|
28
|
+
/** Optional limit to pass to loadSubset for backend optimization */
|
|
29
|
+
limit?: number
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
type RequestLimitedSnapshotOptions = {
|
|
28
33
|
orderBy: OrderBy
|
|
29
34
|
limit: number
|
|
30
|
-
|
|
35
|
+
/** All column values for cursor (first value used for local index, all values for sync layer) */
|
|
36
|
+
minValues?: Array<unknown>
|
|
37
|
+
/** Row offset for offset-based pagination (passed to sync layer) */
|
|
38
|
+
offset?: number
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
type CollectionSubscriptionOptions = {
|
|
@@ -44,6 +52,11 @@ export class CollectionSubscription
|
|
|
44
52
|
{
|
|
45
53
|
private loadedInitialState = false
|
|
46
54
|
|
|
55
|
+
// Flag to skip filtering in filterAndFlipChanges.
|
|
56
|
+
// This is separate from loadedInitialState because we want to allow
|
|
57
|
+
// requestSnapshot to still work even when filtering is skipped.
|
|
58
|
+
private skipFiltering = false
|
|
59
|
+
|
|
47
60
|
// Flag to indicate that we have sent at least 1 snapshot.
|
|
48
61
|
// While `snapshotSent` is false we filter out all changes from subscription to the collection.
|
|
49
62
|
private snapshotSent = false
|
|
@@ -57,6 +70,12 @@ export class CollectionSubscription
|
|
|
57
70
|
// Keep track of the keys we've sent (needed for join and orderBy optimizations)
|
|
58
71
|
private sentKeys = new Set<string | number>()
|
|
59
72
|
|
|
73
|
+
// Track the count of rows sent via requestLimitedSnapshot for offset-based pagination
|
|
74
|
+
private limitedSnapshotRowCount = 0
|
|
75
|
+
|
|
76
|
+
// Track the last key sent via requestLimitedSnapshot for cursor-based pagination
|
|
77
|
+
private lastSentKey: string | number | undefined
|
|
78
|
+
|
|
60
79
|
private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void
|
|
61
80
|
|
|
62
81
|
private orderByIndex: IndexInterface<string | number> | undefined
|
|
@@ -72,7 +91,7 @@ export class CollectionSubscription
|
|
|
72
91
|
constructor(
|
|
73
92
|
private collection: CollectionImpl<any, any, any, any, any>,
|
|
74
93
|
private callback: (changes: Array<ChangeMessage<any, any>>) => void,
|
|
75
|
-
private options: CollectionSubscriptionOptions
|
|
94
|
+
private options: CollectionSubscriptionOptions,
|
|
76
95
|
) {
|
|
77
96
|
super()
|
|
78
97
|
if (options.onUnsubscribe) {
|
|
@@ -85,7 +104,7 @@ export class CollectionSubscription
|
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
const callbackWithSentKeysTracking = (
|
|
88
|
-
changes: Array<ChangeMessage<any, any
|
|
107
|
+
changes: Array<ChangeMessage<any, any>>,
|
|
89
108
|
) => {
|
|
90
109
|
callback(changes)
|
|
91
110
|
this.trackSentKeys(changes)
|
|
@@ -203,6 +222,9 @@ export class CollectionSubscription
|
|
|
203
222
|
const loadOptions: LoadSubsetOptions = {
|
|
204
223
|
where: stateOpts.where,
|
|
205
224
|
subscription: this,
|
|
225
|
+
// Include orderBy and limit if provided so sync layer can optimize the query
|
|
226
|
+
orderBy: opts?.orderBy,
|
|
227
|
+
limit: opts?.limit,
|
|
206
228
|
}
|
|
207
229
|
const syncResult = this.collection._sync.loadSubset(loadOptions)
|
|
208
230
|
|
|
@@ -224,35 +246,53 @@ export class CollectionSubscription
|
|
|
224
246
|
|
|
225
247
|
// Only send changes that have not been sent yet
|
|
226
248
|
const filteredSnapshot = snapshot.filter(
|
|
227
|
-
(change) => !this.sentKeys.has(change.key)
|
|
249
|
+
(change) => !this.sentKeys.has(change.key),
|
|
228
250
|
)
|
|
229
251
|
|
|
252
|
+
// Add keys to sentKeys BEFORE calling callback to prevent race condition.
|
|
253
|
+
// If a change event arrives while the callback is executing, it will see
|
|
254
|
+
// the keys already in sentKeys and filter out duplicates correctly.
|
|
255
|
+
for (const change of filteredSnapshot) {
|
|
256
|
+
this.sentKeys.add(change.key)
|
|
257
|
+
}
|
|
258
|
+
|
|
230
259
|
this.snapshotSent = true
|
|
231
260
|
this.callback(filteredSnapshot)
|
|
232
261
|
return true
|
|
233
262
|
}
|
|
234
263
|
|
|
235
264
|
/**
|
|
236
|
-
* Sends a snapshot that fulfills the `where` clause and all rows are bigger or equal to
|
|
265
|
+
* Sends a snapshot that fulfills the `where` clause and all rows are bigger or equal to the cursor.
|
|
237
266
|
* Requires a range index to be set with `setOrderByIndex` prior to calling this method.
|
|
238
267
|
* It uses that range index to load the items in the order of the index.
|
|
239
|
-
*
|
|
268
|
+
*
|
|
269
|
+
* For multi-column orderBy:
|
|
270
|
+
* - Uses first value from `minValues` for LOCAL index operations (wide bounds, ensures no missed rows)
|
|
271
|
+
* - Uses all `minValues` to build a precise composite cursor for SYNC layer loadSubset
|
|
272
|
+
*
|
|
273
|
+
* Note 1: it may load more rows than the provided LIMIT because it loads all values equal to the first cursor value + limit values greater.
|
|
240
274
|
* This is needed to ensure that it does not accidentally skip duplicate values when the limit falls in the middle of some duplicated values.
|
|
241
275
|
* Note 2: it does not send keys that have already been sent before.
|
|
242
276
|
*/
|
|
243
277
|
requestLimitedSnapshot({
|
|
244
278
|
orderBy,
|
|
245
279
|
limit,
|
|
246
|
-
|
|
280
|
+
minValues,
|
|
281
|
+
offset,
|
|
247
282
|
}: RequestLimitedSnapshotOptions) {
|
|
248
283
|
if (!limit) throw new Error(`limit is required`)
|
|
249
284
|
|
|
250
285
|
if (!this.orderByIndex) {
|
|
251
286
|
throw new Error(
|
|
252
|
-
`Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot
|
|
287
|
+
`Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`,
|
|
253
288
|
)
|
|
254
289
|
}
|
|
255
290
|
|
|
291
|
+
// Derive first column value from minValues (used for local index operations)
|
|
292
|
+
const minValue = minValues?.[0]
|
|
293
|
+
// Cast for index operations (index expects string | number)
|
|
294
|
+
const minValueForIndex = minValue as string | number | undefined
|
|
295
|
+
|
|
256
296
|
const index = this.orderByIndex
|
|
257
297
|
const where = this.options.whereExpression
|
|
258
298
|
const whereFilterFn = where
|
|
@@ -272,7 +312,7 @@ export class CollectionSubscription
|
|
|
272
312
|
return whereFilterFn?.(value) ?? true
|
|
273
313
|
}
|
|
274
314
|
|
|
275
|
-
let biggestObservedValue =
|
|
315
|
+
let biggestObservedValue = minValueForIndex
|
|
276
316
|
const changes: Array<ChangeMessage<any, string | number>> = []
|
|
277
317
|
|
|
278
318
|
// If we have a minValue we need to handle the case
|
|
@@ -281,12 +321,16 @@ export class CollectionSubscription
|
|
|
281
321
|
// so if minValue is 3 then the previous snapshot may not have included all 3s
|
|
282
322
|
// e.g. if it was offset 0 and limit 3 it would only have loaded the first 3
|
|
283
323
|
// so we load all rows equal to minValue first, to be sure we don't skip any duplicate values
|
|
324
|
+
//
|
|
325
|
+
// For multi-column orderBy, we use the first column value for index operations (wide bounds)
|
|
326
|
+
// This may load some duplicates but ensures we never miss any rows.
|
|
284
327
|
let keys: Array<string | number> = []
|
|
285
|
-
if (
|
|
286
|
-
// First, get all items with the same value as minValue
|
|
328
|
+
if (minValueForIndex !== undefined) {
|
|
329
|
+
// First, get all items with the same FIRST COLUMN value as minValue
|
|
330
|
+
// This provides wide bounds for the local index
|
|
287
331
|
const { expression } = orderBy[0]!
|
|
288
332
|
const allRowsWithMinValue = this.collection.currentStateAsChanges({
|
|
289
|
-
where: eq(expression, new Value(
|
|
333
|
+
where: eq(expression, new Value(minValueForIndex)),
|
|
290
334
|
})
|
|
291
335
|
|
|
292
336
|
if (allRowsWithMinValue) {
|
|
@@ -300,15 +344,15 @@ export class CollectionSubscription
|
|
|
300
344
|
// Then get items greater than minValue
|
|
301
345
|
const keysGreaterThanMin = index.take(
|
|
302
346
|
limit - keys.length,
|
|
303
|
-
|
|
304
|
-
filterFn
|
|
347
|
+
minValueForIndex,
|
|
348
|
+
filterFn,
|
|
305
349
|
)
|
|
306
350
|
keys.push(...keysGreaterThanMin)
|
|
307
351
|
} else {
|
|
308
|
-
keys = index.take(limit,
|
|
352
|
+
keys = index.take(limit, minValueForIndex, filterFn)
|
|
309
353
|
}
|
|
310
354
|
} else {
|
|
311
|
-
keys = index.take(limit,
|
|
355
|
+
keys = index.take(limit, minValueForIndex, filterFn)
|
|
312
356
|
}
|
|
313
357
|
|
|
314
358
|
const valuesNeeded = () => Math.max(limit - changes.length, 0)
|
|
@@ -331,76 +375,82 @@ export class CollectionSubscription
|
|
|
331
375
|
keys = index.take(valuesNeeded(), biggestObservedValue, filterFn)
|
|
332
376
|
}
|
|
333
377
|
|
|
378
|
+
// Track row count for offset-based pagination (before sending to callback)
|
|
379
|
+
// Use the current count as the offset for this load
|
|
380
|
+
const currentOffset = this.limitedSnapshotRowCount
|
|
381
|
+
|
|
382
|
+
// Add keys to sentKeys BEFORE calling callback to prevent race condition.
|
|
383
|
+
// If a change event arrives while the callback is executing, it will see
|
|
384
|
+
// the keys already in sentKeys and filter out duplicates correctly.
|
|
385
|
+
for (const change of changes) {
|
|
386
|
+
this.sentKeys.add(change.key)
|
|
387
|
+
}
|
|
388
|
+
|
|
334
389
|
this.callback(changes)
|
|
335
390
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
391
|
+
// Update the row count and last key after sending (for next call's offset/cursor)
|
|
392
|
+
this.limitedSnapshotRowCount += changes.length
|
|
393
|
+
if (changes.length > 0) {
|
|
394
|
+
this.lastSentKey = changes[changes.length - 1]!.key
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Build cursor expressions for sync layer loadSubset
|
|
398
|
+
// The cursor expressions are separate from the main where clause
|
|
399
|
+
// so the sync layer can choose cursor-based or offset-based pagination
|
|
400
|
+
let cursorExpressions:
|
|
401
|
+
| {
|
|
402
|
+
whereFrom: BasicExpression<boolean>
|
|
403
|
+
whereCurrent: BasicExpression<boolean>
|
|
404
|
+
lastKey?: string | number
|
|
405
|
+
}
|
|
406
|
+
| undefined
|
|
407
|
+
|
|
408
|
+
if (minValues !== undefined && minValues.length > 0) {
|
|
409
|
+
const whereFromCursor = buildCursor(orderBy, minValues)
|
|
410
|
+
|
|
411
|
+
if (whereFromCursor) {
|
|
412
|
+
const { expression } = orderBy[0]!
|
|
413
|
+
const minValue = minValues[0]
|
|
414
|
+
|
|
415
|
+
// Build the whereCurrent expression for the first orderBy column
|
|
416
|
+
// For Date values, we need to handle precision differences between JS (ms) and backends (μs)
|
|
417
|
+
// A JS Date represents a 1ms range, so we query for all values within that range
|
|
418
|
+
let whereCurrentCursor: BasicExpression<boolean>
|
|
419
|
+
if (minValue instanceof Date) {
|
|
420
|
+
const minValuePlus1ms = new Date(minValue.getTime() + 1)
|
|
421
|
+
whereCurrentCursor = and(
|
|
422
|
+
gte(expression, new Value(minValue)),
|
|
423
|
+
lt(expression, new Value(minValuePlus1ms)),
|
|
424
|
+
)
|
|
425
|
+
} else {
|
|
426
|
+
whereCurrentCursor = eq(expression, new Value(minValue))
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
cursorExpressions = {
|
|
430
|
+
whereFrom: whereFromCursor,
|
|
431
|
+
whereCurrent: whereCurrentCursor,
|
|
432
|
+
lastKey: this.lastSentKey,
|
|
433
|
+
}
|
|
434
|
+
}
|
|
343
435
|
}
|
|
344
436
|
|
|
345
437
|
// Request the sync layer to load more data
|
|
346
438
|
// don't await it, we will load the data into the collection when it comes in
|
|
347
|
-
|
|
348
|
-
|
|
439
|
+
// Note: `where` does NOT include cursor expressions - they are passed separately
|
|
440
|
+
// The sync layer can choose to use cursor-based or offset-based pagination
|
|
441
|
+
const loadOptions: LoadSubsetOptions = {
|
|
442
|
+
where, // Main filter only, no cursor
|
|
349
443
|
limit,
|
|
350
444
|
orderBy,
|
|
445
|
+
cursor: cursorExpressions, // Cursor expressions passed separately
|
|
446
|
+
offset: offset ?? currentOffset, // Use provided offset, or auto-tracked offset
|
|
351
447
|
subscription: this,
|
|
352
448
|
}
|
|
353
|
-
const syncResult = this.collection._sync.loadSubset(
|
|
449
|
+
const syncResult = this.collection._sync.loadSubset(loadOptions)
|
|
354
450
|
|
|
355
451
|
// Track this loadSubset call
|
|
356
|
-
this.loadedSubsets.push(
|
|
357
|
-
|
|
358
|
-
// Make parallel loadSubset calls for values equal to minValue and values greater than minValue
|
|
359
|
-
const promises: Array<Promise<void>> = []
|
|
360
|
-
|
|
361
|
-
// First promise: load all values equal to minValue
|
|
362
|
-
if (typeof minValue !== `undefined`) {
|
|
363
|
-
const { expression } = orderBy[0]!
|
|
364
|
-
|
|
365
|
-
// For Date values, we need to handle precision differences between JS (ms) and backends (μs)
|
|
366
|
-
// A JS Date represents a 1ms range, so we query for all values within that range
|
|
367
|
-
let exactValueFilter
|
|
368
|
-
if (minValue instanceof Date) {
|
|
369
|
-
const minValuePlus1ms = new Date(minValue.getTime() + 1)
|
|
370
|
-
exactValueFilter = and(
|
|
371
|
-
gte(expression, new Value(minValue)),
|
|
372
|
-
lt(expression, new Value(minValuePlus1ms))
|
|
373
|
-
)
|
|
374
|
-
} else {
|
|
375
|
-
exactValueFilter = eq(expression, new Value(minValue))
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const loadOptions2: LoadSubsetOptions = {
|
|
379
|
-
where: exactValueFilter,
|
|
380
|
-
subscription: this,
|
|
381
|
-
}
|
|
382
|
-
const equalValueResult = this.collection._sync.loadSubset(loadOptions2)
|
|
383
|
-
|
|
384
|
-
// Track this loadSubset call
|
|
385
|
-
this.loadedSubsets.push(loadOptions2)
|
|
386
|
-
|
|
387
|
-
if (equalValueResult instanceof Promise) {
|
|
388
|
-
promises.push(equalValueResult)
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Second promise: load values greater than minValue
|
|
393
|
-
if (syncResult instanceof Promise) {
|
|
394
|
-
promises.push(syncResult)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Track the combined promise
|
|
398
|
-
if (promises.length > 0) {
|
|
399
|
-
const combinedPromise = Promise.all(promises).then(() => {})
|
|
400
|
-
this.trackLoadSubsetPromise(combinedPromise)
|
|
401
|
-
} else {
|
|
402
|
-
this.trackLoadSubsetPromise(syncResult)
|
|
403
|
-
}
|
|
452
|
+
this.loadedSubsets.push(loadOptions)
|
|
453
|
+
this.trackLoadSubsetPromise(syncResult)
|
|
404
454
|
}
|
|
405
455
|
|
|
406
456
|
// TODO: also add similar test but that checks that it can also load it from the collection's loadSubset function
|
|
@@ -410,10 +460,11 @@ export class CollectionSubscription
|
|
|
410
460
|
* Filters and flips changes for keys that have not been sent yet.
|
|
411
461
|
* Deletes are filtered out for keys that have not been sent yet.
|
|
412
462
|
* Updates are flipped into inserts for keys that have not been sent yet.
|
|
463
|
+
* Duplicate inserts are filtered out to prevent D2 multiplicity > 1.
|
|
413
464
|
*/
|
|
414
465
|
private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {
|
|
415
|
-
if (this.loadedInitialState) {
|
|
416
|
-
// We loaded the entire initial state
|
|
466
|
+
if (this.loadedInitialState || this.skipFiltering) {
|
|
467
|
+
// We loaded the entire initial state or filtering is explicitly skipped
|
|
417
468
|
// so no need to filter or flip changes
|
|
418
469
|
return changes
|
|
419
470
|
}
|
|
@@ -421,7 +472,9 @@ export class CollectionSubscription
|
|
|
421
472
|
const newChanges = []
|
|
422
473
|
for (const change of changes) {
|
|
423
474
|
let newChange = change
|
|
424
|
-
|
|
475
|
+
const keyInSentKeys = this.sentKeys.has(change.key)
|
|
476
|
+
|
|
477
|
+
if (!keyInSentKeys) {
|
|
425
478
|
if (change.type === `update`) {
|
|
426
479
|
newChange = { ...change, type: `insert`, previousValue: undefined }
|
|
427
480
|
} else if (change.type === `delete`) {
|
|
@@ -429,6 +482,19 @@ export class CollectionSubscription
|
|
|
429
482
|
continue
|
|
430
483
|
}
|
|
431
484
|
this.sentKeys.add(change.key)
|
|
485
|
+
} else {
|
|
486
|
+
// Key was already sent - handle based on change type
|
|
487
|
+
if (change.type === `insert`) {
|
|
488
|
+
// Filter out duplicate inserts - the key was already inserted.
|
|
489
|
+
// This prevents D2 multiplicity from going above 1, which would
|
|
490
|
+
// cause deletes to not properly remove items (multiplicity would
|
|
491
|
+
// go from 2 to 1 instead of 1 to 0).
|
|
492
|
+
continue
|
|
493
|
+
} else if (change.type === `delete`) {
|
|
494
|
+
// Remove from sentKeys so future inserts for this key are allowed
|
|
495
|
+
// (e.g., after truncate + reinsert)
|
|
496
|
+
this.sentKeys.delete(change.key)
|
|
497
|
+
}
|
|
432
498
|
}
|
|
433
499
|
newChanges.push(newChange)
|
|
434
500
|
}
|
|
@@ -436,17 +502,32 @@ export class CollectionSubscription
|
|
|
436
502
|
}
|
|
437
503
|
|
|
438
504
|
private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {
|
|
439
|
-
if (this.loadedInitialState) {
|
|
440
|
-
// No need to track sent keys if we loaded the entire state.
|
|
441
|
-
// Since
|
|
505
|
+
if (this.loadedInitialState || this.skipFiltering) {
|
|
506
|
+
// No need to track sent keys if we loaded the entire state or filtering is skipped.
|
|
507
|
+
// Since filtering won't be applied, all keys are effectively "observed".
|
|
442
508
|
return
|
|
443
509
|
}
|
|
444
510
|
|
|
445
511
|
for (const change of changes) {
|
|
446
|
-
|
|
512
|
+
if (change.type === `delete`) {
|
|
513
|
+
// Remove deleted keys from sentKeys so future re-inserts are allowed
|
|
514
|
+
this.sentKeys.delete(change.key)
|
|
515
|
+
} else {
|
|
516
|
+
// For inserts and updates, track the key as sent
|
|
517
|
+
this.sentKeys.add(change.key)
|
|
518
|
+
}
|
|
447
519
|
}
|
|
448
520
|
}
|
|
449
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Mark that the subscription should not filter any changes.
|
|
524
|
+
* This is used when includeInitialState is explicitly set to false,
|
|
525
|
+
* meaning the caller doesn't want initial state but does want ALL future changes.
|
|
526
|
+
*/
|
|
527
|
+
markAllStateAsSeen() {
|
|
528
|
+
this.skipFiltering = true
|
|
529
|
+
}
|
|
530
|
+
|
|
450
531
|
unsubscribe() {
|
|
451
532
|
// Unload all subsets that this subscription loaded
|
|
452
533
|
// We pass the exact same LoadSubsetOptions we used for loadSubset
|