Client API
App setup (with or without React)
import {
openEntDB,
entStateManager,
openDiplomaticClient,
} from "@interncom/diplomatic";
const entDB = await openEntDB(); // cache + optimistic UI notify (default)
// const entDB = await openEntDB({ optimistic: false }); // notify after durable
// const entDB = await openEntDB({ cache: false }); // durable IDB only
// const entDB = await openEntDB({ verbose: true }); // [dip] apply/list/persist timings
const state = entStateManager(entDB);
// worker: true → library spawns embedded sync Worker (only supported path)
const { client, dispose } = await openDiplomaticClient({ state, worker: true });
// openDiplomaticClient({ state, worker: true, verbose: true }) — same traces
// useClient({ worker: true, verbose: true }) — sameClient State
setSeed(seed)- Initialize the client's cryptographic seed (used to derive encryption keys and host authentication keys).
wipe()- Wipe all local data and disconnect from hosts.
Hosts
sync()- Synchronize with all connected hosts.
link(host)- Register with a host. See Auth Architecture.
unlink(label)- Stop syncing with the host identified by
label.
- Stop syncing with the host identified by
connect(listen)- Establish active connections to all linked hosts.
disconnect()- Disconnect from all hosts.
Data
Local writes build a message and call state.apply before the persist queue, the apply chain, and the message archive. With the default EntDB cache, apply patches mem immediately, queues IndexedDB persist, then notifies UI subscribers after the next paint (before IDB callbacks). That lets a fire-and-forget save close its modal in the current frame; a prior write's archive/persist cannot delay the notify. Pass { optimistic: false } to notify only after that durable commit. Archive → mark applied → upload still run after.
Shared fields on write ops. type is stored on the msg head (typ); body / pid / tags are msgpack in the msg body:
type EntFields<T> = {
type: string; // application type name (msg head typ)
body?: T; // application payload
pid?: EntityID; // optional parent eid (exclusive hierarchy)
tags?: string[]; // optional multi-value reverse-indexed tags (N:M refs)
};tags are opaque strings. EntDB multiEntry-indexes them for reverse lookup via getEntities({ type, tag }) — same performance model as pid reverse lookup, but multi-value. tag is an exact string, { range: { start, end, excludeStart?, excludeEnd? } } (lexicographic, inclusive by default), or { prefix }. Range start > end returns InvalidParam. Clients define conventions (e.g. impl:${btob64(eid)}, time-week-2026W01). Empty strings and duplicates are dropped on apply.
A rev is the latest observed identity of an ent:
type IEntRev = {
eid: EntityID;
ctr: number;
updatedAt: Date; // last-write time of this rev
};
// From a loaded ent: revFromEntity(ent)
// From a returned msg head: revFromHead(head)Methods
All writes take a single opts object (force optional where skew can apply).
genEID(id?)— allocate a new entity id (optional 8-byte id material).insert(op)— create a new ent (new eid, ctr 0).tsop: EntFields<T> & { id?: Uint8Array } // id = optional eid materialupdate(op)(preferred for edits) — next ctr isprior.ctr + 1; no archive I/O.tsop: EntFields<T> & { prior: IEntRev; force?: boolean; // clock-skew recovery; default client-wide } await client.update({ prior: revFromEntity(ent), type: "todo", body: { text: "milk", done: true }, });delete(op)— by prior (preferred) or eid (archive lookup).tstype IDeleteParams = | { prior: IEntRev; force?: boolean } // preferred | { eid: EntityID; force?: boolean }; // loads prior from archive await client.delete({ prior: revFromEntity(ent) }); await client.delete({ eid: ent.eid }); // slower
Rebuild
rebuild(options?)— wipe application state (e.g. EntDB) and re-derive it by replaying the local message archive.tsawait client.rebuild(); // default: inventory hosts first await client.rebuild({ checkHost: false }); // local archive onlyBy default (
checkHost: true), each linked host is peeked from sequence 0 so any bags missing from the local archive are pulled before replay. Use this after an applier/schema change when msgs are correct but derived ents are wrong. Does not wipe the message archive, seed, or hosts.With a sync worker, rebuild runs on the worker (network + EntDB apply off the main thread).
Checksums
Digests for comparing archives and LWW frontiers across devices. Both use the same set construction: blake3(concat(sort_lex(byte records))). Empty set → blake3 of empty input.
msgcheck() — message archive
msgcheck()→Hash(32-byte blake3 digest)Checksum of the set of msg head hashes (archive keys). Keys are decoded to raw bytes (store encoding such as base64 IDB keys is irrelevant), sorted lexicographically, concatenated, then hashed.
tsconst a = await clientA.msgcheck(); const b = await clientB.msgcheck(); // equal digests ⇒ same set of msgs locallyWith a sync worker, runs off the main thread.
EntDB frontier — checksum / entcheck
Not a content hash of bodies. Fingerprints live ents only (deletes are absent): each row as eid ‖ updatedAt ‖ ctr (varbytes eid, date as ms varint, ctr varint), then the same set-checksum as above.
entDB.checksum(crypto)→ValStat<Hash>— primary API; works on anyIEntDB(memory, IDB, cached). Cached EntDB checksums the durable store, not a partial in-memory cache.tsimport { crypto } from "@interncom/diplomatic"; const [digest, st] = await entDB.checksum(crypto);WorkerClient.entcheck()→Hash— same digest via the sync worker (off main). Not onIClient/SyncClient(those have no EntDB handle); callentDB.checksum(crypto)on the main-thread path.
Interpreting digests
msgcheck | Ent frontier | Meaning |
|---|---|---|
| equal | equal | Archives and LWW frontiers agree |
| equal | differ | Same msgs, wrong derived state → rebuild() |
| differ | * | Archives diverge → sync / rebuild({ checkHost: true }) |
Apply diagnostics
Each archived msg has an apply lifecycle state (apld):
| Value | Constant | Meaning |
|---|---|---|
"f" | APLD_PENDING | stored, not yet successfully applied (also transient storage errors) |
"t" | APLD_APPLIED | exec succeeded (Success / NoChange) |
"e" | APLD_ERROR | terminal apply failure; err holds a Status code |
Use these when digests disagree or the app looks out of sync:
countMsgs(apld?)→ number of msgs (optional filter). Prefer over list+length for badges.listMsgs(opts?)→ enumerate msgs for inspection.opts.apld— filter (e.g.APLD_ERRORfor poison msgs,APLD_PENDINGfor stuck drain)opts.body— include payloads and derivehead.hsh(defaulttrue). Passbody: falsefor lighter listings (body/hshomitted;head.lenstill reflects stored size).
import { APLD_ERROR, Status } from "@interncom/diplomatic";
const n = await client.countMsgs(APLD_ERROR);
if (n > 0) {
const failed = await client.listMsgs({ apld: APLD_ERROR });
for (const m of failed) {
console.warn(Status[m.err ?? 0], m.head.ctr, m.hash);
// m.body present by default — decode as needed
}
// Badge / head-only scan:
const light = await client.listMsgs({ apld: APLD_ERROR, body: false });
}
// After fixing the applier/schema:
await client.rebuild();With a sync worker, list/count read the shared message archive on the main thread (no worker RPC).
Host reconcile
Host bag count and client msg count are different metrics: bags are per-seq envelopes on the host; msgs are the distinct changes in a client archive (archive key = hash of the encoded msg). A host can hold duplicate bags for the same msg and/or be missing msgs the client has.
reconcile(hostLabel, opts?)→ValStat<ReconcileReport>- Full PEEK from seq 0 (inventory); advances
lastSeqon the host row. - Returns ephemeral
{ msgcheck, numBags, numDupes }(not stored). With defaultsync: true, after drain + re-inventory.msgcheckis the set of distinct msgs on the host (same construction asmsgcheck()). opts.pull(default true): enqueue downloads for msgs on the host missing locally.opts.push(default false): enqueue uploads for local archive msgs missing on the host.opts.sync(default true): runsync()afterward so queues drain, then re-inventory. Passsync: falsefor inventory-only.- Cancels debounced sync and waits for in-flight
sync()before inventory (same asrebuild).
- Full PEEK from seq 0 (inventory); advances
const [report, st] = await client.reconcile("primary", {
pull: true,
push: true,
});
// report.msgcheck, report.numBags, report.numDupes — keep in app state if needed
const localCheck = await client.msgcheck();
// report.msgcheck equals localCheck ⇒ same set of msgsnumDupes is the number of extra bags beyond one bag per msg (Σ max(0, bags_for_msg − 1)).
Import/Export
export(filename)- Export stored operations to a file.
import(file, options)- Import operations from a file and apply them locally.

