@tanstack/db 0.0.4 → 0.0.6

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 (102) hide show
  1. package/dist/cjs/collection.cjs +182 -113
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +43 -15
  4. package/dist/cjs/index.cjs +1 -0
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/proxy.cjs +87 -248
  7. package/dist/cjs/proxy.cjs.map +1 -1
  8. package/dist/cjs/proxy.d.cts +5 -5
  9. package/dist/cjs/query/compiled-query.cjs +23 -14
  10. package/dist/cjs/query/compiled-query.cjs.map +1 -1
  11. package/dist/cjs/query/compiled-query.d.cts +3 -1
  12. package/dist/cjs/query/evaluators.cjs +35 -20
  13. package/dist/cjs/query/evaluators.cjs.map +1 -1
  14. package/dist/cjs/query/evaluators.d.cts +8 -3
  15. package/dist/cjs/query/extractors.cjs +20 -20
  16. package/dist/cjs/query/extractors.cjs.map +1 -1
  17. package/dist/cjs/query/extractors.d.cts +3 -3
  18. package/dist/cjs/query/group-by.cjs +12 -15
  19. package/dist/cjs/query/group-by.cjs.map +1 -1
  20. package/dist/cjs/query/group-by.d.cts +7 -7
  21. package/dist/cjs/query/joins.cjs +41 -55
  22. package/dist/cjs/query/joins.cjs.map +1 -1
  23. package/dist/cjs/query/joins.d.cts +3 -3
  24. package/dist/cjs/query/order-by.cjs +37 -84
  25. package/dist/cjs/query/order-by.cjs.map +1 -1
  26. package/dist/cjs/query/order-by.d.cts +2 -2
  27. package/dist/cjs/query/pipeline-compiler.cjs +13 -18
  28. package/dist/cjs/query/pipeline-compiler.cjs.map +1 -1
  29. package/dist/cjs/query/pipeline-compiler.d.cts +2 -1
  30. package/dist/cjs/query/query-builder.cjs +22 -29
  31. package/dist/cjs/query/query-builder.cjs.map +1 -1
  32. package/dist/cjs/query/query-builder.d.cts +16 -10
  33. package/dist/cjs/query/schema.d.cts +12 -11
  34. package/dist/cjs/query/select.cjs +47 -24
  35. package/dist/cjs/query/select.cjs.map +1 -1
  36. package/dist/cjs/query/select.d.cts +2 -2
  37. package/dist/cjs/query/types.d.cts +1 -0
  38. package/dist/cjs/transactions.cjs +20 -9
  39. package/dist/cjs/transactions.cjs.map +1 -1
  40. package/dist/cjs/types.d.cts +66 -7
  41. package/dist/esm/collection.d.ts +43 -15
  42. package/dist/esm/collection.js +183 -114
  43. package/dist/esm/collection.js.map +1 -1
  44. package/dist/esm/index.js +2 -1
  45. package/dist/esm/proxy.d.ts +5 -5
  46. package/dist/esm/proxy.js +87 -248
  47. package/dist/esm/proxy.js.map +1 -1
  48. package/dist/esm/query/compiled-query.d.ts +3 -1
  49. package/dist/esm/query/compiled-query.js +23 -14
  50. package/dist/esm/query/compiled-query.js.map +1 -1
  51. package/dist/esm/query/evaluators.d.ts +8 -3
  52. package/dist/esm/query/evaluators.js +36 -21
  53. package/dist/esm/query/evaluators.js.map +1 -1
  54. package/dist/esm/query/extractors.d.ts +3 -3
  55. package/dist/esm/query/extractors.js +20 -20
  56. package/dist/esm/query/extractors.js.map +1 -1
  57. package/dist/esm/query/group-by.d.ts +7 -7
  58. package/dist/esm/query/group-by.js +14 -17
  59. package/dist/esm/query/group-by.js.map +1 -1
  60. package/dist/esm/query/joins.d.ts +3 -3
  61. package/dist/esm/query/joins.js +42 -56
  62. package/dist/esm/query/joins.js.map +1 -1
  63. package/dist/esm/query/order-by.d.ts +2 -2
  64. package/dist/esm/query/order-by.js +39 -86
  65. package/dist/esm/query/order-by.js.map +1 -1
  66. package/dist/esm/query/pipeline-compiler.d.ts +2 -1
  67. package/dist/esm/query/pipeline-compiler.js +14 -19
  68. package/dist/esm/query/pipeline-compiler.js.map +1 -1
  69. package/dist/esm/query/query-builder.d.ts +16 -10
  70. package/dist/esm/query/query-builder.js +22 -29
  71. package/dist/esm/query/query-builder.js.map +1 -1
  72. package/dist/esm/query/schema.d.ts +12 -11
  73. package/dist/esm/query/select.d.ts +2 -2
  74. package/dist/esm/query/select.js +48 -25
  75. package/dist/esm/query/select.js.map +1 -1
  76. package/dist/esm/query/types.d.ts +1 -0
  77. package/dist/esm/transactions.js +20 -9
  78. package/dist/esm/transactions.js.map +1 -1
  79. package/dist/esm/types.d.ts +66 -7
  80. package/package.json +2 -2
  81. package/src/collection.ts +286 -146
  82. package/src/proxy.ts +141 -358
  83. package/src/query/compiled-query.ts +30 -15
  84. package/src/query/evaluators.ts +49 -21
  85. package/src/query/extractors.ts +24 -21
  86. package/src/query/group-by.ts +24 -22
  87. package/src/query/joins.ts +88 -75
  88. package/src/query/order-by.ts +56 -106
  89. package/src/query/pipeline-compiler.ts +34 -37
  90. package/src/query/query-builder.ts +49 -46
  91. package/src/query/schema.ts +18 -15
  92. package/src/query/select.ts +68 -33
  93. package/src/query/types.ts +1 -0
  94. package/src/transactions.ts +30 -14
  95. package/src/types.ts +76 -7
  96. package/dist/cjs/query/key-by.cjs +0 -43
  97. package/dist/cjs/query/key-by.cjs.map +0 -1
  98. package/dist/cjs/query/key-by.d.cts +0 -3
  99. package/dist/esm/query/key-by.d.ts +0 -3
  100. package/dist/esm/query/key-by.js +0 -43
  101. package/dist/esm/query/key-by.js.map +0 -1
  102. package/src/query/key-by.ts +0 -61
@@ -1,7 +1,16 @@
1
1
  import { Derived, Store } from '@tanstack/store';
2
+ import { Transaction } from './transactions.js';
2
3
  import { SortedMap } from './SortedMap.js';
3
- import { ChangeMessage, CollectionConfig, InsertConfig, OperationConfig, OptimisticChangeMessage, Transaction } from './types.js';
4
+ import { ChangeMessage, CollectionConfig, InsertConfig, OperationConfig, OptimisticChangeMessage, Transaction as TransactionType } from './types.js';
4
5
  export declare const collectionsStore: Store<Map<string, Collection<any>>, (cb: Map<string, Collection<any>>) => Map<string, Collection<any>>>;
6
+ /**
7
+ * Creates a new Collection instance with the given configuration
8
+ *
9
+ * @template T - The type of items in the collection
10
+ * @param config - Configuration for the collection, including id and sync
11
+ * @returns A new Collection instance
12
+ */
13
+ export declare function createCollection<T extends object = Record<string, unknown>>(config: CollectionConfig<T>): Collection<T>;
5
14
  /**
6
15
  * Preloads a collection with the given configuration
7
16
  * Returns a promise that resolves once the sync tool has done its first commit (initial sync is finished)
@@ -44,7 +53,7 @@ export declare class SchemaValidationError extends Error {
44
53
  }>, message?: string);
45
54
  }
46
55
  export declare class Collection<T extends object = Record<string, unknown>> {
47
- transactions: Store<SortedMap<string, Transaction>>;
56
+ transactions: Store<SortedMap<string, TransactionType>>;
48
57
  optimisticOperations: Derived<Array<OptimisticChangeMessage<T>>>;
49
58
  derivedState: Derived<Map<string, T>>;
50
59
  derivedArray: Derived<Array<T>>;
@@ -55,7 +64,6 @@ export declare class Collection<T extends object = Record<string, unknown>> {
55
64
  private syncedKeys;
56
65
  config: CollectionConfig<T>;
57
66
  private hasReceivedFirstCommit;
58
- objectKeyMap: WeakMap<object, string>;
59
67
  private onFirstCommitCallbacks;
60
68
  /**
61
69
  * Register a callback to be executed on the next commit
@@ -63,27 +71,28 @@ export declare class Collection<T extends object = Record<string, unknown>> {
63
71
  * @param callback Function to call after the next commit
64
72
  */
65
73
  onFirstCommit(callback: () => void): void;
66
- id: `${string}-${string}-${string}-${string}-${string}`;
74
+ id: string;
67
75
  /**
68
76
  * Creates a new Collection instance
69
77
  *
70
78
  * @param config - Configuration object for the collection
71
79
  * @throws Error if sync config is missing
72
80
  */
73
- constructor(config?: CollectionConfig<T>);
81
+ constructor(config: CollectionConfig<T>);
74
82
  /**
75
83
  * Attempts to commit pending synced transactions if there are no active transactions
76
84
  * This method processes operations from pending transactions and applies them to the synced data
77
85
  */
78
86
  commitPendingTransactions: () => void;
79
87
  private ensureStandardSchema;
88
+ private getKeyFromId;
89
+ generateObjectKey(id: any, item: any): string;
80
90
  private validateData;
81
- private generateKey;
82
91
  /**
83
92
  * Inserts one or more items into the collection
84
93
  * @param items - Single item or array of items to insert
85
94
  * @param config - Optional configuration including metadata and custom keys
86
- * @returns A Transaction object representing the insert operation(s)
95
+ * @returns A TransactionType object representing the insert operation(s)
87
96
  * @throws {SchemaValidationError} If the data fails schema validation
88
97
  * @example
89
98
  * // Insert a single item
@@ -118,24 +127,43 @@ export declare class Collection<T extends object = Record<string, unknown>> {
118
127
  * // Update with metadata
119
128
  * update(todo, { metadata: { reason: "user update" } }, (draft) => { draft.text = "Updated text" })
120
129
  */
121
- update<TItem extends object = T>(item: TItem, configOrCallback: ((draft: TItem) => void) | OperationConfig, maybeCallback?: (draft: TItem) => void): Transaction;
122
- update<TItem extends object = T>(items: Array<TItem>, configOrCallback: ((draft: Array<TItem>) => void) | OperationConfig, maybeCallback?: (draft: Array<TItem>) => void): Transaction;
130
+ /**
131
+ * Updates one or more items in the collection using a callback function
132
+ * @param ids - Single ID or array of IDs to update
133
+ * @param configOrCallback - Either update configuration or update callback
134
+ * @param maybeCallback - Update callback if config was provided
135
+ * @returns A Transaction object representing the update operation(s)
136
+ * @throws {SchemaValidationError} If the updated data fails schema validation
137
+ * @example
138
+ * // Update a single item
139
+ * update("todo-1", (draft) => { draft.completed = true })
140
+ *
141
+ * // Update multiple items
142
+ * update(["todo-1", "todo-2"], (drafts) => {
143
+ * drafts.forEach(draft => { draft.completed = true })
144
+ * })
145
+ *
146
+ * // Update with metadata
147
+ * update("todo-1", { metadata: { reason: "user update" } }, (draft) => { draft.text = "Updated text" })
148
+ */
149
+ update<TItem extends object = T>(id: unknown, configOrCallback: ((draft: TItem) => void) | OperationConfig, maybeCallback?: (draft: TItem) => void): TransactionType;
150
+ update<TItem extends object = T>(ids: Array<unknown>, configOrCallback: ((draft: Array<TItem>) => void) | OperationConfig, maybeCallback?: (draft: Array<TItem>) => void): TransactionType;
123
151
  /**
124
152
  * Deletes one or more items from the collection
125
- * @param items - Single item/key or array of items/keys to delete
153
+ * @param ids - Single ID or array of IDs to delete
126
154
  * @param config - Optional configuration including metadata
127
- * @returns A Transaction object representing the delete operation(s)
155
+ * @returns A TransactionType object representing the delete operation(s)
128
156
  * @example
129
157
  * // Delete a single item
130
- * delete(todo)
158
+ * delete("todo-1")
131
159
  *
132
160
  * // Delete multiple items
133
- * delete([todo1, todo2])
161
+ * delete(["todo-1", "todo-2"])
134
162
  *
135
163
  * // Delete with metadata
136
- * delete(todo, { metadata: { reason: "completed" } })
164
+ * delete("todo-1", { metadata: { reason: "completed" } })
137
165
  */
138
- delete: (items: Array<T | string> | T | string, config?: OperationConfig) => Transaction;
166
+ delete: (ids: Array<string> | string, config?: OperationConfig) => TransactionType;
139
167
  /**
140
168
  * Gets the current state of the collection as a Map
141
169
  *
@@ -1,10 +1,16 @@
1
1
  import { Store, batch, Derived } from "@tanstack/store";
2
2
  import { withArrayChangeTracking, withChangeTracking } from "./proxy.js";
3
- import { getActiveTransaction } from "./transactions.js";
3
+ import { getActiveTransaction, Transaction } from "./transactions.js";
4
4
  import { SortedMap } from "./SortedMap.js";
5
5
  const collectionsStore = new Store(/* @__PURE__ */ new Map());
6
6
  const loadingCollections = /* @__PURE__ */ new Map();
7
+ function createCollection(config) {
8
+ return new Collection(config);
9
+ }
7
10
  function preloadCollection(config) {
11
+ if (!config.id) {
12
+ throw new Error(`The id property is required for preloadCollection`);
13
+ }
8
14
  if (collectionsStore.state.has(config.id) && !loadingCollections.has(config.id)) {
9
15
  return Promise.resolve(
10
16
  collectionsStore.state.get(config.id)
@@ -16,10 +22,14 @@ function preloadCollection(config) {
16
22
  if (!collectionsStore.state.has(config.id)) {
17
23
  collectionsStore.setState((prev) => {
18
24
  const next = new Map(prev);
25
+ if (!config.id) {
26
+ throw new Error(`The id property is required for preloadCollection`);
27
+ }
19
28
  next.set(
20
29
  config.id,
21
30
  new Collection({
22
31
  id: config.id,
32
+ getId: config.getId,
23
33
  sync: config.sync,
24
34
  schema: config.schema
25
35
  })
@@ -35,6 +45,9 @@ function preloadCollection(config) {
35
45
  };
36
46
  });
37
47
  collection.onFirstCommit(() => {
48
+ if (!config.id) {
49
+ throw new Error(`The id property is required for preloadCollection`);
50
+ }
38
51
  if (loadingCollections.has(config.id)) {
39
52
  loadingCollections.delete(config.id);
40
53
  resolveFirstCommit();
@@ -68,18 +81,15 @@ class Collection {
68
81
  this.pendingSyncedTransactions = [];
69
82
  this.syncedKeys = /* @__PURE__ */ new Set();
70
83
  this.hasReceivedFirstCommit = false;
71
- this.objectKeyMap = /* @__PURE__ */ new WeakMap();
72
84
  this.onFirstCommitCallbacks = [];
73
- this.id = crypto.randomUUID();
85
+ this.id = ``;
74
86
  this.commitPendingTransactions = () => {
75
87
  if (!Array.from(this.transactions.state.values()).some(
76
88
  ({ state }) => state === `persisting`
77
89
  )) {
78
- const keys = /* @__PURE__ */ new Set();
79
90
  batch(() => {
80
91
  for (const transaction of this.pendingSyncedTransactions) {
81
92
  for (const operation of transaction.operations) {
82
- keys.add(operation.key);
83
93
  this.syncedKeys.add(operation.key);
84
94
  this.syncedMetadata.setState((prevData) => {
85
95
  switch (operation.type) {
@@ -118,12 +128,6 @@ class Collection {
118
128
  }
119
129
  }
120
130
  });
121
- keys.forEach((key) => {
122
- const curValue = this.state.get(key);
123
- if (curValue) {
124
- this.objectKeyMap.set(curValue, key);
125
- }
126
- });
127
131
  this.pendingSyncedTransactions = [];
128
132
  if (!this.hasReceivedFirstCommit) {
129
133
  this.hasReceivedFirstCommit = true;
@@ -134,26 +138,25 @@ class Collection {
134
138
  }
135
139
  };
136
140
  this.insert = (data, config2) => {
137
- const transaction = getActiveTransaction();
138
- if (typeof transaction === `undefined`) {
139
- throw `no transaction found when calling collection.insert`;
141
+ const ambientTransaction = getActiveTransaction();
142
+ if (!ambientTransaction && !this.config.onInsert) {
143
+ throw new Error(
144
+ `Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.`
145
+ );
140
146
  }
141
147
  const items = Array.isArray(data) ? data : [data];
142
148
  const mutations = [];
143
- let keys;
144
- if (config2 == null ? void 0 : config2.key) {
145
- const configKeys = Array.isArray(config2.key) ? config2.key : [config2.key];
146
- if (Array.isArray(config2.key) && configKeys.length > items.length) {
147
- throw new Error(`More keys provided than items to insert`);
148
- }
149
- keys = items.map((_, i) => configKeys[i] ?? this.generateKey(items[i]));
150
- } else {
151
- keys = items.map((item) => this.generateKey(item));
152
- }
149
+ const keys = items.map(
150
+ (item) => this.generateObjectKey(this.config.getId(item), item)
151
+ );
153
152
  items.forEach((item, index) => {
154
153
  var _a, _b;
155
154
  const validatedData = this.validateData(item, `insert`);
156
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`;
159
+ }
157
160
  const mutation = {
158
161
  mutationId: crypto.randomUUID(),
159
162
  original: {},
@@ -169,45 +172,48 @@ class Collection {
169
172
  };
170
173
  mutations.push(mutation);
171
174
  });
172
- transaction.applyMutations(mutations);
173
- this.transactions.setState((sortedMap) => {
174
- sortedMap.set(transaction.id, transaction);
175
- return sortedMap;
176
- });
177
- return transaction;
175
+ if (ambientTransaction) {
176
+ ambientTransaction.applyMutations(mutations);
177
+ this.transactions.setState((sortedMap) => {
178
+ sortedMap.set(ambientTransaction.id, ambientTransaction);
179
+ return sortedMap;
180
+ });
181
+ return ambientTransaction;
182
+ } else {
183
+ const directOpTransaction = new Transaction({
184
+ mutationFn: async (params) => {
185
+ return this.config.onInsert(params);
186
+ }
187
+ });
188
+ directOpTransaction.applyMutations(mutations);
189
+ directOpTransaction.commit();
190
+ this.transactions.setState((sortedMap) => {
191
+ sortedMap.set(directOpTransaction.id, directOpTransaction);
192
+ return sortedMap;
193
+ });
194
+ return directOpTransaction;
195
+ }
178
196
  };
179
- this.delete = (items, config2) => {
180
- const transaction = getActiveTransaction();
181
- if (typeof transaction === `undefined`) {
182
- throw `no transaction found when calling collection.delete`;
197
+ this.delete = (ids, config2) => {
198
+ const ambientTransaction = getActiveTransaction();
199
+ if (!ambientTransaction && !this.config.onDelete) {
200
+ throw new Error(
201
+ `Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
202
+ );
183
203
  }
184
- const itemsArray = Array.isArray(items) ? items : [items];
204
+ const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
205
+ (id) => this.getKeyFromId(id)
206
+ );
185
207
  const mutations = [];
186
- for (const item of itemsArray) {
187
- let key;
188
- if (typeof item === `object` && item !== null) {
189
- const objectKey = this.objectKeyMap.get(item);
190
- if (objectKey === void 0) {
191
- throw new Error(
192
- `Object not found in collection: ${JSON.stringify(item)}`
193
- );
194
- }
195
- key = objectKey;
196
- } else if (typeof item === `string`) {
197
- key = item;
198
- } else {
199
- throw new Error(
200
- `Invalid item type for delete - must be an object or string key`
201
- );
202
- }
208
+ for (const id of idsArray) {
203
209
  const mutation = {
204
210
  mutationId: crypto.randomUUID(),
205
- original: this.state.get(key) || {},
206
- modified: { _deleted: true },
207
- changes: { _deleted: true },
208
- key,
211
+ original: this.state.get(id) || {},
212
+ modified: this.state.get(id) || {},
213
+ changes: this.state.get(id) || {},
214
+ key: id,
209
215
  metadata: config2 == null ? void 0 : config2.metadata,
210
- syncMetadata: this.syncedMetadata.state.get(key) || {},
216
+ syncMetadata: this.syncedMetadata.state.get(id) || {},
211
217
  type: `delete`,
212
218
  createdAt: /* @__PURE__ */ new Date(),
213
219
  updatedAt: /* @__PURE__ */ new Date(),
@@ -215,20 +221,37 @@ class Collection {
215
221
  };
216
222
  mutations.push(mutation);
217
223
  }
218
- mutations.forEach((mutation) => {
219
- const curValue = this.state.get(mutation.key);
220
- if (curValue) {
221
- this.objectKeyMap.delete(curValue);
224
+ if (ambientTransaction) {
225
+ ambientTransaction.applyMutations(mutations);
226
+ this.transactions.setState((sortedMap) => {
227
+ sortedMap.set(ambientTransaction.id, ambientTransaction);
228
+ return sortedMap;
229
+ });
230
+ return ambientTransaction;
231
+ }
232
+ const directOpTransaction = new Transaction({
233
+ autoCommit: true,
234
+ mutationFn: async (transaction) => {
235
+ return this.config.onDelete(transaction);
222
236
  }
223
237
  });
224
- transaction.applyMutations(mutations);
238
+ directOpTransaction.applyMutations(mutations);
239
+ directOpTransaction.commit();
225
240
  this.transactions.setState((sortedMap) => {
226
- sortedMap.set(transaction.id, transaction);
241
+ sortedMap.set(directOpTransaction.id, directOpTransaction);
227
242
  return sortedMap;
228
243
  });
229
- return transaction;
244
+ return directOpTransaction;
230
245
  };
231
- if (!(config == null ? void 0 : config.sync)) {
246
+ if (!config) {
247
+ throw new Error(`Collection requires a config`);
248
+ }
249
+ if (config.id) {
250
+ this.id = config.id;
251
+ } else {
252
+ this.id = crypto.randomUUID();
253
+ }
254
+ if (!config.sync) {
232
255
  throw new Error(`Collection requires a sync config`);
233
256
  }
234
257
  this.transactions = new Store(
@@ -263,9 +286,7 @@ class Collection {
263
286
  this.derivedState = new Derived({
264
287
  fn: ({ currDepVals: [syncedData, operations] }) => {
265
288
  const combined = new Map(syncedData);
266
- const optimisticKeys = /* @__PURE__ */ new Set();
267
289
  for (const operation of operations) {
268
- optimisticKeys.add(operation.key);
269
290
  if (operation.isActive) {
270
291
  switch (operation.type) {
271
292
  case `insert`:
@@ -280,11 +301,6 @@ class Collection {
280
301
  }
281
302
  }
282
303
  }
283
- optimisticKeys.forEach((key) => {
284
- if (combined.has(key)) {
285
- this.objectKeyMap.set(combined.get(key), key);
286
- }
287
- });
288
304
  return combined;
289
305
  },
290
306
  deps: [this.syncedData, this.optimisticOperations]
@@ -361,7 +377,7 @@ class Collection {
361
377
  operations: []
362
378
  });
363
379
  },
364
- write: (message) => {
380
+ write: (messageWithoutKey) => {
365
381
  const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
366
382
  if (!pendingTransaction) {
367
383
  throw new Error(`No pending sync transaction to write to`);
@@ -371,6 +387,24 @@ class Collection {
371
387
  `The pending sync transaction is already committed, you can't still write to it.`
372
388
  );
373
389
  }
390
+ const key = this.generateObjectKey(
391
+ this.config.getId(messageWithoutKey.value),
392
+ messageWithoutKey.value
393
+ );
394
+ if (messageWithoutKey.type === `insert`) {
395
+ if (this.syncedData.state.has(key) && !pendingTransaction.operations.some(
396
+ (op) => op.key === key && op.type === `delete`
397
+ )) {
398
+ const id = this.config.getId(messageWithoutKey.value);
399
+ throw new Error(
400
+ `Cannot insert document with ID "${id}" from sync because it already exists in the collection "${this.id}"`
401
+ );
402
+ }
403
+ }
404
+ const message = {
405
+ ...messageWithoutKey,
406
+ key
407
+ };
374
408
  pendingTransaction.operations.push(message);
375
409
  },
376
410
  commit: () => {
@@ -404,6 +438,24 @@ class Collection {
404
438
  `Schema must either implement the standard-schema interface or be a Zod schema`
405
439
  );
406
440
  }
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
+ }
450
+ }
451
+ generateObjectKey(id, item) {
452
+ if (typeof id === `undefined`) {
453
+ throw new Error(
454
+ `An object was created without a defined id: ${JSON.stringify(item)}`
455
+ );
456
+ }
457
+ return `KEY::${this.id}/${id}`;
458
+ }
407
459
  validateData(data, type, key) {
408
460
  if (!this.config.schema) return data;
409
461
  const standardSchema = this.ensureStandardSchema(this.config.schema);
@@ -444,39 +496,31 @@ class Collection {
444
496
  }
445
497
  return result.value;
446
498
  }
447
- generateKey(data) {
448
- const str = JSON.stringify(data);
449
- let h = 0;
450
- for (let i = 0; i < str.length; i++) {
451
- h = Math.imul(31, h) + str.charCodeAt(i) | 0;
452
- }
453
- return `${this.id}/${Math.abs(h).toString(36)}`;
454
- }
455
- update(items, configOrCallback, maybeCallback) {
456
- if (typeof items === `undefined`) {
499
+ update(ids, configOrCallback, maybeCallback) {
500
+ if (typeof ids === `undefined`) {
457
501
  throw new Error(`The first argument to update is missing`);
458
502
  }
459
- const transaction = getActiveTransaction();
460
- if (typeof transaction === `undefined`) {
461
- throw `no transaction found when calling collection.update`;
503
+ const ambientTransaction = getActiveTransaction();
504
+ if (!ambientTransaction && !this.config.onUpdate) {
505
+ throw new Error(
506
+ `Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
507
+ );
462
508
  }
463
- const isArray = Array.isArray(items);
464
- const itemsArray = Array.isArray(items) ? items : [items];
509
+ const isArray = Array.isArray(ids);
510
+ const idsArray = (Array.isArray(ids) ? ids : [ids]).map(
511
+ (id) => this.getKeyFromId(id)
512
+ );
465
513
  const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
466
514
  const config = typeof configOrCallback === `function` ? {} : configOrCallback;
467
- const keys = itemsArray.map((item) => {
468
- if (typeof item === `object` && item !== null) {
469
- const key = this.objectKeyMap.get(item);
470
- if (key === void 0) {
471
- throw new Error(`Object not found in collection`);
472
- }
473
- return key;
515
+ const currentObjects = idsArray.map((id) => {
516
+ const item = this.state.get(id);
517
+ if (!item) {
518
+ throw new Error(
519
+ `The id "${id}" was passed to update but an object for this ID was not found in the collection`
520
+ );
474
521
  }
475
- throw new Error(`Invalid item type for update - must be an object`);
522
+ return item;
476
523
  });
477
- const currentObjects = keys.map((key) => ({
478
- ...this.state.get(key) || {}
479
- }));
480
524
  let changesArray;
481
525
  if (isArray) {
482
526
  changesArray = withArrayChangeTracking(
@@ -490,23 +534,33 @@ class Collection {
490
534
  );
491
535
  changesArray = [result];
492
536
  }
493
- const mutations = keys.map((key, index) => {
494
- const changes = changesArray[index];
495
- if (!changes || Object.keys(changes).length === 0) {
537
+ const mutations = idsArray.map((id, index) => {
538
+ const itemChanges = changesArray[index];
539
+ if (!itemChanges || Object.keys(itemChanges).length === 0) {
496
540
  return null;
497
541
  }
498
- const validatedData = this.validateData(changes, `update`, key);
542
+ const originalItem = currentObjects[index];
543
+ const validatedUpdatePayload = this.validateData(
544
+ itemChanges,
545
+ `update`,
546
+ id
547
+ );
548
+ const modifiedItem = { ...originalItem, ...validatedUpdatePayload };
549
+ const originalItemId = this.config.getId(originalItem);
550
+ const modifiedItemId = this.config.getId(modifiedItem);
551
+ if (originalItemId !== modifiedItemId) {
552
+ 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.`
554
+ );
555
+ }
499
556
  return {
500
557
  mutationId: crypto.randomUUID(),
501
- original: this.state.get(key) || {},
502
- modified: {
503
- ...this.state.get(key) || {},
504
- ...validatedData
505
- },
506
- changes: validatedData,
507
- key,
558
+ original: originalItem,
559
+ modified: modifiedItem,
560
+ changes: validatedUpdatePayload,
561
+ key: id,
508
562
  metadata: config.metadata,
509
- syncMetadata: this.syncedMetadata.state.get(key) || {},
563
+ syncMetadata: this.syncedMetadata.state.get(id) || {},
510
564
  type: `update`,
511
565
  createdAt: /* @__PURE__ */ new Date(),
512
566
  updatedAt: /* @__PURE__ */ new Date(),
@@ -516,12 +570,26 @@ class Collection {
516
570
  if (mutations.length === 0) {
517
571
  throw new Error(`No changes were made to any of the objects`);
518
572
  }
519
- transaction.applyMutations(mutations);
573
+ if (ambientTransaction) {
574
+ ambientTransaction.applyMutations(mutations);
575
+ this.transactions.setState((sortedMap) => {
576
+ sortedMap.set(ambientTransaction.id, ambientTransaction);
577
+ return sortedMap;
578
+ });
579
+ return ambientTransaction;
580
+ }
581
+ const directOpTransaction = new Transaction({
582
+ mutationFn: async (transaction) => {
583
+ return this.config.onUpdate(transaction);
584
+ }
585
+ });
586
+ directOpTransaction.applyMutations(mutations);
587
+ directOpTransaction.commit();
520
588
  this.transactions.setState((sortedMap) => {
521
- sortedMap.set(transaction.id, transaction);
589
+ sortedMap.set(directOpTransaction.id, directOpTransaction);
522
590
  return sortedMap;
523
591
  });
524
- return transaction;
592
+ return directOpTransaction;
525
593
  }
526
594
  /**
527
595
  * Gets the current state of the collection as a Map
@@ -600,6 +668,7 @@ export {
600
668
  Collection,
601
669
  SchemaValidationError,
602
670
  collectionsStore,
671
+ createCollection,
603
672
  preloadCollection
604
673
  };
605
674
  //# sourceMappingURL=collection.js.map