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