@korajs/store 0.3.2 → 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/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, T as Transaction, d as WhereOperators } from './types-DF-KDSK1.cjs';
3
- import { KoraError, Operation, CollectionDefinition, SchemaDefinition, OperationLog, VersionVector, HybridLogicalClock } from '@korajs/core';
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
- * Manages reactive subscriptions. When a mutation occurs on a collection,
50
- * affected subscriptions are re-evaluated in a microtask batch and callbacks
51
- * are invoked only if results actually changed.
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
- constructor(name: string, definition: CollectionDefinition, schema: SchemaDefinition, adapter: StorageAdapter, clock: HybridLogicalClock, nodeId: string, getSequenceNumber: () => number, onMutation: MutationCallback);
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 };