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