@tanstack/db 0.0.26 → 0.0.29

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