@korajs/store 1.0.0-beta.12 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -37
- package/dist/adapters/better-sqlite3.cjs +90 -4
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +21 -12
- package/dist/adapters/better-sqlite3.d.ts +21 -12
- package/dist/adapters/better-sqlite3.js +69 -5
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +1316 -610
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +29 -3
- package/dist/adapters/indexeddb.d.ts +29 -3
- package/dist/adapters/indexeddb.js +247 -221
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +856 -63
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +529 -63
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +1032 -344
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +239 -18
- package/dist/adapters/sqlite-wasm.d.ts +239 -18
- package/dist/adapters/sqlite-wasm.js +111 -5
- package/dist/adapters/sqlite-wasm.js.map +1 -1
- package/dist/backup-IHKVMUAM.js +25 -0
- package/dist/chunk-5AQSGMJV.js +5471 -0
- package/dist/chunk-5AQSGMJV.js.map +1 -0
- package/dist/chunk-5JMGMPUE.js +591 -0
- package/dist/chunk-5JMGMPUE.js.map +1 -0
- package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
- package/dist/chunk-A6GFH5IG.js +36 -0
- package/dist/chunk-A6GFH5IG.js.map +1 -0
- package/dist/chunk-BILSA6XZ.js +608 -0
- package/dist/chunk-BILSA6XZ.js.map +1 -0
- package/dist/chunk-BQSIL6TT.js +1095 -0
- package/dist/chunk-BQSIL6TT.js.map +1 -0
- package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
- package/dist/chunk-J5AGXF3D.js.map +1 -0
- package/dist/chunk-KK3GBONL.js +112 -0
- package/dist/chunk-KK3GBONL.js.map +1 -0
- package/dist/chunk-KTVNANUH.js +876 -0
- package/dist/chunk-KTVNANUH.js.map +1 -0
- package/dist/chunk-QW4UU7NG.js +1003 -0
- package/dist/chunk-QW4UU7NG.js.map +1 -0
- package/dist/chunk-SRHDJF5B.js +186 -0
- package/dist/chunk-SRHDJF5B.js.map +1 -0
- package/dist/chunk-UC2LGFPV.js +810 -0
- package/dist/chunk-UC2LGFPV.js.map +1 -0
- package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
- package/dist/index.cjs +9533 -4246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1143 -110
- package/dist/index.d.ts +1143 -110
- package/dist/index.js +298 -3191
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +581 -264
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +15 -85
- package/dist/internal.d.ts +15 -85
- package/dist/internal.js +10 -8
- package/dist/restore-MNDPR557.js +299 -0
- package/dist/restore-MNDPR557.js.map +1 -0
- package/dist/sql-builder-D0tC3sv9.d.cts +615 -0
- package/dist/sql-builder-D5UMzHEo.d.ts +615 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
- package/dist/sqlite-wasm-channel-QCCNBYCU.js +12 -0
- package/dist/store-HNGX4KTT.js +16 -0
- package/dist/store-HNGX4KTT.js.map +1 -0
- package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
- package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
- package/package.json +3 -2
- package/dist/backup-5S7K2Z35.js +0 -14
- package/dist/chunk-GSJ7EEUC.js.map +0 -1
- package/dist/chunk-HS6DBMST.js +0 -383
- package/dist/chunk-HS6DBMST.js.map +0 -1
- package/dist/chunk-RJIEKEN4.js +0 -751
- package/dist/chunk-RJIEKEN4.js.map +0 -1
- package/dist/chunk-TUR7RGEP.js +0 -84
- package/dist/chunk-TUR7RGEP.js.map +0 -1
- package/dist/chunk-VDBJQ75K.js +0 -78
- package/dist/chunk-VDBJQ75K.js.map +0 -1
- package/dist/chunk-YT7MRFCP.js +0 -687
- package/dist/chunk-YT7MRFCP.js.map +0 -1
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
- package/dist/types-BOO9EPt1.d.ts +0 -170
- package/dist/types-C_GDavG-.d.cts +0 -170
- /package/dist/{backup-5S7K2Z35.js.map → backup-IHKVMUAM.js.map} +0 -0
- /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
- /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-QCCNBYCU.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,
|
|
2
|
-
export { M as MigrationPlan,
|
|
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 './
|
|
6
|
-
export { C as Collection } from './
|
|
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-D0tC3sv9.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-D0tC3sv9.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).
|
|
@@ -86,6 +157,14 @@ declare class NoLeaderError extends KoraError {
|
|
|
86
157
|
declare class PersistenceError extends KoraError {
|
|
87
158
|
constructor(message: string, context?: Record<string, unknown>);
|
|
88
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Thrown by a local write while the store's pinned node id (an auth device id) belongs
|
|
162
|
+
* to another user than the one signed in (F9): a browser two users shared. The write
|
|
163
|
+
* could only be stored under the owner's node and would later upload as the owner's.
|
|
164
|
+
*/
|
|
165
|
+
declare class NodeOwnedByAnotherUserError extends KoraError {
|
|
166
|
+
constructor(nodeId: string, principal: string | null);
|
|
167
|
+
}
|
|
89
168
|
|
|
90
169
|
/**
|
|
91
170
|
* How the local operation log may be compacted.
|
|
@@ -124,6 +203,27 @@ declare function computeAckCompactionWatermark(serverVector: VersionVector): Ver
|
|
|
124
203
|
*/
|
|
125
204
|
declare function compactOperationLog(adapter: StorageAdapter, schema: SchemaDefinition, strategy: CompactionStrategy, serverVector: VersionVector): Promise<CompactionResult>;
|
|
126
205
|
|
|
206
|
+
/**
|
|
207
|
+
* How a database is re-materialized on its first open with the W7 fold:
|
|
208
|
+
*
|
|
209
|
+
* - `'log'`: the log is clean (W8 integrity scan): every record is rebuilt with the
|
|
210
|
+
* fold from its operations. This also repairs replicas that diverged under the
|
|
211
|
+
* pairwise merge of beta.12/13.
|
|
212
|
+
* - `'snapshot+log'`: the log is not complete (it was compacted, or has sequence
|
|
213
|
+
* gaps): each existing row becomes the record's row snapshot (its values at
|
|
214
|
+
* their `_field_versions`, exact where a stored fold state still has them), and
|
|
215
|
+
* the remaining operations are folded on top.
|
|
216
|
+
* - `'kept'`: the log has quarantined rows (W8). Only the records that own a
|
|
217
|
+
* quarantined row are kept exactly as they are (their row becomes their snapshot,
|
|
218
|
+
* nothing is rebuilt from their log); every other record is rebuilt as in `'log'`
|
|
219
|
+
* or `'snapshot+log'` (RT-68).
|
|
220
|
+
*
|
|
221
|
+
* Row snapshots are approximate (see `createSnapshotState`); records that fold
|
|
222
|
+
* against one are listed in `_kora_fold_snapshot` until their history is complete
|
|
223
|
+
* again (a scope entry with the server's fold state, or a full resync).
|
|
224
|
+
*/
|
|
225
|
+
type RematerializationMode = 'log' | 'snapshot+log' | 'kept';
|
|
226
|
+
|
|
127
227
|
/**
|
|
128
228
|
* Result of re-stamping unsynced operations after a clock correction.
|
|
129
229
|
*/
|
|
@@ -138,23 +238,39 @@ interface ClockRebaseResult {
|
|
|
138
238
|
newMaxTimestamp: HLCTimestamp | null;
|
|
139
239
|
}
|
|
140
240
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
241
|
+
/** Result of {@link rotateUnsyncedOperationsInLog}. */
|
|
242
|
+
interface NodeRotationResult {
|
|
243
|
+
/** The node id the device uses from now on. */
|
|
244
|
+
nodeId: string;
|
|
245
|
+
/** The rewritten operations under the new node id, in their original causal order. */
|
|
246
|
+
operations: Operation[];
|
|
247
|
+
/** Maps each old operation id to its new content-addressed id. */
|
|
248
|
+
idMapping: Record<string, string>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Result of `Store.resequenceOperation` (RT-35 renumbering). */
|
|
252
|
+
interface ResequenceResult {
|
|
253
|
+
/** The renumbered operation (a version-2 one under a new id). */
|
|
254
|
+
operation: Operation;
|
|
255
|
+
/** Never-sent dependents rewritten to name the new id (re-hashed), in sequence order. */
|
|
256
|
+
dependents: Operation[];
|
|
257
|
+
/** Old id -> new id, for the operation and every re-hashed dependent. */
|
|
258
|
+
idMapping: Record<string, string>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A delivered operation the client did not apply, as stored in `_kora_unapplied_ops`. */
|
|
262
|
+
interface UnappliedOperation {
|
|
263
|
+
/** The operation as delivered (still encrypted when decryption failed). */
|
|
264
|
+
operation: Operation;
|
|
265
|
+
/** Delivery sequence of the batch that carried it, or null for a legacy batch. */
|
|
266
|
+
deliverySequence: number | null;
|
|
267
|
+
/** Machine-readable reason. */
|
|
268
|
+
code: string;
|
|
269
|
+
/** Human-readable explanation. */
|
|
270
|
+
message: string;
|
|
271
|
+
/** Wall-clock time (ms) it was quarantined. Display only. */
|
|
272
|
+
quarantinedAt: number;
|
|
273
|
+
}
|
|
158
274
|
|
|
159
275
|
/**
|
|
160
276
|
* A merge or constraint decision persisted for enterprise audit export.
|
|
@@ -233,11 +349,14 @@ interface AuditTraceQuery {
|
|
|
233
349
|
* Manifest embedded in every backup file.
|
|
234
350
|
*/
|
|
235
351
|
interface BackupManifest {
|
|
236
|
-
/** Backup format version */
|
|
237
|
-
version:
|
|
352
|
+
/** Backup format version (2 since STORE-5; version 1 files need `convertBackupV1`) */
|
|
353
|
+
version: number;
|
|
238
354
|
/** When the backup was created (epoch ms) */
|
|
239
355
|
createdAt: number;
|
|
240
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Node ID of the originating device. Informational only: a restore never copies a
|
|
358
|
+
* device identity into the importing database.
|
|
359
|
+
*/
|
|
241
360
|
nodeId: string;
|
|
242
361
|
/** Schema version at backup time */
|
|
243
362
|
schemaVersion: number;
|
|
@@ -247,6 +366,20 @@ interface BackupManifest {
|
|
|
247
366
|
collections: string[];
|
|
248
367
|
/** Whether materialized records are included */
|
|
249
368
|
includesRecords: boolean;
|
|
369
|
+
/** Whether the records include deleted rows (tombstones). False for converted v1 files. */
|
|
370
|
+
includesTombstones?: boolean;
|
|
371
|
+
/** Set on a file produced by `convertBackupV1`. */
|
|
372
|
+
convertedFrom?: number;
|
|
373
|
+
/**
|
|
374
|
+
* The file carries the W7 fold's per-record base states (`fold_base`), row
|
|
375
|
+
* snapshots (`fold_snapshot`) and the compacted prefixes (`compacted_through`), so
|
|
376
|
+
* a compacted device's history survives a restore on another device (RT-66).
|
|
377
|
+
* Absent on files from earlier releases: their log may be incomplete (compacted),
|
|
378
|
+
* so a restore rebuilds their records on top of the backup's rows.
|
|
379
|
+
*/
|
|
380
|
+
includesFoldState?: boolean;
|
|
381
|
+
/** The exporting database had compacted its log (it then needs `fold_base`). */
|
|
382
|
+
compacted?: boolean;
|
|
250
383
|
/** SHA-256 hex checksum of all content sections */
|
|
251
384
|
checksum: string;
|
|
252
385
|
}
|
|
@@ -271,6 +404,24 @@ interface RestoreResult {
|
|
|
271
404
|
success: boolean;
|
|
272
405
|
/** Error message if failed */
|
|
273
406
|
error?: string;
|
|
407
|
+
/** Error code if failed (`BACKUP_CHECKSUM_MISMATCH`, `BACKUP_OPERATION_INVALID`, ...) */
|
|
408
|
+
errorCode?: string;
|
|
409
|
+
/**
|
|
410
|
+
* Set when the file was a version-1 backup (Kora 1.0.0-beta.12 and earlier) that the
|
|
411
|
+
* import converted first, as `convertBackupV1` does.
|
|
412
|
+
*/
|
|
413
|
+
convertedFromVersion?: 1;
|
|
414
|
+
/**
|
|
415
|
+
* Replace mode: this device's own writes the sync server had not acknowledged,
|
|
416
|
+
* re-applied on top of the restored data (see `RestoreOptions.keepUnsyncedWrites`).
|
|
417
|
+
*/
|
|
418
|
+
unsyncedWritesKept?: number;
|
|
419
|
+
/**
|
|
420
|
+
* Merge mode: operations of server-authority nodes (`kora:` nodes and the explicit
|
|
421
|
+
* server authorities this device learned) the file held and the import left out.
|
|
422
|
+
* Server decisions reach a device only from the sync server.
|
|
423
|
+
*/
|
|
424
|
+
serverOperationsSkipped?: number;
|
|
274
425
|
/** Duration in ms */
|
|
275
426
|
duration: number;
|
|
276
427
|
}
|
|
@@ -294,12 +445,118 @@ interface RestoreOptions {
|
|
|
294
445
|
/** Progress callback */
|
|
295
446
|
onProgress?: (progress: BackupProgress) => void;
|
|
296
447
|
/**
|
|
297
|
-
* If true, merge
|
|
298
|
-
*
|
|
448
|
+
* If true, merge the backup into the existing data: every operation is applied like
|
|
449
|
+
* one received from sync (deduplicated by id, merged per field), the version vector
|
|
450
|
+
* advances by MAX, and nothing of the exporting device's identity or sync state is
|
|
451
|
+
* imported. If false (default), replace the local data with the backup's.
|
|
299
452
|
*/
|
|
300
453
|
merge?: boolean;
|
|
454
|
+
/**
|
|
455
|
+
* Replace mode only: keep this device's own writes that the sync server has not
|
|
456
|
+
* acknowledged, re-applied on top of the backup, so a restore never discards writes
|
|
457
|
+
* that exist nowhere else. Writes of a node the server has accepted are always kept
|
|
458
|
+
* (they are in flight). Default: true for `store.importBackup`; `app.importBackup`
|
|
459
|
+
* defaults it to whether sync is configured, so a local-only app gets an exact
|
|
460
|
+
* replace.
|
|
461
|
+
*/
|
|
462
|
+
keepUnsyncedWrites?: boolean;
|
|
301
463
|
}
|
|
302
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Log-integrity scan (W8 step 0).
|
|
467
|
+
*
|
|
468
|
+
* Every later fold of the operation log (W7 re-materialization, replay, compaction)
|
|
469
|
+
* trusts that each stored row is a canonical operation. Two earlier releases could
|
|
470
|
+
* break that:
|
|
471
|
+
*
|
|
472
|
+
* - beta.12's backup restore wrote `JSON.stringify(timestamp)` into the timestamp
|
|
473
|
+
* column, which `HybridLogicalClock.deserialize` reads as `wallTime: NaN` (a `null`
|
|
474
|
+
* once re-exported), and a second restore of such a backup nested the damage;
|
|
475
|
+
* - compaction deletes acknowledged operations, leaving holes in a node's sequence.
|
|
476
|
+
*
|
|
477
|
+
* The scan detects rows that do not round-trip through `deserializeOperation` /
|
|
478
|
+
* `serializeOperation`, repairs the ones whose original value is recoverable from the
|
|
479
|
+
* row itself (the JSON-encoded timestamp), and moves the rest to
|
|
480
|
+
* {@link LOG_QUARANTINE_TABLE} (never deleted), so no fold ever reads them. It also
|
|
481
|
+
* reports sequence gaps in this database's own nodes. Remote nodes' gaps are normal
|
|
482
|
+
* (scoped sync only delivers part of another device's log) and are not reported.
|
|
483
|
+
*/
|
|
484
|
+
/** Rows that could not be repaired, kept verbatim. Never cleared automatically. */
|
|
485
|
+
declare const LOG_QUARANTINE_TABLE = "_kora_log_quarantine";
|
|
486
|
+
/** What is wrong with an operation row. */
|
|
487
|
+
type LogRowProblem =
|
|
488
|
+
/** The timestamp is a JSON-encoded HLC (beta.12 backup restore). Repairable. */
|
|
489
|
+
'timestamp-json'
|
|
490
|
+
/** The timestamp cannot be read as an HLC and is not recoverable. */
|
|
491
|
+
| 'timestamp-malformed'
|
|
492
|
+
/** `id`, `node_id` or `record_id` is missing. */
|
|
493
|
+
| 'identity-malformed'
|
|
494
|
+
/** `type` is not insert, update or delete. */
|
|
495
|
+
| 'type-invalid'
|
|
496
|
+
/** `sequence_number` is not a positive integer. */
|
|
497
|
+
| 'sequence-invalid'
|
|
498
|
+
/** `data` or `previous_data` is not a JSON object. */
|
|
499
|
+
| 'data-malformed'
|
|
500
|
+
/** `causal_deps` is not a JSON array of operation ids. */
|
|
501
|
+
| 'causal-deps-malformed'
|
|
502
|
+
/** The row cannot be turned into an operation and back. */
|
|
503
|
+
| 'round-trip-failed';
|
|
504
|
+
/** One operation row the scan repaired or quarantined. */
|
|
505
|
+
interface LogIntegrityRow {
|
|
506
|
+
collection: string;
|
|
507
|
+
operationId: string;
|
|
508
|
+
nodeId: string | null;
|
|
509
|
+
sequenceNumber: number | null;
|
|
510
|
+
problem: LogRowProblem;
|
|
511
|
+
/** Human-readable detail: the bad value, or how it was repaired. */
|
|
512
|
+
detail: string;
|
|
513
|
+
}
|
|
514
|
+
/** Sequence numbers missing from one of this database's own nodes. */
|
|
515
|
+
interface LogSequenceGap {
|
|
516
|
+
nodeId: string;
|
|
517
|
+
/** First missing sequence number (inclusive). */
|
|
518
|
+
from: number;
|
|
519
|
+
/** Last missing sequence number (inclusive). */
|
|
520
|
+
to: number;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Result of {@link scanLogIntegrity} / `Store.verifyLogIntegrity()`.
|
|
524
|
+
*
|
|
525
|
+
* `clean` is the precondition for rebuilding state from the log (W7): every row is
|
|
526
|
+
* canonical, nothing is quarantined, and this database's own nodes have no holes
|
|
527
|
+
* below their highest stored sequence number.
|
|
528
|
+
*/
|
|
529
|
+
interface LogIntegrityReport {
|
|
530
|
+
/** True when the log can be folded as-is (no quarantined rows, no own-node gaps). */
|
|
531
|
+
clean: boolean;
|
|
532
|
+
/** `full`: every row was checked in JS. `quick`: SQL prefilter, suspects checked in JS. */
|
|
533
|
+
mode: 'full' | 'quick';
|
|
534
|
+
/** Rows checked in JS (all rows in full mode, suspects in quick mode). */
|
|
535
|
+
checkedRows: number;
|
|
536
|
+
/** Rows this scan repaired in place. */
|
|
537
|
+
repaired: LogIntegrityRow[];
|
|
538
|
+
/** Rows this scan moved to the quarantine table. */
|
|
539
|
+
newlyQuarantined: LogIntegrityRow[];
|
|
540
|
+
/** Every row in the quarantine table (this scan's and earlier ones'). */
|
|
541
|
+
quarantined: LogIntegrityRow[];
|
|
542
|
+
/** Holes in this database's own nodes' sequences (compaction, a lost tail). */
|
|
543
|
+
gaps: LogSequenceGap[];
|
|
544
|
+
/** When the log was last compacted (epoch ms), or null when never. */
|
|
545
|
+
compactedAt: number | null;
|
|
546
|
+
/** False when the scan only reported (`repair: false`). */
|
|
547
|
+
repairApplied: boolean;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Compares two query result sets; true means subscribers need not be notified. */
|
|
551
|
+
type ResultsEqual = (prev: readonly CollectionRecord[], next: readonly CollectionRecord[]) => boolean;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Per-subscription options accepted by `register` / `registerAndFetch`.
|
|
555
|
+
*/
|
|
556
|
+
interface RegisterOptions extends SubscribeOptions {
|
|
557
|
+
/** Result-set comparator; defaults to structural comparison of every value. */
|
|
558
|
+
resultsEqual?: ResultsEqual;
|
|
559
|
+
}
|
|
303
560
|
/**
|
|
304
561
|
* Configuration options for the SubscriptionManager.
|
|
305
562
|
*/
|
|
@@ -323,7 +580,29 @@ interface SubscriptionManagerOptions {
|
|
|
323
580
|
bloomFalsePositiveRate?: number;
|
|
324
581
|
/** Called when a query subscription is registered (e.g. to register sync query subsets). */
|
|
325
582
|
onQuerySubscribed?: (descriptor: QueryDescriptor) => () => void;
|
|
583
|
+
/**
|
|
584
|
+
* Called for every subscription failure (the store emits `query:error`), in
|
|
585
|
+
* addition to the subscriber's own `onError`.
|
|
586
|
+
*/
|
|
587
|
+
onQueryError?: (failure: QuerySubscriptionError) => void;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* A committed change to the rows of one collection that can affect query results
|
|
591
|
+
* (RT-98). Every invalidation the store makes produces one: local writes and remote
|
|
592
|
+
* applies (with the operation), and changes made without a new operation (rejection
|
|
593
|
+
* re-folds, scope retraction, cascade settling, rematerialization, backup restore,
|
|
594
|
+
* held/discarded writes).
|
|
595
|
+
*/
|
|
596
|
+
interface RecordsChange {
|
|
597
|
+
/** Collection whose rows changed */
|
|
598
|
+
readonly collection: string;
|
|
599
|
+
/** Changed record ids, or null when unknown (treat the whole collection as changed) */
|
|
600
|
+
readonly ids: readonly string[] | null;
|
|
601
|
+
/** The operation that caused the change, when there is one */
|
|
602
|
+
readonly operation?: Operation;
|
|
326
603
|
}
|
|
604
|
+
/** Listener for {@link RecordsChange}s made by this runtime (not those relayed from peers). */
|
|
605
|
+
type RecordsChangeListener = (change: RecordsChange) => void;
|
|
327
606
|
/**
|
|
328
607
|
* Performance statistics for monitoring subscription checking efficiency.
|
|
329
608
|
*/
|
|
@@ -364,6 +643,14 @@ declare class SubscriptionManager {
|
|
|
364
643
|
private pendingCollections;
|
|
365
644
|
private flushScheduled;
|
|
366
645
|
private readonly onQuerySubscribed?;
|
|
646
|
+
private readonly onQueryError?;
|
|
647
|
+
private readonly changeListeners;
|
|
648
|
+
/**
|
|
649
|
+
* Collections changed before the first change listener attached (a store's open:
|
|
650
|
+
* rematerialization, migration backfills). Replayed to that listener, so other tabs
|
|
651
|
+
* hear about changes made while this tab was still opening. Bounded by the schema.
|
|
652
|
+
*/
|
|
653
|
+
private unheardCollections;
|
|
367
654
|
private bloomFilter;
|
|
368
655
|
private bloomDirty;
|
|
369
656
|
private readonly bloomThreshold;
|
|
@@ -383,22 +670,55 @@ declare class SubscriptionManager {
|
|
|
383
670
|
* @param executeFn - Function to re-execute the query and get current results
|
|
384
671
|
* @returns An unsubscribe function
|
|
385
672
|
*/
|
|
386
|
-
register(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]
|
|
673
|
+
register(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]>, options?: RegisterOptions): () => void;
|
|
387
674
|
/**
|
|
388
675
|
* Register a subscription and immediately execute the query.
|
|
389
676
|
* The initial results are stored as lastResults so subsequent flushes
|
|
390
|
-
* correctly diff against the initial state.
|
|
677
|
+
* correctly diff against the initial state. A failing initial run is reported
|
|
678
|
+
* through `options.onError` and the `query:error` hook, never as an unhandled
|
|
679
|
+
* rejection (STORE-12).
|
|
391
680
|
*
|
|
392
681
|
* @returns An unsubscribe function
|
|
393
682
|
*/
|
|
394
|
-
registerAndFetch(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]
|
|
683
|
+
registerAndFetch(descriptor: QueryDescriptor, callback: SubscriptionCallback<CollectionRecord>, executeFn: () => Promise<CollectionRecord[]>, options?: RegisterOptions): () => void;
|
|
684
|
+
private createSubscription;
|
|
685
|
+
private track;
|
|
686
|
+
/**
|
|
687
|
+
* Run a subscription's query and deliver the result when it changed. Never
|
|
688
|
+
* throws: failures go to the error channel. Runs are numbered so a slow,
|
|
689
|
+
* older run never overwrites the result of a newer one.
|
|
690
|
+
*/
|
|
691
|
+
private run;
|
|
692
|
+
private isCurrent;
|
|
693
|
+
private reportError;
|
|
395
694
|
/**
|
|
396
695
|
* Notify the manager that a mutation occurred on a collection.
|
|
397
696
|
* Schedules a microtask flush to batch multiple mutations in the same tick.
|
|
398
697
|
*/
|
|
399
|
-
notify(collection: string,
|
|
400
|
-
/**
|
|
401
|
-
|
|
698
|
+
notify(collection: string, operation: Operation): void;
|
|
699
|
+
/**
|
|
700
|
+
* Invalidate a collection after a local-view change that has no domain operation
|
|
701
|
+
* (a re-fold, a retraction, a restore). Pass the record ids when known.
|
|
702
|
+
*/
|
|
703
|
+
invalidate(collection: string, ids?: readonly string[] | null): void;
|
|
704
|
+
/**
|
|
705
|
+
* The single entry point for a committed change made by this runtime (RT-98): it
|
|
706
|
+
* schedules the affected queries and tells every change listener (the cross-tab
|
|
707
|
+
* bus), so no invalidation path can reach this tab's queries without reaching the
|
|
708
|
+
* other tabs' too.
|
|
709
|
+
*/
|
|
710
|
+
recordsChanged(change: RecordsChange): void;
|
|
711
|
+
/**
|
|
712
|
+
* Invalidate a collection for a change another runtime made to the shared
|
|
713
|
+
* database. Not relayed to change listeners (the peer already broadcast it).
|
|
714
|
+
*/
|
|
715
|
+
invalidateFromPeer(collection: string): void;
|
|
716
|
+
/**
|
|
717
|
+
* Listen to every committed change this runtime makes.
|
|
718
|
+
*
|
|
719
|
+
* @returns An unsubscribe function
|
|
720
|
+
*/
|
|
721
|
+
onRecordsChanged(listener: RecordsChangeListener): () => void;
|
|
402
722
|
/**
|
|
403
723
|
* Immediately flush all pending notifications.
|
|
404
724
|
* Useful for testing. In production, flushing happens via microtask.
|
|
@@ -438,12 +758,6 @@ declare class SubscriptionManager {
|
|
|
438
758
|
*/
|
|
439
759
|
private rebuildBloomFilter;
|
|
440
760
|
private scheduleFlush;
|
|
441
|
-
/**
|
|
442
|
-
* Compare two result sets. Uses ID-based comparison first (same length, same IDs),
|
|
443
|
-
* then deep comparison of remaining fields when IDs match.
|
|
444
|
-
* Avoids JSON.stringify which is O(n) and fragile to field ordering differences.
|
|
445
|
-
*/
|
|
446
|
-
private resultsEqual;
|
|
447
761
|
}
|
|
448
762
|
|
|
449
763
|
/**
|
|
@@ -472,6 +786,10 @@ declare class QueryBuilder<T = CollectionRecord> {
|
|
|
472
786
|
constructor(collectionName: string, definition: CollectionDefinition, adapter: StorageAdapter, subscriptionManager: SubscriptionManager, initialWhere?: WhereClause, schema?: SchemaDefinition | undefined);
|
|
473
787
|
/**
|
|
474
788
|
* Add WHERE conditions (AND semantics, merged with existing conditions).
|
|
789
|
+
*
|
|
790
|
+
* A field whose value is `undefined` adds no condition (it does not remove an
|
|
791
|
+
* earlier one); `null` matches missing values (`IS NULL`). Non-finite numbers are
|
|
792
|
+
* refused. See {@link normalizeWhere} (RT-102).
|
|
475
793
|
*/
|
|
476
794
|
where(conditions: WhereClause): QueryBuilder<T>;
|
|
477
795
|
/**
|
|
@@ -516,7 +834,9 @@ declare class QueryBuilder<T = CollectionRecord> {
|
|
|
516
834
|
*
|
|
517
835
|
* @returns An unsubscribe function
|
|
518
836
|
*/
|
|
519
|
-
subscribe(callback: SubscriptionCallback<T
|
|
837
|
+
subscribe(callback: SubscriptionCallback<T>, options?: SubscribeOptions): () => void;
|
|
838
|
+
/** Comparator built once per collection definition and shared by its queries. */
|
|
839
|
+
private getResultsEqual;
|
|
520
840
|
/** Get the internal descriptor (for testing/debugging) */
|
|
521
841
|
getDescriptor(): QueryDescriptor;
|
|
522
842
|
private clone;
|
|
@@ -595,20 +915,6 @@ declare class SequenceManager {
|
|
|
595
915
|
}): Promise<void>;
|
|
596
916
|
}
|
|
597
917
|
|
|
598
|
-
/**
|
|
599
|
-
* Allocates monotonic sequence numbers for a single open transaction.
|
|
600
|
-
* Loads the current DB watermark once, then increments in memory until commit.
|
|
601
|
-
*/
|
|
602
|
-
declare class TransactionSequenceAllocator {
|
|
603
|
-
private readonly adapter;
|
|
604
|
-
private readonly nodeId;
|
|
605
|
-
private loaded;
|
|
606
|
-
private watermark;
|
|
607
|
-
constructor(adapter: StorageAdapter, nodeId: string);
|
|
608
|
-
allocate(): Promise<number>;
|
|
609
|
-
getHighWaterMark(): number;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
918
|
/**
|
|
613
919
|
* Internal configuration for creating a TransactionContext.
|
|
614
920
|
* Passed from Store to avoid exposing Store internals publicly.
|
|
@@ -618,10 +924,23 @@ interface TransactionContextConfig {
|
|
|
618
924
|
adapter: StorageAdapter;
|
|
619
925
|
clock: HybridLogicalClock;
|
|
620
926
|
nodeId: string;
|
|
621
|
-
sequenceAllocator: TransactionSequenceAllocator;
|
|
622
927
|
relationEnforcer: RelationEnforcer | null;
|
|
623
928
|
causalTracker: CausalTracker | null;
|
|
624
|
-
|
|
929
|
+
/**
|
|
930
|
+
* Key provider for `t.secret()` encrypted fields. Transaction writes transform
|
|
931
|
+
* secret fields through the same helper as single-record writes (STORE-4).
|
|
932
|
+
*/
|
|
933
|
+
secretKeyProvider?: SecretKeyProvider;
|
|
934
|
+
/** Referential hook run for every local delete inside the commit (see {@link LocalDeleteHook}). */
|
|
935
|
+
beforeLocalDelete?: LocalDeleteHook;
|
|
936
|
+
/** Called when the commit's storage transaction fails, before the error propagates. */
|
|
937
|
+
onStorageError?: (error: unknown) => void;
|
|
938
|
+
/** The W7 record fold (see `WriteEnv.fold`). */
|
|
939
|
+
fold?: RecordFolder;
|
|
940
|
+
/** See `WriteEnv.maxOperationBytes`. */
|
|
941
|
+
maxOperationBytes?: number;
|
|
942
|
+
/** See `WriteEnv.assertLocalWriteAllowed`. */
|
|
943
|
+
assertLocalWriteAllowed?: () => void;
|
|
625
944
|
}
|
|
626
945
|
/**
|
|
627
946
|
* A collection accessor within a transaction.
|
|
@@ -636,8 +955,10 @@ interface TransactionCollectionAccessor {
|
|
|
636
955
|
/**
|
|
637
956
|
* TransactionContext provides atomic multi-collection operations.
|
|
638
957
|
*
|
|
639
|
-
*
|
|
640
|
-
* call
|
|
958
|
+
* Mutations are validated and buffered when called (so errors surface at the
|
|
959
|
+
* call site and `insert` returns the new id immediately), then written in a
|
|
960
|
+
* single `StorageAdapter.transaction()` at commit. All operations share the
|
|
961
|
+
* same transactionId (UUID v7) and one contiguous block of sequence numbers.
|
|
641
962
|
*
|
|
642
963
|
* Subscription notifications are deferred until after commit.
|
|
643
964
|
*
|
|
@@ -672,7 +993,13 @@ declare class TransactionContext {
|
|
|
672
993
|
*/
|
|
673
994
|
collection(name: string): TransactionCollectionAccessor;
|
|
674
995
|
/**
|
|
675
|
-
* Commit all buffered
|
|
996
|
+
* Commit all buffered mutations atomically.
|
|
997
|
+
*
|
|
998
|
+
* Every entry is written, in call order, inside ONE storage transaction through
|
|
999
|
+
* the single local write path: sequence numbers are reserved inside the
|
|
1000
|
+
* transaction (one contiguous block covering referential side effects too) and
|
|
1001
|
+
* each operation is built only after its number is reserved.
|
|
1002
|
+
*
|
|
676
1003
|
* Returns the list of operations and affected collections for subscription notification.
|
|
677
1004
|
*/
|
|
678
1005
|
commit(): Promise<{
|
|
@@ -687,32 +1014,35 @@ declare class TransactionContext {
|
|
|
687
1014
|
* Get the transaction ID shared by all operations in this transaction.
|
|
688
1015
|
*/
|
|
689
1016
|
getTransactionId(): string;
|
|
1017
|
+
private writeEnv;
|
|
690
1018
|
private ensureActive;
|
|
691
1019
|
private insert;
|
|
692
1020
|
private update;
|
|
693
1021
|
private deleteRecord;
|
|
694
|
-
private findById;
|
|
695
1022
|
/**
|
|
696
|
-
*
|
|
697
|
-
*
|
|
1023
|
+
* The values an update will write, resolved against `current` (the effective
|
|
1024
|
+
* record) for read-your-writes inside the transaction. Secret fields are shown
|
|
1025
|
+
* in their at-rest form, like the record a committed update returns.
|
|
1026
|
+
*/
|
|
1027
|
+
private previewUpdate;
|
|
1028
|
+
/**
|
|
1029
|
+
* The effective state of a record: the committed row overlaid with this
|
|
1030
|
+
* transaction's buffered mutations, in call order.
|
|
698
1031
|
*/
|
|
699
1032
|
private getEffectiveRecord;
|
|
700
1033
|
}
|
|
701
1034
|
|
|
702
|
-
/**
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
* await store.close()
|
|
714
|
-
* ```
|
|
715
|
-
*/
|
|
1035
|
+
/** Result of {@link Store.bindPrincipal} (RT-42). */
|
|
1036
|
+
interface PrincipalBinding {
|
|
1037
|
+
/** The node id local writes use from now on. */
|
|
1038
|
+
nodeId: string;
|
|
1039
|
+
/** The node id in use before the call. */
|
|
1040
|
+
previousNodeId: string;
|
|
1041
|
+
/** The store moved to another node id. */
|
|
1042
|
+
switched: boolean;
|
|
1043
|
+
/** The node id is pinned and belongs to another user: nothing may upload it now. */
|
|
1044
|
+
conflict: boolean;
|
|
1045
|
+
}
|
|
716
1046
|
declare class Store implements OperationLog {
|
|
717
1047
|
private opened;
|
|
718
1048
|
private nodeId;
|
|
@@ -725,6 +1055,12 @@ declare class Store implements OperationLog {
|
|
|
725
1055
|
private readonly schema;
|
|
726
1056
|
private readonly adapter;
|
|
727
1057
|
private readonly configNodeId;
|
|
1058
|
+
/** The owner of the pinned node while it belongs to another user than the signed-in one (F9). */
|
|
1059
|
+
private pinnedNodeOwnedBy;
|
|
1060
|
+
/** The sync server refused the pinned node as another user's (F9). */
|
|
1061
|
+
private pinnedNodeRefusedByServer;
|
|
1062
|
+
/** The user of the last {@link bindPrincipal}. */
|
|
1063
|
+
private signedInPrincipal;
|
|
728
1064
|
private readonly dbName;
|
|
729
1065
|
private readonly isolation;
|
|
730
1066
|
private readonly emitter;
|
|
@@ -732,6 +1068,18 @@ declare class Store implements OperationLog {
|
|
|
732
1068
|
private relationEnforcer;
|
|
733
1069
|
private causalTracker;
|
|
734
1070
|
private readonly secretKeyProvider;
|
|
1071
|
+
/** Releases the per-tab node lock (RT-40); null when none is held. */
|
|
1072
|
+
private releaseNodeLock;
|
|
1073
|
+
/** How rows are materialized ({@link StoreConfig.materialization}). */
|
|
1074
|
+
private readonly materialization;
|
|
1075
|
+
/** The W7 record fold; null under legacy materialization. */
|
|
1076
|
+
private readonly folder;
|
|
1077
|
+
/** True once every row of the database is a materialization of its fold state. */
|
|
1078
|
+
private foldActive;
|
|
1079
|
+
/** Schema transforms applied at fold time (RT-84); never to stored operations. */
|
|
1080
|
+
private readonly operationTransforms;
|
|
1081
|
+
/** Largest operation a local write may produce (RT-86); default in the write path. */
|
|
1082
|
+
private readonly maxOperationBytes;
|
|
735
1083
|
constructor(config: StoreConfig);
|
|
736
1084
|
/**
|
|
737
1085
|
* Open the store: initialize the database, load or generate a node ID,
|
|
@@ -742,6 +1090,25 @@ declare class Store implements OperationLog {
|
|
|
742
1090
|
* Close the store: clear subscriptions and close the adapter.
|
|
743
1091
|
*/
|
|
744
1092
|
close(): Promise<void>;
|
|
1093
|
+
/**
|
|
1094
|
+
* Check the operation log (W8 step 0): every row must round-trip through the
|
|
1095
|
+
* canonical operation serializer. Rows an earlier release damaged in a recoverable
|
|
1096
|
+
* way (a JSON-encoded timestamp written by a beta.12 backup restore) are repaired;
|
|
1097
|
+
* unrecoverable rows move to the quarantine table, where no fold reads them. Also
|
|
1098
|
+
* reports sequence gaps in this database's own nodes (compaction, a lost tail).
|
|
1099
|
+
*
|
|
1100
|
+
* The store runs a quick variant (SQL prefilter) on every open. Rebuilding state from
|
|
1101
|
+
* the log must only run on a `clean` report.
|
|
1102
|
+
*
|
|
1103
|
+
* @param options - `repair: false` only reports; `mode: 'quick'` checks only the rows
|
|
1104
|
+
* a SQL prefilter flags (default `'full'`: every row)
|
|
1105
|
+
* @returns The integrity report; emits `store:log-integrity` when rows changed
|
|
1106
|
+
*/
|
|
1107
|
+
verifyLogIntegrity(options?: {
|
|
1108
|
+
repair?: boolean;
|
|
1109
|
+
mode?: 'full' | 'quick';
|
|
1110
|
+
}): Promise<LogIntegrityReport>;
|
|
1111
|
+
private scanLog;
|
|
745
1112
|
/**
|
|
746
1113
|
* Get a Collection instance for CRUD operations.
|
|
747
1114
|
* @throws {StoreNotOpenError} If the store is not open
|
|
@@ -762,12 +1129,143 @@ declare class Store implements OperationLog {
|
|
|
762
1129
|
*/
|
|
763
1130
|
setClockReferenceOffset(offsetMs: number): void;
|
|
764
1131
|
getNodeId(): string;
|
|
1132
|
+
/** The schema transforms the fold applies (transforms at fold time, RT-84). */
|
|
1133
|
+
getOperationTransforms(): readonly OperationTransform[];
|
|
765
1134
|
/**
|
|
766
1135
|
* Apply a remote operation received from sync.
|
|
767
1136
|
* Checks for duplicates, applies to the data table, persists the operation,
|
|
768
1137
|
* and updates the version vector.
|
|
769
1138
|
*/
|
|
770
|
-
applyRemoteOperation(
|
|
1139
|
+
applyRemoteOperation(stored: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
1140
|
+
/**
|
|
1141
|
+
* W7 remote apply: append the operation, then merge it into its record's fold
|
|
1142
|
+
* state, which re-materializes the row, all in one write transaction. There is no
|
|
1143
|
+
* read-decide-write race to guard: the state is read and written inside the
|
|
1144
|
+
* transaction, and the merge does not depend on the order operations arrive in.
|
|
1145
|
+
*/
|
|
1146
|
+
private applyRemoteFolded;
|
|
1147
|
+
/** DevTools: every fold decision that was a conflict, plus one completion per merge. */
|
|
1148
|
+
private emitMergeTraces;
|
|
1149
|
+
/** The record fold, once the database's rows are fold materializations. */
|
|
1150
|
+
private activeFold;
|
|
1151
|
+
/**
|
|
1152
|
+
* The record's W7 fold state (null under legacy materialization or when the
|
|
1153
|
+
* record has none). For DevTools, tests and the server-comparison harness.
|
|
1154
|
+
*/
|
|
1155
|
+
getFoldState(collection: string, recordId: string): Promise<FoldState | null>;
|
|
1156
|
+
/** Whether rows are materialized by the W7 fold (false under `materialization: 'legacy'`). */
|
|
1157
|
+
isFoldMaterialized(): boolean;
|
|
1158
|
+
private readMeta;
|
|
1159
|
+
private writeMeta;
|
|
1160
|
+
/**
|
|
1161
|
+
* Write every genuine beta.12 clear of the op log into its logged body, once per
|
|
1162
|
+
* database (RT-83, RT-85): the fold folds bodies as written, so the clear must be in
|
|
1163
|
+
* the body. Records already materialized by the fold are re-folded.
|
|
1164
|
+
*/
|
|
1165
|
+
private ensureLegacyBodiesCanonical;
|
|
1166
|
+
/**
|
|
1167
|
+
* Make every row a materialization of its record's fold state (W7), once per
|
|
1168
|
+
* database and fold-state version. Runs after the W8 log-integrity scan: a
|
|
1169
|
+
* clean log is rebuilt with the fold; an incomplete (compacted) log rebuilds on
|
|
1170
|
+
* top of the rows as base snapshots; a log with quarantined rows is never
|
|
1171
|
+
* rebuilt from (rows are kept and become base snapshots), and
|
|
1172
|
+
* `store:rematerialized` reports which happened.
|
|
1173
|
+
*/
|
|
1174
|
+
private ensureMaterialization;
|
|
1175
|
+
/**
|
|
1176
|
+
* Refuse to open when a schema version in the local operation log has no transform
|
|
1177
|
+
* path to the current schema (RT-103). Operations of a NEWER schema (authored by an
|
|
1178
|
+
* upgraded peer) are not checked: they are kept aside until this device upgrades.
|
|
1179
|
+
*/
|
|
1180
|
+
private assertTransformCoverage;
|
|
1181
|
+
/**
|
|
1182
|
+
* Re-fold the records of every collection whose fold plan changed since the rows
|
|
1183
|
+
* were materialized (RT-63): a field that became `merge('counter')`, an array that
|
|
1184
|
+
* became append-only, a new or edited resolver. Their stored states hold fields of
|
|
1185
|
+
* the old kind, which the fold refuses (`FoldStateError`). Each record is rebuilt
|
|
1186
|
+
* from its log with the new plan; compacted bases and row snapshots are adapted
|
|
1187
|
+
* (a re-planned field restarts from its materialized value at its version). The
|
|
1188
|
+
* client's equivalent of the server stores' `fold_plan_fingerprint`.
|
|
1189
|
+
*/
|
|
1190
|
+
private ensureFoldPlan;
|
|
1191
|
+
/**
|
|
1192
|
+
* Row snapshots were created (RT-68): ask the sync server for a full resync (the
|
|
1193
|
+
* delivery watermarks restart at 0, as after a restore), so each such record gets
|
|
1194
|
+
* its complete history back; {@link settleSnapshotRecords} then drops the
|
|
1195
|
+
* snapshots once the resync has caught up.
|
|
1196
|
+
*/
|
|
1197
|
+
private requestSnapshotResync;
|
|
1198
|
+
/**
|
|
1199
|
+
* Records whose state still folds against a row snapshot (approximate: a late
|
|
1200
|
+
* concurrent write older than a field's version is not folded, RT-68).
|
|
1201
|
+
*/
|
|
1202
|
+
getSnapshotRecords(): Promise<Array<{
|
|
1203
|
+
collection: string;
|
|
1204
|
+
recordId: string;
|
|
1205
|
+
}>>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Called by sync when a delivery stream has caught up (its final batch fully
|
|
1208
|
+
* applied). After the full resync {@link requestSnapshotResync} asked for, every
|
|
1209
|
+
* row-snapshot record whose insert is back in the log is re-folded from its
|
|
1210
|
+
* complete history and stops being approximate (RT-68). Also retires the
|
|
1211
|
+
* provisional cascades of remote deletes (RT-69): the server's own copies were
|
|
1212
|
+
* delivered by now, and any it did not derive must not stay applied locally.
|
|
1213
|
+
*
|
|
1214
|
+
* With `provisionalOnly` only the provisional cascades are retired (RT-93: a stream
|
|
1215
|
+
* batch from a server that derives no cascades, which is not the end of a resync).
|
|
1216
|
+
*
|
|
1217
|
+
* @param options - `provisionalOnly`: leave row snapshots for the next catch-up
|
|
1218
|
+
* @returns How many snapshots were dropped
|
|
1219
|
+
*/
|
|
1220
|
+
settleAfterCatchUp(options?: {
|
|
1221
|
+
provisionalOnly?: boolean;
|
|
1222
|
+
}): Promise<number>;
|
|
1223
|
+
/**
|
|
1224
|
+
* Apply the cascades / set-nulls of a REMOTE delete as local-only provisional
|
|
1225
|
+
* effects (RT-69): folded into the children like operations, but never logged,
|
|
1226
|
+
* sequenced or queued for upload. The server derives and relays its own copy;
|
|
1227
|
+
* when it arrives (same parent, same record) the provisional effect is retired.
|
|
1228
|
+
*
|
|
1229
|
+
* @param parent - The remote delete
|
|
1230
|
+
* @param effects - Its side effects, as the referential check derived them
|
|
1231
|
+
*/
|
|
1232
|
+
applyProvisionalSideEffects(parent: Operation, effects: ReadonlyArray<{
|
|
1233
|
+
type: 'delete' | 'update';
|
|
1234
|
+
collection: string;
|
|
1235
|
+
recordId: string;
|
|
1236
|
+
data: Record<string, unknown> | null;
|
|
1237
|
+
previousData: Record<string, unknown> | null;
|
|
1238
|
+
ruleId: string;
|
|
1239
|
+
}>): Promise<number>;
|
|
1240
|
+
/**
|
|
1241
|
+
* Node ids whose writes win `merge('server-authoritative')` fields (the server's,
|
|
1242
|
+
* from the sync handshake). Every `kora:server:` id is authoritative by prefix, so
|
|
1243
|
+
* only explicit ids (legacy server node ids, configured extras) are kept: the device
|
|
1244
|
+
* persists the UNION of every explicit id it has learned and never drops one (RT-75).
|
|
1245
|
+
* Handshakes differ per server instance (each lists what it knows), so a list that
|
|
1246
|
+
* omits an id is not a revocation.
|
|
1247
|
+
*
|
|
1248
|
+
* Revocation is explicit (RT-81): ids a handshake lists in `revokedAuthoritativeNodeIds`
|
|
1249
|
+
* are removed from the union, persisted as revoked, and never learned again. Records
|
|
1250
|
+
* of collections with such fields are re-folded only when the explicit set changes
|
|
1251
|
+
* (grows, or loses a revoked id).
|
|
1252
|
+
*
|
|
1253
|
+
* @param nodeIds - The server's authoritative node ids (one handshake's list)
|
|
1254
|
+
* @param revokedNodeIds - Explicit ids the server revoked (one handshake's list)
|
|
1255
|
+
*/
|
|
1256
|
+
setAuthoritativeNodeIds(nodeIds: readonly string[], revokedNodeIds?: readonly string[]): Promise<void>;
|
|
1257
|
+
/**
|
|
1258
|
+
* Re-fold the records of these operations from base + log (W7 exclusion: an
|
|
1259
|
+
* operation with a terminal-rejection marker is left out).
|
|
1260
|
+
*/
|
|
1261
|
+
private refoldRecordsOf;
|
|
1262
|
+
/**
|
|
1263
|
+
* Append a remote operation to the log. If another operation of the same node
|
|
1264
|
+
* already holds its sequence number (a beta.12 device could produce such
|
|
1265
|
+
* pairs), the log keeps the existing row and this one is recorded in the
|
|
1266
|
+
* sequence-conflicts table, where dedup and record folds still see it.
|
|
1267
|
+
*/
|
|
1268
|
+
private appendRemoteOperationRow;
|
|
771
1269
|
/**
|
|
772
1270
|
* Hide a record from this client's authorized materialized view. This writes no
|
|
773
1271
|
* replicated operation and therefore cannot be mistaken for a domain deletion.
|
|
@@ -779,6 +1277,26 @@ declare class Store implements OperationLog {
|
|
|
779
1277
|
collection: string;
|
|
780
1278
|
recordId: string;
|
|
781
1279
|
}>>;
|
|
1280
|
+
/**
|
|
1281
|
+
* Materialize a server scope-entry insert (RT-19) whose fields each carry their
|
|
1282
|
+
* own version (RT-27). The entry restates the record's current server values; a
|
|
1283
|
+
* single whole-row stamp would let a field the server last wrote long ago
|
|
1284
|
+
* overwrite this device's newer unsynced edit of it, after which the device's own
|
|
1285
|
+
* operation uploads and wins on the server (divergence). So every field is resolved
|
|
1286
|
+
* on its own, by last-write-wins against this row's `_field_versions`:
|
|
1287
|
+
*
|
|
1288
|
+
* - no row: inserted with each field stamped at its own version, `_created_at`
|
|
1289
|
+
* at the record's creation (the entry's `timestamp`), then any orphaned
|
|
1290
|
+
* updates/deletes are folded in as for any insert;
|
|
1291
|
+
* - live row, or a row hidden by a scope retraction: per-field LWW (a retracted
|
|
1292
|
+
* row is shown again; `_created_at` is kept);
|
|
1293
|
+
* - domain tombstone: revived only when the entry's newest field version is
|
|
1294
|
+
* newer than the tombstone, then per-field LWW.
|
|
1295
|
+
*
|
|
1296
|
+
* Deterministic and idempotent: the outcome depends only on the stored and
|
|
1297
|
+
* incoming per-field versions, and a re-applied entry wins no field.
|
|
1298
|
+
*/
|
|
1299
|
+
private applyFieldVersionedInsert;
|
|
782
1300
|
/**
|
|
783
1301
|
* Materialize update/delete operations that were logged for a record BEFORE
|
|
784
1302
|
* its insert arrived (reordered delivery). Runs inside the insert's write
|
|
@@ -842,11 +1360,34 @@ declare class Store implements OperationLog {
|
|
|
842
1360
|
*/
|
|
843
1361
|
notifyExternalOperation(operation: Operation): void;
|
|
844
1362
|
/**
|
|
845
|
-
*
|
|
1363
|
+
* Notify this store that another same-origin runtime changed rows of a collection
|
|
1364
|
+
* in the shared local database without an operation this tab can see (a re-fold,
|
|
1365
|
+
* a scope retraction, a restore; RT-98). Only re-runs the affected live queries.
|
|
1366
|
+
*
|
|
1367
|
+
* @param collection - The collection whose rows changed
|
|
1368
|
+
*/
|
|
1369
|
+
notifyExternalChange(collection: string): void;
|
|
1370
|
+
/**
|
|
1371
|
+
* Listen to every committed change this store makes to collection rows: local
|
|
1372
|
+
* writes, remote applies, and changes without a new operation (re-folds, scope
|
|
1373
|
+
* retraction, cascade settling, rematerialization, restore). The cross-tab bus
|
|
1374
|
+
* relays these to other tabs on the same database (RT-98).
|
|
1375
|
+
*
|
|
1376
|
+
* @returns An unsubscribe function
|
|
1377
|
+
*/
|
|
1378
|
+
onRecordsChanged(listener: RecordsChangeListener): () => void;
|
|
1379
|
+
/**
|
|
1380
|
+
* Build the local write context for a collection: everything the single local
|
|
1381
|
+
* write path needs (used by Collection writes and by ApplyPipeline).
|
|
846
1382
|
*/
|
|
847
1383
|
createMutationContext(collection: string, options?: {
|
|
848
|
-
inTransaction?: boolean;
|
|
849
1384
|
extraCausalDeps?: string[];
|
|
1385
|
+
/**
|
|
1386
|
+
* Stamp the writes with this clock instead of the store's (it must be this
|
|
1387
|
+
* node's). Used for the side effects of a remote delete, stamped right after
|
|
1388
|
+
* the delete like the server's copy (seam 5), never with the device's "now".
|
|
1389
|
+
*/
|
|
1390
|
+
clock?: HybridLogicalClock;
|
|
850
1391
|
}): LocalMutationContext;
|
|
851
1392
|
/**
|
|
852
1393
|
* Load a materialized row by ID, including soft-deleted tombstones.
|
|
@@ -871,6 +1412,25 @@ declare class Store implements OperationLog {
|
|
|
871
1412
|
* Persist the last server version vector this client believes the server has applied.
|
|
872
1413
|
*/
|
|
873
1414
|
saveLastAckedServerVector(vector: VersionVector): Promise<void>;
|
|
1415
|
+
/**
|
|
1416
|
+
* Load the per-device node token the sync server issued for this node id (RT-12).
|
|
1417
|
+
*/
|
|
1418
|
+
loadNodeToken(nodeId?: string): Promise<string | null>;
|
|
1419
|
+
/**
|
|
1420
|
+
* Persist the per-device node token under its node id (the current one by default).
|
|
1421
|
+
*/
|
|
1422
|
+
saveNodeToken(token: string, nodeId?: string): Promise<void>;
|
|
1423
|
+
/**
|
|
1424
|
+
* Load the node ids the sync server named authoritative (protocol v2), or null when
|
|
1425
|
+
* no protocol-2 server ever answered.
|
|
1426
|
+
*/
|
|
1427
|
+
loadAuthoritativeNodeIds(): Promise<string[] | null>;
|
|
1428
|
+
/**
|
|
1429
|
+
* Persist the node ids the sync server named authoritative (protocol v2). Same as
|
|
1430
|
+
* {@link setAuthoritativeNodeIds}: merged into the union of learned explicit ids,
|
|
1431
|
+
* and the fold re-folds affected records only when that set grows.
|
|
1432
|
+
*/
|
|
1433
|
+
saveAuthoritativeNodeIds(nodeIds: string[]): Promise<void>;
|
|
874
1434
|
/**
|
|
875
1435
|
* Load persisted delta cursor for resuming paginated initial sync.
|
|
876
1436
|
*/
|
|
@@ -896,6 +1456,31 @@ declare class Store implements OperationLog {
|
|
|
896
1456
|
* Delete a persisted view watermark (safe: the view back-fills from 0 when next visited).
|
|
897
1457
|
*/
|
|
898
1458
|
deleteDeliveryWatermark(signature: string): Promise<void>;
|
|
1459
|
+
/**
|
|
1460
|
+
* Record delivered operations the sync engine deliberately did not apply (inbound
|
|
1461
|
+
* quarantine, W4) and, when given, advance a view's delivery watermark in the same
|
|
1462
|
+
* transaction, so the watermark never passes an operation that is neither applied nor
|
|
1463
|
+
* recorded.
|
|
1464
|
+
*/
|
|
1465
|
+
saveInboundQuarantine(entries: UnappliedOperation[], watermark?: {
|
|
1466
|
+
signature: string;
|
|
1467
|
+
watermark: number;
|
|
1468
|
+
}): Promise<void>;
|
|
1469
|
+
/** Every quarantined inbound operation, oldest delivery first. */
|
|
1470
|
+
loadInboundQuarantine(): Promise<UnappliedOperation[]>;
|
|
1471
|
+
/** Remove quarantined inbound operations (applied on replay, or reconciled). */
|
|
1472
|
+
removeInboundQuarantine(operationIds: string[]): Promise<void>;
|
|
1473
|
+
/**
|
|
1474
|
+
* The contiguous acknowledged prefix of this device's own operations for a node id,
|
|
1475
|
+
* or null when none was recorded under that contract (W3).
|
|
1476
|
+
*/
|
|
1477
|
+
loadOwnAckedThrough(nodeId: string): Promise<number | null>;
|
|
1478
|
+
/** Persist the contiguous acknowledged own-operation prefix for a node id. */
|
|
1479
|
+
saveOwnAckedThrough(nodeId: string, sequence: number): Promise<void>;
|
|
1480
|
+
/** The downlink scope the sync server last accepted (null when none). */
|
|
1481
|
+
loadAcceptedDownlinkScope(): Promise<Record<string, Record<string, unknown>> | null>;
|
|
1482
|
+
/** Persist (or clear) the downlink scope the sync server last accepted. */
|
|
1483
|
+
saveAcceptedDownlinkScope(scope: Record<string, Record<string, unknown>> | null): Promise<void>;
|
|
899
1484
|
/**
|
|
900
1485
|
* Local operations not yet reflected on the server version vector.
|
|
901
1486
|
*/
|
|
@@ -911,14 +1496,185 @@ declare class Store implements OperationLog {
|
|
|
911
1496
|
* No data-change notifications are emitted: materialized values are
|
|
912
1497
|
* unchanged, only version stamps move.
|
|
913
1498
|
*/
|
|
1499
|
+
/**
|
|
1500
|
+
* Move this device to a fresh node id, re-authoring its never-acknowledged
|
|
1501
|
+
* operations under it (RT-21). Used when the sync server refuses the current node
|
|
1502
|
+
* id (`NODE_ID_CLAIMED`): the device keeps every local write and uploads it under
|
|
1503
|
+
* the new id. Acknowledged history stays under the old id. Persisted atomically;
|
|
1504
|
+
* later writes use the new id.
|
|
1505
|
+
*
|
|
1506
|
+
* @param unsyncedOpIds - Ids of this node's operations the server never acknowledged
|
|
1507
|
+
* @returns The new node id and the rewritten operations (for the outbound queue)
|
|
1508
|
+
* @throws {KoraError} When the node id is pinned (`StoreConfig.nodeId`, or per-tab isolation)
|
|
1509
|
+
*/
|
|
1510
|
+
rotateNodeId(unsyncedOpIds: string[]): Promise<NodeRotationResult>;
|
|
1511
|
+
/**
|
|
1512
|
+
* Re-author another local node's never-acknowledged operations under a fresh node id
|
|
1513
|
+
* bound to `principal` (the user they belong to). Used when the sync server refuses an
|
|
1514
|
+
* adopted node whose writes belong to the signed-in user (the app assigned them, or
|
|
1515
|
+
* the node was created for them) but which no claims-aware server ever accepted: a
|
|
1516
|
+
* beta.12 database's node with history the server recorded no owner for. The store's
|
|
1517
|
+
* own node id, clock and counters are untouched; the fresh node is registered, so the
|
|
1518
|
+
* sync engine adopts it like any other local node.
|
|
1519
|
+
*
|
|
1520
|
+
* @param fromNodeId - The refused local node (never the store's own)
|
|
1521
|
+
* @param unsyncedOpIds - Ids of its operations the server never acknowledged
|
|
1522
|
+
* @param principal - The user the writes belong to (the fresh node's binding)
|
|
1523
|
+
* @returns The fresh node id and the rewritten operations (for the outbound queue)
|
|
1524
|
+
* @throws {KoraError} `NODE_ID_UNKNOWN` when `fromNodeId` is the store's own node or
|
|
1525
|
+
* not a local node
|
|
1526
|
+
*/
|
|
1527
|
+
reauthorLocalNode(fromNodeId: string, unsyncedOpIds: string[], principal: string): Promise<NodeRotationResult>;
|
|
1528
|
+
/**
|
|
1529
|
+
* Move this database back to a node id it authored under before (RT-38): after the
|
|
1530
|
+
* sync server refused the current node, the device tries a node a returning user
|
|
1531
|
+
* owns. Nothing is rewritten; later writes use `nodeId`.
|
|
1532
|
+
*
|
|
1533
|
+
* @throws {KoraError} When the node id is pinned, or `nodeId` is not a local node
|
|
1534
|
+
*/
|
|
1535
|
+
switchNodeId(nodeId: string): Promise<void>;
|
|
1536
|
+
/** Point the clock, sequence counter and collections at `nodeId` (already persisted). */
|
|
1537
|
+
private rebindToNode;
|
|
1538
|
+
/**
|
|
1539
|
+
* Bind this database's writes to the signed-in user (RT-42). Call it whenever the
|
|
1540
|
+
* signed-in user is known or changes, BEFORE the next local write (`createApp` does
|
|
1541
|
+
* so at start and on every auth change of `sync.authClient`).
|
|
1542
|
+
*
|
|
1543
|
+
* - The current node belongs to `principal`: nothing changes.
|
|
1544
|
+
* - The current node is unbound and has no history (no write, never synced): it is
|
|
1545
|
+
* bound to `principal` (every write under it from now on is theirs).
|
|
1546
|
+
* - The current node is unbound but has history (a database from before RT-42, or
|
|
1547
|
+
* writes made while nobody was signed in): its owner is NOT guessed (RT-50). The
|
|
1548
|
+
* store moves to `principal`'s own node, and the old node's owner is learned from
|
|
1549
|
+
* the sync server (the first accepted handshake as it binds it; a refusal rules
|
|
1550
|
+
* that user out). A node that never synced cannot be attributed by anyone: its
|
|
1551
|
+
* unsynced writes are held for the app to assign or discard (`app.sync.assignHeld`,
|
|
1552
|
+
* `app.sync.discardHeld`).
|
|
1553
|
+
* - It belongs to another user: the store moves to `principal`'s own local node (the
|
|
1554
|
+
* most recent one, not held), or to a fresh node bound to `principal`. Nothing is
|
|
1555
|
+
* rewritten: the other user's unsynced writes stay under their node, and sync never
|
|
1556
|
+
* uploads them on this user's session (they count as `heldOperations`).
|
|
1557
|
+
*
|
|
1558
|
+
* A pinned node id (`StoreConfig.nodeId`, such as an auth device id) cannot move:
|
|
1559
|
+
* `conflict` is then true when it belongs to another user, and sync refuses to
|
|
1560
|
+
* upload it under `principal`; an unbound pinned node with history stays unbound
|
|
1561
|
+
* until the server accepts it for a user. Under `per-tab` isolation the tab moves to
|
|
1562
|
+
* a fresh per-tab node.
|
|
1563
|
+
*
|
|
1564
|
+
* @param principal - The signed-in user id
|
|
1565
|
+
* @returns The node now in use and whether it changed
|
|
1566
|
+
*/
|
|
1567
|
+
bindPrincipal(principal: string): Promise<PrincipalBinding>;
|
|
1568
|
+
/**
|
|
1569
|
+
* The sync server refused this store's pinned node id because another user owns it
|
|
1570
|
+
* (`owned: true`), or later accepted it (`false`). A pinned node cannot move to a fresh
|
|
1571
|
+
* id, so writes made under it could only ever upload as its owner: while the server
|
|
1572
|
+
* says so, local writes are refused (F9). A no-op for a store whose node is not pinned
|
|
1573
|
+
* (it moves to a fresh node instead).
|
|
1574
|
+
*
|
|
1575
|
+
* @param owned - Whether the server reported the node owned by another user
|
|
1576
|
+
*/
|
|
1577
|
+
setPinnedNodeOwnedElsewhere(owned: boolean): void;
|
|
1578
|
+
/** Refuse a local write while the pinned node belongs to another user (F9). */
|
|
1579
|
+
private assertLocalWriteAllowed;
|
|
1580
|
+
private bindPrincipalToNode;
|
|
1581
|
+
/**
|
|
1582
|
+
* A sync handshake as `nodeId` was accepted for `principal` (RT-50): bind an unbound
|
|
1583
|
+
* node (or replace a guessed binding) from the server's answer.
|
|
1584
|
+
*/
|
|
1585
|
+
confirmNodePrincipal(nodeId: string, principal: string): Promise<void>;
|
|
1586
|
+
/**
|
|
1587
|
+
* The sync server refused `nodeId` for `principal` (RT-50): clear a guessed binding,
|
|
1588
|
+
* and remember that an unbound node is not theirs.
|
|
1589
|
+
*/
|
|
1590
|
+
recordNodeRefusedFor(nodeId: string, principal: string): Promise<void>;
|
|
1591
|
+
/**
|
|
1592
|
+
* Assign a held, unbound local node's writes to `principal` (RT-50): an explicit app
|
|
1593
|
+
* decision for writes nobody can attribute (a database that never synced).
|
|
1594
|
+
*
|
|
1595
|
+
* @returns false when the node is unknown, the store's own, already bound, or was
|
|
1596
|
+
* refused for `principal` by the server
|
|
1597
|
+
*/
|
|
1598
|
+
assignNodePrincipal(nodeId: string, principal: string): Promise<boolean>;
|
|
1599
|
+
/**
|
|
1600
|
+
* Forget a local node other than the store's own whatever it still holds (RT-50: the
|
|
1601
|
+
* app discarded its held writes, which were marked never to upload first).
|
|
1602
|
+
*/
|
|
1603
|
+
dropLocalNode(nodeId: string): Promise<void>;
|
|
1604
|
+
/** The adoption schedule of this database's local nodes (RT-46). */
|
|
1605
|
+
loadAdoptionSchedule(): Promise<AdoptionSchedule>;
|
|
1606
|
+
/** Persist the adoption schedule (RT-46). */
|
|
1607
|
+
saveAdoptionSchedule(schedule: AdoptionSchedule): Promise<void>;
|
|
1608
|
+
/**
|
|
1609
|
+
* Raise a local node's sequence counter to at least `floor`, in a transaction (the
|
|
1610
|
+
* same MAX every reservation uses, W6), so the next write never reuses a number the
|
|
1611
|
+
* server already holds (RT-35: the device lost the tail of its log, for example on a
|
|
1612
|
+
* reload inside the IndexedDB snapshot window or after restoring an older copy).
|
|
1613
|
+
*
|
|
1614
|
+
* @returns Whether the counter moved
|
|
1615
|
+
*/
|
|
1616
|
+
raiseSequenceFloor(nodeId: string, floor: number): Promise<boolean>;
|
|
1617
|
+
/**
|
|
1618
|
+
* Give one of a local node's operations a fresh sequence number above `floor`
|
|
1619
|
+
* (RT-35): the server refused it with `SEQUENCE_CONFLICT` because it holds another
|
|
1620
|
+
* operation of this node under that number (one this device lost). A version-1
|
|
1621
|
+
* operation keeps its id (its hash does not cover the sequence number), exactly like
|
|
1622
|
+
* the W6 sequence repair; a version-2 operation (protocol v2) is re-hashed under the
|
|
1623
|
+
* new number, so it carries a new id. The old identity is recorded in
|
|
1624
|
+
* `_kora_seq_conflicts` (`reemitted_as`).
|
|
1625
|
+
*
|
|
1626
|
+
* A new id would leave later operations naming the old one in `causalDeps`: those in
|
|
1627
|
+
* `rewritableDependents` (operations the server never stored: never sent) are
|
|
1628
|
+
* rewritten to name the new id, and re-hashed, transitively, in the same
|
|
1629
|
+
* transaction. Dependents already sent keep their ids (the server may hold them);
|
|
1630
|
+
* their dep resolves through the `_kora_seq_conflicts` record. Every touched record
|
|
1631
|
+
* is re-folded (fold stamps carry operation ids).
|
|
1632
|
+
*
|
|
1633
|
+
* @param rewritableDependents - Ids of this node's operations that were never sent
|
|
1634
|
+
* @returns The renumbered operation and the rewritten dependents, or null when the
|
|
1635
|
+
* operation is not in the log
|
|
1636
|
+
*/
|
|
1637
|
+
resequenceOperation(operationId: string, nodeId: string, floor: number, rewritableDependents?: readonly string[]): Promise<ResequenceResult | null>;
|
|
1638
|
+
/**
|
|
1639
|
+
* Durability barrier (RT-35): resolves once every write committed so far is durable
|
|
1640
|
+
* on this device. Sync awaits it before an operation leaves the device.
|
|
1641
|
+
*/
|
|
1642
|
+
ensureDurable(): Promise<void>;
|
|
1643
|
+
/** Record operations the server refused for good (RT-36). Never cleared. */
|
|
1644
|
+
recordTerminalRejections(entries: TerminalRejection[]): Promise<void>;
|
|
1645
|
+
/** Which of these operation ids the server refused for good (RT-36). */
|
|
1646
|
+
findTerminalRejections(operationIds: string[]): Promise<Set<string>>;
|
|
1647
|
+
/** Every node id this database authored operations under (RT-38, RT-40). */
|
|
1648
|
+
listLocalNodes(): Promise<LocalNodeRecord[]>;
|
|
1649
|
+
/** Forget a drained local node other than this store's own (RT-40). */
|
|
1650
|
+
forgetLocalNode(nodeId: string): Promise<void>;
|
|
1651
|
+
/** The current refusal cycle: the count of accepted sync handshakes (RT-38). */
|
|
1652
|
+
loadAcceptedCycle(): Promise<number>;
|
|
1653
|
+
/** Record an accepted sync handshake as `nodeId` (RT-38). */
|
|
1654
|
+
markLocalNodeAccepted(nodeId: string): Promise<void>;
|
|
1655
|
+
/** Record that the sync server refused `nodeId`; `held` holds its writes (RT-38). */
|
|
1656
|
+
markLocalNodeRefused(nodeId: string, held: boolean): Promise<void>;
|
|
1657
|
+
/**
|
|
1658
|
+
* Take over another local node's unsynced writes (RT-40): resolves to a release
|
|
1659
|
+
* function when no live tab uses `nodeId` (its lock is free), or null when one does.
|
|
1660
|
+
* Without Web Locks (Node, old browsers) the node is assumed free.
|
|
1661
|
+
*/
|
|
1662
|
+
claimLocalNode(nodeId: string): Promise<(() => void) | null>;
|
|
1663
|
+
/** Whether a live tab holds `nodeId` (per-tab isolation, RT-40). */
|
|
1664
|
+
isLocalNodeLive(nodeId: string): Promise<boolean>;
|
|
914
1665
|
rebaseUnsyncedOperations(unsyncedOpIds: string[], correctedNowMs: number): Promise<ClockRebaseResult>;
|
|
915
1666
|
/**
|
|
916
1667
|
* Count of local operations ahead of the server version vector.
|
|
917
1668
|
*/
|
|
918
1669
|
countUnsyncedOperations(serverVector: VersionVector): Promise<number>;
|
|
919
1670
|
/**
|
|
920
|
-
* Compact the local operation log
|
|
921
|
-
*
|
|
1671
|
+
* Compact the local operation log. Only removes ops the server has acknowledged
|
|
1672
|
+
* (per {@link CompactionStrategy}). With the W7 fold (STORE-14), the effect of the
|
|
1673
|
+
* removed operations is first joined into each record's base fold state, so a
|
|
1674
|
+
* re-fold (rejection exclusion, a later re-materialization) and the merge of a
|
|
1675
|
+
* late operation stay exact; delete, atomic and custom-resolver operations are
|
|
1676
|
+
* kept, and a re-delivered compacted id is a duplicate by its node's compacted
|
|
1677
|
+
* prefix.
|
|
922
1678
|
*/
|
|
923
1679
|
compact(strategy: CompactionStrategy): Promise<CompactionResult>;
|
|
924
1680
|
/**
|
|
@@ -971,13 +1727,47 @@ declare class Store implements OperationLog {
|
|
|
971
1727
|
*/
|
|
972
1728
|
exportBackup(options?: BackupOptions): Promise<Uint8Array>;
|
|
973
1729
|
/**
|
|
974
|
-
* Restore data from a backup binary.
|
|
1730
|
+
* Restore data from a backup binary (format version 2, STORE-5).
|
|
1731
|
+
*
|
|
1732
|
+
* - `merge: true`: every backup operation is applied through the remote-apply path
|
|
1733
|
+
* (deduplicated by id, merged per field), the version vector advances by MAX, and
|
|
1734
|
+
* nothing of the exporting device's identity or sync state is imported.
|
|
1735
|
+
* - Replace (default): the data is replaced by the backup's while this device keeps
|
|
1736
|
+
* its node id, local nodes and principal bindings, its own sequence counters never
|
|
1737
|
+
* move backwards, and unsynced own writes are kept (see
|
|
1738
|
+
* `RestoreOptions.keepUnsyncedWrites`). Then {@link reloadFromDisk} runs.
|
|
1739
|
+
*
|
|
1740
|
+
* A version-1 file (Kora 1.0.0-beta.12 and earlier) is converted first, exactly as
|
|
1741
|
+
* `convertBackupV1` does (`result.convertedFromVersion: 1`); one holding an operation
|
|
1742
|
+
* whose timestamp cannot be recovered is refused (`BACKUP_OPERATION_INVALID`).
|
|
975
1743
|
*
|
|
976
1744
|
* @param data - The backup data
|
|
977
|
-
* @param options - Restore options (merge, collections, onProgress)
|
|
1745
|
+
* @param options - Restore options (merge, collections, keepUnsyncedWrites, onProgress)
|
|
1746
|
+
* @param internal - `applyOperation`: the app's remote-apply path (defaults to
|
|
1747
|
+
* {@link applyRemoteOperation}); `createApp` passes the merge-aware pipeline sync uses
|
|
978
1748
|
* @returns Result of the restore operation
|
|
979
1749
|
*/
|
|
980
|
-
importBackup(data: Uint8Array, options?: RestoreOptions
|
|
1750
|
+
importBackup(data: Uint8Array, options?: RestoreOptions, internal?: {
|
|
1751
|
+
applyOperation?: (operation: Operation) => Promise<ApplyResult>;
|
|
1752
|
+
}): Promise<RestoreResult>;
|
|
1753
|
+
/**
|
|
1754
|
+
* Merge mode (RT-66): join the backup's compacted base states and row snapshots
|
|
1755
|
+
* into this database's, and re-fold those records, before the backup's operations
|
|
1756
|
+
* are applied. The join is the fold's state-based merge, so the result equals
|
|
1757
|
+
* folding both devices' histories together. Compacted prefixes advance by MAX
|
|
1758
|
+
* (unfiltered restores only: the prefix is per node, across collections).
|
|
1759
|
+
*/
|
|
1760
|
+
private mergeBackupFoldState;
|
|
1761
|
+
/**
|
|
1762
|
+
* Re-read this store's state from the database after it changed underneath it (a
|
|
1763
|
+
* backup restore): the node id, version vector and own sequence counter, the clock
|
|
1764
|
+
* (advanced past every timestamp in the log), and a fresh causal tracker. Every live
|
|
1765
|
+
* query re-runs. The local node registry and terminal rejections are always read
|
|
1766
|
+
* from the database, so they need no reload.
|
|
1767
|
+
*/
|
|
1768
|
+
reloadFromDisk(): Promise<void>;
|
|
1769
|
+
/** The greatest HLC timestamp in the operation log (canonical strings sort by HLC). */
|
|
1770
|
+
private loadNewestLogTimestamp;
|
|
981
1771
|
/**
|
|
982
1772
|
* Read backup manifest without loading the entire backup.
|
|
983
1773
|
*
|
|
@@ -986,21 +1776,35 @@ declare class Store implements OperationLog {
|
|
|
986
1776
|
*/
|
|
987
1777
|
static readBackupManifest(data: Uint8Array): BackupManifest;
|
|
988
1778
|
private recordOperationSequence;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
* Check the stored schema version and run any pending migrations.
|
|
992
|
-
* Migrations are applied in version order within a transaction.
|
|
993
|
-
*/
|
|
994
|
-
private runMigrationsIfNeeded;
|
|
1779
|
+
/** Post-commit publication of a committed local operation. */
|
|
1780
|
+
private publishLocalOperation;
|
|
995
1781
|
/**
|
|
996
|
-
*
|
|
1782
|
+
* Map an out-of-space storage failure to `store:quota-exceeded` (STORE-15).
|
|
1783
|
+
* The error itself still propagates to the caller; this only makes the
|
|
1784
|
+
* condition observable on every adapter, not just IndexedDB.
|
|
997
1785
|
*/
|
|
998
|
-
private
|
|
1786
|
+
private reportStorageError;
|
|
1787
|
+
/** Report a database a newer build migrated (`store:schema-ahead`) and build the error. */
|
|
1788
|
+
private schemaAhead;
|
|
999
1789
|
/**
|
|
1000
|
-
* Run
|
|
1001
|
-
*
|
|
1790
|
+
* Run pending schema migrations (STORE-13, NEW-STORE-1): one transaction per version
|
|
1791
|
+
* (DDL, backfills, `schema_version`); backfills write operations through the local
|
|
1792
|
+
* write path so they sync, unless declared `localOnly`.
|
|
1002
1793
|
*/
|
|
1003
|
-
private
|
|
1794
|
+
private runMigrationsIfNeeded;
|
|
1795
|
+
/** The node id the database persisted before this open, if any. */
|
|
1796
|
+
private readPersistedNodeId;
|
|
1797
|
+
/**
|
|
1798
|
+
* Register node ids this database authored under that the registry does not know
|
|
1799
|
+
* yet: the node id a pinned `nodeId` (the auth device id) just replaced, and the
|
|
1800
|
+
* authors of queued operations. A beta.12 database had no registry, and a template
|
|
1801
|
+
* app created before its user signed in authored under a random node; once the
|
|
1802
|
+
* store is pinned to the device id, that node's queued writes would otherwise sit in
|
|
1803
|
+
* the queue forever, neither uploaded nor reported. Registered, they are adopted or
|
|
1804
|
+
* held (`status.heldNodes`) like any other local node's writes. Only the outbound
|
|
1805
|
+
* queue is read: received operations never enter it.
|
|
1806
|
+
*/
|
|
1807
|
+
private registerUnregisteredAuthors;
|
|
1004
1808
|
private loadOrGenerateNodeId;
|
|
1005
1809
|
private loadSequenceNumber;
|
|
1006
1810
|
private loadVersionVector;
|
|
@@ -1017,6 +1821,147 @@ interface CollectionAccessor {
|
|
|
1017
1821
|
where(conditions: Record<string, unknown>): QueryBuilder;
|
|
1018
1822
|
}
|
|
1019
1823
|
|
|
1824
|
+
declare const LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
|
|
1825
|
+
/**
|
|
1826
|
+
* Serialize a version vector for `_kora_meta` storage.
|
|
1827
|
+
*/
|
|
1828
|
+
declare function serializeVersionVectorToMeta(vector: VersionVector): string;
|
|
1829
|
+
/**
|
|
1830
|
+
* Deserialize a version vector from `_kora_meta`.
|
|
1831
|
+
*/
|
|
1832
|
+
declare function deserializeVersionVectorFromMeta(value: string): VersionVector;
|
|
1833
|
+
/**
|
|
1834
|
+
* Merge two version vectors, keeping the maximum sequence per node.
|
|
1835
|
+
*/
|
|
1836
|
+
declare function mergeVersionVectors(a: VersionVector, b: VersionVector): VersionVector;
|
|
1837
|
+
/**
|
|
1838
|
+
* Operations present locally but not yet on the server (per server version vector).
|
|
1839
|
+
*/
|
|
1840
|
+
declare function collectOperationsAheadOfServer(localVector: VersionVector, serverVector: VersionVector, fetchRange: (nodeId: string, fromSeq: number, toSeq: number) => Promise<Operation[]>): Promise<Operation[]>;
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Normalise a `where` clause to its canonical form (RT-102). This is the ONE place that
|
|
1844
|
+
* decides what a where value means, so the SQL the store runs, the descriptor and the
|
|
1845
|
+
* query key (store cache, React/Vue/Svelte bindings) always agree:
|
|
1846
|
+
*
|
|
1847
|
+
* - `undefined` means "no condition": `where({ projectId: undefined })` is `where({})`
|
|
1848
|
+
* (the usual "nothing selected yet" pattern), and an operator whose operand is
|
|
1849
|
+
* `undefined` (`{ $gt: undefined }`) is dropped; a field left with no operator is
|
|
1850
|
+
* dropped. A later `.where({ x: undefined })` therefore does not remove an earlier
|
|
1851
|
+
* condition on `x`.
|
|
1852
|
+
* - `null` means SQL `IS NULL` (`$ne: null` means `IS NOT NULL`).
|
|
1853
|
+
* - Non-finite numbers (`NaN`, `Infinity`) have no stored representation: refused.
|
|
1854
|
+
* - `undefined` inside a `$in` list is refused (ambiguous: no condition cannot be one
|
|
1855
|
+
* element of a list).
|
|
1856
|
+
*
|
|
1857
|
+
* @param where - The clause as the developer wrote it
|
|
1858
|
+
* @returns A new clause with the rules above applied
|
|
1859
|
+
* @throws {QueryError} For a non-finite number or an `undefined` `$in` element
|
|
1860
|
+
*/
|
|
1861
|
+
declare function normalizeWhere(where: WhereClause): WhereClause;
|
|
1862
|
+
/**
|
|
1863
|
+
* The canonical identity of a query: equal keys if and only if the descriptors run the
|
|
1864
|
+
* same query (RT-102). Used by the store's query-store cache and by every framework
|
|
1865
|
+
* binding to decide when to share a store or re-subscribe.
|
|
1866
|
+
*
|
|
1867
|
+
* Unlike `JSON.stringify`, nothing is dropped or conflated: `where` keys are sorted
|
|
1868
|
+
* (their order does not change the query), `undefined` is dropped exactly as
|
|
1869
|
+
* {@link normalizeWhere} drops it, and `NaN`, `Date` and byte values keep distinct,
|
|
1870
|
+
* typed encodings.
|
|
1871
|
+
*
|
|
1872
|
+
* @param descriptor - The query descriptor (`queryBuilder.getDescriptor()`)
|
|
1873
|
+
* @returns A string key
|
|
1874
|
+
*/
|
|
1875
|
+
declare function queryKey(descriptor: QueryDescriptor): string;
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* Durable-storage state of this origin, as far as Kora knows (NEW-STORE-4).
|
|
1879
|
+
* - `unknown`: not checked yet (the boot check runs in the background).
|
|
1880
|
+
* - `persisted`: the browser granted persistent storage; it will not evict this
|
|
1881
|
+
* origin's data under storage pressure.
|
|
1882
|
+
* - `best-effort`: storage may be evicted under pressure (the browser default).
|
|
1883
|
+
* - `unsupported`: the runtime has no StorageManager persistence API (Node,
|
|
1884
|
+
* older browsers, some WebViews).
|
|
1885
|
+
* - `error`: the check or request threw; `lastError` says why.
|
|
1886
|
+
*/
|
|
1887
|
+
type StoragePersistenceState = 'unknown' | 'persisted' | 'best-effort' | 'unsupported' | 'error';
|
|
1888
|
+
/** Snapshot returned by `app.storage.persistence.status()`. */
|
|
1889
|
+
interface StoragePersistenceStatus {
|
|
1890
|
+
state: StoragePersistenceState;
|
|
1891
|
+
/** True only when the browser reported persistent storage. */
|
|
1892
|
+
persisted: boolean;
|
|
1893
|
+
/** True once `persist()` was asked for (explicitly or by an automatic trigger). */
|
|
1894
|
+
requested: boolean;
|
|
1895
|
+
/** Message of the last failure, when `state` is `error`. */
|
|
1896
|
+
lastError?: string;
|
|
1897
|
+
}
|
|
1898
|
+
/** Why an automatic (background) persistence request was made. */
|
|
1899
|
+
type PersistenceRequestReason = 'explicit' | 'sign-in' | 'first-write' | 'installed-app';
|
|
1900
|
+
/** The subset of `navigator.storage` used here. */
|
|
1901
|
+
interface PersistenceStorageManager {
|
|
1902
|
+
persist?: () => Promise<boolean>;
|
|
1903
|
+
persisted?: () => Promise<boolean>;
|
|
1904
|
+
}
|
|
1905
|
+
/** Options for {@link StoragePersistence}. */
|
|
1906
|
+
interface StoragePersistenceOptions {
|
|
1907
|
+
/** Receives `storage:persistence` events. */
|
|
1908
|
+
emitter?: KoraEventEmitter | null;
|
|
1909
|
+
/**
|
|
1910
|
+
* The StorageManager to use. Defaults to `globalThis.navigator.storage`,
|
|
1911
|
+
* resolved on each call so tests and late polyfills are honoured.
|
|
1912
|
+
*/
|
|
1913
|
+
storage?: PersistenceStorageManager | null;
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Tracks and requests durable storage (`navigator.storage.persist()`) without ever
|
|
1917
|
+
* putting it on the startup path (NEW-STORE-4).
|
|
1918
|
+
*
|
|
1919
|
+
* In Firefox `persist()` opens a permission prompt and its promise stays pending
|
|
1920
|
+
* until the user answers, so awaiting it inside startup could hang `app.ready`
|
|
1921
|
+
* forever. Kora therefore:
|
|
1922
|
+
* - checks `persisted()` at boot, which never prompts, in the background;
|
|
1923
|
+
* - requests persistence only on an explicit `request()` or, without awaiting,
|
|
1924
|
+
* after a meaningful moment (sign-in, the first local write, running as an
|
|
1925
|
+
* installed app);
|
|
1926
|
+
* - surfaces every result through `status()` and the `storage:persistence` event,
|
|
1927
|
+
* never by blocking.
|
|
1928
|
+
*/
|
|
1929
|
+
declare class StoragePersistence {
|
|
1930
|
+
private current;
|
|
1931
|
+
private inFlightRequest;
|
|
1932
|
+
private readonly emitter;
|
|
1933
|
+
private readonly storageOverride;
|
|
1934
|
+
constructor(options?: StoragePersistenceOptions);
|
|
1935
|
+
/** Synchronous snapshot of the last known state. Never prompts. */
|
|
1936
|
+
status(): StoragePersistenceStatus;
|
|
1937
|
+
/**
|
|
1938
|
+
* Read whether storage is already persistent (`navigator.storage.persisted()`).
|
|
1939
|
+
* Never prompts and never throws: a failure becomes `state: 'error'`.
|
|
1940
|
+
*/
|
|
1941
|
+
check(): Promise<StoragePersistenceStatus>;
|
|
1942
|
+
/**
|
|
1943
|
+
* Ask the browser for persistent storage (`navigator.storage.persist()`).
|
|
1944
|
+
* May show a permission prompt (Firefox), so call it from a user gesture or a
|
|
1945
|
+
* meaningful moment, and do not block rendering on it. Concurrent calls share
|
|
1946
|
+
* one request. Never throws: a failure becomes `state: 'error'`.
|
|
1947
|
+
*
|
|
1948
|
+
* @returns The state after the browser answered
|
|
1949
|
+
*/
|
|
1950
|
+
request(): Promise<StoragePersistenceStatus>;
|
|
1951
|
+
/**
|
|
1952
|
+
* Fire-and-forget `request()` for an automatic trigger. Never awaited by Kora,
|
|
1953
|
+
* so a pending prompt cannot block anything; the outcome arrives as a
|
|
1954
|
+
* `storage:persistence` event. Skipped when storage is already persistent or a
|
|
1955
|
+
* request was already made this session.
|
|
1956
|
+
*
|
|
1957
|
+
* @param _reason - Which trigger fired (kept for tracing at call sites)
|
|
1958
|
+
*/
|
|
1959
|
+
requestInBackground(_reason: PersistenceRequestReason): void;
|
|
1960
|
+
private storage;
|
|
1961
|
+
private update;
|
|
1962
|
+
private emit;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1020
1965
|
/**
|
|
1021
1966
|
* Bridges the async QueryBuilder.subscribe() API with synchronous snapshot reads
|
|
1022
1967
|
* required by React useSyncExternalStore, Vue composables, and Svelte stores.
|
|
@@ -1030,6 +1975,7 @@ declare class QueryStore<T = CollectionRecord> {
|
|
|
1030
1975
|
private unsubscribeQuery;
|
|
1031
1976
|
private active;
|
|
1032
1977
|
private hasEmittedSnapshot;
|
|
1978
|
+
private error;
|
|
1033
1979
|
private readonly queryBuilder;
|
|
1034
1980
|
constructor(queryBuilder: QueryBuilder<T>);
|
|
1035
1981
|
/**
|
|
@@ -1045,6 +1991,12 @@ declare class QueryStore<T = CollectionRecord> {
|
|
|
1045
1991
|
* authoritative result for the current subscription lifecycle.
|
|
1046
1992
|
*/
|
|
1047
1993
|
hasSnapshot: () => boolean;
|
|
1994
|
+
/**
|
|
1995
|
+
* The latest query failure, or null. Set when the query (or a re-run after a
|
|
1996
|
+
* write) fails; cleared by the next successful delivery. Listeners are notified
|
|
1997
|
+
* on both transitions, so bindings read it next to `getSnapshot()`.
|
|
1998
|
+
*/
|
|
1999
|
+
getError: () => Error | null;
|
|
1048
2000
|
/** Tear down listeners and the underlying query subscription. */
|
|
1049
2001
|
destroy(): void;
|
|
1050
2002
|
private startSubscription;
|
|
@@ -1282,6 +2234,12 @@ interface RichTextControllerSnapshot {
|
|
|
1282
2234
|
canUndo: boolean;
|
|
1283
2235
|
canRedo: boolean;
|
|
1284
2236
|
cursors: readonly RichTextCursorInfo[];
|
|
2237
|
+
/**
|
|
2238
|
+
* True while local edits are not in a successful save: a save is pending, in
|
|
2239
|
+
* flight, or was refused (see `error`). The edits stay in the live document and
|
|
2240
|
+
* are saved by the next edit or `retrySave()`.
|
|
2241
|
+
*/
|
|
2242
|
+
hasUnsavedChanges: boolean;
|
|
1285
2243
|
}
|
|
1286
2244
|
interface RichTextController {
|
|
1287
2245
|
readonly doc: yjs.Doc;
|
|
@@ -1293,12 +2251,29 @@ interface RichTextController {
|
|
|
1293
2251
|
setCursor(anchor: number, head: number): void;
|
|
1294
2252
|
clearCursor(): void;
|
|
1295
2253
|
setUser(user: RichTextAwarenessUser | undefined): void;
|
|
2254
|
+
/**
|
|
2255
|
+
* Save the live document now (for example after a refused save, once the cause
|
|
2256
|
+
* is fixed). Resolves when the save settled; its outcome is in the snapshot.
|
|
2257
|
+
*/
|
|
2258
|
+
retrySave(): Promise<void>;
|
|
2259
|
+
/**
|
|
2260
|
+
* The full document state (a Yjs update) while local edits are unsaved, else
|
|
2261
|
+
* null. Keep it as a recovery copy when saves keep failing.
|
|
2262
|
+
*/
|
|
2263
|
+
getUnsavedState(): Uint8Array | null;
|
|
2264
|
+
/** Stop syncing; edits still waiting for the debounced save are saved first. */
|
|
1296
2265
|
destroy(): void;
|
|
1297
2266
|
}
|
|
1298
2267
|
|
|
1299
2268
|
/**
|
|
1300
2269
|
* Framework-agnostic richtext binding: Yjs document lifecycle, persistence,
|
|
1301
2270
|
* incremental doc channel, and awareness cursors.
|
|
2271
|
+
*
|
|
2272
|
+
* Every save writes the full state of the live Y.Doc, which holds every local edit
|
|
2273
|
+
* and every merged remote change, so a stored change arriving while a save waits
|
|
2274
|
+
* cannot drop local typing. A refused save keeps the edits unsaved
|
|
2275
|
+
* (`hasUnsavedChanges`, `getUnsavedState()`) and the next edit or `retrySave()`
|
|
2276
|
+
* saves them again; the first successful save clears `error`.
|
|
1302
2277
|
*/
|
|
1303
2278
|
declare function createRichTextController(options: CreateRichTextControllerOptions): RichTextController;
|
|
1304
2279
|
|
|
@@ -1360,14 +2335,22 @@ declare function pluralize(word: string): string;
|
|
|
1360
2335
|
*/
|
|
1361
2336
|
declare function singularize(word: string): string;
|
|
1362
2337
|
|
|
2338
|
+
/** Current backup format version. */
|
|
2339
|
+
declare const BACKUP_VERSION = 2;
|
|
2340
|
+
/** A backup file this release cannot restore as-is. */
|
|
2341
|
+
declare class BackupFormatError extends KoraError {
|
|
2342
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
2343
|
+
}
|
|
1363
2344
|
/**
|
|
1364
|
-
* Export a backup from
|
|
2345
|
+
* Export a backup (format version 2) from an opened database.
|
|
1365
2346
|
*
|
|
1366
|
-
*
|
|
2347
|
+
* Everything is read inside one transaction, so the backup is a consistent snapshot.
|
|
2348
|
+
* Operations are exported in canonical form (exactly what `deserializeOperation` reads
|
|
2349
|
+
* back), including tombstones, so a restore rebuilds the same log.
|
|
1367
2350
|
*
|
|
1368
2351
|
* @param adapter - The storage adapter
|
|
1369
2352
|
* @param schema - The schema definition
|
|
1370
|
-
* @param nodeId -
|
|
2353
|
+
* @param nodeId - Node id of the exporting device (informational only; never imported)
|
|
1371
2354
|
* @param schemaVersion - Schema version
|
|
1372
2355
|
* @param options - Backup options
|
|
1373
2356
|
* @returns Backup as a Uint8Array
|
|
@@ -1375,7 +2358,6 @@ declare function singularize(word: string): string;
|
|
|
1375
2358
|
declare function exportBackup(adapter: StorageAdapter, schema: SchemaDefinition, nodeId: string, schemaVersion: number, options?: BackupOptions): Promise<Uint8Array>;
|
|
1376
2359
|
/**
|
|
1377
2360
|
* Read backup manifest without loading the entire backup.
|
|
1378
|
-
* Parses only the first section (which is always the manifest).
|
|
1379
2361
|
*
|
|
1380
2362
|
* @param data - The raw backup data
|
|
1381
2363
|
* @returns The backup manifest
|
|
@@ -1388,14 +2370,41 @@ declare function readBackupManifest(data: Uint8Array): BackupManifest;
|
|
|
1388
2370
|
* @returns True if the checksum is valid
|
|
1389
2371
|
*/
|
|
1390
2372
|
declare function verifyBackupChecksum(data: Uint8Array): Promise<boolean>;
|
|
2373
|
+
/** Options for {@link convertBackupV1}. */
|
|
2374
|
+
interface ConvertBackupV1Options {
|
|
2375
|
+
/**
|
|
2376
|
+
* Drop operations whose timestamp cannot be recovered instead of refusing the file.
|
|
2377
|
+
* Default false.
|
|
2378
|
+
*/
|
|
2379
|
+
dropUnrecoverable?: boolean;
|
|
2380
|
+
}
|
|
1391
2381
|
/**
|
|
1392
|
-
*
|
|
2382
|
+
* Convert a version-1 backup (Kora 1.0.0-beta.12 and earlier) to version 2.
|
|
1393
2383
|
*
|
|
1394
|
-
*
|
|
2384
|
+
* - Operations: a v1 file exported from a database that a v1 restore had damaged holds
|
|
2385
|
+
* misread timestamps (`wallTime: null`); they are recovered from the misread values
|
|
2386
|
+
* (the same rule as the log-integrity scan). An unrecoverable one refuses the file
|
|
2387
|
+
* unless `dropUnrecoverable` is set.
|
|
2388
|
+
* - The `meta` section (the exporting device's node id, node tokens, sync state) is
|
|
2389
|
+
* dropped: it must never be imported into another database.
|
|
2390
|
+
* - Record rows are kept; v1 exported live rows only, so the manifest records
|
|
2391
|
+
* `includesTombstones: false`. Bytes that v1 serialized as `{"0":..,"1":..}` objects
|
|
2392
|
+
* are decoded on restore.
|
|
2393
|
+
*
|
|
2394
|
+
* @throws {BackupFormatError} When the input is not a valid version-1 backup
|
|
2395
|
+
*/
|
|
2396
|
+
declare function convertBackupV1(data: Uint8Array, options?: ConvertBackupV1Options): Promise<Uint8Array>;
|
|
2397
|
+
/**
|
|
2398
|
+
* Restore a backup into the database behind `adapter`.
|
|
2399
|
+
*
|
|
2400
|
+
* @deprecated Use `store.importBackup()` / `app.importBackup()`: restoring needs the
|
|
2401
|
+
* store (the device's identity, version vector, clock and live queries). This entry
|
|
2402
|
+
* point opens a temporary `Store` on `adapter`, imports, and CLOSES the adapter.
|
|
2403
|
+
*
|
|
2404
|
+
* @param adapter - A storage adapter no store is using
|
|
1395
2405
|
* @param schema - The schema definition
|
|
1396
2406
|
* @param data - The backup data
|
|
1397
2407
|
* @param options - Restore options
|
|
1398
|
-
* @returns Result of the restore operation
|
|
1399
2408
|
*/
|
|
1400
2409
|
declare function restoreBackup(adapter: StorageAdapter, schema: SchemaDefinition, data: Uint8Array, options?: RestoreOptions): Promise<RestoreResult>;
|
|
1401
2410
|
|
|
@@ -1471,8 +2480,8 @@ declare class OpfsBlobStore implements ContentAddressedBlobStore {
|
|
|
1471
2480
|
}
|
|
1472
2481
|
/**
|
|
1473
2482
|
* Create an {@link OpfsBlobDirectory} backed by the browser's Origin Private
|
|
1474
|
-
* File System, rooted at a named subdirectory.
|
|
1475
|
-
* storage
|
|
2483
|
+
* File System, rooted at a named subdirectory. It does not request persistent
|
|
2484
|
+
* storage: that is `app.storage.persistence`, kept off the startup path.
|
|
1476
2485
|
*
|
|
1477
2486
|
* @param rootDirName - The OPFS subdirectory to store blobs under (default `kora-blobs`)
|
|
1478
2487
|
* @throws {Error} If OPFS is unavailable (e.g. called outside a browser)
|
|
@@ -1759,6 +2768,13 @@ interface ChunkResponseMessage {
|
|
|
1759
2768
|
requestId: string;
|
|
1760
2769
|
/** The chunk bytes, or null when the responder does not hold that hash. */
|
|
1761
2770
|
bytes: Uint8Array | null;
|
|
2771
|
+
/**
|
|
2772
|
+
* The server refused the request for rate, not because the chunk is missing
|
|
2773
|
+
* (RT-24). The requester retries after `retryAfterMs` instead of failing.
|
|
2774
|
+
*/
|
|
2775
|
+
throttled?: boolean;
|
|
2776
|
+
/** With `throttled`: how long to wait before retrying, in ms. */
|
|
2777
|
+
retryAfterMs?: number;
|
|
1762
2778
|
}
|
|
1763
2779
|
type ChunkMessage = ChunkRequestMessage | ChunkResponseMessage;
|
|
1764
2780
|
/**
|
|
@@ -1776,18 +2792,35 @@ interface RemoteChunkProvider extends ChunkProvider {
|
|
|
1776
2792
|
/** Number of requests still awaiting a response (for diagnostics/tests). */
|
|
1777
2793
|
pendingCount(): number;
|
|
1778
2794
|
}
|
|
2795
|
+
/** Options for {@link createRemoteChunkProvider}. */
|
|
2796
|
+
interface RemoteChunkProviderOptions {
|
|
2797
|
+
/** Per-attempt timeout in ms (default 30s). */
|
|
2798
|
+
timeoutMs?: number;
|
|
2799
|
+
/**
|
|
2800
|
+
* Longest total time one chunk may spend waiting on "throttled" answers before
|
|
2801
|
+
* the request fails (default 10 minutes). The transfer is resumable, so a failed
|
|
2802
|
+
* request can simply be retried later.
|
|
2803
|
+
*/
|
|
2804
|
+
maxThrottleWaitMs?: number;
|
|
2805
|
+
/** Smallest back-off after a throttled answer, in ms (default 250). */
|
|
2806
|
+
minThrottleDelayMs?: number;
|
|
2807
|
+
/** Largest single back-off after a throttled answer, in ms (default 30s). */
|
|
2808
|
+
maxThrottleDelayMs?: number;
|
|
2809
|
+
}
|
|
1779
2810
|
/**
|
|
1780
2811
|
* Create a {@link ChunkProvider} that requests chunks over a message port,
|
|
1781
2812
|
* correlating each answer to its request and timing out a stalled request so a
|
|
1782
2813
|
* dropped response cannot hang a transfer forever (the transfer is resumable, so
|
|
1783
2814
|
* a timed-out request can simply be retried).
|
|
1784
2815
|
*
|
|
2816
|
+
* A "throttled" answer (the server's blob request budget is spent, RT-24) is not a
|
|
2817
|
+
* failure: the request is re-sent after the server's `retryAfterMs` (bounded, with
|
|
2818
|
+
* exponential back-off when absent), until `maxThrottleWaitMs` has elapsed.
|
|
2819
|
+
*
|
|
1785
2820
|
* @param port - The message port to the peer that holds the blob
|
|
1786
|
-
* @param options
|
|
2821
|
+
* @param options - Timeouts and throttle back-off bounds
|
|
1787
2822
|
*/
|
|
1788
|
-
declare function createRemoteChunkProvider(port: ChunkMessagePort, options?:
|
|
1789
|
-
timeoutMs?: number;
|
|
1790
|
-
}): RemoteChunkProvider;
|
|
2823
|
+
declare function createRemoteChunkProvider(port: ChunkMessagePort, options?: RemoteChunkProviderOptions): RemoteChunkProvider;
|
|
1791
2824
|
/**
|
|
1792
2825
|
* Serve blob chunk requests arriving on a message port from a blob store. Answers
|
|
1793
2826
|
* each `blob-chunk-request` with the bytes for that hash, or null when the store
|
|
@@ -1807,4 +2840,4 @@ declare function createChunkPortPair(): {
|
|
|
1807
2840
|
b: ChunkMessagePort;
|
|
1808
2841
|
};
|
|
1809
2842
|
|
|
1810
|
-
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 };
|
|
2843
|
+
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, NodeOwnedByAnotherUserError, 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 };
|