mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
The pure swap is gone: a reload restarts every live object of a changed plugin (documents and tables kept). Calls running at the commit and every call they cause (the ctx call chain; an old flag on call from trusted links) run on the open old attempt, which seals once its work drained and its successor's recorded requests are ready, bounded by reloadDrainTimeout (5 s); then the successor's factory runs. New work waits at the gate; same-host consumers see the draining object live; a reload cycle fails at once with unavailable/reload_cycle. Example 64, design section 21 rewritten. |
||
|---|---|---|
| .. | ||
| binary.ts | ||
| channel.ts | ||
| errors.ts | ||
| index.ts | ||
| internal.ts | ||
| ipc.ts | ||
| link.ts | ||
| message.ts | ||
| part.ts | ||
| README.md | ||
| text.ts | ||
| values.ts | ||
| websocket.ts | ||
link
Links between hosts and the protocol on them (§3, §22, slices 3a 2b, 4a and 4b): one Message type checked after
every decode (skipped on trusted links), the codecs that turn it into bytes, opaque parts with their limits and
compression at the origin, the handshake, the heartbeat, in-order handling of messages waiting for an asynchronous
inflate, the IPC and WebSocket transports, values with raw byte segments, and errors on the wire.
For apps (index.ts)
import { memoryLinkPair, textCodec } from "./index.ts";
const [up, down] = memoryLinkPair();
server.accept(up, { principal: { kind: "guest", session: "S1" } }, ctx); // the entry point authenticated it
client.connect(down, { principal: { kind: "server" } }, ctx); // both ends: binaryCodec by default
// For a readable wire while debugging: { principal, codec: textCodec } on both ends.
// A forked worker over IPC (the parent end; the child uses ipcLink(process)), trusted: no message schema check.
const child = fork("worker.ts", [], { serialization: "advanced" });
server.accept(ipcLink(child), { principal: { kind: "worker" }, trusted: true }, ctx);
// A browser tab over a WebSocket (`ws` on the server, the browser's WebSocket in the tab).
wss.on("connection", (ws, req) => server.accept(webSocketLink(ws), { principal: authenticate(req) }, ctx));
tab.connect(webSocketLink(new WebSocket(url)), { principal: { kind: "server" } }, ctx);
Link: an ordered, bidirectional channel ofUint8Arraymessages:send(message)(a no-op once closed),onMessage(listener)(messages that arrived earlier are delivered to it first; not kept on a closed link),close()(both ends; undelivered messages are dropped),onClose(listener)(runs at once on a closed link). Adapters only move whole messages, in order, and close both ends.memoryLinkPair(): two ends in one process; each message arrives a microtask later (queueMicrotask), in order. Closing one end closes it at once and the other a microtask later.ipcLink(port): over a Node IPC channel, aChildProcessforked withserialization: "advanced"orprocessin the child (IpcPort, typed structurally: Chord imports nothing from Node). Each message is oneUint8Array; other messages on the channel (the app's own, such as a start message) are skipped, and messages that arrive before the link's listener is set are queued, so a child makes its link before awaiting its start message.sendreturningfalseis backpressure, not a failure; a thrownsend, an error in its callback ordisconnectcloses the link once and removes its listeners.webSocketLink(socket): over a WebSocket, the browser's orws's (WebSocketLike, typed structurally): binary frames,binaryType = "arraybuffer"; messages sent while it connects wait foropen; a socket closing or closed already gives a closed link;close,errorand a text frame close the link once. The transport'smaxPayloadis its hard cap (a larger frame closes the socket, code 1009); the limits per part are the host's (host/).Compression({ deflate(bytes), inflate(bytes, limit) }, synchronous): whatcreateHost({ compression })takes.deflateis raw deflate at level 1;inflategives a taggedInflated:{ kind: "bytes", bytes }(at mostlimitbytes;Infinity: none; 0 allowed),{ kind: "too_large" }when the output would be larger,{ kind: "corrupt", error }for data that is no raw deflate stream. Chord shipsnodeCompression(zlib'sdeflateRawSync/inflateRawSyncwithmaxOutputLength) insrc/node/compression.ts, outside the portable code.PartLimits({ part?, decoded? }, bytes) is what a link'slimitstake.Codec { name, encode(message), decode(bytes) }, chosen per link (codecinaccept/connectand a route'sresolveresult, the same at both ends; no negotiation). Two links of one relay may use different codecs:binaryCodec(the default): a kind byte (its high bit set when anoriginfollows as a varint), then the kind's fields: unsigned varints (ids, indexes, counts, lengths), length-prefixed UTF-8 strings, the incarnation as 16 and the fingerprint as 8 raw bytes, parts as length-prefixed bytes (decoded as views of the received message, never parsed), other JSON fields (a hold's address params and principal, errors) as length-prefixed JSON; a hold's optional fields behind one flags byte; a payload marked as carrying bytes adds its segments (count, lengths, raw bytes);failed'sretryInas one more than it (0: absent). A part's length prefix is its length doubled, plus one when the part is deflated. A hold'schainas a count and 16 raw bytes per instance id, itslimitas a varint. No outer length.textCodec, for debugging: one JSON tuple per message with a short kind tag and positional fields (["R", hold],["c", call, hold, method, args?],["b", [[hold, variant, [[member, value]…], [[member, error]…]?]…]],["s", [[hold, [[member, ops]…], [[member, error]…]?]…]],["f", hold, "t" | "b", error, retryIn?],["T", hold, hops]forretry,["l", hold]forloading,["a"]ack,["C"]closed,["p"]ping,["P"]pong, …; a message with an origin wrapped as["@", origin, tuple]); parts are spliced into the tuple as their JSON text and re-serialized at decode (one parse per part per hop, accepted for a debugging codec); a payload marked as carrying bytes ends with its segments' lengths, the segments following the tuple after a newline (JSON text has no raw one). Every part is tagged with its codec, so no JSON value is reserved:[0, json], or[1, base64]for a part its origin deflated (the text codec itself never compresses);["V", revocation]isrevoke.describeMessage(message): the text tuple form of any message, for logs and tests.
accept,connectand a route'sresolvetaketrusted(defaultfalse): a trusted link skips theMESSAGEschema check (framing, the codec's decode and the owners' checks still run). For links to the app's own processes; trust never grants a hold's principal fields (they are read only on a parent link,remote/).
For Chord's layers (internal.ts)
Message(message.ts): the protocol's one tagged union, from the schemaMESSAGE:hello { protocol, incarnation, calls }(calls: how many concurrent calls this side accepts per origin, its call window),hold { hold, type, version, fingerprint, params, address, principal?, incarnation? }(address:{ kind, params }the caller'shostAddresscomputed, it routes the hold; the provider places it by its params;principalandincarnation: whom it acts for, read only on a parent link;chain?: the instance ids of the activations waiting for the hold, at most 32, read only on a trusted link;limit?: the decoded limit per part of the link a relay admitted it on, read only on a parent link),release,call { call, hold, method, args?, segments?, old? }(old: true: old work of a reload on the caller's host, honoured only from a trusted link and dropped by a relay from one that is not; binary:0x20in the call's kind byte, text: the seventh element, after the trace carrier),abort,bound { holds: [{ hold, variant, members: [{ member, value }], failed }] }(one message may bind several holds: first bindings, re-holds),state { holds: [{ hold, members: [{ member, ops }], failed }] },ack {}(one per appliedboundorstateof its origin),closed { origin }(a relay's downstream origin ended: its holds and calls go),ping {},pong {},revoke { revocation }(from the parent, applied with the child's ownrevokedBy; no origin),failed { hold, cause, retryIn? }(retryIn: the milliseconds left of the provider's backoff window, absent for a broken failure),loading { hold }(the hold's object, or a relay's upstream, starts over),retry { hold, hops }(the consumer asks for an attempt now;hops, 1 to 16, is its budget: the first host sends 16, the host there passes it on to its remote dependencies with one less, never at 0; relays forward it as it is),result { call, value?, segments? },error,gone.error,goneand afailedcause carry the wire error format (WireError: a ChordError's JSON, or a plugin's code and message).argsabsent is a method without arguments,valueabsent isnull;segmentspresent marks a payload with byte segments (its method's schema has abytes()leaf,Snapshot.bytes).member,methodandvariantare indexes into the hold's bound version's names, each variant's sorted (variantNames,variantOrderin contracts). Every kind buthello,revoke,pingandpongmay carryorigin(an integer from 1; absent: the sender's own work): a relay's number for one of its downstream origins, keying holds, calls, windows and acks with the ids (OriginMessage; decision "Relay flow control per origin").PROTOCOL_VERSIONis 1.Part = string | Uint8Array | DeflatedPart(params,args,value,boundmember values,statemember ops,part.ts): a value's JSON text at its origin (and after a text decode), its UTF-8 bytes once a binary codec received it, or those bytes deflated by an origin that compresses. A part is opaque on the way and opened and parsed only where it is owned, while its message is handled (a malformed part is still a protocol error that closes the link): the provider opensparamsandargs, the consumer results, snapshots and ops. A relay forwards parts unparsed and still deflated; a message naming an unknown id is ignored before its parts are parsed (Q1).- Parts and their limits (
part.ts):originPart(json, compression, ctx)(a JSON part at its origin, deflated above 16 KiB of UTF-8,COMPRESS_ABOVE, when the host compresses and only if it got smaller, else the bytes encoded for the attempt; countedchord.link.deflates),oversized(part, limit)(thetoo_large/parterror of a part past a wire limit: one comparison),openPart(part, limits, compression, ctx)(the JSON its owner parses: the wire size checked, then the decoded size, a deflated part inflated within it; countedchord.link.inflates),inflateAsync(part, limit, signal, ctx)(theDecompressionStreamfallback, counting output bytes and stopping past the limit; it leaves its taggedInflatedon the part foropenPart;signal, the link's close, cancels it at once),linkLimits(trusted, limits?)(none on trusted links, 16 MiB on the wire and 64 MiB decoded otherwise). Sizes are UTF-8 bytes (a text part's counted without encoding it); byte segments are not parts and no part limit bounds them (the transport's cap does). Channel: a host's end of a link. It sendshello { protocol, incarnation, calls }first and queues its own messages until the peer's arrived. Every received message is decoded by the codec and, unless the link is trusted, checked withcheckMessage(the compiledMESSAGEschema, counted inchord.link.checks); a first message that is nohelloof this codec ("the first message is no binary message"), another protocol version, a secondhello, a failed check, or a throw while itsChannelHandlerhandles a message, is a protocol error: reported (protocol_error), and the link closes.peeris the peer's incarnation,peerCallsits call window. A handler that returns a promise (a message waiting for an asynchronous inflate) holds every later message,pingincluded, until it settles; they are then handled in order, and dropped when the link closes meanwhile; a rejection is a protocol error.- The heartbeat, in
Channel: every received message stampslastReceived(sends do not count); the host's one interval timer callstick(now, paused, ping, close)on every link: a link silent forclosems (30 s by default), the handshake included, closes; an open one silent forpingms (15 s) sends oneping.pausedis the time this process did not run before a tick at least one interval late (a sleep, a blocked event loop): it is no silence, the silence stays what it was before, at leastping, so an open link pings at once. Apingis answered withpongat once, never queued behind the window; neither reaches the handler. It detects a silent transport, not a peer that answers pings but withholds acks (the window and the pending cap bound that). - Who checks what (layers 1-3 failing close the link):
- the codec's
decode: lengths and varints fit, the bytes consumed exactly, a known kind and arity, strict UTF-8, each JSON value parses, segment references valid and only in payloads marked as carrying bytes; checkMessage: the envelope (kinds, ids and indexes, lists, error shapes), not the parts; skipped on a trusted link;- the remote side (
remote/): ids not reused while live (unknown ids ignored), indexes within the bound version,boundbeforestate, at most N concurrent calls; - owners, where a part is owned:
checkParams,checkArgs, a route's address check, Delta's decoder andapplyImmutablebefore adoption,chordErrorFromJson; snapshots and results are never validated.
- the codec's
- Parts (
values.ts):jsonPart(value)(a value's JSON text),payloadPart(value, bytes)(a payload's JSON, with segments and{"$bytes": i}references when marked: an own key$bytesthere failsinvalid/args; elsewhere it is plain data),parsePart(part)andparsePayload(part, segments)(strict UTF-8 JSON; references read back as views of the message, no copy). - Errors:
wireError(thrown)is{ kind: "chord", error }for a ChordError,{ kind: "plugin", code, message, details? }for a PluginError, and{ kind: "plugin", code: "internal", message }for anything else; never a stack or a cause.errorFromWirerebuilds a ChordError (L1's envelope check; a malformed one throws, a protocol error) or a PluginError. Failure causes andgonecarry ChordError JSON directly.