@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.
Files changed (164) hide show
  1. package/dist/cjs/{change-events.cjs → collection/change-events.cjs} +13 -42
  2. package/dist/cjs/collection/change-events.cjs.map +1 -0
  3. package/dist/{esm/change-events.d.ts → cjs/collection/change-events.d.cts} +6 -6
  4. package/dist/cjs/collection/changes.cjs +108 -0
  5. package/dist/cjs/collection/changes.cjs.map +1 -0
  6. package/dist/cjs/collection/changes.d.cts +53 -0
  7. package/dist/cjs/{collection-events.cjs → collection/events.cjs} +7 -5
  8. package/dist/cjs/collection/events.cjs.map +1 -0
  9. package/dist/cjs/{collection-events.d.cts → collection/events.d.cts} +7 -4
  10. package/dist/cjs/collection/index.cjs +417 -0
  11. package/dist/cjs/collection/index.cjs.map +1 -0
  12. package/dist/{esm/collection.d.ts → cjs/collection/index.d.cts} +46 -184
  13. package/dist/cjs/collection/indexes.cjs +124 -0
  14. package/dist/cjs/collection/indexes.cjs.map +1 -0
  15. package/dist/cjs/collection/indexes.d.cts +47 -0
  16. package/dist/cjs/collection/lifecycle.cjs +150 -0
  17. package/dist/cjs/collection/lifecycle.cjs.map +1 -0
  18. package/dist/cjs/collection/lifecycle.d.cts +70 -0
  19. package/dist/cjs/collection/mutations.cjs +315 -0
  20. package/dist/cjs/collection/mutations.cjs.map +1 -0
  21. package/dist/cjs/collection/mutations.d.cts +33 -0
  22. package/dist/cjs/collection/state.cjs +597 -0
  23. package/dist/cjs/collection/state.cjs.map +1 -0
  24. package/dist/cjs/collection/state.d.cts +122 -0
  25. package/dist/cjs/collection/subscription.cjs +160 -0
  26. package/dist/cjs/collection/subscription.cjs.map +1 -0
  27. package/dist/cjs/collection/subscription.d.cts +57 -0
  28. package/dist/cjs/collection/sync.cjs +154 -0
  29. package/dist/cjs/collection/sync.cjs.map +1 -0
  30. package/dist/cjs/collection/sync.d.cts +34 -0
  31. package/dist/cjs/index.cjs +8 -8
  32. package/dist/cjs/index.d.cts +2 -2
  33. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  34. package/dist/cjs/indexes/auto-index.d.cts +1 -1
  35. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  36. package/dist/cjs/indexes/base-index.d.cts +2 -2
  37. package/dist/cjs/indexes/btree-index.cjs +2 -2
  38. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  39. package/dist/cjs/indexes/btree-index.d.cts +1 -1
  40. package/dist/cjs/query/builder/index.cjs +2 -2
  41. package/dist/cjs/query/builder/index.cjs.map +1 -1
  42. package/dist/cjs/query/builder/types.d.cts +1 -1
  43. package/dist/cjs/query/compiler/index.cjs +5 -2
  44. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/index.d.cts +3 -2
  46. package/dist/cjs/query/compiler/joins.cjs +22 -24
  47. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  48. package/dist/cjs/query/compiler/joins.d.cts +3 -2
  49. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  50. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  51. package/dist/cjs/query/ir.cjs.map +1 -1
  52. package/dist/cjs/query/ir.d.cts +1 -1
  53. package/dist/cjs/query/live/collection-config-builder.cjs +29 -12
  54. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-config-builder.d.cts +3 -0
  56. package/dist/cjs/query/live/collection-subscriber.cjs +43 -104
  57. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  58. package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
  59. package/dist/cjs/query/live-query-collection.cjs +2 -2
  60. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  61. package/dist/cjs/query/live-query-collection.d.cts +1 -1
  62. package/dist/cjs/transactions.cjs +3 -3
  63. package/dist/cjs/transactions.cjs.map +1 -1
  64. package/dist/cjs/types.d.cts +12 -10
  65. package/dist/{cjs/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
  66. package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
  67. package/dist/esm/collection/change-events.js.map +1 -0
  68. package/dist/esm/collection/changes.d.ts +53 -0
  69. package/dist/esm/collection/changes.js +108 -0
  70. package/dist/esm/collection/changes.js.map +1 -0
  71. package/dist/esm/{collection-events.d.ts → collection/events.d.ts} +7 -4
  72. package/dist/esm/{collection-events.js → collection/events.js} +7 -5
  73. package/dist/esm/collection/events.js.map +1 -0
  74. package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +46 -184
  75. package/dist/esm/collection/index.js +417 -0
  76. package/dist/esm/collection/index.js.map +1 -0
  77. package/dist/esm/collection/indexes.d.ts +47 -0
  78. package/dist/esm/collection/indexes.js +124 -0
  79. package/dist/esm/collection/indexes.js.map +1 -0
  80. package/dist/esm/collection/lifecycle.d.ts +70 -0
  81. package/dist/esm/collection/lifecycle.js +150 -0
  82. package/dist/esm/collection/lifecycle.js.map +1 -0
  83. package/dist/esm/collection/mutations.d.ts +33 -0
  84. package/dist/esm/collection/mutations.js +315 -0
  85. package/dist/esm/collection/mutations.js.map +1 -0
  86. package/dist/esm/collection/state.d.ts +122 -0
  87. package/dist/esm/collection/state.js +597 -0
  88. package/dist/esm/collection/state.js.map +1 -0
  89. package/dist/esm/collection/subscription.d.ts +57 -0
  90. package/dist/esm/collection/subscription.js +160 -0
  91. package/dist/esm/collection/subscription.js.map +1 -0
  92. package/dist/esm/collection/sync.d.ts +34 -0
  93. package/dist/esm/collection/sync.js +154 -0
  94. package/dist/esm/collection/sync.js.map +1 -0
  95. package/dist/esm/index.d.ts +2 -2
  96. package/dist/esm/index.js +1 -1
  97. package/dist/esm/indexes/auto-index.d.ts +1 -1
  98. package/dist/esm/indexes/auto-index.js.map +1 -1
  99. package/dist/esm/indexes/base-index.d.ts +2 -2
  100. package/dist/esm/indexes/base-index.js.map +1 -1
  101. package/dist/esm/indexes/btree-index.d.ts +1 -1
  102. package/dist/esm/indexes/btree-index.js +2 -2
  103. package/dist/esm/indexes/btree-index.js.map +1 -1
  104. package/dist/esm/proxy.js +1 -1
  105. package/dist/esm/query/builder/index.js +1 -1
  106. package/dist/esm/query/builder/index.js.map +1 -1
  107. package/dist/esm/query/builder/types.d.ts +1 -1
  108. package/dist/esm/query/compiler/index.d.ts +3 -2
  109. package/dist/esm/query/compiler/index.js +5 -2
  110. package/dist/esm/query/compiler/index.js.map +1 -1
  111. package/dist/esm/query/compiler/joins.d.ts +3 -2
  112. package/dist/esm/query/compiler/joins.js +22 -24
  113. package/dist/esm/query/compiler/joins.js.map +1 -1
  114. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  115. package/dist/esm/query/compiler/order-by.js.map +1 -1
  116. package/dist/esm/query/ir.d.ts +1 -1
  117. package/dist/esm/query/ir.js.map +1 -1
  118. package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
  119. package/dist/esm/query/live/collection-config-builder.js +29 -12
  120. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  121. package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
  122. package/dist/esm/query/live/collection-subscriber.js +43 -104
  123. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  124. package/dist/esm/query/live-query-collection.d.ts +1 -1
  125. package/dist/esm/query/live-query-collection.js +1 -1
  126. package/dist/esm/query/live-query-collection.js.map +1 -1
  127. package/dist/esm/transactions.js +3 -3
  128. package/dist/esm/transactions.js.map +1 -1
  129. package/dist/esm/types.d.ts +12 -10
  130. package/package.json +2 -2
  131. package/src/{change-events.ts → collection/change-events.ts} +25 -39
  132. package/src/collection/changes.ts +163 -0
  133. package/src/{collection-events.ts → collection/events.ts} +8 -6
  134. package/src/collection/index.ts +808 -0
  135. package/src/collection/indexes.ts +172 -0
  136. package/src/collection/lifecycle.ts +221 -0
  137. package/src/collection/mutations.ts +535 -0
  138. package/src/collection/state.ts +866 -0
  139. package/src/collection/subscription.ts +239 -0
  140. package/src/collection/sync.ts +235 -0
  141. package/src/index.ts +2 -2
  142. package/src/indexes/auto-index.ts +1 -1
  143. package/src/indexes/base-index.ts +3 -3
  144. package/src/indexes/btree-index.ts +2 -2
  145. package/src/query/builder/index.ts +1 -1
  146. package/src/query/builder/types.ts +1 -1
  147. package/src/query/compiler/index.ts +7 -1
  148. package/src/query/compiler/joins.ts +28 -41
  149. package/src/query/compiler/order-by.ts +1 -1
  150. package/src/query/ir.ts +1 -1
  151. package/src/query/live/collection-config-builder.ts +48 -22
  152. package/src/query/live/collection-subscriber.ts +63 -168
  153. package/src/query/live-query-collection.ts +2 -2
  154. package/src/transactions.ts +3 -3
  155. package/src/types.ts +14 -15
  156. package/dist/cjs/change-events.cjs.map +0 -1
  157. package/dist/cjs/collection-events.cjs.map +0 -1
  158. package/dist/cjs/collection.cjs +0 -1625
  159. package/dist/cjs/collection.cjs.map +0 -1
  160. package/dist/esm/change-events.js.map +0 -1
  161. package/dist/esm/collection-events.js.map +0 -1
  162. package/dist/esm/collection.js +0 -1625
  163. package/dist/esm/collection.js.map +0 -1
  164. package/src/collection.ts +0 -2564
@@ -1,1625 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const proxy = require("./proxy.cjs");
4
- const utils = require("./utils.cjs");
5
- const SortedMap = require("./SortedMap.cjs");
6
- const refProxy = require("./query/builder/ref-proxy.cjs");
7
- const btreeIndex = require("./indexes/btree-index.cjs");
8
- const lazyIndex = require("./indexes/lazy-index.cjs");
9
- const autoIndex = require("./indexes/auto-index.cjs");
10
- const transactions = require("./transactions.cjs");
11
- const errors = require("./errors.cjs");
12
- const changeEvents = require("./change-events.cjs");
13
- const collectionEvents = require("./collection-events.cjs");
14
- function createCollection(options) {
15
- const collection = new CollectionImpl(
16
- options
17
- );
18
- if (options.utils) {
19
- collection.utils = { ...options.utils };
20
- } else {
21
- collection.utils = {};
22
- }
23
- return collection;
24
- }
25
- class CollectionImpl {
26
- /**
27
- * Creates a new Collection instance
28
- *
29
- * @param config - Configuration object for the collection
30
- * @throws Error if sync config is missing
31
- */
32
- constructor(config) {
33
- this.pendingSyncedTransactions = [];
34
- this.syncedMetadata = /* @__PURE__ */ new Map();
35
- this.optimisticUpserts = /* @__PURE__ */ new Map();
36
- this.optimisticDeletes = /* @__PURE__ */ new Set();
37
- this._size = 0;
38
- this.lazyIndexes = /* @__PURE__ */ new Map();
39
- this.resolvedIndexes = /* @__PURE__ */ new Map();
40
- this.isIndexesResolved = false;
41
- this.indexCounter = 0;
42
- this.changeListeners = /* @__PURE__ */ new Set();
43
- this.changeKeyListeners = /* @__PURE__ */ new Map();
44
- this.utils = {};
45
- this.syncedKeys = /* @__PURE__ */ new Set();
46
- this.preSyncVisibleState = /* @__PURE__ */ new Map();
47
- this.recentlySyncedKeys = /* @__PURE__ */ new Set();
48
- this.hasReceivedFirstCommit = false;
49
- this.isCommittingSyncTransactions = false;
50
- this.onFirstReadyCallbacks = [];
51
- this.hasBeenReady = false;
52
- this.batchedEvents = [];
53
- this.shouldBatchEvents = false;
54
- this._status = `idle`;
55
- this.activeSubscribersCount = 0;
56
- this.gcTimeoutId = null;
57
- this.preloadPromise = null;
58
- this.syncCleanupFn = null;
59
- this.id = ``;
60
- this.commitPendingTransactions = () => {
61
- let hasPersistingTransaction = false;
62
- for (const transaction of this.transactions.values()) {
63
- if (transaction.state === `persisting`) {
64
- hasPersistingTransaction = true;
65
- break;
66
- }
67
- }
68
- const {
69
- committedSyncedTransactions,
70
- uncommittedSyncedTransactions,
71
- hasTruncateSync
72
- } = this.pendingSyncedTransactions.reduce(
73
- (acc, t) => {
74
- if (t.committed) {
75
- acc.committedSyncedTransactions.push(t);
76
- if (t.truncate === true) {
77
- acc.hasTruncateSync = true;
78
- }
79
- } else {
80
- acc.uncommittedSyncedTransactions.push(t);
81
- }
82
- return acc;
83
- },
84
- {
85
- committedSyncedTransactions: [],
86
- uncommittedSyncedTransactions: [],
87
- hasTruncateSync: false
88
- }
89
- );
90
- if (!hasPersistingTransaction || hasTruncateSync) {
91
- this.isCommittingSyncTransactions = true;
92
- const changedKeys = /* @__PURE__ */ new Set();
93
- for (const transaction of committedSyncedTransactions) {
94
- for (const operation of transaction.operations) {
95
- changedKeys.add(operation.key);
96
- }
97
- }
98
- let currentVisibleState = this.preSyncVisibleState;
99
- if (currentVisibleState.size === 0) {
100
- currentVisibleState = /* @__PURE__ */ new Map();
101
- for (const key of changedKeys) {
102
- const currentValue = this.get(key);
103
- if (currentValue !== void 0) {
104
- currentVisibleState.set(key, currentValue);
105
- }
106
- }
107
- }
108
- const events = [];
109
- const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`;
110
- for (const transaction of committedSyncedTransactions) {
111
- if (transaction.truncate) {
112
- for (const key of this.syncedData.keys()) {
113
- if (this.optimisticDeletes.has(key)) continue;
114
- const previousValue = this.optimisticUpserts.get(key) || this.syncedData.get(key);
115
- if (previousValue !== void 0) {
116
- events.push({ type: `delete`, key, value: previousValue });
117
- }
118
- }
119
- this.syncedData.clear();
120
- this.syncedMetadata.clear();
121
- this.syncedKeys.clear();
122
- for (const key of changedKeys) {
123
- currentVisibleState.delete(key);
124
- }
125
- }
126
- for (const operation of transaction.operations) {
127
- const key = operation.key;
128
- this.syncedKeys.add(key);
129
- switch (operation.type) {
130
- case `insert`:
131
- this.syncedMetadata.set(key, operation.metadata);
132
- break;
133
- case `update`:
134
- this.syncedMetadata.set(
135
- key,
136
- Object.assign(
137
- {},
138
- this.syncedMetadata.get(key),
139
- operation.metadata
140
- )
141
- );
142
- break;
143
- case `delete`:
144
- this.syncedMetadata.delete(key);
145
- break;
146
- }
147
- switch (operation.type) {
148
- case `insert`:
149
- this.syncedData.set(key, operation.value);
150
- break;
151
- case `update`: {
152
- if (rowUpdateMode === `partial`) {
153
- const updatedValue = Object.assign(
154
- {},
155
- this.syncedData.get(key),
156
- operation.value
157
- );
158
- this.syncedData.set(key, updatedValue);
159
- } else {
160
- this.syncedData.set(key, operation.value);
161
- }
162
- break;
163
- }
164
- case `delete`:
165
- this.syncedData.delete(key);
166
- break;
167
- }
168
- }
169
- }
170
- if (hasTruncateSync) {
171
- const syncedInsertedOrUpdatedKeys = /* @__PURE__ */ new Set();
172
- for (const t of committedSyncedTransactions) {
173
- for (const op of t.operations) {
174
- if (op.type === `insert` || op.type === `update`) {
175
- syncedInsertedOrUpdatedKeys.add(op.key);
176
- }
177
- }
178
- }
179
- const reapplyUpserts = /* @__PURE__ */ new Map();
180
- const reapplyDeletes = /* @__PURE__ */ new Set();
181
- for (const tx of this.transactions.values()) {
182
- if ([`completed`, `failed`].includes(tx.state)) continue;
183
- for (const mutation of tx.mutations) {
184
- if (mutation.collection !== this || !mutation.optimistic) continue;
185
- const key = mutation.key;
186
- switch (mutation.type) {
187
- case `insert`:
188
- reapplyUpserts.set(key, mutation.modified);
189
- reapplyDeletes.delete(key);
190
- break;
191
- case `update`: {
192
- const base = this.syncedData.get(key);
193
- const next = base ? Object.assign({}, base, mutation.changes) : mutation.modified;
194
- reapplyUpserts.set(key, next);
195
- reapplyDeletes.delete(key);
196
- break;
197
- }
198
- case `delete`:
199
- reapplyUpserts.delete(key);
200
- reapplyDeletes.add(key);
201
- break;
202
- }
203
- }
204
- }
205
- for (const [key, value] of reapplyUpserts) {
206
- if (reapplyDeletes.has(key)) continue;
207
- if (syncedInsertedOrUpdatedKeys.has(key)) {
208
- let foundInsert = false;
209
- for (let i = events.length - 1; i >= 0; i--) {
210
- const evt = events[i];
211
- if (evt.key === key && evt.type === `insert`) {
212
- evt.value = value;
213
- foundInsert = true;
214
- break;
215
- }
216
- }
217
- if (!foundInsert) {
218
- events.push({ type: `insert`, key, value });
219
- }
220
- } else {
221
- events.push({ type: `insert`, key, value });
222
- }
223
- }
224
- if (events.length > 0 && reapplyDeletes.size > 0) {
225
- const filtered = [];
226
- for (const evt of events) {
227
- if (evt.type === `insert` && reapplyDeletes.has(evt.key)) {
228
- continue;
229
- }
230
- filtered.push(evt);
231
- }
232
- events.length = 0;
233
- events.push(...filtered);
234
- }
235
- if (!this.isReady()) {
236
- this.setStatus(`ready`);
237
- }
238
- }
239
- this.optimisticUpserts.clear();
240
- this.optimisticDeletes.clear();
241
- this.isCommittingSyncTransactions = false;
242
- for (const transaction of this.transactions.values()) {
243
- if (![`completed`, `failed`].includes(transaction.state)) {
244
- for (const mutation of transaction.mutations) {
245
- if (mutation.collection === this && mutation.optimistic) {
246
- switch (mutation.type) {
247
- case `insert`:
248
- case `update`:
249
- this.optimisticUpserts.set(
250
- mutation.key,
251
- mutation.modified
252
- );
253
- this.optimisticDeletes.delete(mutation.key);
254
- break;
255
- case `delete`:
256
- this.optimisticUpserts.delete(mutation.key);
257
- this.optimisticDeletes.add(mutation.key);
258
- break;
259
- }
260
- }
261
- }
262
- }
263
- }
264
- const completedOptimisticOps = /* @__PURE__ */ new Map();
265
- for (const transaction of this.transactions.values()) {
266
- if (transaction.state === `completed`) {
267
- for (const mutation of transaction.mutations) {
268
- if (mutation.collection === this && changedKeys.has(mutation.key)) {
269
- completedOptimisticOps.set(mutation.key, {
270
- type: mutation.type,
271
- value: mutation.modified
272
- });
273
- }
274
- }
275
- }
276
- }
277
- for (const key of changedKeys) {
278
- const previousVisibleValue = currentVisibleState.get(key);
279
- const newVisibleValue = this.get(key);
280
- const completedOp = completedOptimisticOps.get(key);
281
- const isRedundantSync = completedOp && newVisibleValue !== void 0 && utils.deepEquals(completedOp.value, newVisibleValue);
282
- if (!isRedundantSync) {
283
- if (previousVisibleValue === void 0 && newVisibleValue !== void 0) {
284
- events.push({
285
- type: `insert`,
286
- key,
287
- value: newVisibleValue
288
- });
289
- } else if (previousVisibleValue !== void 0 && newVisibleValue === void 0) {
290
- events.push({
291
- type: `delete`,
292
- key,
293
- value: previousVisibleValue
294
- });
295
- } else if (previousVisibleValue !== void 0 && newVisibleValue !== void 0 && !utils.deepEquals(previousVisibleValue, newVisibleValue)) {
296
- events.push({
297
- type: `update`,
298
- key,
299
- value: newVisibleValue,
300
- previousValue: previousVisibleValue
301
- });
302
- }
303
- }
304
- }
305
- this._size = this.calculateSize();
306
- if (events.length > 0) {
307
- this.updateIndexes(events);
308
- }
309
- this.emitEvents(events, true);
310
- this.pendingSyncedTransactions = uncommittedSyncedTransactions;
311
- this.preSyncVisibleState.clear();
312
- Promise.resolve().then(() => {
313
- this.recentlySyncedKeys.clear();
314
- });
315
- if (!this.hasReceivedFirstCommit) {
316
- this.hasReceivedFirstCommit = true;
317
- const callbacks = [...this.onFirstReadyCallbacks];
318
- this.onFirstReadyCallbacks = [];
319
- callbacks.forEach((callback) => callback());
320
- }
321
- }
322
- };
323
- this.insert = (data, config2) => {
324
- this.validateCollectionUsable(`insert`);
325
- const ambientTransaction = transactions.getActiveTransaction();
326
- if (!ambientTransaction && !this.config.onInsert) {
327
- throw new errors.MissingInsertHandlerError();
328
- }
329
- const items = Array.isArray(data) ? data : [data];
330
- const mutations = [];
331
- items.forEach((item) => {
332
- var _a, _b;
333
- const validatedData = this.validateData(item, `insert`);
334
- const key = this.getKeyFromItem(validatedData);
335
- if (this.has(key)) {
336
- throw new errors.DuplicateKeyError(key);
337
- }
338
- const globalKey = this.generateGlobalKey(key, item);
339
- const mutation = {
340
- mutationId: crypto.randomUUID(),
341
- original: {},
342
- modified: validatedData,
343
- // Pick the values from validatedData based on what's passed in - this is for cases
344
- // where a schema has default values. The validated data has the extra default
345
- // values but for changes, we just want to show the data that was actually passed in.
346
- changes: Object.fromEntries(
347
- Object.keys(item).map((k) => [
348
- k,
349
- validatedData[k]
350
- ])
351
- ),
352
- globalKey,
353
- key,
354
- metadata: config2 == null ? void 0 : config2.metadata,
355
- syncMetadata: ((_b = (_a = this.config.sync).getSyncMetadata) == null ? void 0 : _b.call(_a)) || {},
356
- optimistic: (config2 == null ? void 0 : config2.optimistic) ?? true,
357
- type: `insert`,
358
- createdAt: /* @__PURE__ */ new Date(),
359
- updatedAt: /* @__PURE__ */ new Date(),
360
- collection: this
361
- };
362
- mutations.push(mutation);
363
- });
364
- if (ambientTransaction) {
365
- ambientTransaction.applyMutations(mutations);
366
- this.transactions.set(ambientTransaction.id, ambientTransaction);
367
- this.scheduleTransactionCleanup(ambientTransaction);
368
- this.recomputeOptimisticState(true);
369
- return ambientTransaction;
370
- } else {
371
- const directOpTransaction = transactions.createTransaction({
372
- mutationFn: async (params) => {
373
- return await this.config.onInsert({
374
- transaction: params.transaction,
375
- collection: this
376
- });
377
- }
378
- });
379
- directOpTransaction.applyMutations(mutations);
380
- directOpTransaction.commit();
381
- this.transactions.set(directOpTransaction.id, directOpTransaction);
382
- this.scheduleTransactionCleanup(directOpTransaction);
383
- this.recomputeOptimisticState(true);
384
- return directOpTransaction;
385
- }
386
- };
387
- this.delete = (keys, config2) => {
388
- this.validateCollectionUsable(`delete`);
389
- const ambientTransaction = transactions.getActiveTransaction();
390
- if (!ambientTransaction && !this.config.onDelete) {
391
- throw new errors.MissingDeleteHandlerError();
392
- }
393
- if (Array.isArray(keys) && keys.length === 0) {
394
- throw new errors.NoKeysPassedToDeleteError();
395
- }
396
- const keysArray = Array.isArray(keys) ? keys : [keys];
397
- const mutations = [];
398
- for (const key of keysArray) {
399
- if (!this.has(key)) {
400
- throw new errors.DeleteKeyNotFoundError(key);
401
- }
402
- const globalKey = this.generateGlobalKey(key, this.get(key));
403
- const mutation = {
404
- mutationId: crypto.randomUUID(),
405
- original: this.get(key),
406
- modified: this.get(key),
407
- changes: this.get(key),
408
- globalKey,
409
- key,
410
- metadata: config2 == null ? void 0 : config2.metadata,
411
- syncMetadata: this.syncedMetadata.get(key) || {},
412
- optimistic: (config2 == null ? void 0 : config2.optimistic) ?? true,
413
- type: `delete`,
414
- createdAt: /* @__PURE__ */ new Date(),
415
- updatedAt: /* @__PURE__ */ new Date(),
416
- collection: this
417
- };
418
- mutations.push(mutation);
419
- }
420
- if (ambientTransaction) {
421
- ambientTransaction.applyMutations(mutations);
422
- this.transactions.set(ambientTransaction.id, ambientTransaction);
423
- this.scheduleTransactionCleanup(ambientTransaction);
424
- this.recomputeOptimisticState(true);
425
- return ambientTransaction;
426
- }
427
- const directOpTransaction = transactions.createTransaction({
428
- autoCommit: true,
429
- mutationFn: async (params) => {
430
- return this.config.onDelete({
431
- transaction: params.transaction,
432
- collection: this
433
- });
434
- }
435
- });
436
- directOpTransaction.applyMutations(mutations);
437
- directOpTransaction.commit();
438
- this.transactions.set(directOpTransaction.id, directOpTransaction);
439
- this.scheduleTransactionCleanup(directOpTransaction);
440
- this.recomputeOptimisticState(true);
441
- return directOpTransaction;
442
- };
443
- if (!config) {
444
- throw new errors.CollectionRequiresConfigError();
445
- }
446
- if (config.id) {
447
- this.id = config.id;
448
- } else {
449
- this.id = crypto.randomUUID();
450
- }
451
- if (!config.sync) {
452
- throw new errors.CollectionRequiresSyncConfigError();
453
- }
454
- this.transactions = new SortedMap.SortedMap(
455
- (a, b) => a.compareCreatedAt(b)
456
- );
457
- this.config = {
458
- ...config,
459
- autoIndex: config.autoIndex ?? `eager`
460
- };
461
- if (this.config.compare) {
462
- this.syncedData = new SortedMap.SortedMap(this.config.compare);
463
- } else {
464
- this.syncedData = /* @__PURE__ */ new Map();
465
- }
466
- this.events = new collectionEvents.CollectionEvents(this);
467
- if (config.startSync === true) {
468
- this.startSync();
469
- }
470
- }
471
- /**
472
- * Register a callback to be executed when the collection first becomes ready
473
- * Useful for preloading collections
474
- * @param callback Function to call when the collection first becomes ready
475
- * @example
476
- * collection.onFirstReady(() => {
477
- * console.log('Collection is ready for the first time')
478
- * // Safe to access collection.state now
479
- * })
480
- */
481
- onFirstReady(callback) {
482
- if (this.hasBeenReady) {
483
- callback();
484
- return;
485
- }
486
- this.onFirstReadyCallbacks.push(callback);
487
- }
488
- /**
489
- * Check if the collection is ready for use
490
- * Returns true if the collection has been marked as ready by its sync implementation
491
- * @returns true if the collection is ready, false otherwise
492
- * @example
493
- * if (collection.isReady()) {
494
- * console.log('Collection is ready, data is available')
495
- * // Safe to access collection.state
496
- * } else {
497
- * console.log('Collection is still loading')
498
- * }
499
- */
500
- isReady() {
501
- return this._status === `ready`;
502
- }
503
- /**
504
- * Mark the collection as ready for use
505
- * This is called by sync implementations to explicitly signal that the collection is ready,
506
- * providing a more intuitive alternative to using commits for readiness signaling
507
- * @private - Should only be called by sync implementations
508
- */
509
- markReady() {
510
- if (this._status === `loading` || this._status === `initialCommit`) {
511
- this.setStatus(`ready`);
512
- if (!this.hasBeenReady) {
513
- this.hasBeenReady = true;
514
- if (!this.hasReceivedFirstCommit) {
515
- this.hasReceivedFirstCommit = true;
516
- }
517
- const callbacks = [...this.onFirstReadyCallbacks];
518
- this.onFirstReadyCallbacks = [];
519
- callbacks.forEach((callback) => callback());
520
- }
521
- }
522
- if (this.changeListeners.size > 0) {
523
- this.emitEmptyReadyEvent();
524
- }
525
- }
526
- /**
527
- * Gets the current status of the collection
528
- */
529
- get status() {
530
- return this._status;
531
- }
532
- /**
533
- * Get the number of subscribers to the collection
534
- */
535
- get subscriberCount() {
536
- return this.activeSubscribersCount;
537
- }
538
- /**
539
- * Validates that the collection is in a usable state for data operations
540
- * @private
541
- */
542
- validateCollectionUsable(operation) {
543
- switch (this._status) {
544
- case `error`:
545
- throw new errors.CollectionInErrorStateError(operation, this.id);
546
- case `cleaned-up`:
547
- this.startSync();
548
- break;
549
- }
550
- }
551
- /**
552
- * Validates state transitions to prevent invalid status changes
553
- * @private
554
- */
555
- validateStatusTransition(from, to) {
556
- if (from === to) {
557
- return;
558
- }
559
- const validTransitions = {
560
- idle: [`loading`, `error`, `cleaned-up`],
561
- loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],
562
- initialCommit: [`ready`, `error`, `cleaned-up`],
563
- ready: [`cleaned-up`, `error`],
564
- error: [`cleaned-up`, `idle`],
565
- "cleaned-up": [`loading`, `error`]
566
- };
567
- if (!validTransitions[from].includes(to)) {
568
- throw new errors.InvalidCollectionStatusTransitionError(from, to, this.id);
569
- }
570
- }
571
- /**
572
- * Safely update the collection status with validation
573
- * @private
574
- */
575
- setStatus(newStatus) {
576
- this.validateStatusTransition(this._status, newStatus);
577
- const previousStatus = this._status;
578
- this._status = newStatus;
579
- if (newStatus === `ready` && !this.isIndexesResolved) {
580
- this.resolveAllIndexes().catch((error) => {
581
- console.warn(`Failed to resolve indexes:`, error);
582
- });
583
- }
584
- this.events.emitStatusChange(newStatus, previousStatus);
585
- }
586
- /**
587
- * Start sync immediately - internal method for compiled queries
588
- * This bypasses lazy loading for special cases like live query results
589
- */
590
- startSyncImmediate() {
591
- this.startSync();
592
- }
593
- /**
594
- * Start the sync process for this collection
595
- * This is called when the collection is first accessed or preloaded
596
- */
597
- startSync() {
598
- if (this._status !== `idle` && this._status !== `cleaned-up`) {
599
- return;
600
- }
601
- this.setStatus(`loading`);
602
- try {
603
- const cleanupFn = this.config.sync.sync({
604
- collection: this,
605
- begin: () => {
606
- this.pendingSyncedTransactions.push({
607
- committed: false,
608
- operations: [],
609
- deletedKeys: /* @__PURE__ */ new Set()
610
- });
611
- },
612
- write: (messageWithoutKey) => {
613
- const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
614
- if (!pendingTransaction) {
615
- throw new errors.NoPendingSyncTransactionWriteError();
616
- }
617
- if (pendingTransaction.committed) {
618
- throw new errors.SyncTransactionAlreadyCommittedWriteError();
619
- }
620
- const key = this.getKeyFromItem(messageWithoutKey.value);
621
- if (messageWithoutKey.type === `insert`) {
622
- const insertingIntoExistingSynced = this.syncedData.has(key);
623
- const hasPendingDeleteForKey = pendingTransaction.deletedKeys.has(key);
624
- const isTruncateTransaction = pendingTransaction.truncate === true;
625
- if (insertingIntoExistingSynced && !hasPendingDeleteForKey && !isTruncateTransaction) {
626
- throw new errors.DuplicateKeySyncError(key, this.id);
627
- }
628
- }
629
- const message = {
630
- ...messageWithoutKey,
631
- key
632
- };
633
- pendingTransaction.operations.push(message);
634
- if (messageWithoutKey.type === `delete`) {
635
- pendingTransaction.deletedKeys.add(key);
636
- }
637
- },
638
- commit: () => {
639
- const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
640
- if (!pendingTransaction) {
641
- throw new errors.NoPendingSyncTransactionCommitError();
642
- }
643
- if (pendingTransaction.committed) {
644
- throw new errors.SyncTransactionAlreadyCommittedError();
645
- }
646
- pendingTransaction.committed = true;
647
- if (this._status === `loading`) {
648
- this.setStatus(`initialCommit`);
649
- }
650
- this.commitPendingTransactions();
651
- },
652
- markReady: () => {
653
- this.markReady();
654
- },
655
- truncate: () => {
656
- const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
657
- if (!pendingTransaction) {
658
- throw new errors.NoPendingSyncTransactionWriteError();
659
- }
660
- if (pendingTransaction.committed) {
661
- throw new errors.SyncTransactionAlreadyCommittedWriteError();
662
- }
663
- pendingTransaction.operations = [];
664
- pendingTransaction.deletedKeys.clear();
665
- pendingTransaction.truncate = true;
666
- }
667
- });
668
- this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null;
669
- } catch (error) {
670
- this.setStatus(`error`);
671
- throw error;
672
- }
673
- }
674
- /**
675
- * Preload the collection data by starting sync if not already started
676
- * Multiple concurrent calls will share the same promise
677
- */
678
- preload() {
679
- if (this.preloadPromise) {
680
- return this.preloadPromise;
681
- }
682
- this.preloadPromise = new Promise((resolve, reject) => {
683
- if (this._status === `ready`) {
684
- resolve();
685
- return;
686
- }
687
- if (this._status === `error`) {
688
- reject(new errors.CollectionIsInErrorStateError());
689
- return;
690
- }
691
- this.onFirstReady(() => {
692
- resolve();
693
- });
694
- if (this._status === `idle` || this._status === `cleaned-up`) {
695
- try {
696
- this.startSync();
697
- } catch (error) {
698
- reject(error);
699
- return;
700
- }
701
- }
702
- });
703
- return this.preloadPromise;
704
- }
705
- /**
706
- * Clean up the collection by stopping sync and clearing data
707
- * This can be called manually or automatically by garbage collection
708
- */
709
- async cleanup() {
710
- if (this.gcTimeoutId) {
711
- clearTimeout(this.gcTimeoutId);
712
- this.gcTimeoutId = null;
713
- }
714
- try {
715
- if (this.syncCleanupFn) {
716
- this.syncCleanupFn();
717
- this.syncCleanupFn = null;
718
- }
719
- } catch (error) {
720
- queueMicrotask(() => {
721
- if (error instanceof Error) {
722
- const wrappedError = new errors.SyncCleanupError(this.id, error);
723
- wrappedError.cause = error;
724
- wrappedError.stack = error.stack;
725
- throw wrappedError;
726
- } else {
727
- throw new errors.SyncCleanupError(this.id, error);
728
- }
729
- });
730
- }
731
- this.syncedData.clear();
732
- this.syncedMetadata.clear();
733
- this.optimisticUpserts.clear();
734
- this.optimisticDeletes.clear();
735
- this._size = 0;
736
- this.pendingSyncedTransactions = [];
737
- this.syncedKeys.clear();
738
- this.hasReceivedFirstCommit = false;
739
- this.hasBeenReady = false;
740
- this.onFirstReadyCallbacks = [];
741
- this.preloadPromise = null;
742
- this.batchedEvents = [];
743
- this.shouldBatchEvents = false;
744
- this.events.cleanup();
745
- this.setStatus(`cleaned-up`);
746
- return Promise.resolve();
747
- }
748
- /**
749
- * Start the garbage collection timer
750
- * Called when the collection becomes inactive (no subscribers)
751
- */
752
- startGCTimer() {
753
- if (this.gcTimeoutId) {
754
- clearTimeout(this.gcTimeoutId);
755
- }
756
- const gcTime = this.config.gcTime ?? 3e5;
757
- if (gcTime === 0) {
758
- return;
759
- }
760
- this.gcTimeoutId = setTimeout(() => {
761
- if (this.activeSubscribersCount === 0) {
762
- this.cleanup();
763
- }
764
- }, gcTime);
765
- }
766
- /**
767
- * Cancel the garbage collection timer
768
- * Called when the collection becomes active again
769
- */
770
- cancelGCTimer() {
771
- if (this.gcTimeoutId) {
772
- clearTimeout(this.gcTimeoutId);
773
- this.gcTimeoutId = null;
774
- }
775
- }
776
- /**
777
- * Increment the active subscribers count and start sync if needed
778
- */
779
- addSubscriber() {
780
- const previousSubscriberCount = this.activeSubscribersCount;
781
- this.activeSubscribersCount++;
782
- this.cancelGCTimer();
783
- if (this._status === `cleaned-up` || this._status === `idle`) {
784
- this.startSync();
785
- }
786
- this.events.emitSubscribersChange(
787
- this.activeSubscribersCount,
788
- previousSubscriberCount
789
- );
790
- }
791
- /**
792
- * Decrement the active subscribers count and start GC timer if needed
793
- */
794
- removeSubscriber() {
795
- const previousSubscriberCount = this.activeSubscribersCount;
796
- this.activeSubscribersCount--;
797
- if (this.activeSubscribersCount === 0) {
798
- this.startGCTimer();
799
- } else if (this.activeSubscribersCount < 0) {
800
- throw new errors.NegativeActiveSubscribersError();
801
- }
802
- this.events.emitSubscribersChange(
803
- this.activeSubscribersCount,
804
- previousSubscriberCount
805
- );
806
- }
807
- /**
808
- * Recompute optimistic state from active transactions
809
- */
810
- recomputeOptimisticState(triggeredByUserAction = false) {
811
- if (this.isCommittingSyncTransactions) {
812
- return;
813
- }
814
- const previousState = new Map(this.optimisticUpserts);
815
- const previousDeletes = new Set(this.optimisticDeletes);
816
- this.optimisticUpserts.clear();
817
- this.optimisticDeletes.clear();
818
- const activeTransactions = [];
819
- for (const transaction of this.transactions.values()) {
820
- if (![`completed`, `failed`].includes(transaction.state)) {
821
- activeTransactions.push(transaction);
822
- }
823
- }
824
- for (const transaction of activeTransactions) {
825
- for (const mutation of transaction.mutations) {
826
- if (mutation.collection === this && mutation.optimistic) {
827
- switch (mutation.type) {
828
- case `insert`:
829
- case `update`:
830
- this.optimisticUpserts.set(
831
- mutation.key,
832
- mutation.modified
833
- );
834
- this.optimisticDeletes.delete(mutation.key);
835
- break;
836
- case `delete`:
837
- this.optimisticUpserts.delete(mutation.key);
838
- this.optimisticDeletes.add(mutation.key);
839
- break;
840
- }
841
- }
842
- }
843
- }
844
- this._size = this.calculateSize();
845
- const events = [];
846
- this.collectOptimisticChanges(previousState, previousDeletes, events);
847
- const filteredEventsBySyncStatus = events.filter((event) => {
848
- if (!this.recentlySyncedKeys.has(event.key)) {
849
- return true;
850
- }
851
- if (triggeredByUserAction) {
852
- return true;
853
- }
854
- return false;
855
- });
856
- if (this.pendingSyncedTransactions.length > 0 && !triggeredByUserAction) {
857
- const pendingSyncKeys = /* @__PURE__ */ new Set();
858
- for (const transaction of this.pendingSyncedTransactions) {
859
- for (const operation of transaction.operations) {
860
- pendingSyncKeys.add(operation.key);
861
- }
862
- }
863
- const filteredEvents = filteredEventsBySyncStatus.filter((event) => {
864
- if (event.type === `delete` && pendingSyncKeys.has(event.key)) {
865
- const hasActiveOptimisticMutation = activeTransactions.some(
866
- (tx) => tx.mutations.some(
867
- (m) => m.collection === this && m.key === event.key
868
- )
869
- );
870
- if (!hasActiveOptimisticMutation) {
871
- return false;
872
- }
873
- }
874
- return true;
875
- });
876
- if (filteredEvents.length > 0) {
877
- this.updateIndexes(filteredEvents);
878
- }
879
- this.emitEvents(filteredEvents, triggeredByUserAction);
880
- } else {
881
- if (filteredEventsBySyncStatus.length > 0) {
882
- this.updateIndexes(filteredEventsBySyncStatus);
883
- }
884
- this.emitEvents(filteredEventsBySyncStatus, triggeredByUserAction);
885
- }
886
- }
887
- /**
888
- * Calculate the current size based on synced data and optimistic changes
889
- */
890
- calculateSize() {
891
- const syncedSize = this.syncedData.size;
892
- const deletesFromSynced = Array.from(this.optimisticDeletes).filter(
893
- (key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key)
894
- ).length;
895
- const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter(
896
- (key) => !this.syncedData.has(key)
897
- ).length;
898
- return syncedSize - deletesFromSynced + upsertsNotInSynced;
899
- }
900
- /**
901
- * Collect events for optimistic changes
902
- */
903
- collectOptimisticChanges(previousUpserts, previousDeletes, events) {
904
- const allKeys = /* @__PURE__ */ new Set([
905
- ...previousUpserts.keys(),
906
- ...this.optimisticUpserts.keys(),
907
- ...previousDeletes,
908
- ...this.optimisticDeletes
909
- ]);
910
- for (const key of allKeys) {
911
- const currentValue = this.get(key);
912
- const previousValue = this.getPreviousValue(
913
- key,
914
- previousUpserts,
915
- previousDeletes
916
- );
917
- if (previousValue !== void 0 && currentValue === void 0) {
918
- events.push({ type: `delete`, key, value: previousValue });
919
- } else if (previousValue === void 0 && currentValue !== void 0) {
920
- events.push({ type: `insert`, key, value: currentValue });
921
- } else if (previousValue !== void 0 && currentValue !== void 0 && previousValue !== currentValue) {
922
- events.push({
923
- type: `update`,
924
- key,
925
- value: currentValue,
926
- previousValue
927
- });
928
- }
929
- }
930
- }
931
- /**
932
- * Get the previous value for a key given previous optimistic state
933
- */
934
- getPreviousValue(key, previousUpserts, previousDeletes) {
935
- if (previousDeletes.has(key)) {
936
- return void 0;
937
- }
938
- if (previousUpserts.has(key)) {
939
- return previousUpserts.get(key);
940
- }
941
- return this.syncedData.get(key);
942
- }
943
- /**
944
- * Emit an empty ready event to notify subscribers that the collection is ready
945
- * This bypasses the normal empty array check in emitEvents
946
- */
947
- emitEmptyReadyEvent() {
948
- for (const listener of this.changeListeners) {
949
- listener([]);
950
- }
951
- for (const [_key, keyListeners] of this.changeKeyListeners) {
952
- for (const listener of keyListeners) {
953
- listener([]);
954
- }
955
- }
956
- }
957
- /**
958
- * Emit events either immediately or batch them for later emission
959
- */
960
- emitEvents(changes, forceEmit = false) {
961
- if (this.shouldBatchEvents && !forceEmit) {
962
- this.batchedEvents.push(...changes);
963
- return;
964
- }
965
- let eventsToEmit = changes;
966
- if (this.batchedEvents.length > 0 && forceEmit) {
967
- eventsToEmit = [...this.batchedEvents, ...changes];
968
- this.batchedEvents = [];
969
- this.shouldBatchEvents = false;
970
- }
971
- if (eventsToEmit.length === 0) return;
972
- for (const listener of this.changeListeners) {
973
- listener(eventsToEmit);
974
- }
975
- if (this.changeKeyListeners.size > 0) {
976
- const changesByKey = /* @__PURE__ */ new Map();
977
- for (const change of eventsToEmit) {
978
- if (this.changeKeyListeners.has(change.key)) {
979
- if (!changesByKey.has(change.key)) {
980
- changesByKey.set(change.key, []);
981
- }
982
- changesByKey.get(change.key).push(change);
983
- }
984
- }
985
- for (const [key, keyChanges] of changesByKey) {
986
- const keyListeners = this.changeKeyListeners.get(key);
987
- for (const listener of keyListeners) {
988
- listener(keyChanges);
989
- }
990
- }
991
- }
992
- }
993
- /**
994
- * Get the current value for a key (virtual derived state)
995
- */
996
- get(key) {
997
- if (this.optimisticDeletes.has(key)) {
998
- return void 0;
999
- }
1000
- if (this.optimisticUpserts.has(key)) {
1001
- return this.optimisticUpserts.get(key);
1002
- }
1003
- return this.syncedData.get(key);
1004
- }
1005
- /**
1006
- * Check if a key exists in the collection (virtual derived state)
1007
- */
1008
- has(key) {
1009
- if (this.optimisticDeletes.has(key)) {
1010
- return false;
1011
- }
1012
- if (this.optimisticUpserts.has(key)) {
1013
- return true;
1014
- }
1015
- return this.syncedData.has(key);
1016
- }
1017
- /**
1018
- * Get the current size of the collection (cached)
1019
- */
1020
- get size() {
1021
- return this._size;
1022
- }
1023
- /**
1024
- * Get all keys (virtual derived state)
1025
- */
1026
- *keys() {
1027
- for (const key of this.syncedData.keys()) {
1028
- if (!this.optimisticDeletes.has(key)) {
1029
- yield key;
1030
- }
1031
- }
1032
- for (const key of this.optimisticUpserts.keys()) {
1033
- if (!this.syncedData.has(key) && !this.optimisticDeletes.has(key)) {
1034
- yield key;
1035
- }
1036
- }
1037
- }
1038
- /**
1039
- * Get all values (virtual derived state)
1040
- */
1041
- *values() {
1042
- for (const key of this.keys()) {
1043
- const value = this.get(key);
1044
- if (value !== void 0) {
1045
- yield value;
1046
- }
1047
- }
1048
- }
1049
- /**
1050
- * Get all entries (virtual derived state)
1051
- */
1052
- *entries() {
1053
- for (const key of this.keys()) {
1054
- const value = this.get(key);
1055
- if (value !== void 0) {
1056
- yield [key, value];
1057
- }
1058
- }
1059
- }
1060
- /**
1061
- * Get all entries (virtual derived state)
1062
- */
1063
- *[Symbol.iterator]() {
1064
- for (const [key, value] of this.entries()) {
1065
- yield [key, value];
1066
- }
1067
- }
1068
- /**
1069
- * Execute a callback for each entry in the collection
1070
- */
1071
- forEach(callbackfn) {
1072
- let index = 0;
1073
- for (const [key, value] of this.entries()) {
1074
- callbackfn(value, key, index++);
1075
- }
1076
- }
1077
- /**
1078
- * Create a new array with the results of calling a function for each entry in the collection
1079
- */
1080
- map(callbackfn) {
1081
- const result = [];
1082
- let index = 0;
1083
- for (const [key, value] of this.entries()) {
1084
- result.push(callbackfn(value, key, index++));
1085
- }
1086
- return result;
1087
- }
1088
- /**
1089
- * Schedule cleanup of a transaction when it completes
1090
- * @private
1091
- */
1092
- scheduleTransactionCleanup(transaction) {
1093
- if (transaction.state === `completed`) {
1094
- this.transactions.delete(transaction.id);
1095
- return;
1096
- }
1097
- transaction.isPersisted.promise.then(() => {
1098
- this.transactions.delete(transaction.id);
1099
- }).catch(() => {
1100
- });
1101
- }
1102
- ensureStandardSchema(schema) {
1103
- if (schema && `~standard` in schema) {
1104
- return schema;
1105
- }
1106
- throw new errors.InvalidSchemaError();
1107
- }
1108
- getKeyFromItem(item) {
1109
- return this.config.getKey(item);
1110
- }
1111
- generateGlobalKey(key, item) {
1112
- if (typeof key === `undefined`) {
1113
- throw new errors.UndefinedKeyError(item);
1114
- }
1115
- return `KEY::${this.id}/${key}`;
1116
- }
1117
- /**
1118
- * Creates an index on a collection for faster queries.
1119
- * Indexes significantly improve query performance by allowing constant time lookups
1120
- * and logarithmic time range queries instead of full scans.
1121
- *
1122
- * @template TResolver - The type of the index resolver (constructor or async loader)
1123
- * @param indexCallback - Function that extracts the indexed value from each item
1124
- * @param config - Configuration including index type and type-specific options
1125
- * @returns An index proxy that provides access to the index when ready
1126
- *
1127
- * @example
1128
- * // Create a default B+ tree index
1129
- * const ageIndex = collection.createIndex((row) => row.age)
1130
- *
1131
- * // Create a ordered index with custom options
1132
- * const ageIndex = collection.createIndex((row) => row.age, {
1133
- * indexType: BTreeIndex,
1134
- * options: { compareFn: customComparator },
1135
- * name: 'age_btree'
1136
- * })
1137
- *
1138
- * // Create an async-loaded index
1139
- * const textIndex = collection.createIndex((row) => row.content, {
1140
- * indexType: async () => {
1141
- * const { FullTextIndex } = await import('./indexes/fulltext.js')
1142
- * return FullTextIndex
1143
- * },
1144
- * options: { language: 'en' }
1145
- * })
1146
- */
1147
- createIndex(indexCallback, config = {}) {
1148
- this.validateCollectionUsable(`createIndex`);
1149
- const indexId = ++this.indexCounter;
1150
- const singleRowRefProxy = refProxy.createSingleRowRefProxy();
1151
- const indexExpression = indexCallback(singleRowRefProxy);
1152
- const expression = refProxy.toExpression(indexExpression);
1153
- const resolver = config.indexType ?? btreeIndex.BTreeIndex;
1154
- const lazyIndex$1 = new lazyIndex.LazyIndexWrapper(
1155
- indexId,
1156
- expression,
1157
- config.name,
1158
- resolver,
1159
- config.options,
1160
- this.entries()
1161
- );
1162
- this.lazyIndexes.set(indexId, lazyIndex$1);
1163
- if (resolver === btreeIndex.BTreeIndex) {
1164
- try {
1165
- const resolvedIndex = lazyIndex$1.getResolved();
1166
- this.resolvedIndexes.set(indexId, resolvedIndex);
1167
- } catch (error) {
1168
- console.warn(`Failed to resolve BTreeIndex:`, error);
1169
- }
1170
- } else if (typeof resolver === `function` && resolver.prototype) {
1171
- try {
1172
- const resolvedIndex = lazyIndex$1.getResolved();
1173
- this.resolvedIndexes.set(indexId, resolvedIndex);
1174
- } catch {
1175
- this.resolveSingleIndex(indexId, lazyIndex$1).catch((error) => {
1176
- console.warn(`Failed to resolve single index:`, error);
1177
- });
1178
- }
1179
- } else if (this.isIndexesResolved) {
1180
- this.resolveSingleIndex(indexId, lazyIndex$1).catch((error) => {
1181
- console.warn(`Failed to resolve single index:`, error);
1182
- });
1183
- }
1184
- return new lazyIndex.IndexProxy(indexId, lazyIndex$1);
1185
- }
1186
- /**
1187
- * Resolve all lazy indexes (called when collection first syncs)
1188
- * @private
1189
- */
1190
- async resolveAllIndexes() {
1191
- if (this.isIndexesResolved) return;
1192
- const resolutionPromises = Array.from(this.lazyIndexes.entries()).map(
1193
- async ([indexId, lazyIndex2]) => {
1194
- const resolvedIndex = await lazyIndex2.resolve();
1195
- resolvedIndex.build(this.entries());
1196
- this.resolvedIndexes.set(indexId, resolvedIndex);
1197
- return { indexId, resolvedIndex };
1198
- }
1199
- );
1200
- await Promise.all(resolutionPromises);
1201
- this.isIndexesResolved = true;
1202
- }
1203
- /**
1204
- * Resolve a single index immediately
1205
- * @private
1206
- */
1207
- async resolveSingleIndex(indexId, lazyIndex2) {
1208
- const resolvedIndex = await lazyIndex2.resolve();
1209
- resolvedIndex.build(this.entries());
1210
- this.resolvedIndexes.set(indexId, resolvedIndex);
1211
- return resolvedIndex;
1212
- }
1213
- /**
1214
- * Get resolved indexes for query optimization
1215
- */
1216
- get indexes() {
1217
- return this.resolvedIndexes;
1218
- }
1219
- /**
1220
- * Updates all indexes when the collection changes
1221
- * @private
1222
- */
1223
- updateIndexes(changes) {
1224
- for (const index of this.resolvedIndexes.values()) {
1225
- for (const change of changes) {
1226
- switch (change.type) {
1227
- case `insert`:
1228
- index.add(change.key, change.value);
1229
- break;
1230
- case `update`:
1231
- if (change.previousValue) {
1232
- index.update(change.key, change.previousValue, change.value);
1233
- } else {
1234
- index.add(change.key, change.value);
1235
- }
1236
- break;
1237
- case `delete`:
1238
- index.remove(change.key, change.value);
1239
- break;
1240
- }
1241
- }
1242
- }
1243
- }
1244
- validateData(data, type, key) {
1245
- if (!this.config.schema) return data;
1246
- const standardSchema = this.ensureStandardSchema(this.config.schema);
1247
- if (type === `update` && key) {
1248
- const existingData = this.get(key);
1249
- if (existingData && data && typeof data === `object` && typeof existingData === `object`) {
1250
- const mergedData = Object.assign({}, existingData, data);
1251
- const result2 = standardSchema[`~standard`].validate(mergedData);
1252
- if (result2 instanceof Promise) {
1253
- throw new errors.SchemaMustBeSynchronousError();
1254
- }
1255
- if (`issues` in result2 && result2.issues) {
1256
- const typedIssues = result2.issues.map((issue) => {
1257
- var _a;
1258
- return {
1259
- message: issue.message,
1260
- path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
1261
- };
1262
- });
1263
- throw new errors.SchemaValidationError(type, typedIssues);
1264
- }
1265
- const validatedMergedData = result2.value;
1266
- const modifiedKeys = Object.keys(data);
1267
- const extractedChanges = Object.fromEntries(
1268
- modifiedKeys.map((k) => [k, validatedMergedData[k]])
1269
- );
1270
- return extractedChanges;
1271
- }
1272
- }
1273
- const result = standardSchema[`~standard`].validate(data);
1274
- if (result instanceof Promise) {
1275
- throw new errors.SchemaMustBeSynchronousError();
1276
- }
1277
- if (`issues` in result && result.issues) {
1278
- const typedIssues = result.issues.map((issue) => {
1279
- var _a;
1280
- return {
1281
- message: issue.message,
1282
- path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
1283
- };
1284
- });
1285
- throw new errors.SchemaValidationError(type, typedIssues);
1286
- }
1287
- return result.value;
1288
- }
1289
- update(keys, configOrCallback, maybeCallback) {
1290
- if (typeof keys === `undefined`) {
1291
- throw new errors.MissingUpdateArgumentError();
1292
- }
1293
- this.validateCollectionUsable(`update`);
1294
- const ambientTransaction = transactions.getActiveTransaction();
1295
- if (!ambientTransaction && !this.config.onUpdate) {
1296
- throw new errors.MissingUpdateHandlerError();
1297
- }
1298
- const isArray = Array.isArray(keys);
1299
- const keysArray = isArray ? keys : [keys];
1300
- if (isArray && keysArray.length === 0) {
1301
- throw new errors.NoKeysPassedToUpdateError();
1302
- }
1303
- const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
1304
- const config = typeof configOrCallback === `function` ? {} : configOrCallback;
1305
- const currentObjects = keysArray.map((key) => {
1306
- const item = this.get(key);
1307
- if (!item) {
1308
- throw new errors.UpdateKeyNotFoundError(key);
1309
- }
1310
- return item;
1311
- });
1312
- let changesArray;
1313
- if (isArray) {
1314
- changesArray = proxy.withArrayChangeTracking(
1315
- currentObjects,
1316
- callback
1317
- );
1318
- } else {
1319
- const result = proxy.withChangeTracking(
1320
- currentObjects[0],
1321
- callback
1322
- );
1323
- changesArray = [result];
1324
- }
1325
- const mutations = keysArray.map((key, index) => {
1326
- const itemChanges = changesArray[index];
1327
- if (!itemChanges || Object.keys(itemChanges).length === 0) {
1328
- return null;
1329
- }
1330
- const originalItem = currentObjects[index];
1331
- const validatedUpdatePayload = this.validateData(
1332
- itemChanges,
1333
- `update`,
1334
- key
1335
- );
1336
- const modifiedItem = Object.assign(
1337
- {},
1338
- originalItem,
1339
- validatedUpdatePayload
1340
- );
1341
- const originalItemId = this.getKeyFromItem(originalItem);
1342
- const modifiedItemId = this.getKeyFromItem(modifiedItem);
1343
- if (originalItemId !== modifiedItemId) {
1344
- throw new errors.KeyUpdateNotAllowedError(originalItemId, modifiedItemId);
1345
- }
1346
- const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem);
1347
- return {
1348
- mutationId: crypto.randomUUID(),
1349
- original: originalItem,
1350
- modified: modifiedItem,
1351
- // Pick the values from modifiedItem based on what's passed in - this is for cases
1352
- // where a schema has default values or transforms. The modified data has the extra
1353
- // default or transformed values but for changes, we just want to show the data that
1354
- // was actually passed in.
1355
- changes: Object.fromEntries(
1356
- Object.keys(itemChanges).map((k) => [
1357
- k,
1358
- modifiedItem[k]
1359
- ])
1360
- ),
1361
- globalKey,
1362
- key,
1363
- metadata: config.metadata,
1364
- syncMetadata: this.syncedMetadata.get(key) || {},
1365
- optimistic: config.optimistic ?? true,
1366
- type: `update`,
1367
- createdAt: /* @__PURE__ */ new Date(),
1368
- updatedAt: /* @__PURE__ */ new Date(),
1369
- collection: this
1370
- };
1371
- }).filter(Boolean);
1372
- if (mutations.length === 0) {
1373
- const emptyTransaction = transactions.createTransaction({
1374
- mutationFn: async () => {
1375
- }
1376
- });
1377
- emptyTransaction.commit();
1378
- this.scheduleTransactionCleanup(emptyTransaction);
1379
- return emptyTransaction;
1380
- }
1381
- if (ambientTransaction) {
1382
- ambientTransaction.applyMutations(mutations);
1383
- this.transactions.set(ambientTransaction.id, ambientTransaction);
1384
- this.scheduleTransactionCleanup(ambientTransaction);
1385
- this.recomputeOptimisticState(true);
1386
- return ambientTransaction;
1387
- }
1388
- const directOpTransaction = transactions.createTransaction({
1389
- mutationFn: async (params) => {
1390
- return this.config.onUpdate({
1391
- transaction: params.transaction,
1392
- collection: this
1393
- });
1394
- }
1395
- });
1396
- directOpTransaction.applyMutations(mutations);
1397
- directOpTransaction.commit();
1398
- this.transactions.set(directOpTransaction.id, directOpTransaction);
1399
- this.scheduleTransactionCleanup(directOpTransaction);
1400
- this.recomputeOptimisticState(true);
1401
- return directOpTransaction;
1402
- }
1403
- /**
1404
- * Gets the current state of the collection as a Map
1405
- * @returns Map containing all items in the collection, with keys as identifiers
1406
- * @example
1407
- * const itemsMap = collection.state
1408
- * console.log(`Collection has ${itemsMap.size} items`)
1409
- *
1410
- * for (const [key, item] of itemsMap) {
1411
- * console.log(`${key}: ${item.title}`)
1412
- * }
1413
- *
1414
- * // Check if specific item exists
1415
- * if (itemsMap.has("todo-1")) {
1416
- * console.log("Todo 1 exists:", itemsMap.get("todo-1"))
1417
- * }
1418
- */
1419
- get state() {
1420
- const result = /* @__PURE__ */ new Map();
1421
- for (const [key, value] of this.entries()) {
1422
- result.set(key, value);
1423
- }
1424
- return result;
1425
- }
1426
- /**
1427
- * Gets the current state of the collection as a Map, but only resolves when data is available
1428
- * Waits for the first sync commit to complete before resolving
1429
- *
1430
- * @returns Promise that resolves to a Map containing all items in the collection
1431
- */
1432
- stateWhenReady() {
1433
- if (this.size > 0 || this.isReady()) {
1434
- return Promise.resolve(this.state);
1435
- }
1436
- return this.preload().then(() => this.state);
1437
- }
1438
- /**
1439
- * Gets the current state of the collection as an Array
1440
- *
1441
- * @returns An Array containing all items in the collection
1442
- */
1443
- get toArray() {
1444
- return Array.from(this.values());
1445
- }
1446
- /**
1447
- * Gets the current state of the collection as an Array, but only resolves when data is available
1448
- * Waits for the first sync commit to complete before resolving
1449
- *
1450
- * @returns Promise that resolves to an Array containing all items in the collection
1451
- */
1452
- toArrayWhenReady() {
1453
- if (this.size > 0 || this.isReady()) {
1454
- return Promise.resolve(this.toArray);
1455
- }
1456
- return this.preload().then(() => this.toArray);
1457
- }
1458
- /**
1459
- * Returns the current state of the collection as an array of changes
1460
- * @param options - Options including optional where filter
1461
- * @returns An array of changes
1462
- * @example
1463
- * // Get all items as changes
1464
- * const allChanges = collection.currentStateAsChanges()
1465
- *
1466
- * // Get only items matching a condition
1467
- * const activeChanges = collection.currentStateAsChanges({
1468
- * where: (row) => row.status === 'active'
1469
- * })
1470
- *
1471
- * // Get only items using a pre-compiled expression
1472
- * const activeChanges = collection.currentStateAsChanges({
1473
- * whereExpression: eq(row.status, 'active')
1474
- * })
1475
- */
1476
- currentStateAsChanges(options = {}) {
1477
- return changeEvents.currentStateAsChanges(this, options);
1478
- }
1479
- /**
1480
- * Subscribe to changes in the collection
1481
- * @param callback - Function called when items change
1482
- * @param options - Subscription options including includeInitialState and where filter
1483
- * @returns Unsubscribe function - Call this to stop listening for changes
1484
- * @example
1485
- * // Basic subscription
1486
- * const unsubscribe = collection.subscribeChanges((changes) => {
1487
- * changes.forEach(change => {
1488
- * console.log(`${change.type}: ${change.key}`, change.value)
1489
- * })
1490
- * })
1491
- *
1492
- * // Later: unsubscribe()
1493
- *
1494
- * @example
1495
- * // Include current state immediately
1496
- * const unsubscribe = collection.subscribeChanges((changes) => {
1497
- * updateUI(changes)
1498
- * }, { includeInitialState: true })
1499
- *
1500
- * @example
1501
- * // Subscribe only to changes matching a condition
1502
- * const unsubscribe = collection.subscribeChanges((changes) => {
1503
- * updateUI(changes)
1504
- * }, {
1505
- * includeInitialState: true,
1506
- * where: (row) => row.status === 'active'
1507
- * })
1508
- *
1509
- * @example
1510
- * // Subscribe using a pre-compiled expression
1511
- * const unsubscribe = collection.subscribeChanges((changes) => {
1512
- * updateUI(changes)
1513
- * }, {
1514
- * includeInitialState: true,
1515
- * whereExpression: eq(row.status, 'active')
1516
- * })
1517
- */
1518
- subscribeChanges(callback, options = {}) {
1519
- this.addSubscriber();
1520
- if (options.whereExpression) {
1521
- autoIndex.ensureIndexForExpression(options.whereExpression, this);
1522
- }
1523
- const filteredCallback = options.where || options.whereExpression ? changeEvents.createFilteredCallback(callback, options) : callback;
1524
- if (options.includeInitialState) {
1525
- const initialChanges = this.currentStateAsChanges({
1526
- where: options.where,
1527
- whereExpression: options.whereExpression
1528
- });
1529
- filteredCallback(initialChanges);
1530
- }
1531
- this.changeListeners.add(filteredCallback);
1532
- return () => {
1533
- this.changeListeners.delete(filteredCallback);
1534
- this.removeSubscriber();
1535
- };
1536
- }
1537
- /**
1538
- * Subscribe to changes for a specific key
1539
- */
1540
- subscribeChangesKey(key, listener, { includeInitialState = false } = {}) {
1541
- this.addSubscriber();
1542
- if (!this.changeKeyListeners.has(key)) {
1543
- this.changeKeyListeners.set(key, /* @__PURE__ */ new Set());
1544
- }
1545
- if (includeInitialState) {
1546
- listener([
1547
- {
1548
- type: `insert`,
1549
- key,
1550
- value: this.get(key)
1551
- }
1552
- ]);
1553
- }
1554
- this.changeKeyListeners.get(key).add(listener);
1555
- return () => {
1556
- const listeners = this.changeKeyListeners.get(key);
1557
- if (listeners) {
1558
- listeners.delete(listener);
1559
- if (listeners.size === 0) {
1560
- this.changeKeyListeners.delete(key);
1561
- }
1562
- }
1563
- this.removeSubscriber();
1564
- };
1565
- }
1566
- /**
1567
- * Capture visible state for keys that will be affected by pending sync operations
1568
- * This must be called BEFORE onTransactionStateChange clears optimistic state
1569
- */
1570
- capturePreSyncVisibleState() {
1571
- if (this.pendingSyncedTransactions.length === 0) return;
1572
- this.preSyncVisibleState.clear();
1573
- const syncedKeys = /* @__PURE__ */ new Set();
1574
- for (const transaction of this.pendingSyncedTransactions) {
1575
- for (const operation of transaction.operations) {
1576
- syncedKeys.add(operation.key);
1577
- }
1578
- }
1579
- for (const key of syncedKeys) {
1580
- this.recentlySyncedKeys.add(key);
1581
- }
1582
- for (const key of syncedKeys) {
1583
- const currentValue = this.get(key);
1584
- if (currentValue !== void 0) {
1585
- this.preSyncVisibleState.set(key, currentValue);
1586
- }
1587
- }
1588
- }
1589
- /**
1590
- * Trigger a recomputation when transactions change
1591
- * This method should be called by the Transaction class when state changes
1592
- */
1593
- onTransactionStateChange() {
1594
- this.shouldBatchEvents = this.pendingSyncedTransactions.length > 0;
1595
- this.capturePreSyncVisibleState();
1596
- this.recomputeOptimisticState(false);
1597
- }
1598
- /**
1599
- * Subscribe to a collection event
1600
- */
1601
- on(event, callback) {
1602
- return this.events.on(event, callback);
1603
- }
1604
- /**
1605
- * Subscribe to a collection event once
1606
- */
1607
- once(event, callback) {
1608
- return this.events.once(event, callback);
1609
- }
1610
- /**
1611
- * Unsubscribe from a collection event
1612
- */
1613
- off(event, callback) {
1614
- this.events.off(event, callback);
1615
- }
1616
- /**
1617
- * Wait for a collection event
1618
- */
1619
- waitFor(event, timeout) {
1620
- return this.events.waitFor(event, timeout);
1621
- }
1622
- }
1623
- exports.CollectionImpl = CollectionImpl;
1624
- exports.createCollection = createCollection;
1625
- //# sourceMappingURL=collection.cjs.map