@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
package/src/collection.ts CHANGED
@@ -1,12 +1,51 @@
1
1
  import { withArrayChangeTracking, withChangeTracking } from "./proxy"
2
- import { createTransaction, getActiveTransaction } from "./transactions"
3
2
  import { SortedMap } from "./SortedMap"
3
+ import {
4
+ createSingleRowRefProxy,
5
+ toExpression,
6
+ } from "./query/builder/ref-proxy"
7
+ import { OrderedIndex } from "./indexes/ordered-index.js"
8
+ import { IndexProxy, LazyIndexWrapper } from "./indexes/lazy-index.js"
9
+ import { ensureIndexForExpression } from "./indexes/auto-index.js"
10
+ import { createTransaction, getActiveTransaction } from "./transactions"
11
+ import {
12
+ CollectionInErrorStateError,
13
+ CollectionIsInErrorStateError,
14
+ CollectionRequiresConfigError,
15
+ CollectionRequiresSyncConfigError,
16
+ DeleteKeyNotFoundError,
17
+ DuplicateKeyError,
18
+ DuplicateKeySyncError,
19
+ InvalidCollectionStatusTransitionError,
20
+ InvalidSchemaError,
21
+ KeyUpdateNotAllowedError,
22
+ MissingDeleteHandlerError,
23
+ MissingInsertHandlerError,
24
+ MissingUpdateArgumentError,
25
+ MissingUpdateHandlerError,
26
+ NegativeActiveSubscribersError,
27
+ NoKeysPassedToDeleteError,
28
+ NoKeysPassedToUpdateError,
29
+ NoPendingSyncTransactionCommitError,
30
+ NoPendingSyncTransactionWriteError,
31
+ SchemaMustBeSynchronousError,
32
+ SchemaValidationError,
33
+ SyncCleanupError,
34
+ SyncTransactionAlreadyCommittedError,
35
+ SyncTransactionAlreadyCommittedWriteError,
36
+ UndefinedKeyError,
37
+ UpdateKeyNotFoundError,
38
+ } from "./errors"
39
+ import { createFilteredCallback, currentStateAsChanges } from "./change-events"
4
40
  import type { Transaction } from "./transactions"
41
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
42
+ import type { SingleRowRefProxy } from "./query/builder/ref-proxy"
5
43
  import type {
6
44
  ChangeListener,
7
45
  ChangeMessage,
8
46
  CollectionConfig,
9
47
  CollectionStatus,
48
+ CurrentStateAsChangesOptions,
10
49
  Fn,
11
50
  InsertConfig,
12
51
  OperationConfig,
@@ -15,11 +54,13 @@ import type {
15
54
  ResolveInsertInput,
16
55
  ResolveType,
17
56
  StandardSchema,
57
+ SubscribeChangesOptions,
18
58
  Transaction as TransactionType,
19
59
  TransactionWithMutations,
20
60
  UtilsRecord,
21
61
  } from "./types"
22
- import type { StandardSchemaV1 } from "@standard-schema/spec"
62
+ import type { IndexOptions } from "./indexes/index-options.js"
63
+ import type { BaseIndex, IndexResolver } from "./indexes/base-index.js"
23
64
 
24
65
  // Store collections in memory
25
66
  export const collectionsStore = new Map<string, CollectionImpl<any, any, any>>()
@@ -163,35 +204,6 @@ export function createCollection<
163
204
  >
164
205
  }
165
206
 
166
- /**
167
- * Custom error class for schema validation errors
168
- */
169
- export class SchemaValidationError extends Error {
170
- type: `insert` | `update`
171
- issues: ReadonlyArray<{
172
- message: string
173
- path?: ReadonlyArray<string | number | symbol>
174
- }>
175
-
176
- constructor(
177
- type: `insert` | `update`,
178
- issues: ReadonlyArray<{
179
- message: string
180
- path?: ReadonlyArray<string | number | symbol>
181
- }>,
182
- message?: string
183
- ) {
184
- const defaultMessage = `${type === `insert` ? `Insert` : `Update`} validation failed: ${issues
185
- .map((issue) => `\n- ${issue.message} - path: ${issue.path}`)
186
- .join(``)}`
187
-
188
- super(message || defaultMessage)
189
- this.name = `SchemaValidationError`
190
- this.type = type
191
- this.issues = issues
192
- }
193
- }
194
-
195
207
  export class CollectionImpl<
196
208
  T extends object = Record<string, unknown>,
197
209
  TKey extends string | number = string | number,
@@ -214,6 +226,12 @@ export class CollectionImpl<
214
226
  // Cached size for performance
215
227
  private _size = 0
216
228
 
229
+ // Index storage
230
+ private lazyIndexes = new Map<number, LazyIndexWrapper<TKey>>()
231
+ private resolvedIndexes = new Map<number, BaseIndex<TKey>>()
232
+ private isIndexesResolved = false
233
+ private indexCounter = 0
234
+
217
235
  // Event system
218
236
  private changeListeners = new Set<ChangeListener<T, TKey>>()
219
237
  private changeKeyListeners = new Map<TKey, Set<ChangeListener<T, TKey>>>()
@@ -323,15 +341,11 @@ export class CollectionImpl<
323
341
  private validateCollectionUsable(operation: string): void {
324
342
  switch (this._status) {
325
343
  case `error`:
326
- throw new Error(
327
- `Cannot perform ${operation} on collection "${this.id}" - collection is in error state. ` +
328
- `Try calling cleanup() and restarting the collection.`
329
- )
344
+ throw new CollectionInErrorStateError(operation, this.id)
330
345
  case `cleaned-up`:
331
- throw new Error(
332
- `Cannot perform ${operation} on collection "${this.id}" - collection has been cleaned up. ` +
333
- `The collection will automatically restart on next access.`
334
- )
346
+ // Automatically restart the collection when operations are called on cleaned-up collections
347
+ this.startSync()
348
+ break
335
349
  }
336
350
  }
337
351
 
@@ -360,9 +374,7 @@ export class CollectionImpl<
360
374
  }
361
375
 
362
376
  if (!validTransitions[from].includes(to)) {
363
- throw new Error(
364
- `Invalid collection status transition from "${from}" to "${to}" for collection "${this.id}"`
365
- )
377
+ throw new InvalidCollectionStatusTransitionError(from, to, this.id)
366
378
  }
367
379
  }
368
380
 
@@ -373,6 +385,14 @@ export class CollectionImpl<
373
385
  private setStatus(newStatus: CollectionStatus): void {
374
386
  this.validateStatusTransition(this._status, newStatus)
375
387
  this._status = newStatus
388
+
389
+ // Resolve indexes when collection becomes ready
390
+ if (newStatus === `ready` && !this.isIndexesResolved) {
391
+ // Resolve indexes asynchronously without blocking
392
+ this.resolveAllIndexes().catch((error) => {
393
+ console.warn(`Failed to resolve indexes:`, error)
394
+ })
395
+ }
376
396
  }
377
397
 
378
398
  /**
@@ -384,7 +404,7 @@ export class CollectionImpl<
384
404
  constructor(config: CollectionConfig<T, TKey, TSchema, TInsertInput>) {
385
405
  // eslint-disable-next-line
386
406
  if (!config) {
387
- throw new Error(`Collection requires a config`)
407
+ throw new CollectionRequiresConfigError()
388
408
  }
389
409
  if (config.id) {
390
410
  this.id = config.id
@@ -394,14 +414,18 @@ export class CollectionImpl<
394
414
 
395
415
  // eslint-disable-next-line
396
416
  if (!config.sync) {
397
- throw new Error(`Collection requires a sync config`)
417
+ throw new CollectionRequiresSyncConfigError()
398
418
  }
399
419
 
400
420
  this.transactions = new SortedMap<string, Transaction<any>>((a, b) =>
401
421
  a.compareCreatedAt(b)
402
422
  )
403
423
 
404
- this.config = config
424
+ // Set default values for optional config properties
425
+ this.config = {
426
+ ...config,
427
+ autoIndex: config.autoIndex ?? `eager`,
428
+ }
405
429
 
406
430
  // Store in global collections store
407
431
  collectionsStore.set(this.id, this)
@@ -453,12 +477,10 @@ export class CollectionImpl<
453
477
  this.pendingSyncedTransactions.length - 1
454
478
  ]
455
479
  if (!pendingTransaction) {
456
- throw new Error(`No pending sync transaction to write to`)
480
+ throw new NoPendingSyncTransactionWriteError()
457
481
  }
458
482
  if (pendingTransaction.committed) {
459
- throw new Error(
460
- `The pending sync transaction is already committed, you can't still write to it.`
461
- )
483
+ throw new SyncTransactionAlreadyCommittedWriteError()
462
484
  }
463
485
  const key = this.getKeyFromItem(messageWithoutKey.value)
464
486
 
@@ -470,9 +492,7 @@ export class CollectionImpl<
470
492
  (op) => op.key === key && op.type === `delete`
471
493
  )
472
494
  ) {
473
- throw new Error(
474
- `Cannot insert document with key "${key}" from sync because it already exists in the collection "${this.id}"`
475
- )
495
+ throw new DuplicateKeySyncError(key, this.id)
476
496
  }
477
497
  }
478
498
 
@@ -488,12 +508,10 @@ export class CollectionImpl<
488
508
  this.pendingSyncedTransactions.length - 1
489
509
  ]
490
510
  if (!pendingTransaction) {
491
- throw new Error(`No pending sync transaction to commit`)
511
+ throw new NoPendingSyncTransactionCommitError()
492
512
  }
493
513
  if (pendingTransaction.committed) {
494
- throw new Error(
495
- `The pending sync transaction is already committed, you can't commit it again.`
496
- )
514
+ throw new SyncTransactionAlreadyCommittedError()
497
515
  }
498
516
 
499
517
  pendingTransaction.committed = true
@@ -535,7 +553,7 @@ export class CollectionImpl<
535
553
  }
536
554
 
537
555
  if (this._status === `error`) {
538
- reject(new Error(`Collection is in error state`))
556
+ reject(new CollectionIsInErrorStateError())
539
557
  return
540
558
  }
541
559
 
@@ -580,16 +598,12 @@ export class CollectionImpl<
580
598
  queueMicrotask(() => {
581
599
  if (error instanceof Error) {
582
600
  // Preserve the original error and stack trace
583
- const wrappedError = new Error(
584
- `Collection "${this.id}" sync cleanup function threw an error: ${error.message}`
585
- )
601
+ const wrappedError = new SyncCleanupError(this.id, error)
586
602
  wrappedError.cause = error
587
603
  wrappedError.stack = error.stack
588
604
  throw wrappedError
589
605
  } else {
590
- throw new Error(
591
- `Collection "${this.id}" sync cleanup function threw an error: ${String(error)}`
592
- )
606
+ throw new SyncCleanupError(this.id, error as Error | string)
593
607
  }
594
608
  })
595
609
  }
@@ -666,9 +680,7 @@ export class CollectionImpl<
666
680
  this.activeSubscribersCount = 0
667
681
  this.startGCTimer()
668
682
  } else if (this.activeSubscribersCount < 0) {
669
- throw new Error(
670
- `Active subscribers count is negative - this should never happen`
671
- )
683
+ throw new NegativeActiveSubscribersError()
672
684
  }
673
685
  }
674
686
 
@@ -772,8 +784,16 @@ export class CollectionImpl<
772
784
  return true
773
785
  })
774
786
 
787
+ // Update indexes for the filtered events
788
+ if (filteredEvents.length > 0) {
789
+ this.updateIndexes(filteredEvents)
790
+ }
775
791
  this.emitEvents(filteredEvents)
776
792
  } else {
793
+ // Update indexes for all events
794
+ if (filteredEventsBySyncStatus.length > 0) {
795
+ this.updateIndexes(filteredEventsBySyncStatus)
796
+ }
777
797
  // Emit all events if no pending sync transactions
778
798
  this.emitEvents(filteredEventsBySyncStatus)
779
799
  }
@@ -1217,6 +1237,11 @@ export class CollectionImpl<
1217
1237
  // Update cached size after synced data changes
1218
1238
  this._size = this.calculateSize()
1219
1239
 
1240
+ // Update indexes for all events before emitting
1241
+ if (events.length > 0) {
1242
+ this.updateIndexes(events)
1243
+ }
1244
+
1220
1245
  // End batching and emit all events (combines any batched events with sync events)
1221
1246
  this.emitEvents(events, true)
1222
1247
 
@@ -1242,13 +1267,11 @@ export class CollectionImpl<
1242
1267
 
1243
1268
  private ensureStandardSchema(schema: unknown): StandardSchema<T> {
1244
1269
  // If the schema already implements the standard-schema interface, return it
1245
- if (schema && typeof schema === `object` && `~standard` in schema) {
1270
+ if (schema && `~standard` in (schema as {})) {
1246
1271
  return schema as StandardSchema<T>
1247
1272
  }
1248
1273
 
1249
- throw new Error(
1250
- `Schema must either implement the standard-schema interface or be a Zod schema`
1251
- )
1274
+ throw new InvalidSchemaError()
1252
1275
  }
1253
1276
 
1254
1277
  public getKeyFromItem(item: T): TKey {
@@ -1257,14 +1280,169 @@ export class CollectionImpl<
1257
1280
 
1258
1281
  public generateGlobalKey(key: any, item: any): string {
1259
1282
  if (typeof key === `undefined`) {
1260
- throw new Error(
1261
- `An object was created without a defined key: ${JSON.stringify(item)}`
1262
- )
1283
+ throw new UndefinedKeyError(item)
1263
1284
  }
1264
1285
 
1265
1286
  return `KEY::${this.id}/${key}`
1266
1287
  }
1267
1288
 
1289
+ /**
1290
+ * Creates an index on a collection for faster queries.
1291
+ * Indexes significantly improve query performance by allowing binary search
1292
+ * and range queries instead of full scans.
1293
+ *
1294
+ * @template TResolver - The type of the index resolver (constructor or async loader)
1295
+ * @param indexCallback - Function that extracts the indexed value from each item
1296
+ * @param config - Configuration including index type and type-specific options
1297
+ * @returns An index proxy that provides access to the index when ready
1298
+ *
1299
+ * @example
1300
+ * // Create a default ordered index
1301
+ * const ageIndex = collection.createIndex((row) => row.age)
1302
+ *
1303
+ * // Create a ordered index with custom options
1304
+ * const ageIndex = collection.createIndex((row) => row.age, {
1305
+ * indexType: OrderedIndex,
1306
+ * options: { compareFn: customComparator },
1307
+ * name: 'age_btree'
1308
+ * })
1309
+ *
1310
+ * // Create an async-loaded index
1311
+ * const textIndex = collection.createIndex((row) => row.content, {
1312
+ * indexType: async () => {
1313
+ * const { FullTextIndex } = await import('./indexes/fulltext.js')
1314
+ * return FullTextIndex
1315
+ * },
1316
+ * options: { language: 'en' }
1317
+ * })
1318
+ */
1319
+ public createIndex<
1320
+ TResolver extends IndexResolver<TKey> = typeof OrderedIndex,
1321
+ >(
1322
+ indexCallback: (row: SingleRowRefProxy<T>) => any,
1323
+ config: IndexOptions<TResolver> = {}
1324
+ ): IndexProxy<TKey> {
1325
+ this.validateCollectionUsable(`createIndex`)
1326
+
1327
+ const indexId = ++this.indexCounter
1328
+ const singleRowRefProxy = createSingleRowRefProxy<T>()
1329
+ const indexExpression = indexCallback(singleRowRefProxy)
1330
+ const expression = toExpression(indexExpression)
1331
+
1332
+ // Default to OrderedIndex if no type specified
1333
+ const resolver = config.indexType ?? (OrderedIndex as unknown as TResolver)
1334
+
1335
+ // Create lazy wrapper
1336
+ const lazyIndex = new LazyIndexWrapper<TKey>(
1337
+ indexId,
1338
+ expression,
1339
+ config.name,
1340
+ resolver,
1341
+ config.options,
1342
+ this.entries()
1343
+ )
1344
+
1345
+ this.lazyIndexes.set(indexId, lazyIndex)
1346
+
1347
+ // For OrderedIndex, resolve immediately and synchronously
1348
+ if ((resolver as unknown) === OrderedIndex) {
1349
+ try {
1350
+ const resolvedIndex = lazyIndex.getResolved()
1351
+ this.resolvedIndexes.set(indexId, resolvedIndex)
1352
+ } catch (error) {
1353
+ console.warn(`Failed to resolve OrderedIndex:`, error)
1354
+ }
1355
+ } else if (typeof resolver === `function` && resolver.prototype) {
1356
+ // Other synchronous constructors - resolve immediately
1357
+ try {
1358
+ const resolvedIndex = lazyIndex.getResolved()
1359
+ this.resolvedIndexes.set(indexId, resolvedIndex)
1360
+ } catch {
1361
+ // Fallback to async resolution
1362
+ this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
1363
+ console.warn(`Failed to resolve single index:`, error)
1364
+ })
1365
+ }
1366
+ } else if (this.isIndexesResolved) {
1367
+ // Async loader but indexes are already resolved - resolve this one
1368
+ this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
1369
+ console.warn(`Failed to resolve single index:`, error)
1370
+ })
1371
+ }
1372
+
1373
+ return new IndexProxy(indexId, lazyIndex)
1374
+ }
1375
+
1376
+ /**
1377
+ * Resolve all lazy indexes (called when collection first syncs)
1378
+ * @private
1379
+ */
1380
+ private async resolveAllIndexes(): Promise<void> {
1381
+ if (this.isIndexesResolved) return
1382
+
1383
+ const resolutionPromises = Array.from(this.lazyIndexes.entries()).map(
1384
+ async ([indexId, lazyIndex]) => {
1385
+ const resolvedIndex = await lazyIndex.resolve()
1386
+
1387
+ // Build index with current data
1388
+ resolvedIndex.build(this.entries())
1389
+
1390
+ this.resolvedIndexes.set(indexId, resolvedIndex)
1391
+ return { indexId, resolvedIndex }
1392
+ }
1393
+ )
1394
+
1395
+ await Promise.all(resolutionPromises)
1396
+ this.isIndexesResolved = true
1397
+ }
1398
+
1399
+ /**
1400
+ * Resolve a single index immediately
1401
+ * @private
1402
+ */
1403
+ private async resolveSingleIndex(
1404
+ indexId: number,
1405
+ lazyIndex: LazyIndexWrapper<TKey>
1406
+ ): Promise<BaseIndex<TKey>> {
1407
+ const resolvedIndex = await lazyIndex.resolve()
1408
+ resolvedIndex.build(this.entries())
1409
+ this.resolvedIndexes.set(indexId, resolvedIndex)
1410
+ return resolvedIndex
1411
+ }
1412
+
1413
+ /**
1414
+ * Get resolved indexes for query optimization
1415
+ */
1416
+ get indexes(): Map<number, BaseIndex<TKey>> {
1417
+ return this.resolvedIndexes
1418
+ }
1419
+
1420
+ /**
1421
+ * Updates all indexes when the collection changes
1422
+ * @private
1423
+ */
1424
+ private updateIndexes(changes: Array<ChangeMessage<T, TKey>>): void {
1425
+ for (const index of this.resolvedIndexes.values()) {
1426
+ for (const change of changes) {
1427
+ switch (change.type) {
1428
+ case `insert`:
1429
+ index.add(change.key, change.value)
1430
+ break
1431
+ case `update`:
1432
+ if (change.previousValue) {
1433
+ index.update(change.key, change.previousValue, change.value)
1434
+ } else {
1435
+ index.add(change.key, change.value)
1436
+ }
1437
+ break
1438
+ case `delete`:
1439
+ index.remove(change.key, change.value)
1440
+ break
1441
+ }
1442
+ }
1443
+ }
1444
+ }
1445
+
1268
1446
  private deepEqual(a: any, b: any): boolean {
1269
1447
  if (a === b) return true
1270
1448
  if (a == null || b == null) return false
@@ -1316,7 +1494,7 @@ export class CollectionImpl<
1316
1494
 
1317
1495
  // Ensure validation is synchronous
1318
1496
  if (result instanceof Promise) {
1319
- throw new TypeError(`Schema validation must be synchronous`)
1497
+ throw new SchemaMustBeSynchronousError()
1320
1498
  }
1321
1499
 
1322
1500
  // If validation fails, throw a SchemaValidationError with the issues
@@ -1339,7 +1517,7 @@ export class CollectionImpl<
1339
1517
 
1340
1518
  // Ensure validation is synchronous
1341
1519
  if (result instanceof Promise) {
1342
- throw new TypeError(`Schema validation must be synchronous`)
1520
+ throw new SchemaMustBeSynchronousError()
1343
1521
  }
1344
1522
 
1345
1523
  // If validation fails, throw a SchemaValidationError with the issues
@@ -1399,9 +1577,7 @@ export class CollectionImpl<
1399
1577
 
1400
1578
  // If no ambient transaction exists, check for an onInsert handler early
1401
1579
  if (!ambientTransaction && !this.config.onInsert) {
1402
- throw new Error(
1403
- `Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.`
1404
- )
1580
+ throw new MissingInsertHandlerError()
1405
1581
  }
1406
1582
 
1407
1583
  const items = Array.isArray(data) ? data : [data]
@@ -1415,7 +1591,7 @@ export class CollectionImpl<
1415
1591
  // Check if an item with this ID already exists in the collection
1416
1592
  const key = this.getKeyFromItem(validatedData)
1417
1593
  if (this.has(key)) {
1418
- throw `Cannot insert document with ID "${key}" because it already exists in the collection`
1594
+ throw new DuplicateKeyError(key)
1419
1595
  }
1420
1596
  const globalKey = this.generateGlobalKey(key, item)
1421
1597
 
@@ -1554,7 +1730,7 @@ export class CollectionImpl<
1554
1730
  maybeCallback?: (draft: TItem | Array<TItem>) => void
1555
1731
  ) {
1556
1732
  if (typeof keys === `undefined`) {
1557
- throw new Error(`The first argument to update is missing`)
1733
+ throw new MissingUpdateArgumentError()
1558
1734
  }
1559
1735
 
1560
1736
  this.validateCollectionUsable(`update`)
@@ -1563,16 +1739,14 @@ export class CollectionImpl<
1563
1739
 
1564
1740
  // If no ambient transaction exists, check for an onUpdate handler early
1565
1741
  if (!ambientTransaction && !this.config.onUpdate) {
1566
- throw new Error(
1567
- `Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
1568
- )
1742
+ throw new MissingUpdateHandlerError()
1569
1743
  }
1570
1744
 
1571
1745
  const isArray = Array.isArray(keys)
1572
1746
  const keysArray = isArray ? keys : [keys]
1573
1747
 
1574
1748
  if (isArray && keysArray.length === 0) {
1575
- throw new Error(`No keys were passed to update`)
1749
+ throw new NoKeysPassedToUpdateError()
1576
1750
  }
1577
1751
 
1578
1752
  const callback =
@@ -1584,9 +1758,7 @@ export class CollectionImpl<
1584
1758
  const currentObjects = keysArray.map((key) => {
1585
1759
  const item = this.get(key)
1586
1760
  if (!item) {
1587
- throw new Error(
1588
- `The key "${key}" was passed to update but an object for this key was not found in the collection`
1589
- )
1761
+ throw new UpdateKeyNotFoundError(key)
1590
1762
  }
1591
1763
 
1592
1764
  return item
@@ -1637,9 +1809,7 @@ export class CollectionImpl<
1637
1809
  const modifiedItemId = this.getKeyFromItem(modifiedItem)
1638
1810
 
1639
1811
  if (originalItemId !== modifiedItemId) {
1640
- throw new Error(
1641
- `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.`
1642
- )
1812
+ throw new KeyUpdateNotAllowedError(originalItemId, modifiedItemId)
1643
1813
  }
1644
1814
 
1645
1815
  const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem)
@@ -1753,13 +1923,11 @@ export class CollectionImpl<
1753
1923
 
1754
1924
  // If no ambient transaction exists, check for an onDelete handler early
1755
1925
  if (!ambientTransaction && !this.config.onDelete) {
1756
- throw new Error(
1757
- `Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
1758
- )
1926
+ throw new MissingDeleteHandlerError()
1759
1927
  }
1760
1928
 
1761
1929
  if (Array.isArray(keys) && keys.length === 0) {
1762
- throw new Error(`No keys were passed to delete`)
1930
+ throw new NoKeysPassedToDeleteError()
1763
1931
  }
1764
1932
 
1765
1933
  const keysArray = Array.isArray(keys) ? keys : [keys]
@@ -1767,9 +1935,7 @@ export class CollectionImpl<
1767
1935
 
1768
1936
  for (const key of keysArray) {
1769
1937
  if (!this.has(key)) {
1770
- throw new Error(
1771
- `Collection.delete was called with key '${key}' but there is no item in the collection with this key`
1772
- )
1938
+ throw new DeleteKeyNotFoundError(key)
1773
1939
  }
1774
1940
  const globalKey = this.generateGlobalKey(key, this.get(key)!)
1775
1941
  const mutation: PendingMutation<T, `delete`, this> = {
@@ -1905,20 +2071,32 @@ export class CollectionImpl<
1905
2071
 
1906
2072
  /**
1907
2073
  * Returns the current state of the collection as an array of changes
2074
+ * @param options - Options including optional where filter
1908
2075
  * @returns An array of changes
2076
+ * @example
2077
+ * // Get all items as changes
2078
+ * const allChanges = collection.currentStateAsChanges()
2079
+ *
2080
+ * // Get only items matching a condition
2081
+ * const activeChanges = collection.currentStateAsChanges({
2082
+ * where: (row) => row.status === 'active'
2083
+ * })
2084
+ *
2085
+ * // Get only items using a pre-compiled expression
2086
+ * const activeChanges = collection.currentStateAsChanges({
2087
+ * whereExpression: eq(row.status, 'active')
2088
+ * })
1909
2089
  */
1910
- public currentStateAsChanges(): Array<ChangeMessage<T>> {
1911
- return Array.from(this.entries()).map(([key, value]) => ({
1912
- type: `insert`,
1913
- key,
1914
- value,
1915
- }))
2090
+ public currentStateAsChanges(
2091
+ options: CurrentStateAsChangesOptions<T> = {}
2092
+ ): Array<ChangeMessage<T>> {
2093
+ return currentStateAsChanges(this, options)
1916
2094
  }
1917
2095
 
1918
2096
  /**
1919
2097
  * Subscribe to changes in the collection
1920
2098
  * @param callback - Function called when items change
1921
- * @param options.includeInitialState - If true, immediately calls callback with current data
2099
+ * @param options - Subscription options including includeInitialState and where filter
1922
2100
  * @returns Unsubscribe function - Call this to stop listening for changes
1923
2101
  * @example
1924
2102
  * // Basic subscription
@@ -1935,24 +2113,57 @@ export class CollectionImpl<
1935
2113
  * const unsubscribe = collection.subscribeChanges((changes) => {
1936
2114
  * updateUI(changes)
1937
2115
  * }, { includeInitialState: true })
2116
+ *
2117
+ * @example
2118
+ * // Subscribe only to changes matching a condition
2119
+ * const unsubscribe = collection.subscribeChanges((changes) => {
2120
+ * updateUI(changes)
2121
+ * }, {
2122
+ * includeInitialState: true,
2123
+ * where: (row) => row.status === 'active'
2124
+ * })
2125
+ *
2126
+ * @example
2127
+ * // Subscribe using a pre-compiled expression
2128
+ * const unsubscribe = collection.subscribeChanges((changes) => {
2129
+ * updateUI(changes)
2130
+ * }, {
2131
+ * includeInitialState: true,
2132
+ * whereExpression: eq(row.status, 'active')
2133
+ * })
1938
2134
  */
1939
2135
  public subscribeChanges(
1940
2136
  callback: (changes: Array<ChangeMessage<T>>) => void,
1941
- { includeInitialState = false }: { includeInitialState?: boolean } = {}
2137
+ options: SubscribeChangesOptions<T> = {}
1942
2138
  ): () => void {
1943
2139
  // Start sync and track subscriber
1944
2140
  this.addSubscriber()
1945
2141
 
1946
- if (includeInitialState) {
1947
- // First send the current state as changes
1948
- callback(this.currentStateAsChanges())
2142
+ // Auto-index for where expressions if enabled
2143
+ if (options.whereExpression) {
2144
+ ensureIndexForExpression(options.whereExpression, this)
2145
+ }
2146
+
2147
+ // Create a filtered callback if where clause is provided
2148
+ const filteredCallback =
2149
+ options.where || options.whereExpression
2150
+ ? createFilteredCallback(callback, options)
2151
+ : callback
2152
+
2153
+ if (options.includeInitialState) {
2154
+ // First send the current state as changes (filtered if needed)
2155
+ const initialChanges = this.currentStateAsChanges({
2156
+ where: options.where,
2157
+ whereExpression: options.whereExpression,
2158
+ })
2159
+ filteredCallback(initialChanges)
1949
2160
  }
1950
2161
 
1951
2162
  // Add to batched listeners
1952
- this.changeListeners.add(callback)
2163
+ this.changeListeners.add(filteredCallback)
1953
2164
 
1954
2165
  return () => {
1955
- this.changeListeners.delete(callback)
2166
+ this.changeListeners.delete(filteredCallback)
1956
2167
  this.removeSubscriber()
1957
2168
  }
1958
2169
  }