这个包是什么What this package ispi-protocol 是远程 pi 会话的线上格式,且只有格式:协议版本 8 定义了 3 种客户端消息和 5 种服务端消息、把调用围在一台逻辑服务器 / 一个 Session / 一个实时附着里的路由、一个严格的 CBOR 子集,以及“4 字节大端长度 + 一个 CBOR 项”的分帧。它不懂任何服务契约:call、result、update 只被检查为严格 JSON,其语义归 chord,数据归应用。pi-client 与 pi-server 是它的两端。pi-protocol is the wire format for remote pi sessions, and only the format: protocol version 8 defines 3 client messages and 5 server messages, routes that fence a call to one logical server / one Session / one live attachment, a strict CBOR subset, and framing as “a 4-byte big-endian length + one CBOR item”. It knows no service contract: call, result and update are only checked to be strict JSON, their meaning belongs to chord and the data to the application. pi-client and pi-server are its two ends.
869 行 TypeScript,运行时无关(不依赖 Node API),一个导入路径。依赖 chord(JsonValue / isJsonValue)与 typebox(schema)。被 pi-client、pi-server 以及 coding-agent 的实验性 server / client 运行时使用。README 明确写着:实验性,不保证兼容。869 lines of TypeScript, runtime-neutral (no Node APIs), one import path. Depends on chord (JsonValue / isJsonValue) and typebox (schemas). Used by pi-client, pi-server and coding-agent's experimental server / client runtime. The README is explicit: experimental, no compatibility guarantees.
三层:信封、编解码、分帧Three layers: envelopes, codec, framing#
一句话结论Takeawayprotocol.ts 用 TypeBox 声明信封 schema;codec.ts 把“校验 → CBOR → 分帧”串成 encodeClientMessage() / encodeServerMessage(),反向是增量的 ClientMessageDecoder / ServerMessageDecoder;framing.ts 只管长度前缀;cbor/ 是一个手写的、只支持定长项的 CBOR 编解码器。index.ts 把这些全部从一个导入路径导出。protocol.ts declares the envelope schemas with TypeBox; codec.ts chains “validate → CBOR → frame” into encodeClientMessage() / encodeServerMessage(), and the incremental ClientMessageDecoder / ServerMessageDecoder run it backwards; framing.ts only handles the length prefix; cbor/ is a hand-written CBOR codec for definite-length items only. index.ts exports all of it from one import path.
要点Key points
- 没有任何 Node 专用 API:同一份代码可以用在浏览器、Bun 或 worker 里;传输(Unix socket、WebSocket…)由 client / server 提供。No Node-only APIs: the same code works in a browser, Bun or a worker; transports (Unix sockets, WebSockets…) come from client / server.
- schema 是
additionalProperties: false的 StrictObject,所以多一个字段也会被拒绝。Schemas are StrictObjects withadditionalProperties: false, so even one extra field is rejected. - coding-agent 把 protocol、client、server 三个包都只列为 devDependencies,供
experimental/下的 server 与 client 运行时使用。coding-agent lists protocol, client and server only as devDependencies, for the server and client runtime underexperimental/.
工作原理How it works
- 发送:
transport.send(encodeClientMessage({ type: "hello", version: PROTOCOL_VERSION }))。Send:transport.send(encodeClientMessage({ type: "hello", version: PROTOCOL_VERSION })). - 接收:
const decoder = new ServerMessageDecoder({ maxFrameLength }),然后for (const m of decoder.push(chunk)) …,流结束时decoder.end()。Receive:const decoder = new ServerMessageDecoder({ maxFrameLength }), thenfor (const m of decoder.push(chunk)) …, anddecoder.end()when the stream ends. - 两端的
maxFrameLength要一致(默认 16 MiB)。Both peers must use the samemaxFrameLength(16 MiB by default).
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/protocol/src/index.tsL2 | export * | 唯一入口the only entry |
| packages/protocol/src/protocol.tsL5 | PROTOCOL_VERSION | 协议版本protocol version |
| packages/protocol/src/codec.tsL56 | encodeClientMessage() | 校验并编码一帧validate and encode one frame |
| packages/protocol/src/framing.tsL44 | FrameDecoder | 拆帧frame splitting |
| packages/protocol/src/cbor/encoder.tsL211 | encodeCbor() | CBOR 编码CBOR encoding |
公共 APIPublic API#
一句话结论Takeaway只有 1 个导入路径。index.ts 通过 export * 与一个具名列表公开了 6 个模块里的 20 个值和 17 个类型:编码 / 解码函数与类、分帧、CBOR,以及从 protocol.ts 选出的信封类型(schema 本身不导出)。Just 1 import path. Through export * and one named list, index.ts makes 20 values and 17 types public from 6 modules: the encode / decode functions and classes, framing, CBOR, and a selection of envelope types from protocol.ts (the schemas themselves are not exported).
| 导入路径Import path | 目标Target | 说明Notes |
|---|---|---|
@earendil-works/pi-protocol | ./dist/index.js | 信封类型、校验与编码、增量解码、分帧与 CBORenvelope types, validation and encoding, incremental decoding, framing and CBOR |
常用导出Key exports
| 模块Module | 常用导出Key exports | 用途Use |
|---|---|---|
| packages/protocol/src/codec.ts | encodeClientMessage encodeServerMessage | 校验并编码一整帧validate and encode one whole frame |
| packages/protocol/src/codec.ts | ClientMessageDecoder ServerMessageDecoder | 增量解码并校验decode and validate incrementally |
| packages/protocol/src/codec.ts | parseClientMessage parseServerMessage ProtocolValidationError | 只校验,以及唯一的错误类型validation only, and the one error type |
| packages/protocol/src/protocol.ts | PROTOCOL_VERSION isServerId | 协议版本 8 与 serverId 检查protocol version 8 and the serverId check |
| packages/protocol/src/framing.ts | encodeFrame FrameDecoder DEFAULT_MAX_FRAME_LENGTH | 只用分帧framing on its own |
| packages/protocol/src/cbor/encoder.ts | encodeCbor decodeCbor CborError | 只用 CBORCBOR on its own |
根入口的全部导出(按声明所在模块,逐行链接)Every root export, by declaring module (line-linked)
由构建脚本展开 index.ts 的 export * 与 export { … } from 并扫描声明生成;每个符号链接到 v1.1.0 中的定义行。Generated at build time by expanding the export * and export { … } from statements in index.ts and scanning the declarations; each symbol links to its definition at v1.1.0.
一条消息的往返One message, there and back#
一句话结论Takeaway编码先校验:parseClientMessage() 用 TypeBox Check 检查 schema,再用 chord 的 isJsonValue 递归检查严格 JSON;然后 encodeCbor()(上限 = maxFrameLength)和 encodeFrame()。接收方把任意切分的字节交给 push():FrameDecoder 产出完整负载,decodeCbor() 解出恰好一个项,同一套 schema 再检查一次。Encoding validates first: parseClientMessage() checks the schema with TypeBox Check, then strict JSON recursively with chord's isJsonValue; then encodeCbor() (limit = maxFrameLength) and encodeFrame(). The receiver hands arbitrarily split bytes to push(): FrameDecoder yields complete payloads, decodeCbor() decodes exactly one item, and the same schemas are checked again.
要点Key points
- 任何失败都变成
ProtocolValidationError:CborError 与 FrameError 被包装,错误消息截断到 500 个字符。Every failure becomes aProtocolValidationError: CborError and FrameError are wrapped and the message is cut to 500 characters. - 解码器失败后进入 failed:之后的
push()和end()都直接抛错,没有“跳过坏帧”这回事——调用方应该断开连接。A failed decoder stays failed: every laterpush()andend()throws; there is no “skip the bad frame” — the caller should drop the connection. - 被拒绝的非 JSON 值:
Uint8Array、非有限数、undefined、带原型的对象和环。Rejected non-JSON values:Uint8Array, non-finite numbers,undefined, objects with prototypes, and cycles.
工作原理How it works
encodeProtocolMessage(value, parse, kind):先parse(value),再encodeFrame(encodeCbor(validated, { maxByteLength }))。encodeProtocolMessage(value, parse, kind):parse(value)first, thenencodeFrame(encodeCbor(validated, { maxByteLength })).ValidatedMessageDecoder.push(chunk):对每个完整帧parse(decodeCbor(frame)),返回 0..n 条消息。ValidatedMessageDecoder.push(chunk):parse(decodeCbor(frame))for every complete frame, returning 0..n messages.- 流结束时调用
end():还有半帧就报 "Truncated frame at end of stream"。Callend()when the stream ends: a partial frame reports "Truncated frame at end of stream".
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/protocol/src/codec.tsL20 | parseClientMessage() | schema + 严格 JSONschema + strict JSON |
| packages/protocol/src/codec.tsL39 | encodeProtocolMessage() | 编码流水线the encode pipeline |
| packages/protocol/src/codec.tsL65 | ValidatedMessageDecoder | 增量解码incremental decoding |
| packages/protocol/src/codec.tsL34 | boundedErrorMessage() | 500 字符500 characters |
| packages/protocol/src/codec.tsL13 | ProtocolValidationError | 唯一的错误类型the one error type |
8 种信封The 8 envelopes#
一句话结论Takeaway客户端发 hello(必须是第一帧)、request、cancel;服务端发 hello(带 serverId)、hello_error(终止)、response(ok: true 带可选 result,或 ok: false 带 error)、service_update(某个订阅的更新)和 attachment(带外的路由变化)。错误是 { code, message },code 是非空字符串,对协议不透明。The client sends hello (must be the first frame), request and cancel; the server sends hello (with serverId), hello_error (terminal), response (ok: true with an optional result, or ok: false with error), service_update (an update for one subscription) and attachment (an out-of-band route change). Errors are { code, message }, the code a non-empty string that is opaque to the protocol.
要点Key points
- 客户端
hello的version是任意非负整数,服务端的则是字面量 8:版本不符由服务端用hello_error { code: "version" }回答。The client'shello.versionis any non-negative integer, the server's the literal 8: a mismatch is answered by the server withhello_error { code: "version" }. - 请求 id 是客户端选的非空字符串(pi-client 用
request-1、request-2…),回应用同一个 id 关联。Request ids are non-empty strings chosen by the client (pi-client usesrequest-1,request-2…); responses correlate by the same id. service_update只按subscriptionId寻址,不带 target:订阅已经属于某个连接和附着。service_updateis addressed bysubscriptionIdonly, without a target: the subscription already belongs to a connection and attachment.
工作原理How it works
- 所有消息都是
Type.Union:ClientMessageSchema有 3 个成员,ServerMessageSchema有 5 个(ResponseEnvelope自身又是 ok / not-ok 的联合)。All messages are aType.Union:ClientMessageSchemahas 3 members andServerMessageSchema5 (ResponseEnvelopeis itself an ok / not-ok union). - 类型从 schema 推导:
type ClientMessage = Static<typeof ClientMessageSchema>。Types are derived from the schemas:type ClientMessage = Static<typeof ClientMessageSchema>. - void 回应:
{ type: "response", id, ok: true },没有result字段。A void response:{ type: "response", id, ok: true }, without aresultfield.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/protocol/src/protocol.tsL29 | ClientHelloSchema | 客户端 helloclient hello |
| packages/protocol/src/protocol.tsL49 | RequestEnvelopeSchema | 请求request |
| packages/protocol/src/protocol.tsL74 | ResponseEnvelopeSchema | 回应response |
| packages/protocol/src/protocol.tsL88 | ServiceEventEnvelopeSchema | 订阅更新subscription update |
| packages/protocol/src/protocol.tsL98 | ServerMessageSchema | 5 种服务端消息5 server messages |
路由:serverId、sessionId、attachmentIdRoutes: serverId, sessionId, attachmentId#
一句话结论Takeaway每个 request 与 cancel 都带一个 RpcTarget:服务器级调用是 { serverId },Session 调用是 { serverId, sessionId, attachmentId }。serverId 是规范的小写 UUIDv4,是逻辑身份而不是 socket 地址;attachmentId 由服务器在每次成功 attach 时生成,只通过带外的 attachment 消息告诉客户端。三者合起来防止跨服务器、跨 Session 和过期的调用。Every request and cancel carries an RpcTarget: server-wide calls use { serverId }, Session calls { serverId, sessionId, attachmentId }. serverId is a canonical lowercase UUIDv4, a logical identity rather than a socket address; attachmentId is generated by the server on every successful attach and reaches the client only through the out-of-band attachment message. Together they prevent cross-server, cross-Session and stale calls.
要点Key points
- 管理服务的
attach()/detach()不返回任何路由 id:服务器装好路由后才发attachment,解除时发attachment: null。Managementattach()/detach()return no route ids: the server sendsattachmentonce the route is installed, andattachment: nullwhen it is cleared. - 切换或重新附着后,带旧
attachmentId的延迟帧被拒绝(session_not_attached)。After switching or reattaching, delayed frames with an oldattachmentIdare rejected (session_not_attached). - 一个 Session 可以同时被多个 presentation 附着;同一连接重复 attach 同一个 Session 是幂等的。A Session can have several presentation attachments at once; repeating attach for the same Session on one connection is idempotent.
isServerId()是导出的校验函数;client 与 server 的构造函数都用它拒绝非规范的 id。isServerId()is the exported check; the client and server constructors both use it to reject non-canonical ids.
工作原理How it works
- 服务器级:
client.request({ serverId: client.hello.serverId }, call)。Server-wide:client.request({ serverId: client.hello.serverId }, call). - Session:
client.request(client.attachment, call)——attachment 是最近一次带外消息给出的完整路由。Session:client.request(client.attachment, call)— the attachment is the full route from the latest out-of-band message. - 服务端先比较
target.serverId(wrong_server),再在 SessionRouter 里比较 sessionId 与 attachmentId。The server comparestarget.serverIdfirst (wrong_server), then sessionId and attachmentId in the SessionRouter.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/protocol/src/protocol.tsL12 | ServerIdSchema | UUIDv4 正则the UUIDv4 pattern |
| packages/protocol/src/protocol.tsL40 | SessionTargetSchema | Session 路由Session route |
| packages/protocol/src/protocol.tsL94 | AttachmentEnvelopeSchema | 带外路由变化out-of-band route change |
| packages/server/src/session-router.tsL168 | attachment.id | 服务器生成 attachmentIdthe server generates attachmentId |
| packages/server/src/session-router.tsL224 | requireAttachment() | 检查路由checking the route |
一条连接上的消息顺序Message order on one connection#
一句话结论Takeaway客户端先发 hello { version: 8 };服务端检查版本、为这个连接准备服务器级服务,然后回 hello { version, serverId }——或者 hello_error 并关闭。之后客户端发 request / cancel,服务端发 response、service_update 和 attachment。订阅的快照在回应里,在快照之前产生的更新在服务端排队,回应之后才发出。The client sends hello { version: 8 } first; the server checks the version, prepares the server-scoped services for this connection, then answers hello { version, serverId } — or hello_error and closes. After that the client sends request / cancel and the server sends response, service_update and attachment. A subscription's snapshot is in its response; updates produced before it queue on the server and go out after the response.
要点Key points
- 服务端握手超时默认 5 秒;第一条消息不是 hello、或之后再发 hello,都会以
hello_error { code: "invalid_request" }结束连接。The server's handshake timeout is 5 seconds by default; a first message that is not hello, or a later hello, ends the connection withhello_error { code: "invalid_request" }. - 客户端检查服务端报告的
serverId是否就是它期望的那台逻辑服务器,不符就断开。The client checks that the reportedserverIdis the logical server it expects, and disconnects otherwise. - 握手期间到达的请求不会丢:服务端在握手完成后再处理它们。Requests arriving during the handshake are not lost: the server handles them once the handshake completes.
cancel只取消同一连接上、同一 id 且同一 target 的活动请求;被取消的请求以cancelled回应。cancelonly cancels an active request with the same id and the same target on the same connection; the cancelled request answerscancelled.
工作原理How it works
Server.dispatchMessage():awaitingHello → handshaking → ready;ready 之后按类型调用handleRequest或handleCancel。Server.dispatchMessage(): awaitingHello → handshaking → ready; after that it callshandleRequestorhandleCancelby type.Connection.#handleMessage()(客户端):连接中只接受 hello / hello_error;连上后 hello 类消息都是协议错误。Connection.#handleMessage()(client): while connecting only hello / hello_error are accepted; once connected, any hello-type message is a protocol error.- attach 的顺序:路由安装 → 带外
attachment→ attach 调用的response。Attach ordering: route installed → out-of-bandattachment→ the attach call'sresponse.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/server/src/server.tsL42 | DEFAULT_HANDSHAKE_TIMEOUT_MS | 5 秒5 seconds |
| packages/server/src/server.tsL262 | finishHandshake() | 版本检查与服务器 helloversion check and server hello |
| packages/server/src/server.tsL298 | handleCancel() | 取消cancellation |
| packages/client/src/connection.tsL163 | #handleMessage() | 客户端握手client handshake |
| packages/protocol/src/codec.tsL139 | isSupportedProtocolVersion() | 版本判断version check |
分帧与 FrameDecoderFraming and FrameDecoder#
一句话结论Takeaway一帧 = 4 字节无符号大端长度 + 恰好一个定长 CBOR 项。FrameDecoder.push(chunk) 接受任意切分与合并:它先凑齐 4 字节头,长度超过 maxFrameLength 时立刻失败(不等负载到达),长度为 0 时产出空帧,否则按 64 KiB 的块收集负载,收满后返回。状态只有 open、ended、failed 三个。A frame = a 4-byte unsigned big-endian length + exactly one definite-length CBOR item. FrameDecoder.push(chunk) accepts any splitting and coalescing: it fills the 4-byte header, fails at once if the length exceeds maxFrameLength (without waiting for the payload), yields an empty frame for length 0, and otherwise collects the payload in 64 KiB blocks and returns it when complete. There are only three states: open, ended and failed.
要点Key points
- 负载总是被复制:返回的帧不引用调用方传入的 chunk,调用方可以复用自己的缓冲区。Payloads are always copied: returned frames never alias the caller's chunks, so callers may reuse their buffers.
- 按 64 KiB 分块分配,所以一个声明很大但迟迟不到的帧不会一次性占用全部内存。Allocation is in 64 KiB blocks, so a frame that declares a large size but arrives slowly does not reserve all of its memory up front.
maxFrameLength可以是 0 到 0xffffffff;客户端与服务端还要求它 ≥ 1。maxFrameLengthmay be 0 to 0xffffffff; the client and server additionally require at least 1.
工作原理How it works
encodeFrame(payload):分配 4 + n 字节,写长度,复制负载。encodeFrame(payload): allocate 4 + n bytes, write the length, copy the payload.push()的外层循环:头还没齐就继续读头;齐了就检查长度并开始收集负载;负载齐了就产出一帧并重置。push()'s outer loop: keep reading the header until complete; then check the length and start collecting; when the payload is complete, emit a frame and reset.end():头或负载只读了一部分 → failed + FrameError。end(): a partially read header or payload → failed + FrameError.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/protocol/src/framing.tsL3 | PAYLOAD_BLOCK_SIZE | 64 KiB 块64 KiB blocks |
| packages/protocol/src/framing.tsL6 | DEFAULT_MAX_FRAME_LENGTH | 16 MiB16 MiB |
| packages/protocol/src/framing.tsL28 | encodeFrame() | 加长度前缀add the length prefix |
| packages/protocol/src/framing.tsL59 | push() | 增量拆帧incremental splitting |
| packages/protocol/src/framing.tsL132 | end() | 截断检查truncation check |
严格的 CBOR 子集A strict CBOR subset#
一句话结论Takeawaycbor/ 实现 RFC 8949 的一个严格子集:主类型 0–5 与 7 中的 false、true、null 和 float64;tag(主类型 6)、不定长项、半精度与单精度浮点一律拒绝。映射的键必须是字符串且不重复。默认上限:16 MiB、1,000,000 个元素、64 层嵌套(可配置到 512 层),长度与深度在遍历之前就检查。cbor/ implements a strict subset of RFC 8949: major types 0–5 plus false, true, null and float64 from type 7; tags (major type 6), indefinite-length items and half / single floats are always rejected. Map keys must be unique strings. Default limits: 16 MiB, 1,000,000 elements and 64 nesting levels (configurable up to 512), with lengths and depth checked before traversal.
要点Key points
- 编码器为整数选最短的参数;非整数与
-0写成 float64(0xfb);非有限数、非安全整数直接报错。The encoder picks the shortest argument for integers; non-integers and-0are written as float64 (0xfb); non-finite numbers and unsafe integers are errors. - 文本按 fatal UTF-8 解码并保留开头的 BOM;编码时孤立代理项会被检测并拒绝。Text is decoded as fatal UTF-8 and keeps a leading BOM; lone surrogates are detected and rejected on encode.
- 解码出的对象用
defineProperty设置属性,所以__proto__只是普通的数据键。Decoded objects set properties withdefineProperty, so__proto__is just a data key. - 字节串(主类型 2)在 CBOR 层是合法的,但信封层的严格 JSON 检查会拒绝它。Byte strings (major type 2) are legal at the CBOR layer but rejected by the envelope's strict-JSON check.
工作原理How it works
encodeCbor(value, { maxByteLength, maxContainerLength, maxDepth })→Uint8Array。encodeCbor(value, { maxByteLength, maxContainerLength, maxDepth })→Uint8Array.decodeCbor(bytes, options)→ 恰好一个值;尾随数据是错误。decodeCbor(bytes, options)→ exactly one value; trailing data is an error.hello的编码是 25 字节:4 字节长度 + 21 字节 CBOR(见图中的例子)。helloencodes to 25 bytes: a 4-byte length + 21 bytes of CBOR (see the example in the diagram).
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/protocol/src/cbor/options.tsL6 | DEFAULT_MAX_CBOR_* | 默认上限default limits |
| packages/protocol/src/cbor/decoder.tsL26 | readItem() | 按主类型解码decoding by major type |
| packages/protocol/src/cbor/decoder.tsL112 | readLength() | 拒绝不定长indefinite lengths refused |
| packages/protocol/src/cbor/encoder.tsL83 | writeArgument() | 最短参数shortest argument |
| packages/protocol/src/cbor/encoder.tsL119 | encodeValue() | 按值类型编码encoding by value type |
protocol、chord 与应用的分界Where protocol, chord and the application meet#
一句话结论Takeawayprotocol 只拥有信封:类型、id、路由和“负载是严格 JSON”这条规则。call 的语法 { serviceId, instance?, member, args }、$chord.service 控制词汇(目录、订阅、退订)、订阅快照与更新、服务错误码以及复制状态的 Delta 编解码都属于 chord;Session 目录、管理结果、transcript、模型、插件这些值属于应用。The protocol owns only the envelope: types, ids, routes and the rule “payloads are strict JSON”. The call grammar { serviceId, instance?, member, args }, the $chord.service control vocabulary (catalogue, subscribe, unsubscribe), subscription snapshots and updates, service error codes and the Delta codecs for replicated state belong to chord; the Session directory, management results, transcripts, models and plugins belong to the application.
要点Key points
- 因此服务端可以把 Session 调用原样转发给 Session worker,而不需要加载那个服务的契约。So the server can forward a Session call to the Session worker as is, without loading that service's contract.
- client 在服务适配边界用 chord 的解析器(
parseServiceCall、parseServiceCatalogue、createServiceStateDecoder),server 用decodeServiceControlCall与createServiceStateEncoder。At the service-adapter boundary the client uses chord's parsers (parseServiceCall,parseServiceCatalogue,createServiceStateDecoder), the serverdecodeServiceControlCallandcreateServiceStateEncoder. - 不在公共协议里:服务器与 worker 的生命周期(实验性的本地协调器只是不透明的消息路由),以及对端认证。Not in the public protocol: server and worker lifecycle (the experimental local coordinator is just an opaque message router), and peer authentication.
工作原理How it works
- coding-agent:
createClientServiceTransport(client, () => client.attachment)把 Session 路由变成一个 chord 传输,再交给应用的类型化服务绑定。coding-agent:createClientServiceTransport(client, () => client.attachment)turns the Session route into a chord transport for the application's typed service bindings. - 服务端收到订阅时为它建一个
ServiceStateEncoder,之后的更新都经它编码。On a subscription the server creates aServiceStateEncoderfor it, and later updates are encoded through it. - 应用的 payload 语义校验在 chord 服务里完成,而不是在协议层。Semantic payload validation happens in the chord services, not in the protocol.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/protocol/README.mdL15 | README.md | 分界的原文the boundary, as written |
| packages/protocol/src/protocol.tsL8 | OpaqueJsonValueSchema | 不透明 JSONopaque JSON |
| packages/client/src/client.tsL448 | createClientServiceTransport() | 路由 → chord 传输route → chord transport |
| packages/server/src/server.tsL332 | decodeServiceControlCall | 服务端识别控制调用the server recognises control calls |
扩展点Extension points#
maxFrameLength每一端的帧上限(默认 16 MiB);两端必须一致。Each peer's frame limit (16 MiB by default); both peers must match.encodeCbor / decodeCbor(…, CborOptions)单独使用 CBOR 编解码,配置字节、容器与深度上限。Use the CBOR codec on its own, with byte, container and depth limits.FrameDecoder / encodeFrame只用分帧,承载你自己的负载。Use only the framing, carrying your own payloads.parseClientMessage / parseServerMessage校验从别的通道得到的消息对象。Validate message objects obtained through another channel.ByteTransport (client) · ServerListener (server)任何保持字节顺序的流都能承载协议:Unix socket、WebSocket 中继、管道。Any order-preserving byte stream can carry the protocol: Unix sockets, a WebSocket relay, pipes.测试Tests#
一句话结论Takeaway三个测试文件:分帧(每个切分点、合并、空帧、上限)、信封校验(版本 8、不透明负载、取消、附着、未知字段)与 CBOR(undefined、BOM、__proto__、环、深度与长度上限)。Three test files: framing (every split point, coalescing, empty frames, limits), envelope validation (version 8, opaque payloads, cancellation, attachments, unknown fields) and CBOR (undefined, BOM, __proto__, cycles, depth and length limits).
| 测试文件Test file | 用例cases |
|---|---|
| packages/protocol/test/protocol.test.ts | 18 |
| packages/protocol/test/framing.test.ts | 10 |
| packages/protocol/test/cbor/cbor.test.ts | 8 |
用例数 = 源码中 it( / test( 的出现次数(静态统计,不运行测试)。Cases = occurrences of it( / test( in the source (static count; tests were not run).
交叉链接Cross-links#
协议的客户端一端:握手、请求、订阅the client end: handshake, requests, subscriptions
server已完成Done协议的服务端一端:路由到 Sessionthe server end: routing to Sessions
chord已完成Donepayload 的语义:服务调用、订阅与错误码payload semantics: service calls, subscriptions and error codes
coding-agent已完成Doneexperimental/ 下的 server 与 client 运行时the server and client runtime under experimental/