3.7 KiB
vault sync
(no submodules)
Purpose
sync.ts exposes the VaultSync orchestrator that delegates pull / push / sync operations to a VaultSyncLike adapter (production wires this to @suaveplan/vault-sync's SyncEngine). The orchestrator's responsibilities are: (1) project the in-memory item rows into the wire-shaped SyncSnapshot, (2) hydrate the delegate with the latest local snapshot before every operation, (3) translate the delegate's SyncDelegationResult into the public SyncResult envelope, and (4) wrap any delegate failure in a VAULT.SYNC.FAILED VaultError so consumers see a stable error code regardless of the underlying transport.
This keeps the sync engine generic — it has no knowledge of the vault's encryption layer, item schema, or container format. The orchestrator handles all the impedance matching at the boundary.
Features
pull() → SyncResult— pull remote changes via the delegate.push() → SyncResult— push local changes via the delegate.sync() → SyncResult— bidirectional sync via the delegate.currentSnapshot()— read-through to the delegate's view.- Snapshot is built on-demand from
rows()— the orchestrator never caches a stale snapshot. - All operations emit a histogram sample and an
op-tagged outcome counter; failures map toVAULT.SYNC.FAILEDwith the underlying error chained ascause.
Usage: Basic Example
import { VaultSync } from "@suaveplan/vault/sync/sync";
const sync = new VaultSync({
delegate: vaultSyncEngine,
rows: () => items.allRows(),
clock: Date.now,
});
const result = await sync.sync();
console.log(`merged ${result.merged.length} items`);
Usage: Advanced Example
import { VaultSync } from "@suaveplan/vault/sync/sync";
import { VaultError, VAULT_CODES } from "@suaveplan/vault/errors";
async function syncWithBackoff(sync: VaultSync, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await sync.sync();
} catch (err) {
if (
!VaultError.is(err) ||
err.code !== VAULT_CODES["SYNC.FAILED"] ||
i === attempts - 1
) throw err;
await new Promise((r) => setTimeout(r, 100 * 2 ** i));
}
}
throw new Error("unreachable");
}
API Reference
class VaultSync.constructor(options: SyncOptions).pull(): Promise<SyncResult>— throwsSYNC.FAILED.push(): Promise<SyncResult>— throwsSYNC.FAILED.sync(): Promise<SyncResult>— throwsSYNC.FAILED.currentSnapshot(): SyncSnapshot.
interface SyncOptions—{ delegate, rows, clock }.
Implementation Notes
buildSnapshotprojects only the wire-essential fields (id,updatedAt,version,deleted,fieldsJson,concealedFieldsJson,tagsJson). It deliberately does not decrypt concealed fields — sync sees ciphertext only, end-to-end. The remote service stores ciphertext, the merge layer compares it as opaque bytes, and concealed-field plaintext never leaves the vault layer.- The orchestrator hydrates the delegate before every operation. This keeps the snapshot correct even if the caller mutates items between successive
pull/pushcalls. VAULT.SYNC.FAILEDis the canonical wrapping for any error raised from the delegate. Callers can inspecterr.causefor the underlying reason (transport timeout, merge conflict, payload-too-large, etc.).- The OTel
outcomecounter uses a fixed allowlist (success | failed | rejected); the orchestrator emitssuccessandfailed. Therejectedslot is reserved for the lifecycle reject path (e.g.vault.sync.pullcalled while locked) which is enforced one layer up inVault.