@tanstack/db 0.0.27 → 0.0.30

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 (167) hide show
  1. package/dist/cjs/change-events.cjs +141 -0
  2. package/dist/cjs/change-events.cjs.map +1 -0
  3. package/dist/cjs/change-events.d.cts +49 -0
  4. package/dist/cjs/collection.cjs +234 -86
  5. package/dist/cjs/collection.cjs.map +1 -1
  6. package/dist/cjs/collection.d.cts +95 -20
  7. package/dist/cjs/errors.cjs +509 -1
  8. package/dist/cjs/errors.cjs.map +1 -1
  9. package/dist/cjs/errors.d.cts +225 -1
  10. package/dist/cjs/index.cjs +82 -3
  11. package/dist/cjs/index.cjs.map +1 -1
  12. package/dist/cjs/index.d.cts +5 -1
  13. package/dist/cjs/indexes/auto-index.cjs +64 -0
  14. package/dist/cjs/indexes/auto-index.cjs.map +1 -0
  15. package/dist/cjs/indexes/auto-index.d.cts +9 -0
  16. package/dist/cjs/indexes/base-index.cjs +46 -0
  17. package/dist/cjs/indexes/base-index.cjs.map +1 -0
  18. package/dist/cjs/indexes/base-index.d.cts +54 -0
  19. package/dist/cjs/indexes/btree-index.cjs +191 -0
  20. package/dist/cjs/indexes/btree-index.cjs.map +1 -0
  21. package/dist/cjs/indexes/btree-index.d.cts +74 -0
  22. package/dist/cjs/indexes/index-options.d.cts +13 -0
  23. package/dist/cjs/indexes/lazy-index.cjs +193 -0
  24. package/dist/cjs/indexes/lazy-index.cjs.map +1 -0
  25. package/dist/cjs/indexes/lazy-index.d.cts +96 -0
  26. package/dist/cjs/local-storage.cjs +9 -15
  27. package/dist/cjs/local-storage.cjs.map +1 -1
  28. package/dist/cjs/query/builder/functions.cjs +11 -0
  29. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  30. package/dist/cjs/query/builder/functions.d.cts +4 -0
  31. package/dist/cjs/query/builder/index.cjs +6 -7
  32. package/dist/cjs/query/builder/index.cjs.map +1 -1
  33. package/dist/cjs/query/builder/ref-proxy.cjs +37 -0
  34. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  35. package/dist/cjs/query/builder/ref-proxy.d.cts +12 -0
  36. package/dist/cjs/query/compiler/evaluators.cjs +83 -58
  37. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  38. package/dist/cjs/query/compiler/evaluators.d.cts +8 -0
  39. package/dist/cjs/query/compiler/expressions.cjs +61 -0
  40. package/dist/cjs/query/compiler/expressions.cjs.map +1 -0
  41. package/dist/cjs/query/compiler/expressions.d.cts +25 -0
  42. package/dist/cjs/query/compiler/group-by.cjs +5 -10
  43. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  44. package/dist/cjs/query/compiler/index.cjs +23 -17
  45. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  46. package/dist/cjs/query/compiler/index.d.cts +12 -3
  47. package/dist/cjs/query/compiler/joins.cjs +61 -12
  48. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  49. package/dist/cjs/query/compiler/order-by.cjs +4 -34
  50. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  51. package/dist/cjs/query/compiler/types.d.cts +2 -2
  52. package/dist/cjs/query/live-query-collection.cjs +54 -12
  53. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  54. package/dist/cjs/query/optimizer.cjs +45 -7
  55. package/dist/cjs/query/optimizer.cjs.map +1 -1
  56. package/dist/cjs/query/optimizer.d.cts +13 -3
  57. package/dist/cjs/transactions.cjs +5 -4
  58. package/dist/cjs/transactions.cjs.map +1 -1
  59. package/dist/cjs/types.d.cts +31 -0
  60. package/dist/cjs/utils/array-utils.d.cts +8 -0
  61. package/dist/cjs/utils/btree.cjs +677 -0
  62. package/dist/cjs/utils/btree.cjs.map +1 -0
  63. package/dist/cjs/utils/btree.d.cts +197 -0
  64. package/dist/cjs/utils/comparison.cjs +52 -0
  65. package/dist/cjs/utils/comparison.cjs.map +1 -0
  66. package/dist/cjs/utils/comparison.d.cts +11 -0
  67. package/dist/cjs/utils/index-optimization.cjs +270 -0
  68. package/dist/cjs/utils/index-optimization.cjs.map +1 -0
  69. package/dist/cjs/utils/index-optimization.d.cts +29 -0
  70. package/dist/esm/change-events.d.ts +49 -0
  71. package/dist/esm/change-events.js +141 -0
  72. package/dist/esm/change-events.js.map +1 -0
  73. package/dist/esm/collection.d.ts +95 -20
  74. package/dist/esm/collection.js +232 -84
  75. package/dist/esm/collection.js.map +1 -1
  76. package/dist/esm/errors.d.ts +225 -1
  77. package/dist/esm/errors.js +510 -2
  78. package/dist/esm/errors.js.map +1 -1
  79. package/dist/esm/index.d.ts +5 -1
  80. package/dist/esm/index.js +81 -2
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/indexes/auto-index.d.ts +9 -0
  83. package/dist/esm/indexes/auto-index.js +64 -0
  84. package/dist/esm/indexes/auto-index.js.map +1 -0
  85. package/dist/esm/indexes/base-index.d.ts +54 -0
  86. package/dist/esm/indexes/base-index.js +46 -0
  87. package/dist/esm/indexes/base-index.js.map +1 -0
  88. package/dist/esm/indexes/btree-index.d.ts +74 -0
  89. package/dist/esm/indexes/btree-index.js +191 -0
  90. package/dist/esm/indexes/btree-index.js.map +1 -0
  91. package/dist/esm/indexes/index-options.d.ts +13 -0
  92. package/dist/esm/indexes/lazy-index.d.ts +96 -0
  93. package/dist/esm/indexes/lazy-index.js +193 -0
  94. package/dist/esm/indexes/lazy-index.js.map +1 -0
  95. package/dist/esm/local-storage.js +9 -15
  96. package/dist/esm/local-storage.js.map +1 -1
  97. package/dist/esm/query/builder/functions.d.ts +4 -0
  98. package/dist/esm/query/builder/functions.js +11 -0
  99. package/dist/esm/query/builder/functions.js.map +1 -1
  100. package/dist/esm/query/builder/index.js +6 -7
  101. package/dist/esm/query/builder/index.js.map +1 -1
  102. package/dist/esm/query/builder/ref-proxy.d.ts +12 -0
  103. package/dist/esm/query/builder/ref-proxy.js +37 -0
  104. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  105. package/dist/esm/query/compiler/evaluators.d.ts +8 -0
  106. package/dist/esm/query/compiler/evaluators.js +84 -59
  107. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  108. package/dist/esm/query/compiler/expressions.d.ts +25 -0
  109. package/dist/esm/query/compiler/expressions.js +61 -0
  110. package/dist/esm/query/compiler/expressions.js.map +1 -0
  111. package/dist/esm/query/compiler/group-by.js +5 -10
  112. package/dist/esm/query/compiler/group-by.js.map +1 -1
  113. package/dist/esm/query/compiler/index.d.ts +12 -3
  114. package/dist/esm/query/compiler/index.js +23 -17
  115. package/dist/esm/query/compiler/index.js.map +1 -1
  116. package/dist/esm/query/compiler/joins.js +61 -12
  117. package/dist/esm/query/compiler/joins.js.map +1 -1
  118. package/dist/esm/query/compiler/order-by.js +1 -31
  119. package/dist/esm/query/compiler/order-by.js.map +1 -1
  120. package/dist/esm/query/compiler/types.d.ts +2 -2
  121. package/dist/esm/query/live-query-collection.js +54 -12
  122. package/dist/esm/query/live-query-collection.js.map +1 -1
  123. package/dist/esm/query/optimizer.d.ts +13 -3
  124. package/dist/esm/query/optimizer.js +40 -2
  125. package/dist/esm/query/optimizer.js.map +1 -1
  126. package/dist/esm/transactions.js +5 -4
  127. package/dist/esm/transactions.js.map +1 -1
  128. package/dist/esm/types.d.ts +31 -0
  129. package/dist/esm/utils/array-utils.d.ts +8 -0
  130. package/dist/esm/utils/btree.d.ts +197 -0
  131. package/dist/esm/utils/btree.js +677 -0
  132. package/dist/esm/utils/btree.js.map +1 -0
  133. package/dist/esm/utils/comparison.d.ts +11 -0
  134. package/dist/esm/utils/comparison.js +52 -0
  135. package/dist/esm/utils/comparison.js.map +1 -0
  136. package/dist/esm/utils/index-optimization.d.ts +29 -0
  137. package/dist/esm/utils/index-optimization.js +270 -0
  138. package/dist/esm/utils/index-optimization.js.map +1 -0
  139. package/package.json +1 -1
  140. package/src/change-events.ts +257 -0
  141. package/src/collection.ts +316 -105
  142. package/src/errors.ts +545 -1
  143. package/src/index.ts +7 -1
  144. package/src/indexes/auto-index.ts +108 -0
  145. package/src/indexes/base-index.ts +119 -0
  146. package/src/indexes/btree-index.ts +263 -0
  147. package/src/indexes/index-options.ts +42 -0
  148. package/src/indexes/lazy-index.ts +251 -0
  149. package/src/local-storage.ts +16 -17
  150. package/src/query/builder/functions.ts +14 -0
  151. package/src/query/builder/index.ts +12 -7
  152. package/src/query/builder/ref-proxy.ts +65 -0
  153. package/src/query/compiler/evaluators.ts +114 -62
  154. package/src/query/compiler/expressions.ts +92 -0
  155. package/src/query/compiler/group-by.ts +10 -10
  156. package/src/query/compiler/index.ts +52 -22
  157. package/src/query/compiler/joins.ts +114 -15
  158. package/src/query/compiler/order-by.ts +1 -45
  159. package/src/query/compiler/types.ts +2 -2
  160. package/src/query/live-query-collection.ts +95 -15
  161. package/src/query/optimizer.ts +94 -5
  162. package/src/transactions.ts +10 -4
  163. package/src/types.ts +38 -0
  164. package/src/utils/array-utils.ts +28 -0
  165. package/src/utils/btree.ts +1010 -0
  166. package/src/utils/comparison.ts +79 -0
  167. package/src/utils/index-optimization.ts +546 -0
@@ -2,7 +2,13 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const proxy = require("./proxy.cjs");
4
4
  const SortedMap = require("./SortedMap.cjs");
5
+ const refProxy = require("./query/builder/ref-proxy.cjs");
6
+ const btreeIndex = require("./indexes/btree-index.cjs");
7
+ const lazyIndex = require("./indexes/lazy-index.cjs");
8
+ const autoIndex = require("./indexes/auto-index.cjs");
5
9
  const transactions = require("./transactions.cjs");
10
+ const errors = require("./errors.cjs");
11
+ const changeEvents = require("./change-events.cjs");
6
12
  const collectionsStore = /* @__PURE__ */ new Map();
7
13
  function createCollection(options) {
8
14
  const collection = new CollectionImpl(options);
@@ -13,16 +19,6 @@ function createCollection(options) {
13
19
  }
14
20
  return collection;
15
21
  }
16
- class SchemaValidationError extends Error {
17
- constructor(type, issues, message) {
18
- const defaultMessage = `${type === `insert` ? `Insert` : `Update`} validation failed: ${issues.map((issue) => `
19
- - ${issue.message} - path: ${issue.path}`).join(``)}`;
20
- super(message || defaultMessage);
21
- this.name = `SchemaValidationError`;
22
- this.type = type;
23
- this.issues = issues;
24
- }
25
- }
26
22
  class CollectionImpl {
27
23
  /**
28
24
  * Creates a new Collection instance
@@ -36,6 +32,10 @@ class CollectionImpl {
36
32
  this.optimisticUpserts = /* @__PURE__ */ new Map();
37
33
  this.optimisticDeletes = /* @__PURE__ */ new Set();
38
34
  this._size = 0;
35
+ this.lazyIndexes = /* @__PURE__ */ new Map();
36
+ this.resolvedIndexes = /* @__PURE__ */ new Map();
37
+ this.isIndexesResolved = false;
38
+ this.indexCounter = 0;
39
39
  this.changeListeners = /* @__PURE__ */ new Set();
40
40
  this.changeKeyListeners = /* @__PURE__ */ new Map();
41
41
  this.utils = {};
@@ -194,6 +194,9 @@ class CollectionImpl {
194
194
  }
195
195
  }
196
196
  this._size = this.calculateSize();
197
+ if (events.length > 0) {
198
+ this.updateIndexes(events);
199
+ }
197
200
  this.emitEvents(events, true);
198
201
  this.pendingSyncedTransactions = [];
199
202
  this.preSyncVisibleState.clear();
@@ -212,9 +215,7 @@ class CollectionImpl {
212
215
  this.validateCollectionUsable(`insert`);
213
216
  const ambientTransaction = transactions.getActiveTransaction();
214
217
  if (!ambientTransaction && !this.config.onInsert) {
215
- throw new Error(
216
- `Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.`
217
- );
218
+ throw new errors.MissingInsertHandlerError();
218
219
  }
219
220
  const items = Array.isArray(data) ? data : [data];
220
221
  const mutations = [];
@@ -223,7 +224,7 @@ class CollectionImpl {
223
224
  const validatedData = this.validateData(item, `insert`);
224
225
  const key = this.getKeyFromItem(validatedData);
225
226
  if (this.has(key)) {
226
- throw `Cannot insert document with ID "${key}" because it already exists in the collection`;
227
+ throw new errors.DuplicateKeyError(key);
227
228
  }
228
229
  const globalKey = this.generateGlobalKey(key, item);
229
230
  const mutation = {
@@ -276,20 +277,16 @@ class CollectionImpl {
276
277
  this.validateCollectionUsable(`delete`);
277
278
  const ambientTransaction = transactions.getActiveTransaction();
278
279
  if (!ambientTransaction && !this.config.onDelete) {
279
- throw new Error(
280
- `Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
281
- );
280
+ throw new errors.MissingDeleteHandlerError();
282
281
  }
283
282
  if (Array.isArray(keys) && keys.length === 0) {
284
- throw new Error(`No keys were passed to delete`);
283
+ throw new errors.NoKeysPassedToDeleteError();
285
284
  }
286
285
  const keysArray = Array.isArray(keys) ? keys : [keys];
287
286
  const mutations = [];
288
287
  for (const key of keysArray) {
289
288
  if (!this.has(key)) {
290
- throw new Error(
291
- `Collection.delete was called with key '${key}' but there is no item in the collection with this key`
292
- );
289
+ throw new errors.DeleteKeyNotFoundError(key);
293
290
  }
294
291
  const globalKey = this.generateGlobalKey(key, this.get(key));
295
292
  const mutation = {
@@ -331,7 +328,7 @@ class CollectionImpl {
331
328
  return directOpTransaction;
332
329
  };
333
330
  if (!config) {
334
- throw new Error(`Collection requires a config`);
331
+ throw new errors.CollectionRequiresConfigError();
335
332
  }
336
333
  if (config.id) {
337
334
  this.id = config.id;
@@ -339,12 +336,15 @@ class CollectionImpl {
339
336
  this.id = crypto.randomUUID();
340
337
  }
341
338
  if (!config.sync) {
342
- throw new Error(`Collection requires a sync config`);
339
+ throw new errors.CollectionRequiresSyncConfigError();
343
340
  }
344
341
  this.transactions = new SortedMap.SortedMap(
345
342
  (a, b) => a.compareCreatedAt(b)
346
343
  );
347
- this.config = config;
344
+ this.config = {
345
+ ...config,
346
+ autoIndex: config.autoIndex ?? `eager`
347
+ };
348
348
  collectionsStore.set(this.id, this);
349
349
  if (this.config.compare) {
350
350
  this.syncedData = new SortedMap.SortedMap(this.config.compare);
@@ -420,13 +420,10 @@ class CollectionImpl {
420
420
  validateCollectionUsable(operation) {
421
421
  switch (this._status) {
422
422
  case `error`:
423
- throw new Error(
424
- `Cannot perform ${operation} on collection "${this.id}" - collection is in error state. Try calling cleanup() and restarting the collection.`
425
- );
423
+ throw new errors.CollectionInErrorStateError(operation, this.id);
426
424
  case `cleaned-up`:
427
- throw new Error(
428
- `Cannot perform ${operation} on collection "${this.id}" - collection has been cleaned up. The collection will automatically restart on next access.`
429
- );
425
+ this.startSync();
426
+ break;
430
427
  }
431
428
  }
432
429
  /**
@@ -446,9 +443,7 @@ class CollectionImpl {
446
443
  "cleaned-up": [`loading`, `error`]
447
444
  };
448
445
  if (!validTransitions[from].includes(to)) {
449
- throw new Error(
450
- `Invalid collection status transition from "${from}" to "${to}" for collection "${this.id}"`
451
- );
446
+ throw new errors.InvalidCollectionStatusTransitionError(from, to, this.id);
452
447
  }
453
448
  }
454
449
  /**
@@ -458,6 +453,11 @@ class CollectionImpl {
458
453
  setStatus(newStatus) {
459
454
  this.validateStatusTransition(this._status, newStatus);
460
455
  this._status = newStatus;
456
+ if (newStatus === `ready` && !this.isIndexesResolved) {
457
+ this.resolveAllIndexes().catch((error) => {
458
+ console.warn(`Failed to resolve indexes:`, error);
459
+ });
460
+ }
461
461
  }
462
462
  /**
463
463
  * Start sync immediately - internal method for compiled queries
@@ -487,21 +487,17 @@ class CollectionImpl {
487
487
  write: (messageWithoutKey) => {
488
488
  const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
489
489
  if (!pendingTransaction) {
490
- throw new Error(`No pending sync transaction to write to`);
490
+ throw new errors.NoPendingSyncTransactionWriteError();
491
491
  }
492
492
  if (pendingTransaction.committed) {
493
- throw new Error(
494
- `The pending sync transaction is already committed, you can't still write to it.`
495
- );
493
+ throw new errors.SyncTransactionAlreadyCommittedWriteError();
496
494
  }
497
495
  const key = this.getKeyFromItem(messageWithoutKey.value);
498
496
  if (messageWithoutKey.type === `insert`) {
499
497
  if (this.syncedData.has(key) && !pendingTransaction.operations.some(
500
498
  (op) => op.key === key && op.type === `delete`
501
499
  )) {
502
- throw new Error(
503
- `Cannot insert document with key "${key}" from sync because it already exists in the collection "${this.id}"`
504
- );
500
+ throw new errors.DuplicateKeySyncError(key, this.id);
505
501
  }
506
502
  }
507
503
  const message = {
@@ -513,12 +509,10 @@ class CollectionImpl {
513
509
  commit: () => {
514
510
  const pendingTransaction = this.pendingSyncedTransactions[this.pendingSyncedTransactions.length - 1];
515
511
  if (!pendingTransaction) {
516
- throw new Error(`No pending sync transaction to commit`);
512
+ throw new errors.NoPendingSyncTransactionCommitError();
517
513
  }
518
514
  if (pendingTransaction.committed) {
519
- throw new Error(
520
- `The pending sync transaction is already committed, you can't commit it again.`
521
- );
515
+ throw new errors.SyncTransactionAlreadyCommittedError();
522
516
  }
523
517
  pendingTransaction.committed = true;
524
518
  if (this._status === `loading`) {
@@ -550,7 +544,7 @@ class CollectionImpl {
550
544
  return;
551
545
  }
552
546
  if (this._status === `error`) {
553
- reject(new Error(`Collection is in error state`));
547
+ reject(new errors.CollectionIsInErrorStateError());
554
548
  return;
555
549
  }
556
550
  this.onFirstReady(() => {
@@ -584,16 +578,12 @@ class CollectionImpl {
584
578
  } catch (error) {
585
579
  queueMicrotask(() => {
586
580
  if (error instanceof Error) {
587
- const wrappedError = new Error(
588
- `Collection "${this.id}" sync cleanup function threw an error: ${error.message}`
589
- );
581
+ const wrappedError = new errors.SyncCleanupError(this.id, error);
590
582
  wrappedError.cause = error;
591
583
  wrappedError.stack = error.stack;
592
584
  throw wrappedError;
593
585
  } else {
594
- throw new Error(
595
- `Collection "${this.id}" sync cleanup function threw an error: ${String(error)}`
596
- );
586
+ throw new errors.SyncCleanupError(this.id, error);
597
587
  }
598
588
  });
599
589
  }
@@ -657,9 +647,7 @@ class CollectionImpl {
657
647
  this.activeSubscribersCount = 0;
658
648
  this.startGCTimer();
659
649
  } else if (this.activeSubscribersCount < 0) {
660
- throw new Error(
661
- `Active subscribers count is negative - this should never happen`
662
- );
650
+ throw new errors.NegativeActiveSubscribersError();
663
651
  }
664
652
  }
665
653
  /**
@@ -733,8 +721,14 @@ class CollectionImpl {
733
721
  }
734
722
  return true;
735
723
  });
724
+ if (filteredEvents.length > 0) {
725
+ this.updateIndexes(filteredEvents);
726
+ }
736
727
  this.emitEvents(filteredEvents);
737
728
  } else {
729
+ if (filteredEventsBySyncStatus.length > 0) {
730
+ this.updateIndexes(filteredEventsBySyncStatus);
731
+ }
738
732
  this.emitEvents(filteredEventsBySyncStatus);
739
733
  }
740
734
  }
@@ -931,19 +925,144 @@ class CollectionImpl {
931
925
  if (schema && `~standard` in schema) {
932
926
  return schema;
933
927
  }
934
- throw new Error(`Schema must implement the standard-schema interface`);
928
+ throw new errors.InvalidSchemaError();
935
929
  }
936
930
  getKeyFromItem(item) {
937
931
  return this.config.getKey(item);
938
932
  }
939
933
  generateGlobalKey(key, item) {
940
934
  if (typeof key === `undefined`) {
941
- throw new Error(
942
- `An object was created without a defined key: ${JSON.stringify(item)}`
943
- );
935
+ throw new errors.UndefinedKeyError(item);
944
936
  }
945
937
  return `KEY::${this.id}/${key}`;
946
938
  }
939
+ /**
940
+ * Creates an index on a collection for faster queries.
941
+ * Indexes significantly improve query performance by allowing binary search
942
+ * and range queries instead of full scans.
943
+ *
944
+ * @template TResolver - The type of the index resolver (constructor or async loader)
945
+ * @param indexCallback - Function that extracts the indexed value from each item
946
+ * @param config - Configuration including index type and type-specific options
947
+ * @returns An index proxy that provides access to the index when ready
948
+ *
949
+ * @example
950
+ * // Create a default B+ tree index
951
+ * const ageIndex = collection.createIndex((row) => row.age)
952
+ *
953
+ * // Create a ordered index with custom options
954
+ * const ageIndex = collection.createIndex((row) => row.age, {
955
+ * indexType: BTreeIndex,
956
+ * options: { compareFn: customComparator },
957
+ * name: 'age_btree'
958
+ * })
959
+ *
960
+ * // Create an async-loaded index
961
+ * const textIndex = collection.createIndex((row) => row.content, {
962
+ * indexType: async () => {
963
+ * const { FullTextIndex } = await import('./indexes/fulltext.js')
964
+ * return FullTextIndex
965
+ * },
966
+ * options: { language: 'en' }
967
+ * })
968
+ */
969
+ createIndex(indexCallback, config = {}) {
970
+ this.validateCollectionUsable(`createIndex`);
971
+ const indexId = ++this.indexCounter;
972
+ const singleRowRefProxy = refProxy.createSingleRowRefProxy();
973
+ const indexExpression = indexCallback(singleRowRefProxy);
974
+ const expression = refProxy.toExpression(indexExpression);
975
+ const resolver = config.indexType ?? btreeIndex.BTreeIndex;
976
+ const lazyIndex$1 = new lazyIndex.LazyIndexWrapper(
977
+ indexId,
978
+ expression,
979
+ config.name,
980
+ resolver,
981
+ config.options,
982
+ this.entries()
983
+ );
984
+ this.lazyIndexes.set(indexId, lazyIndex$1);
985
+ if (resolver === btreeIndex.BTreeIndex) {
986
+ try {
987
+ const resolvedIndex = lazyIndex$1.getResolved();
988
+ this.resolvedIndexes.set(indexId, resolvedIndex);
989
+ } catch (error) {
990
+ console.warn(`Failed to resolve BTreeIndex:`, error);
991
+ }
992
+ } else if (typeof resolver === `function` && resolver.prototype) {
993
+ try {
994
+ const resolvedIndex = lazyIndex$1.getResolved();
995
+ this.resolvedIndexes.set(indexId, resolvedIndex);
996
+ } catch {
997
+ this.resolveSingleIndex(indexId, lazyIndex$1).catch((error) => {
998
+ console.warn(`Failed to resolve single index:`, error);
999
+ });
1000
+ }
1001
+ } else if (this.isIndexesResolved) {
1002
+ this.resolveSingleIndex(indexId, lazyIndex$1).catch((error) => {
1003
+ console.warn(`Failed to resolve single index:`, error);
1004
+ });
1005
+ }
1006
+ return new lazyIndex.IndexProxy(indexId, lazyIndex$1);
1007
+ }
1008
+ /**
1009
+ * Resolve all lazy indexes (called when collection first syncs)
1010
+ * @private
1011
+ */
1012
+ async resolveAllIndexes() {
1013
+ if (this.isIndexesResolved) return;
1014
+ const resolutionPromises = Array.from(this.lazyIndexes.entries()).map(
1015
+ async ([indexId, lazyIndex2]) => {
1016
+ const resolvedIndex = await lazyIndex2.resolve();
1017
+ resolvedIndex.build(this.entries());
1018
+ this.resolvedIndexes.set(indexId, resolvedIndex);
1019
+ return { indexId, resolvedIndex };
1020
+ }
1021
+ );
1022
+ await Promise.all(resolutionPromises);
1023
+ this.isIndexesResolved = true;
1024
+ }
1025
+ /**
1026
+ * Resolve a single index immediately
1027
+ * @private
1028
+ */
1029
+ async resolveSingleIndex(indexId, lazyIndex2) {
1030
+ const resolvedIndex = await lazyIndex2.resolve();
1031
+ resolvedIndex.build(this.entries());
1032
+ this.resolvedIndexes.set(indexId, resolvedIndex);
1033
+ return resolvedIndex;
1034
+ }
1035
+ /**
1036
+ * Get resolved indexes for query optimization
1037
+ */
1038
+ get indexes() {
1039
+ return this.resolvedIndexes;
1040
+ }
1041
+ /**
1042
+ * Updates all indexes when the collection changes
1043
+ * @private
1044
+ */
1045
+ updateIndexes(changes) {
1046
+ for (const index of this.resolvedIndexes.values()) {
1047
+ for (const change of changes) {
1048
+ switch (change.type) {
1049
+ case `insert`:
1050
+ index.add(change.key, change.value);
1051
+ break;
1052
+ case `update`:
1053
+ if (change.previousValue) {
1054
+ index.update(change.key, change.previousValue, change.value);
1055
+ } else {
1056
+ index.add(change.key, change.value);
1057
+ }
1058
+ break;
1059
+ case `delete`:
1060
+ index.remove(change.key, change.value);
1061
+ break;
1062
+ }
1063
+ }
1064
+ }
1065
+ }
947
1066
  deepEqual(a, b) {
948
1067
  if (a === b) return true;
949
1068
  if (a == null || b == null) return false;
@@ -971,7 +1090,7 @@ class CollectionImpl {
971
1090
  const mergedData = Object.assign({}, existingData, data);
972
1091
  const result2 = standardSchema[`~standard`].validate(mergedData);
973
1092
  if (result2 instanceof Promise) {
974
- throw new TypeError(`Schema validation must be synchronous`);
1093
+ throw new errors.SchemaMustBeSynchronousError();
975
1094
  }
976
1095
  if (`issues` in result2 && result2.issues) {
977
1096
  const typedIssues = result2.issues.map((issue) => {
@@ -981,14 +1100,14 @@ class CollectionImpl {
981
1100
  path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
982
1101
  };
983
1102
  });
984
- throw new SchemaValidationError(type, typedIssues);
1103
+ throw new errors.SchemaValidationError(type, typedIssues);
985
1104
  }
986
1105
  return data;
987
1106
  }
988
1107
  }
989
1108
  const result = standardSchema[`~standard`].validate(data);
990
1109
  if (result instanceof Promise) {
991
- throw new TypeError(`Schema validation must be synchronous`);
1110
+ throw new errors.SchemaMustBeSynchronousError();
992
1111
  }
993
1112
  if (`issues` in result && result.issues) {
994
1113
  const typedIssues = result.issues.map((issue) => {
@@ -998,34 +1117,30 @@ class CollectionImpl {
998
1117
  path: (_a = issue.path) == null ? void 0 : _a.map((p) => String(p))
999
1118
  };
1000
1119
  });
1001
- throw new SchemaValidationError(type, typedIssues);
1120
+ throw new errors.SchemaValidationError(type, typedIssues);
1002
1121
  }
1003
1122
  return result.value;
1004
1123
  }
1005
1124
  update(keys, configOrCallback, maybeCallback) {
1006
1125
  if (typeof keys === `undefined`) {
1007
- throw new Error(`The first argument to update is missing`);
1126
+ throw new errors.MissingUpdateArgumentError();
1008
1127
  }
1009
1128
  this.validateCollectionUsable(`update`);
1010
1129
  const ambientTransaction = transactions.getActiveTransaction();
1011
1130
  if (!ambientTransaction && !this.config.onUpdate) {
1012
- throw new Error(
1013
- `Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
1014
- );
1131
+ throw new errors.MissingUpdateHandlerError();
1015
1132
  }
1016
1133
  const isArray = Array.isArray(keys);
1017
1134
  const keysArray = isArray ? keys : [keys];
1018
1135
  if (isArray && keysArray.length === 0) {
1019
- throw new Error(`No keys were passed to update`);
1136
+ throw new errors.NoKeysPassedToUpdateError();
1020
1137
  }
1021
1138
  const callback = typeof configOrCallback === `function` ? configOrCallback : maybeCallback;
1022
1139
  const config = typeof configOrCallback === `function` ? {} : configOrCallback;
1023
1140
  const currentObjects = keysArray.map((key) => {
1024
1141
  const item = this.get(key);
1025
1142
  if (!item) {
1026
- throw new Error(
1027
- `The key "${key}" was passed to update but an object for this key was not found in the collection`
1028
- );
1143
+ throw new errors.UpdateKeyNotFoundError(key);
1029
1144
  }
1030
1145
  return item;
1031
1146
  });
@@ -1061,9 +1176,7 @@ class CollectionImpl {
1061
1176
  const originalItemId = this.getKeyFromItem(originalItem);
1062
1177
  const modifiedItemId = this.getKeyFromItem(modifiedItem);
1063
1178
  if (originalItemId !== modifiedItemId) {
1064
- throw new Error(
1065
- `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.`
1066
- );
1179
+ throw new errors.KeyUpdateNotAllowedError(originalItemId, modifiedItemId);
1067
1180
  }
1068
1181
  const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem);
1069
1182
  return {
@@ -1175,19 +1288,29 @@ class CollectionImpl {
1175
1288
  }
1176
1289
  /**
1177
1290
  * Returns the current state of the collection as an array of changes
1291
+ * @param options - Options including optional where filter
1178
1292
  * @returns An array of changes
1293
+ * @example
1294
+ * // Get all items as changes
1295
+ * const allChanges = collection.currentStateAsChanges()
1296
+ *
1297
+ * // Get only items matching a condition
1298
+ * const activeChanges = collection.currentStateAsChanges({
1299
+ * where: (row) => row.status === 'active'
1300
+ * })
1301
+ *
1302
+ * // Get only items using a pre-compiled expression
1303
+ * const activeChanges = collection.currentStateAsChanges({
1304
+ * whereExpression: eq(row.status, 'active')
1305
+ * })
1179
1306
  */
1180
- currentStateAsChanges() {
1181
- return Array.from(this.entries()).map(([key, value]) => ({
1182
- type: `insert`,
1183
- key,
1184
- value
1185
- }));
1307
+ currentStateAsChanges(options = {}) {
1308
+ return changeEvents.currentStateAsChanges(this, options);
1186
1309
  }
1187
1310
  /**
1188
1311
  * Subscribe to changes in the collection
1189
1312
  * @param callback - Function called when items change
1190
- * @param options.includeInitialState - If true, immediately calls callback with current data
1313
+ * @param options - Subscription options including includeInitialState and where filter
1191
1314
  * @returns Unsubscribe function - Call this to stop listening for changes
1192
1315
  * @example
1193
1316
  * // Basic subscription
@@ -1204,15 +1327,41 @@ class CollectionImpl {
1204
1327
  * const unsubscribe = collection.subscribeChanges((changes) => {
1205
1328
  * updateUI(changes)
1206
1329
  * }, { includeInitialState: true })
1330
+ *
1331
+ * @example
1332
+ * // Subscribe only to changes matching a condition
1333
+ * const unsubscribe = collection.subscribeChanges((changes) => {
1334
+ * updateUI(changes)
1335
+ * }, {
1336
+ * includeInitialState: true,
1337
+ * where: (row) => row.status === 'active'
1338
+ * })
1339
+ *
1340
+ * @example
1341
+ * // Subscribe using a pre-compiled expression
1342
+ * const unsubscribe = collection.subscribeChanges((changes) => {
1343
+ * updateUI(changes)
1344
+ * }, {
1345
+ * includeInitialState: true,
1346
+ * whereExpression: eq(row.status, 'active')
1347
+ * })
1207
1348
  */
1208
- subscribeChanges(callback, { includeInitialState = false } = {}) {
1349
+ subscribeChanges(callback, options = {}) {
1209
1350
  this.addSubscriber();
1210
- if (includeInitialState) {
1211
- callback(this.currentStateAsChanges());
1351
+ if (options.whereExpression) {
1352
+ autoIndex.ensureIndexForExpression(options.whereExpression, this);
1353
+ }
1354
+ const filteredCallback = options.where || options.whereExpression ? changeEvents.createFilteredCallback(callback, options) : callback;
1355
+ if (options.includeInitialState) {
1356
+ const initialChanges = this.currentStateAsChanges({
1357
+ where: options.where,
1358
+ whereExpression: options.whereExpression
1359
+ });
1360
+ filteredCallback(initialChanges);
1212
1361
  }
1213
- this.changeListeners.add(callback);
1362
+ this.changeListeners.add(filteredCallback);
1214
1363
  return () => {
1215
- this.changeListeners.delete(callback);
1364
+ this.changeListeners.delete(filteredCallback);
1216
1365
  this.removeSubscriber();
1217
1366
  };
1218
1367
  }
@@ -1279,7 +1428,6 @@ class CollectionImpl {
1279
1428
  }
1280
1429
  }
1281
1430
  exports.CollectionImpl = CollectionImpl;
1282
- exports.SchemaValidationError = SchemaValidationError;
1283
1431
  exports.collectionsStore = collectionsStore;
1284
1432
  exports.createCollection = createCollection;
1285
1433
  //# sourceMappingURL=collection.cjs.map