@kolu/surface-map reference
A dynamic keyed map of remote surfaces — one entry surface, typed once, keyed at
runtime, served as one. It depends only on @kolu/surface and has three public
subpaths: the declaration, the server, and the client (plus a test-only /testing
subpath, whose testMembershipId() mints a branded MembershipId for fixtures, and a
schema-only /evidence leaf carrying the failure-evidence vocabulary — EvidenceLine,
EvidenceLineSchema, FailureEvidence, FailureEvidenceSchema, all also re-exported
from . — for a consumer that must not pull the group builder into its bundle).
The three halves
| Export | Subpath | Role |
|---|---|---|
defineSurfaceMap({ key, entry, codec, failure, connection?, entriesClient?, name? }) | . | build a map from a key schema, an entry Surface<ES>, the key’s string codec, a failure schema for the domain failure value, an optional connection schema for the fine per-entry connection payload (SR9 — carried on the live arms, validated but never enumerated, like failure), an optional entriesClient policy for the membership collection (SR11), and an optional mount name; returns a SurfaceMap |
serveSurfaceMap(map, registry) | /server | back the map with a MapRegistry; returns { group, handlers, dispose } |
connectSurfaceMap(map, transport, opts?) | /client | consume the map; returns a SurfaceMapClient (with the Solid useEntry) |
defineSurfaceMap’s key schema need not be a branded string — its decoded type
is Key, which can be any validated value (kolu’s HostKey is a discriminated-sum
object). Its failure schema is required and fixes the domain failure value; Failure
is inferred from it (no explicit type args). The optional name is the sibling
key the map is mounted under in a combined surface (kolu’s "padi", drishti’s
"hosts"); it decides the map’s own tagPrefix at declaration time and drives the
client’s transport slice (below), so no stringly key is written at any connection site.
The SurfaceMap carries { keySchema, entry, group, tagPrefix, name?, entriesSpec, codec }.
group is the key-folded wire group: one Rpc per entry-member verb (with a
canonical-string mapKey folded into its payload) plus the two unfolded entries
membership members. A host that serves this map merges that group into its own —
there is no second “fragment” value to splice and nothing to cast, because a tag
carries its own route. tagPrefix is "surface/" for a root-served map and
"surface/<name>/" for a named one, read off the value by both the server’s handler
keys and the client’s re-tagging so there is one authority.
entriesSpec is the membership collection’s wire spec, always string-keyed. The domain
failure schema is the failure argument itself — embedded in entriesSpec.schema and
the entries.get success schema, so there is no separate failureSchema field. There
is deliberately no matching evidence option: failure evidence is a fixed
structural type this package owns (FailureEvidence and EvidenceLine, both exported
from . and from /evidence), not a domain value to parameterize — see EntryStatus
below. Their Effect schemas (FailureEvidenceSchema, EvidenceLineSchema) are exported
too: evidence reaches the map’s wire through the one door entryStatusSchema owns,
but the same line vocabulary is validated by @kolu/surface-remote’s
ConnectionInfoSchema.log, which now is EvidenceLineSchema rather than a re-declared
twin. The optional entriesClient (SR11) is the app-typed
client error policy (ClientCollectionPolicy<EP>) threaded onto entriesSpec.client,
so a map-membership subscription failure reaches the app’s connectSurfaceMap
onClientError; its EP is inferred from the value and defaults to never (a
policy-free map — every existing caller — keeps entriesSpec.client unfillable). The
membership collection has no per-key origin, so its interpreter fires origin-free.
connectSurfaceMap’s transport must be the branded parent handle (e.g.
conn.transport from connectSurfaces) or an in-process directDispatch — any
other wire dispatch throws. The scoping key derives from map.name: a named
map is re-tagged onto surface/<name>/… after the half-open guard, so it
inherits the parent’s watchdog by construction; a nameless map is served at the
transport root and not re-tagged. There is no siblingKey argument — the key
comes from the declaration. Scoping is now a tag rewrite over the resolved
dispatch, never a re-wrap of the transport value, so there is no step left that
could strip the half-open brand.
connectSurfaceMap’s optional third argument is ConnectSurfaceMapOptions<Key>,
whose onClientError?: (policy, err, origin?: { key: Key }) => void is the app’s
client error interpreter — the keyed-map counterpart to connectSurfaces’
onClientError (see the
core reference).
@kolu/surface-map is where a { key } origin exists, so — unlike the origin-free
base interpreter — this one carries it: a per-key entry member’s declared
client.onError fires with origin = { key } (injected by the per-key client
builder, which holds the decoded key), while the membership entries collection (no
per-key origin) fires with origin omitted. So an origin-requiring policy arm is only
reachable where the key exists, and the base buildSurfaceClient stays
origin-agnostic. Required in practice when the entry surface carries a policy — a
declared policy with no interpreter throws at construction.
const HostKeySchema = Schema.String;
const identityCodec: KeyCodec<string> = { encode: (k) => k, decode: (s) => s };
// The domain failure schema validates the value a failed entry publishes — a
// failed member cannot exist without one (there is no fabricated fallback cause).
const hostFailureSchema = Schema.Struct({ reason: Schema.String });
type HostFailure = typeof hostFailureSchema.Type;
const hostMap = defineSurfaceMap({
key: HostKeySchema,
entry,
codec: identityCodec,
failure: hostFailureSchema,
});
The client
SurfaceMapClient exposes entries (the one membership authority), live
(resolved transport liveness), codec (the one key-identity authority), and two
lenses onto an entry.
| Member | Returns |
|---|---|
entries | ReadOnlyBoundCollection<Key, EntryStatus> — the single membership authority; read-only (the map server is the sole writer — no client upsert/delete) |
live | Accessor<boolean> — resolved transport liveness for the membership strip |
codec | KeyCodec<Key> — the one key-identity authority; encode/decode the canonical wire string |
entry(key) | Entry<ES> — the pure point lens (no owner, no I/O, total) |
useEntry(keyAccessor) | Entry<ES> — the Solid lens; re-keys on key change and on a same-key membership re-add (a new membershipId); throws ownerless |
dispose() | tear down |
An Entry<ES> carries cells / collections / streams / events /
procedures (the same bound hooks a single surface has), plus:
| Member | Returns |
|---|---|
procedures | the entry’s declared procedures, bound and typed from the spec — entry.procedures.<ns>.<verb>(input); folds { mapKey } per call so the caller never passes the key |
rpc | the raw member face (rpc.surface.<member>.<verb>) — the addressing-layer escape hatch only, typed unknown per leaf (see below). The map folds no reserved system/* member, so per-entry liveness rides the entries authority instead |
clock.toLocal(remoteMs) | reproject a host-stamped epoch: number | null (null when the entry has no offset yet) |
state() | EntryState = ObservedEntryStatus | { kind: "not-a-member" } — a total fold over entries, never nullable |
entry(key) is a partial application of the key: a per-key SurfaceClient<ES>,
cached by the entry’s {encodedKey, membershipId} identity (see the
membership note below), so two views of one live member share one upstream
subscription. useEntry re-keys its reactive subscription whenever that identity
changes — an active-key switch (the encoded key changes: the old key’s
subscriptions dispose, the new key’s populate synchronously) and a same-key
membership re-add or authority restart (a new membershipId for the same
encoded key). It also canonicalizes its accessor’s key by its encoded string, so a
same-key re-decode of the same membership reads as a no-op, never a re-key.
The pure entry(key) point lens itself stays owner-free (an imperative call reads
the pending identity and routes identically by encoded key); the {encodedKey, membershipId} re-key is the reactive .use() lifetime useEntry drives.
const entries = app.entries.use(); // the ONE membership authority (the chips)
const hosts = entries.keys(); // each chip's key
const status = (host: string) => app.entry(host).state().kind; // warming/connected/failed
const active = app.useEntry(activeHost); // re-keys on switch; old subs dispose
const load = active.cells.load.use();
const processes = active.collections.processes.use();
Declared procedures ride entry.procedures.<ns>.<verb>(input) — a narrow
mapped type over the entry spec’s procedures, so it types straight from the
declaration with no cast, while dodging the union-budget overflow (TS2590) a
second precise mapped type over an abstract entry spec would trip. The
key-injecting dispatch folds { mapKey } into every call and rewrites the tag
onto the map’s own prefix, so the caller never passes the key. A procedure’s
declared error schema (ProcedureSpec.error) rides the folded member too, so
a tagged error raised behind the map’s keyed proxy arrives at the outer client
with its _tag and data intact — the map hop never demotes a declared error to a
defect. entry.rpc stays typed unknown per leaf — it is the structural
addressing face, and a declared procedure never needs it.
// Every declared procedure rides `entry.procedures.<ns>.<verb>`, bound and typed
// straight from the declaration — NO cast. (`entry.rpc` is the STRUCTURAL member
// face, reserved for the `system.*` members plus the escape hatch.) The
// key-injecting dispatch folds `{ mapKey }` into every call, so the caller never
// passes the key.
const kill = (
active: Entry<typeof entry.spec>,
pid: number,
): Effect.Effect<{ ok: boolean }, unknown> =>
active.procedures.proc.kill({ pid });
scopedByEntry — per-key state retained by membership
The second export of /client. It is the retained-owner dual of useEntry:
where useEntry disposes the old key’s world on every switch (right for cheap
wire subscriptions), scopedByEntry builds a per-key reactive owner for the
client’s own state and keeps it across switches — the focused tile, the camera,
per-host view posture — so switching hosts restores the world you left. Built over
@solid-primitives/keyed’s
keyArray (the ecosystem’s retained-per-key-root primitive), keyed by membership.
scopedByEntry(client, active, build) → { active(): T | undefined, get(key): T | undefined }
active: Accessor<K | null>is app policy —nullmeans nothing selected (drishti’s fleet grid; kolu never). It is not the map’s business which key matters.build(key, ctx)runs once per key on its first activation; its return value is that key’s owned world.ctx.isActiveis anAccessor<boolean>— the active-only discipline (WebGL release/re-acquire, center-on-active) lives inside the owner, not as a bridge between two independently-timed lifecycles.- Lifetime is
entriesmembership, not the wire. An owner is built lazily (a never-visited background host costs nothing), retained across every switch-away, and disposed the instant its key leavesentries. A removed-then-re-added host is a fresh member — lazy again, never resurrected. active()returns the active key’s world, orundefinedfor both anullactive and an active key that is not a current member (a removal race; a dev-modeconsole.warnnames the vanished key).get(key)is a background peek at any key’s world (W5 attention rollups) that never creates an owner —undefinedwhen the key was never activated or is not a member.- Throws if called outside a reactive owner — it holds a
keyArrayof per-key roots that must be disposed with the app.
Keys are folded to their canonical wire string through client.codec (below), not
compared by ===: two logically-equal keys from independent decodes need not be
reference-equal.
// Per-host CLIENT state whose lifetime is `entries` MEMBERSHIP — the retained
// dual of `useEntry`'s dispose-on-switch. An owner is built LAZILY on a key's
// first activation, RETAINED across every switch-away, and DISPOSED the instant
// its key leaves `entries` (a removed-then-re-added host is a FRESH member).
const scopes = scopedByEntry(app, activeHost, (host, ctx) => ({
tiles: new Map<number, string>(), // this host's OWN state — plain, per-host
focusedPid: createSignal<number | null>(null),
isFocused: ctx.isActive, // active-only discipline lives INSIDE the owner
label: host, // the key is in scope for whatever the owner builds
}));
scopes.active(); // the ACTIVE host's world: `T | undefined` (null / vanished)
scopes.get("web-01"); // a background peek at ANY key — never CREATES an owner
watchByEntry — per-member attention, watched eagerly
The third export of /client, and scopedByEntry’s eager twin on the same
shared membership kernel (codec identity, the entries view, retained-per-key
roots disposed on membership exit). Where scopedByEntry is lazy — an owner is
built on a key’s first activation — watchByEntry is eager: every member
gets a root the moment it joins, because a background host is precisely the one
you need to hear from.
watchByEntry(client, cell, items, onRaise) → { get(key): { kind: "live" | "stale"; value } | undefined }
cell(entry)selects the per-entry cell to watch (e => e.cells.urgency) — a get-onlyWatchableCell<A>whose.use()yields the current value and the rawSubscription(whoseupdatedcarries the change pairs).items(value)extracts stablePropertyKeyids from the cell value (v => v.awaitingIds). Ids arestring/number/symbol, never objects — the set-diff compares by value, and object ids (rebuilt fresh per frame) would compare by reference and re-raise every frame, so an object-iditemsis a compile error. Stability is the app’s one obligation — it is what makes “same item, not a new one” decidable.onRaise(key, raised, value)fires for newly-raised ids. Raise detection is a pure set-diff over the framework’supdated{ prev, next }pairs:items(next) ∖ items(prev). The change-iff-fired law upstream (seeSubscription.updated) is what makes a plain set-diff trustworthy — no hand-held previous frame, no frame classification, no per-window memory.get(key)is a point read —{ kind: "live" | "stale"; value }— orundefinedwhen the key is not a member OR has no frame yet (a mirror stays silent until the authority speaks — see @kolu/surface-remote).livemeans our link to the host is up and connected and this cell’s own subscription is neither errored nor ended; an errored/ended cell readsstale(its last value dims) rather than lying live, so a consumer never counts frozen data.stalealso holds the last value under a down link.- There is deliberately no
total(): aggregation is app policy (kolu sums awaiting terminals across live hosts; drishti counts hosts in trouble — a sum would be noise), so the watcher hands facts and each app folds. - Throws if called outside a reactive owner — it holds a
keyArrayof per-key roots that must be disposed with the app.
EntryStatus<Failure, Conn> — the projected per-entry status
// `Conn` (SR9): the fine per-entry connection payload, carried on the LIVE arms and
// parameterized like `Failure` — the map validates it against its own `connection`
// schema, never enumerating it. It is the ONE authority the coarse `kind` (the dot) and
// the fine word both derive from; optional, so a connection-less map omits it. The
// `failed` arm carries none at all — see the note on that arm below.
type EntryStatus<Failure = unknown, Conn = unknown> =
// `membershipId`: opaque, never-reused per-add identity — a BRANDED `MembershipId`
// (an empty/fabricated bare string is a compile error), minted only by
// `serveSurfaceMap` / the wire decode. Clients key cached owners on
// `{encodedKey, membershipId}`, so a same-key re-add / authority restart rebuilds.
| { kind: "warming"; membershipId: MembershipId; connection?: Conn }
| {
kind: "connected";
membershipId: MembershipId;
clockOffset: number | null; // own-clock offset; null = not-yet-measured
connection?: Conn;
}
// No `connection` on this arm — deliberately. The failed entry's live word would be
// the same frame `evidence` was pinned from, so `connection?.log` here was a second,
// floorable copy of the tail. Removing the field makes that read a compile error.
| {
kind: "failed";
membershipId: MembershipId;
failure: Failure; // schema-valid domain failure
evidence: FailureEvidence; // the retained output tail that EVIDENCES it
};
- Absence from
entriesis “not a member”. There is noabsentarm — a collection already expresses absence by not carrying the key. Client reads stay total via an explicit{ kind: "not-a-member" }value; the wire never carries it. - Every arm carries a
membershipId.serveSurfaceMapstamps an opaque, never-reusedcrypto.randomUUID()when a key enters membership, drops it when the key leaves, and publishes it on thewarming/connected/failedarms alike. The client keys every cached per-key owner on{encodedKey, membershipId}, so a same-key remove/re-add (a new id) and an authority restart (fresh ids for every member — the id map is per-server-instance, never reused across a restart) rebuild subscriptions by construction — no hand-rolled generation rearm. It is framework identity, not something a display consumer reads (they switch onkind, readfailure/clockOffset). membershipIdis a brandedMembershipId, not a barestring. The type isSchema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("MembershipId")), so an empty""or a client-fabricated id is a compile error — an opaque never-reused identity is unspellable by a consumer (P4). It is produced only two ways:serveSurfaceMap’s mint (decodeMembershipId(crypto.randomUUID()), the one sanctioned decode — it throws on an empty string, the fail-fast semantic at exactly this boundary) and the wireentryStatusSchemadecode (the one boundary a status is decoded through). The brand is erased at runtime (the value is the plain string), so keying and serialization are unchanged. Tests mint one through thetestMembershipId()helper on the@kolu/surface-map/testingsubpath — never a literal. The transient pre-frame gap (a key seen in the membership keyset before its first status frame) uses the single sanctionedPENDING_MEMBERSHIP_IDmarker, which is display-only and never keyed against.- The projection contract. Status is a projection of the resolved session’s state, never a second writer:
copying/connecting→warming;connected→connected; adisconnectedcarrying a standing domain failure, or a terminalfailed, →failed; a plain transientdisconnected(no specific failure) →warming.warmingmeans in motion (self-heals — an unreachable box retrying at backoff is warming, not failed);failedmeans it needs intervention. Failureis a type parameter. It defaults tounknownand is inferred from the map’sfailureschema; a domain instantiatesEntryStatus<PadiEntryFailure>at its own map so.failurenarrows there. The package carries the discriminant, never the enumeration.Connis the fine connection payload (SR9), carried on thewarmingandconnectedarms. Optional and inferred from the map’sconnectionschema, exactly likeFailure: the package carriesstate.connectionand validates it against that schema, never enumerating what a domain’s connection states are. It is the ONE authority the coarse dot (kind) and the fine word both derive from —serveHostMapproduces both from the sameSessionStateframe in one projection, so a “dot connected, word connecting” split (the drishti#102 divergence) has no construction path. A consumer reads the word off the same entry it reads the dot from (state().connection), never a second subscription; a map with noconnectionschema simply omits the field. Thefailedarm carries noconnectionat all — see the evidence bullets below for why that field’s absence is the point rather than an oversight.- The
failedarm carries a schema-valid domainfailurevalue, validated by the map’s ownfailureschema — not a loose object. padi’sPadiEntryFailureis aSchema.UnionofSchema.Structs discriminated on a structuralcauseliteral, each arm carrying its humanreason(and thecontract-skew-refusedarm additionally a typedrunning/expectedversion pair), so a consumer readsstate.failure.cause/state.failure.reason(and.running/.expectedon the skew arm) with full narrowing. - The
failedarm also carries the failure’sevidence, and it is required:FailureEvidence = readonly { source: "local" | "remote"; line: string }[], the retained output tail of the episode that failed. A failed status without it failsentryStatusSchema— enforcement at the codec, so “a reason without its evidence” cannot even be decoded off the wire.EvidenceLine.sourcesays where the line came from —"local"is the serving process’s own output (provisioning, lifecycle chatter),"remote"is the far end’s forwarded output — and it is a field, never an in-band[local]prefix a reader would have to parse back off the string. The two together areFailureRecord<Failure>, exported from.. - Evidence is a fixed structural type the package owns, not a third generic. Unlike
FailureandConn, evidence is not domain volatility: any transport that can fail can produce provenance-tagged output lines, and@kolu/surface-remote’sSessionState.logis structurally exactly this — so the producer passes its retained tail straight through, with no injection and no second evidence pipe. There is only one vocabulary to keep straight, and only one definition of it:@kolu/surface-remote’sLogEntryisEvidenceLineand itsLogEntrySchemaisEvidenceLineSchema, so “the two drifted” has no spelling rather than a pin that watches for it. Both live in the schema-only/evidenceleaf, so a browser-bundle-constrained consumer can hold the schema without pulling the group builder. The schema is a module const, not a function of a domain schema. - Evidence rides the failure record, and the
failedarm has noconnectionfield.floorOnLivenesstakes the live arms offconnectionentirely over a dead link while passing the failure record through, so evidence carried there survives the floor by construction with the reason it belongs to, andentry.connection?.logon a failed entry no longer compiles.[]is a real value with one meaning — the failure genuinely produced no output — minted only by the seam that knows; it is never a fallback for “we couldn’t see it”. Why the pairing is a shape rather than a convention is the explanation page’s job. - The framework fixes only the shape, not the fields.
failureis whatever the map’sfailureschema (WireSchema<Failure>) accepts — a bare string, a code-only object, or a discriminated union are all valid; the package fixes only thatfailureis a sibling field on thefailedarm (never a secondkind), validated by that schema. Thereasonfield is padi’s own convention (a human string, never parsed for control flow), not a framework guarantee — a domain whose schema carries noreasonis equally well-formed. EntryState<Failure, Conn>=ObservedEntryStatus<Failure, Conn> | { kind: "not-a-member" }, andObservedEntryStatus<Failure, Conn>=EntryStatus<Failure, Conn> | UnobservableEntry. Both live in the solid-free contract module, so a node consumer re-exports them type-only.EntryStatusis the published (wire) type and is unchanged;ObservedEntryStatusis what a CLIENT can honestly say, and it is the value type of the client’sentriescollection — every value that collection hands out has already been throughfloorOnLiveness.UnobservableEntry— the client-only arm, minted by the liveness floor.{ kind: "unobservable"; membershipId: MembershipId; published: "warming" | "connected" }. It means “our link to the publisher is dead”, and it exists so that a consumer which times an entry (a deadline, an escalation, a “failed to start” verdict) cannot mistake our own outage for a real, self-healing campaign — a conflation that certified a healthy daemon dead in kolu#2129. It is an arm rather than a flag onwarmingdeliberately: a flag can be ignored, an arm is a compile error at every.exhaustive(). It carries noclockOffsetand noconnection, so nothing green is paintable and no frozen live word can keep narrating;membershipIdrides through (the floor is about liveness, not identity). There is nofailedinhabitant ofpublished— a post-mortem does not go stale like a live claim, sofailedpasses the floor untouched. It has no wire schema: it is a projection of the consumer’s own transport, soentryStatusSchemahas no arm to encode it into and a floored value can never be republished.isSettling(state): boolean— the spin-only consumer’s one call, true forwarmingandunobservable. Exported from.so splitting the arm costs a spinner nothing; anything that puts a clock on an entry must still narrow the union itself.
MapRegistry<K> — the server seam
interface MapRegistry<K, Prov = "copying", Failure = unknown, Conn = unknown> {
members(): K[];
subscribe(onChange: () => void): () => void; // fires only AFTER members()/has() reflect the change
has(key: K): boolean;
resolve(key: K): // a kind-tagged sum, provably disjoint
| EntrySession<Prov, Failure, Conn> // { kind: "session", dispatch, state, connection? }
| EntryFault<Failure>; // { kind: "fault", failure }
}
- The registry is the one writer of membership;
entriesis that truth published; status is derived from each resolved session’s state. - A departure and a same-key re-add are separate, non-coalesced transitions. When a key leaves and its spelling returns, the registry must publish the removal and the re-add as two distinct membership events (the returning member carries a fresh
membershipId), never fold them into one coalesced upsert. A coalesced re-add would let a client-side adapter retain the departed member’s oldmembershipIdand, with it, the stale per-key subscription — defeating the{encodedKey, membershipId}rebuild.serveSurfaceMapdelivers them non-coalesced; a hand-rolledMapRegistrymust too. - A call carries its key in every frame: an unknown key is a typed rejection (unary —
MapKeyUnknown) or an immediate typed end (stream), because membership loss mid-stream is ordinary and only a one-shot call cannot end gracefully. A key that leaves membership mid-stream ends its subs before the session is destroyed — so no socket-error frame follows a typed end. - The map’s declared rejection vocabulary is
MapRejectionSchema— the union ofMapKeyNonCanonical,MapKeyUnknownandMapEntryFailed. The classes live in@kolu/surface/errors, not here: a map entry’s call crosses the same re-serving parent hop as every other surface error, so both ends must have been built from one schema for the_tagand its data to survive serialize → deserialize → re-serialize. Location is structure. The union is declared on every folded member, streaming ones included — a non-canonical wire key is a real rejection on any verb, and a stream whose error channel did not declare it would flatten it into a defect. - A folded member also declares the transport deaths its forward relays. The map raises none of them itself:
unaryHandler/forwardStreamhand the call straight tosession.dispatch, so whatever the entry’s own link fails with becomes that member’s failure —SurfaceStdioTransportClosedwhen the leg’s subprocess/socket is gone (the daemon behind the entry is respawning),SurfaceRelayTransportLostwhen a re-serve’s middle hop drops. Both are on every folded member’s error channel alongsideMapRejectionSchema(and alongside the entry’s own declared error when it has one). Undeclared, they were encoded against a union that does not contain them and reached the caller as an opaque string defect carrying only the parse prose — the exact flattening the declaration exists to kill, and the reason a caller could not tell a respawning daemon (not yet) from a terminal fault (never).SurfaceTransportRetiredis deliberately absent: it is the browser socket’s own 4001 retirement, raised by the consumer’s link, never carried up through a forward. resolvereturnsEntrySession({ kind: "session", dispatch, state, connection? }) orEntryFault({ kind: "fault", failure }, a schema-valid domain value) — a kind-tagged sum, so a consumer switches on the tag rather than on field presence.dispatchis the erasedSurfaceDispatchthe map forwards member calls to, by tag: whatever minted it — a wire link,directDispatch, a mirror — the map never learns.members()andhas()answer from one consistent snapshot. The connection state’sfailedarm requires a domainfailure— “failed with no failure” is unconstructible at the type, not caught by a runtime throw; the serving adapter (serveHostMap) fails loud at its own classification seam rather than falling back to a framework"other"cause.FailureRecord<Failure>({ failure, evidence }, exported from.) is the reason and its evidence as one named value.EntryConnectionState’sfailedarm is one; itsdisconnectedarm carries an optionalrefuse: FailureRecord— present is a standing refuse (publishes asfailed), absent is a transient drop (publishes aswarming). Because the pair is one value rather than two correlated fields, “a reason without its evidence” is unspellable at the shape level, andstate.refuse !== undefinednarrows it with no hand-written predicate.EntryFaultcarries no evidence. A fault has no session, hence no retained output tail — a structural fact of the shape, so the one seam that knows it (serveSurfaceMap’sstatusOf) states the[]once, exactly as it already supplies the fault’skindandmembershipId. A hand-rolled registry mints{ kind: "fault", failure }and nothing else.
The fold envelope
Every procedure folds as { mapKey, input } — one wire shape for any input.
- Field constants:
MAP_KEY_FIELD = "mapKey",INPUT_FIELD = "input". An entry input that itself carries amapKeyfield cannot collide with the folded key — it rides nested underinput. - Void-input rule. A void-input member carries no
inputfield at all — the envelope schema isSchema.Struct({ mapKey }), notSchema.Struct({ mapKey, input: Schema.Void }). Relying on JSON dropping anundefinedvalue and on the decoder accepting the missing key is fragile; omitting the field makes “void = no input key” the one representation on both encode and decode. - The envelope is a wrap, not a spread merge, and its decoded type is
{ mapKey: string, input: <decoded member input> }— the same decoded sideSurfaceDispatchcarries, so folding is a plain value wrap with no second encode/decode to keep in step. - Codec functions
fold(mapKey, input)/unfoldInput(wire)/unfoldKeyField(wire)reference these constants, so the envelope shape lives in exactly one place. Misroute-by-collision is unconstructible, not merely unlikely.
KeyCodec<K>
interface KeyCodec<K> { encode(key: K): string; decode(wire: string): K; }
Bridges Key to the canonical wire string every channel name, dedup key, and
membership entry is keyed on. For a plain-string key it is the identity pair;
kolu’s HostKey passes its own encode/decode. decode is paired with
a decode through keySchema and need not validate on its own; encode is a bare cheap call
on the hottest paths (per-key cache lookup, membership fold, per-tick republish).
The server re-derives and re-validates real K from the wire string via
codec.decode then a keySchema decode.
The connected SurfaceMapClient re-exposes this codec as its codec member —
the one key-identity authority on the client. scopedByEntry and any consumer
keying its own per-entry structure off membership fold a key through it rather than
trusting === reference identity, which the client cannot guarantee across
independent decodes of the same logical key.
// `codec` — the ONE key-identity authority: the canonical wire string every
// channel name, dedup key, and membership entry is keyed on. `scopedByEntry`
// folds each key through it rather than trusting `===` reference identity.
const wire: string = app.codec.encode("web-01"); // K → wire string
const key: string = app.codec.decode(wire); // wire string → K
Typed sub ends and the clock seam
entry(key).proceduresbinds the entry surface’s declared procedures, typed from the spec (entry.procedures.<ns>.<verb>(input)); the key-injecting dispatch folds{ mapKey }into every call and rewrites the tag onto the map’s prefix. A narrow mapped type — no cast, and it sidesteps the “union too complex” a second precise mapped type over a generic entry spec trips. An absent-key call is a typed rejection (MapKeyUnknown), the one-shot twin of a sub’s typed stream-end.entry(key).rpcstays typedunknownper leaf — the structural addressing face only; the map folds no reservedsystem/*member, so reaching one through it fails at the map hop by construction.entry(key).clock.toLocal(remoteMs)reprojects a host-stamped timestamp into the local clock via the entry’s measured offset:remoteMs − offset. It returnsnull(never a silent identity) whenever the entry has no measured offset —warming/failed/unobservable/not-a-member, AND aconnectedentry whoseclockOffsetis stillnull(link live, offset not yet probed — readiness is decoupled from the offset); thenumber | nulltype forces the caller to render a pending ”—” rather than fall back to the raw remote value. It reads only.kind, never.failure, and folds the membership collection so it re-answers as the entry connects.
For why the wire is shaped this way, see Entry contracts; to serve a map, see How to serve a map.