@korajs/store 1.0.0-beta.11 → 1.0.0-beta.13

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 (92) hide show
  1. package/README.md +29 -37
  2. package/dist/adapters/better-sqlite3.cjs +90 -4
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +21 -12
  5. package/dist/adapters/better-sqlite3.d.ts +21 -12
  6. package/dist/adapters/better-sqlite3.js +69 -5
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +1316 -610
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +29 -3
  11. package/dist/adapters/indexeddb.d.ts +29 -3
  12. package/dist/adapters/indexeddb.js +247 -221
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm-worker.cjs +803 -62
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +480 -62
  17. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  18. package/dist/adapters/sqlite-wasm.cjs +1032 -344
  19. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  20. package/dist/adapters/sqlite-wasm.d.cts +239 -18
  21. package/dist/adapters/sqlite-wasm.d.ts +239 -18
  22. package/dist/adapters/sqlite-wasm.js +111 -5
  23. package/dist/adapters/sqlite-wasm.js.map +1 -1
  24. package/dist/backup-4NUPDEMB.js +25 -0
  25. package/dist/chunk-32EDKWMI.js +876 -0
  26. package/dist/chunk-32EDKWMI.js.map +1 -0
  27. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  28. package/dist/chunk-A6GFH5IG.js +36 -0
  29. package/dist/chunk-A6GFH5IG.js.map +1 -0
  30. package/dist/chunk-DUKTWVZW.js +175 -0
  31. package/dist/chunk-DUKTWVZW.js.map +1 -0
  32. package/dist/chunk-HZTDBLDB.js +607 -0
  33. package/dist/chunk-HZTDBLDB.js.map +1 -0
  34. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  35. package/dist/chunk-J5AGXF3D.js.map +1 -0
  36. package/dist/chunk-MCQKEHOD.js +112 -0
  37. package/dist/chunk-MCQKEHOD.js.map +1 -0
  38. package/dist/chunk-P4V375FY.js +810 -0
  39. package/dist/chunk-P4V375FY.js.map +1 -0
  40. package/dist/chunk-QDUSJMFV.js +1095 -0
  41. package/dist/chunk-QDUSJMFV.js.map +1 -0
  42. package/dist/chunk-SJP2HTYZ.js +1003 -0
  43. package/dist/chunk-SJP2HTYZ.js.map +1 -0
  44. package/dist/chunk-W5F2LWG6.js +591 -0
  45. package/dist/chunk-W5F2LWG6.js.map +1 -0
  46. package/dist/chunk-Y5LRH7KI.js +5341 -0
  47. package/dist/chunk-Y5LRH7KI.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9354 -4157
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1064 -108
  52. package/dist/index.d.ts +1064 -108
  53. package/dist/index.js +249 -3092
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +580 -264
  56. package/dist/internal.cjs.map +1 -1
  57. package/dist/internal.d.cts +15 -85
  58. package/dist/internal.d.ts +15 -85
  59. package/dist/internal.js +10 -8
  60. package/dist/restore-FR7XIKT2.js +299 -0
  61. package/dist/restore-FR7XIKT2.js.map +1 -0
  62. package/dist/sql-builder-C4u55jKW.d.cts +608 -0
  63. package/dist/sql-builder-CGckdBDL.d.ts +608 -0
  64. package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
  65. package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
  66. package/dist/sqlite-wasm-channel-MLHLKEED.js +12 -0
  67. package/dist/store-IMWKZLWB.js +16 -0
  68. package/dist/store-IMWKZLWB.js.map +1 -0
  69. package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
  70. package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
  71. package/package.json +3 -2
  72. package/dist/backup-5S7K2Z35.js +0 -14
  73. package/dist/chunk-GSJ7EEUC.js.map +0 -1
  74. package/dist/chunk-HS6DBMST.js +0 -383
  75. package/dist/chunk-HS6DBMST.js.map +0 -1
  76. package/dist/chunk-RJIEKEN4.js +0 -751
  77. package/dist/chunk-RJIEKEN4.js.map +0 -1
  78. package/dist/chunk-TUR7RGEP.js +0 -84
  79. package/dist/chunk-TUR7RGEP.js.map +0 -1
  80. package/dist/chunk-VDBJQ75K.js +0 -78
  81. package/dist/chunk-VDBJQ75K.js.map +0 -1
  82. package/dist/chunk-YT7MRFCP.js +0 -687
  83. package/dist/chunk-YT7MRFCP.js.map +0 -1
  84. package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
  85. package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
  86. package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
  87. package/dist/types-BOO9EPt1.d.ts +0 -170
  88. package/dist/types-C_GDavG-.d.cts +0 -170
  89. /package/dist/{backup-5S7K2Z35.js.map → backup-4NUPDEMB.js.map} +0 -0
  90. /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
  91. /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
  92. /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-MLHLKEED.js.map} +0 -0
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
- import { C as CollectionRecord, S as StorageAdapter, Q as QueryDescriptor, d as SubscriptionCallback, W as WhereClause, e as OrderByDirection, L as LocalMutationHandler, f as StoreConfig, A as ApplyRemoteOptions, g as MaterializedRowSnapshot, h as RowVersionState } from './types-BslDOTk0.cjs';
2
- export { M as MigrationPlan, i as OrderByClause, j as StorageFallbackReason, a as StorageOpenState, k as StoreIsolation, T as Transaction, l as TransactionBufferedEntry, m as TransactionCommitBatch, n as TransactionCommitResult, o as WhereOperators } from './types-BslDOTk0.cjs';
3
- import { Operation, KoraError, VersionVector, SchemaDefinition, HLCTimestamp, MergeTrace, CollectionDefinition, SequenceConfig, HybridLogicalClock, CausalTracker, OperationLog, ApplyResult, StateMachineDefinition, KoraBytesValue, KoraEvent, BlobRefMetadata, BlobRef } from '@korajs/core';
1
+ import { C as CollectionRecord, S as StorageAdapter, Q as QueryDescriptor, d as QuerySubscriptionError, e as SubscriptionCallback, f as SubscribeOptions, W as WhereClause, g as OrderByDirection, h as StoreConfig, A as ApplyRemoteOptions, L as LocalMutationHandler, i as MaterializedRowSnapshot, j as RowVersionState } from './types-Dh-xyUjP.cjs';
2
+ export { M as MigrationPlan, k as OrderByClause, l as QueryErrorPhase, m as StorageFallbackReason, a as StorageOpenState, n as StoreIsolation, T as Transaction, o as WhereOperators } from './types-Dh-xyUjP.cjs';
3
+ import { Operation, KoraError, VersionVector, SchemaDefinition, HLCTimestamp, MergeTrace, CollectionDefinition, SequenceConfig, HybridLogicalClock, CausalTracker, SecretKeyProvider, OperationLog, OperationTransform, ApplyResult, FoldState, KoraEventEmitter, StateMachineDefinition, KoraBytesValue, KoraEvent, BlobRefMetadata, BlobRef } from '@korajs/core';
4
4
  export { ApplyResult } from '@korajs/core';
5
- import { R as RelationEnforcer, L as LocalMutationContext } from './types-C_GDavG-.cjs';
6
- export { C as Collection } from './types-C_GDavG-.cjs';
5
+ import { R as RelationEnforcer, j as LocalDeleteHook, k as RecordFolder, L as LocalMutationContext, A as AdoptionSchedule, T as TerminalRejection, l as LocalNodeRecord } from './sql-builder-C4u55jKW.cjs';
6
+ export { C as Collection, P as ParkedAdoption, V as VIRTUAL_TIMESTAMP_FIELDS, m as VIRTUAL_TIMESTAMP_FIELD_NAMES, n as VirtualTimestampField, o as mergeYjsUpdates } from './sql-builder-C4u55jKW.cjs';
7
7
  import * as yjs from 'yjs';
8
8
  import { C as ContentAddressedBlobStore, M as MemoryBlobStore } from './content-addressed-blob-store-Dcj3SB9r.cjs';
9
9
  export { B as BlobIntegrityError } from './content-addressed-blob-store-Dcj3SB9r.cjs';
@@ -53,6 +53,34 @@ declare class OptimisticLockError extends KoraError {
53
53
  declare class AdapterError extends KoraError {
54
54
  constructor(message: string, context?: Record<string, unknown>);
55
55
  }
56
+ /**
57
+ * Thrown when a write reaches a storage adapter that could not obtain durable
58
+ * storage (it opened, or was promoted to leader, on a non-persistent in-memory
59
+ * database). Kora refuses such writes instead of accepting data that would be
60
+ * lost on reload. Reads still work.
61
+ *
62
+ * Fix: close other tabs or apps using the same origin's OPFS storage and reopen,
63
+ * or opt in explicitly with `allowNonDurable: true` if in-memory storage is
64
+ * acceptable for this app.
65
+ */
66
+ declare class StorageDurabilityError extends KoraError {
67
+ constructor(dbName: string, phase: 'open' | 'promotion', reason: string);
68
+ }
69
+ /**
70
+ * Thrown when the store opens a database a NEWER build of the app already migrated: its
71
+ * stored schema version is above this code's `schema.version` (RT-109). Running older
72
+ * code against it would write operations of the old schema into the new one, or fail
73
+ * part-way, so the store refuses before changing anything and emits `store:schema-ahead`.
74
+ *
75
+ * Fix: run the newer build (reload while online, or accept the app's update prompt).
76
+ * Never lower `schema.version` in a deployed app.
77
+ */
78
+ declare class SchemaVersionAheadError extends KoraError {
79
+ readonly dbName: string;
80
+ readonly storedVersion: number;
81
+ readonly codeVersion: number;
82
+ constructor(dbName: string, storedVersion: number, codeVersion: number);
83
+ }
56
84
  /**
57
85
  * Thrown when an operation is attempted on a store that has not been opened.
58
86
  */
@@ -78,7 +106,50 @@ declare class WorkerTimeoutError extends KoraError {
78
106
  * leader instead of waiting out the full RPC timeout.
79
107
  */
80
108
  declare class NoLeaderError extends KoraError {
81
- constructor(operation: string);
109
+ constructor(operation: string, message?: string, code?: string, context?: Record<string, unknown>);
110
+ }
111
+ /**
112
+ * Thrown to a follower tab's pending storage requests when the leader tab that
113
+ * was answering stopped sending heartbeats (hung, frozen, or suspended). The
114
+ * request may or may not have been applied: retry it with the same `requestId`
115
+ * (in the error context) and the leader de-duplicates it. Retriable.
116
+ */
117
+ declare class LeaderUnresponsiveError extends NoLeaderError {
118
+ constructor(operation: string, silentMs: number, requestId: string);
119
+ }
120
+ /**
121
+ * Thrown to pending storage requests when their bridge was torn down, typically
122
+ * because this tab was just promoted to leader or the leader handed over.
123
+ * Retriable: the next attempt goes to the new leader.
124
+ */
125
+ declare class BridgeTerminatedError extends KoraError {
126
+ constructor(operation: string, reason: string);
127
+ }
128
+ /** Thrown when a storage request is cancelled through its `AbortSignal`. */
129
+ declare class RequestAbortedError extends KoraError {
130
+ constructor(operation: string, requestId: string);
131
+ }
132
+ /**
133
+ * Thrown by `deleteDatabase()` when the database is open in some tab or worker
134
+ * on this origin. Close it everywhere (or call `app.close()`) and retry.
135
+ */
136
+ declare class StorageInUseError extends KoraError {
137
+ constructor(dbName: string);
138
+ }
139
+ /**
140
+ * Thrown by `deleteDatabase()` when the database still holds operations that
141
+ * never reached the server. Sync first, or pass `{ force: true }` to discard them.
142
+ */
143
+ declare class UnsyncedDataError extends KoraError {
144
+ constructor(dbName: string);
145
+ }
146
+ /**
147
+ * Thrown when a database's data lives in a storage backend this runtime cannot
148
+ * read (for example it was written to OPFS, and OPFS is unavailable now). Kora
149
+ * refuses to start an empty, disjoint copy rather than hide the existing data.
150
+ */
151
+ declare class StorageBackendMismatchError extends KoraError {
152
+ constructor(dbName: string, recorded: string, requested: string, reason: string);
82
153
  }
83
154
  /**
84
155
  * Thrown when IndexedDB persistence operations fail (serialize/deserialize).
@@ -124,6 +195,27 @@ declare function computeAckCompactionWatermark(serverVector: VersionVector): Ver
124
195
  */
125
196
  declare function compactOperationLog(adapter: StorageAdapter, schema: SchemaDefinition, strategy: CompactionStrategy, serverVector: VersionVector): Promise<CompactionResult>;
126
197
 
198
+ /**
199
+ * How a database is re-materialized on its first open with the W7 fold:
200
+ *
201
+ * - `'log'`: the log is clean (W8 integrity scan): every record is rebuilt with the
202
+ * fold from its operations. This also repairs replicas that diverged under the
203
+ * pairwise merge of beta.12/13.
204
+ * - `'snapshot+log'`: the log is not complete (it was compacted, or has sequence
205
+ * gaps): each existing row becomes the record's row snapshot (its values at
206
+ * their `_field_versions`, exact where a stored fold state still has them), and
207
+ * the remaining operations are folded on top.
208
+ * - `'kept'`: the log has quarantined rows (W8). Only the records that own a
209
+ * quarantined row are kept exactly as they are (their row becomes their snapshot,
210
+ * nothing is rebuilt from their log); every other record is rebuilt as in `'log'`
211
+ * or `'snapshot+log'` (RT-68).
212
+ *
213
+ * Row snapshots are approximate (see `createSnapshotState`); records that fold
214
+ * against one are listed in `_kora_fold_snapshot` until their history is complete
215
+ * again (a scope entry with the server's fold state, or a full resync).
216
+ */
217
+ type RematerializationMode = 'log' | 'snapshot+log' | 'kept';
218
+
127
219
  /**
128
220
  * Result of re-stamping unsynced operations after a clock correction.
129
221
  */
@@ -138,23 +230,39 @@ interface ClockRebaseResult {
138
230
  newMaxTimestamp: HLCTimestamp | null;
139
231
  }
140
232
 
141
- declare const LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
142
- /**
143
- * Serialize a version vector for `_kora_meta` storage.
144
- */
145
- declare function serializeVersionVectorToMeta(vector: VersionVector): string;
146
- /**
147
- * Deserialize a version vector from `_kora_meta`.
148
- */
149
- declare function deserializeVersionVectorFromMeta(value: string): VersionVector;
150
- /**
151
- * Merge two version vectors, keeping the maximum sequence per node.
152
- */
153
- declare function mergeVersionVectors(a: VersionVector, b: VersionVector): VersionVector;
154
- /**
155
- * Operations present locally but not yet on the server (per server version vector).
156
- */
157
- declare function collectOperationsAheadOfServer(localVector: VersionVector, serverVector: VersionVector, fetchRange: (nodeId: string, fromSeq: number, toSeq: number) => Promise<Operation[]>): Promise<Operation[]>;
233
+ /** Result of {@link rotateUnsyncedOperationsInLog}. */
234
+ interface NodeRotationResult {
235
+ /** The node id the device uses from now on. */
236
+ nodeId: string;
237
+ /** The rewritten operations under the new node id, in their original causal order. */
238
+ operations: Operation[];
239
+ /** Maps each old operation id to its new content-addressed id. */
240
+ idMapping: Record<string, string>;
241
+ }
242
+
243
+ /** Result of `Store.resequenceOperation` (RT-35 renumbering). */
244
+ interface ResequenceResult {
245
+ /** The renumbered operation (a version-2 one under a new id). */
246
+ operation: Operation;
247
+ /** Never-sent dependents rewritten to name the new id (re-hashed), in sequence order. */
248
+ dependents: Operation[];
249
+ /** Old id -> new id, for the operation and every re-hashed dependent. */
250
+ idMapping: Record<string, string>;
251
+ }
252
+
253
+ /** A delivered operation the client did not apply, as stored in `_kora_unapplied_ops`. */
254
+ interface UnappliedOperation {
255
+ /** The operation as delivered (still encrypted when decryption failed). */
256
+ operation: Operation;
257
+ /** Delivery sequence of the batch that carried it, or null for a legacy batch. */
258
+ deliverySequence: number | null;
259
+ /** Machine-readable reason. */
260
+ code: string;
261
+ /** Human-readable explanation. */
262
+ message: string;
263
+ /** Wall-clock time (ms) it was quarantined. Display only. */
264
+ quarantinedAt: number;
265
+ }
158
266
 
159
267
  /**
160
268
  * A merge or constraint decision persisted for enterprise audit export.
@@ -233,11 +341,14 @@ interface AuditTraceQuery {
233
341
  * Manifest embedded in every backup file.
234
342
  */
235
343
  interface BackupManifest {
236
- /** Backup format version */
237
- version: 1;
344
+ /** Backup format version (2 since STORE-5; version 1 files need `convertBackupV1`) */
345
+ version: number;
238
346
  /** When the backup was created (epoch ms) */
239
347
  createdAt: number;
240
- /** Node ID of the originating device */
348
+ /**
349
+ * Node ID of the originating device. Informational only: a restore never copies a
350
+ * device identity into the importing database.
351
+ */
241
352
  nodeId: string;
242
353
  /** Schema version at backup time */
243
354
  schemaVersion: number;
@@ -247,6 +358,20 @@ interface BackupManifest {
247
358
  collections: string[];
248
359
  /** Whether materialized records are included */
249
360
  includesRecords: boolean;
361
+ /** Whether the records include deleted rows (tombstones). False for converted v1 files. */
362
+ includesTombstones?: boolean;
363
+ /** Set on a file produced by `convertBackupV1`. */
364
+ convertedFrom?: number;
365
+ /**
366
+ * The file carries the W7 fold's per-record base states (`fold_base`), row
367
+ * snapshots (`fold_snapshot`) and the compacted prefixes (`compacted_through`), so
368
+ * a compacted device's history survives a restore on another device (RT-66).
369
+ * Absent on files from earlier releases: their log may be incomplete (compacted),
370
+ * so a restore rebuilds their records on top of the backup's rows.
371
+ */
372
+ includesFoldState?: boolean;
373
+ /** The exporting database had compacted its log (it then needs `fold_base`). */
374
+ compacted?: boolean;
250
375
  /** SHA-256 hex checksum of all content sections */
251
376
  checksum: string;
252
377
  }
@@ -271,6 +396,19 @@ interface RestoreResult {
271
396
  success: boolean;
272
397
  /** Error message if failed */
273
398
  error?: string;
399
+ /** Error code if failed (`BACKUP_FORMAT_OUTDATED`, `BACKUP_CHECKSUM_MISMATCH`, ...) */
400
+ errorCode?: string;
401
+ /**
402
+ * Replace mode: this device's own writes the sync server had not acknowledged,
403
+ * re-applied on top of the restored data (see `RestoreOptions.keepUnsyncedWrites`).
404
+ */
405
+ unsyncedWritesKept?: number;
406
+ /**
407
+ * Merge mode: operations of server-authority nodes (`kora:` nodes and the explicit
408
+ * server authorities this device learned) the file held and the import left out.
409
+ * Server decisions reach a device only from the sync server.
410
+ */
411
+ serverOperationsSkipped?: number;
274
412
  /** Duration in ms */
275
413
  duration: number;
276
414
  }
@@ -294,12 +432,118 @@ interface RestoreOptions {
294
432
  /** Progress callback */
295
433
  onProgress?: (progress: BackupProgress) => void;
296
434
  /**
297
- * If true, merge operations with existing data (replay through applyRemoteOperation).
298
- * If false, clear and replace all data (default: false).
435
+ * If true, merge the backup into the existing data: every operation is applied like
436
+ * one received from sync (deduplicated by id, merged per field), the version vector
437
+ * advances by MAX, and nothing of the exporting device's identity or sync state is
438
+ * imported. If false (default), replace the local data with the backup's.
299
439
  */
300
440
  merge?: boolean;
441
+ /**
442
+ * Replace mode only: keep this device's own writes that the sync server has not
443
+ * acknowledged, re-applied on top of the backup, so a restore never discards writes
444
+ * that exist nowhere else. Writes of a node the server has accepted are always kept
445
+ * (they are in flight). Default: true for `store.importBackup`; `app.importBackup`
446
+ * defaults it to whether sync is configured, so a local-only app gets an exact
447
+ * replace.
448
+ */
449
+ keepUnsyncedWrites?: boolean;
301
450
  }
302
451
 
452
+ /**
453
+ * Log-integrity scan (W8 step 0).
454
+ *
455
+ * Every later fold of the operation log (W7 re-materialization, replay, compaction)
456
+ * trusts that each stored row is a canonical operation. Two earlier releases could
457
+ * break that:
458
+ *
459
+ * - beta.12's backup restore wrote `JSON.stringify(timestamp)` into the timestamp
460
+ * column, which `HybridLogicalClock.deserialize` reads as `wallTime: NaN` (a `null`
461
+ * once re-exported), and a second restore of such a backup nested the damage;
462
+ * - compaction deletes acknowledged operations, leaving holes in a node's sequence.
463
+ *
464
+ * The scan detects rows that do not round-trip through `deserializeOperation` /
465
+ * `serializeOperation`, repairs the ones whose original value is recoverable from the
466
+ * row itself (the JSON-encoded timestamp), and moves the rest to
467
+ * {@link LOG_QUARANTINE_TABLE} (never deleted), so no fold ever reads them. It also
468
+ * reports sequence gaps in this database's own nodes. Remote nodes' gaps are normal
469
+ * (scoped sync only delivers part of another device's log) and are not reported.
470
+ */
471
+ /** Rows that could not be repaired, kept verbatim. Never cleared automatically. */
472
+ declare const LOG_QUARANTINE_TABLE = "_kora_log_quarantine";
473
+ /** What is wrong with an operation row. */
474
+ type LogRowProblem =
475
+ /** The timestamp is a JSON-encoded HLC (beta.12 backup restore). Repairable. */
476
+ 'timestamp-json'
477
+ /** The timestamp cannot be read as an HLC and is not recoverable. */
478
+ | 'timestamp-malformed'
479
+ /** `id`, `node_id` or `record_id` is missing. */
480
+ | 'identity-malformed'
481
+ /** `type` is not insert, update or delete. */
482
+ | 'type-invalid'
483
+ /** `sequence_number` is not a positive integer. */
484
+ | 'sequence-invalid'
485
+ /** `data` or `previous_data` is not a JSON object. */
486
+ | 'data-malformed'
487
+ /** `causal_deps` is not a JSON array of operation ids. */
488
+ | 'causal-deps-malformed'
489
+ /** The row cannot be turned into an operation and back. */
490
+ | 'round-trip-failed';
491
+ /** One operation row the scan repaired or quarantined. */
492
+ interface LogIntegrityRow {
493
+ collection: string;
494
+ operationId: string;
495
+ nodeId: string | null;
496
+ sequenceNumber: number | null;
497
+ problem: LogRowProblem;
498
+ /** Human-readable detail: the bad value, or how it was repaired. */
499
+ detail: string;
500
+ }
501
+ /** Sequence numbers missing from one of this database's own nodes. */
502
+ interface LogSequenceGap {
503
+ nodeId: string;
504
+ /** First missing sequence number (inclusive). */
505
+ from: number;
506
+ /** Last missing sequence number (inclusive). */
507
+ to: number;
508
+ }
509
+ /**
510
+ * Result of {@link scanLogIntegrity} / `Store.verifyLogIntegrity()`.
511
+ *
512
+ * `clean` is the precondition for rebuilding state from the log (W7): every row is
513
+ * canonical, nothing is quarantined, and this database's own nodes have no holes
514
+ * below their highest stored sequence number.
515
+ */
516
+ interface LogIntegrityReport {
517
+ /** True when the log can be folded as-is (no quarantined rows, no own-node gaps). */
518
+ clean: boolean;
519
+ /** `full`: every row was checked in JS. `quick`: SQL prefilter, suspects checked in JS. */
520
+ mode: 'full' | 'quick';
521
+ /** Rows checked in JS (all rows in full mode, suspects in quick mode). */
522
+ checkedRows: number;
523
+ /** Rows this scan repaired in place. */
524
+ repaired: LogIntegrityRow[];
525
+ /** Rows this scan moved to the quarantine table. */
526
+ newlyQuarantined: LogIntegrityRow[];
527
+ /** Every row in the quarantine table (this scan's and earlier ones'). */
528
+ quarantined: LogIntegrityRow[];
529
+ /** Holes in this database's own nodes' sequences (compaction, a lost tail). */
530
+ gaps: LogSequenceGap[];
531
+ /** When the log was last compacted (epoch ms), or null when never. */
532
+ compactedAt: number | null;
533
+ /** False when the scan only reported (`repair: false`). */
534
+ repairApplied: boolean;
535
+ }
536
+
537
+ /** Compares two query result sets; true means subscribers need not be notified. */
538
+ type ResultsEqual = (prev: readonly CollectionRecord[], next: readonly CollectionRecord[]) => boolean;
539
+
540
+ /**
541
+ * Per-subscription options accepted by `register` / `registerAndFetch`.
542
+ */
543
+ interface RegisterOptions extends SubscribeOptions {
544
+ /** Result-set comparator; defaults to structural comparison of every value. */
545
+ resultsEqual?: ResultsEqual;
546
+ }
303
547
  /**
304
548
  * Configuration options for the SubscriptionManager.
305
549
  */
@@ -323,7 +567,29 @@ interface SubscriptionManagerOptions {
323
567
  bloomFalsePositiveRate?: number;
324
568
  /** Called when a query subscription is registered (e.g. to register sync query subsets). */
325
569
  onQuerySubscribed?: (descriptor: QueryDescriptor) => () => void;
570
+ /**
571
+ * Called for every subscription failure (the store emits `query:error`), in
572
+ * addition to the subscriber's own `onError`.
573
+ */
574
+ onQueryError?: (failure: QuerySubscriptionError) => void;
326
575
  }
576
+ /**
577
+ * A committed change to the rows of one collection that can affect query results
578
+ * (RT-98). Every invalidation the store makes produces one: local writes and remote
579
+ * applies (with the operation), and changes made without a new operation (rejection
580
+ * re-folds, scope retraction, cascade settling, rematerialization, backup restore,
581
+ * held/discarded writes).
582
+ */
583
+ interface RecordsChange {
584
+ /** Collection whose rows changed */
585
+ readonly collection: string;
586
+ /** Changed record ids, or null when unknown (treat the whole collection as changed) */
587
+ readonly ids: readonly string[] | null;
588
+ /** The operation that caused the change, when there is one */
589
+ readonly operation?: Operation;
590
+ }
591
+ /** Listener for {@link RecordsChange}s made by this runtime (not those relayed from peers). */
592
+ type RecordsChangeListener = (change: RecordsChange) => void;
327
593
  /**
328
594
  * Performance statistics for monitoring subscription checking efficiency.
329
595
  */
@@ -364,6 +630,14 @@ declare class SubscriptionManager {
364
630
  private pendingCollections;
365
631
  private flushScheduled;
366
632
  private readonly onQuerySubscribed?;
633
+ private readonly onQueryError?;
634
+ private readonly changeListeners;
635
+ /**
636
+ * Collections changed before the first change listener attached (a store's open:
637
+ * rematerialization, migration backfills). Replayed to that listener, so other tabs
638
+ * hear about changes made while this tab was still opening. Bounded by the schema.
639
+ */
640
+ private unheardCollections;
367
641
  private bloomFilter;
368
642
  private bloomDirty;
369
643
  private readonly bloomThreshold;
@@ -383,20 +657,55 @@ declare class SubscriptionManager {
383
657
  * @param executeFn - Function to re-execute the query and get current results
384
658
  * @returns An unsubscribe function
385
659
  */
386
- register(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]>): () => void;
660
+ register(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]>, options?: RegisterOptions): () => void;
387
661
  /**
388
662
  * Register a subscription and immediately execute the query.
389
663
  * The initial results are stored as lastResults so subsequent flushes
390
- * correctly diff against the initial state.
664
+ * correctly diff against the initial state. A failing initial run is reported
665
+ * through `options.onError` and the `query:error` hook, never as an unhandled
666
+ * rejection (STORE-12).
391
667
  *
392
668
  * @returns An unsubscribe function
393
669
  */
394
- registerAndFetch(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]>): () => void;
670
+ registerAndFetch(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]>, options?: RegisterOptions): () => void;
671
+ private createSubscription;
672
+ private track;
673
+ /**
674
+ * Run a subscription's query and deliver the result when it changed. Never
675
+ * throws: failures go to the error channel. Runs are numbered so a slow,
676
+ * older run never overwrites the result of a newer one.
677
+ */
678
+ private run;
679
+ private isCurrent;
680
+ private reportError;
395
681
  /**
396
682
  * Notify the manager that a mutation occurred on a collection.
397
683
  * Schedules a microtask flush to batch multiple mutations in the same tick.
398
684
  */
399
- notify(collection: string, _operation: Operation): void;
685
+ notify(collection: string, operation: Operation): void;
686
+ /**
687
+ * Invalidate a collection after a local-view change that has no domain operation
688
+ * (a re-fold, a retraction, a restore). Pass the record ids when known.
689
+ */
690
+ invalidate(collection: string, ids?: readonly string[] | null): void;
691
+ /**
692
+ * The single entry point for a committed change made by this runtime (RT-98): it
693
+ * schedules the affected queries and tells every change listener (the cross-tab
694
+ * bus), so no invalidation path can reach this tab's queries without reaching the
695
+ * other tabs' too.
696
+ */
697
+ recordsChanged(change: RecordsChange): void;
698
+ /**
699
+ * Invalidate a collection for a change another runtime made to the shared
700
+ * database. Not relayed to change listeners (the peer already broadcast it).
701
+ */
702
+ invalidateFromPeer(collection: string): void;
703
+ /**
704
+ * Listen to every committed change this runtime makes.
705
+ *
706
+ * @returns An unsubscribe function
707
+ */
708
+ onRecordsChanged(listener: RecordsChangeListener): () => void;
400
709
  /**
401
710
  * Immediately flush all pending notifications.
402
711
  * Useful for testing. In production, flushing happens via microtask.
@@ -436,12 +745,6 @@ declare class SubscriptionManager {
436
745
  */
437
746
  private rebuildBloomFilter;
438
747
  private scheduleFlush;
439
- /**
440
- * Compare two result sets. Uses ID-based comparison first (same length, same IDs),
441
- * then deep comparison of remaining fields when IDs match.
442
- * Avoids JSON.stringify which is O(n) and fragile to field ordering differences.
443
- */
444
- private resultsEqual;
445
748
  }
446
749
 
447
750
  /**
@@ -470,6 +773,10 @@ declare class QueryBuilder<T = CollectionRecord> {
470
773
  constructor(collectionName: string, definition: CollectionDefinition, adapter: StorageAdapter, subscriptionManager: SubscriptionManager, initialWhere?: WhereClause, schema?: SchemaDefinition | undefined);
471
774
  /**
472
775
  * Add WHERE conditions (AND semantics, merged with existing conditions).
776
+ *
777
+ * A field whose value is `undefined` adds no condition (it does not remove an
778
+ * earlier one); `null` matches missing values (`IS NULL`). Non-finite numbers are
779
+ * refused. See {@link normalizeWhere} (RT-102).
473
780
  */
474
781
  where(conditions: WhereClause): QueryBuilder<T>;
475
782
  /**
@@ -514,7 +821,9 @@ declare class QueryBuilder<T = CollectionRecord> {
514
821
  *
515
822
  * @returns An unsubscribe function
516
823
  */
517
- subscribe(callback: SubscriptionCallback<T>): () => void;
824
+ subscribe(callback: SubscriptionCallback<T>, options?: SubscribeOptions): () => void;
825
+ /** Comparator built once per collection definition and shared by its queries. */
826
+ private getResultsEqual;
518
827
  /** Get the internal descriptor (for testing/debugging) */
519
828
  getDescriptor(): QueryDescriptor;
520
829
  private clone;
@@ -593,20 +902,6 @@ declare class SequenceManager {
593
902
  }): Promise<void>;
594
903
  }
595
904
 
596
- /**
597
- * Allocates monotonic sequence numbers for a single open transaction.
598
- * Loads the current DB watermark once, then increments in memory until commit.
599
- */
600
- declare class TransactionSequenceAllocator {
601
- private readonly adapter;
602
- private readonly nodeId;
603
- private loaded;
604
- private watermark;
605
- constructor(adapter: StorageAdapter, nodeId: string);
606
- allocate(): Promise<number>;
607
- getHighWaterMark(): number;
608
- }
609
-
610
905
  /**
611
906
  * Internal configuration for creating a TransactionContext.
612
907
  * Passed from Store to avoid exposing Store internals publicly.
@@ -616,10 +911,19 @@ interface TransactionContextConfig {
616
911
  adapter: StorageAdapter;
617
912
  clock: HybridLogicalClock;
618
913
  nodeId: string;
619
- sequenceAllocator: TransactionSequenceAllocator;
620
914
  relationEnforcer: RelationEnforcer | null;
621
915
  causalTracker: CausalTracker | null;
622
- localMutationHandler: LocalMutationHandler | null;
916
+ /**
917
+ * Key provider for `t.secret()` encrypted fields. Transaction writes transform
918
+ * secret fields through the same helper as single-record writes (STORE-4).
919
+ */
920
+ secretKeyProvider?: SecretKeyProvider;
921
+ /** Referential hook run for every local delete inside the commit (see {@link LocalDeleteHook}). */
922
+ beforeLocalDelete?: LocalDeleteHook;
923
+ /** Called when the commit's storage transaction fails, before the error propagates. */
924
+ onStorageError?: (error: unknown) => void;
925
+ /** The W7 record fold (see `WriteEnv.fold`). */
926
+ fold?: RecordFolder;
623
927
  }
624
928
  /**
625
929
  * A collection accessor within a transaction.
@@ -634,8 +938,10 @@ interface TransactionCollectionAccessor {
634
938
  /**
635
939
  * TransactionContext provides atomic multi-collection operations.
636
940
  *
637
- * All mutations are buffered and committed in a single StorageAdapter.transaction()
638
- * call. All operations share the same transactionId (UUID v7).
941
+ * Mutations are validated and buffered when called (so errors surface at the
942
+ * call site and `insert` returns the new id immediately), then written in a
943
+ * single `StorageAdapter.transaction()` at commit. All operations share the
944
+ * same transactionId (UUID v7) and one contiguous block of sequence numbers.
639
945
  *
640
946
  * Subscription notifications are deferred until after commit.
641
947
  *
@@ -670,7 +976,13 @@ declare class TransactionContext {
670
976
  */
671
977
  collection(name: string): TransactionCollectionAccessor;
672
978
  /**
673
- * Commit all buffered operations atomically.
979
+ * Commit all buffered mutations atomically.
980
+ *
981
+ * Every entry is written, in call order, inside ONE storage transaction through
982
+ * the single local write path: sequence numbers are reserved inside the
983
+ * transaction (one contiguous block covering referential side effects too) and
984
+ * each operation is built only after its number is reserved.
985
+ *
674
986
  * Returns the list of operations and affected collections for subscription notification.
675
987
  */
676
988
  commit(): Promise<{
@@ -685,32 +997,35 @@ declare class TransactionContext {
685
997
  * Get the transaction ID shared by all operations in this transaction.
686
998
  */
687
999
  getTransactionId(): string;
1000
+ private writeEnv;
688
1001
  private ensureActive;
689
1002
  private insert;
690
1003
  private update;
691
1004
  private deleteRecord;
692
- private findById;
693
1005
  /**
694
- * Get the effective state of a record, considering both the database and buffered operations.
695
- * Buffered inserts/updates take precedence over the database state.
1006
+ * The values an update will write, resolved against `current` (the effective
1007
+ * record) for read-your-writes inside the transaction. Secret fields are shown
1008
+ * in their at-rest form, like the record a committed update returns.
1009
+ */
1010
+ private previewUpdate;
1011
+ /**
1012
+ * The effective state of a record: the committed row overlaid with this
1013
+ * transaction's buffered mutations, in call order.
696
1014
  */
697
1015
  private getEffectiveRecord;
698
1016
  }
699
1017
 
700
- /**
701
- * Store is the main orchestrator. It owns a schema, a storage adapter,
702
- * a clock, and a subscription manager. It creates Collection instances
703
- * for each schema collection, and provides the sync contract via
704
- * applyRemoteOperation and getOperationRange.
705
- *
706
- * @example
707
- * ```typescript
708
- * const store = new Store({ schema, adapter })
709
- * await store.open()
710
- * const todo = await store.collection('todos').insert({ title: 'Hello' })
711
- * await store.close()
712
- * ```
713
- */
1018
+ /** Result of {@link Store.bindPrincipal} (RT-42). */
1019
+ interface PrincipalBinding {
1020
+ /** The node id local writes use from now on. */
1021
+ nodeId: string;
1022
+ /** The node id in use before the call. */
1023
+ previousNodeId: string;
1024
+ /** The store moved to another node id. */
1025
+ switched: boolean;
1026
+ /** The node id is pinned and belongs to another user: nothing may upload it now. */
1027
+ conflict: boolean;
1028
+ }
714
1029
  declare class Store implements OperationLog {
715
1030
  private opened;
716
1031
  private nodeId;
@@ -730,6 +1045,18 @@ declare class Store implements OperationLog {
730
1045
  private relationEnforcer;
731
1046
  private causalTracker;
732
1047
  private readonly secretKeyProvider;
1048
+ /** Releases the per-tab node lock (RT-40); null when none is held. */
1049
+ private releaseNodeLock;
1050
+ /** How rows are materialized ({@link StoreConfig.materialization}). */
1051
+ private readonly materialization;
1052
+ /** The W7 record fold; null under legacy materialization. */
1053
+ private readonly folder;
1054
+ /** True once every row of the database is a materialization of its fold state. */
1055
+ private foldActive;
1056
+ /** Schema transforms applied at fold time (RT-84); never to stored operations. */
1057
+ private readonly operationTransforms;
1058
+ /** Largest operation a local write may produce (RT-86); default in the write path. */
1059
+ private readonly maxOperationBytes;
733
1060
  constructor(config: StoreConfig);
734
1061
  /**
735
1062
  * Open the store: initialize the database, load or generate a node ID,
@@ -740,6 +1067,25 @@ declare class Store implements OperationLog {
740
1067
  * Close the store: clear subscriptions and close the adapter.
741
1068
  */
742
1069
  close(): Promise<void>;
1070
+ /**
1071
+ * Check the operation log (W8 step 0): every row must round-trip through the
1072
+ * canonical operation serializer. Rows an earlier release damaged in a recoverable
1073
+ * way (a JSON-encoded timestamp written by a beta.12 backup restore) are repaired;
1074
+ * unrecoverable rows move to the quarantine table, where no fold reads them. Also
1075
+ * reports sequence gaps in this database's own nodes (compaction, a lost tail).
1076
+ *
1077
+ * The store runs a quick variant (SQL prefilter) on every open. Rebuilding state from
1078
+ * the log must only run on a `clean` report.
1079
+ *
1080
+ * @param options - `repair: false` only reports; `mode: 'quick'` checks only the rows
1081
+ * a SQL prefilter flags (default `'full'`: every row)
1082
+ * @returns The integrity report; emits `store:log-integrity` when rows changed
1083
+ */
1084
+ verifyLogIntegrity(options?: {
1085
+ repair?: boolean;
1086
+ mode?: 'full' | 'quick';
1087
+ }): Promise<LogIntegrityReport>;
1088
+ private scanLog;
743
1089
  /**
744
1090
  * Get a Collection instance for CRUD operations.
745
1091
  * @throws {StoreNotOpenError} If the store is not open
@@ -760,12 +1106,174 @@ declare class Store implements OperationLog {
760
1106
  */
761
1107
  setClockReferenceOffset(offsetMs: number): void;
762
1108
  getNodeId(): string;
1109
+ /** The schema transforms the fold applies (transforms at fold time, RT-84). */
1110
+ getOperationTransforms(): readonly OperationTransform[];
763
1111
  /**
764
1112
  * Apply a remote operation received from sync.
765
1113
  * Checks for duplicates, applies to the data table, persists the operation,
766
1114
  * and updates the version vector.
767
1115
  */
768
- applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
1116
+ applyRemoteOperation(stored: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
1117
+ /**
1118
+ * W7 remote apply: append the operation, then merge it into its record's fold
1119
+ * state, which re-materializes the row, all in one write transaction. There is no
1120
+ * read-decide-write race to guard: the state is read and written inside the
1121
+ * transaction, and the merge does not depend on the order operations arrive in.
1122
+ */
1123
+ private applyRemoteFolded;
1124
+ /** DevTools: every fold decision that was a conflict, plus one completion per merge. */
1125
+ private emitMergeTraces;
1126
+ /** The record fold, once the database's rows are fold materializations. */
1127
+ private activeFold;
1128
+ /**
1129
+ * The record's W7 fold state (null under legacy materialization or when the
1130
+ * record has none). For DevTools, tests and the server-comparison harness.
1131
+ */
1132
+ getFoldState(collection: string, recordId: string): Promise<FoldState | null>;
1133
+ /** Whether rows are materialized by the W7 fold (false under `materialization: 'legacy'`). */
1134
+ isFoldMaterialized(): boolean;
1135
+ private readMeta;
1136
+ private writeMeta;
1137
+ /**
1138
+ * Write every genuine beta.12 clear of the op log into its logged body, once per
1139
+ * database (RT-83, RT-85): the fold folds bodies as written, so the clear must be in
1140
+ * the body. Records already materialized by the fold are re-folded.
1141
+ */
1142
+ private ensureLegacyBodiesCanonical;
1143
+ /**
1144
+ * Make every row a materialization of its record's fold state (W7), once per
1145
+ * database and fold-state version. Runs after the W8 log-integrity scan: a
1146
+ * clean log is rebuilt with the fold; an incomplete (compacted) log rebuilds on
1147
+ * top of the rows as base snapshots; a log with quarantined rows is never
1148
+ * rebuilt from (rows are kept and become base snapshots), and
1149
+ * `store:rematerialized` reports which happened.
1150
+ */
1151
+ private ensureMaterialization;
1152
+ /**
1153
+ * Refuse to open when a schema version in the local operation log has no transform
1154
+ * path to the current schema (RT-103). Operations of a NEWER schema (authored by an
1155
+ * upgraded peer) are not checked: they are kept aside until this device upgrades.
1156
+ */
1157
+ private assertTransformCoverage;
1158
+ /**
1159
+ * Re-fold the records of every collection whose fold plan changed since the rows
1160
+ * were materialized (RT-63): a field that became `merge('counter')`, an array that
1161
+ * became append-only, a new or edited resolver. Their stored states hold fields of
1162
+ * the old kind, which the fold refuses (`FoldStateError`). Each record is rebuilt
1163
+ * from its log with the new plan; compacted bases and row snapshots are adapted
1164
+ * (a re-planned field restarts from its materialized value at its version). The
1165
+ * client's equivalent of the server stores' `fold_plan_fingerprint`.
1166
+ */
1167
+ private ensureFoldPlan;
1168
+ /**
1169
+ * Row snapshots were created (RT-68): ask the sync server for a full resync (the
1170
+ * delivery watermarks restart at 0, as after a restore), so each such record gets
1171
+ * its complete history back; {@link settleSnapshotRecords} then drops the
1172
+ * snapshots once the resync has caught up.
1173
+ */
1174
+ private requestSnapshotResync;
1175
+ /**
1176
+ * Records whose state still folds against a row snapshot (approximate: a late
1177
+ * concurrent write older than a field's version is not folded, RT-68).
1178
+ */
1179
+ getSnapshotRecords(): Promise<Array<{
1180
+ collection: string;
1181
+ recordId: string;
1182
+ }>>;
1183
+ /**
1184
+ * Called by sync when a delivery stream has caught up (its final batch fully
1185
+ * applied). After the full resync {@link requestSnapshotResync} asked for, every
1186
+ * row-snapshot record whose insert is back in the log is re-folded from its
1187
+ * complete history and stops being approximate (RT-68). Also retires the
1188
+ * provisional cascades of remote deletes (RT-69): the server's own copies were
1189
+ * delivered by now, and any it did not derive must not stay applied locally.
1190
+ *
1191
+ * With `provisionalOnly` only the provisional cascades are retired (RT-93: a stream
1192
+ * batch from a server that derives no cascades, which is not the end of a resync).
1193
+ *
1194
+ * @param options - `provisionalOnly`: leave row snapshots for the next catch-up
1195
+ * @returns How many snapshots were dropped
1196
+ */
1197
+ settleAfterCatchUp(options?: {
1198
+ provisionalOnly?: boolean;
1199
+ }): Promise<number>;
1200
+ /**
1201
+ * Apply the cascades / set-nulls of a REMOTE delete as local-only provisional
1202
+ * effects (RT-69): folded into the children like operations, but never logged,
1203
+ * sequenced or queued for upload. The server derives and relays its own copy;
1204
+ * when it arrives (same parent, same record) the provisional effect is retired.
1205
+ *
1206
+ * @param parent - The remote delete
1207
+ * @param effects - Its side effects, as the referential check derived them
1208
+ */
1209
+ applyProvisionalSideEffects(parent: Operation, effects: ReadonlyArray<{
1210
+ type: 'delete' | 'update';
1211
+ collection: string;
1212
+ recordId: string;
1213
+ data: Record<string, unknown> | null;
1214
+ previousData: Record<string, unknown> | null;
1215
+ ruleId: string;
1216
+ }>): Promise<number>;
1217
+ /**
1218
+ * Node ids whose writes win `merge('server-authoritative')` fields (the server's,
1219
+ * from the sync handshake). Every `kora:server:` id is authoritative by prefix, so
1220
+ * only explicit ids (legacy server node ids, configured extras) are kept: the device
1221
+ * persists the UNION of every explicit id it has learned and never drops one (RT-75).
1222
+ * Handshakes differ per server instance (each lists what it knows), so a list that
1223
+ * omits an id is not a revocation.
1224
+ *
1225
+ * Revocation is explicit (RT-81): ids a handshake lists in `revokedAuthoritativeNodeIds`
1226
+ * are removed from the union, persisted as revoked, and never learned again. Records
1227
+ * of collections with such fields are re-folded only when the explicit set changes
1228
+ * (grows, or loses a revoked id).
1229
+ *
1230
+ * @param nodeIds - The server's authoritative node ids (one handshake's list)
1231
+ * @param revokedNodeIds - Explicit ids the server revoked (one handshake's list)
1232
+ */
1233
+ setAuthoritativeNodeIds(nodeIds: readonly string[], revokedNodeIds?: readonly string[]): Promise<void>;
1234
+ /**
1235
+ * Re-fold the records of these operations from base + log (W7 exclusion: an
1236
+ * operation with a terminal-rejection marker is left out).
1237
+ */
1238
+ private refoldRecordsOf;
1239
+ /**
1240
+ * Append a remote operation to the log. If another operation of the same node
1241
+ * already holds its sequence number (a beta.12 device could produce such
1242
+ * pairs), the log keeps the existing row and this one is recorded in the
1243
+ * sequence-conflicts table, where dedup and record folds still see it.
1244
+ */
1245
+ private appendRemoteOperationRow;
1246
+ /**
1247
+ * Hide a record from this client's authorized materialized view. This writes no
1248
+ * replicated operation and therefore cannot be mistaken for a domain deletion.
1249
+ * The marker lets a later authoritative backfill reactivate the retained row.
1250
+ */
1251
+ applyScopeRetraction(collection: string, recordId: string): Promise<void>;
1252
+ /** Hide all live rows that no longer match a newly accepted server scope. */
1253
+ applyScopeNarrowing(scopes: Record<string, Record<string, unknown>>): Promise<Array<{
1254
+ collection: string;
1255
+ recordId: string;
1256
+ }>>;
1257
+ /**
1258
+ * Materialize a server scope-entry insert (RT-19) whose fields each carry their
1259
+ * own version (RT-27). The entry restates the record's current server values; a
1260
+ * single whole-row stamp would let a field the server last wrote long ago
1261
+ * overwrite this device's newer unsynced edit of it, after which the device's own
1262
+ * operation uploads and wins on the server (divergence). So every field is resolved
1263
+ * on its own, by last-write-wins against this row's `_field_versions`:
1264
+ *
1265
+ * - no row: inserted with each field stamped at its own version, `_created_at`
1266
+ * at the record's creation (the entry's `timestamp`), then any orphaned
1267
+ * updates/deletes are folded in as for any insert;
1268
+ * - live row, or a row hidden by a scope retraction: per-field LWW (a retracted
1269
+ * row is shown again; `_created_at` is kept);
1270
+ * - domain tombstone: revived only when the entry's newest field version is
1271
+ * newer than the tombstone, then per-field LWW.
1272
+ *
1273
+ * Deterministic and idempotent: the outcome depends only on the stored and
1274
+ * incoming per-field versions, and a re-applied entry wins no field.
1275
+ */
1276
+ private applyFieldVersionedInsert;
769
1277
  /**
770
1278
  * Materialize update/delete operations that were logged for a record BEFORE
771
1279
  * its insert arrived (reordered delivery). Runs inside the insert's write
@@ -829,11 +1337,34 @@ declare class Store implements OperationLog {
829
1337
  */
830
1338
  notifyExternalOperation(operation: Operation): void;
831
1339
  /**
832
- * Build mutation context for a collection (used by ApplyPipeline side effects).
1340
+ * Notify this store that another same-origin runtime changed rows of a collection
1341
+ * in the shared local database without an operation this tab can see (a re-fold,
1342
+ * a scope retraction, a restore; RT-98). Only re-runs the affected live queries.
1343
+ *
1344
+ * @param collection - The collection whose rows changed
1345
+ */
1346
+ notifyExternalChange(collection: string): void;
1347
+ /**
1348
+ * Listen to every committed change this store makes to collection rows: local
1349
+ * writes, remote applies, and changes without a new operation (re-folds, scope
1350
+ * retraction, cascade settling, rematerialization, restore). The cross-tab bus
1351
+ * relays these to other tabs on the same database (RT-98).
1352
+ *
1353
+ * @returns An unsubscribe function
1354
+ */
1355
+ onRecordsChanged(listener: RecordsChangeListener): () => void;
1356
+ /**
1357
+ * Build the local write context for a collection: everything the single local
1358
+ * write path needs (used by Collection writes and by ApplyPipeline).
833
1359
  */
834
1360
  createMutationContext(collection: string, options?: {
835
- inTransaction?: boolean;
836
1361
  extraCausalDeps?: string[];
1362
+ /**
1363
+ * Stamp the writes with this clock instead of the store's (it must be this
1364
+ * node's). Used for the side effects of a remote delete, stamped right after
1365
+ * the delete like the server's copy (seam 5), never with the device's "now".
1366
+ */
1367
+ clock?: HybridLogicalClock;
837
1368
  }): LocalMutationContext;
838
1369
  /**
839
1370
  * Load a materialized row by ID, including soft-deleted tombstones.
@@ -858,6 +1389,25 @@ declare class Store implements OperationLog {
858
1389
  * Persist the last server version vector this client believes the server has applied.
859
1390
  */
860
1391
  saveLastAckedServerVector(vector: VersionVector): Promise<void>;
1392
+ /**
1393
+ * Load the per-device node token the sync server issued for this node id (RT-12).
1394
+ */
1395
+ loadNodeToken(nodeId?: string): Promise<string | null>;
1396
+ /**
1397
+ * Persist the per-device node token under its node id (the current one by default).
1398
+ */
1399
+ saveNodeToken(token: string, nodeId?: string): Promise<void>;
1400
+ /**
1401
+ * Load the node ids the sync server named authoritative (protocol v2), or null when
1402
+ * no protocol-2 server ever answered.
1403
+ */
1404
+ loadAuthoritativeNodeIds(): Promise<string[] | null>;
1405
+ /**
1406
+ * Persist the node ids the sync server named authoritative (protocol v2). Same as
1407
+ * {@link setAuthoritativeNodeIds}: merged into the union of learned explicit ids,
1408
+ * and the fold re-folds affected records only when that set grows.
1409
+ */
1410
+ saveAuthoritativeNodeIds(nodeIds: string[]): Promise<void>;
861
1411
  /**
862
1412
  * Load persisted delta cursor for resuming paginated initial sync.
863
1413
  */
@@ -883,6 +1433,31 @@ declare class Store implements OperationLog {
883
1433
  * Delete a persisted view watermark (safe: the view back-fills from 0 when next visited).
884
1434
  */
885
1435
  deleteDeliveryWatermark(signature: string): Promise<void>;
1436
+ /**
1437
+ * Record delivered operations the sync engine deliberately did not apply (inbound
1438
+ * quarantine, W4) and, when given, advance a view's delivery watermark in the same
1439
+ * transaction, so the watermark never passes an operation that is neither applied nor
1440
+ * recorded.
1441
+ */
1442
+ saveInboundQuarantine(entries: UnappliedOperation[], watermark?: {
1443
+ signature: string;
1444
+ watermark: number;
1445
+ }): Promise<void>;
1446
+ /** Every quarantined inbound operation, oldest delivery first. */
1447
+ loadInboundQuarantine(): Promise<UnappliedOperation[]>;
1448
+ /** Remove quarantined inbound operations (applied on replay, or reconciled). */
1449
+ removeInboundQuarantine(operationIds: string[]): Promise<void>;
1450
+ /**
1451
+ * The contiguous acknowledged prefix of this device's own operations for a node id,
1452
+ * or null when none was recorded under that contract (W3).
1453
+ */
1454
+ loadOwnAckedThrough(nodeId: string): Promise<number | null>;
1455
+ /** Persist the contiguous acknowledged own-operation prefix for a node id. */
1456
+ saveOwnAckedThrough(nodeId: string, sequence: number): Promise<void>;
1457
+ /** The downlink scope the sync server last accepted (null when none). */
1458
+ loadAcceptedDownlinkScope(): Promise<Record<string, Record<string, unknown>> | null>;
1459
+ /** Persist (or clear) the downlink scope the sync server last accepted. */
1460
+ saveAcceptedDownlinkScope(scope: Record<string, Record<string, unknown>> | null): Promise<void>;
886
1461
  /**
887
1462
  * Local operations not yet reflected on the server version vector.
888
1463
  */
@@ -898,14 +1473,155 @@ declare class Store implements OperationLog {
898
1473
  * No data-change notifications are emitted: materialized values are
899
1474
  * unchanged, only version stamps move.
900
1475
  */
1476
+ /**
1477
+ * Move this device to a fresh node id, re-authoring its never-acknowledged
1478
+ * operations under it (RT-21). Used when the sync server refuses the current node
1479
+ * id (`NODE_ID_CLAIMED`): the device keeps every local write and uploads it under
1480
+ * the new id. Acknowledged history stays under the old id. Persisted atomically;
1481
+ * later writes use the new id.
1482
+ *
1483
+ * @param unsyncedOpIds - Ids of this node's operations the server never acknowledged
1484
+ * @returns The new node id and the rewritten operations (for the outbound queue)
1485
+ * @throws {KoraError} When the node id is pinned (`StoreConfig.nodeId`, or per-tab isolation)
1486
+ */
1487
+ rotateNodeId(unsyncedOpIds: string[]): Promise<NodeRotationResult>;
1488
+ /**
1489
+ * Move this database back to a node id it authored under before (RT-38): after the
1490
+ * sync server refused the current node, the device tries a node a returning user
1491
+ * owns. Nothing is rewritten; later writes use `nodeId`.
1492
+ *
1493
+ * @throws {KoraError} When the node id is pinned, or `nodeId` is not a local node
1494
+ */
1495
+ switchNodeId(nodeId: string): Promise<void>;
1496
+ /** Point the clock, sequence counter and collections at `nodeId` (already persisted). */
1497
+ private rebindToNode;
1498
+ /**
1499
+ * Bind this database's writes to the signed-in user (RT-42). Call it whenever the
1500
+ * signed-in user is known or changes, BEFORE the next local write (`createApp` does
1501
+ * so at start and on every auth change of `sync.authClient`).
1502
+ *
1503
+ * - The current node belongs to `principal`: nothing changes.
1504
+ * - The current node is unbound and has no history (no write, never synced): it is
1505
+ * bound to `principal` (every write under it from now on is theirs).
1506
+ * - The current node is unbound but has history (a database from before RT-42, or
1507
+ * writes made while nobody was signed in): its owner is NOT guessed (RT-50). The
1508
+ * store moves to `principal`'s own node, and the old node's owner is learned from
1509
+ * the sync server (the first accepted handshake as it binds it; a refusal rules
1510
+ * that user out). A node that never synced cannot be attributed by anyone: its
1511
+ * unsynced writes are held for the app to assign or discard (`app.sync.assignHeld`,
1512
+ * `app.sync.discardHeld`).
1513
+ * - It belongs to another user: the store moves to `principal`'s own local node (the
1514
+ * most recent one, not held), or to a fresh node bound to `principal`. Nothing is
1515
+ * rewritten: the other user's unsynced writes stay under their node, and sync never
1516
+ * uploads them on this user's session (they count as `heldOperations`).
1517
+ *
1518
+ * A pinned node id (`StoreConfig.nodeId`, such as an auth device id) cannot move:
1519
+ * `conflict` is then true when it belongs to another user, and sync refuses to
1520
+ * upload it under `principal`; an unbound pinned node with history stays unbound
1521
+ * until the server accepts it for a user. Under `per-tab` isolation the tab moves to
1522
+ * a fresh per-tab node.
1523
+ *
1524
+ * @param principal - The signed-in user id
1525
+ * @returns The node now in use and whether it changed
1526
+ */
1527
+ bindPrincipal(principal: string): Promise<PrincipalBinding>;
1528
+ /**
1529
+ * A sync handshake as `nodeId` was accepted for `principal` (RT-50): bind an unbound
1530
+ * node (or replace a guessed binding) from the server's answer.
1531
+ */
1532
+ confirmNodePrincipal(nodeId: string, principal: string): Promise<void>;
1533
+ /**
1534
+ * The sync server refused `nodeId` for `principal` (RT-50): clear a guessed binding,
1535
+ * and remember that an unbound node is not theirs.
1536
+ */
1537
+ recordNodeRefusedFor(nodeId: string, principal: string): Promise<void>;
1538
+ /**
1539
+ * Assign a held, unbound local node's writes to `principal` (RT-50): an explicit app
1540
+ * decision for writes nobody can attribute (a database that never synced).
1541
+ *
1542
+ * @returns false when the node is unknown, the store's own, already bound, or was
1543
+ * refused for `principal` by the server
1544
+ */
1545
+ assignNodePrincipal(nodeId: string, principal: string): Promise<boolean>;
1546
+ /**
1547
+ * Forget a local node other than the store's own whatever it still holds (RT-50: the
1548
+ * app discarded its held writes, which were marked never to upload first).
1549
+ */
1550
+ dropLocalNode(nodeId: string): Promise<void>;
1551
+ /** The adoption schedule of this database's local nodes (RT-46). */
1552
+ loadAdoptionSchedule(): Promise<AdoptionSchedule>;
1553
+ /** Persist the adoption schedule (RT-46). */
1554
+ saveAdoptionSchedule(schedule: AdoptionSchedule): Promise<void>;
1555
+ /**
1556
+ * Raise a local node's sequence counter to at least `floor`, in a transaction (the
1557
+ * same MAX every reservation uses, W6), so the next write never reuses a number the
1558
+ * server already holds (RT-35: the device lost the tail of its log, for example on a
1559
+ * reload inside the IndexedDB snapshot window or after restoring an older copy).
1560
+ *
1561
+ * @returns Whether the counter moved
1562
+ */
1563
+ raiseSequenceFloor(nodeId: string, floor: number): Promise<boolean>;
1564
+ /**
1565
+ * Give one of a local node's operations a fresh sequence number above `floor`
1566
+ * (RT-35): the server refused it with `SEQUENCE_CONFLICT` because it holds another
1567
+ * operation of this node under that number (one this device lost). A version-1
1568
+ * operation keeps its id (its hash does not cover the sequence number), exactly like
1569
+ * the W6 sequence repair; a version-2 operation (protocol v2) is re-hashed under the
1570
+ * new number, so it carries a new id. The old identity is recorded in
1571
+ * `_kora_seq_conflicts` (`reemitted_as`).
1572
+ *
1573
+ * A new id would leave later operations naming the old one in `causalDeps`: those in
1574
+ * `rewritableDependents` (operations the server never stored: never sent) are
1575
+ * rewritten to name the new id, and re-hashed, transitively, in the same
1576
+ * transaction. Dependents already sent keep their ids (the server may hold them);
1577
+ * their dep resolves through the `_kora_seq_conflicts` record. Every touched record
1578
+ * is re-folded (fold stamps carry operation ids).
1579
+ *
1580
+ * @param rewritableDependents - Ids of this node's operations that were never sent
1581
+ * @returns The renumbered operation and the rewritten dependents, or null when the
1582
+ * operation is not in the log
1583
+ */
1584
+ resequenceOperation(operationId: string, nodeId: string, floor: number, rewritableDependents?: readonly string[]): Promise<ResequenceResult | null>;
1585
+ /**
1586
+ * Durability barrier (RT-35): resolves once every write committed so far is durable
1587
+ * on this device. Sync awaits it before an operation leaves the device.
1588
+ */
1589
+ ensureDurable(): Promise<void>;
1590
+ /** Record operations the server refused for good (RT-36). Never cleared. */
1591
+ recordTerminalRejections(entries: TerminalRejection[]): Promise<void>;
1592
+ /** Which of these operation ids the server refused for good (RT-36). */
1593
+ findTerminalRejections(operationIds: string[]): Promise<Set<string>>;
1594
+ /** Every node id this database authored operations under (RT-38, RT-40). */
1595
+ listLocalNodes(): Promise<LocalNodeRecord[]>;
1596
+ /** Forget a drained local node other than this store's own (RT-40). */
1597
+ forgetLocalNode(nodeId: string): Promise<void>;
1598
+ /** The current refusal cycle: the count of accepted sync handshakes (RT-38). */
1599
+ loadAcceptedCycle(): Promise<number>;
1600
+ /** Record an accepted sync handshake as `nodeId` (RT-38). */
1601
+ markLocalNodeAccepted(nodeId: string): Promise<void>;
1602
+ /** Record that the sync server refused `nodeId`; `held` holds its writes (RT-38). */
1603
+ markLocalNodeRefused(nodeId: string, held: boolean): Promise<void>;
1604
+ /**
1605
+ * Take over another local node's unsynced writes (RT-40): resolves to a release
1606
+ * function when no live tab uses `nodeId` (its lock is free), or null when one does.
1607
+ * Without Web Locks (Node, old browsers) the node is assumed free.
1608
+ */
1609
+ claimLocalNode(nodeId: string): Promise<(() => void) | null>;
1610
+ /** Whether a live tab holds `nodeId` (per-tab isolation, RT-40). */
1611
+ isLocalNodeLive(nodeId: string): Promise<boolean>;
901
1612
  rebaseUnsyncedOperations(unsyncedOpIds: string[], correctedNowMs: number): Promise<ClockRebaseResult>;
902
1613
  /**
903
1614
  * Count of local operations ahead of the server version vector.
904
1615
  */
905
1616
  countUnsyncedOperations(serverVector: VersionVector): Promise<number>;
906
1617
  /**
907
- * Compact the local operation log using materialized rows as the baseline.
908
- * Only removes ops the server has acknowledged (per {@link CompactionStrategy}).
1618
+ * Compact the local operation log. Only removes ops the server has acknowledged
1619
+ * (per {@link CompactionStrategy}). With the W7 fold (STORE-14), the effect of the
1620
+ * removed operations is first joined into each record's base fold state, so a
1621
+ * re-fold (rejection exclusion, a later re-materialization) and the merge of a
1622
+ * late operation stay exact; delete, atomic and custom-resolver operations are
1623
+ * kept, and a re-delivered compacted id is a duplicate by its node's compacted
1624
+ * prefix.
909
1625
  */
910
1626
  compact(strategy: CompactionStrategy): Promise<CompactionResult>;
911
1627
  /**
@@ -958,13 +1674,46 @@ declare class Store implements OperationLog {
958
1674
  */
959
1675
  exportBackup(options?: BackupOptions): Promise<Uint8Array>;
960
1676
  /**
961
- * Restore data from a backup binary.
1677
+ * Restore data from a backup binary (format version 2, STORE-5).
1678
+ *
1679
+ * - `merge: true`: every backup operation is applied through the remote-apply path
1680
+ * (deduplicated by id, merged per field), the version vector advances by MAX, and
1681
+ * nothing of the exporting device's identity or sync state is imported.
1682
+ * - Replace (default): the data is replaced by the backup's while this device keeps
1683
+ * its node id, local nodes and principal bindings, its own sequence counters never
1684
+ * move backwards, and unsynced own writes are kept (see
1685
+ * `RestoreOptions.keepUnsyncedWrites`). Then {@link reloadFromDisk} runs.
1686
+ *
1687
+ * A version-1 file is refused (`errorCode: 'BACKUP_FORMAT_OUTDATED'`); convert it with
1688
+ * `convertBackupV1`.
962
1689
  *
963
1690
  * @param data - The backup data
964
- * @param options - Restore options (merge, collections, onProgress)
1691
+ * @param options - Restore options (merge, collections, keepUnsyncedWrites, onProgress)
1692
+ * @param internal - `applyOperation`: the app's remote-apply path (defaults to
1693
+ * {@link applyRemoteOperation}); `createApp` passes the merge-aware pipeline sync uses
965
1694
  * @returns Result of the restore operation
966
1695
  */
967
- importBackup(data: Uint8Array, options?: RestoreOptions): Promise<RestoreResult>;
1696
+ importBackup(data: Uint8Array, options?: RestoreOptions, internal?: {
1697
+ applyOperation?: (operation: Operation) => Promise<ApplyResult>;
1698
+ }): Promise<RestoreResult>;
1699
+ /**
1700
+ * Merge mode (RT-66): join the backup's compacted base states and row snapshots
1701
+ * into this database's, and re-fold those records, before the backup's operations
1702
+ * are applied. The join is the fold's state-based merge, so the result equals
1703
+ * folding both devices' histories together. Compacted prefixes advance by MAX
1704
+ * (unfiltered restores only: the prefix is per node, across collections).
1705
+ */
1706
+ private mergeBackupFoldState;
1707
+ /**
1708
+ * Re-read this store's state from the database after it changed underneath it (a
1709
+ * backup restore): the node id, version vector and own sequence counter, the clock
1710
+ * (advanced past every timestamp in the log), and a fresh causal tracker. Every live
1711
+ * query re-runs. The local node registry and terminal rejections are always read
1712
+ * from the database, so they need no reload.
1713
+ */
1714
+ reloadFromDisk(): Promise<void>;
1715
+ /** The greatest HLC timestamp in the operation log (canonical strings sort by HLC). */
1716
+ private loadNewestLogTimestamp;
968
1717
  /**
969
1718
  * Read backup manifest without loading the entire backup.
970
1719
  *
@@ -973,21 +1722,22 @@ declare class Store implements OperationLog {
973
1722
  */
974
1723
  static readBackupManifest(data: Uint8Array): BackupManifest;
975
1724
  private recordOperationSequence;
976
- private allocateSequenceNumber;
1725
+ /** Post-commit publication of a committed local operation. */
1726
+ private publishLocalOperation;
977
1727
  /**
978
- * Check the stored schema version and run any pending migrations.
979
- * Migrations are applied in version order within a transaction.
1728
+ * Map an out-of-space storage failure to `store:quota-exceeded` (STORE-15).
1729
+ * The error itself still propagates to the caller; this only makes the
1730
+ * condition observable on every adapter, not just IndexedDB.
980
1731
  */
981
- private runMigrationsIfNeeded;
1732
+ private reportStorageError;
1733
+ /** Report a database a newer build migrated (`store:schema-ahead`) and build the error. */
1734
+ private schemaAhead;
982
1735
  /**
983
- * Get the stored schema version from _kora_meta. Returns 0 if not set.
1736
+ * Run pending schema migrations (STORE-13, NEW-STORE-1): one transaction per version
1737
+ * (DDL, backfills, `schema_version`); backfills write operations through the local
1738
+ * write path so they sync, unless declared `localOnly`.
984
1739
  */
985
- private getStoredSchemaVersion;
986
- /**
987
- * Run a backfill transform on all records in a collection.
988
- * Reads all rows, applies the transform, and updates changed fields.
989
- */
990
- private runBackfill;
1740
+ private runMigrationsIfNeeded;
991
1741
  private loadOrGenerateNodeId;
992
1742
  private loadSequenceNumber;
993
1743
  private loadVersionVector;
@@ -1004,6 +1754,147 @@ interface CollectionAccessor {
1004
1754
  where(conditions: Record<string, unknown>): QueryBuilder;
1005
1755
  }
1006
1756
 
1757
+ declare const LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
1758
+ /**
1759
+ * Serialize a version vector for `_kora_meta` storage.
1760
+ */
1761
+ declare function serializeVersionVectorToMeta(vector: VersionVector): string;
1762
+ /**
1763
+ * Deserialize a version vector from `_kora_meta`.
1764
+ */
1765
+ declare function deserializeVersionVectorFromMeta(value: string): VersionVector;
1766
+ /**
1767
+ * Merge two version vectors, keeping the maximum sequence per node.
1768
+ */
1769
+ declare function mergeVersionVectors(a: VersionVector, b: VersionVector): VersionVector;
1770
+ /**
1771
+ * Operations present locally but not yet on the server (per server version vector).
1772
+ */
1773
+ declare function collectOperationsAheadOfServer(localVector: VersionVector, serverVector: VersionVector, fetchRange: (nodeId: string, fromSeq: number, toSeq: number) => Promise<Operation[]>): Promise<Operation[]>;
1774
+
1775
+ /**
1776
+ * Normalise a `where` clause to its canonical form (RT-102). This is the ONE place that
1777
+ * decides what a where value means, so the SQL the store runs, the descriptor and the
1778
+ * query key (store cache, React/Vue/Svelte bindings) always agree:
1779
+ *
1780
+ * - `undefined` means "no condition": `where({ projectId: undefined })` is `where({})`
1781
+ * (the usual "nothing selected yet" pattern), and an operator whose operand is
1782
+ * `undefined` (`{ $gt: undefined }`) is dropped; a field left with no operator is
1783
+ * dropped. A later `.where({ x: undefined })` therefore does not remove an earlier
1784
+ * condition on `x`.
1785
+ * - `null` means SQL `IS NULL` (`$ne: null` means `IS NOT NULL`).
1786
+ * - Non-finite numbers (`NaN`, `Infinity`) have no stored representation: refused.
1787
+ * - `undefined` inside a `$in` list is refused (ambiguous: no condition cannot be one
1788
+ * element of a list).
1789
+ *
1790
+ * @param where - The clause as the developer wrote it
1791
+ * @returns A new clause with the rules above applied
1792
+ * @throws {QueryError} For a non-finite number or an `undefined` `$in` element
1793
+ */
1794
+ declare function normalizeWhere(where: WhereClause): WhereClause;
1795
+ /**
1796
+ * The canonical identity of a query: equal keys if and only if the descriptors run the
1797
+ * same query (RT-102). Used by the store's query-store cache and by every framework
1798
+ * binding to decide when to share a store or re-subscribe.
1799
+ *
1800
+ * Unlike `JSON.stringify`, nothing is dropped or conflated: `where` keys are sorted
1801
+ * (their order does not change the query), `undefined` is dropped exactly as
1802
+ * {@link normalizeWhere} drops it, and `NaN`, `Date` and byte values keep distinct,
1803
+ * typed encodings.
1804
+ *
1805
+ * @param descriptor - The query descriptor (`queryBuilder.getDescriptor()`)
1806
+ * @returns A string key
1807
+ */
1808
+ declare function queryKey(descriptor: QueryDescriptor): string;
1809
+
1810
+ /**
1811
+ * Durable-storage state of this origin, as far as Kora knows (NEW-STORE-4).
1812
+ * - `unknown`: not checked yet (the boot check runs in the background).
1813
+ * - `persisted`: the browser granted persistent storage; it will not evict this
1814
+ * origin's data under storage pressure.
1815
+ * - `best-effort`: storage may be evicted under pressure (the browser default).
1816
+ * - `unsupported`: the runtime has no StorageManager persistence API (Node,
1817
+ * older browsers, some WebViews).
1818
+ * - `error`: the check or request threw; `lastError` says why.
1819
+ */
1820
+ type StoragePersistenceState = 'unknown' | 'persisted' | 'best-effort' | 'unsupported' | 'error';
1821
+ /** Snapshot returned by `app.storage.persistence.status()`. */
1822
+ interface StoragePersistenceStatus {
1823
+ state: StoragePersistenceState;
1824
+ /** True only when the browser reported persistent storage. */
1825
+ persisted: boolean;
1826
+ /** True once `persist()` was asked for (explicitly or by an automatic trigger). */
1827
+ requested: boolean;
1828
+ /** Message of the last failure, when `state` is `error`. */
1829
+ lastError?: string;
1830
+ }
1831
+ /** Why an automatic (background) persistence request was made. */
1832
+ type PersistenceRequestReason = 'explicit' | 'sign-in' | 'first-write' | 'installed-app';
1833
+ /** The subset of `navigator.storage` used here. */
1834
+ interface PersistenceStorageManager {
1835
+ persist?: () => Promise<boolean>;
1836
+ persisted?: () => Promise<boolean>;
1837
+ }
1838
+ /** Options for {@link StoragePersistence}. */
1839
+ interface StoragePersistenceOptions {
1840
+ /** Receives `storage:persistence` events. */
1841
+ emitter?: KoraEventEmitter | null;
1842
+ /**
1843
+ * The StorageManager to use. Defaults to `globalThis.navigator.storage`,
1844
+ * resolved on each call so tests and late polyfills are honoured.
1845
+ */
1846
+ storage?: PersistenceStorageManager | null;
1847
+ }
1848
+ /**
1849
+ * Tracks and requests durable storage (`navigator.storage.persist()`) without ever
1850
+ * putting it on the startup path (NEW-STORE-4).
1851
+ *
1852
+ * In Firefox `persist()` opens a permission prompt and its promise stays pending
1853
+ * until the user answers, so awaiting it inside startup could hang `app.ready`
1854
+ * forever. Kora therefore:
1855
+ * - checks `persisted()` at boot, which never prompts, in the background;
1856
+ * - requests persistence only on an explicit `request()` or, without awaiting,
1857
+ * after a meaningful moment (sign-in, the first local write, running as an
1858
+ * installed app);
1859
+ * - surfaces every result through `status()` and the `storage:persistence` event,
1860
+ * never by blocking.
1861
+ */
1862
+ declare class StoragePersistence {
1863
+ private current;
1864
+ private inFlightRequest;
1865
+ private readonly emitter;
1866
+ private readonly storageOverride;
1867
+ constructor(options?: StoragePersistenceOptions);
1868
+ /** Synchronous snapshot of the last known state. Never prompts. */
1869
+ status(): StoragePersistenceStatus;
1870
+ /**
1871
+ * Read whether storage is already persistent (`navigator.storage.persisted()`).
1872
+ * Never prompts and never throws: a failure becomes `state: 'error'`.
1873
+ */
1874
+ check(): Promise<StoragePersistenceStatus>;
1875
+ /**
1876
+ * Ask the browser for persistent storage (`navigator.storage.persist()`).
1877
+ * May show a permission prompt (Firefox), so call it from a user gesture or a
1878
+ * meaningful moment, and do not block rendering on it. Concurrent calls share
1879
+ * one request. Never throws: a failure becomes `state: 'error'`.
1880
+ *
1881
+ * @returns The state after the browser answered
1882
+ */
1883
+ request(): Promise<StoragePersistenceStatus>;
1884
+ /**
1885
+ * Fire-and-forget `request()` for an automatic trigger. Never awaited by Kora,
1886
+ * so a pending prompt cannot block anything; the outcome arrives as a
1887
+ * `storage:persistence` event. Skipped when storage is already persistent or a
1888
+ * request was already made this session.
1889
+ *
1890
+ * @param _reason - Which trigger fired (kept for tracing at call sites)
1891
+ */
1892
+ requestInBackground(_reason: PersistenceRequestReason): void;
1893
+ private storage;
1894
+ private update;
1895
+ private emit;
1896
+ }
1897
+
1007
1898
  /**
1008
1899
  * Bridges the async QueryBuilder.subscribe() API with synchronous snapshot reads
1009
1900
  * required by React useSyncExternalStore, Vue composables, and Svelte stores.
@@ -1017,6 +1908,7 @@ declare class QueryStore<T = CollectionRecord> {
1017
1908
  private unsubscribeQuery;
1018
1909
  private active;
1019
1910
  private hasEmittedSnapshot;
1911
+ private error;
1020
1912
  private readonly queryBuilder;
1021
1913
  constructor(queryBuilder: QueryBuilder<T>);
1022
1914
  /**
@@ -1032,6 +1924,12 @@ declare class QueryStore<T = CollectionRecord> {
1032
1924
  * authoritative result for the current subscription lifecycle.
1033
1925
  */
1034
1926
  hasSnapshot: () => boolean;
1927
+ /**
1928
+ * The latest query failure, or null. Set when the query (or a re-run after a
1929
+ * write) fails; cleared by the next successful delivery. Listeners are notified
1930
+ * on both transitions, so bindings read it next to `getSnapshot()`.
1931
+ */
1932
+ getError: () => Error | null;
1035
1933
  /** Tear down listeners and the underlying query subscription. */
1036
1934
  destroy(): void;
1037
1935
  private startSubscription;
@@ -1347,14 +2245,22 @@ declare function pluralize(word: string): string;
1347
2245
  */
1348
2246
  declare function singularize(word: string): string;
1349
2247
 
2248
+ /** Current backup format version. */
2249
+ declare const BACKUP_VERSION = 2;
2250
+ /** A backup file this release cannot restore as-is. */
2251
+ declare class BackupFormatError extends KoraError {
2252
+ constructor(message: string, code: string, context?: Record<string, unknown>);
2253
+ }
1350
2254
  /**
1351
- * Export a backup from a store.
2255
+ * Export a backup (format version 2) from an opened database.
1352
2256
  *
1353
- * Reads all data via the adapter and produces a portable binary backup.
2257
+ * Everything is read inside one transaction, so the backup is a consistent snapshot.
2258
+ * Operations are exported in canonical form (exactly what `deserializeOperation` reads
2259
+ * back), including tombstones, so a restore rebuilds the same log.
1354
2260
  *
1355
2261
  * @param adapter - The storage adapter
1356
2262
  * @param schema - The schema definition
1357
- * @param nodeId - Current node ID
2263
+ * @param nodeId - Node id of the exporting device (informational only; never imported)
1358
2264
  * @param schemaVersion - Schema version
1359
2265
  * @param options - Backup options
1360
2266
  * @returns Backup as a Uint8Array
@@ -1362,7 +2268,6 @@ declare function singularize(word: string): string;
1362
2268
  declare function exportBackup(adapter: StorageAdapter, schema: SchemaDefinition, nodeId: string, schemaVersion: number, options?: BackupOptions): Promise<Uint8Array>;
1363
2269
  /**
1364
2270
  * Read backup manifest without loading the entire backup.
1365
- * Parses only the first section (which is always the manifest).
1366
2271
  *
1367
2272
  * @param data - The raw backup data
1368
2273
  * @returns The backup manifest
@@ -1375,14 +2280,41 @@ declare function readBackupManifest(data: Uint8Array): BackupManifest;
1375
2280
  * @returns True if the checksum is valid
1376
2281
  */
1377
2282
  declare function verifyBackupChecksum(data: Uint8Array): Promise<boolean>;
2283
+ /** Options for {@link convertBackupV1}. */
2284
+ interface ConvertBackupV1Options {
2285
+ /**
2286
+ * Drop operations whose timestamp cannot be recovered instead of refusing the file.
2287
+ * Default false.
2288
+ */
2289
+ dropUnrecoverable?: boolean;
2290
+ }
1378
2291
  /**
1379
- * Restore a backup into a store.
2292
+ * Convert a version-1 backup (Kora 1.0.0-beta.12 and earlier) to version 2.
1380
2293
  *
1381
- * @param adapter - The storage adapter
2294
+ * - Operations: a v1 file exported from a database that a v1 restore had damaged holds
2295
+ * misread timestamps (`wallTime: null`); they are recovered from the misread values
2296
+ * (the same rule as the log-integrity scan). An unrecoverable one refuses the file
2297
+ * unless `dropUnrecoverable` is set.
2298
+ * - The `meta` section (the exporting device's node id, node tokens, sync state) is
2299
+ * dropped: it must never be imported into another database.
2300
+ * - Record rows are kept; v1 exported live rows only, so the manifest records
2301
+ * `includesTombstones: false`. Bytes that v1 serialized as `{"0":..,"1":..}` objects
2302
+ * are decoded on restore.
2303
+ *
2304
+ * @throws {BackupFormatError} When the input is not a valid version-1 backup
2305
+ */
2306
+ declare function convertBackupV1(data: Uint8Array, options?: ConvertBackupV1Options): Promise<Uint8Array>;
2307
+ /**
2308
+ * Restore a backup into the database behind `adapter`.
2309
+ *
2310
+ * @deprecated Use `store.importBackup()` / `app.importBackup()`: restoring needs the
2311
+ * store (the device's identity, version vector, clock and live queries). This entry
2312
+ * point opens a temporary `Store` on `adapter`, imports, and CLOSES the adapter.
2313
+ *
2314
+ * @param adapter - A storage adapter no store is using
1382
2315
  * @param schema - The schema definition
1383
2316
  * @param data - The backup data
1384
2317
  * @param options - Restore options
1385
- * @returns Result of the restore operation
1386
2318
  */
1387
2319
  declare function restoreBackup(adapter: StorageAdapter, schema: SchemaDefinition, data: Uint8Array, options?: RestoreOptions): Promise<RestoreResult>;
1388
2320
 
@@ -1458,8 +2390,8 @@ declare class OpfsBlobStore implements ContentAddressedBlobStore {
1458
2390
  }
1459
2391
  /**
1460
2392
  * Create an {@link OpfsBlobDirectory} backed by the browser's Origin Private
1461
- * File System, rooted at a named subdirectory. Best-effort requests persistent
1462
- * storage so the browser is less likely to evict blobs under storage pressure.
2393
+ * File System, rooted at a named subdirectory. It does not request persistent
2394
+ * storage: that is `app.storage.persistence`, kept off the startup path.
1463
2395
  *
1464
2396
  * @param rootDirName - The OPFS subdirectory to store blobs under (default `kora-blobs`)
1465
2397
  * @throws {Error} If OPFS is unavailable (e.g. called outside a browser)
@@ -1746,6 +2678,13 @@ interface ChunkResponseMessage {
1746
2678
  requestId: string;
1747
2679
  /** The chunk bytes, or null when the responder does not hold that hash. */
1748
2680
  bytes: Uint8Array | null;
2681
+ /**
2682
+ * The server refused the request for rate, not because the chunk is missing
2683
+ * (RT-24). The requester retries after `retryAfterMs` instead of failing.
2684
+ */
2685
+ throttled?: boolean;
2686
+ /** With `throttled`: how long to wait before retrying, in ms. */
2687
+ retryAfterMs?: number;
1749
2688
  }
1750
2689
  type ChunkMessage = ChunkRequestMessage | ChunkResponseMessage;
1751
2690
  /**
@@ -1763,18 +2702,35 @@ interface RemoteChunkProvider extends ChunkProvider {
1763
2702
  /** Number of requests still awaiting a response (for diagnostics/tests). */
1764
2703
  pendingCount(): number;
1765
2704
  }
2705
+ /** Options for {@link createRemoteChunkProvider}. */
2706
+ interface RemoteChunkProviderOptions {
2707
+ /** Per-attempt timeout in ms (default 30s). */
2708
+ timeoutMs?: number;
2709
+ /**
2710
+ * Longest total time one chunk may spend waiting on "throttled" answers before
2711
+ * the request fails (default 10 minutes). The transfer is resumable, so a failed
2712
+ * request can simply be retried later.
2713
+ */
2714
+ maxThrottleWaitMs?: number;
2715
+ /** Smallest back-off after a throttled answer, in ms (default 250). */
2716
+ minThrottleDelayMs?: number;
2717
+ /** Largest single back-off after a throttled answer, in ms (default 30s). */
2718
+ maxThrottleDelayMs?: number;
2719
+ }
1766
2720
  /**
1767
2721
  * Create a {@link ChunkProvider} that requests chunks over a message port,
1768
2722
  * correlating each answer to its request and timing out a stalled request so a
1769
2723
  * dropped response cannot hang a transfer forever (the transfer is resumable, so
1770
2724
  * a timed-out request can simply be retried).
1771
2725
  *
2726
+ * A "throttled" answer (the server's blob request budget is spent, RT-24) is not a
2727
+ * failure: the request is re-sent after the server's `retryAfterMs` (bounded, with
2728
+ * exponential back-off when absent), until `maxThrottleWaitMs` has elapsed.
2729
+ *
1772
2730
  * @param port - The message port to the peer that holds the blob
1773
- * @param options.timeoutMs - Per-request timeout (default 30s)
2731
+ * @param options - Timeouts and throttle back-off bounds
1774
2732
  */
1775
- declare function createRemoteChunkProvider(port: ChunkMessagePort, options?: {
1776
- timeoutMs?: number;
1777
- }): RemoteChunkProvider;
2733
+ declare function createRemoteChunkProvider(port: ChunkMessagePort, options?: RemoteChunkProviderOptions): RemoteChunkProvider;
1778
2734
  /**
1779
2735
  * Serve blob chunk requests arriving on a message port from a blob store. Answers
1780
2736
  * each `blob-chunk-request` with the bytes for that hash, or null when the store
@@ -1794,4 +2750,4 @@ declare function createChunkPortPair(): {
1794
2750
  b: ChunkMessagePort;
1795
2751
  };
1796
2752
 
1797
- export { AdapterError, ApplyRemoteOptions, type AuditExportManifest, type AuditExportOptions, type AuditExportPayload, type AuditExportProgress, type AuditTraceQuery, type BackupManifest, type BackupOptions, type BackupProgress, type BlobGcOptions, type BlobGcResult, type BlobManifest, type BlobManifestMetadata, COMPACTION_BASELINE_META_KEY, type ChunkMessage, type ChunkMessagePort, type ChunkProvider, type ChunkRequestMessage, type ChunkResponseMessage, type ClockRebaseResult, type CollectionAccessor, CollectionRecord, type CompactionResult, type CompactionStrategy, ContentAddressedBlobStore, type CreateRichTextControllerOptions, DEFAULT_CHUNK_SIZE, InvalidStateTransitionError, LAST_ACKED_SERVER_VECTOR_META_KEY, LocalMutationHandler, MaterializedRowSnapshot, MemoryBlobStore, NoLeaderError, type OpfsBlobDirectory, OpfsBlobStore, OptimisticLockError, OrderByDirection, type PersistedAuditTrace, PersistenceError, QueryBuilder, QueryDescriptor, QueryError, QueryStore, QueryStoreCache, type ReceiveBlobResult, type ReceiveBlobStores, RecordNotFoundError, type RemoteChunkProvider, type ReplaySnapshot, type RestoreOptions, type RestoreResult, type RichTextAwarenessUser, type RichTextController, type RichTextControllerSnapshot, type RichTextCursorInfo, type RichTextSyncEngine, RowVersionState, SequenceManager, type ServerBlobCallbacks, StorageAdapter, Store, StoreConfig, StoreNotOpenError, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, WhereClause, WorkerInitError, WorkerTimeoutError, asRichTextSyncEngine, assertQueryReady, chunkBlob, collectBlobGarbage, collectOperationsAheadOfServer, compactOperationLog, computeAckCompactionWatermark, createChunkPortPair, createMemoryServerBlobStore, createOpfsBlobDirectory, createOpfsBlobStore, createRemoteChunkProvider, createRichTextController, decodeAuditExport, decodeRichtext, deserializeVersionVectorFromMeta, encodeRichtext, exportBackup, extractBlobRefs, fetchBlobManifest, getSharedQueryStoreCache, mergeVersionVectors, parseBlobManifest, persistedAuditTraceFromEvent, pluralize, prepareBlobForSend, putBlobForTransfer, readAuditExportManifest, readBackupManifest, reassembleBlob, receiveBlob, resolveBlobManifest, restoreBackup, richtextStatesEqual, richtextToPlainText, serializeBlobManifest, serializeVersionVectorToMeta, serveBlobChunks, singularize, toServerBlobCallbacks, validateStateTransition, validateUpdateStateMachine, verifyAuditExportChecksum, verifyBackupChecksum };
2753
+ export { AdapterError, AdoptionSchedule, ApplyRemoteOptions, type AuditExportManifest, type AuditExportOptions, type AuditExportPayload, type AuditExportProgress, type AuditTraceQuery, BACKUP_VERSION, BackupFormatError, type BackupManifest, type BackupOptions, type BackupProgress, type BlobGcOptions, type BlobGcResult, type BlobManifest, type BlobManifestMetadata, BridgeTerminatedError, COMPACTION_BASELINE_META_KEY, type ChunkMessage, type ChunkMessagePort, type ChunkProvider, type ChunkRequestMessage, type ChunkResponseMessage, type ClockRebaseResult, type CollectionAccessor, CollectionRecord, type CompactionResult, type CompactionStrategy, ContentAddressedBlobStore, type ConvertBackupV1Options, type CreateRichTextControllerOptions, DEFAULT_CHUNK_SIZE, InvalidStateTransitionError, LAST_ACKED_SERVER_VECTOR_META_KEY, LOG_QUARANTINE_TABLE, LeaderUnresponsiveError, LocalMutationHandler, LocalNodeRecord, type LogIntegrityReport, type LogIntegrityRow, type LogRowProblem, type LogSequenceGap, MaterializedRowSnapshot, MemoryBlobStore, NoLeaderError, type NodeRotationResult, type OpfsBlobDirectory, OpfsBlobStore, OptimisticLockError, OrderByDirection, type PersistedAuditTrace, PersistenceError, type PersistenceRequestReason, type PersistenceStorageManager, type PrincipalBinding, QueryBuilder, QueryDescriptor, QueryError, QueryStore, QueryStoreCache, QuerySubscriptionError, type ReceiveBlobResult, type ReceiveBlobStores, RecordNotFoundError, type RecordsChange, type RecordsChangeListener, type RegisterOptions, type RematerializationMode, type RemoteChunkProvider, type RemoteChunkProviderOptions, type ReplaySnapshot, RequestAbortedError, type ResequenceResult, type RestoreOptions, type RestoreResult, type ResultsEqual, type RichTextAwarenessUser, type RichTextController, type RichTextControllerSnapshot, type RichTextCursorInfo, type RichTextSyncEngine, RowVersionState, SchemaVersionAheadError, SequenceManager, type ServerBlobCallbacks, StorageAdapter, StorageBackendMismatchError, StorageDurabilityError, StorageInUseError, StoragePersistence, type StoragePersistenceOptions, type StoragePersistenceState, type StoragePersistenceStatus, Store, StoreConfig, StoreNotOpenError, SubscribeOptions, SubscriptionBloomFilter, SubscriptionCallback, SubscriptionManager, type SubscriptionManagerOptions, type SubscriptionStats, TerminalRejection, type TransactionCollectionAccessor, TransactionContext, type TransactionContextConfig, type UnappliedOperation, UnsyncedDataError, WhereClause, WorkerInitError, WorkerTimeoutError, asRichTextSyncEngine, assertQueryReady, chunkBlob, collectBlobGarbage, collectOperationsAheadOfServer, compactOperationLog, computeAckCompactionWatermark, convertBackupV1, createChunkPortPair, createMemoryServerBlobStore, createOpfsBlobDirectory, createOpfsBlobStore, createRemoteChunkProvider, createRichTextController, decodeAuditExport, decodeRichtext, deserializeVersionVectorFromMeta, encodeRichtext, exportBackup, extractBlobRefs, fetchBlobManifest, getSharedQueryStoreCache, mergeVersionVectors, normalizeWhere, parseBlobManifest, persistedAuditTraceFromEvent, pluralize, prepareBlobForSend, putBlobForTransfer, queryKey, readAuditExportManifest, readBackupManifest, reassembleBlob, receiveBlob, resolveBlobManifest, restoreBackup, richtextStatesEqual, richtextToPlainText, serializeBlobManifest, serializeVersionVectorToMeta, serveBlobChunks, singularize, toServerBlobCallbacks, validateStateTransition, validateUpdateStateMachine, verifyAuditExportChecksum, verifyBackupChecksum };