@nuucognition/mesh-client
v0.0.1-beta.0
Published
The mesh session layer. Mounts a mesh from **any** adapter, holds it in memory, serves every read synchronously, writes optimistically, and invalidates by content hash.
Keywords
Readme
@nuucognition/mesh-client
The mesh session layer. Mounts a mesh from any adapter, holds it in memory, serves every read synchronously, writes optimistically, and invalidates by content hash.
It is the veil: a host cannot tell a local filesystem mesh from a Convex-hosted
one. Web-API only — no node: imports, no process, no bare window or
indexedDB. Runs unchanged in browsers, Node, Bun, Deno, and workers.
import { mountMesh, MemoryStore } from "@nuucognition/mesh-client"
const client = await mountMesh({ adapter, store: new MemoryStore() })
client.listDocuments()
client.backlinks("Alpha")
client.sectionsTree()
client.titleSearch("alph", { limit: 10 })
const operations = await client.buildUpdateDocument({ title, frontmatter, content })
const result = await client.apply(operations, { intent: "editor.save" })
if (result.status === "rejected") {
// conflicts are data, not exceptions
}Mounting
mountMesh({ adapter, store?, indexDefinitions?, facetExtractors?, meshId?, meshName?, identityKey?, buster?, maxAgeMs?, author? })
adapter is either a mesh-core MeshAdapter (e.g. mesh-adapter-fs) or a
RemoteAdapter. The mesh is built with mesh-core's buildMesh and patched with
applyDocumentDelta / applyBlobDelta from then on — it is never rebuilt.
Reads
All synchronous except the three that need bytes off the wire.
| Method | Notes |
| --- | --- |
| listDocuments() | Every document in the mesh |
| getDocument(title) | undefined when absent |
| getContent(title) | Raw content, frontmatter included; memoized against the believed content hash |
| getDocumentView(title) | mesh-core loadDocumentView |
| listBlobs() / getBlob(blobKey) | getBlob normalizes bytes, base64 and URL transports into one shape |
| backlinks(title) | |
| sectionsTree() | Nested tree from #ie/sections/<name> |
| groups() | Flat list from #ie/groups/<name> |
| titleSearch(query, options?) | Ranked exact → prefix → word-prefix → substring |
| hydrationContext() | mesh-core createHydrationContext over the live mesh |
sectionsTree() and groups() prefer mesh-core's registered sections /
groups custom indexes and fall back to a prefix scan over the tag index, so
neither is a hard dependency.
Writes
apply(operations, meta?) is the only mutation path.
- Snapshot the pre-image of every touched document and blob.
- Apply the batch to the in-memory mesh and predict the resulting content
hashes with mesh-core's
hashContent. - Emit
optimistic-applied, submit to the adapter. - On ack, adopt server truth: any title whose hash disagrees with the
prediction is refetched. Emit
committed, write behind to the store. - On rejection, restore the pre-image exactly — no inverse-operation
computation — emit
rolled-backandconflict, and return the conflicts.
Exactly one batch is in flight; the rest queue FIFO. No rebasing: a batch
built on optimistic state that was just rolled back would carry an invalid
previousHash, and serialising removes that class of bug outright.
Conflicts never throw. apply() resolves to
{ status: "rejected", reason, conflicts }, where each conflict carries
{ title, expectedPreviousHash, actualHash, rejectedBatch }. Only transport
failures throw.
Operation builders are composed onto the client (buildCreateDocument,
buildUpdateDocument, buildDeleteDocument, buildRenameDocument,
buildRenameMedia, buildCreateBlob, buildDeleteBlob) and the raw mesh-core
builders are re-exported for hosts that want them.
Invalidation
refresh()— with a manifest-capable adapter: one round trip of hashes, a content-hash diff, then a batched refetch of only the stale titles. Without one: a full index reload diffed locally.refreshDocument(title)— unconditional refetch of one document.
contentHash is the only comparison key. mtime and updatedAt are
provenance, never invalidation.
Events
client.on("documents-changed", ({ titles, reason }) => {}) // optimistic | commit | rollback | refresh
client.on("batch", ({ batchId, phase, titles, intent }) => {}) // optimistic-applied | committed | rolled-back
client.on("conflict", ({ batchId, reason, conflicts }) => {})on returns an unsubscribe function. A listener that throws is isolated.
RemoteAdapter
The server-backed contract (v1.1). Two members diverge from mesh-core's adapter interfaces, so the base members are omitted rather than inherited.
interface RemoteAdapter {
capabilities: MeshAdapterCapabilities
loadIndex(): Promise<{ documents: MeshDocument[]; blobs: MeshBlob[] }>
getContent(title: string): Promise<string>
getContents(titles: string[]): Promise<Map<string, string>>
getSnapshot(title: string): Promise<MeshDocumentSnapshot>
getSnapshots(titles: string[]): Promise<MeshDocumentSnapshot[]>
getManifest(): Promise<{
version: number
entries: Array<{ title: string; id: string; contentHash: string }>
}>
getBlobData(blobKey: BlobKey): Promise<{ url: string } | { data: string }>
applyOperations(
operations: MeshOperation[],
opts?: { author?: string; intent?: string },
): Promise<
| { status: "applied"; appliedAt: string; batchId: string
hashes: Array<{ title: string; contentHash: string }> }
| { status: "rejected"; reason: "hash-conflict" | "invalid"
conflicts: Array<{ title: string; expectedPreviousHash?: string; actualHash?: string }> }
>
}MeshWriteAdapter.applyOperations resolves to void. MeshClient.apply
handles both: a void resolution means "applied, no server truth to
reconcile", which is the local-adapter path.
Store
MeshStore is the local mirror contract — async and chunk-friendly so an
IndexedDB, OPFS or worker-backed implementation drops in without touching a
call site. MemoryStore is the default and is exported from the package root
and from @nuucognition/mesh-client/store/memory.
Two rules govern every implementation:
- Confirmed state only. Optimistic state never reaches the store, so a reload mid-flight cannot resurrect a rejected write. Writes happen after ack.
- Best effort. Any store failure sets
client.storeDegradedand the client continues memory-only. A mirror can never fail a mesh operation.
The store is scoped by { meshId, identityKey, buster, maxAgeMs }. A buster or
max-age mismatch wipes it — caches are never migrated.
Not in v1
Live subscriptions, delta/cursor sync, offline write queues, mutation rebase, CRDTs, multi-tab coordination, persisted derived indexes, and an IndexedDB store implementation. See the research report for the full deferral list.
