@tanstack/db 0.0.6 → 0.0.8

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 (43) hide show
  1. package/dist/cjs/collection.cjs +452 -286
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +115 -26
  4. package/dist/cjs/index.cjs +1 -1
  5. package/dist/cjs/index.d.cts +1 -1
  6. package/dist/cjs/proxy.cjs +2 -2
  7. package/dist/cjs/proxy.cjs.map +1 -1
  8. package/dist/cjs/query/compiled-query.cjs +24 -38
  9. package/dist/cjs/query/compiled-query.cjs.map +1 -1
  10. package/dist/cjs/query/compiled-query.d.cts +2 -2
  11. package/dist/cjs/query/order-by.cjs +41 -38
  12. package/dist/cjs/query/order-by.cjs.map +1 -1
  13. package/dist/cjs/query/schema.d.cts +3 -3
  14. package/dist/cjs/transactions.cjs +7 -6
  15. package/dist/cjs/transactions.cjs.map +1 -1
  16. package/dist/cjs/transactions.d.cts +9 -9
  17. package/dist/cjs/types.d.cts +36 -22
  18. package/dist/esm/collection.d.ts +115 -26
  19. package/dist/esm/collection.js +453 -287
  20. package/dist/esm/collection.js.map +1 -1
  21. package/dist/esm/index.d.ts +1 -1
  22. package/dist/esm/index.js +2 -2
  23. package/dist/esm/proxy.js +2 -2
  24. package/dist/esm/proxy.js.map +1 -1
  25. package/dist/esm/query/compiled-query.d.ts +2 -2
  26. package/dist/esm/query/compiled-query.js +25 -39
  27. package/dist/esm/query/compiled-query.js.map +1 -1
  28. package/dist/esm/query/order-by.js +41 -38
  29. package/dist/esm/query/order-by.js.map +1 -1
  30. package/dist/esm/query/schema.d.ts +3 -3
  31. package/dist/esm/transactions.d.ts +9 -9
  32. package/dist/esm/transactions.js +7 -6
  33. package/dist/esm/transactions.js.map +1 -1
  34. package/dist/esm/types.d.ts +36 -22
  35. package/package.json +2 -2
  36. package/src/collection.ts +652 -368
  37. package/src/index.ts +1 -1
  38. package/src/proxy.ts +2 -2
  39. package/src/query/compiled-query.ts +29 -39
  40. package/src/query/order-by.ts +69 -67
  41. package/src/query/schema.ts +3 -3
  42. package/src/transactions.ts +24 -22
  43. package/src/types.ts +54 -22
@@ -4,61 +4,59 @@ const store = require("@tanstack/store");
4
4
  const proxy = require("./proxy.cjs");
5
5
  const transactions = require("./transactions.cjs");
6
6
  const SortedMap = require("./SortedMap.cjs");
7
- const collectionsStore = new store.Store(/* @__PURE__ */ new Map());
8
- const loadingCollections = /* @__PURE__ */ new Map();
9
- function createCollection(config) {
10
- return new Collection(config);
7
+ const collectionsStore = /* @__PURE__ */ new Map();
8
+ const loadingCollectionResolvers = /* @__PURE__ */ new Map();
9
+ function createCollection(options) {
10
+ const collection = new CollectionImpl(options);
11
+ if (options.utils) {
12
+ collection.utils = { ...options.utils };
13
+ } else {
14
+ collection.utils = {};
15
+ }
16
+ return collection;
11
17
  }
12
18
  function preloadCollection(config) {
13
19
  if (!config.id) {
14
20
  throw new Error(`The id property is required for preloadCollection`);
15
21
  }
16
- if (collectionsStore.state.has(config.id) && !loadingCollections.has(config.id)) {
22
+ if (collectionsStore.has(config.id) && !loadingCollectionResolvers.has(config.id)) {
17
23
  return Promise.resolve(
18
- collectionsStore.state.get(config.id)
24
+ collectionsStore.get(config.id)
19
25
  );
20
26
  }
21
- if (loadingCollections.has(config.id)) {
22
- return loadingCollections.get(config.id);
27
+ if (loadingCollectionResolvers.has(config.id)) {
28
+ return loadingCollectionResolvers.get(config.id).promise;
23
29
  }
24
- if (!collectionsStore.state.has(config.id)) {
25
- collectionsStore.setState((prev) => {
26
- const next = new Map(prev);
27
- if (!config.id) {
28
- throw new Error(`The id property is required for preloadCollection`);
29
- }
30
- next.set(
31
- config.id,
32
- new Collection({
33
- id: config.id,
34
- getId: config.getId,
35
- sync: config.sync,
36
- schema: config.schema
37
- })
38
- );
39
- return next;
40
- });
30
+ if (!collectionsStore.has(config.id)) {
31
+ collectionsStore.set(
32
+ config.id,
33
+ createCollection({
34
+ id: config.id,
35
+ getKey: config.getKey,
36
+ sync: config.sync,
37
+ schema: config.schema
38
+ })
39
+ );
41
40
  }
42
- const collection = collectionsStore.state.get(config.id);
41
+ const collection = collectionsStore.get(config.id);
43
42
  let resolveFirstCommit;
44
43
  const firstCommitPromise = new Promise((resolve) => {
45
- resolveFirstCommit = () => {
46
- resolve(collection);
47
- };
44
+ resolveFirstCommit = resolve;
45
+ });
46
+ loadingCollectionResolvers.set(config.id, {
47
+ promise: firstCommitPromise,
48
+ resolve: resolveFirstCommit
48
49
  });
49
50
  collection.onFirstCommit(() => {
50
51
  if (!config.id) {
51
52
  throw new Error(`The id property is required for preloadCollection`);
52
53
  }
53
- if (loadingCollections.has(config.id)) {
54
- loadingCollections.delete(config.id);
55
- resolveFirstCommit();
54
+ if (loadingCollectionResolvers.has(config.id)) {
55
+ const resolver = loadingCollectionResolvers.get(config.id);
56
+ loadingCollectionResolvers.delete(config.id);
57
+ resolver.resolve(collection);
56
58
  }
57
59
  });
58
- loadingCollections.set(
59
- config.id,
60
- firstCommitPromise
61
- );
62
60
  return firstCommitPromise;
63
61
  }
64
62
  class SchemaValidationError extends Error {
@@ -70,7 +68,7 @@ class SchemaValidationError extends Error {
70
68
  this.issues = issues;
71
69
  }
72
70
  }
73
- class Collection {
71
+ class CollectionImpl {
74
72
  /**
75
73
  * Creates a new Collection instance
76
74
  *
@@ -78,58 +76,94 @@ class Collection {
78
76
  * @throws Error if sync config is missing
79
77
  */
80
78
  constructor(config) {
81
- this.syncedData = new store.Store(/* @__PURE__ */ new Map());
82
- this.syncedMetadata = new store.Store(/* @__PURE__ */ new Map());
79
+ this.syncedData = /* @__PURE__ */ new Map();
80
+ this.syncedMetadata = /* @__PURE__ */ new Map();
81
+ this.derivedUpserts = /* @__PURE__ */ new Map();
82
+ this.derivedDeletes = /* @__PURE__ */ new Set();
83
+ this._size = 0;
84
+ this.changeListeners = /* @__PURE__ */ new Set();
85
+ this.changeKeyListeners = /* @__PURE__ */ new Map();
86
+ this.utils = {};
83
87
  this.pendingSyncedTransactions = [];
84
88
  this.syncedKeys = /* @__PURE__ */ new Set();
85
89
  this.hasReceivedFirstCommit = false;
86
90
  this.onFirstCommitCallbacks = [];
87
91
  this.id = ``;
88
92
  this.commitPendingTransactions = () => {
89
- if (!Array.from(this.transactions.state.values()).some(
93
+ if (!Array.from(this.transactions.values()).some(
90
94
  ({ state }) => state === `persisting`
91
95
  )) {
92
- store.batch(() => {
93
- for (const transaction of this.pendingSyncedTransactions) {
94
- for (const operation of transaction.operations) {
95
- this.syncedKeys.add(operation.key);
96
- this.syncedMetadata.setState((prevData) => {
97
- switch (operation.type) {
98
- case `insert`:
99
- prevData.set(operation.key, operation.metadata);
100
- break;
101
- case `update`:
102
- prevData.set(operation.key, {
103
- ...prevData.get(operation.key),
104
- ...operation.metadata
105
- });
106
- break;
107
- case `delete`:
108
- prevData.delete(operation.key);
109
- break;
96
+ const changedKeys = /* @__PURE__ */ new Set();
97
+ const events = [];
98
+ for (const transaction of this.pendingSyncedTransactions) {
99
+ for (const operation of transaction.operations) {
100
+ const key = operation.key;
101
+ changedKeys.add(key);
102
+ this.syncedKeys.add(key);
103
+ switch (operation.type) {
104
+ case `insert`:
105
+ this.syncedMetadata.set(key, operation.metadata);
106
+ break;
107
+ case `update`:
108
+ this.syncedMetadata.set(
109
+ key,
110
+ Object.assign(
111
+ {},
112
+ this.syncedMetadata.get(key),
113
+ operation.metadata
114
+ )
115
+ );
116
+ break;
117
+ case `delete`:
118
+ this.syncedMetadata.delete(key);
119
+ break;
120
+ }
121
+ const previousValue = this.syncedData.get(key);
122
+ switch (operation.type) {
123
+ case `insert`:
124
+ this.syncedData.set(key, operation.value);
125
+ if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
126
+ events.push({
127
+ type: `insert`,
128
+ key,
129
+ value: operation.value
130
+ });
131
+ }
132
+ break;
133
+ case `update`: {
134
+ const updatedValue = Object.assign(
135
+ {},
136
+ this.syncedData.get(key),
137
+ operation.value
138
+ );
139
+ this.syncedData.set(key, updatedValue);
140
+ if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
141
+ events.push({
142
+ type: `update`,
143
+ key,
144
+ value: updatedValue,
145
+ previousValue
146
+ });
110
147
  }
111
- return prevData;
112
- });
113
- this.syncedData.setState((prevData) => {
114
- switch (operation.type) {
115
- case `insert`:
116
- prevData.set(operation.key, operation.value);
117
- break;
118
- case `update`:
119
- prevData.set(operation.key, {
120
- ...prevData.get(operation.key),
121
- ...operation.value
148
+ break;
149
+ }
150
+ case `delete`:
151
+ this.syncedData.delete(key);
152
+ if (!this.derivedDeletes.has(key) && !this.derivedUpserts.has(key)) {
153
+ if (previousValue) {
154
+ events.push({
155
+ type: `delete`,
156
+ key,
157
+ value: previousValue
122
158
  });
123
- break;
124
- case `delete`:
125
- prevData.delete(operation.key);
126
- break;
159
+ }
127
160
  }
128
- return prevData;
129
- });
161
+ break;
130
162
  }
131
163
  }
132
- });
164
+ }
165
+ this._size = this.calculateSize();
166
+ this.emitEvents(events);
133
167
  this.pendingSyncedTransactions = [];
134
168
  if (!this.hasReceivedFirstCommit) {
135
169
  this.hasReceivedFirstCommit = true;
@@ -148,22 +182,20 @@ class Collection {
148
182
  }
149
183
  const items = Array.isArray(data) ? data : [data];
150
184
  const mutations = [];
151
- const keys = items.map(
152
- (item) => this.generateObjectKey(this.config.getId(item), item)
153
- );
154
- items.forEach((item, index) => {
185
+ items.forEach((item) => {
155
186
  var _a, _b;
156
187
  const validatedData = this.validateData(item, `insert`);
157
- const key = keys[index];
158
- const id = this.config.getId(item);
159
- if (this.state.has(this.getKeyFromId(id))) {
160
- throw `Cannot insert document with ID "${id}" because it already exists in the collection`;
188
+ const key = this.getKeyFromItem(item);
189
+ if (this.has(key)) {
190
+ throw `Cannot insert document with ID "${key}" because it already exists in the collection`;
161
191
  }
192
+ const globalKey = this.generateGlobalKey(key, item);
162
193
  const mutation = {
163
194
  mutationId: crypto.randomUUID(),
164
195
  original: {},
165
196
  modified: validatedData,
166
197
  changes: validatedData,
198
+ globalKey,
167
199
  key,
168
200
  metadata: config2 == null ? void 0 : config2.metadata,
169
201
  syncMetadata: ((_b = (_a = this.config.sync).getSyncMetadata) == null ? void 0 : _b.call(_a)) || {},
@@ -176,10 +208,8 @@ class Collection {
176
208
  });
177
209
  if (ambientTransaction) {
178
210
  ambientTransaction.applyMutations(mutations);
179
- this.transactions.setState((sortedMap) => {
180
- sortedMap.set(ambientTransaction.id, ambientTransaction);
181
- return sortedMap;
182
- });
211
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
212
+ this.recomputeOptimisticState();
183
213
  return ambientTransaction;
184
214
  } else {
185
215
  const directOpTransaction = new transactions.Transaction({
@@ -189,33 +219,34 @@ class Collection {
189
219
  });
190
220
  directOpTransaction.applyMutations(mutations);
191
221
  directOpTransaction.commit();
192
- this.transactions.setState((sortedMap) => {
193
- sortedMap.set(directOpTransaction.id, directOpTransaction);
194
- return sortedMap;
195
- });
222
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
223
+ this.recomputeOptimisticState();
196
224
  return directOpTransaction;
197
225
  }
198
226
  };
199
- this.delete = (ids, config2) => {
227
+ this.delete = (keys, config2) => {
200
228
  const ambientTransaction = transactions.getActiveTransaction();
201
229
  if (!ambientTransaction && !this.config.onDelete) {
202
230
  throw new Error(
203
231
  `Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
204
232
  );
205
233
  }
206
- const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
207
- (id) => this.getKeyFromId(id)
208
- );
234
+ if (Array.isArray(keys) && keys.length === 0) {
235
+ throw new Error(`No keys were passed to delete`);
236
+ }
237
+ const keysArray = Array.isArray(keys) ? keys : [keys];
209
238
  const mutations = [];
210
- for (const id of idsArray) {
239
+ for (const key of keysArray) {
240
+ const globalKey = this.generateGlobalKey(key, this.get(key));
211
241
  const mutation = {
212
242
  mutationId: crypto.randomUUID(),
213
- original: this.state.get(id) || {},
214
- modified: this.state.get(id) || {},
215
- changes: this.state.get(id) || {},
216
- key: id,
243
+ original: this.get(key) || {},
244
+ modified: this.get(key),
245
+ changes: this.get(key) || {},
246
+ globalKey,
247
+ key,
217
248
  metadata: config2 == null ? void 0 : config2.metadata,
218
- syncMetadata: this.syncedMetadata.state.get(id) || {},
249
+ syncMetadata: this.syncedMetadata.get(key) || {},
219
250
  type: `delete`,
220
251
  createdAt: /* @__PURE__ */ new Date(),
221
252
  updatedAt: /* @__PURE__ */ new Date(),
@@ -225,24 +256,20 @@ class Collection {
225
256
  }
226
257
  if (ambientTransaction) {
227
258
  ambientTransaction.applyMutations(mutations);
228
- this.transactions.setState((sortedMap) => {
229
- sortedMap.set(ambientTransaction.id, ambientTransaction);
230
- return sortedMap;
231
- });
259
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
260
+ this.recomputeOptimisticState();
232
261
  return ambientTransaction;
233
262
  }
234
263
  const directOpTransaction = new transactions.Transaction({
235
264
  autoCommit: true,
236
- mutationFn: async (transaction) => {
237
- return this.config.onDelete(transaction);
265
+ mutationFn: async (params) => {
266
+ return this.config.onDelete(params);
238
267
  }
239
268
  });
240
269
  directOpTransaction.applyMutations(mutations);
241
270
  directOpTransaction.commit();
242
- this.transactions.setState((sortedMap) => {
243
- sortedMap.set(directOpTransaction.id, directOpTransaction);
244
- return sortedMap;
245
- });
271
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
272
+ this.recomputeOptimisticState();
246
273
  return directOpTransaction;
247
274
  };
248
275
  if (!config) {
@@ -256,121 +283,10 @@ class Collection {
256
283
  if (!config.sync) {
257
284
  throw new Error(`Collection requires a sync config`);
258
285
  }
259
- this.transactions = new store.Store(
260
- new SortedMap.SortedMap(
261
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
262
- )
286
+ this.transactions = new SortedMap.SortedMap(
287
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
263
288
  );
264
- this.optimisticOperations = new store.Derived({
265
- fn: ({ currDepVals: [transactions2] }) => {
266
- const result = Array.from(transactions2.values()).map((transaction) => {
267
- const isActive = ![`completed`, `failed`].includes(
268
- transaction.state
269
- );
270
- return transaction.mutations.filter((mutation) => mutation.collection === this).map((mutation) => {
271
- const message = {
272
- type: mutation.type,
273
- key: mutation.key,
274
- value: mutation.modified,
275
- isActive
276
- };
277
- if (mutation.metadata !== void 0 && mutation.metadata !== null) {
278
- message.metadata = mutation.metadata;
279
- }
280
- return message;
281
- });
282
- }).flat();
283
- return result;
284
- },
285
- deps: [this.transactions]
286
- });
287
- this.optimisticOperations.mount();
288
- this.derivedState = new store.Derived({
289
- fn: ({ currDepVals: [syncedData, operations] }) => {
290
- const combined = new Map(syncedData);
291
- for (const operation of operations) {
292
- if (operation.isActive) {
293
- switch (operation.type) {
294
- case `insert`:
295
- combined.set(operation.key, operation.value);
296
- break;
297
- case `update`:
298
- combined.set(operation.key, operation.value);
299
- break;
300
- case `delete`:
301
- combined.delete(operation.key);
302
- break;
303
- }
304
- }
305
- }
306
- return combined;
307
- },
308
- deps: [this.syncedData, this.optimisticOperations]
309
- });
310
- this.derivedArray = new store.Derived({
311
- fn: ({ currDepVals: [stateMap] }) => {
312
- const array = Array.from(
313
- stateMap.values()
314
- );
315
- if (array[0] && `_orderByIndex` in array[0]) {
316
- array.sort((a, b) => {
317
- if (a._orderByIndex === b._orderByIndex) {
318
- return 0;
319
- }
320
- return a._orderByIndex < b._orderByIndex ? -1 : 1;
321
- });
322
- }
323
- return array;
324
- },
325
- deps: [this.derivedState]
326
- });
327
- this.derivedArray.mount();
328
- this.derivedChanges = new store.Derived({
329
- fn: ({
330
- currDepVals: [derivedState, optimisticOperations],
331
- prevDepVals
332
- }) => {
333
- const prevDerivedState = (prevDepVals == null ? void 0 : prevDepVals[0]) ?? /* @__PURE__ */ new Map();
334
- const prevOptimisticOperations = (prevDepVals == null ? void 0 : prevDepVals[1]) ?? [];
335
- const changedKeys = new Set(this.syncedKeys);
336
- optimisticOperations.flat().filter((op) => op.isActive).forEach((op) => changedKeys.add(op.key));
337
- prevOptimisticOperations.flat().forEach((op) => {
338
- changedKeys.add(op.key);
339
- });
340
- if (changedKeys.size === 0) {
341
- return [];
342
- }
343
- const changes = [];
344
- for (const key of changedKeys) {
345
- if (prevDerivedState.has(key) && !derivedState.has(key)) {
346
- changes.push({
347
- type: `delete`,
348
- key,
349
- value: prevDerivedState.get(key)
350
- });
351
- } else if (!prevDerivedState.has(key) && derivedState.has(key)) {
352
- changes.push({ type: `insert`, key, value: derivedState.get(key) });
353
- } else if (prevDerivedState.has(key) && derivedState.has(key)) {
354
- const value = derivedState.get(key);
355
- const previousValue = prevDerivedState.get(key);
356
- if (value !== previousValue) {
357
- changes.push({
358
- type: `update`,
359
- key,
360
- value,
361
- previousValue
362
- });
363
- }
364
- }
365
- }
366
- this.syncedKeys.clear();
367
- return changes;
368
- },
369
- deps: [this.derivedState, this.optimisticOperations]
370
- });
371
- this.derivedChanges.mount();
372
289
  this.config = config;
373
- this.derivedState.mount();
374
290
  config.sync.sync({
375
291
  collection: this,
376
292
  begin: () => {
@@ -389,17 +305,13 @@ class Collection {
389
305
  `The pending sync transaction is already committed, you can't still write to it.`
390
306
  );
391
307
  }
392
- const key = this.generateObjectKey(
393
- this.config.getId(messageWithoutKey.value),
394
- messageWithoutKey.value
395
- );
308
+ const key = this.getKeyFromItem(messageWithoutKey.value);
396
309
  if (messageWithoutKey.type === `insert`) {
397
- if (this.syncedData.state.has(key) && !pendingTransaction.operations.some(
310
+ if (this.syncedData.has(key) && !pendingTransaction.operations.some(
398
311
  (op) => op.key === key && op.type === `delete`
399
312
  )) {
400
- const id = this.config.getId(messageWithoutKey.value);
401
313
  throw new Error(
402
- `Cannot insert document with ID "${id}" from sync because it already exists in the collection "${this.id}"`
314
+ `Cannot insert document with key "${key}" from sync because it already exists in the collection "${this.id}"`
403
315
  );
404
316
  }
405
317
  }
@@ -432,6 +344,189 @@ class Collection {
432
344
  onFirstCommit(callback) {
433
345
  this.onFirstCommitCallbacks.push(callback);
434
346
  }
347
+ /**
348
+ * Recompute optimistic state from active transactions
349
+ */
350
+ recomputeOptimisticState() {
351
+ const previousState = new Map(this.derivedUpserts);
352
+ const previousDeletes = new Set(this.derivedDeletes);
353
+ this.derivedUpserts.clear();
354
+ this.derivedDeletes.clear();
355
+ const activeTransactions = Array.from(this.transactions.values());
356
+ for (const transaction of activeTransactions) {
357
+ if (![`completed`, `failed`].includes(transaction.state)) {
358
+ for (const mutation of transaction.mutations) {
359
+ if (mutation.collection === this) {
360
+ switch (mutation.type) {
361
+ case `insert`:
362
+ case `update`:
363
+ this.derivedUpserts.set(mutation.key, mutation.modified);
364
+ this.derivedDeletes.delete(mutation.key);
365
+ break;
366
+ case `delete`:
367
+ this.derivedUpserts.delete(mutation.key);
368
+ this.derivedDeletes.add(mutation.key);
369
+ break;
370
+ }
371
+ }
372
+ }
373
+ }
374
+ }
375
+ this._size = this.calculateSize();
376
+ const events = [];
377
+ this.collectOptimisticChanges(previousState, previousDeletes, events);
378
+ this.emitEvents(events);
379
+ }
380
+ /**
381
+ * Calculate the current size based on synced data and optimistic changes
382
+ */
383
+ calculateSize() {
384
+ const syncedSize = this.syncedData.size;
385
+ const deletesFromSynced = Array.from(this.derivedDeletes).filter(
386
+ (key) => this.syncedData.has(key) && !this.derivedUpserts.has(key)
387
+ ).length;
388
+ const upsertsNotInSynced = Array.from(this.derivedUpserts.keys()).filter(
389
+ (key) => !this.syncedData.has(key)
390
+ ).length;
391
+ return syncedSize - deletesFromSynced + upsertsNotInSynced;
392
+ }
393
+ /**
394
+ * Collect events for optimistic changes
395
+ */
396
+ collectOptimisticChanges(previousUpserts, previousDeletes, events) {
397
+ const allKeys = /* @__PURE__ */ new Set([
398
+ ...previousUpserts.keys(),
399
+ ...this.derivedUpserts.keys(),
400
+ ...previousDeletes,
401
+ ...this.derivedDeletes
402
+ ]);
403
+ for (const key of allKeys) {
404
+ const currentValue = this.get(key);
405
+ const previousValue = this.getPreviousValue(
406
+ key,
407
+ previousUpserts,
408
+ previousDeletes
409
+ );
410
+ if (previousValue !== void 0 && currentValue === void 0) {
411
+ events.push({ type: `delete`, key, value: previousValue });
412
+ } else if (previousValue === void 0 && currentValue !== void 0) {
413
+ events.push({ type: `insert`, key, value: currentValue });
414
+ } else if (previousValue !== void 0 && currentValue !== void 0 && previousValue !== currentValue) {
415
+ events.push({
416
+ type: `update`,
417
+ key,
418
+ value: currentValue,
419
+ previousValue
420
+ });
421
+ }
422
+ }
423
+ }
424
+ /**
425
+ * Get the previous value for a key given previous optimistic state
426
+ */
427
+ getPreviousValue(key, previousUpserts, previousDeletes) {
428
+ if (previousDeletes.has(key)) {
429
+ return void 0;
430
+ }
431
+ if (previousUpserts.has(key)) {
432
+ return previousUpserts.get(key);
433
+ }
434
+ return this.syncedData.get(key);
435
+ }
436
+ /**
437
+ * Emit multiple events at once to all listeners
438
+ */
439
+ emitEvents(changes) {
440
+ if (changes.length > 0) {
441
+ for (const listener of this.changeListeners) {
442
+ listener(changes);
443
+ }
444
+ if (this.changeKeyListeners.size > 0) {
445
+ const changesByKey = /* @__PURE__ */ new Map();
446
+ for (const change of changes) {
447
+ if (this.changeKeyListeners.has(change.key)) {
448
+ if (!changesByKey.has(change.key)) {
449
+ changesByKey.set(change.key, []);
450
+ }
451
+ changesByKey.get(change.key).push(change);
452
+ }
453
+ }
454
+ for (const [key, keyChanges] of changesByKey) {
455
+ const keyListeners = this.changeKeyListeners.get(key);
456
+ for (const listener of keyListeners) {
457
+ listener(keyChanges);
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ /**
464
+ * Get the current value for a key (virtual derived state)
465
+ */
466
+ get(key) {
467
+ if (this.derivedDeletes.has(key)) {
468
+ return void 0;
469
+ }
470
+ if (this.derivedUpserts.has(key)) {
471
+ return this.derivedUpserts.get(key);
472
+ }
473
+ return this.syncedData.get(key);
474
+ }
475
+ /**
476
+ * Check if a key exists in the collection (virtual derived state)
477
+ */
478
+ has(key) {
479
+ if (this.derivedDeletes.has(key)) {
480
+ return false;
481
+ }
482
+ if (this.derivedUpserts.has(key)) {
483
+ return true;
484
+ }
485
+ return this.syncedData.has(key);
486
+ }
487
+ /**
488
+ * Get the current size of the collection (cached)
489
+ */
490
+ get size() {
491
+ return this._size;
492
+ }
493
+ /**
494
+ * Get all keys (virtual derived state)
495
+ */
496
+ *keys() {
497
+ for (const key of this.syncedData.keys()) {
498
+ if (!this.derivedDeletes.has(key)) {
499
+ yield key;
500
+ }
501
+ }
502
+ for (const key of this.derivedUpserts.keys()) {
503
+ if (!this.syncedData.has(key) && !this.derivedDeletes.has(key)) {
504
+ yield key;
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * Get all values (virtual derived state)
510
+ */
511
+ *values() {
512
+ for (const key of this.keys()) {
513
+ const value = this.get(key);
514
+ if (value !== void 0) {
515
+ yield value;
516
+ }
517
+ }
518
+ }
519
+ /**
520
+ * Get all entries (virtual derived state)
521
+ */
522
+ *entries() {
523
+ for (const key of this.keys()) {
524
+ const value = this.get(key);
525
+ if (value !== void 0) {
526
+ yield [key, value];
527
+ }
528
+ }
529
+ }
435
530
  ensureStandardSchema(schema) {
436
531
  if (schema && typeof schema === `object` && `~standard` in schema) {
437
532
  return schema;
@@ -440,31 +535,24 @@ class Collection {
440
535
  `Schema must either implement the standard-schema interface or be a Zod schema`
441
536
  );
442
537
  }
443
- getKeyFromId(id) {
444
- if (typeof id === `undefined`) {
445
- throw new Error(`id is undefined`);
446
- }
447
- if (typeof id === `string` && id.startsWith(`KEY::`)) {
448
- return id;
449
- } else {
450
- return this.generateObjectKey(id, null);
451
- }
538
+ getKeyFromItem(item) {
539
+ return this.config.getKey(item);
452
540
  }
453
- generateObjectKey(id, item) {
454
- if (typeof id === `undefined`) {
541
+ generateGlobalKey(key, item) {
542
+ if (typeof key === `undefined`) {
455
543
  throw new Error(
456
- `An object was created without a defined id: ${JSON.stringify(item)}`
544
+ `An object was created without a defined key: ${JSON.stringify(item)}`
457
545
  );
458
546
  }
459
- return `KEY::${this.id}/${id}`;
547
+ return `KEY::${this.id}/${key}`;
460
548
  }
461
549
  validateData(data, type, key) {
462
550
  if (!this.config.schema) return data;
463
551
  const standardSchema = this.ensureStandardSchema(this.config.schema);
464
552
  if (type === `update` && key) {
465
- const existingData = this.state.get(key);
553
+ const existingData = this.get(key);
466
554
  if (existingData && data && typeof data === `object` && typeof existingData === `object`) {
467
- const mergedData = { ...existingData, ...data };
555
+ const mergedData = Object.assign({}, existingData, data);
468
556
  const result2 = standardSchema[`~standard`].validate(mergedData);
469
557
  if (result2 instanceof Promise) {
470
558
  throw new TypeError(`Schema validation must be synchronous`);
@@ -498,8 +586,8 @@ class Collection {
498
586
  }
499
587
  return result.value;
500
588
  }
501
- update(ids, configOrCallback, maybeCallback) {
502
- if (typeof ids === `undefined`) {
589
+ update(keys, configOrCallback, maybeCallback) {
590
+ if (typeof keys === `undefined`) {
503
591
  throw new Error(`The first argument to update is missing`);
504
592
  }
505
593
  const ambientTransaction = transactions.getActiveTransaction();
@@ -508,17 +596,18 @@ class Collection {
508
596
  `Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
509
597
  );
510
598
  }
511
- const isArray = Array.isArray(ids);
512
- const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
513
- (id) => this.getKeyFromId(id)
514
- );
599
+ const isArray = Array.isArray(keys);
600
+ const keysArray = isArray ? keys : [keys];
601
+ if (isArray && keysArray.length === 0) {
602
+ throw new Error(`No keys were passed to update`);
603
+ }
515
604
  const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
516
605
  const config = typeof configOrCallback === `function` ? {} : configOrCallback;
517
- const currentObjects = idsArray.map((id) => {
518
- const item = this.state.get(id);
606
+ const currentObjects = keysArray.map((key) => {
607
+ const item = this.get(key);
519
608
  if (!item) {
520
609
  throw new Error(
521
- `The id "${id}" was passed to update but an object for this ID was not found in the collection`
610
+ `The key "${key}" was passed to update but an object for this key was not found in the collection`
522
611
  );
523
612
  }
524
613
  return item;
@@ -536,7 +625,7 @@ class Collection {
536
625
  );
537
626
  changesArray = [result];
538
627
  }
539
- const mutations = idsArray.map((id, index) => {
628
+ const mutations = keysArray.map((key, index) => {
540
629
  const itemChanges = changesArray[index];
541
630
  if (!itemChanges || Object.keys(itemChanges).length === 0) {
542
631
  return null;
@@ -545,24 +634,30 @@ class Collection {
545
634
  const validatedUpdatePayload = this.validateData(
546
635
  itemChanges,
547
636
  `update`,
548
- id
637
+ key
549
638
  );
550
- const modifiedItem = { ...originalItem, ...validatedUpdatePayload };
551
- const originalItemId = this.config.getId(originalItem);
552
- const modifiedItemId = this.config.getId(modifiedItem);
639
+ const modifiedItem = Object.assign(
640
+ {},
641
+ originalItem,
642
+ validatedUpdatePayload
643
+ );
644
+ const originalItemId = this.getKeyFromItem(originalItem);
645
+ const modifiedItemId = this.getKeyFromItem(modifiedItem);
553
646
  if (originalItemId !== modifiedItemId) {
554
647
  throw new Error(
555
- `Updating the ID of an item is not allowed. Original ID: "${originalItemId}", Attempted new ID: "${modifiedItemId}". Please delete the old item and create a new one if an ID change is necessary.`
648
+ `Updating the key of an item is not allowed. Original key: "${originalItemId}", Attempted new key: "${modifiedItemId}". Please delete the old item and create a new one if a key change is necessary.`
556
649
  );
557
650
  }
651
+ const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem);
558
652
  return {
559
653
  mutationId: crypto.randomUUID(),
560
654
  original: originalItem,
561
655
  modified: modifiedItem,
562
656
  changes: validatedUpdatePayload,
563
- key: id,
657
+ globalKey,
658
+ key,
564
659
  metadata: config.metadata,
565
- syncMetadata: this.syncedMetadata.state.get(id) || {},
660
+ syncMetadata: this.syncedMetadata.get(key) || {},
566
661
  type: `update`,
567
662
  createdAt: /* @__PURE__ */ new Date(),
568
663
  updatedAt: /* @__PURE__ */ new Date(),
@@ -574,23 +669,19 @@ class Collection {
574
669
  }
575
670
  if (ambientTransaction) {
576
671
  ambientTransaction.applyMutations(mutations);
577
- this.transactions.setState((sortedMap) => {
578
- sortedMap.set(ambientTransaction.id, ambientTransaction);
579
- return sortedMap;
580
- });
672
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
673
+ this.recomputeOptimisticState();
581
674
  return ambientTransaction;
582
675
  }
583
676
  const directOpTransaction = new transactions.Transaction({
584
- mutationFn: async (transaction) => {
585
- return this.config.onUpdate(transaction);
677
+ mutationFn: async (params) => {
678
+ return this.config.onUpdate(params);
586
679
  }
587
680
  });
588
681
  directOpTransaction.applyMutations(mutations);
589
682
  directOpTransaction.commit();
590
- this.transactions.setState((sortedMap) => {
591
- sortedMap.set(directOpTransaction.id, directOpTransaction);
592
- return sortedMap;
593
- });
683
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
684
+ this.recomputeOptimisticState();
594
685
  return directOpTransaction;
595
686
  }
596
687
  /**
@@ -599,7 +690,11 @@ class Collection {
599
690
  * @returns A Map containing all items in the collection, with keys as identifiers
600
691
  */
601
692
  get state() {
602
- return this.derivedState.state;
693
+ const result = /* @__PURE__ */ new Map();
694
+ for (const [key, value] of this.entries()) {
695
+ result.set(key, value);
696
+ }
697
+ return result;
603
698
  }
604
699
  /**
605
700
  * Gets the current state of the collection as a Map, but only resolves when data is available
@@ -608,7 +703,7 @@ class Collection {
608
703
  * @returns Promise that resolves to a Map containing all items in the collection
609
704
  */
610
705
  stateWhenReady() {
611
- if (this.state.size > 0 || this.hasReceivedFirstCommit === true) {
706
+ if (this.size > 0 || this.hasReceivedFirstCommit === true) {
612
707
  return Promise.resolve(this.state);
613
708
  }
614
709
  return new Promise((resolve) => {
@@ -623,7 +718,13 @@ class Collection {
623
718
  * @returns An Array containing all items in the collection
624
719
  */
625
720
  get toArray() {
626
- return this.derivedArray.state;
721
+ const array = Array.from(this.values());
722
+ if (array[0] && array[0]._orderByIndex) {
723
+ return array.sort(
724
+ (a, b) => a._orderByIndex - b._orderByIndex
725
+ );
726
+ }
727
+ return array;
627
728
  }
628
729
  /**
629
730
  * Gets the current state of the collection as an Array, but only resolves when data is available
@@ -632,7 +733,7 @@ class Collection {
632
733
  * @returns Promise that resolves to an Array containing all items in the collection
633
734
  */
634
735
  toArrayWhenReady() {
635
- if (this.toArray.length > 0 || this.hasReceivedFirstCommit === true) {
736
+ if (this.size > 0 || this.hasReceivedFirstCommit === true) {
636
737
  return Promise.resolve(this.toArray);
637
738
  }
638
739
  return new Promise((resolve) => {
@@ -646,7 +747,7 @@ class Collection {
646
747
  * @returns An array of changes
647
748
  */
648
749
  currentStateAsChanges() {
649
- return [...this.state.entries()].map(([key, value]) => ({
750
+ return Array.from(this.entries()).map(([key, value]) => ({
650
751
  type: `insert`,
651
752
  key,
652
753
  value
@@ -657,16 +758,81 @@ class Collection {
657
758
  * @param callback - A function that will be called with the changes in the collection
658
759
  * @returns A function that can be called to unsubscribe from the changes
659
760
  */
660
- subscribeChanges(callback) {
661
- callback(this.currentStateAsChanges());
662
- return this.derivedChanges.subscribe((changes) => {
663
- if (changes.currentVal.length > 0) {
664
- callback(changes.currentVal);
761
+ subscribeChanges(callback, { includeInitialState = false } = {}) {
762
+ if (includeInitialState) {
763
+ callback(this.currentStateAsChanges());
764
+ }
765
+ this.changeListeners.add(callback);
766
+ return () => {
767
+ this.changeListeners.delete(callback);
768
+ };
769
+ }
770
+ /**
771
+ * Subscribe to changes for a specific key
772
+ */
773
+ subscribeChangesKey(key, listener, { includeInitialState = false } = {}) {
774
+ if (!this.changeKeyListeners.has(key)) {
775
+ this.changeKeyListeners.set(key, /* @__PURE__ */ new Set());
776
+ }
777
+ if (includeInitialState) {
778
+ listener([
779
+ {
780
+ type: `insert`,
781
+ key,
782
+ value: this.get(key)
783
+ }
784
+ ]);
785
+ }
786
+ this.changeKeyListeners.get(key).add(listener);
787
+ return () => {
788
+ const listeners = this.changeKeyListeners.get(key);
789
+ if (listeners) {
790
+ listeners.delete(listener);
791
+ if (listeners.size === 0) {
792
+ this.changeKeyListeners.delete(key);
793
+ }
665
794
  }
666
- });
795
+ };
796
+ }
797
+ /**
798
+ * Trigger a recomputation when transactions change
799
+ * This method should be called by the Transaction class when state changes
800
+ */
801
+ onTransactionStateChange() {
802
+ this.recomputeOptimisticState();
803
+ }
804
+ /**
805
+ * Returns a Tanstack Store Map that is updated when the collection changes
806
+ * This is a temporary solution to enable the existing framework hooks to work
807
+ * with the new internals of Collection until they are rewritten.
808
+ * TODO: Remove this once the framework hooks are rewritten.
809
+ */
810
+ asStoreMap() {
811
+ if (!this._storeMap) {
812
+ this._storeMap = new store.Store(new Map(this.entries()));
813
+ this.subscribeChanges(() => {
814
+ this._storeMap.setState(() => new Map(this.entries()));
815
+ });
816
+ }
817
+ return this._storeMap;
818
+ }
819
+ /**
820
+ * Returns a Tanstack Store Array that is updated when the collection changes
821
+ * This is a temporary solution to enable the existing framework hooks to work
822
+ * with the new internals of Collection until they are rewritten.
823
+ * TODO: Remove this once the framework hooks are rewritten.
824
+ */
825
+ asStoreArray() {
826
+ if (!this._storeArray) {
827
+ this._storeArray = new store.Store(this.toArray);
828
+ this.subscribeChanges(() => {
829
+ this._storeArray.setState(() => this.toArray);
830
+ });
831
+ }
832
+ return this._storeArray;
667
833
  }
668
834
  }
669
- exports.Collection = Collection;
835
+ exports.CollectionImpl = CollectionImpl;
670
836
  exports.SchemaValidationError = SchemaValidationError;
671
837
  exports.collectionsStore = collectionsStore;
672
838
  exports.createCollection = createCollection;