/** * Local persistence for document bodies. * * Two things live on disk per document: * * - a **snapshot** — the whole document, rewritten when the pending log gets * long or when the document is evicted from memory; * - a **pending log** — an append-only stream of every update blob applied * since that snapshot, local *and* remote. * * The pending log is not an outbox. It exists so a hard crash between a commit * and the next snapshot loses nothing; the *network* catch-up on reconnect is a * version-vector diff, which covers whatever the server is missing regardless * of how the client got there. Local records additionally carry an `op_id` and * stay in the log until the server acks them, which is what makes "resend * unacked blobs in order on reconnect" possible across a reload. * * ## Record framing * * ``` * u32 record_len bytes that follow this field * u8 kind 1 = update, 2 = ack tombstone * u32 header_len * ... header UTF-8 JSON: { op_id, actor, local } * ... payload the Loro update blob (empty for a tombstone) * ``` * * Acks append a tombstone rather than rewriting the log, so every write is an * append and the log is only ever rewritten when it is folded into a snapshot. */ declare const RECORD_UPDATE = 1; declare const RECORD_ACK = 2; /** One update blob held in the pending log. */ export interface PendingRecord { /** Monotonic within one document. Also the resend order. */ seq: number; /** * The `op_id` the server deduplicates on. Present only for local records — * remote blobs are persisted for crash safety but are never sent back. */ op_id: string | null; actor: string | null; /** True for a blob this device produced and the server has not acked. */ local: boolean; blob: Uint8Array; } /** Everything needed to rebuild one document with no network. */ export interface LoadedDoc { snapshot: Uint8Array | null; /** In append order. Import them in this order after the snapshot. */ pending: PendingRecord[]; /** Highest `seq` ever used, so appends continue past a reload. */ next_seq: number; } /** Per-document metadata the quota sweeper reads without opening the document. */ export interface StoredDocMeta { node_id: string; byte_size: number; /** Epoch ms of the last open. Used only for LRU ordering, never for merges. */ last_access: number; /** True while at least one local blob is unacked. Never evicted for quota. */ has_unacked: boolean; } /** The per-document half of a {@link CrdtStorage}. */ export interface CrdtDocStore { readonly node_id: string; /** Snapshot + pending log, ready to import in order. */ load(): Promise; /** * Append one update blob. * * **Synchronous by contract.** The bytes must be handed to the filesystem * before control returns to the event loop — see * {@link OpfsCrdtStorage} for what that costs and when it is not achievable. */ appendUpdate(record: Omit): PendingRecord; /** Record that the server acked `op_id`; the blob stops being resendable. */ appendAck(op_id: string): void; /** Replace snapshot + log with one snapshot of the whole document. */ writeSnapshot(snapshot: Uint8Array): void; /** Resolves once every issued write has actually landed. */ flush(): Promise; /** Bytes this document currently occupies. */ byteSize(): number; /** Release handles. The files stay. */ close(): Promise; } /** The storage backend for a whole workspace of documents. */ export interface CrdtStorage { /** Whether this backend can run in the current environment. */ readonly available: boolean; open(node_id: string): Promise; /** Everything on disk, for the quota sweeper. Never opens a document. */ list(): Promise; /** Delete a document's local copy entirely. */ remove(node_id: string): Promise; /** Total bytes across all documents. */ usage(): Promise; } interface RecordHeader { op_id?: string; actor?: string; local?: boolean; } /** Encode one log record. Exported for the storage backends and their tests. */ export declare function encodeRecord(kind: typeof RECORD_UPDATE | typeof RECORD_ACK, header: RecordHeader, payload: Uint8Array): Uint8Array; export declare function encodeUpdateRecord(record: Omit): Uint8Array; export declare function encodeAckRecord(op_id: string): Uint8Array; /** * Decode a whole pending log, applying ack tombstones as it goes. * * A truncated tail — the signature of a crash mid-append — is **dropped * silently**. That is the correct behaviour: a half-written record was never * acked, so the document simply lands one edit earlier than the user's last * keystroke rather than failing to open at all. */ export declare function decodePendingLog(bytes: Uint8Array, start_seq?: number): LoadedDoc['pending']; interface MemoryDocState { snapshot: Uint8Array | null; log: Uint8Array[]; last_access: number; next_seq: number; } /** * An in-memory backend behind the real interface. * * Used by the test suite — OPFS does not exist in Node, and a fake filesystem * that pretends it does would test the fake. This stores the **same framed * bytes** the OPFS backend writes, so log framing, tombstone handling, crash * truncation and snapshot folding are all exercised for real; only the * `FileSystemSyncAccessHandle` calls are not. It is exported because a * consumer's own tests want it too, and because it is a correct backend for an * environment with no persistent storage at all. */ export declare class MemoryCrdtStorage implements CrdtStorage { readonly available = true; readonly docs: Map; open(node_id: string): Promise; list(): Promise; remove(node_id: string): Promise; usage(): Promise; } /** * The IndexedDB fallback, deliberately unimplemented. * * OPFS is available in every browser this package targets (Safari 15.2+, * Chrome 86+, Firefox 111+) and, crucially, is the *only* API that can offer * the synchronous append `transact()` promises. An IndexedDB backend could not * make that guarantee, so it would silently weaken the durability contract * rather than widen support. `storage: 'idb'` therefore throws rather than * quietly degrading; use {@link MemoryCrdtStorage} if you want a non-durable * backend on purpose. */ export declare class IdbCrdtStorage implements CrdtStorage { readonly available = false; constructor(); open(): Promise; list(): Promise; remove(): Promise; usage(): Promise; } export {}; //# sourceMappingURL=storage.d.ts.map