@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
@@ -1,62 +1,60 @@
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(/* @__PURE__ */ new Map());
6
- const loadingCollections = /* @__PURE__ */ new Map();
7
- function createCollection(config) {
8
- return new Collection(config);
5
+ const collectionsStore = /* @__PURE__ */ new Map();
6
+ const loadingCollectionResolvers = /* @__PURE__ */ new Map();
7
+ function createCollection(options) {
8
+ const collection = new CollectionImpl(options);
9
+ if (options.utils) {
10
+ collection.utils = { ...options.utils };
11
+ } else {
12
+ collection.utils = {};
13
+ }
14
+ return collection;
9
15
  }
10
16
  function preloadCollection(config) {
11
17
  if (!config.id) {
12
18
  throw new Error(`The id property is required for preloadCollection`);
13
19
  }
14
- if (collectionsStore.state.has(config.id) && !loadingCollections.has(config.id)) {
20
+ if (collectionsStore.has(config.id) && !loadingCollectionResolvers.has(config.id)) {
15
21
  return Promise.resolve(
16
- collectionsStore.state.get(config.id)
22
+ collectionsStore.get(config.id)
17
23
  );
18
24
  }
19
- if (loadingCollections.has(config.id)) {
20
- return loadingCollections.get(config.id);
25
+ if (loadingCollectionResolvers.has(config.id)) {
26
+ return loadingCollectionResolvers.get(config.id).promise;
21
27
  }
22
- if (!collectionsStore.state.has(config.id)) {
23
- collectionsStore.setState((prev) => {
24
- const next = new Map(prev);
25
- if (!config.id) {
26
- throw new Error(`The id property is required for preloadCollection`);
27
- }
28
- next.set(
29
- config.id,
30
- new Collection({
31
- id: config.id,
32
- getId: config.getId,
33
- sync: config.sync,
34
- schema: config.schema
35
- })
36
- );
37
- return next;
38
- });
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
+ );
39
38
  }
40
- const collection = collectionsStore.state.get(config.id);
39
+ const collection = collectionsStore.get(config.id);
41
40
  let resolveFirstCommit;
42
41
  const firstCommitPromise = new Promise((resolve) => {
43
- resolveFirstCommit = () => {
44
- resolve(collection);
45
- };
42
+ resolveFirstCommit = resolve;
43
+ });
44
+ loadingCollectionResolvers.set(config.id, {
45
+ promise: firstCommitPromise,
46
+ resolve: resolveFirstCommit
46
47
  });
47
48
  collection.onFirstCommit(() => {
48
49
  if (!config.id) {
49
50
  throw new Error(`The id property is required for preloadCollection`);
50
51
  }
51
- if (loadingCollections.has(config.id)) {
52
- loadingCollections.delete(config.id);
53
- resolveFirstCommit();
52
+ if (loadingCollectionResolvers.has(config.id)) {
53
+ const resolver = loadingCollectionResolvers.get(config.id);
54
+ loadingCollectionResolvers.delete(config.id);
55
+ resolver.resolve(collection);
54
56
  }
55
57
  });
56
- loadingCollections.set(
57
- config.id,
58
- firstCommitPromise
59
- );
60
58
  return firstCommitPromise;
61
59
  }
62
60
  class SchemaValidationError extends Error {
@@ -68,7 +66,7 @@ class SchemaValidationError extends Error {
68
66
  this.issues = issues;
69
67
  }
70
68
  }
71
- class Collection {
69
+ class CollectionImpl {
72
70
  /**
73
71
  * Creates a new Collection instance
74
72
  *
@@ -76,58 +74,94 @@ class Collection {
76
74
  * @throws Error if sync config is missing
77
75
  */
78
76
  constructor(config) {
79
- this.syncedData = new Store(/* @__PURE__ */ new Map());
80
- this.syncedMetadata = new Store(/* @__PURE__ */ new Map());
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();
84
+ this.utils = {};
81
85
  this.pendingSyncedTransactions = [];
82
86
  this.syncedKeys = /* @__PURE__ */ new Set();
83
87
  this.hasReceivedFirstCommit = false;
84
88
  this.onFirstCommitCallbacks = [];
85
89
  this.id = ``;
86
90
  this.commitPendingTransactions = () => {
87
- if (!Array.from(this.transactions.state.values()).some(
91
+ if (!Array.from(this.transactions.values()).some(
88
92
  ({ state }) => state === `persisting`
89
93
  )) {
90
- batch(() => {
91
- for (const transaction of this.pendingSyncedTransactions) {
92
- for (const operation of transaction.operations) {
93
- this.syncedKeys.add(operation.key);
94
- this.syncedMetadata.setState((prevData) => {
95
- switch (operation.type) {
96
- case `insert`:
97
- prevData.set(operation.key, operation.metadata);
98
- break;
99
- case `update`:
100
- prevData.set(operation.key, {
101
- ...prevData.get(operation.key),
102
- ...operation.metadata
103
- });
104
- break;
105
- case `delete`:
106
- prevData.delete(operation.key);
107
- 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
+ });
129
+ }
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
+ });
108
145
  }
109
- return prevData;
110
- });
111
- this.syncedData.setState((prevData) => {
112
- switch (operation.type) {
113
- case `insert`:
114
- prevData.set(operation.key, operation.value);
115
- break;
116
- case `update`:
117
- prevData.set(operation.key, {
118
- ...prevData.get(operation.key),
119
- ...operation.value
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
120
156
  });
121
- break;
122
- case `delete`:
123
- prevData.delete(operation.key);
124
- break;
157
+ }
125
158
  }
126
- return prevData;
127
- });
159
+ break;
128
160
  }
129
161
  }
130
- });
162
+ }
163
+ this._size = this.calculateSize();
164
+ this.emitEvents(events);
131
165
  this.pendingSyncedTransactions = [];
132
166
  if (!this.hasReceivedFirstCommit) {
133
167
  this.hasReceivedFirstCommit = true;
@@ -146,22 +180,20 @@ class Collection {
146
180
  }
147
181
  const items = Array.isArray(data) ? data : [data];
148
182
  const mutations = [];
149
- const keys = items.map(
150
- (item) => this.generateObjectKey(this.config.getId(item), item)
151
- );
152
- items.forEach((item, index) => {
183
+ items.forEach((item) => {
153
184
  var _a, _b;
154
185
  const validatedData = this.validateData(item, `insert`);
155
- const key = keys[index];
156
- const id = this.config.getId(item);
157
- if (this.state.has(this.getKeyFromId(id))) {
158
- 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`;
159
189
  }
190
+ const globalKey = this.generateGlobalKey(key, item);
160
191
  const mutation = {
161
192
  mutationId: crypto.randomUUID(),
162
193
  original: {},
163
194
  modified: validatedData,
164
195
  changes: validatedData,
196
+ globalKey,
165
197
  key,
166
198
  metadata: config2 == null ? void 0 : config2.metadata,
167
199
  syncMetadata: ((_b = (_a = this.config.sync).getSyncMetadata) == null ? void 0 : _b.call(_a)) || {},
@@ -174,10 +206,8 @@ class Collection {
174
206
  });
175
207
  if (ambientTransaction) {
176
208
  ambientTransaction.applyMutations(mutations);
177
- this.transactions.setState((sortedMap) => {
178
- sortedMap.set(ambientTransaction.id, ambientTransaction);
179
- return sortedMap;
180
- });
209
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
210
+ this.recomputeOptimisticState();
181
211
  return ambientTransaction;
182
212
  } else {
183
213
  const directOpTransaction = new Transaction({
@@ -187,33 +217,34 @@ class Collection {
187
217
  });
188
218
  directOpTransaction.applyMutations(mutations);
189
219
  directOpTransaction.commit();
190
- this.transactions.setState((sortedMap) => {
191
- sortedMap.set(directOpTransaction.id, directOpTransaction);
192
- return sortedMap;
193
- });
220
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
221
+ this.recomputeOptimisticState();
194
222
  return directOpTransaction;
195
223
  }
196
224
  };
197
- this.delete = (ids, config2) => {
225
+ this.delete = (keys, config2) => {
198
226
  const ambientTransaction = getActiveTransaction();
199
227
  if (!ambientTransaction && !this.config.onDelete) {
200
228
  throw new Error(
201
229
  `Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
202
230
  );
203
231
  }
204
- const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
205
- (id) => this.getKeyFromId(id)
206
- );
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];
207
236
  const mutations = [];
208
- for (const id of idsArray) {
237
+ for (const key of keysArray) {
238
+ const globalKey = this.generateGlobalKey(key, this.get(key));
209
239
  const mutation = {
210
240
  mutationId: crypto.randomUUID(),
211
- original: this.state.get(id) || {},
212
- modified: this.state.get(id) || {},
213
- changes: this.state.get(id) || {},
214
- key: id,
241
+ original: this.get(key) || {},
242
+ modified: this.get(key),
243
+ changes: this.get(key) || {},
244
+ globalKey,
245
+ key,
215
246
  metadata: config2 == null ? void 0 : config2.metadata,
216
- syncMetadata: this.syncedMetadata.state.get(id) || {},
247
+ syncMetadata: this.syncedMetadata.get(key) || {},
217
248
  type: `delete`,
218
249
  createdAt: /* @__PURE__ */ new Date(),
219
250
  updatedAt: /* @__PURE__ */ new Date(),
@@ -223,24 +254,20 @@ class Collection {
223
254
  }
224
255
  if (ambientTransaction) {
225
256
  ambientTransaction.applyMutations(mutations);
226
- this.transactions.setState((sortedMap) => {
227
- sortedMap.set(ambientTransaction.id, ambientTransaction);
228
- return sortedMap;
229
- });
257
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
258
+ this.recomputeOptimisticState();
230
259
  return ambientTransaction;
231
260
  }
232
261
  const directOpTransaction = new Transaction({
233
262
  autoCommit: true,
234
- mutationFn: async (transaction) => {
235
- return this.config.onDelete(transaction);
263
+ mutationFn: async (params) => {
264
+ return this.config.onDelete(params);
236
265
  }
237
266
  });
238
267
  directOpTransaction.applyMutations(mutations);
239
268
  directOpTransaction.commit();
240
- this.transactions.setState((sortedMap) => {
241
- sortedMap.set(directOpTransaction.id, directOpTransaction);
242
- return sortedMap;
243
- });
269
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
270
+ this.recomputeOptimisticState();
244
271
  return directOpTransaction;
245
272
  };
246
273
  if (!config) {
@@ -254,121 +281,10 @@ class Collection {
254
281
  if (!config.sync) {
255
282
  throw new Error(`Collection requires a sync config`);
256
283
  }
257
- this.transactions = new Store(
258
- new SortedMap(
259
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
260
- )
284
+ this.transactions = new SortedMap(
285
+ (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
261
286
  );
262
- this.optimisticOperations = new Derived({
263
- fn: ({ currDepVals: [transactions] }) => {
264
- const result = Array.from(transactions.values()).map((transaction) => {
265
- const isActive = ![`completed`, `failed`].includes(
266
- transaction.state
267
- );
268
- return transaction.mutations.filter((mutation) => mutation.collection === this).map((mutation) => {
269
- const message = {
270
- type: mutation.type,
271
- key: mutation.key,
272
- value: mutation.modified,
273
- isActive
274
- };
275
- if (mutation.metadata !== void 0 && mutation.metadata !== null) {
276
- message.metadata = mutation.metadata;
277
- }
278
- return message;
279
- });
280
- }).flat();
281
- return result;
282
- },
283
- deps: [this.transactions]
284
- });
285
- this.optimisticOperations.mount();
286
- this.derivedState = new Derived({
287
- fn: ({ currDepVals: [syncedData, operations] }) => {
288
- const combined = new Map(syncedData);
289
- for (const operation of operations) {
290
- if (operation.isActive) {
291
- switch (operation.type) {
292
- case `insert`:
293
- combined.set(operation.key, operation.value);
294
- break;
295
- case `update`:
296
- combined.set(operation.key, operation.value);
297
- break;
298
- case `delete`:
299
- combined.delete(operation.key);
300
- break;
301
- }
302
- }
303
- }
304
- return combined;
305
- },
306
- deps: [this.syncedData, this.optimisticOperations]
307
- });
308
- this.derivedArray = new Derived({
309
- fn: ({ currDepVals: [stateMap] }) => {
310
- const array = Array.from(
311
- stateMap.values()
312
- );
313
- if (array[0] && `_orderByIndex` in array[0]) {
314
- array.sort((a, b) => {
315
- if (a._orderByIndex === b._orderByIndex) {
316
- return 0;
317
- }
318
- return a._orderByIndex < b._orderByIndex ? -1 : 1;
319
- });
320
- }
321
- return array;
322
- },
323
- deps: [this.derivedState]
324
- });
325
- this.derivedArray.mount();
326
- this.derivedChanges = new Derived({
327
- fn: ({
328
- currDepVals: [derivedState, optimisticOperations],
329
- prevDepVals
330
- }) => {
331
- const prevDerivedState = (prevDepVals == null ? void 0 : prevDepVals[0]) ?? /* @__PURE__ */ new Map();
332
- const prevOptimisticOperations = (prevDepVals == null ? void 0 : prevDepVals[1]) ?? [];
333
- const changedKeys = new Set(this.syncedKeys);
334
- optimisticOperations.flat().filter((op) => op.isActive).forEach((op) => changedKeys.add(op.key));
335
- prevOptimisticOperations.flat().forEach((op) => {
336
- changedKeys.add(op.key);
337
- });
338
- if (changedKeys.size === 0) {
339
- return [];
340
- }
341
- const changes = [];
342
- for (const key of changedKeys) {
343
- if (prevDerivedState.has(key) && !derivedState.has(key)) {
344
- changes.push({
345
- type: `delete`,
346
- key,
347
- value: prevDerivedState.get(key)
348
- });
349
- } else if (!prevDerivedState.has(key) && derivedState.has(key)) {
350
- changes.push({ type: `insert`, key, value: derivedState.get(key) });
351
- } else if (prevDerivedState.has(key) && derivedState.has(key)) {
352
- const value = derivedState.get(key);
353
- const previousValue = prevDerivedState.get(key);
354
- if (value !== previousValue) {
355
- changes.push({
356
- type: `update`,
357
- key,
358
- value,
359
- previousValue
360
- });
361
- }
362
- }
363
- }
364
- this.syncedKeys.clear();
365
- return changes;
366
- },
367
- deps: [this.derivedState, this.optimisticOperations]
368
- });
369
- this.derivedChanges.mount();
370
287
  this.config = config;
371
- this.derivedState.mount();
372
288
  config.sync.sync({
373
289
  collection: this,
374
290
  begin: () => {
@@ -387,17 +303,13 @@ class Collection {
387
303
  `The pending sync transaction is already committed, you can't still write to it.`
388
304
  );
389
305
  }
390
- const key = this.generateObjectKey(
391
- this.config.getId(messageWithoutKey.value),
392
- messageWithoutKey.value
393
- );
306
+ const key = this.getKeyFromItem(messageWithoutKey.value);
394
307
  if (messageWithoutKey.type === `insert`) {
395
- if (this.syncedData.state.has(key) && !pendingTransaction.operations.some(
308
+ if (this.syncedData.has(key) && !pendingTransaction.operations.some(
396
309
  (op) => op.key === key && op.type === `delete`
397
310
  )) {
398
- const id = this.config.getId(messageWithoutKey.value);
399
311
  throw new Error(
400
- `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}"`
401
313
  );
402
314
  }
403
315
  }
@@ -430,6 +342,189 @@ class Collection {
430
342
  onFirstCommit(callback) {
431
343
  this.onFirstCommitCallbacks.push(callback);
432
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
+ }
433
528
  ensureStandardSchema(schema) {
434
529
  if (schema && typeof schema === `object` && `~standard` in schema) {
435
530
  return schema;
@@ -438,31 +533,24 @@ class Collection {
438
533
  `Schema must either implement the standard-schema interface or be a Zod schema`
439
534
  );
440
535
  }
441
- getKeyFromId(id) {
442
- if (typeof id === `undefined`) {
443
- throw new Error(`id is undefined`);
444
- }
445
- if (typeof id === `string` && id.startsWith(`KEY::`)) {
446
- return id;
447
- } else {
448
- return this.generateObjectKey(id, null);
449
- }
536
+ getKeyFromItem(item) {
537
+ return this.config.getKey(item);
450
538
  }
451
- generateObjectKey(id, item) {
452
- if (typeof id === `undefined`) {
539
+ generateGlobalKey(key, item) {
540
+ if (typeof key === `undefined`) {
453
541
  throw new Error(
454
- `An object was created without a defined id: ${JSON.stringify(item)}`
542
+ `An object was created without a defined key: ${JSON.stringify(item)}`
455
543
  );
456
544
  }
457
- return `KEY::${this.id}/${id}`;
545
+ return `KEY::${this.id}/${key}`;
458
546
  }
459
547
  validateData(data, type, key) {
460
548
  if (!this.config.schema) return data;
461
549
  const standardSchema = this.ensureStandardSchema(this.config.schema);
462
550
  if (type === `update` && key) {
463
- const existingData = this.state.get(key);
551
+ const existingData = this.get(key);
464
552
  if (existingData && data && typeof data === `object` && typeof existingData === `object`) {
465
- const mergedData = { ...existingData, ...data };
553
+ const mergedData = Object.assign({}, existingData, data);
466
554
  const result2 = standardSchema[`~standard`].validate(mergedData);
467
555
  if (result2 instanceof Promise) {
468
556
  throw new TypeError(`Schema validation must be synchronous`);
@@ -496,8 +584,8 @@ class Collection {
496
584
  }
497
585
  return result.value;
498
586
  }
499
- update(ids, configOrCallback, maybeCallback) {
500
- if (typeof ids === `undefined`) {
587
+ update(keys, configOrCallback, maybeCallback) {
588
+ if (typeof keys === `undefined`) {
501
589
  throw new Error(`The first argument to update is missing`);
502
590
  }
503
591
  const ambientTransaction = getActiveTransaction();
@@ -506,17 +594,18 @@ class Collection {
506
594
  `Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
507
595
  );
508
596
  }
509
- const isArray = Array.isArray(ids);
510
- const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
511
- (id) => this.getKeyFromId(id)
512
- );
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
+ }
513
602
  const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
514
603
  const config = typeof configOrCallback === `function` ? {} : configOrCallback;
515
- const currentObjects = idsArray.map((id) => {
516
- const item = this.state.get(id);
604
+ const currentObjects = keysArray.map((key) => {
605
+ const item = this.get(key);
517
606
  if (!item) {
518
607
  throw new Error(
519
- `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`
520
609
  );
521
610
  }
522
611
  return item;
@@ -534,7 +623,7 @@ class Collection {
534
623
  );
535
624
  changesArray = [result];
536
625
  }
537
- const mutations = idsArray.map((id, index) => {
626
+ const mutations = keysArray.map((key, index) => {
538
627
  const itemChanges = changesArray[index];
539
628
  if (!itemChanges || Object.keys(itemChanges).length === 0) {
540
629
  return null;
@@ -543,24 +632,30 @@ class Collection {
543
632
  const validatedUpdatePayload = this.validateData(
544
633
  itemChanges,
545
634
  `update`,
546
- id
635
+ key
547
636
  );
548
- const modifiedItem = { ...originalItem, ...validatedUpdatePayload };
549
- const originalItemId = this.config.getId(originalItem);
550
- const modifiedItemId = this.config.getId(modifiedItem);
637
+ const modifiedItem = Object.assign(
638
+ {},
639
+ originalItem,
640
+ validatedUpdatePayload
641
+ );
642
+ const originalItemId = this.getKeyFromItem(originalItem);
643
+ const modifiedItemId = this.getKeyFromItem(modifiedItem);
551
644
  if (originalItemId !== modifiedItemId) {
552
645
  throw new Error(
553
- `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.`
554
647
  );
555
648
  }
649
+ const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem);
556
650
  return {
557
651
  mutationId: crypto.randomUUID(),
558
652
  original: originalItem,
559
653
  modified: modifiedItem,
560
654
  changes: validatedUpdatePayload,
561
- key: id,
655
+ globalKey,
656
+ key,
562
657
  metadata: config.metadata,
563
- syncMetadata: this.syncedMetadata.state.get(id) || {},
658
+ syncMetadata: this.syncedMetadata.get(key) || {},
564
659
  type: `update`,
565
660
  createdAt: /* @__PURE__ */ new Date(),
566
661
  updatedAt: /* @__PURE__ */ new Date(),
@@ -572,23 +667,19 @@ class Collection {
572
667
  }
573
668
  if (ambientTransaction) {
574
669
  ambientTransaction.applyMutations(mutations);
575
- this.transactions.setState((sortedMap) => {
576
- sortedMap.set(ambientTransaction.id, ambientTransaction);
577
- return sortedMap;
578
- });
670
+ this.transactions.set(ambientTransaction.id, ambientTransaction);
671
+ this.recomputeOptimisticState();
579
672
  return ambientTransaction;
580
673
  }
581
674
  const directOpTransaction = new Transaction({
582
- mutationFn: async (transaction) => {
583
- return this.config.onUpdate(transaction);
675
+ mutationFn: async (params) => {
676
+ return this.config.onUpdate(params);
584
677
  }
585
678
  });
586
679
  directOpTransaction.applyMutations(mutations);
587
680
  directOpTransaction.commit();
588
- this.transactions.setState((sortedMap) => {
589
- sortedMap.set(directOpTransaction.id, directOpTransaction);
590
- return sortedMap;
591
- });
681
+ this.transactions.set(directOpTransaction.id, directOpTransaction);
682
+ this.recomputeOptimisticState();
592
683
  return directOpTransaction;
593
684
  }
594
685
  /**
@@ -597,7 +688,11 @@ class Collection {
597
688
  * @returns A Map containing all items in the collection, with keys as identifiers
598
689
  */
599
690
  get state() {
600
- 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;
601
696
  }
602
697
  /**
603
698
  * Gets the current state of the collection as a Map, but only resolves when data is available
@@ -606,7 +701,7 @@ class Collection {
606
701
  * @returns Promise that resolves to a Map containing all items in the collection
607
702
  */
608
703
  stateWhenReady() {
609
- if (this.state.size > 0 || this.hasReceivedFirstCommit === true) {
704
+ if (this.size > 0 || this.hasReceivedFirstCommit === true) {
610
705
  return Promise.resolve(this.state);
611
706
  }
612
707
  return new Promise((resolve) => {
@@ -621,7 +716,13 @@ class Collection {
621
716
  * @returns An Array containing all items in the collection
622
717
  */
623
718
  get toArray() {
624
- 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;
625
726
  }
626
727
  /**
627
728
  * Gets the current state of the collection as an Array, but only resolves when data is available
@@ -630,7 +731,7 @@ class Collection {
630
731
  * @returns Promise that resolves to an Array containing all items in the collection
631
732
  */
632
733
  toArrayWhenReady() {
633
- if (this.toArray.length > 0 || this.hasReceivedFirstCommit === true) {
734
+ if (this.size > 0 || this.hasReceivedFirstCommit === true) {
634
735
  return Promise.resolve(this.toArray);
635
736
  }
636
737
  return new Promise((resolve) => {
@@ -644,7 +745,7 @@ class Collection {
644
745
  * @returns An array of changes
645
746
  */
646
747
  currentStateAsChanges() {
647
- return [...this.state.entries()].map(([key, value]) => ({
748
+ return Array.from(this.entries()).map(([key, value]) => ({
648
749
  type: `insert`,
649
750
  key,
650
751
  value
@@ -655,17 +756,82 @@ class Collection {
655
756
  * @param callback - A function that will be called with the changes in the collection
656
757
  * @returns A function that can be called to unsubscribe from the changes
657
758
  */
658
- subscribeChanges(callback) {
659
- callback(this.currentStateAsChanges());
660
- return this.derivedChanges.subscribe((changes) => {
661
- if (changes.currentVal.length > 0) {
662
- 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
+ }
663
792
  }
664
- });
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;
665
831
  }
666
832
  }
667
833
  export {
668
- Collection,
834
+ CollectionImpl,
669
835
  SchemaValidationError,
670
836
  collectionsStore,
671
837
  createCollection,