@korajs/store 0.3.2 → 0.5.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.
Files changed (73) hide show
  1. package/dist/adapters/better-sqlite3.cjs +1 -1
  2. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  3. package/dist/adapters/better-sqlite3.d.cts +1 -1
  4. package/dist/adapters/better-sqlite3.d.ts +1 -1
  5. package/dist/adapters/better-sqlite3.js +1 -1
  6. package/dist/adapters/better-sqlite3.js.map +1 -1
  7. package/dist/adapters/indexeddb.cjs +413 -18
  8. package/dist/adapters/indexeddb.cjs.map +1 -1
  9. package/dist/adapters/indexeddb.d.cts +23 -5
  10. package/dist/adapters/indexeddb.d.ts +23 -5
  11. package/dist/adapters/indexeddb.js +166 -19
  12. package/dist/adapters/indexeddb.js.map +1 -1
  13. package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
  14. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
  15. package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
  16. package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
  17. package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
  18. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
  19. package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
  20. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  21. package/dist/adapters/sqlite-wasm-worker.js +34 -5
  22. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  23. package/dist/adapters/sqlite-wasm.cjs +249 -2
  24. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  25. package/dist/adapters/sqlite-wasm.d.cts +15 -3
  26. package/dist/adapters/sqlite-wasm.d.ts +15 -3
  27. package/dist/adapters/sqlite-wasm.js +2 -2
  28. package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
  29. package/dist/backup-JN3LB4YT.js +14 -0
  30. package/dist/backup-JN3LB4YT.js.map +1 -0
  31. package/dist/chunk-56LL4XP6.js +205 -0
  32. package/dist/chunk-56LL4XP6.js.map +1 -0
  33. package/dist/chunk-6AHYGQQT.js +425 -0
  34. package/dist/chunk-6AHYGQQT.js.map +1 -0
  35. package/dist/chunk-7BPETY5Y.js +570 -0
  36. package/dist/chunk-7BPETY5Y.js.map +1 -0
  37. package/dist/chunk-AGN3CIPB.js +188 -0
  38. package/dist/chunk-AGN3CIPB.js.map +1 -0
  39. package/dist/chunk-EZSTD2XC.js +380 -0
  40. package/dist/chunk-EZSTD2XC.js.map +1 -0
  41. package/dist/chunk-MYSJN3DF.js +117 -0
  42. package/dist/chunk-MYSJN3DF.js.map +1 -0
  43. package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
  44. package/dist/chunk-XO4LMPAH.js.map +1 -0
  45. package/dist/export-audit-5GV6VRDZ.js +15 -0
  46. package/dist/export-audit-5GV6VRDZ.js.map +1 -0
  47. package/dist/index.cjs +3351 -510
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +822 -50
  50. package/dist/index.d.ts +822 -50
  51. package/dist/index.js +1976 -558
  52. package/dist/index.js.map +1 -1
  53. package/dist/internal.cjs +789 -0
  54. package/dist/internal.cjs.map +1 -0
  55. package/dist/internal.d.cts +154 -0
  56. package/dist/internal.d.ts +154 -0
  57. package/dist/internal.js +48 -0
  58. package/dist/internal.js.map +1 -0
  59. package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
  60. package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
  61. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
  62. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
  63. package/dist/types-BMyHmwvn.d.ts +163 -0
  64. package/dist/types-CTr00g_d.d.cts +163 -0
  65. package/dist/types-DepnRDk_.d.cts +212 -0
  66. package/dist/types-DepnRDk_.d.ts +212 -0
  67. package/package.json +23 -2
  68. package/dist/chunk-DXKLAQ6P.js.map +0 -1
  69. package/dist/chunk-ZP5AXQ3Z.js +0 -179
  70. package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
  71. package/dist/types-DF-KDSK1.d.cts +0 -106
  72. package/dist/types-DF-KDSK1.d.ts +0 -106
  73. /package/dist/{sqlite-wasm-channel-46AOWNPM.js.map → audit-trace-store-AM4EWQNZ.js.map} +0 -0
package/dist/index.d.cts CHANGED
@@ -1,6 +1,24 @@
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 { C as CollectionRecord, S as StorageAdapter, Q as QueryDescriptor, c as SubscriptionCallback, W as WhereClause, d as OrderByDirection, L as LocalMutationHandler, e as StoreConfig, A as ApplyRemoteOptions, f as MaterializedRowSnapshot } from './types-DepnRDk_.cjs';
2
+ export { M as MigrationPlan, g as OrderByClause, h as StoreIsolation, T as Transaction, i as TransactionBufferedEntry, j as TransactionCommitBatch, k as TransactionCommitResult, l as WhereOperators } from './types-DepnRDk_.cjs';
3
+ import { Operation, KoraError, VersionVector, SchemaDefinition, MergeTrace, CollectionDefinition, SequenceConfig, HybridLogicalClock, CausalTracker, OperationLog, ApplyResult, StateMachineDefinition, KoraEvent } from '@korajs/core';
4
+ export { ApplyResult } from '@korajs/core';
5
+ import { R as RelationEnforcer, L as LocalMutationContext } from './types-CTr00g_d.cjs';
6
+ export { C as Collection } from './types-CTr00g_d.cjs';
7
+
8
+ /**
9
+ * In-memory materialized state at a causal cut in the operation log.
10
+ * Read-only — does not mutate the live store.
11
+ */
12
+ interface ReplaySnapshot {
13
+ /** The operation whose causal past was replayed (inclusive). */
14
+ targetOperation: Operation;
15
+ /** Operations applied in causal order (ancestors + target). */
16
+ operationsApplied: Operation[];
17
+ /** Non-deleted records per collection after replay. */
18
+ collections: Record<string, CollectionRecord[]>;
19
+ /** Look up a single record at the replay cut. Returns null if deleted or missing. */
20
+ findRecord(collection: string, recordId: string): CollectionRecord | null;
21
+ }
4
22
 
5
23
  /**
6
24
  * Thrown when a query is invalid (bad field names, invalid operators, etc.).
@@ -46,14 +64,279 @@ declare class PersistenceError extends KoraError {
46
64
  }
47
65
 
48
66
  /**
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.
67
+ * How the local operation log may be compacted.
68
+ * Materialized collection rows are always kept as the convergence baseline.
69
+ */
70
+ type CompactionStrategy = {
71
+ mode: 'never';
72
+ } | {
73
+ mode: 'after-ack';
74
+ /** Server version vector; defaults to persisted last-acked vector on the store. */
75
+ serverVector?: VersionVector;
76
+ } | {
77
+ mode: 'after-days';
78
+ /** Delete only ops older than this many days (still requires ack watermark). */
79
+ days: number;
80
+ serverVector?: VersionVector;
81
+ };
82
+ /**
83
+ * Result of a compaction run.
84
+ */
85
+ interface CompactionResult {
86
+ /** Number of operation log rows removed. */
87
+ deletedCount: number;
88
+ /** Per-node sequence ceiling used for deletion (from server ack vector). */
89
+ watermark: VersionVector;
90
+ }
91
+ declare const COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
92
+
93
+ /**
94
+ * Compute per-node sequence watermarks safe to compact (server has acknowledged these ops).
95
+ */
96
+ declare function computeAckCompactionWatermark(serverVector: VersionVector): VersionVector;
97
+ /**
98
+ * Remove acknowledged, materialized operation log entries from `_kora_ops_*` tables.
99
+ * Does not modify collection rows — they are the compaction baseline snapshot.
100
+ */
101
+ declare function compactOperationLog(adapter: StorageAdapter, schema: SchemaDefinition, strategy: CompactionStrategy, serverVector: VersionVector): Promise<CompactionResult>;
102
+
103
+ declare const LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
104
+ /**
105
+ * Serialize a version vector for `_kora_meta` storage.
106
+ */
107
+ declare function serializeVersionVectorToMeta(vector: VersionVector): string;
108
+ /**
109
+ * Deserialize a version vector from `_kora_meta`.
110
+ */
111
+ declare function deserializeVersionVectorFromMeta(value: string): VersionVector;
112
+ /**
113
+ * Merge two version vectors, keeping the maximum sequence per node.
114
+ */
115
+ declare function mergeVersionVectors(a: VersionVector, b: VersionVector): VersionVector;
116
+ /**
117
+ * Operations present locally but not yet on the server (per server version vector).
118
+ */
119
+ declare function collectOperationsAheadOfServer(localVector: VersionVector, serverVector: VersionVector, fetchRange: (nodeId: string, fromSeq: number, toSeq: number) => Promise<Operation[]>): Promise<Operation[]>;
120
+
121
+ /**
122
+ * A merge or constraint decision persisted for enterprise audit export.
123
+ */
124
+ interface PersistedAuditTrace {
125
+ /** Unique row id (UUID v7). */
126
+ id: string;
127
+ /** Wall-clock time when the trace was recorded (epoch ms). */
128
+ recordedAt: number;
129
+ /** Source event type from the Kora event bus. */
130
+ eventType: 'merge:completed' | 'merge:conflict' | 'constraint:violated';
131
+ /** Constraint name when {@link eventType} is `constraint:violated`. */
132
+ constraint?: string;
133
+ /** Full merge trace including conflicting operations and resolution details. */
134
+ trace: MergeTrace;
135
+ }
136
+ /**
137
+ * Manifest embedded in audit export files.
138
+ */
139
+ interface AuditExportManifest {
140
+ /** Audit export format version */
141
+ version: 1;
142
+ /** When the export was created (epoch ms) */
143
+ exportedAt: number;
144
+ /** Node ID of the originating device */
145
+ nodeId: string;
146
+ /** Schema version at export time */
147
+ schemaVersion: number;
148
+ /** Number of operations included */
149
+ operationCount: number;
150
+ /** Number of merge traces included */
151
+ mergeTraceCount: number;
152
+ /** SHA-256 hex checksum of content sections */
153
+ checksum: string;
154
+ }
155
+ /**
156
+ * Decoded audit export payload.
157
+ */
158
+ interface AuditExportPayload {
159
+ manifest: AuditExportManifest;
160
+ operations: Operation[];
161
+ mergeTraces: PersistedAuditTrace[];
162
+ }
163
+ /**
164
+ * Progress reported during audit export.
165
+ */
166
+ interface AuditExportProgress {
167
+ phase: 'reading' | 'writing' | 'verifying';
168
+ /** 0-1 progress ratio */
169
+ progress: number;
170
+ message: string;
171
+ }
172
+ /**
173
+ * Options for {@link exportAudit}.
174
+ */
175
+ interface AuditExportOptions {
176
+ /** Subset of collections for operations and traces. All if omitted. */
177
+ collections?: string[];
178
+ /** Include only traces recorded at or after this timestamp (epoch ms). */
179
+ since?: number;
180
+ /** Include only traces recorded at or before this timestamp (epoch ms). */
181
+ until?: number;
182
+ /** Progress callback */
183
+ onProgress?: (progress: AuditExportProgress) => void;
184
+ }
185
+ /**
186
+ * Query filters for reading persisted audit traces.
187
+ */
188
+ interface AuditTraceQuery {
189
+ collections?: string[];
190
+ since?: number;
191
+ until?: number;
192
+ }
193
+
194
+ /**
195
+ * Manifest embedded in every backup file.
196
+ */
197
+ interface BackupManifest {
198
+ /** Backup format version */
199
+ version: 1;
200
+ /** When the backup was created (epoch ms) */
201
+ createdAt: number;
202
+ /** Node ID of the originating device */
203
+ nodeId: string;
204
+ /** Schema version at backup time */
205
+ schemaVersion: number;
206
+ /** Total number of operations in the backup */
207
+ operationCount: number;
208
+ /** Collection names included in the backup */
209
+ collections: string[];
210
+ /** Whether materialized records are included */
211
+ includesRecords: boolean;
212
+ /** SHA-256 hex checksum of all content sections */
213
+ checksum: string;
214
+ }
215
+ /**
216
+ * Progress reported during backup/restore operations.
217
+ */
218
+ interface BackupProgress {
219
+ phase: 'reading' | 'writing' | 'verifying' | 'restoring';
220
+ /** 0-1 progress ratio */
221
+ progress: number;
222
+ message: string;
223
+ }
224
+ /**
225
+ * Result of a restore operation.
226
+ */
227
+ interface RestoreResult {
228
+ /** Number of operations restored */
229
+ operationsRestored: number;
230
+ /** Number of records restored */
231
+ recordsRestored: number;
232
+ /** Whether the restore completed successfully */
233
+ success: boolean;
234
+ /** Error message if failed */
235
+ error?: string;
236
+ /** Duration in ms */
237
+ duration: number;
238
+ }
239
+ /**
240
+ * Options for exporting a backup.
241
+ */
242
+ interface BackupOptions {
243
+ /** Include materialized record snapshots (default: true) */
244
+ includeRecords?: boolean;
245
+ /** Subset of collections to backup. All if omitted. */
246
+ collections?: string[];
247
+ /** Progress callback */
248
+ onProgress?: (progress: BackupProgress) => void;
249
+ }
250
+ /**
251
+ * Options for restoring from a backup.
252
+ */
253
+ interface RestoreOptions {
254
+ /** Subset of collections to restore. All if omitted. */
255
+ collections?: string[];
256
+ /** Progress callback */
257
+ onProgress?: (progress: BackupProgress) => void;
258
+ /**
259
+ * If true, merge operations with existing data (replay through applyRemoteOperation).
260
+ * If false, clear and replace all data (default: false).
261
+ */
262
+ merge?: boolean;
263
+ }
264
+
265
+ /**
266
+ * Configuration options for the SubscriptionManager.
267
+ */
268
+ interface SubscriptionManagerOptions {
269
+ /**
270
+ * Minimum number of subscriptions before activating bloom filter.
271
+ * Below this threshold, linear scanning is used (bloom filter overhead not worth it).
272
+ * @default 100
273
+ */
274
+ bloomThreshold?: number;
275
+ /**
276
+ * Expected number of unique collection+field dependencies for bloom filter sizing.
277
+ * @default 500
278
+ */
279
+ bloomExpectedItems?: number;
280
+ /**
281
+ * Target false positive rate for the bloom filter.
282
+ * Lower values require more memory but reduce unnecessary precise checks.
283
+ * @default 0.01
284
+ */
285
+ bloomFalsePositiveRate?: number;
286
+ /** Called when a query subscription is registered (e.g. to register sync query subsets). */
287
+ onQuerySubscribed?: (descriptor: QueryDescriptor) => () => void;
288
+ }
289
+ /**
290
+ * Performance statistics for monitoring subscription checking efficiency.
291
+ */
292
+ interface SubscriptionStats {
293
+ /** Total number of mutation notifications processed */
294
+ totalChecks: number;
295
+ /** Number of times bloom filter said "maybe" (proceeded to precise check) */
296
+ bloomFilterHits: number;
297
+ /** Number of times bloom filter said "definitely not" (skipped all subscriptions) */
298
+ bloomFilterMisses: number;
299
+ /** Number of times bloom filter said "maybe" but precise check found no match */
300
+ falsePositives: number;
301
+ /** Average time per check in milliseconds */
302
+ averageCheckTimeMs: number;
303
+ /** Whether bloom filter is currently active */
304
+ bloomFilterActive: boolean;
305
+ /** Current subscription count */
306
+ subscriptionCount: number;
307
+ }
308
+ /**
309
+ * Manages reactive subscriptions with two-level dependency checking.
310
+ *
311
+ * When a mutation occurs on a collection, affected subscriptions are re-evaluated
312
+ * in a microtask batch and callbacks are invoked only if results actually changed.
313
+ *
314
+ * For large subscription counts (>= bloomThreshold), a bloom filter provides O(k)
315
+ * pre-filtering to avoid scanning all subscriptions on every mutation:
316
+ *
317
+ * Level 1 (Bloom filter): Fast O(k) check -- does this mutation potentially affect
318
+ * any subscription? If NO: skip all subscriptions (guaranteed correct).
319
+ * If MAYBE: proceed to Level 2.
320
+ *
321
+ * Level 2 (Precise check): Only evaluate subscriptions that match the mutated
322
+ * collection, including included (related) collection tracking.
52
323
  */
53
324
  declare class SubscriptionManager {
54
325
  private subscriptions;
55
326
  private pendingCollections;
56
327
  private flushScheduled;
328
+ private readonly onQuerySubscribed?;
329
+ private bloomFilter;
330
+ private bloomDirty;
331
+ private readonly bloomThreshold;
332
+ private readonly bloomExpectedItems;
333
+ private readonly bloomFalsePositiveRate;
334
+ private totalChecks;
335
+ private bloomFilterHits;
336
+ private bloomFilterMisses;
337
+ private falsePositives;
338
+ private totalCheckTimeMs;
339
+ constructor(options?: SubscriptionManagerOptions);
57
340
  /**
58
341
  * Register a new subscription.
59
342
  *
@@ -87,10 +370,38 @@ declare class SubscriptionManager {
87
370
  clear(): void;
88
371
  /** Number of active subscriptions (for testing/debugging) */
89
372
  get size(): number;
373
+ /**
374
+ * Get performance statistics for monitoring subscription checking efficiency.
375
+ * Useful for DevTools integration and performance tuning.
376
+ */
377
+ getStats(): SubscriptionStats;
378
+ /**
379
+ * Check if bloom filter is currently active.
380
+ * Active when subscription count meets or exceeds the threshold.
381
+ */
382
+ isBloomActive(): boolean;
383
+ /**
384
+ * Find subscriptions affected by mutations to the given collections.
385
+ * Uses two-level checking when bloom filter is active:
386
+ *
387
+ * Level 1: Bloom filter pre-check -- if no subscription depends on any
388
+ * of the mutated collections, skip everything (O(k) per collection).
389
+ *
390
+ * Level 2: Precise check -- linear scan of subscriptions, matching
391
+ * against the mutated collections.
392
+ */
393
+ private findAffectedSubscriptions;
394
+ /**
395
+ * Rebuild the bloom filter from all current subscriptions.
396
+ * Adds collection-level dependencies for every subscription, plus
397
+ * any included collection dependencies.
398
+ */
399
+ private rebuildBloomFilter;
90
400
  private scheduleFlush;
91
401
  /**
92
- * Compare two result sets. Uses length check + JSON comparison as pragmatic approach.
93
- * Sufficient for typical query results. Can be optimized to id-based diffing if profiling shows need.
402
+ * Compare two result sets. Uses ID-based comparison first (same length, same IDs),
403
+ * then deep comparison of remaining fields when IDs match.
404
+ * Avoids JSON.stringify which is O(n) and fragile to field ordering differences.
94
405
  */
95
406
  private resultsEqual;
96
407
  }
@@ -192,6 +503,162 @@ declare class QueryBuilder<T = CollectionRecord> {
192
503
  private findRelation;
193
504
  }
194
505
 
506
+ /**
507
+ * Manages offline-safe sequences backed by a `_kora_sequences` table.
508
+ *
509
+ * Each sequence counter is scoped by (name, scope, nodeId), ensuring
510
+ * that different devices never collide. The counter is atomically
511
+ * incremented within a database transaction.
512
+ *
513
+ * @example
514
+ * ```typescript
515
+ * const mgr = new SequenceManager(adapter, 'node-abc')
516
+ *
517
+ * const receipt = await mgr.next('receipt', {
518
+ * scope: 'store-1',
519
+ * format: 'S-{date}-{node4}-{seq}',
520
+ * })
521
+ * // → "S-20260508-node-0001"
522
+ * ```
523
+ */
524
+ declare class SequenceManager {
525
+ private readonly adapter;
526
+ private readonly nodeId;
527
+ constructor(adapter: StorageAdapter, nodeId: string);
528
+ /**
529
+ * Get the next value in a sequence, atomically incrementing the counter.
530
+ *
531
+ * @param name - The sequence name (e.g., 'receipt', 'invoice')
532
+ * @param config - Optional configuration for scope, format, and starting value
533
+ * @returns The formatted sequence value
534
+ */
535
+ next(name: string, config?: SequenceConfig): Promise<string>;
536
+ /**
537
+ * Get the current counter value without incrementing.
538
+ *
539
+ * @param name - The sequence name
540
+ * @param config - Optional scope
541
+ * @returns The current counter value, or 0 if the sequence has never been used
542
+ */
543
+ current(name: string, config?: {
544
+ scope?: string;
545
+ }): Promise<number>;
546
+ /**
547
+ * Reset a sequence counter.
548
+ *
549
+ * @param name - The sequence name
550
+ * @param config - Optional scope and target value
551
+ */
552
+ reset(name: string, config?: {
553
+ scope?: string;
554
+ to?: number;
555
+ }): Promise<void>;
556
+ }
557
+
558
+ /**
559
+ * Allocates monotonic sequence numbers for a single open transaction.
560
+ * Loads the current DB watermark once, then increments in memory until commit.
561
+ */
562
+ declare class TransactionSequenceAllocator {
563
+ private readonly adapter;
564
+ private readonly nodeId;
565
+ private loaded;
566
+ private watermark;
567
+ constructor(adapter: StorageAdapter, nodeId: string);
568
+ allocate(): Promise<number>;
569
+ getHighWaterMark(): number;
570
+ }
571
+
572
+ /**
573
+ * Internal configuration for creating a TransactionContext.
574
+ * Passed from Store to avoid exposing Store internals publicly.
575
+ */
576
+ interface TransactionContextConfig {
577
+ schema: SchemaDefinition;
578
+ adapter: StorageAdapter;
579
+ clock: HybridLogicalClock;
580
+ nodeId: string;
581
+ sequenceAllocator: TransactionSequenceAllocator;
582
+ relationEnforcer: RelationEnforcer | null;
583
+ causalTracker: CausalTracker | null;
584
+ localMutationHandler: LocalMutationHandler | null;
585
+ }
586
+ /**
587
+ * A collection accessor within a transaction.
588
+ * Operations are buffered and committed atomically when the transaction completes.
589
+ */
590
+ interface TransactionCollectionAccessor {
591
+ insert(data: Record<string, unknown>): Promise<CollectionRecord>;
592
+ update(id: string, data: Record<string, unknown>): Promise<CollectionRecord>;
593
+ delete(id: string): Promise<void>;
594
+ findById(id: string): Promise<CollectionRecord | null>;
595
+ }
596
+ /**
597
+ * TransactionContext provides atomic multi-collection operations.
598
+ *
599
+ * All mutations are buffered and committed in a single StorageAdapter.transaction()
600
+ * call. All operations share the same transactionId (UUID v7).
601
+ *
602
+ * Subscription notifications are deferred until after commit.
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * const { operations, affectedCollections } = await txContext.commit()
607
+ * // Notify subscriptions after commit
608
+ * for (const op of operations) {
609
+ * subscriptionManager.notify(op.collection, op)
610
+ * }
611
+ * ```
612
+ */
613
+ declare class TransactionContext {
614
+ private readonly transactionId;
615
+ private mutationName;
616
+ private readonly buffer;
617
+ private committed;
618
+ private rolledBack;
619
+ private readonly config;
620
+ constructor(config: TransactionContextConfig);
621
+ /**
622
+ * Set a human-readable mutation name for this transaction.
623
+ * Propagated to all operations for DevTools display.
624
+ */
625
+ setMutationName(name: string): void;
626
+ /**
627
+ * Get the mutation name, if set.
628
+ */
629
+ getMutationName(): string | undefined;
630
+ /**
631
+ * Get a collection accessor for buffered operations within this transaction.
632
+ */
633
+ collection(name: string): TransactionCollectionAccessor;
634
+ /**
635
+ * Commit all buffered operations atomically.
636
+ * Returns the list of operations and affected collections for subscription notification.
637
+ */
638
+ commit(): Promise<{
639
+ operations: Operation[];
640
+ affectedCollections: Set<string>;
641
+ }>;
642
+ /**
643
+ * Mark the transaction as rolled back. No operations will be committed.
644
+ */
645
+ rollback(): void;
646
+ /**
647
+ * Get the transaction ID shared by all operations in this transaction.
648
+ */
649
+ getTransactionId(): string;
650
+ private ensureActive;
651
+ private insert;
652
+ private update;
653
+ private deleteRecord;
654
+ private findById;
655
+ /**
656
+ * Get the effective state of a record, considering both the database and buffered operations.
657
+ * Buffered inserts/updates take precedence over the database state.
658
+ */
659
+ private getEffectiveRecord;
660
+ }
661
+
195
662
  /**
196
663
  * Store is the main orchestrator. It owns a schema, a storage adapter,
197
664
  * a clock, and a subscription manager. It creates Collection instances
@@ -214,10 +681,16 @@ declare class Store implements OperationLog {
214
681
  private clock;
215
682
  private collections;
216
683
  private subscriptionManager;
684
+ private sequenceManager;
217
685
  private readonly schema;
218
686
  private readonly adapter;
219
687
  private readonly configNodeId;
688
+ private readonly dbName;
689
+ private readonly isolation;
220
690
  private readonly emitter;
691
+ private localMutationHandler;
692
+ private relationEnforcer;
693
+ private causalTracker;
221
694
  constructor(config: StoreConfig);
222
695
  /**
223
696
  * Open the store: initialize the database, load or generate a node ID,
@@ -247,23 +720,166 @@ declare class Store implements OperationLog {
247
720
  * Checks for duplicates, applies to the data table, persists the operation,
248
721
  * and updates the version vector.
249
722
  */
250
- applyRemoteOperation(op: Operation): Promise<ApplyResult>;
723
+ applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
251
724
  /**
252
725
  * Get operations from a node within a sequence number range.
253
726
  * Implements the OperationLog interface for computeDelta.
254
727
  */
255
- getRange(nodeId: string, fromSeq: number, toSeq: number): Operation[];
728
+ getRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
256
729
  /**
257
- * Async version of getRange for use by the sync layer.
730
+ * Get operations from a node within a sequence number range.
258
731
  */
259
732
  getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
733
+ /**
734
+ * Load every operation from the local append-only log across all collections.
735
+ * Used by sync delta computation, backup export, and time-travel replay.
736
+ */
737
+ getAllOperations(): Promise<Operation[]>;
738
+ /**
739
+ * Rebuild an in-memory snapshot of materialized state at a causal cut in the op log.
740
+ * Does not mutate the live store — intended for DevTools time-travel inspection.
741
+ *
742
+ * @param operationId - Content-addressed id of the operation to replay through (inclusive)
743
+ * @throws {OperationError} When the operation id is not present in the local log
744
+ */
745
+ replayTo(operationId: string): Promise<ReplaySnapshot>;
746
+ /**
747
+ * Persist a merge trace to the durable audit log.
748
+ */
749
+ appendAuditTrace(trace: PersistedAuditTrace): Promise<void>;
750
+ /**
751
+ * Read persisted audit traces with optional filters.
752
+ */
753
+ getAuditTraces(query?: AuditTraceQuery): Promise<PersistedAuditTrace[]>;
754
+ /**
755
+ * Export operations and merge traces as a portable audit bundle.
756
+ */
757
+ exportAudit(options?: AuditExportOptions): Promise<Uint8Array>;
260
758
  /**
261
759
  * Get the schema definition.
262
760
  */
263
761
  getSchema(): SchemaDefinition;
762
+ /**
763
+ * Route local CRUD through the unified apply pipeline (korajs ApplyPipeline).
764
+ */
765
+ setLocalMutationHandler(handler: LocalMutationHandler | null): void;
766
+ /**
767
+ * Build mutation context for a collection (used by ApplyPipeline side effects).
768
+ */
769
+ createMutationContext(collection: string, options?: {
770
+ inTransaction?: boolean;
771
+ extraCausalDeps?: string[];
772
+ }): LocalMutationContext;
773
+ /**
774
+ * Load a materialized row by ID, including soft-deleted tombstones.
775
+ */
776
+ findMaterializedRow(collection: string, recordId: string): Promise<MaterializedRowSnapshot | null>;
777
+ /**
778
+ * Latest operation from this device for a record (used for delete-vs-update merge on sync).
779
+ */
780
+ /**
781
+ * Load the last server version vector acknowledged by this client (persisted in `_kora_meta`).
782
+ */
783
+ loadLastAckedServerVector(): Promise<VersionVector>;
784
+ /**
785
+ * Persist the last server version vector this client believes the server has applied.
786
+ */
787
+ saveLastAckedServerVector(vector: VersionVector): Promise<void>;
788
+ /**
789
+ * Load persisted delta cursor for resuming paginated initial sync.
790
+ */
791
+ loadDeltaCursor(): Promise<string | null>;
792
+ /**
793
+ * Persist or clear the delta cursor for paginated initial sync resume.
794
+ */
795
+ saveDeltaCursor(cursor: string | null): Promise<void>;
796
+ /**
797
+ * Local operations not yet reflected on the server version vector.
798
+ */
799
+ getUnsyncedOperations(serverVector: VersionVector): Promise<Operation[]>;
800
+ /**
801
+ * Count of local operations ahead of the server version vector.
802
+ */
803
+ countUnsyncedOperations(serverVector: VersionVector): Promise<number>;
804
+ /**
805
+ * Compact the local operation log using materialized rows as the baseline.
806
+ * Only removes ops the server has acknowledged (per {@link CompactionStrategy}).
807
+ */
808
+ compact(strategy: CompactionStrategy): Promise<CompactionResult>;
809
+ /**
810
+ * Merge session remote vector with persisted last-acked vector (max per node).
811
+ */
812
+ mergeServerVectors(sessionVector: VersionVector, persistedVector: VersionVector): VersionVector;
813
+ getLatestLocalOperationForRecord(collection: string, recordId: string): Promise<Operation | null>;
814
+ /**
815
+ * Latest operation for a record from any node (for 3-way merge when local op log is empty).
816
+ */
817
+ getLatestOperationForRecord(collection: string, recordId: string): Promise<Operation | null>;
264
818
  /** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
265
819
  getSubscriptionManager(): SubscriptionManager;
266
- private nextSequenceNumber;
820
+ /**
821
+ * Get the sequence manager for offline-safe sequence generation.
822
+ * @throws {StoreNotOpenError} If the store is not open
823
+ */
824
+ getSequenceManager(): SequenceManager;
825
+ /**
826
+ * Create a TransactionContext for atomic multi-collection operations.
827
+ * The returned context buffers all mutations and commits them atomically.
828
+ *
829
+ * After commit, the caller is responsible for notifying subscriptions
830
+ * and emitting events for each operation.
831
+ */
832
+ createTransaction(): TransactionContext;
833
+ /**
834
+ * Execute a function within a transaction. All mutations performed on the
835
+ * TransactionContext are committed atomically. Subscription notifications
836
+ * are batched and fired after the commit.
837
+ *
838
+ * If the function throws, the transaction is rolled back and the error is re-thrown.
839
+ *
840
+ * @param fn - Function receiving a TransactionContext for buffered operations
841
+ * @returns The operations that were committed
842
+ */
843
+ transaction(fn: (tx: TransactionContext) => Promise<void>): Promise<Operation[]>;
844
+ /**
845
+ * Export all data as a portable backup binary.
846
+ * Includes operations, version vector, metadata, and optionally materialized records.
847
+ *
848
+ * @param options - Backup options (includeRecords, collections, onProgress)
849
+ * @returns Backup as a Uint8Array
850
+ */
851
+ exportBackup(options?: BackupOptions): Promise<Uint8Array>;
852
+ /**
853
+ * Restore data from a backup binary.
854
+ *
855
+ * @param data - The backup data
856
+ * @param options - Restore options (merge, collections, onProgress)
857
+ * @returns Result of the restore operation
858
+ */
859
+ importBackup(data: Uint8Array, options?: RestoreOptions): Promise<RestoreResult>;
860
+ /**
861
+ * Read backup manifest without loading the entire backup.
862
+ *
863
+ * @param data - The raw backup data
864
+ * @returns The backup manifest
865
+ */
866
+ static readBackupManifest(data: Uint8Array): BackupManifest;
867
+ private recordOperationSequence;
868
+ private allocateSequenceNumber;
869
+ /**
870
+ * Check the stored schema version and run any pending migrations.
871
+ * Migrations are applied in version order within a transaction.
872
+ */
873
+ private runMigrationsIfNeeded;
874
+ /**
875
+ * Get the stored schema version from _kora_meta. Returns 0 if not set.
876
+ */
877
+ private getStoredSchemaVersion;
878
+ /**
879
+ * Run a backfill transform on all records in a collection.
880
+ * Reads all rows, applies the transform, and updates changed fields.
881
+ */
882
+ private runBackfill;
267
883
  private loadOrGenerateNodeId;
268
884
  private loadSequenceNumber;
269
885
  private loadVersionVector;
@@ -281,56 +897,148 @@ interface CollectionAccessor {
281
897
  }
282
898
 
283
899
  /**
284
- * Callback invoked after a mutation so the Store can notify subscriptions.
285
- */
286
- type MutationCallback = (collection: string, operation: Operation) => void;
287
- /**
288
- * Collection provides CRUD operations on a single schema collection.
289
- * Each mutation creates an Operation and persists both the data and the operation atomically.
900
+ * Bloom filter for subscription dependency tracking.
901
+ *
902
+ * Each subscription adds its watched collection (and optionally fields) to the
903
+ * filter. When a mutation arrives, we check "collection" and "collection:field"
904
+ * keys against the filter. A negative result means no subscription cares about
905
+ * this mutation, so we can skip the expensive per-subscription scan.
906
+ *
907
+ * @example
908
+ * ```typescript
909
+ * const filter = new SubscriptionBloomFilter(100, 0.01)
910
+ * filter.add('todos')
911
+ * filter.add('todos', 'completed')
912
+ *
913
+ * filter.mightContain('todos') // true (definitely added)
914
+ * filter.mightContain('projects') // false (definitely not added)
915
+ * filter.mightContain('todos', 'completed') // true (definitely added)
916
+ * ```
290
917
  */
291
- declare class Collection {
292
- private readonly name;
293
- private readonly definition;
294
- private readonly schema;
295
- private readonly adapter;
296
- private readonly clock;
297
- private readonly nodeId;
298
- private readonly getSequenceNumber;
299
- private readonly onMutation;
300
- constructor(name: string, definition: CollectionDefinition, schema: SchemaDefinition, adapter: StorageAdapter, clock: HybridLogicalClock, nodeId: string, getSequenceNumber: () => number, onMutation: MutationCallback);
918
+ declare class SubscriptionBloomFilter {
919
+ private bits;
920
+ private readonly bitCount;
921
+ private readonly hashCount;
922
+ private itemCount;
923
+ constructor(expectedItems: number, falsePositiveRate?: number);
301
924
  /**
302
- * Insert a new record into the collection.
303
- * Generates a UUID v7 for the id, validates data, and persists atomically.
925
+ * Add a collection (and optional field) to the bloom filter.
304
926
  *
305
- * @param data - The record data (auto fields and defaults are applied automatically)
306
- * @returns The inserted record with id, createdAt, updatedAt
927
+ * @param collection - Collection name (e.g., "todos")
928
+ * @param field - Optional field name (e.g., "completed")
307
929
  */
308
- insert(data: Record<string, unknown>): Promise<CollectionRecord>;
930
+ add(collection: string, field?: string): void;
309
931
  /**
310
- * Find a record by its ID. Returns null if not found or soft-deleted.
932
+ * Check if a collection (and optional field) might be in the filter.
933
+ *
934
+ * A return value of `false` means the item is DEFINITELY NOT in the filter.
935
+ * A return value of `true` means the item MIGHT be in the filter (possible false positive).
936
+ *
937
+ * @param collection - Collection name to check
938
+ * @param field - Optional field name to check
939
+ * @returns false = definitely absent, true = possibly present
311
940
  */
312
- findById(id: string): Promise<CollectionRecord | null>;
941
+ mightContain(collection: string, field?: string): boolean;
313
942
  /**
314
- * Update an existing record. Only the provided fields are changed.
943
+ * Reset the bloom filter, clearing all bits and the item count.
944
+ */
945
+ clear(): void;
946
+ /**
947
+ * Estimate the current false positive rate based on the number of items inserted.
948
+ *
949
+ * Formula: (1 - e^(-kn/m))^k
950
+ * where k = hash count, n = item count, m = bit count.
315
951
  *
316
- * @param id - The record ID to update
317
- * @param data - Partial data with only the fields to change
318
- * @returns The updated record
319
- * @throws {RecordNotFoundError} If the record doesn't exist or is deleted
952
+ * @returns Estimated false positive rate as a number between 0 and 1
320
953
  */
321
- update(id: string, data: Record<string, unknown>): Promise<CollectionRecord>;
954
+ estimatedFalsePositiveRate(): number;
955
+ /**
956
+ * @returns The number of items that have been added to the filter
957
+ */
958
+ getItemCount(): number;
959
+ /**
960
+ * @returns The total number of bits in the filter
961
+ */
962
+ getBitCount(): number;
322
963
  /**
323
- * Soft-delete a record by its ID.
964
+ * @returns The number of hash functions used
965
+ */
966
+ getHashCount(): number;
967
+ /**
968
+ * Count the number of bits currently set to 1.
969
+ * Uses Brian Kernighan's algorithm: each iteration clears the lowest set bit,
970
+ * so the loop runs exactly as many times as there are set bits.
324
971
  *
325
- * @param id - The record ID to delete
326
- * @throws {RecordNotFoundError} If the record doesn't exist or is already deleted
972
+ * @returns Number of bits set to 1
327
973
  */
328
- delete(id: string): Promise<void>;
329
- /** Get the collection name */
330
- getName(): string;
331
- /** Get the collection definition */
332
- getDefinition(): CollectionDefinition;
974
+ getSetBitCount(): number;
975
+ /**
976
+ * Compute k bit positions for a given key using Kirsch-Mitzenmacker double hashing.
977
+ *
978
+ * Instead of computing k independent hash functions, we compute two base hashes
979
+ * (h1 and h2) and derive the rest as: h_i = (h1 + i * h2) mod m.
980
+ * This technique is proven to have the same asymptotic false positive rate as
981
+ * k independent hash functions.
982
+ *
983
+ * We derive h1 and h2 from a single FNV-1a hash by splitting and mixing:
984
+ * h1 = fnv1a(key), h2 = fnv1a(key + "\0salt") to ensure independence.
985
+ */
986
+ private getPositions;
987
+ }
988
+
989
+ /**
990
+ * Error thrown when a local mutation attempts an invalid state transition.
991
+ *
992
+ * Contains enough context to debug without reproduction:
993
+ * the collection, record, field, current state, attempted state, and allowed transitions.
994
+ */
995
+ declare class InvalidStateTransitionError extends KoraError {
996
+ readonly collection: string;
997
+ readonly recordId: string;
998
+ readonly field: string;
999
+ readonly fromState: string;
1000
+ readonly toState: string;
1001
+ readonly allowedStates: string[];
1002
+ constructor(collection: string, recordId: string, field: string, fromState: string, toState: string, allowedStates: string[]);
333
1003
  }
1004
+ /**
1005
+ * Validates a state machine transition for a local mutation (insert or update).
1006
+ *
1007
+ * For inserts: validates that the initial state (from the data or the default value)
1008
+ * is a known state in the state machine.
1009
+ *
1010
+ * For updates: looks up the current state field value and checks whether
1011
+ * the transition to the new value is allowed.
1012
+ *
1013
+ * @param collectionName - Name of the collection
1014
+ * @param recordId - The record being mutated
1015
+ * @param stateMachine - The state machine definition
1016
+ * @param currentState - The current value of the state field (null for inserts)
1017
+ * @param newState - The new value being set for the state field
1018
+ * @returns An object indicating whether the transition is valid, and if not, the allowed states.
1019
+ * When `onInvalidTransition` is 'last-valid-state', callers should suppress the field update
1020
+ * rather than throwing.
1021
+ */
1022
+ declare function validateStateTransition(collectionName: string, recordId: string, stateMachine: StateMachineDefinition, currentState: string | null, newState: string): {
1023
+ valid: boolean;
1024
+ allowedStates: string[];
1025
+ };
1026
+ /**
1027
+ * Checks whether a collection update data object contains a change to the state machine field,
1028
+ * and if so, validates the transition.
1029
+ *
1030
+ * If the state field is not in the update data, returns the data unchanged.
1031
+ * If the transition is invalid and mode is 'last-valid-state', removes the field from the data.
1032
+ * If the transition is invalid and mode is 'reject', throws InvalidStateTransitionError.
1033
+ *
1034
+ * @param collectionName - Name of the collection
1035
+ * @param recordId - The record being updated
1036
+ * @param collectionDef - The collection definition from the schema
1037
+ * @param currentRecord - The current record data (must include the state field)
1038
+ * @param updateData - The partial update data
1039
+ * @returns The update data, potentially with the state field removed if invalid and mode is 'last-valid-state'
1040
+ */
1041
+ declare function validateUpdateStateMachine(collectionName: string, recordId: string, collectionDef: CollectionDefinition, currentRecord: Record<string, unknown>, updateData: Record<string, unknown>): Record<string, unknown>;
334
1042
 
335
1043
  type RichtextInput = string | Uint8Array | ArrayBuffer | null | undefined;
336
1044
  /**
@@ -345,6 +1053,11 @@ declare function decodeRichtext(value: unknown): Uint8Array | null;
345
1053
  * Reads plain text from a richtext Yjs state update.
346
1054
  */
347
1055
  declare function richtextToPlainText(value: RichtextInput): string;
1056
+ /**
1057
+ * Compares two richtext values by decoded plain text.
1058
+ * Yjs update bytes are not stable across separate encodes of the same string.
1059
+ */
1060
+ declare function richtextStatesEqual(a: unknown, b: unknown): boolean;
348
1061
 
349
1062
  /**
350
1063
  * Minimal pluralization/singularization utilities for relation name resolution.
@@ -373,4 +1086,63 @@ declare function pluralize(word: string): string;
373
1086
  */
374
1087
  declare function singularize(word: string): string;
375
1088
 
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 };
1089
+ /**
1090
+ * Export a backup from a store.
1091
+ *
1092
+ * Reads all data via the adapter and produces a portable binary backup.
1093
+ *
1094
+ * @param adapter - The storage adapter
1095
+ * @param schema - The schema definition
1096
+ * @param nodeId - Current node ID
1097
+ * @param schemaVersion - Schema version
1098
+ * @param options - Backup options
1099
+ * @returns Backup as a Uint8Array
1100
+ */
1101
+ declare function exportBackup(adapter: StorageAdapter, schema: SchemaDefinition, nodeId: string, schemaVersion: number, options?: BackupOptions): Promise<Uint8Array>;
1102
+ /**
1103
+ * Read backup manifest without loading the entire backup.
1104
+ * Parses only the first section (which is always the manifest).
1105
+ *
1106
+ * @param data - The raw backup data
1107
+ * @returns The backup manifest
1108
+ */
1109
+ declare function readBackupManifest(data: Uint8Array): BackupManifest;
1110
+ /**
1111
+ * Validate the backup checksum.
1112
+ *
1113
+ * @param data - The raw backup data
1114
+ * @returns True if the checksum is valid
1115
+ */
1116
+ declare function verifyBackupChecksum(data: Uint8Array): Promise<boolean>;
1117
+ /**
1118
+ * Restore a backup into a store.
1119
+ *
1120
+ * @param adapter - The storage adapter
1121
+ * @param schema - The schema definition
1122
+ * @param data - The backup data
1123
+ * @param options - Restore options
1124
+ * @returns Result of the restore operation
1125
+ */
1126
+ declare function restoreBackup(adapter: StorageAdapter, schema: SchemaDefinition, data: Uint8Array, options?: RestoreOptions): Promise<RestoreResult>;
1127
+
1128
+ /**
1129
+ * Build a {@link PersistedAuditTrace} from a merge-related Kora event.
1130
+ */
1131
+ declare function persistedAuditTraceFromEvent(event: Extract<KoraEvent, {
1132
+ type: 'merge:completed' | 'merge:conflict' | 'constraint:violated';
1133
+ }>): PersistedAuditTrace;
1134
+
1135
+ /**
1136
+ * Decode an audit export binary into structured data.
1137
+ */
1138
+ declare function decodeAuditExport(data: Uint8Array): AuditExportPayload;
1139
+ /**
1140
+ * Read only the manifest from an audit export without decoding all sections.
1141
+ */
1142
+ declare function readAuditExportManifest(data: Uint8Array): AuditExportManifest;
1143
+ /**
1144
+ * Verify the checksum of an audit export file.
1145
+ */
1146
+ declare function verifyAuditExportChecksum(data: Uint8Array): Promise<boolean>;
1147
+
1148
+ export { AdapterError, ApplyRemoteOptions, type AuditExportManifest, type AuditExportOptions, type AuditExportPayload, type AuditExportProgress, type AuditTraceQuery, type BackupManifest, type BackupOptions, type BackupProgress, COMPACTION_BASELINE_META_KEY, type CollectionAccessor, CollectionRecord, type CompactionResult, type CompactionStrategy, InvalidStateTransitionError, LAST_ACKED_SERVER_VECTOR_META_KEY, LocalMutationHandler, MaterializedRowSnapshot, OrderByDirection, type PersistedAuditTrace, PersistenceError, QueryBuilder, QueryDescriptor, QueryError, RecordNotFoundError, type ReplaySnapshot, type RestoreOptions, type RestoreResult, SequenceManager, StorageAdapter, Store, StoreConfig, StoreNotOpenError, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, WhereClause, WorkerInitError, WorkerTimeoutError, collectOperationsAheadOfServer, compactOperationLog, computeAckCompactionWatermark, decodeAuditExport, decodeRichtext, deserializeVersionVectorFromMeta, encodeRichtext, exportBackup, mergeVersionVectors, persistedAuditTraceFromEvent, pluralize, readAuditExportManifest, readBackupManifest, restoreBackup, richtextStatesEqual, richtextToPlainText, serializeVersionVectorToMeta, singularize, validateStateTransition, validateUpdateStateMachine, verifyAuditExportChecksum, verifyBackupChecksum };