@tanstack/db 0.3.2 → 0.4.0
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/{change-events.cjs → collection/change-events.cjs} +13 -42
- package/dist/cjs/collection/change-events.cjs.map +1 -0
- package/dist/{esm/change-events.d.ts → cjs/collection/change-events.d.cts} +6 -6
- package/dist/cjs/collection/changes.cjs +108 -0
- package/dist/cjs/collection/changes.cjs.map +1 -0
- package/dist/cjs/collection/changes.d.cts +53 -0
- package/dist/cjs/{collection-events.cjs → collection/events.cjs} +7 -5
- package/dist/cjs/collection/events.cjs.map +1 -0
- package/dist/cjs/{collection-events.d.cts → collection/events.d.cts} +7 -4
- package/dist/cjs/collection/index.cjs +417 -0
- package/dist/cjs/collection/index.cjs.map +1 -0
- package/dist/{esm/collection.d.ts → cjs/collection/index.d.cts} +46 -184
- package/dist/cjs/collection/indexes.cjs +124 -0
- package/dist/cjs/collection/indexes.cjs.map +1 -0
- package/dist/cjs/collection/indexes.d.cts +47 -0
- package/dist/cjs/collection/lifecycle.cjs +150 -0
- package/dist/cjs/collection/lifecycle.cjs.map +1 -0
- package/dist/cjs/collection/lifecycle.d.cts +70 -0
- package/dist/cjs/collection/mutations.cjs +315 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -0
- package/dist/cjs/collection/mutations.d.cts +33 -0
- package/dist/cjs/collection/state.cjs +597 -0
- package/dist/cjs/collection/state.cjs.map +1 -0
- package/dist/cjs/collection/state.d.cts +122 -0
- package/dist/cjs/collection/subscription.cjs +160 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -0
- package/dist/cjs/collection/subscription.d.cts +57 -0
- package/dist/cjs/collection/sync.cjs +154 -0
- package/dist/cjs/collection/sync.cjs.map +1 -0
- package/dist/cjs/collection/sync.d.cts +34 -0
- package/dist/cjs/index.cjs +8 -8
- package/dist/cjs/index.d.cts +2 -2
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.d.cts +1 -1
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -2
- package/dist/cjs/indexes/btree-index.cjs +2 -2
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +5 -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 +22 -24
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +3 -2
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +29 -12
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +3 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +43 -104
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
- package/dist/cjs/query/live-query-collection.cjs +2 -2
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +1 -1
- package/dist/cjs/transactions.cjs +3 -3
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +12 -10
- package/dist/{cjs/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
- package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
- package/dist/esm/collection/change-events.js.map +1 -0
- package/dist/esm/collection/changes.d.ts +53 -0
- package/dist/esm/collection/changes.js +108 -0
- package/dist/esm/collection/changes.js.map +1 -0
- package/dist/esm/{collection-events.d.ts → collection/events.d.ts} +7 -4
- package/dist/esm/{collection-events.js → collection/events.js} +7 -5
- package/dist/esm/collection/events.js.map +1 -0
- package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +46 -184
- package/dist/esm/collection/index.js +417 -0
- package/dist/esm/collection/index.js.map +1 -0
- package/dist/esm/collection/indexes.d.ts +47 -0
- package/dist/esm/collection/indexes.js +124 -0
- package/dist/esm/collection/indexes.js.map +1 -0
- package/dist/esm/collection/lifecycle.d.ts +70 -0
- package/dist/esm/collection/lifecycle.js +150 -0
- package/dist/esm/collection/lifecycle.js.map +1 -0
- package/dist/esm/collection/mutations.d.ts +33 -0
- package/dist/esm/collection/mutations.js +315 -0
- package/dist/esm/collection/mutations.js.map +1 -0
- package/dist/esm/collection/state.d.ts +122 -0
- package/dist/esm/collection/state.js +597 -0
- package/dist/esm/collection/state.js.map +1 -0
- package/dist/esm/collection/subscription.d.ts +57 -0
- package/dist/esm/collection/subscription.js +160 -0
- package/dist/esm/collection/subscription.js.map +1 -0
- package/dist/esm/collection/sync.d.ts +34 -0
- package/dist/esm/collection/sync.js +154 -0
- package/dist/esm/collection/sync.js.map +1 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/indexes/auto-index.d.ts +1 -1
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -2
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js +2 -2
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/proxy.js +1 -1
- package/dist/esm/query/builder/index.js +1 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +3 -2
- package/dist/esm/query/compiler/index.js +5 -2
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +3 -2
- package/dist/esm/query/compiler/joins.js +22 -24
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/ir.d.ts +1 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
- package/dist/esm/query/live/collection-config-builder.js +29 -12
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
- package/dist/esm/query/live/collection-subscriber.js +43 -104
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +1 -1
- package/dist/esm/query/live-query-collection.js +1 -1
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/transactions.js +3 -3
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +12 -10
- package/package.json +2 -2
- package/src/{change-events.ts → collection/change-events.ts} +25 -39
- package/src/collection/changes.ts +163 -0
- package/src/{collection-events.ts → collection/events.ts} +8 -6
- package/src/collection/index.ts +808 -0
- package/src/collection/indexes.ts +172 -0
- package/src/collection/lifecycle.ts +221 -0
- package/src/collection/mutations.ts +535 -0
- package/src/collection/state.ts +866 -0
- package/src/collection/subscription.ts +239 -0
- package/src/collection/sync.ts +235 -0
- package/src/index.ts +2 -2
- package/src/indexes/auto-index.ts +1 -1
- package/src/indexes/base-index.ts +3 -3
- package/src/indexes/btree-index.ts +2 -2
- package/src/query/builder/index.ts +1 -1
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +7 -1
- package/src/query/compiler/joins.ts +28 -41
- package/src/query/compiler/order-by.ts +1 -1
- package/src/query/ir.ts +1 -1
- package/src/query/live/collection-config-builder.ts +48 -22
- package/src/query/live/collection-subscriber.ts +63 -168
- package/src/query/live-query-collection.ts +2 -2
- package/src/transactions.ts +3 -3
- package/src/types.ts +14 -15
- package/dist/cjs/change-events.cjs.map +0 -1
- package/dist/cjs/collection-events.cjs.map +0 -1
- package/dist/cjs/collection.cjs +0 -1625
- package/dist/cjs/collection.cjs.map +0 -1
- package/dist/esm/change-events.js.map +0 -1
- package/dist/esm/collection-events.js.map +0 -1
- package/dist/esm/collection.js +0 -1625
- package/dist/esm/collection.js.map +0 -1
- package/src/collection.ts +0 -2564
|
@@ -0,0 +1,866 @@
|
|
|
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
|
+
import type {
|
|
6
|
+
ChangeMessage,
|
|
7
|
+
CollectionConfig,
|
|
8
|
+
OptimisticChangeMessage,
|
|
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
|
+
|
|
15
|
+
interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
16
|
+
committed: boolean
|
|
17
|
+
operations: Array<OptimisticChangeMessage<T>>
|
|
18
|
+
truncate?: boolean
|
|
19
|
+
deletedKeys: Set<string | number>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class CollectionStateManager<
|
|
23
|
+
TOutput extends object = Record<string, unknown>,
|
|
24
|
+
TKey extends string | number = string | number,
|
|
25
|
+
TSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
26
|
+
TInput extends object = TOutput,
|
|
27
|
+
> {
|
|
28
|
+
public config!: CollectionConfig<TOutput, TKey, TSchema>
|
|
29
|
+
public collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
30
|
+
public lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
|
|
31
|
+
public changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
|
|
32
|
+
public indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
|
|
33
|
+
|
|
34
|
+
// Core state - make public for testing
|
|
35
|
+
public transactions: SortedMap<string, Transaction<any>>
|
|
36
|
+
public pendingSyncedTransactions: Array<PendingSyncedTransaction<TOutput>> =
|
|
37
|
+
[]
|
|
38
|
+
public syncedData: Map<TKey, TOutput> | SortedMap<TKey, TOutput>
|
|
39
|
+
public syncedMetadata = new Map<TKey, unknown>()
|
|
40
|
+
|
|
41
|
+
// Optimistic state tracking - make public for testing
|
|
42
|
+
public optimisticUpserts = new Map<TKey, TOutput>()
|
|
43
|
+
public optimisticDeletes = new Set<TKey>()
|
|
44
|
+
|
|
45
|
+
// Cached size for performance
|
|
46
|
+
public size = 0
|
|
47
|
+
|
|
48
|
+
// State used for computing the change events
|
|
49
|
+
public syncedKeys = new Set<TKey>()
|
|
50
|
+
public preSyncVisibleState = new Map<TKey, TOutput>()
|
|
51
|
+
public recentlySyncedKeys = new Set<TKey>()
|
|
52
|
+
public hasReceivedFirstCommit = false
|
|
53
|
+
public isCommittingSyncTransactions = false
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a new CollectionState manager
|
|
57
|
+
*/
|
|
58
|
+
constructor(config: CollectionConfig<TOutput, TKey, TSchema>) {
|
|
59
|
+
this.config = config
|
|
60
|
+
this.transactions = new SortedMap<string, Transaction<any>>((a, b) =>
|
|
61
|
+
a.compareCreatedAt(b)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// Set up data storage with optional comparison function
|
|
65
|
+
if (config.compare) {
|
|
66
|
+
this.syncedData = new SortedMap<TKey, TOutput>(config.compare)
|
|
67
|
+
} else {
|
|
68
|
+
this.syncedData = new Map<TKey, TOutput>()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setDeps(deps: {
|
|
73
|
+
collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
74
|
+
lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
|
|
75
|
+
changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
|
|
76
|
+
indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
|
|
77
|
+
}) {
|
|
78
|
+
this.collection = deps.collection
|
|
79
|
+
this.lifecycle = deps.lifecycle
|
|
80
|
+
this.changes = deps.changes
|
|
81
|
+
this.indexes = deps.indexes
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the current value for a key (virtual derived state)
|
|
86
|
+
*/
|
|
87
|
+
public get(key: TKey): TOutput | undefined {
|
|
88
|
+
const { optimisticDeletes, optimisticUpserts, syncedData } = this
|
|
89
|
+
// Check if optimistically deleted
|
|
90
|
+
if (optimisticDeletes.has(key)) {
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check optimistic upserts first
|
|
95
|
+
if (optimisticUpserts.has(key)) {
|
|
96
|
+
return optimisticUpserts.get(key)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fall back to synced data
|
|
100
|
+
return syncedData.get(key)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a key exists in the collection (virtual derived state)
|
|
105
|
+
*/
|
|
106
|
+
public has(key: TKey): boolean {
|
|
107
|
+
const { optimisticDeletes, optimisticUpserts, syncedData } = this
|
|
108
|
+
// Check if optimistically deleted
|
|
109
|
+
if (optimisticDeletes.has(key)) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check optimistic upserts first
|
|
114
|
+
if (optimisticUpserts.has(key)) {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fall back to synced data
|
|
119
|
+
return syncedData.has(key)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all keys (virtual derived state)
|
|
124
|
+
*/
|
|
125
|
+
public *keys(): IterableIterator<TKey> {
|
|
126
|
+
const { syncedData, optimisticDeletes, optimisticUpserts } = this
|
|
127
|
+
// Yield keys from synced data, skipping any that are deleted.
|
|
128
|
+
for (const key of syncedData.keys()) {
|
|
129
|
+
if (!optimisticDeletes.has(key)) {
|
|
130
|
+
yield key
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Yield keys from upserts that were not already in synced data.
|
|
134
|
+
for (const key of optimisticUpserts.keys()) {
|
|
135
|
+
if (!syncedData.has(key) && !optimisticDeletes.has(key)) {
|
|
136
|
+
// The optimisticDeletes check is technically redundant if inserts/updates always remove from deletes,
|
|
137
|
+
// but it's safer to keep it.
|
|
138
|
+
yield key
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get all values (virtual derived state)
|
|
145
|
+
*/
|
|
146
|
+
public *values(): IterableIterator<TOutput> {
|
|
147
|
+
for (const key of this.keys()) {
|
|
148
|
+
const value = this.get(key)
|
|
149
|
+
if (value !== undefined) {
|
|
150
|
+
yield value
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get all entries (virtual derived state)
|
|
157
|
+
*/
|
|
158
|
+
public *entries(): IterableIterator<[TKey, TOutput]> {
|
|
159
|
+
for (const key of this.keys()) {
|
|
160
|
+
const value = this.get(key)
|
|
161
|
+
if (value !== undefined) {
|
|
162
|
+
yield [key, value]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get all entries (virtual derived state)
|
|
169
|
+
*/
|
|
170
|
+
public *[Symbol.iterator](): IterableIterator<[TKey, TOutput]> {
|
|
171
|
+
for (const [key, value] of this.entries()) {
|
|
172
|
+
yield [key, value]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Execute a callback for each entry in the collection
|
|
178
|
+
*/
|
|
179
|
+
public forEach(
|
|
180
|
+
callbackfn: (value: TOutput, key: TKey, index: number) => void
|
|
181
|
+
): void {
|
|
182
|
+
let index = 0
|
|
183
|
+
for (const [key, value] of this.entries()) {
|
|
184
|
+
callbackfn(value, key, index++)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create a new array with the results of calling a function for each entry in the collection
|
|
190
|
+
*/
|
|
191
|
+
public map<U>(
|
|
192
|
+
callbackfn: (value: TOutput, key: TKey, index: number) => U
|
|
193
|
+
): Array<U> {
|
|
194
|
+
const result: Array<U> = []
|
|
195
|
+
let index = 0
|
|
196
|
+
for (const [key, value] of this.entries()) {
|
|
197
|
+
result.push(callbackfn(value, key, index++))
|
|
198
|
+
}
|
|
199
|
+
return result
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check if the given collection is this collection
|
|
204
|
+
* @param collection The collection to check
|
|
205
|
+
* @returns True if the given collection is this collection, false otherwise
|
|
206
|
+
*/
|
|
207
|
+
private isThisCollection(
|
|
208
|
+
collection: CollectionImpl<any, any, any, any, any>
|
|
209
|
+
): boolean {
|
|
210
|
+
return collection === this.collection
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Recompute optimistic state from active transactions
|
|
215
|
+
*/
|
|
216
|
+
public recomputeOptimisticState(
|
|
217
|
+
triggeredByUserAction: boolean = false
|
|
218
|
+
): void {
|
|
219
|
+
// Skip redundant recalculations when we're in the middle of committing sync transactions
|
|
220
|
+
if (this.isCommittingSyncTransactions) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const previousState = new Map(this.optimisticUpserts)
|
|
225
|
+
const previousDeletes = new Set(this.optimisticDeletes)
|
|
226
|
+
|
|
227
|
+
// Clear current optimistic state
|
|
228
|
+
this.optimisticUpserts.clear()
|
|
229
|
+
this.optimisticDeletes.clear()
|
|
230
|
+
|
|
231
|
+
const activeTransactions: Array<Transaction<any>> = []
|
|
232
|
+
|
|
233
|
+
for (const transaction of this.transactions.values()) {
|
|
234
|
+
if (![`completed`, `failed`].includes(transaction.state)) {
|
|
235
|
+
activeTransactions.push(transaction)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Apply active transactions only (completed transactions are handled by sync operations)
|
|
240
|
+
for (const transaction of activeTransactions) {
|
|
241
|
+
for (const mutation of transaction.mutations) {
|
|
242
|
+
if (this.isThisCollection(mutation.collection) && mutation.optimistic) {
|
|
243
|
+
switch (mutation.type) {
|
|
244
|
+
case `insert`:
|
|
245
|
+
case `update`:
|
|
246
|
+
this.optimisticUpserts.set(
|
|
247
|
+
mutation.key,
|
|
248
|
+
mutation.modified as TOutput
|
|
249
|
+
)
|
|
250
|
+
this.optimisticDeletes.delete(mutation.key)
|
|
251
|
+
break
|
|
252
|
+
case `delete`:
|
|
253
|
+
this.optimisticUpserts.delete(mutation.key)
|
|
254
|
+
this.optimisticDeletes.add(mutation.key)
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Update cached size
|
|
262
|
+
this.size = this.calculateSize()
|
|
263
|
+
|
|
264
|
+
// Collect events for changes
|
|
265
|
+
const events: Array<ChangeMessage<TOutput, TKey>> = []
|
|
266
|
+
this.collectOptimisticChanges(previousState, previousDeletes, events)
|
|
267
|
+
|
|
268
|
+
// Filter out events for recently synced keys to prevent duplicates
|
|
269
|
+
// BUT: Only filter out events that are actually from sync operations
|
|
270
|
+
// New user transactions should NOT be filtered even if the key was recently synced
|
|
271
|
+
const filteredEventsBySyncStatus = events.filter((event) => {
|
|
272
|
+
if (!this.recentlySyncedKeys.has(event.key)) {
|
|
273
|
+
return true // Key not recently synced, allow event through
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Key was recently synced - allow if this is a user-triggered action
|
|
277
|
+
if (triggeredByUserAction) {
|
|
278
|
+
return true
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Otherwise filter out duplicate sync events
|
|
282
|
+
return false
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Filter out redundant delete events if there are pending sync transactions
|
|
286
|
+
// that will immediately restore the same data, but only for completed transactions
|
|
287
|
+
// IMPORTANT: Skip complex filtering for user-triggered actions to prevent UI blocking
|
|
288
|
+
if (this.pendingSyncedTransactions.length > 0 && !triggeredByUserAction) {
|
|
289
|
+
const pendingSyncKeys = new Set<TKey>()
|
|
290
|
+
|
|
291
|
+
// Collect keys from pending sync operations
|
|
292
|
+
for (const transaction of this.pendingSyncedTransactions) {
|
|
293
|
+
for (const operation of transaction.operations) {
|
|
294
|
+
pendingSyncKeys.add(operation.key as TKey)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Only filter out delete events for keys that:
|
|
299
|
+
// 1. Have pending sync operations AND
|
|
300
|
+
// 2. Are from completed transactions (being cleaned up)
|
|
301
|
+
const filteredEvents = filteredEventsBySyncStatus.filter((event) => {
|
|
302
|
+
if (event.type === `delete` && pendingSyncKeys.has(event.key)) {
|
|
303
|
+
// Check if this delete is from clearing optimistic state of completed transactions
|
|
304
|
+
// We can infer this by checking if we have no remaining optimistic mutations for this key
|
|
305
|
+
const hasActiveOptimisticMutation = activeTransactions.some((tx) =>
|
|
306
|
+
tx.mutations.some(
|
|
307
|
+
(m) => this.isThisCollection(m.collection) && m.key === event.key
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if (!hasActiveOptimisticMutation) {
|
|
312
|
+
return false // Skip this delete event as sync will restore the data
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return true
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
// Update indexes for the filtered events
|
|
319
|
+
if (filteredEvents.length > 0) {
|
|
320
|
+
this.indexes.updateIndexes(filteredEvents)
|
|
321
|
+
}
|
|
322
|
+
this.changes.emitEvents(filteredEvents, triggeredByUserAction)
|
|
323
|
+
} else {
|
|
324
|
+
// Update indexes for all events
|
|
325
|
+
if (filteredEventsBySyncStatus.length > 0) {
|
|
326
|
+
this.indexes.updateIndexes(filteredEventsBySyncStatus)
|
|
327
|
+
}
|
|
328
|
+
// Emit all events if no pending sync transactions
|
|
329
|
+
this.changes.emitEvents(filteredEventsBySyncStatus, triggeredByUserAction)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Calculate the current size based on synced data and optimistic changes
|
|
335
|
+
*/
|
|
336
|
+
private calculateSize(): number {
|
|
337
|
+
const syncedSize = this.syncedData.size
|
|
338
|
+
const deletesFromSynced = Array.from(this.optimisticDeletes).filter(
|
|
339
|
+
(key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key)
|
|
340
|
+
).length
|
|
341
|
+
const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter(
|
|
342
|
+
(key) => !this.syncedData.has(key)
|
|
343
|
+
).length
|
|
344
|
+
|
|
345
|
+
return syncedSize - deletesFromSynced + upsertsNotInSynced
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Collect events for optimistic changes
|
|
350
|
+
*/
|
|
351
|
+
private collectOptimisticChanges(
|
|
352
|
+
previousUpserts: Map<TKey, TOutput>,
|
|
353
|
+
previousDeletes: Set<TKey>,
|
|
354
|
+
events: Array<ChangeMessage<TOutput, TKey>>
|
|
355
|
+
): void {
|
|
356
|
+
const allKeys = new Set([
|
|
357
|
+
...previousUpserts.keys(),
|
|
358
|
+
...this.optimisticUpserts.keys(),
|
|
359
|
+
...previousDeletes,
|
|
360
|
+
...this.optimisticDeletes,
|
|
361
|
+
])
|
|
362
|
+
|
|
363
|
+
for (const key of allKeys) {
|
|
364
|
+
const currentValue = this.get(key)
|
|
365
|
+
const previousValue = this.getPreviousValue(
|
|
366
|
+
key,
|
|
367
|
+
previousUpserts,
|
|
368
|
+
previousDeletes
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if (previousValue !== undefined && currentValue === undefined) {
|
|
372
|
+
events.push({ type: `delete`, key, value: previousValue })
|
|
373
|
+
} else if (previousValue === undefined && currentValue !== undefined) {
|
|
374
|
+
events.push({ type: `insert`, key, value: currentValue })
|
|
375
|
+
} else if (
|
|
376
|
+
previousValue !== undefined &&
|
|
377
|
+
currentValue !== undefined &&
|
|
378
|
+
previousValue !== currentValue
|
|
379
|
+
) {
|
|
380
|
+
events.push({
|
|
381
|
+
type: `update`,
|
|
382
|
+
key,
|
|
383
|
+
value: currentValue,
|
|
384
|
+
previousValue,
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get the previous value for a key given previous optimistic state
|
|
392
|
+
*/
|
|
393
|
+
private getPreviousValue(
|
|
394
|
+
key: TKey,
|
|
395
|
+
previousUpserts: Map<TKey, TOutput>,
|
|
396
|
+
previousDeletes: Set<TKey>
|
|
397
|
+
): TOutput | undefined {
|
|
398
|
+
if (previousDeletes.has(key)) {
|
|
399
|
+
return undefined
|
|
400
|
+
}
|
|
401
|
+
if (previousUpserts.has(key)) {
|
|
402
|
+
return previousUpserts.get(key)
|
|
403
|
+
}
|
|
404
|
+
return this.syncedData.get(key)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Attempts to commit pending synced transactions if there are no active transactions
|
|
409
|
+
* This method processes operations from pending transactions and applies them to the synced data
|
|
410
|
+
*/
|
|
411
|
+
commitPendingTransactions = () => {
|
|
412
|
+
// Check if there are any persisting transaction
|
|
413
|
+
let hasPersistingTransaction = false
|
|
414
|
+
for (const transaction of this.transactions.values()) {
|
|
415
|
+
if (transaction.state === `persisting`) {
|
|
416
|
+
hasPersistingTransaction = true
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// pending synced transactions could be either `committed` or still open.
|
|
422
|
+
// we only want to process `committed` transactions here
|
|
423
|
+
const {
|
|
424
|
+
committedSyncedTransactions,
|
|
425
|
+
uncommittedSyncedTransactions,
|
|
426
|
+
hasTruncateSync,
|
|
427
|
+
} = this.pendingSyncedTransactions.reduce(
|
|
428
|
+
(acc, t) => {
|
|
429
|
+
if (t.committed) {
|
|
430
|
+
acc.committedSyncedTransactions.push(t)
|
|
431
|
+
if (t.truncate === true) {
|
|
432
|
+
acc.hasTruncateSync = true
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
acc.uncommittedSyncedTransactions.push(t)
|
|
436
|
+
}
|
|
437
|
+
return acc
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
committedSyncedTransactions: [] as Array<
|
|
441
|
+
PendingSyncedTransaction<TOutput>
|
|
442
|
+
>,
|
|
443
|
+
uncommittedSyncedTransactions: [] as Array<
|
|
444
|
+
PendingSyncedTransaction<TOutput>
|
|
445
|
+
>,
|
|
446
|
+
hasTruncateSync: false,
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if (!hasPersistingTransaction || hasTruncateSync) {
|
|
451
|
+
// Set flag to prevent redundant optimistic state recalculations
|
|
452
|
+
this.isCommittingSyncTransactions = true
|
|
453
|
+
|
|
454
|
+
// First collect all keys that will be affected by sync operations
|
|
455
|
+
const changedKeys = new Set<TKey>()
|
|
456
|
+
for (const transaction of committedSyncedTransactions) {
|
|
457
|
+
for (const operation of transaction.operations) {
|
|
458
|
+
changedKeys.add(operation.key as TKey)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Use pre-captured state if available (from optimistic scenarios),
|
|
463
|
+
// otherwise capture current state (for pure sync scenarios)
|
|
464
|
+
let currentVisibleState = this.preSyncVisibleState
|
|
465
|
+
if (currentVisibleState.size === 0) {
|
|
466
|
+
// No pre-captured state, capture it now for pure sync operations
|
|
467
|
+
currentVisibleState = new Map<TKey, TOutput>()
|
|
468
|
+
for (const key of changedKeys) {
|
|
469
|
+
const currentValue = this.get(key)
|
|
470
|
+
if (currentValue !== undefined) {
|
|
471
|
+
currentVisibleState.set(key, currentValue)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const events: Array<ChangeMessage<TOutput, TKey>> = []
|
|
477
|
+
const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`
|
|
478
|
+
|
|
479
|
+
for (const transaction of committedSyncedTransactions) {
|
|
480
|
+
// Handle truncate operations first
|
|
481
|
+
if (transaction.truncate) {
|
|
482
|
+
// TRUNCATE PHASE
|
|
483
|
+
// 1) Emit a delete for every currently-synced key so downstream listeners/indexes
|
|
484
|
+
// observe a clear-before-rebuild. We intentionally skip keys already in
|
|
485
|
+
// optimisticDeletes because their delete was previously emitted by the user.
|
|
486
|
+
for (const key of this.syncedData.keys()) {
|
|
487
|
+
if (this.optimisticDeletes.has(key)) continue
|
|
488
|
+
const previousValue =
|
|
489
|
+
this.optimisticUpserts.get(key) || this.syncedData.get(key)
|
|
490
|
+
if (previousValue !== undefined) {
|
|
491
|
+
events.push({ type: `delete`, key, value: previousValue })
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 2) Clear the authoritative synced base. Subsequent server ops in this
|
|
496
|
+
// same commit will rebuild the base atomically.
|
|
497
|
+
this.syncedData.clear()
|
|
498
|
+
this.syncedMetadata.clear()
|
|
499
|
+
this.syncedKeys.clear()
|
|
500
|
+
|
|
501
|
+
// 3) Clear currentVisibleState for truncated keys to ensure subsequent operations
|
|
502
|
+
// are compared against the post-truncate state (undefined) rather than pre-truncate state
|
|
503
|
+
// This ensures that re-inserted keys are emitted as INSERT events, not UPDATE events
|
|
504
|
+
for (const key of changedKeys) {
|
|
505
|
+
currentVisibleState.delete(key)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const operation of transaction.operations) {
|
|
510
|
+
const key = operation.key as TKey
|
|
511
|
+
this.syncedKeys.add(key)
|
|
512
|
+
|
|
513
|
+
// Update metadata
|
|
514
|
+
switch (operation.type) {
|
|
515
|
+
case `insert`:
|
|
516
|
+
this.syncedMetadata.set(key, operation.metadata)
|
|
517
|
+
break
|
|
518
|
+
case `update`:
|
|
519
|
+
this.syncedMetadata.set(
|
|
520
|
+
key,
|
|
521
|
+
Object.assign(
|
|
522
|
+
{},
|
|
523
|
+
this.syncedMetadata.get(key),
|
|
524
|
+
operation.metadata
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
break
|
|
528
|
+
case `delete`:
|
|
529
|
+
this.syncedMetadata.delete(key)
|
|
530
|
+
break
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Update synced data
|
|
534
|
+
switch (operation.type) {
|
|
535
|
+
case `insert`:
|
|
536
|
+
this.syncedData.set(key, operation.value)
|
|
537
|
+
break
|
|
538
|
+
case `update`: {
|
|
539
|
+
if (rowUpdateMode === `partial`) {
|
|
540
|
+
const updatedValue = Object.assign(
|
|
541
|
+
{},
|
|
542
|
+
this.syncedData.get(key),
|
|
543
|
+
operation.value
|
|
544
|
+
)
|
|
545
|
+
this.syncedData.set(key, updatedValue)
|
|
546
|
+
} else {
|
|
547
|
+
this.syncedData.set(key, operation.value)
|
|
548
|
+
}
|
|
549
|
+
break
|
|
550
|
+
}
|
|
551
|
+
case `delete`:
|
|
552
|
+
this.syncedData.delete(key)
|
|
553
|
+
break
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// After applying synced operations, if this commit included a truncate,
|
|
559
|
+
// re-apply optimistic mutations on top of the fresh synced base. This ensures
|
|
560
|
+
// the UI preserves local intent while respecting server rebuild semantics.
|
|
561
|
+
// Ordering: deletes (above) -> server ops (just applied) -> optimistic upserts.
|
|
562
|
+
if (hasTruncateSync) {
|
|
563
|
+
// Avoid duplicating keys that were inserted/updated by synced operations in this commit
|
|
564
|
+
const syncedInsertedOrUpdatedKeys = new Set<TKey>()
|
|
565
|
+
for (const t of committedSyncedTransactions) {
|
|
566
|
+
for (const op of t.operations) {
|
|
567
|
+
if (op.type === `insert` || op.type === `update`) {
|
|
568
|
+
syncedInsertedOrUpdatedKeys.add(op.key as TKey)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Build re-apply sets from ACTIVE optimistic transactions against the new synced base
|
|
574
|
+
// We do not copy maps; we compute intent directly from transactions to avoid drift.
|
|
575
|
+
const reapplyUpserts = new Map<TKey, TOutput>()
|
|
576
|
+
const reapplyDeletes = new Set<TKey>()
|
|
577
|
+
|
|
578
|
+
for (const tx of this.transactions.values()) {
|
|
579
|
+
if ([`completed`, `failed`].includes(tx.state)) continue
|
|
580
|
+
for (const mutation of tx.mutations) {
|
|
581
|
+
if (
|
|
582
|
+
!this.isThisCollection(mutation.collection) ||
|
|
583
|
+
!mutation.optimistic
|
|
584
|
+
)
|
|
585
|
+
continue
|
|
586
|
+
const key = mutation.key as TKey
|
|
587
|
+
switch (mutation.type) {
|
|
588
|
+
case `insert`:
|
|
589
|
+
reapplyUpserts.set(key, mutation.modified as TOutput)
|
|
590
|
+
reapplyDeletes.delete(key)
|
|
591
|
+
break
|
|
592
|
+
case `update`: {
|
|
593
|
+
const base = this.syncedData.get(key)
|
|
594
|
+
const next = base
|
|
595
|
+
? (Object.assign({}, base, mutation.changes) as TOutput)
|
|
596
|
+
: (mutation.modified as TOutput)
|
|
597
|
+
reapplyUpserts.set(key, next)
|
|
598
|
+
reapplyDeletes.delete(key)
|
|
599
|
+
break
|
|
600
|
+
}
|
|
601
|
+
case `delete`:
|
|
602
|
+
reapplyUpserts.delete(key)
|
|
603
|
+
reapplyDeletes.add(key)
|
|
604
|
+
break
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Emit inserts for re-applied upserts, skipping any keys that have an optimistic delete.
|
|
610
|
+
// If the server also inserted/updated the same key in this batch, override that value
|
|
611
|
+
// with the optimistic value to preserve local intent.
|
|
612
|
+
for (const [key, value] of reapplyUpserts) {
|
|
613
|
+
if (reapplyDeletes.has(key)) continue
|
|
614
|
+
if (syncedInsertedOrUpdatedKeys.has(key)) {
|
|
615
|
+
let foundInsert = false
|
|
616
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
617
|
+
const evt = events[i]!
|
|
618
|
+
if (evt.key === key && evt.type === `insert`) {
|
|
619
|
+
evt.value = value
|
|
620
|
+
foundInsert = true
|
|
621
|
+
break
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (!foundInsert) {
|
|
625
|
+
events.push({ type: `insert`, key, value })
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
events.push({ type: `insert`, key, value })
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Finally, ensure we do NOT insert keys that have an outstanding optimistic delete.
|
|
633
|
+
if (events.length > 0 && reapplyDeletes.size > 0) {
|
|
634
|
+
const filtered: Array<ChangeMessage<TOutput, TKey>> = []
|
|
635
|
+
for (const evt of events) {
|
|
636
|
+
if (evt.type === `insert` && reapplyDeletes.has(evt.key)) {
|
|
637
|
+
continue
|
|
638
|
+
}
|
|
639
|
+
filtered.push(evt)
|
|
640
|
+
}
|
|
641
|
+
events.length = 0
|
|
642
|
+
events.push(...filtered)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Ensure listeners are active before emitting this critical batch
|
|
646
|
+
if (this.lifecycle.status !== `ready`) {
|
|
647
|
+
this.lifecycle.setStatus(`ready`)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Maintain optimistic state appropriately
|
|
652
|
+
// Clear optimistic state since sync operations will now provide the authoritative data.
|
|
653
|
+
// Any still-active user transactions will be re-applied below in recompute.
|
|
654
|
+
this.optimisticUpserts.clear()
|
|
655
|
+
this.optimisticDeletes.clear()
|
|
656
|
+
|
|
657
|
+
// Reset flag and recompute optimistic state for any remaining active transactions
|
|
658
|
+
this.isCommittingSyncTransactions = false
|
|
659
|
+
for (const transaction of this.transactions.values()) {
|
|
660
|
+
if (![`completed`, `failed`].includes(transaction.state)) {
|
|
661
|
+
for (const mutation of transaction.mutations) {
|
|
662
|
+
if (
|
|
663
|
+
this.isThisCollection(mutation.collection) &&
|
|
664
|
+
mutation.optimistic
|
|
665
|
+
) {
|
|
666
|
+
switch (mutation.type) {
|
|
667
|
+
case `insert`:
|
|
668
|
+
case `update`:
|
|
669
|
+
this.optimisticUpserts.set(
|
|
670
|
+
mutation.key,
|
|
671
|
+
mutation.modified as TOutput
|
|
672
|
+
)
|
|
673
|
+
this.optimisticDeletes.delete(mutation.key)
|
|
674
|
+
break
|
|
675
|
+
case `delete`:
|
|
676
|
+
this.optimisticUpserts.delete(mutation.key)
|
|
677
|
+
this.optimisticDeletes.add(mutation.key)
|
|
678
|
+
break
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Check for redundant sync operations that match completed optimistic operations
|
|
686
|
+
const completedOptimisticOps = new Map<TKey, any>()
|
|
687
|
+
|
|
688
|
+
for (const transaction of this.transactions.values()) {
|
|
689
|
+
if (transaction.state === `completed`) {
|
|
690
|
+
for (const mutation of transaction.mutations) {
|
|
691
|
+
if (
|
|
692
|
+
this.isThisCollection(mutation.collection) &&
|
|
693
|
+
changedKeys.has(mutation.key)
|
|
694
|
+
) {
|
|
695
|
+
completedOptimisticOps.set(mutation.key, {
|
|
696
|
+
type: mutation.type,
|
|
697
|
+
value: mutation.modified,
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Now check what actually changed in the final visible state
|
|
705
|
+
for (const key of changedKeys) {
|
|
706
|
+
const previousVisibleValue = currentVisibleState.get(key)
|
|
707
|
+
const newVisibleValue = this.get(key) // This returns the new derived state
|
|
708
|
+
|
|
709
|
+
// Check if this sync operation is redundant with a completed optimistic operation
|
|
710
|
+
const completedOp = completedOptimisticOps.get(key)
|
|
711
|
+
const isRedundantSync =
|
|
712
|
+
completedOp &&
|
|
713
|
+
newVisibleValue !== undefined &&
|
|
714
|
+
deepEquals(completedOp.value, newVisibleValue)
|
|
715
|
+
|
|
716
|
+
if (!isRedundantSync) {
|
|
717
|
+
if (
|
|
718
|
+
previousVisibleValue === undefined &&
|
|
719
|
+
newVisibleValue !== undefined
|
|
720
|
+
) {
|
|
721
|
+
events.push({
|
|
722
|
+
type: `insert`,
|
|
723
|
+
key,
|
|
724
|
+
value: newVisibleValue,
|
|
725
|
+
})
|
|
726
|
+
} else if (
|
|
727
|
+
previousVisibleValue !== undefined &&
|
|
728
|
+
newVisibleValue === undefined
|
|
729
|
+
) {
|
|
730
|
+
events.push({
|
|
731
|
+
type: `delete`,
|
|
732
|
+
key,
|
|
733
|
+
value: previousVisibleValue,
|
|
734
|
+
})
|
|
735
|
+
} else if (
|
|
736
|
+
previousVisibleValue !== undefined &&
|
|
737
|
+
newVisibleValue !== undefined &&
|
|
738
|
+
!deepEquals(previousVisibleValue, newVisibleValue)
|
|
739
|
+
) {
|
|
740
|
+
events.push({
|
|
741
|
+
type: `update`,
|
|
742
|
+
key,
|
|
743
|
+
value: newVisibleValue,
|
|
744
|
+
previousValue: previousVisibleValue,
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Update cached size after synced data changes
|
|
751
|
+
this.size = this.calculateSize()
|
|
752
|
+
|
|
753
|
+
// Update indexes for all events before emitting
|
|
754
|
+
if (events.length > 0) {
|
|
755
|
+
this.indexes.updateIndexes(events)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// End batching and emit all events (combines any batched events with sync events)
|
|
759
|
+
this.changes.emitEvents(events, true)
|
|
760
|
+
|
|
761
|
+
this.pendingSyncedTransactions = uncommittedSyncedTransactions
|
|
762
|
+
|
|
763
|
+
// Clear the pre-sync state since sync operations are complete
|
|
764
|
+
this.preSyncVisibleState.clear()
|
|
765
|
+
|
|
766
|
+
// Clear recently synced keys after a microtask to allow recomputeOptimisticState to see them
|
|
767
|
+
Promise.resolve().then(() => {
|
|
768
|
+
this.recentlySyncedKeys.clear()
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
// Call any registered one-time commit listeners
|
|
772
|
+
if (!this.hasReceivedFirstCommit) {
|
|
773
|
+
this.hasReceivedFirstCommit = true
|
|
774
|
+
const callbacks = [...this.lifecycle.onFirstReadyCallbacks]
|
|
775
|
+
this.lifecycle.onFirstReadyCallbacks = []
|
|
776
|
+
callbacks.forEach((callback) => callback())
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Schedule cleanup of a transaction when it completes
|
|
783
|
+
*/
|
|
784
|
+
public scheduleTransactionCleanup(transaction: Transaction<any>): void {
|
|
785
|
+
// Only schedule cleanup for transactions that aren't already completed
|
|
786
|
+
if (transaction.state === `completed`) {
|
|
787
|
+
this.transactions.delete(transaction.id)
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Schedule cleanup when the transaction completes
|
|
792
|
+
transaction.isPersisted.promise
|
|
793
|
+
.then(() => {
|
|
794
|
+
// Transaction completed successfully, remove it immediately
|
|
795
|
+
this.transactions.delete(transaction.id)
|
|
796
|
+
})
|
|
797
|
+
.catch(() => {
|
|
798
|
+
// Transaction failed, but we want to keep failed transactions for reference
|
|
799
|
+
// so don't remove it.
|
|
800
|
+
// This empty catch block is necessary to prevent unhandled promise rejections.
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Capture visible state for keys that will be affected by pending sync operations
|
|
806
|
+
* This must be called BEFORE onTransactionStateChange clears optimistic state
|
|
807
|
+
*/
|
|
808
|
+
public capturePreSyncVisibleState(): void {
|
|
809
|
+
if (this.pendingSyncedTransactions.length === 0) return
|
|
810
|
+
|
|
811
|
+
// Clear any previous capture
|
|
812
|
+
this.preSyncVisibleState.clear()
|
|
813
|
+
|
|
814
|
+
// Get all keys that will be affected by sync operations
|
|
815
|
+
const syncedKeys = new Set<TKey>()
|
|
816
|
+
for (const transaction of this.pendingSyncedTransactions) {
|
|
817
|
+
for (const operation of transaction.operations) {
|
|
818
|
+
syncedKeys.add(operation.key as TKey)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Mark keys as about to be synced to suppress intermediate events from recomputeOptimisticState
|
|
823
|
+
for (const key of syncedKeys) {
|
|
824
|
+
this.recentlySyncedKeys.add(key)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Only capture current visible state for keys that will be affected by sync operations
|
|
828
|
+
// This is much more efficient than capturing the entire collection state
|
|
829
|
+
for (const key of syncedKeys) {
|
|
830
|
+
const currentValue = this.get(key)
|
|
831
|
+
if (currentValue !== undefined) {
|
|
832
|
+
this.preSyncVisibleState.set(key, currentValue)
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Trigger a recomputation when transactions change
|
|
839
|
+
* This method should be called by the Transaction class when state changes
|
|
840
|
+
*/
|
|
841
|
+
public onTransactionStateChange(): void {
|
|
842
|
+
// Check if commitPendingTransactions will be called after this
|
|
843
|
+
// by checking if there are pending sync transactions (same logic as in transactions.ts)
|
|
844
|
+
this.changes.shouldBatchEvents = this.pendingSyncedTransactions.length > 0
|
|
845
|
+
|
|
846
|
+
// CRITICAL: Capture visible state BEFORE clearing optimistic state
|
|
847
|
+
this.capturePreSyncVisibleState()
|
|
848
|
+
|
|
849
|
+
this.recomputeOptimisticState(false)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Clean up the collection by stopping sync and clearing data
|
|
854
|
+
* This can be called manually or automatically by garbage collection
|
|
855
|
+
*/
|
|
856
|
+
public cleanup(): void {
|
|
857
|
+
this.syncedData.clear()
|
|
858
|
+
this.syncedMetadata.clear()
|
|
859
|
+
this.optimisticUpserts.clear()
|
|
860
|
+
this.optimisticDeletes.clear()
|
|
861
|
+
this.size = 0
|
|
862
|
+
this.pendingSyncedTransactions = []
|
|
863
|
+
this.syncedKeys.clear()
|
|
864
|
+
this.hasReceivedFirstCommit = false
|
|
865
|
+
}
|
|
866
|
+
}
|