@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.
- package/dist/cjs/change-events.cjs +141 -0
- package/dist/cjs/change-events.cjs.map +1 -0
- package/dist/cjs/change-events.d.cts +49 -0
- package/dist/cjs/collection.cjs +236 -90
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +95 -20
- package/dist/cjs/errors.cjs +509 -1
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +225 -1
- package/dist/cjs/index.cjs +82 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +5 -1
- package/dist/cjs/indexes/auto-index.cjs +64 -0
- package/dist/cjs/indexes/auto-index.cjs.map +1 -0
- package/dist/cjs/indexes/auto-index.d.cts +9 -0
- package/dist/cjs/indexes/base-index.cjs +46 -0
- package/dist/cjs/indexes/base-index.cjs.map +1 -0
- package/dist/cjs/indexes/base-index.d.cts +54 -0
- package/dist/cjs/indexes/index-options.d.cts +13 -0
- package/dist/cjs/indexes/lazy-index.cjs +193 -0
- package/dist/cjs/indexes/lazy-index.cjs.map +1 -0
- package/dist/cjs/indexes/lazy-index.d.cts +96 -0
- package/dist/cjs/indexes/ordered-index.cjs +227 -0
- package/dist/cjs/indexes/ordered-index.cjs.map +1 -0
- package/dist/cjs/indexes/ordered-index.d.cts +72 -0
- package/dist/cjs/local-storage.cjs +9 -15
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +11 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +4 -0
- package/dist/cjs/query/builder/index.cjs +6 -7
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.cjs +37 -0
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +12 -0
- package/dist/cjs/query/compiler/evaluators.cjs +83 -58
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +8 -0
- package/dist/cjs/query/compiler/expressions.cjs +61 -0
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -0
- package/dist/cjs/query/compiler/expressions.d.cts +25 -0
- package/dist/cjs/query/compiler/group-by.cjs +5 -10
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +23 -17
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +12 -3
- package/dist/cjs/query/compiler/joins.cjs +61 -12
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +4 -34
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/types.d.cts +2 -2
- package/dist/cjs/query/live-query-collection.cjs +54 -12
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/optimizer.cjs +45 -7
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +13 -3
- package/dist/cjs/transactions.cjs +5 -4
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +31 -0
- package/dist/cjs/utils/array-utils.cjs +18 -0
- package/dist/cjs/utils/array-utils.cjs.map +1 -0
- package/dist/cjs/utils/array-utils.d.cts +8 -0
- package/dist/cjs/utils/comparison.cjs +52 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -0
- package/dist/cjs/utils/comparison.d.cts +11 -0
- package/dist/cjs/utils/index-optimization.cjs +270 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -0
- package/dist/cjs/utils/index-optimization.d.cts +29 -0
- package/dist/esm/change-events.d.ts +49 -0
- package/dist/esm/change-events.js +141 -0
- package/dist/esm/change-events.js.map +1 -0
- package/dist/esm/collection.d.ts +95 -20
- package/dist/esm/collection.js +234 -88
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/errors.d.ts +225 -1
- package/dist/esm/errors.js +510 -2
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +5 -1
- package/dist/esm/index.js +81 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.d.ts +9 -0
- package/dist/esm/indexes/auto-index.js +64 -0
- package/dist/esm/indexes/auto-index.js.map +1 -0
- package/dist/esm/indexes/base-index.d.ts +54 -0
- package/dist/esm/indexes/base-index.js +46 -0
- package/dist/esm/indexes/base-index.js.map +1 -0
- package/dist/esm/indexes/index-options.d.ts +13 -0
- package/dist/esm/indexes/lazy-index.d.ts +96 -0
- package/dist/esm/indexes/lazy-index.js +193 -0
- package/dist/esm/indexes/lazy-index.js.map +1 -0
- package/dist/esm/indexes/ordered-index.d.ts +72 -0
- package/dist/esm/indexes/ordered-index.js +227 -0
- package/dist/esm/indexes/ordered-index.js.map +1 -0
- package/dist/esm/local-storage.js +9 -15
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +4 -0
- package/dist/esm/query/builder/functions.js +11 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js +6 -7
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +12 -0
- package/dist/esm/query/builder/ref-proxy.js +37 -0
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.d.ts +8 -0
- package/dist/esm/query/compiler/evaluators.js +84 -59
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.d.ts +25 -0
- package/dist/esm/query/compiler/expressions.js +61 -0
- package/dist/esm/query/compiler/expressions.js.map +1 -0
- package/dist/esm/query/compiler/group-by.js +5 -10
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +12 -3
- package/dist/esm/query/compiler/index.js +23 -17
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +61 -12
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.js +1 -31
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/types.d.ts +2 -2
- package/dist/esm/query/live-query-collection.js +54 -12
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +13 -3
- package/dist/esm/query/optimizer.js +40 -2
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/transactions.js +5 -4
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +31 -0
- package/dist/esm/utils/array-utils.d.ts +8 -0
- package/dist/esm/utils/array-utils.js +18 -0
- package/dist/esm/utils/array-utils.js.map +1 -0
- package/dist/esm/utils/comparison.d.ts +11 -0
- package/dist/esm/utils/comparison.js +52 -0
- package/dist/esm/utils/comparison.js.map +1 -0
- package/dist/esm/utils/index-optimization.d.ts +29 -0
- package/dist/esm/utils/index-optimization.js +270 -0
- package/dist/esm/utils/index-optimization.js.map +1 -0
- package/package.json +3 -2
- package/src/change-events.ts +257 -0
- package/src/collection.ts +321 -110
- package/src/errors.ts +545 -1
- package/src/index.ts +7 -1
- package/src/indexes/auto-index.ts +108 -0
- package/src/indexes/base-index.ts +119 -0
- package/src/indexes/index-options.ts +42 -0
- package/src/indexes/lazy-index.ts +251 -0
- package/src/indexes/ordered-index.ts +305 -0
- package/src/local-storage.ts +16 -17
- package/src/query/builder/functions.ts +14 -0
- package/src/query/builder/index.ts +12 -7
- package/src/query/builder/ref-proxy.ts +65 -0
- package/src/query/compiler/evaluators.ts +114 -62
- package/src/query/compiler/expressions.ts +92 -0
- package/src/query/compiler/group-by.ts +10 -10
- package/src/query/compiler/index.ts +52 -22
- package/src/query/compiler/joins.ts +114 -15
- package/src/query/compiler/order-by.ts +1 -45
- package/src/query/compiler/types.ts +2 -2
- package/src/query/live-query-collection.ts +95 -15
- package/src/query/optimizer.ts +94 -5
- package/src/transactions.ts +10 -4
- package/src/types.ts +38 -0
- package/src/utils/array-utils.ts +28 -0
- package/src/utils/comparison.ts +79 -0
- 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 {
|
|
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
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
480
|
+
throw new NoPendingSyncTransactionWriteError()
|
|
457
481
|
}
|
|
458
482
|
if (pendingTransaction.committed) {
|
|
459
|
-
throw new
|
|
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
|
|
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
|
|
511
|
+
throw new NoPendingSyncTransactionCommitError()
|
|
492
512
|
}
|
|
493
513
|
if (pendingTransaction.committed) {
|
|
494
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
1270
|
+
if (schema && `~standard` in (schema as {})) {
|
|
1246
1271
|
return schema as StandardSchema<T>
|
|
1247
1272
|
}
|
|
1248
1273
|
|
|
1249
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
|
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
|
-
|
|
2137
|
+
options: SubscribeChangesOptions<T> = {}
|
|
1942
2138
|
): () => void {
|
|
1943
2139
|
// Start sync and track subscriber
|
|
1944
2140
|
this.addSubscriber()
|
|
1945
2141
|
|
|
1946
|
-
if
|
|
1947
|
-
|
|
1948
|
-
|
|
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(
|
|
2163
|
+
this.changeListeners.add(filteredCallback)
|
|
1953
2164
|
|
|
1954
2165
|
return () => {
|
|
1955
|
-
this.changeListeners.delete(
|
|
2166
|
+
this.changeListeners.delete(filteredCallback)
|
|
1956
2167
|
this.removeSubscriber()
|
|
1957
2168
|
}
|
|
1958
2169
|
}
|