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