@korajs/store 0.3.1 → 0.4.0
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/adapters/better-sqlite3.cjs +1 -1
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.js +1 -1
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +3 -1
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.js +3 -1
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/index.cjs +1283 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +544 -8
- package/dist/index.d.ts +544 -8
- package/dist/index.js +1287 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Q as QueryDescriptor, a as SubscriptionCallback, C as CollectionRecord, S as StorageAdapter, W as WhereClause, O as OrderByDirection, b as StoreConfig, A as ApplyResult } from './types-DF-KDSK1.cjs';
|
|
2
|
-
export { M as MigrationPlan, c as OrderByClause,
|
|
3
|
-
import { KoraError, Operation, CollectionDefinition, SchemaDefinition, OperationLog, VersionVector,
|
|
1
|
+
import { Q as QueryDescriptor, a as SubscriptionCallback, C as CollectionRecord, S as StorageAdapter, W as WhereClause, O as OrderByDirection, b as StoreConfig, A as ApplyResult, T as Transaction } from './types-DF-KDSK1.cjs';
|
|
2
|
+
export { M as MigrationPlan, c as OrderByClause, d as WhereOperators } from './types-DF-KDSK1.cjs';
|
|
3
|
+
import { KoraError, Operation, CollectionDefinition, SchemaDefinition, SequenceConfig, HybridLogicalClock, OperationLog, VersionVector, OnDeleteAction, RelationDefinition, StateMachineDefinition } from '@korajs/core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Thrown when a query is invalid (bad field names, invalid operators, etc.).
|
|
@@ -46,14 +46,77 @@ declare class PersistenceError extends KoraError {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
* Configuration options for the SubscriptionManager.
|
|
50
|
+
*/
|
|
51
|
+
interface SubscriptionManagerOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Minimum number of subscriptions before activating bloom filter.
|
|
54
|
+
* Below this threshold, linear scanning is used (bloom filter overhead not worth it).
|
|
55
|
+
* @default 100
|
|
56
|
+
*/
|
|
57
|
+
bloomThreshold?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Expected number of unique collection+field dependencies for bloom filter sizing.
|
|
60
|
+
* @default 500
|
|
61
|
+
*/
|
|
62
|
+
bloomExpectedItems?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Target false positive rate for the bloom filter.
|
|
65
|
+
* Lower values require more memory but reduce unnecessary precise checks.
|
|
66
|
+
* @default 0.01
|
|
67
|
+
*/
|
|
68
|
+
bloomFalsePositiveRate?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Performance statistics for monitoring subscription checking efficiency.
|
|
72
|
+
*/
|
|
73
|
+
interface SubscriptionStats {
|
|
74
|
+
/** Total number of mutation notifications processed */
|
|
75
|
+
totalChecks: number;
|
|
76
|
+
/** Number of times bloom filter said "maybe" (proceeded to precise check) */
|
|
77
|
+
bloomFilterHits: number;
|
|
78
|
+
/** Number of times bloom filter said "definitely not" (skipped all subscriptions) */
|
|
79
|
+
bloomFilterMisses: number;
|
|
80
|
+
/** Number of times bloom filter said "maybe" but precise check found no match */
|
|
81
|
+
falsePositives: number;
|
|
82
|
+
/** Average time per check in milliseconds */
|
|
83
|
+
averageCheckTimeMs: number;
|
|
84
|
+
/** Whether bloom filter is currently active */
|
|
85
|
+
bloomFilterActive: boolean;
|
|
86
|
+
/** Current subscription count */
|
|
87
|
+
subscriptionCount: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Manages reactive subscriptions with two-level dependency checking.
|
|
91
|
+
*
|
|
92
|
+
* When a mutation occurs on a collection, affected subscriptions are re-evaluated
|
|
93
|
+
* in a microtask batch and callbacks are invoked only if results actually changed.
|
|
94
|
+
*
|
|
95
|
+
* For large subscription counts (>= bloomThreshold), a bloom filter provides O(k)
|
|
96
|
+
* pre-filtering to avoid scanning all subscriptions on every mutation:
|
|
97
|
+
*
|
|
98
|
+
* Level 1 (Bloom filter): Fast O(k) check -- does this mutation potentially affect
|
|
99
|
+
* any subscription? If NO: skip all subscriptions (guaranteed correct).
|
|
100
|
+
* If MAYBE: proceed to Level 2.
|
|
101
|
+
*
|
|
102
|
+
* Level 2 (Precise check): Only evaluate subscriptions that match the mutated
|
|
103
|
+
* collection, including included (related) collection tracking.
|
|
52
104
|
*/
|
|
53
105
|
declare class SubscriptionManager {
|
|
54
106
|
private subscriptions;
|
|
55
107
|
private pendingCollections;
|
|
56
108
|
private flushScheduled;
|
|
109
|
+
private bloomFilter;
|
|
110
|
+
private bloomDirty;
|
|
111
|
+
private readonly bloomThreshold;
|
|
112
|
+
private readonly bloomExpectedItems;
|
|
113
|
+
private readonly bloomFalsePositiveRate;
|
|
114
|
+
private totalChecks;
|
|
115
|
+
private bloomFilterHits;
|
|
116
|
+
private bloomFilterMisses;
|
|
117
|
+
private falsePositives;
|
|
118
|
+
private totalCheckTimeMs;
|
|
119
|
+
constructor(options?: SubscriptionManagerOptions);
|
|
57
120
|
/**
|
|
58
121
|
* Register a new subscription.
|
|
59
122
|
*
|
|
@@ -87,6 +150,33 @@ declare class SubscriptionManager {
|
|
|
87
150
|
clear(): void;
|
|
88
151
|
/** Number of active subscriptions (for testing/debugging) */
|
|
89
152
|
get size(): number;
|
|
153
|
+
/**
|
|
154
|
+
* Get performance statistics for monitoring subscription checking efficiency.
|
|
155
|
+
* Useful for DevTools integration and performance tuning.
|
|
156
|
+
*/
|
|
157
|
+
getStats(): SubscriptionStats;
|
|
158
|
+
/**
|
|
159
|
+
* Check if bloom filter is currently active.
|
|
160
|
+
* Active when subscription count meets or exceeds the threshold.
|
|
161
|
+
*/
|
|
162
|
+
isBloomActive(): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Find subscriptions affected by mutations to the given collections.
|
|
165
|
+
* Uses two-level checking when bloom filter is active:
|
|
166
|
+
*
|
|
167
|
+
* Level 1: Bloom filter pre-check -- if no subscription depends on any
|
|
168
|
+
* of the mutated collections, skip everything (O(k) per collection).
|
|
169
|
+
*
|
|
170
|
+
* Level 2: Precise check -- linear scan of subscriptions, matching
|
|
171
|
+
* against the mutated collections.
|
|
172
|
+
*/
|
|
173
|
+
private findAffectedSubscriptions;
|
|
174
|
+
/**
|
|
175
|
+
* Rebuild the bloom filter from all current subscriptions.
|
|
176
|
+
* Adds collection-level dependencies for every subscription, plus
|
|
177
|
+
* any included collection dependencies.
|
|
178
|
+
*/
|
|
179
|
+
private rebuildBloomFilter;
|
|
90
180
|
private scheduleFlush;
|
|
91
181
|
/**
|
|
92
182
|
* Compare two result sets. Uses length check + JSON comparison as pragmatic approach.
|
|
@@ -192,6 +282,145 @@ declare class QueryBuilder<T = CollectionRecord> {
|
|
|
192
282
|
private findRelation;
|
|
193
283
|
}
|
|
194
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Manages offline-safe sequences backed by a `_kora_sequences` table.
|
|
287
|
+
*
|
|
288
|
+
* Each sequence counter is scoped by (name, scope, nodeId), ensuring
|
|
289
|
+
* that different devices never collide. The counter is atomically
|
|
290
|
+
* incremented within a database transaction.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* const mgr = new SequenceManager(adapter, 'node-abc')
|
|
295
|
+
*
|
|
296
|
+
* const receipt = await mgr.next('receipt', {
|
|
297
|
+
* scope: 'store-1',
|
|
298
|
+
* format: 'S-{date}-{node4}-{seq}',
|
|
299
|
+
* })
|
|
300
|
+
* // → "S-20260508-node-0001"
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
declare class SequenceManager {
|
|
304
|
+
private readonly adapter;
|
|
305
|
+
private readonly nodeId;
|
|
306
|
+
constructor(adapter: StorageAdapter, nodeId: string);
|
|
307
|
+
/**
|
|
308
|
+
* Get the next value in a sequence, atomically incrementing the counter.
|
|
309
|
+
*
|
|
310
|
+
* @param name - The sequence name (e.g., 'receipt', 'invoice')
|
|
311
|
+
* @param config - Optional configuration for scope, format, and starting value
|
|
312
|
+
* @returns The formatted sequence value
|
|
313
|
+
*/
|
|
314
|
+
next(name: string, config?: SequenceConfig): Promise<string>;
|
|
315
|
+
/**
|
|
316
|
+
* Get the current counter value without incrementing.
|
|
317
|
+
*
|
|
318
|
+
* @param name - The sequence name
|
|
319
|
+
* @param config - Optional scope
|
|
320
|
+
* @returns The current counter value, or 0 if the sequence has never been used
|
|
321
|
+
*/
|
|
322
|
+
current(name: string, config?: {
|
|
323
|
+
scope?: string;
|
|
324
|
+
}): Promise<number>;
|
|
325
|
+
/**
|
|
326
|
+
* Reset a sequence counter.
|
|
327
|
+
*
|
|
328
|
+
* @param name - The sequence name
|
|
329
|
+
* @param config - Optional scope and target value
|
|
330
|
+
*/
|
|
331
|
+
reset(name: string, config?: {
|
|
332
|
+
scope?: string;
|
|
333
|
+
to?: number;
|
|
334
|
+
}): Promise<void>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Internal configuration for creating a TransactionContext.
|
|
339
|
+
* Passed from Store to avoid exposing Store internals publicly.
|
|
340
|
+
*/
|
|
341
|
+
interface TransactionContextConfig {
|
|
342
|
+
schema: SchemaDefinition;
|
|
343
|
+
adapter: StorageAdapter;
|
|
344
|
+
clock: HybridLogicalClock;
|
|
345
|
+
nodeId: string;
|
|
346
|
+
getSequenceNumber: () => number;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A collection accessor within a transaction.
|
|
350
|
+
* Operations are buffered and committed atomically when the transaction completes.
|
|
351
|
+
*/
|
|
352
|
+
interface TransactionCollectionAccessor {
|
|
353
|
+
insert(data: Record<string, unknown>): Promise<CollectionRecord>;
|
|
354
|
+
update(id: string, data: Record<string, unknown>): Promise<CollectionRecord>;
|
|
355
|
+
delete(id: string): Promise<void>;
|
|
356
|
+
findById(id: string): Promise<CollectionRecord | null>;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* TransactionContext provides atomic multi-collection operations.
|
|
360
|
+
*
|
|
361
|
+
* All mutations are buffered and committed in a single StorageAdapter.transaction()
|
|
362
|
+
* call. All operations share the same transactionId (UUID v7).
|
|
363
|
+
*
|
|
364
|
+
* Subscription notifications are deferred until after commit.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const { operations, affectedCollections } = await txContext.commit()
|
|
369
|
+
* // Notify subscriptions after commit
|
|
370
|
+
* for (const op of operations) {
|
|
371
|
+
* subscriptionManager.notify(op.collection, op)
|
|
372
|
+
* }
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
declare class TransactionContext {
|
|
376
|
+
private readonly transactionId;
|
|
377
|
+
private mutationName;
|
|
378
|
+
private readonly buffer;
|
|
379
|
+
private committed;
|
|
380
|
+
private rolledBack;
|
|
381
|
+
private readonly config;
|
|
382
|
+
constructor(config: TransactionContextConfig);
|
|
383
|
+
/**
|
|
384
|
+
* Set a human-readable mutation name for this transaction.
|
|
385
|
+
* Propagated to all operations for DevTools display.
|
|
386
|
+
*/
|
|
387
|
+
setMutationName(name: string): void;
|
|
388
|
+
/**
|
|
389
|
+
* Get the mutation name, if set.
|
|
390
|
+
*/
|
|
391
|
+
getMutationName(): string | undefined;
|
|
392
|
+
/**
|
|
393
|
+
* Get a collection accessor for buffered operations within this transaction.
|
|
394
|
+
*/
|
|
395
|
+
collection(name: string): TransactionCollectionAccessor;
|
|
396
|
+
/**
|
|
397
|
+
* Commit all buffered operations atomically.
|
|
398
|
+
* Returns the list of operations and affected collections for subscription notification.
|
|
399
|
+
*/
|
|
400
|
+
commit(): Promise<{
|
|
401
|
+
operations: Operation[];
|
|
402
|
+
affectedCollections: Set<string>;
|
|
403
|
+
}>;
|
|
404
|
+
/**
|
|
405
|
+
* Mark the transaction as rolled back. No operations will be committed.
|
|
406
|
+
*/
|
|
407
|
+
rollback(): void;
|
|
408
|
+
/**
|
|
409
|
+
* Get the transaction ID shared by all operations in this transaction.
|
|
410
|
+
*/
|
|
411
|
+
getTransactionId(): string;
|
|
412
|
+
private ensureActive;
|
|
413
|
+
private insert;
|
|
414
|
+
private update;
|
|
415
|
+
private deleteRecord;
|
|
416
|
+
private findById;
|
|
417
|
+
/**
|
|
418
|
+
* Get the effective state of a record, considering both the database and buffered operations.
|
|
419
|
+
* Buffered inserts/updates take precedence over the database state.
|
|
420
|
+
*/
|
|
421
|
+
private getEffectiveRecord;
|
|
422
|
+
}
|
|
423
|
+
|
|
195
424
|
/**
|
|
196
425
|
* Store is the main orchestrator. It owns a schema, a storage adapter,
|
|
197
426
|
* a clock, and a subscription manager. It creates Collection instances
|
|
@@ -214,6 +443,7 @@ declare class Store implements OperationLog {
|
|
|
214
443
|
private clock;
|
|
215
444
|
private collections;
|
|
216
445
|
private subscriptionManager;
|
|
446
|
+
private sequenceManager;
|
|
217
447
|
private readonly schema;
|
|
218
448
|
private readonly adapter;
|
|
219
449
|
private readonly configNodeId;
|
|
@@ -263,7 +493,45 @@ declare class Store implements OperationLog {
|
|
|
263
493
|
getSchema(): SchemaDefinition;
|
|
264
494
|
/** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
|
|
265
495
|
getSubscriptionManager(): SubscriptionManager;
|
|
496
|
+
/**
|
|
497
|
+
* Get the sequence manager for offline-safe sequence generation.
|
|
498
|
+
* @throws {StoreNotOpenError} If the store is not open
|
|
499
|
+
*/
|
|
500
|
+
getSequenceManager(): SequenceManager;
|
|
501
|
+
/**
|
|
502
|
+
* Create a TransactionContext for atomic multi-collection operations.
|
|
503
|
+
* The returned context buffers all mutations and commits them atomically.
|
|
504
|
+
*
|
|
505
|
+
* After commit, the caller is responsible for notifying subscriptions
|
|
506
|
+
* and emitting events for each operation.
|
|
507
|
+
*/
|
|
508
|
+
createTransaction(): TransactionContext;
|
|
509
|
+
/**
|
|
510
|
+
* Execute a function within a transaction. All mutations performed on the
|
|
511
|
+
* TransactionContext are committed atomically. Subscription notifications
|
|
512
|
+
* are batched and fired after the commit.
|
|
513
|
+
*
|
|
514
|
+
* If the function throws, the transaction is rolled back and the error is re-thrown.
|
|
515
|
+
*
|
|
516
|
+
* @param fn - Function receiving a TransactionContext for buffered operations
|
|
517
|
+
* @returns The operations that were committed
|
|
518
|
+
*/
|
|
519
|
+
transaction(fn: (tx: TransactionContext) => Promise<void>): Promise<Operation[]>;
|
|
266
520
|
private nextSequenceNumber;
|
|
521
|
+
/**
|
|
522
|
+
* Check the stored schema version and run any pending migrations.
|
|
523
|
+
* Migrations are applied in version order within a transaction.
|
|
524
|
+
*/
|
|
525
|
+
private runMigrationsIfNeeded;
|
|
526
|
+
/**
|
|
527
|
+
* Get the stored schema version from _kora_meta. Returns 0 if not set.
|
|
528
|
+
*/
|
|
529
|
+
private getStoredSchemaVersion;
|
|
530
|
+
/**
|
|
531
|
+
* Run a backfill transform on all records in a collection.
|
|
532
|
+
* Reads all rows, applies the transform, and updates changed fields.
|
|
533
|
+
*/
|
|
534
|
+
private runBackfill;
|
|
267
535
|
private loadOrGenerateNodeId;
|
|
268
536
|
private loadSequenceNumber;
|
|
269
537
|
private loadVersionVector;
|
|
@@ -280,6 +548,205 @@ interface CollectionAccessor {
|
|
|
280
548
|
where(conditions: Record<string, unknown>): QueryBuilder;
|
|
281
549
|
}
|
|
282
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Bloom filter for subscription dependency tracking.
|
|
553
|
+
*
|
|
554
|
+
* Each subscription adds its watched collection (and optionally fields) to the
|
|
555
|
+
* filter. When a mutation arrives, we check "collection" and "collection:field"
|
|
556
|
+
* keys against the filter. A negative result means no subscription cares about
|
|
557
|
+
* this mutation, so we can skip the expensive per-subscription scan.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```typescript
|
|
561
|
+
* const filter = new SubscriptionBloomFilter(100, 0.01)
|
|
562
|
+
* filter.add('todos')
|
|
563
|
+
* filter.add('todos', 'completed')
|
|
564
|
+
*
|
|
565
|
+
* filter.mightContain('todos') // true (definitely added)
|
|
566
|
+
* filter.mightContain('projects') // false (definitely not added)
|
|
567
|
+
* filter.mightContain('todos', 'completed') // true (definitely added)
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
declare class SubscriptionBloomFilter {
|
|
571
|
+
private bits;
|
|
572
|
+
private readonly bitCount;
|
|
573
|
+
private readonly hashCount;
|
|
574
|
+
private itemCount;
|
|
575
|
+
constructor(expectedItems: number, falsePositiveRate?: number);
|
|
576
|
+
/**
|
|
577
|
+
* Add a collection (and optional field) to the bloom filter.
|
|
578
|
+
*
|
|
579
|
+
* @param collection - Collection name (e.g., "todos")
|
|
580
|
+
* @param field - Optional field name (e.g., "completed")
|
|
581
|
+
*/
|
|
582
|
+
add(collection: string, field?: string): void;
|
|
583
|
+
/**
|
|
584
|
+
* Check if a collection (and optional field) might be in the filter.
|
|
585
|
+
*
|
|
586
|
+
* A return value of `false` means the item is DEFINITELY NOT in the filter.
|
|
587
|
+
* A return value of `true` means the item MIGHT be in the filter (possible false positive).
|
|
588
|
+
*
|
|
589
|
+
* @param collection - Collection name to check
|
|
590
|
+
* @param field - Optional field name to check
|
|
591
|
+
* @returns false = definitely absent, true = possibly present
|
|
592
|
+
*/
|
|
593
|
+
mightContain(collection: string, field?: string): boolean;
|
|
594
|
+
/**
|
|
595
|
+
* Reset the bloom filter, clearing all bits and the item count.
|
|
596
|
+
*/
|
|
597
|
+
clear(): void;
|
|
598
|
+
/**
|
|
599
|
+
* Estimate the current false positive rate based on the number of items inserted.
|
|
600
|
+
*
|
|
601
|
+
* Formula: (1 - e^(-kn/m))^k
|
|
602
|
+
* where k = hash count, n = item count, m = bit count.
|
|
603
|
+
*
|
|
604
|
+
* @returns Estimated false positive rate as a number between 0 and 1
|
|
605
|
+
*/
|
|
606
|
+
estimatedFalsePositiveRate(): number;
|
|
607
|
+
/**
|
|
608
|
+
* @returns The number of items that have been added to the filter
|
|
609
|
+
*/
|
|
610
|
+
getItemCount(): number;
|
|
611
|
+
/**
|
|
612
|
+
* @returns The total number of bits in the filter
|
|
613
|
+
*/
|
|
614
|
+
getBitCount(): number;
|
|
615
|
+
/**
|
|
616
|
+
* @returns The number of hash functions used
|
|
617
|
+
*/
|
|
618
|
+
getHashCount(): number;
|
|
619
|
+
/**
|
|
620
|
+
* Count the number of bits currently set to 1.
|
|
621
|
+
* Uses Brian Kernighan's algorithm: each iteration clears the lowest set bit,
|
|
622
|
+
* so the loop runs exactly as many times as there are set bits.
|
|
623
|
+
*
|
|
624
|
+
* @returns Number of bits set to 1
|
|
625
|
+
*/
|
|
626
|
+
getSetBitCount(): number;
|
|
627
|
+
/**
|
|
628
|
+
* Compute k bit positions for a given key using Kirsch-Mitzenmacker double hashing.
|
|
629
|
+
*
|
|
630
|
+
* Instead of computing k independent hash functions, we compute two base hashes
|
|
631
|
+
* (h1 and h2) and derive the rest as: h_i = (h1 + i * h2) mod m.
|
|
632
|
+
* This technique is proven to have the same asymptotic false positive rate as
|
|
633
|
+
* k independent hash functions.
|
|
634
|
+
*
|
|
635
|
+
* We derive h1 and h2 from a single FNV-1a hash by splitting and mixing:
|
|
636
|
+
* h1 = fnv1a(key), h2 = fnv1a(key + "\0salt") to ensure independence.
|
|
637
|
+
*/
|
|
638
|
+
private getPositions;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* A resolved relation reference: describes a relation that points TO a given collection.
|
|
643
|
+
* When a record in `targetCollection` is deleted, we must handle records in
|
|
644
|
+
* `sourceCollection` that reference it via `foreignKeyField`.
|
|
645
|
+
*/
|
|
646
|
+
interface IncomingRelation {
|
|
647
|
+
/** Name of the relation in the schema */
|
|
648
|
+
relationName: string;
|
|
649
|
+
/** Collection that holds the foreign key (the "from" side) */
|
|
650
|
+
sourceCollection: string;
|
|
651
|
+
/** Field in the source collection that references the target */
|
|
652
|
+
foreignKeyField: string;
|
|
653
|
+
/** What to do when the referenced record is deleted */
|
|
654
|
+
onDelete: OnDeleteAction;
|
|
655
|
+
/** The full relation definition */
|
|
656
|
+
relation: RelationDefinition;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Configuration for the RelationEnforcer.
|
|
661
|
+
*/
|
|
662
|
+
interface RelationEnforcerConfig {
|
|
663
|
+
schema: SchemaDefinition;
|
|
664
|
+
adapter: StorageAdapter;
|
|
665
|
+
clock: HybridLogicalClock;
|
|
666
|
+
nodeId: string;
|
|
667
|
+
getSequenceNumber: () => number;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Result of enforcing referential integrity on a delete operation.
|
|
671
|
+
* Contains all additional operations that were created as side effects
|
|
672
|
+
* (cascaded deletes and set-null updates).
|
|
673
|
+
*/
|
|
674
|
+
interface EnforcementResult {
|
|
675
|
+
/** Additional operations created by cascading deletes and set-null updates */
|
|
676
|
+
operations: Operation[];
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Enforces referential integrity constraints during local delete operations.
|
|
680
|
+
*
|
|
681
|
+
* When a record is deleted, this enforcer checks all relations that reference
|
|
682
|
+
* the deleted record's collection and applies the appropriate onDelete policy:
|
|
683
|
+
*
|
|
684
|
+
* - **cascade**: Recursively deletes all referencing records
|
|
685
|
+
* - **set-null**: Sets the foreign key to null on all referencing records
|
|
686
|
+
* - **restrict**: Throws a ReferentialIntegrityError if any references exist
|
|
687
|
+
* - **no-action**: Does nothing (the foreign key is left dangling)
|
|
688
|
+
*
|
|
689
|
+
* The enforcer operates within a provided transaction to ensure atomicity.
|
|
690
|
+
* All generated operations (cascaded deletes, set-null updates) share a
|
|
691
|
+
* causal dependency chain through the original delete operation.
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* ```typescript
|
|
695
|
+
* const enforcer = new RelationEnforcer({
|
|
696
|
+
* schema, adapter, clock, nodeId,
|
|
697
|
+
* getSequenceNumber: () => ++seq,
|
|
698
|
+
* })
|
|
699
|
+
* const result = await enforcer.enforceDelete(
|
|
700
|
+
* 'projects', 'proj-1', tx, ['delete-op-id']
|
|
701
|
+
* )
|
|
702
|
+
* // result.operations contains any cascaded delete/update ops
|
|
703
|
+
* ```
|
|
704
|
+
*/
|
|
705
|
+
declare class RelationEnforcer {
|
|
706
|
+
private readonly lookup;
|
|
707
|
+
private readonly schema;
|
|
708
|
+
private readonly adapter;
|
|
709
|
+
private readonly clock;
|
|
710
|
+
private readonly nodeId;
|
|
711
|
+
private readonly getSequenceNumber;
|
|
712
|
+
constructor(config: RelationEnforcerConfig);
|
|
713
|
+
/**
|
|
714
|
+
* Enforce referential integrity after deleting a record.
|
|
715
|
+
*
|
|
716
|
+
* Must be called within a transaction. The transaction handle is used
|
|
717
|
+
* for all cascaded writes to ensure atomicity.
|
|
718
|
+
*
|
|
719
|
+
* @param collection - The collection the deleted record belongs to
|
|
720
|
+
* @param recordId - The ID of the deleted record
|
|
721
|
+
* @param tx - The active transaction handle
|
|
722
|
+
* @param causalDeps - Causal dependencies for generated operations
|
|
723
|
+
* @returns All additional operations created as side effects
|
|
724
|
+
* @throws {ReferentialIntegrityError} If a 'restrict' policy is violated
|
|
725
|
+
*/
|
|
726
|
+
enforceDelete(collection: string, recordId: string, tx: Transaction, causalDeps: string[]): Promise<EnforcementResult>;
|
|
727
|
+
/**
|
|
728
|
+
* Enforce a single relation's onDelete policy.
|
|
729
|
+
*/
|
|
730
|
+
private enforceRelation;
|
|
731
|
+
/**
|
|
732
|
+
* Cascade: delete all records in the source collection that reference
|
|
733
|
+
* the deleted record, then recursively cascade those deletes.
|
|
734
|
+
*/
|
|
735
|
+
private enforceCascade;
|
|
736
|
+
/**
|
|
737
|
+
* Set-null: update all referencing records to set the foreign key to null.
|
|
738
|
+
*/
|
|
739
|
+
private enforceSetNull;
|
|
740
|
+
/**
|
|
741
|
+
* Restrict: refuse the delete if any referencing records exist.
|
|
742
|
+
*/
|
|
743
|
+
private enforceRestrict;
|
|
744
|
+
/**
|
|
745
|
+
* Get the relation lookup map for external use (e.g., by the merge engine).
|
|
746
|
+
*/
|
|
747
|
+
getRelationLookup(): Map<string, IncomingRelation[]>;
|
|
748
|
+
}
|
|
749
|
+
|
|
283
750
|
/**
|
|
284
751
|
* Callback invoked after a mutation so the Store can notify subscriptions.
|
|
285
752
|
*/
|
|
@@ -287,6 +754,9 @@ type MutationCallback = (collection: string, operation: Operation) => void;
|
|
|
287
754
|
/**
|
|
288
755
|
* Collection provides CRUD operations on a single schema collection.
|
|
289
756
|
* Each mutation creates an Operation and persists both the data and the operation atomically.
|
|
757
|
+
*
|
|
758
|
+
* When a RelationEnforcer is provided, delete operations enforce referential integrity
|
|
759
|
+
* policies (cascade, set-null, restrict) defined in the schema's relations.
|
|
290
760
|
*/
|
|
291
761
|
declare class Collection {
|
|
292
762
|
private readonly name;
|
|
@@ -297,7 +767,8 @@ declare class Collection {
|
|
|
297
767
|
private readonly nodeId;
|
|
298
768
|
private readonly getSequenceNumber;
|
|
299
769
|
private readonly onMutation;
|
|
300
|
-
|
|
770
|
+
private readonly relationEnforcer;
|
|
771
|
+
constructor(name: string, definition: CollectionDefinition, schema: SchemaDefinition, adapter: StorageAdapter, clock: HybridLogicalClock, nodeId: string, getSequenceNumber: () => number, onMutation: MutationCallback, relationEnforcer?: RelationEnforcer);
|
|
301
772
|
/**
|
|
302
773
|
* Insert a new record into the collection.
|
|
303
774
|
* Generates a UUID v7 for the id, validates data, and persists atomically.
|
|
@@ -322,8 +793,19 @@ declare class Collection {
|
|
|
322
793
|
/**
|
|
323
794
|
* Soft-delete a record by its ID.
|
|
324
795
|
*
|
|
796
|
+
* When a RelationEnforcer is configured, referential integrity policies
|
|
797
|
+
* are enforced within the same transaction:
|
|
798
|
+
* - **cascade**: Recursively deletes all referencing records
|
|
799
|
+
* - **set-null**: Sets foreign keys to null on referencing records
|
|
800
|
+
* - **restrict**: Throws ReferentialIntegrityError if references exist
|
|
801
|
+
* - **no-action**: Does nothing (dangling references are left)
|
|
802
|
+
*
|
|
803
|
+
* All cascaded operations are created atomically within the same transaction
|
|
804
|
+
* and share causal dependencies with the original delete.
|
|
805
|
+
*
|
|
325
806
|
* @param id - The record ID to delete
|
|
326
807
|
* @throws {RecordNotFoundError} If the record doesn't exist or is already deleted
|
|
808
|
+
* @throws {ReferentialIntegrityError} If a 'restrict' policy is violated
|
|
327
809
|
*/
|
|
328
810
|
delete(id: string): Promise<void>;
|
|
329
811
|
/** Get the collection name */
|
|
@@ -332,6 +814,60 @@ declare class Collection {
|
|
|
332
814
|
getDefinition(): CollectionDefinition;
|
|
333
815
|
}
|
|
334
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Error thrown when a local mutation attempts an invalid state transition.
|
|
819
|
+
*
|
|
820
|
+
* Contains enough context to debug without reproduction:
|
|
821
|
+
* the collection, record, field, current state, attempted state, and allowed transitions.
|
|
822
|
+
*/
|
|
823
|
+
declare class InvalidStateTransitionError extends KoraError {
|
|
824
|
+
readonly collection: string;
|
|
825
|
+
readonly recordId: string;
|
|
826
|
+
readonly field: string;
|
|
827
|
+
readonly fromState: string;
|
|
828
|
+
readonly toState: string;
|
|
829
|
+
readonly allowedStates: string[];
|
|
830
|
+
constructor(collection: string, recordId: string, field: string, fromState: string, toState: string, allowedStates: string[]);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Validates a state machine transition for a local mutation (insert or update).
|
|
834
|
+
*
|
|
835
|
+
* For inserts: validates that the initial state (from the data or the default value)
|
|
836
|
+
* is a known state in the state machine.
|
|
837
|
+
*
|
|
838
|
+
* For updates: looks up the current state field value and checks whether
|
|
839
|
+
* the transition to the new value is allowed.
|
|
840
|
+
*
|
|
841
|
+
* @param collectionName - Name of the collection
|
|
842
|
+
* @param recordId - The record being mutated
|
|
843
|
+
* @param stateMachine - The state machine definition
|
|
844
|
+
* @param currentState - The current value of the state field (null for inserts)
|
|
845
|
+
* @param newState - The new value being set for the state field
|
|
846
|
+
* @returns An object indicating whether the transition is valid, and if not, the allowed states.
|
|
847
|
+
* When `onInvalidTransition` is 'last-valid-state', callers should suppress the field update
|
|
848
|
+
* rather than throwing.
|
|
849
|
+
*/
|
|
850
|
+
declare function validateStateTransition(collectionName: string, recordId: string, stateMachine: StateMachineDefinition, currentState: string | null, newState: string): {
|
|
851
|
+
valid: boolean;
|
|
852
|
+
allowedStates: string[];
|
|
853
|
+
};
|
|
854
|
+
/**
|
|
855
|
+
* Checks whether a collection update data object contains a change to the state machine field,
|
|
856
|
+
* and if so, validates the transition.
|
|
857
|
+
*
|
|
858
|
+
* If the state field is not in the update data, returns the data unchanged.
|
|
859
|
+
* If the transition is invalid and mode is 'last-valid-state', removes the field from the data.
|
|
860
|
+
* If the transition is invalid and mode is 'reject', throws InvalidStateTransitionError.
|
|
861
|
+
*
|
|
862
|
+
* @param collectionName - Name of the collection
|
|
863
|
+
* @param recordId - The record being updated
|
|
864
|
+
* @param collectionDef - The collection definition from the schema
|
|
865
|
+
* @param currentRecord - The current record data (must include the state field)
|
|
866
|
+
* @param updateData - The partial update data
|
|
867
|
+
* @returns The update data, potentially with the state field removed if invalid and mode is 'last-valid-state'
|
|
868
|
+
*/
|
|
869
|
+
declare function validateUpdateStateMachine(collectionName: string, recordId: string, collectionDef: CollectionDefinition, currentRecord: Record<string, unknown>, updateData: Record<string, unknown>): Record<string, unknown>;
|
|
870
|
+
|
|
335
871
|
type RichtextInput = string | Uint8Array | ArrayBuffer | null | undefined;
|
|
336
872
|
/**
|
|
337
873
|
* Encodes richtext values into Yjs document updates.
|
|
@@ -373,4 +909,4 @@ declare function pluralize(word: string): string;
|
|
|
373
909
|
*/
|
|
374
910
|
declare function singularize(word: string): string;
|
|
375
911
|
|
|
376
|
-
export { AdapterError, ApplyResult, Collection, type CollectionAccessor, CollectionRecord, OrderByDirection, PersistenceError, QueryBuilder, QueryDescriptor, QueryError, RecordNotFoundError, StorageAdapter, Store, StoreConfig, StoreNotOpenError, SubscriptionCallback, SubscriptionManager, WhereClause, WorkerInitError, WorkerTimeoutError, decodeRichtext, encodeRichtext, pluralize, richtextToPlainText, singularize };
|
|
912
|
+
export { AdapterError, ApplyResult, Collection, type CollectionAccessor, CollectionRecord, InvalidStateTransitionError, OrderByDirection, PersistenceError, QueryBuilder, QueryDescriptor, QueryError, RecordNotFoundError, SequenceManager, StorageAdapter, Store, StoreConfig, StoreNotOpenError, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, Transaction, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, WhereClause, WorkerInitError, WorkerTimeoutError, decodeRichtext, encodeRichtext, pluralize, richtextToPlainText, singularize, validateStateTransition, validateUpdateStateMachine };
|