@tanstack/db 0.0.7 → 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 (38) hide show
  1. package/dist/cjs/collection.cjs +441 -284
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +103 -30
  4. package/dist/cjs/proxy.cjs +2 -2
  5. package/dist/cjs/proxy.cjs.map +1 -1
  6. package/dist/cjs/query/compiled-query.cjs +23 -37
  7. package/dist/cjs/query/compiled-query.cjs.map +1 -1
  8. package/dist/cjs/query/compiled-query.d.cts +2 -2
  9. package/dist/cjs/query/order-by.cjs +41 -38
  10. package/dist/cjs/query/order-by.cjs.map +1 -1
  11. package/dist/cjs/query/schema.d.cts +3 -3
  12. package/dist/cjs/transactions.cjs +7 -6
  13. package/dist/cjs/transactions.cjs.map +1 -1
  14. package/dist/cjs/transactions.d.cts +9 -9
  15. package/dist/cjs/types.d.cts +28 -22
  16. package/dist/esm/collection.d.ts +103 -30
  17. package/dist/esm/collection.js +442 -285
  18. package/dist/esm/collection.js.map +1 -1
  19. package/dist/esm/proxy.js +2 -2
  20. package/dist/esm/proxy.js.map +1 -1
  21. package/dist/esm/query/compiled-query.d.ts +2 -2
  22. package/dist/esm/query/compiled-query.js +23 -37
  23. package/dist/esm/query/compiled-query.js.map +1 -1
  24. package/dist/esm/query/order-by.js +41 -38
  25. package/dist/esm/query/order-by.js.map +1 -1
  26. package/dist/esm/query/schema.d.ts +3 -3
  27. package/dist/esm/transactions.d.ts +9 -9
  28. package/dist/esm/transactions.js +7 -6
  29. package/dist/esm/transactions.js.map +1 -1
  30. package/dist/esm/types.d.ts +28 -22
  31. package/package.json +2 -2
  32. package/src/collection.ts +624 -372
  33. package/src/proxy.ts +2 -2
  34. package/src/query/compiled-query.ts +26 -37
  35. package/src/query/order-by.ts +69 -67
  36. package/src/query/schema.ts +3 -3
  37. package/src/transactions.ts +24 -22
  38. package/src/types.ts +44 -22
@@ -4,10 +4,8 @@ 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(
8
- /* @__PURE__ */ new Map()
9
- );
10
- const loadingCollections = /* @__PURE__ */ new Map();
7
+ const collectionsStore = /* @__PURE__ */ new Map();
8
+ const loadingCollectionResolvers = /* @__PURE__ */ new Map();
11
9
  function createCollection(options) {
12
10
  const collection = new CollectionImpl(options);
13
11
  if (options.utils) {
@@ -21,52 +19,44 @@ function preloadCollection(config) {
21
19
  if (!config.id) {
22
20
  throw new Error(`The id property is required for preloadCollection`);
23
21
  }
24
- if (collectionsStore.state.has(config.id) && !loadingCollections.has(config.id)) {
22
+ if (collectionsStore.has(config.id) && !loadingCollectionResolvers.has(config.id)) {
25
23
  return Promise.resolve(
26
- collectionsStore.state.get(config.id)
24
+ collectionsStore.get(config.id)
27
25
  );
28
26
  }
29
- if (loadingCollections.has(config.id)) {
30
- return loadingCollections.get(config.id);
27
+ if (loadingCollectionResolvers.has(config.id)) {
28
+ return loadingCollectionResolvers.get(config.id).promise;
31
29
  }
32
- if (!collectionsStore.state.has(config.id)) {
33
- collectionsStore.setState((prev) => {
34
- const next = new Map(prev);
35
- if (!config.id) {
36
- throw new Error(`The id property is required for preloadCollection`);
37
- }
38
- next.set(
39
- config.id,
40
- createCollection({
41
- id: config.id,
42
- getId: config.getId,
43
- sync: config.sync,
44
- schema: config.schema
45
- })
46
- );
47
- return next;
48
- });
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
+ );
49
40
  }
50
- const collection = collectionsStore.state.get(config.id);
41
+ const collection = collectionsStore.get(config.id);
51
42
  let resolveFirstCommit;
52
43
  const firstCommitPromise = new Promise((resolve) => {
53
- resolveFirstCommit = () => {
54
- resolve(collection);
55
- };
44
+ resolveFirstCommit = resolve;
45
+ });
46
+ loadingCollectionResolvers.set(config.id, {
47
+ promise: firstCommitPromise,
48
+ resolve: resolveFirstCommit
56
49
  });
57
50
  collection.onFirstCommit(() => {
58
51
  if (!config.id) {
59
52
  throw new Error(`The id property is required for preloadCollection`);
60
53
  }
61
- if (loadingCollections.has(config.id)) {
62
- loadingCollections.delete(config.id);
63
- resolveFirstCommit();
54
+ if (loadingCollectionResolvers.has(config.id)) {
55
+ const resolver = loadingCollectionResolvers.get(config.id);
56
+ loadingCollectionResolvers.delete(config.id);
57
+ resolver.resolve(collection);
64
58
  }
65
59
  });
66
- loadingCollections.set(
67
- config.id,
68
- firstCommitPromise
69
- );
70
60
  return firstCommitPromise;
71
61
  }
72
62
  class SchemaValidationError extends Error {
@@ -86,59 +76,94 @@ class CollectionImpl {
86
76
  * @throws Error if sync config is missing
87
77
  */
88
78
  constructor(config) {
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();
89
86
  this.utils = {};
90
- this.syncedData = new store.Store(/* @__PURE__ */ new Map());
91
- this.syncedMetadata = new store.Store(/* @__PURE__ */ new Map());
92
87
  this.pendingSyncedTransactions = [];
93
88
  this.syncedKeys = /* @__PURE__ */ new Set();
94
89
  this.hasReceivedFirstCommit = false;
95
90
  this.onFirstCommitCallbacks = [];
96
91
  this.id = ``;
97
92
  this.commitPendingTransactions = () => {
98
- if (!Array.from(this.transactions.state.values()).some(
93
+ if (!Array.from(this.transactions.values()).some(
99
94
  ({ state }) => state === `persisting`
100
95
  )) {
101
- store.batch(() => {
102
- for (const transaction of this.pendingSyncedTransactions) {
103
- for (const operation of transaction.operations) {
104
- this.syncedKeys.add(operation.key);
105
- this.syncedMetadata.setState((prevData) => {
106
- switch (operation.type) {
107
- case `insert`:
108
- prevData.set(operation.key, operation.metadata);
109
- break;
110
- case `update`:
111
- prevData.set(operation.key, {
112
- ...prevData.get(operation.key),
113
- ...operation.metadata
114
- });
115
- break;
116
- case `delete`:
117
- prevData.delete(operation.key);
118
- 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
+ });
119
131
  }
120
- return prevData;
121
- });
122
- this.syncedData.setState((prevData) => {
123
- switch (operation.type) {
124
- case `insert`:
125
- prevData.set(operation.key, operation.value);
126
- break;
127
- case `update`:
128
- prevData.set(operation.key, {
129
- ...prevData.get(operation.key),
130
- ...operation.value
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
+ });
147
+ }
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
131
158
  });
132
- break;
133
- case `delete`:
134
- prevData.delete(operation.key);
135
- break;
159
+ }
136
160
  }
137
- return prevData;
138
- });
161
+ break;
139
162
  }
140
163
  }
141
- });
164
+ }
165
+ this._size = this.calculateSize();
166
+ this.emitEvents(events);
142
167
  this.pendingSyncedTransactions = [];
143
168
  if (!this.hasReceivedFirstCommit) {
144
169
  this.hasReceivedFirstCommit = true;
@@ -157,22 +182,20 @@ class CollectionImpl {
157
182
  }
158
183
  const items = Array.isArray(data) ? data : [data];
159
184
  const mutations = [];
160
- const keys = items.map(
161
- (item) => this.generateObjectKey(this.config.getId(item), item)
162
- );
163
- items.forEach((item, index) => {
185
+ items.forEach((item) => {
164
186
  var _a, _b;
165
187
  const validatedData = this.validateData(item, `insert`);
166
- const key = keys[index];
167
- const id = this.config.getId(item);
168
- if (this.state.has(this.getKeyFromId(id))) {
169
- 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`;
170
191
  }
192
+ const globalKey = this.generateGlobalKey(key, item);
171
193
  const mutation = {
172
194
  mutationId: crypto.randomUUID(),
173
195
  original: {},
174
196
  modified: validatedData,
175
197
  changes: validatedData,
198
+ globalKey,
176
199
  key,
177
200
  metadata: config2 == null ? void 0 : config2.metadata,
178
201
  syncMetadata: ((_b = (_a = this.config.sync).getSyncMetadata) == null ? void 0 : _b.call(_a)) || {},
@@ -185,10 +208,8 @@ class CollectionImpl {
185
208
  });
186
209
  if (ambientTransaction) {
187
210
  ambientTransaction.applyMutations(mutations);
188
- this.transactions.setState((sortedMap) => {
189
- sortedMap.set(ambientTransaction.id, ambientTransaction);
190
- return sortedMap;
191
- });
211
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
212
+ this.recomputeOptimisticState();
192
213
  return ambientTransaction;
193
214
  } else {
194
215
  const directOpTransaction = new transactions.Transaction({
@@ -198,33 +219,34 @@ class CollectionImpl {
198
219
  });
199
220
  directOpTransaction.applyMutations(mutations);
200
221
  directOpTransaction.commit();
201
- this.transactions.setState((sortedMap) => {
202
- sortedMap.set(directOpTransaction.id, directOpTransaction);
203
- return sortedMap;
204
- });
222
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
223
+ this.recomputeOptimisticState();
205
224
  return directOpTransaction;
206
225
  }
207
226
  };
208
- this.delete = (ids, config2) => {
227
+ this.delete = (keys, config2) => {
209
228
  const ambientTransaction = transactions.getActiveTransaction();
210
229
  if (!ambientTransaction && !this.config.onDelete) {
211
230
  throw new Error(
212
231
  `Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
213
232
  );
214
233
  }
215
- const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
216
- (id) => this.getKeyFromId(id)
217
- );
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];
218
238
  const mutations = [];
219
- for (const id of idsArray) {
239
+ for (const key of keysArray) {
240
+ const globalKey = this.generateGlobalKey(key, this.get(key));
220
241
  const mutation = {
221
242
  mutationId: crypto.randomUUID(),
222
- original: this.state.get(id) || {},
223
- modified: this.state.get(id) || {},
224
- changes: this.state.get(id) || {},
225
- key: id,
243
+ original: this.get(key) || {},
244
+ modified: this.get(key),
245
+ changes: this.get(key) || {},
246
+ globalKey,
247
+ key,
226
248
  metadata: config2 == null ? void 0 : config2.metadata,
227
- syncMetadata: this.syncedMetadata.state.get(id) || {},
249
+ syncMetadata: this.syncedMetadata.get(key) || {},
228
250
  type: `delete`,
229
251
  createdAt: /* @__PURE__ */ new Date(),
230
252
  updatedAt: /* @__PURE__ */ new Date(),
@@ -234,24 +256,20 @@ class CollectionImpl {
234
256
  }
235
257
  if (ambientTransaction) {
236
258
  ambientTransaction.applyMutations(mutations);
237
- this.transactions.setState((sortedMap) => {
238
- sortedMap.set(ambientTransaction.id, ambientTransaction);
239
- return sortedMap;
240
- });
259
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
260
+ this.recomputeOptimisticState();
241
261
  return ambientTransaction;
242
262
  }
243
263
  const directOpTransaction = new transactions.Transaction({
244
264
  autoCommit: true,
245
- mutationFn: async (transaction) => {
246
- return this.config.onDelete(transaction);
265
+ mutationFn: async (params) => {
266
+ return this.config.onDelete(params);
247
267
  }
248
268
  });
249
269
  directOpTransaction.applyMutations(mutations);
250
270
  directOpTransaction.commit();
251
- this.transactions.setState((sortedMap) => {
252
- sortedMap.set(directOpTransaction.id, directOpTransaction);
253
- return sortedMap;
254
- });
271
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
272
+ this.recomputeOptimisticState();
255
273
  return directOpTransaction;
256
274
  };
257
275
  if (!config) {
@@ -265,121 +283,10 @@ class CollectionImpl {
265
283
  if (!config.sync) {
266
284
  throw new Error(`Collection requires a sync config`);
267
285
  }
268
- this.transactions = new store.Store(
269
- new SortedMap.SortedMap(
270
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
271
- )
286
+ this.transactions = new SortedMap.SortedMap(
287
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
272
288
  );
273
- this.optimisticOperations = new store.Derived({
274
- fn: ({ currDepVals: [transactions2] }) => {
275
- const result = Array.from(transactions2.values()).map((transaction) => {
276
- const isActive = ![`completed`, `failed`].includes(
277
- transaction.state
278
- );
279
- return transaction.mutations.filter((mutation) => mutation.collection === this).map((mutation) => {
280
- const message = {
281
- type: mutation.type,
282
- key: mutation.key,
283
- value: mutation.modified,
284
- isActive
285
- };
286
- if (mutation.metadata !== void 0 && mutation.metadata !== null) {
287
- message.metadata = mutation.metadata;
288
- }
289
- return message;
290
- });
291
- }).flat();
292
- return result;
293
- },
294
- deps: [this.transactions]
295
- });
296
- this.optimisticOperations.mount();
297
- this.derivedState = new store.Derived({
298
- fn: ({ currDepVals: [syncedData, operations] }) => {
299
- const combined = new Map(syncedData);
300
- for (const operation of operations) {
301
- if (operation.isActive) {
302
- switch (operation.type) {
303
- case `insert`:
304
- combined.set(operation.key, operation.value);
305
- break;
306
- case `update`:
307
- combined.set(operation.key, operation.value);
308
- break;
309
- case `delete`:
310
- combined.delete(operation.key);
311
- break;
312
- }
313
- }
314
- }
315
- return combined;
316
- },
317
- deps: [this.syncedData, this.optimisticOperations]
318
- });
319
- this.derivedArray = new store.Derived({
320
- fn: ({ currDepVals: [stateMap] }) => {
321
- const array = Array.from(
322
- stateMap.values()
323
- );
324
- if (array[0] && `_orderByIndex` in array[0]) {
325
- array.sort((a, b) => {
326
- if (a._orderByIndex === b._orderByIndex) {
327
- return 0;
328
- }
329
- return a._orderByIndex < b._orderByIndex ? -1 : 1;
330
- });
331
- }
332
- return array;
333
- },
334
- deps: [this.derivedState]
335
- });
336
- this.derivedArray.mount();
337
- this.derivedChanges = new store.Derived({
338
- fn: ({
339
- currDepVals: [derivedState, optimisticOperations],
340
- prevDepVals
341
- }) => {
342
- const prevDerivedState = (prevDepVals == null ? void 0 : prevDepVals[0]) ?? /* @__PURE__ */ new Map();
343
- const prevOptimisticOperations = (prevDepVals == null ? void 0 : prevDepVals[1]) ?? [];
344
- const changedKeys = new Set(this.syncedKeys);
345
- optimisticOperations.flat().filter((op) => op.isActive).forEach((op) => changedKeys.add(op.key));
346
- prevOptimisticOperations.flat().forEach((op) => {
347
- changedKeys.add(op.key);
348
- });
349
- if (changedKeys.size === 0) {
350
- return [];
351
- }
352
- const changes = [];
353
- for (const key of changedKeys) {
354
- if (prevDerivedState.has(key) && !derivedState.has(key)) {
355
- changes.push({
356
- type: `delete`,
357
- key,
358
- value: prevDerivedState.get(key)
359
- });
360
- } else if (!prevDerivedState.has(key) && derivedState.has(key)) {
361
- changes.push({ type: `insert`, key, value: derivedState.get(key) });
362
- } else if (prevDerivedState.has(key) && derivedState.has(key)) {
363
- const value = derivedState.get(key);
364
- const previousValue = prevDerivedState.get(key);
365
- if (value !== previousValue) {
366
- changes.push({
367
- type: `update`,
368
- key,
369
- value,
370
- previousValue
371
- });
372
- }
373
- }
374
- }
375
- this.syncedKeys.clear();
376
- return changes;
377
- },
378
- deps: [this.derivedState, this.optimisticOperations]
379
- });
380
- this.derivedChanges.mount();
381
289
  this.config = config;
382
- this.derivedState.mount();
383
290
  config.sync.sync({
384
291
  collection: this,
385
292
  begin: () => {
@@ -398,17 +305,13 @@ class CollectionImpl {
398
305
  `The pending sync transaction is already committed, you can't still write to it.`
399
306
  );
400
307
  }
401
- const key = this.generateObjectKey(
402
- this.config.getId(messageWithoutKey.value),
403
- messageWithoutKey.value
404
- );
308
+ const key = this.getKeyFromItem(messageWithoutKey.value);
405
309
  if (messageWithoutKey.type === `insert`) {
406
- if (this.syncedData.state.has(key) && !pendingTransaction.operations.some(
310
+ if (this.syncedData.has(key) && !pendingTransaction.operations.some(
407
311
  (op) => op.key === key && op.type === `delete`
408
312
  )) {
409
- const id = this.config.getId(messageWithoutKey.value);
410
313
  throw new Error(
411
- `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}"`
412
315
  );
413
316
  }
414
317
  }
@@ -441,6 +344,189 @@ class CollectionImpl {
441
344
  onFirstCommit(callback) {
442
345
  this.onFirstCommitCallbacks.push(callback);
443
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
+ }
444
530
  ensureStandardSchema(schema) {
445
531
  if (schema && typeof schema === `object` && `~standard` in schema) {
446
532
  return schema;
@@ -449,31 +535,24 @@ class CollectionImpl {
449
535
  `Schema must either implement the standard-schema interface or be a Zod schema`
450
536
  );
451
537
  }
452
- getKeyFromId(id) {
453
- if (typeof id === `undefined`) {
454
- throw new Error(`id is undefined`);
455
- }
456
- if (typeof id === `string` && id.startsWith(`KEY::`)) {
457
- return id;
458
- } else {
459
- return this.generateObjectKey(id, null);
460
- }
538
+ getKeyFromItem(item) {
539
+ return this.config.getKey(item);
461
540
  }
462
- generateObjectKey(id, item) {
463
- if (typeof id === `undefined`) {
541
+ generateGlobalKey(key, item) {
542
+ if (typeof key === `undefined`) {
464
543
  throw new Error(
465
- `An object was created without a defined id: ${JSON.stringify(item)}`
544
+ `An object was created without a defined key: ${JSON.stringify(item)}`
466
545
  );
467
546
  }
468
- return `KEY::${this.id}/${id}`;
547
+ return `KEY::${this.id}/${key}`;
469
548
  }
470
549
  validateData(data, type, key) {
471
550
  if (!this.config.schema) return data;
472
551
  const standardSchema = this.ensureStandardSchema(this.config.schema);
473
552
  if (type === `update` && key) {
474
- const existingData = this.state.get(key);
553
+ const existingData = this.get(key);
475
554
  if (existingData && data && typeof data === `object` && typeof existingData === `object`) {
476
- const mergedData = { ...existingData, ...data };
555
+ const mergedData = Object.assign({}, existingData, data);
477
556
  const result2 = standardSchema[`~standard`].validate(mergedData);
478
557
  if (result2 instanceof Promise) {
479
558
  throw new TypeError(`Schema validation must be synchronous`);
@@ -507,8 +586,8 @@ class CollectionImpl {
507
586
  }
508
587
  return result.value;
509
588
  }
510
- update(ids, configOrCallback, maybeCallback) {
511
- if (typeof ids === `undefined`) {
589
+ update(keys, configOrCallback, maybeCallback) {
590
+ if (typeof keys === `undefined`) {
512
591
  throw new Error(`The first argument to update is missing`);
513
592
  }
514
593
  const ambientTransaction = transactions.getActiveTransaction();
@@ -517,17 +596,18 @@ class CollectionImpl {
517
596
  `Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
518
597
  );
519
598
  }
520
- const isArray = Array.isArray(ids);
521
- const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
522
- (id) => this.getKeyFromId(id)
523
- );
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
+ }
524
604
  const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
525
605
  const config = typeof configOrCallback === `function` ? {} : configOrCallback;
526
- const currentObjects = idsArray.map((id) => {
527
- const item = this.state.get(id);
606
+ const currentObjects = keysArray.map((key) => {
607
+ const item = this.get(key);
528
608
  if (!item) {
529
609
  throw new Error(
530
- `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`
531
611
  );
532
612
  }
533
613
  return item;
@@ -545,7 +625,7 @@ class CollectionImpl {
545
625
  );
546
626
  changesArray = [result];
547
627
  }
548
- const mutations = idsArray.map((id, index) => {
628
+ const mutations = keysArray.map((key, index) => {
549
629
  const itemChanges = changesArray[index];
550
630
  if (!itemChanges || Object.keys(itemChanges).length === 0) {
551
631
  return null;
@@ -554,24 +634,30 @@ class CollectionImpl {
554
634
  const validatedUpdatePayload = this.validateData(
555
635
  itemChanges,
556
636
  `update`,
557
- id
637
+ key
638
+ );
639
+ const modifiedItem = Object.assign(
640
+ {},
641
+ originalItem,
642
+ validatedUpdatePayload
558
643
  );
559
- const modifiedItem = { ...originalItem, ...validatedUpdatePayload };
560
- const originalItemId = this.config.getId(originalItem);
561
- const modifiedItemId = this.config.getId(modifiedItem);
644
+ const originalItemId = this.getKeyFromItem(originalItem);
645
+ const modifiedItemId = this.getKeyFromItem(modifiedItem);
562
646
  if (originalItemId !== modifiedItemId) {
563
647
  throw new Error(
564
- `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.`
565
649
  );
566
650
  }
651
+ const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem);
567
652
  return {
568
653
  mutationId: crypto.randomUUID(),
569
654
  original: originalItem,
570
655
  modified: modifiedItem,
571
656
  changes: validatedUpdatePayload,
572
- key: id,
657
+ globalKey,
658
+ key,
573
659
  metadata: config.metadata,
574
- syncMetadata: this.syncedMetadata.state.get(id) || {},
660
+ syncMetadata: this.syncedMetadata.get(key) || {},
575
661
  type: `update`,
576
662
  createdAt: /* @__PURE__ */ new Date(),
577
663
  updatedAt: /* @__PURE__ */ new Date(),
@@ -583,23 +669,19 @@ class CollectionImpl {
583
669
  }
584
670
  if (ambientTransaction) {
585
671
  ambientTransaction.applyMutations(mutations);
586
- this.transactions.setState((sortedMap) => {
587
- sortedMap.set(ambientTransaction.id, ambientTransaction);
588
- return sortedMap;
589
- });
672
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
673
+ this.recomputeOptimisticState();
590
674
  return ambientTransaction;
591
675
  }
592
676
  const directOpTransaction = new transactions.Transaction({
593
- mutationFn: async (transaction) => {
594
- return this.config.onUpdate(transaction);
677
+ mutationFn: async (params) => {
678
+ return this.config.onUpdate(params);
595
679
  }
596
680
  });
597
681
  directOpTransaction.applyMutations(mutations);
598
682
  directOpTransaction.commit();
599
- this.transactions.setState((sortedMap) => {
600
- sortedMap.set(directOpTransaction.id, directOpTransaction);
601
- return sortedMap;
602
- });
683
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
684
+ this.recomputeOptimisticState();
603
685
  return directOpTransaction;
604
686
  }
605
687
  /**
@@ -608,7 +690,11 @@ class CollectionImpl {
608
690
  * @returns A Map containing all items in the collection, with keys as identifiers
609
691
  */
610
692
  get state() {
611
- 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;
612
698
  }
613
699
  /**
614
700
  * Gets the current state of the collection as a Map, but only resolves when data is available
@@ -617,7 +703,7 @@ class CollectionImpl {
617
703
  * @returns Promise that resolves to a Map containing all items in the collection
618
704
  */
619
705
  stateWhenReady() {
620
- if (this.state.size > 0 || this.hasReceivedFirstCommit === true) {
706
+ if (this.size > 0 || this.hasReceivedFirstCommit === true) {
621
707
  return Promise.resolve(this.state);
622
708
  }
623
709
  return new Promise((resolve) => {
@@ -632,7 +718,13 @@ class CollectionImpl {
632
718
  * @returns An Array containing all items in the collection
633
719
  */
634
720
  get toArray() {
635
- 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;
636
728
  }
637
729
  /**
638
730
  * Gets the current state of the collection as an Array, but only resolves when data is available
@@ -641,7 +733,7 @@ class CollectionImpl {
641
733
  * @returns Promise that resolves to an Array containing all items in the collection
642
734
  */
643
735
  toArrayWhenReady() {
644
- if (this.toArray.length > 0 || this.hasReceivedFirstCommit === true) {
736
+ if (this.size > 0 || this.hasReceivedFirstCommit === true) {
645
737
  return Promise.resolve(this.toArray);
646
738
  }
647
739
  return new Promise((resolve) => {
@@ -655,7 +747,7 @@ class CollectionImpl {
655
747
  * @returns An array of changes
656
748
  */
657
749
  currentStateAsChanges() {
658
- return [...this.state.entries()].map(([key, value]) => ({
750
+ return Array.from(this.entries()).map(([key, value]) => ({
659
751
  type: `insert`,
660
752
  key,
661
753
  value
@@ -666,13 +758,78 @@ class CollectionImpl {
666
758
  * @param callback - A function that will be called with the changes in the collection
667
759
  * @returns A function that can be called to unsubscribe from the changes
668
760
  */
669
- subscribeChanges(callback) {
670
- callback(this.currentStateAsChanges());
671
- return this.derivedChanges.subscribe((changes) => {
672
- if (changes.currentVal.length > 0) {
673
- 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
+ }
674
794
  }
675
- });
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;
676
833
  }
677
834
  }
678
835
  exports.CollectionImpl = CollectionImpl;