这个包是什么What this package ischord 是 Pi 新架构的底座:一个facet 宿主把小的功能单元(facet)按依赖顺序装配、激活、热重载和释放;facet 之间只通过有类型的服务交流,而服务可以在同一进程里,也可以跨传输在另一个进程里;服务暴露的状态是复制状态——提供方用普通的赋值修改,chord 算出最小的 JSON delta 发给每个订阅者。另有 ./context(取消与调用范围的值)、./delta(独立的变更追踪器)和 ./node(用 esbuild 打包插件、带 SHA-256 校验地加载)。chord is the foundation of Pi's new architecture: a facet host assembles small units of functionality (facets) in dependency order, activates them, hot-reloads them and disposes them; facets talk only through typed services, which can live in the same process or in another one across a transport; the state services expose is replicated state — the provider changes it with plain assignments and chord computes the minimal JSON delta for every subscriber. Around that sit ./context (cancellation and call-scoped values), ./delta (a standalone change tracker) and ./node (bundling plugins with esbuild and loading them with SHA-256 verification).
共 8,817 行 TypeScript,唯一的运行时依赖是 esbuild,不依赖任何其他 pi 包。使用者:durable(41 个文件)、coding-agent(26 个文件)、server(7 个文件)、client(2 个文件)、env(2 个文件)、protocol(2 个文件)——coding-agent 里用到的都在 src/experimental/ 下。8,817 lines of TypeScript; the only runtime dependency is esbuild, and it depends on no other pi package. Users: durable (41 files), coding-agent (26 files), server (7 files), client (2 files), env (2 files), protocol (2 files) — everything coding-agent uses lives under src/experimental/.
模块结构Module structure#
一句话结论Takeaway五块:API 与类型(api.ts、types.ts)、facet 宿主(facets/host.ts 里的 FacetKernel)、服务与复制状态(services/)、JSON delta(delta/,最大的一块)、以及 context 与 Node 打包加载(context/、node/)。五个导入路径各对应其中一块。Five blocks: API & types (api.ts, types.ts), the facet host (FacetKernel in facets/host.ts), services & replicated state (services/), JSON deltas (delta/, the largest), and context plus Node bundling and loading (context/, node/). Each of the five import paths maps to one of them.
要点Key points
- 最大的单个文件是
delta/tracker.ts:它让提供方像改普通对象一样改状态(Draft 代理),再算出 op。The largest single file isdelta/tracker.ts: it lets a provider change state like a plain object (a Draft proxy) and then computes the ops. ./delta和./context可以单独使用,不需要 facet 宿主;./bundler与./node只在 Node 上需要。./deltaand./contextwork on their own without the facet host;./bundlerand./nodeare only needed on Node.durable是最大的使用者:它的viewState()、watch()和任务图都是 chord 的复制状态,每次异步调用都带一个 chordContext。durableis the biggest user: itsviewState(),watch()and task graph are chord replicated states, and every async call takes a chordContext.
工作原理How it works
defineService<T>(id)定义令牌,defineFacet({ id, setup })定义功能单元。defineService<T>(id)defines a token,defineFacet({ id, setup })a unit of functionality.createFacetHost({ facets, serviceSources? })装配并激活,返回{ services, reload, dispose }。createFacetHost({ facets, serviceSources? })assembles and activates, returning{ services, reload, dispose }.- 跨进程时,一边
createRemoteServiceEndpoint(provider),另一边createRemoteServiceBinding({ transport, … })。Across processes, one side usescreateRemoteServiceEndpoint(provider)and the othercreateRemoteServiceBinding({ transport, … }).
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/index.tsL12 | export … | 包入口package entry |
| packages/chord/src/api.tsL22 | createFacetHost() | 宿主入口host entry |
| packages/chord/src/types.tsL281 | FacetEnvironment | setup 的参数setup's argument |
| packages/chord/src/facets/host.tsL340 | FacetKernel | 宿主内核the host kernel |
| packages/chord/src/delta/tracker.tsL321 | track() | 变更追踪器the change tracker |
公共 APIPublic API#
一句话结论Takeaway5 个导入路径(另有 ./package.json)。根入口从 8 个模块重新导出 26 个值和 46 个类型:createFacetHost、defineFacet / defineService、replicatedState 与远程服务的线格式;打包与磁盘加载分别在 ./bundler 与 ./node。5 import paths (plus ./package.json). The root re-exports 26 values and 46 types from 8 modules: createFacetHost, defineFacet / defineService, replicatedState and the remote-service wire format; bundling and loading from disk live in ./bundler and ./node.
| 导入路径Import path | 目标Target | 说明Notes |
|---|---|---|
@earendil-works/chord | ./dist/index.js | facet 宿主、服务、复制状态、RPC 线格式与类型the facet host, services, replicated state, the RPC wire format and types |
@earendil-works/chord/context | ./dist/context/index.js | 上下文与取消context and cancellation |
@earendil-works/chord/delta | ./dist/delta/index.js | JSON 草稿与增量JSON drafts and deltas |
@earendil-works/chord/bundler | ./dist/bundler.js | 把 facet 包打成可校验的 bundlebundle a facet package with integrity data |
@earendil-works/chord/node | ./dist/node.js | Node 专用:从磁盘加载 bundleNode only: load bundles from disk |
@earendil-works/chord/package.json | ./package.json | 包元数据package metadata |
常用导出Key exports
| 模块Module | 常用导出Key exports | 用途Use |
|---|---|---|
| packages/chord/src/api.ts | createFacetHost | 启动宿主:加载 facet、激活、提供服务start a host: load facets, activate, provide services |
| packages/chord/src/api.ts | defineFacet defineService | 声明插件与服务令牌declare plugins and service tokens |
| packages/chord/src/api.ts | replicatedState | 跨边界复制的状态state replicated across the boundary |
| packages/chord/src/api.ts | createStaticFacetLoader combineFacetLoaders | 组合 facet 来源compose facet sources |
| packages/chord/src/api.ts | createRemoteServiceBinding | 把远端服务接进宿主bind a remote service into a host |
| packages/chord/src/services/provider.ts | RemoteServiceProvider createRemoteServiceEndpoint | 把本地服务暴露给远端expose local services to a remote side |
根入口 index.ts 的全部导出(按来源模块)Every root index.ts export, by source module
展开全部 8 个来源模块Show all 8 source modules26 values46 types
| 来源模块Source module | 值导出Value exports | 类型导出Type exports |
|---|---|---|
| packages/chord/src/api.ts | combineFacetLoaders createFacetHost createRemoteServiceBinding createStaticFacetLoader defineFacet defineService replicatedState | — |
| packages/chord/src/delta/index.ts | — | Draft |
| packages/chord/src/json.ts | copyJson isJsonValue | CopyJsonOptions |
| packages/chord/src/services/errors.ts | isRemoteServiceErrorCode REMOTE_SERVICE_ERROR_CODES RemoteServiceError | RemoteServiceErrorCode |
| packages/chord/src/services/provider.ts | createRemoteServiceEndpoint RemoteServiceProvider | RemoteServiceEndpoint ServiceUpdatePublisher |
| packages/chord/src/services/state-codec.ts | createServiceStateDecoder createServiceStateEncoder | ServiceStateDecoder ServiceStateEncoder |
| packages/chord/src/services/wire.ts | createServiceCatalogueCall createServiceSubscribeCall createServiceUnsubscribeCall decodeServiceControlCall parseServiceCall parseServiceCatalogue parseServiceProviderUpdate parseServiceSubscriptionSnapshot parseWireServiceProviderUpdate parseWireServiceSubscriptionSnapshot | ServiceControlCall WireServiceInstanceSnapshot WireServiceMemberSnapshot WireServiceProviderUpdate WireServiceSubscriptionSnapshot |
| packages/chord/src/types.ts | — | AttachedReplicatedState Context ContextKey Facet FacetEnvironment FacetHost FacetLoader FacetOptions JsonRepresentation JsonValue +24 |
由构建脚本解析 export { … } from 语句生成。Generated by parsing the export { … } from statements at build time.
Facet 的一代:装配与激活A facet generation: assembly and activation#
一句话结论Takeawayactivate() 依次经过 setup → assembling → connecting → activating → active:先同步调用每个 facet 的 setup(env) 收集它要用和要提供的服务,再整体校验(一个服务一个提供者、模式一致、无环),按拓扑顺序给出激活顺序,等所有远程绑定 ready,最后按顺序运行各自的 onActivate。释放按相反顺序进行。activate() goes setup → assembling → connecting → activating → active: each facet's setup(env) runs synchronously to collect what it uses and provides, the whole graph is validated (one provider per service, matching modes, no cycles), a topological sort gives the activation order, every remote binding is awaited until ready, and each onActivate runs in order. Disposal runs in reverse.
要点Key points
setup必须同步:返回 Promise 会被拒绝。这让依赖图在任何异步工作开始前就是完整、可校验的。setupmust be synchronous: returning a Promise is rejected. That makes the dependency graph complete and checkable before any async work starts.- 校验错误都带着服务和 facet 的名字:"is provided by both …"、"… but no facet provides it"、"Facet dependency cycle: …"。Validation errors name the service and facets: "is provided by both …", "… but no facet provides it", "Facet dependency cycle: …".
- 任何一步失败,
#terminate()释放已经建好的部分,把原错误和清理错误合成一个 AggregateError 抛出。If any step fails,#terminate()disposes what was built and throws the original error together with cleanup errors as one AggregateError. - 每个 facet 内部,
own()登记的释放函数也按逆序执行——后建的先拆。Within each facet, the disposals registered withown()also run in reverse — last built, first torn down.
工作原理How it works
FacetKernel.activate():对每个 facet 调setup,得到FacetShape(requires / provides)。FacetKernel.activate(): callsetupfor each facet to get itsFacetShape(requires / provides).#resolveExternalServices()读宿主的serviceSources目录 →validateFacets()→topologicalOrder()。#resolveExternalServices()reads the host'sserviceSourcescatalogues →validateFacets()→topologicalOrder().- 绑定服务 → 等待
ready()→ 按#activationOrder逐个activate()。Bind services → awaitready()→activate()each facet in#activationOrder.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/facets/host.tsL398 | activate() | 激活一代activating a generation |
| packages/chord/src/facets/host.tsL808 | validateFacets() | 整图校验graph validation |
| packages/chord/src/facets/host.tsL858 | topologicalOrder() | 激活顺序activation order |
| packages/chord/src/facets/host.tsL753 | #terminate() | 失败清理cleanup on failure |
| packages/chord/src/facets/host.tsL796 | disposeFacetRecords() | 逆序释放reverse disposal |
热重载Hot reload#
一句话结论Takeawayreload(facets) 用新实现替换一组已激活的 facet,而使用方手里的句柄不断开:先 setup 候选并检查它们的 requires / provides 与旧的完全相同,再在旧实现仍然服务请求时激活候选,然后切换(单例重新绑定、远程提供者 replace()),再退役旧 facet、连接新的 keyed 实例。reload(facets) replaces a set of active facets with new implementations without disconnecting anyone's handles: set up the candidates and check their requires / provides are exactly the same, activate them while the old ones still serve, then cut over (rebind singletons, replace() remote providers), retire the old facets and connect the new keyed instances.
要点Key points
- 形状不变是硬性要求:重载不能增删依赖或提供的服务("must preserve its service requirements and provisions")。改形状需要新的一代。Unchanged shape is a hard rule: a reload cannot add or remove dependencies or provisions ("must preserve its service requirements and provisions"). Changing the shape needs a new generation.
- 切换之前失败:释放候选,宿主回到 active,旧实现继续工作。切换之后失败:整个代被中止("Facet reload failed after cutover")。A failure before the cutover disposes the candidates and returns to active with the old code still working. After the cutover, the whole generation is aborted ("Facet reload failed after cutover").
- 句柄是代理:每次访问都通过
ServiceSlot.resolve()取当前实现,所以重新bind之后已有句柄立即用上新代码。Handles are proxies: every access resolves the current implementation throughServiceSlot.resolve(), so after a rebind existing handles use the new code immediately. - keyed 实例按 generation 区分:重载后旧实例关闭,新实例以新的 generation 出现,观察者会看到 closed 再 spawned。Keyed instances are generation-specific: after a reload the old ones close and new ones appear with new generations, so observers see closed then spawned.
工作原理How it works
- 宿主先用 loader 的
load()拿到候选的LoadedFacets。The host first gets the candidateLoadedFacetsfrom a loader'sload(). host.reload(loaded.facets);失败就释放候选。host.reload(loaded.facets); on failure, dispose the candidate.- 成功后再释放退役的那一代
LoadedFacets,旧代码随之可以被回收。After success, dispose the retiredLoadedFacets, so the old code can be collected.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/facets/host.tsL423 | reload() | 重载流程the reload flow |
| packages/chord/src/facets/host.tsL891 | sameFacetShape() | 形状检查shape check |
| packages/chord/src/facets/host.tsL747 | #abort() | 切换后失败failure after cutover |
| packages/chord/src/services/handle.tsL9 | ServiceSlot | 可重新绑定的槽the rebindable slot |
| packages/chord/src/services/instances.tsL18 | InstanceDirectory | keyed 实例与代keyed instances and generations |
服务、句柄与 ContextServices, handles and Context#
一句话结论Takeaway服务令牌是 defineService<T>(id, options?)。默认是远程服务:它的成员只能是 (…json, Context) => Promise<json> 或 ReplicatedState<json>,这样同一份接口既能在进程内用,也能跨传输用;{ local: true } 的服务可以是任意 TypeScript 形状,但永远不出本进程。提供方式有单例(provide / use)与 keyed(provideMany / observe)两种。A service token is defineService<T>(id, options?). By default it is remote: its members must be (…json, Context) => Promise<json> or ReplicatedState<json>, so one interface works both in-process and across a transport; a { local: true } service can have any TypeScript shape but never leaves the process. Provision is either singleton (provide / use) or keyed (provideMany / observe).
要点Key points
- 远程约束在类型层面检查(
RemoteServiceContract<T>),运行时在边界上再校验一次(validateRemoteServiceImplementation)。The remote constraint is checked in the type system (RemoteServiceContract<T>) and again at runtime at the boundary (validateRemoteServiceImplementation). coding-agent的实验服务就是例子:pi.transcript只有一个state: ReplicatedState<ConversationView>;pi.local.presentation-ui是本地服务。coding-agent's experimental services are examples:pi.transcripthas juststate: ReplicatedState<ConversationView>;pi.local.presentation-uiis a local service.- 每个调用最后一个参数都是
Context:它携带取消信号和调用范围的值。awaitWithContext()取消时只拒绝等待者,不会中止正在进行的工作。Every call's last argument is aContext: it carries cancellation and call-scoped values.awaitWithContext()on cancellation rejects only the waiter, never the work in progress. - ID 以
$chord.开头的服务是保留的:那是 chord 自己的控制调用。Service IDs starting with$chord.are reserved: that prefix is chord's own control calls.
工作原理How it works
export const Models = defineService<Models>("pi.models")——类型和令牌同名。export const Models = defineService<Models>("pi.models")— the type and the token share a name.- 在 facet 的 setup 里:
const models = env.use(Models);在 onActivate 或之后调用await models.select(m, ctx)。In a facet's setup:const models = env.use(Models); from onActivate on, callawait models.select(m, ctx). - 取消:
const { context, cancel } = withCancel(parent),把context传下去。Cancellation:const { context, cancel } = withCancel(parent)and passcontextdown.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/api.tsL80 | defineService() | 服务令牌service tokens |
| packages/chord/src/types.tsL182 | RemoteServiceContract | 远程约束the remote constraint |
| packages/chord/src/services/provider.tsL587 | validateRemoteServiceImplementation() | 运行时校验runtime check |
| packages/chord/src/context/index.tsL83 | withCancel() | 取消cancellation |
| packages/chord/src/context/index.tsL98 | awaitWithContext() | 只取消等待者cancel the waiter only |
| packages/coding-agent/src/experimental/services/transcript.tsL9 | pi.transcript | 一个真实的服务a real service |
跨进程的服务Services across processes#
一句话结论Takeaway提供方一侧由 RemoteServiceProvider 执行调用、管理订阅,createRemoteServiceEndpoint() 处理 $chord.service 控制调用(catalogue、subscribe、unsubscribe);使用方一侧的 createRemoteServiceBinding() 给出与本地一样的外观。中间的传输只要实现 invoke 与 subscribe,外层信封由应用决定。On the provider side, RemoteServiceProvider runs calls and manages subscriptions, and createRemoteServiceEndpoint() serves the $chord.service control calls (catalogue, subscribe, unsubscribe); on the consumer side, createRemoteServiceBinding() gives the same facade as a local service. The transport in between only implements invoke and subscribe; the outer envelope is up to the application.
要点Key points
- 订阅更新有 6 种:
state(某成员的一批 op)、reset、unavailable、replaced、spawned、closed。Subscription updates come in 6 kinds:state(a batch of ops for one member),reset,unavailable,replaced,spawned,closed. - 背压:每个订阅最多 100 个待发更新;第 101 个到来时整个队列换成一个
reset,带着当前完整快照和新的 sequence 基线。Backpressure: at most 100 pending updates per subscription; when the 101st arrives the whole queue becomes oneresetwith a full current snapshot and new sequence baselines. - 普通
state更新绝不允许跳号;传输适配器必须保持快照 / reset / 更新的顺序,不能丢批。An ordinarystateupdate never permits a gap; transport adapters must keep snapshot / reset / update order and must not drop batches. - 能跨边界的错误只有 8 个
service_*码,例如service_not_found、service_stale_instance、service_invalid_value。Only 8service_*codes may cross the boundary, e.g.service_not_found,service_stale_instance,service_invalid_value.
工作原理How it works
- 提供方:用宿主的 provider 创建
createRemoteServiceEndpoint(provider),把收到的调用交给它。Provider: createcreateRemoteServiceEndpoint(provider)from the host's provider and hand it incoming calls. - 使用方:实现
RemoteServiceTransport;再实现一个RemoteServiceSource(catalogue()+open()),在open()里用createRemoteServiceBinding({ services, transport }),作为宿主的serviceSources传入——coding-agent的experimental/services/connection.ts就是这样做的。Consumer: implementRemoteServiceTransport; then aRemoteServiceSource(catalogue()+open()) whoseopen()usescreateRemoteServiceBinding({ services, transport }), passed to the host inserviceSources— ascoding-agent'sexperimental/services/connection.tsdoes. - 同进程测试:
createLoopbackServiceTransport(provider)直接把两边接起来。In-process tests:createLoopbackServiceTransport(provider)wires both sides directly.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/services/provider.tsL79 | RemoteServiceProvider | 提供方the provider |
| packages/chord/src/services/provider.tsL445 | #emit() | 100 → reset100 → reset |
| packages/chord/src/services/provider.tsL549 | createRemoteServiceEndpoint() | 控制调用control calls |
| packages/chord/src/services/consumer.tsL444 | RemoteServiceBindingImpl | 使用方外观the consumer facade |
| packages/chord/src/services/wire.tsL40 | $chord.service | 控制服务 IDcontrol service ID |
| packages/chord/src/services/errors.tsL1 | REMOTE_SERVICE_ERROR_CODES | 8 个错误码8 error codes |
| packages/chord/src/types.tsL308 | RemoteServiceSource | 外部服务来源external service sources |
复制状态Replicated state#
一句话结论Takeawayenv.replicatedState(initial) 返回一个可变的复制状态。change(ctx, draft => …) 在一个 Draft 上做普通的赋值,chord 算出 op、换上新的不可变版本、sequence 加一,然后发给两类订阅者:内部的精确订阅(同步拿到每个版本的 op,用来编码给远端)和公开的 subscribe()(每个订阅串行收到完整值)。env.replicatedState(initial) returns a mutable replicated state. change(ctx, draft => …) makes plain assignments on a Draft; chord computes the ops, swaps in the new immutable revision, increments the sequence and publishes to two kinds of subscriber: internal exact subscriptions (every revision's ops, synchronously, for encoding to remotes) and public subscribe() (complete values, serialized per subscription).
要点Key points
- 一次
change()恰好发布一个原子版本;回调抛错时值和 sequence 都不变;没有产生 op 就不发布。A successfulchange()publishes exactly one atomic revision; if the callback throws, value and sequence stay unchanged; no ops, no publication. - 回调必须同步,也不能在回调里再次 change("cannot be changed reentrantly")。The callback must be synchronous and cannot change the state again from inside ("cannot be changed reentrantly").
- 公开订阅最多积压 100 个值,溢出时只留最新的——所以公开的 sequence 可能跳号;异步回调里应读捕获的
value而不是state.value。Public subscriptions keep at most 100 pending values and keep only the newest on overflow — so public sequences may skip; async callbacks should read the capturedvalue, notstate.value. - 远端副本严格按序:
update要求 sequence 正好是上一个加一,否则清空并抛出 "update sequence has a gap"。The remote replica is strictly ordered:updaterequires exactly the previous sequence plus one, else it clears and throws "update sequence has a gap".
工作原理How it works
status.change(ctx, d => { d.output += "done\n"; d.count += 1 })。status.change(ctx, d => { d.output += "done\n"; d.count += 1 }).- 内部:
tracker.beginChange()→ 回调 →prepare()→adopt()→publisher.publish(value, ops, ctx)。Internally:tracker.beginChange()→ callback →prepare()→adopt()→publisher.publish(value, ops, ctx). - 远端:
ReplicatedStateReplica.hydrate(seq, ops)建立基线,之后每个update(seq, ops)用applyImmutable重放。Remote:ReplicatedStateReplica.hydrate(seq, ops)sets the baseline, then eachupdate(seq, ops)replays withapplyImmutable.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/services/state.tsL184 | MutableReplicatedStateImpl | 可变状态mutable state |
| packages/chord/src/services/state.tsL202 | change() | 一次变更one change |
| packages/chord/src/services/state.tsL344 | ReplicatedStateReplica | 远端副本the remote replica |
| packages/chord/src/services/state.tsL390 | update() | 按序检查order check |
| packages/chord/src/services/state-codec.tsL60 | createServiceStateEncoder() | 每个订阅的编码器per-subscription encoder |
JSON deltaJSON deltas#
一句话结论Takeaway./delta 定义 7 种 op:r 替换根、s 设置、d 删除、a 字符串追加、t 字符串去头、p 数组 splice、m 数组重排。追踪器为字符串挑最省的写法(追加;或去头加追加——适合滚动日志;否则整体设置),op 太多时折叠成一个 r。线上格式 WireOp 只多两种压缩:路径编号和省略路径。./delta defines 7 ops: r replace root, s set, d delete, a string append, t string front-truncate, p array splice, m array permute. The tracker picks the cheapest string form (append; or truncate plus append — good for rolling logs; else a set) and folds into one r when there are too many ops. The wire format WireOp adds only two compressions: path ids and omitted paths.
要点Key points
- 超过 4,096 个 op 时折叠成
["r", 整个根];去头 + 追加的重叠扫描最多 65,536 个字符。More than 4,096 ops fold into["r", whole root]; the truncate-plus-append overlap scan covers up to 65,536 characters. - 路径在第二次使用时才定义编号(
["#", id, path]),因为定义比路径本身还长,只用一次的路径内联更省。A path gets an id on its second use (["#", id, path]), because a definition costs more than the path; paths used once stay inline. - 省略路径只在同一批内有效;跨批的唯一状态是编号字典,遇到
r就清空——每个r都是一个恢复点。Omitted paths only work within a batch; the only cross-batch state is the id dictionary, cleared at everyr— eachris a recovery point. - 值是不可变、结构共享的:没变的子树在版本之间共用,不做防御性拷贝也不冻结。Values are immutable and structurally shared: unchanged subtrees are reused across revisions, with no defensive copies and no freezing.
工作原理How it works
const tracker = track(initial);const change = tracker.beginChange();改change.state。const tracker = track(initial);const change = tracker.beginChange(); mutatechange.state.const prepared = change.prepare();tracker.adopt(prepared);副本用applyImmutable(prepared.base, prepared.ops)。const prepared = change.prepare();tracker.adopt(prepared); a replica usesapplyImmutable(prepared.base, prepared.ops).- 跨边界:
encoder().encode(ops)→ 发送 →decoder().decode(wire)。Across a boundary:encoder().encode(ops)→ send →decoder().decode(wire).
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/delta/index.tsL32 | Op | 7 种 opthe 7 ops |
| packages/chord/src/delta/index.tsL52 | WireOp | 线上格式the wire format |
| packages/chord/src/delta/index.tsL523 | encoder() | 第二次使用时编号ids on second use |
| packages/chord/src/delta/tracker.tsL112 | MAX_DELTA_OPERATIONS | 折叠上限fold limit |
| packages/chord/src/delta/index.tsL409 | applyImmutable() | 重放replay |
| packages/chord/src/delta/diff.tsL513 | diffRevisions() | 两个版本的差异diffing two revisions |
打包与加载插件Bundling and loading plugins#
一句话结论TakeawaybundleFacetPackage() 读插件包 package.json 里的 chord.facets,用 esbuild 打成 .cjs(peer 依赖和 chord 本身保持 external),写出 chord-facets.json 清单,每个入口带 SHA-256 完整性值。createFacetBundleLoader() 加载时先校验哈希,再用 node:vm 的 compileFunction 编译,只允许 require 清单里声明过的 external。bundleFacetPackage() reads chord.facets from a plugin's package.json, bundles it with esbuild into .cjs (peer dependencies and chord itself stay external) and writes a chord-facets.json manifest with a SHA-256 integrity value per entry. createFacetBundleLoader() verifies the hash at load time, compiles with node:vm's compileFunction, and lets the code require only externals the manifest declares.
要点Key points
- 输出先写到临时目录,再整体改名替换;旧目录先挪到
.old-<uuid>,替换失败就挪回来——加载方永远看不到写了一半的包。Output goes to a temporary directory and is renamed into place; the old directory is moved to.old-<uuid>first and moved back if the swap fails — a loader never sees a half-written bundle. - 入口文件名带内容哈希(
facet-<短哈希>-[hash].cjs),不同版本不会互相覆盖。Entry file names carry content hashes (facet-<short hash>-[hash].cjs), so versions never overwrite each other. - 也可以把整个包读成一个 artifact(
readFacetBundleArtifact)再加载,适合把插件作为一个值传递。A bundle can also be read as one artifact (readFacetBundleArtifact) and loaded from that, handy for passing a plugin around as one value.
工作原理How it works
- 构建:
bundleFacetPackage({ packagePath, outdir })。Build:bundleFacetPackage({ packagePath, outdir }). - 加载:
createFacetBundleLoader({ manifestPath, entry, resolveExternal? })→load()得到LoadedFacets;完整性校验默认开启(verifyIntegrity)。Load:createFacetBundleLoader({ manifestPath, entry, resolveExternal? })→load()givesLoadedFacets; the integrity check is on by default (verifyIntegrity). - 交给
host.reload(loaded.facets)热替换,或作为新一代的 facets。Hand them tohost.reload(loaded.facets)for a hot swap, or use them for a new generation.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/chord/src/node/package.tsL30 | bundleFacetPackage() | 打包插件包bundling a package |
| packages/chord/src/node/bundle.tsL39 | bundleFacets() | esbuild 与原子替换esbuild and the atomic swap |
| packages/chord/src/node/manifest.tsL18 | FacetBundleManifest | 清单格式manifest format |
| packages/chord/src/node/bundle-loader.tsL134 | createFacetBundleLoader() | 校验并加载verify and load |
| packages/chord/src/node/bundle-loader.tsL56 | readFacetBundleArtifact() | artifact 形式artifact form |
扩展点Extension points#
defineFacet({ id, setup })写一个功能单元:在 setup 里声明 use / provide。Write a unit of functionality: declare use / provide in setup.defineService<T>(id, { local? })定义一个服务契约,远程或本地。Define a service contract, remote or local.RemoteServiceTransport实现 invoke / subscribe,把服务接到任何传输上。Implement invoke / subscribe to carry services over any transport.FacetLoaderload() 返回 LoadedFacets:静态、组合或从打包文件加载。load() returns LoadedFacets: static, combined or from bundles.RemoteServiceSourcecatalogue() + open():把另一个进程的服务目录接进宿主。catalogue() + open(): plug another process's service catalogue into a host.createContextKey<T>()定义调用范围的值,随 Context 传下去。Define call-scoped values carried by a Context.track() · encoder() / decoder()单独使用 delta 追踪与线上编码。Use delta tracking and wire encoding on their own.测试Tests#
一句话结论Takeaway测试按模块分文件:facet 宿主与加载器、服务(含线格式与更新投递)、复制状态(值、差分、草稿、投递与模糊测试)、增量(应用、克隆、差分)、上下文与打包,另有增量性能基准。Tests are split by area: the facet host and loaders, services (including the wire format and update delivery), replicated state (values, diffs, drafts, delivery and a fuzz test), deltas (apply, clone, diff), context and bundling, plus delta benchmarks.
另有 1 个测试文件未列出(共 21 个,286 个用例)。1 more test files not listed (21 files, 286 cases in total).
用例数 = 源码中 it( / test( 的出现次数(静态统计,不运行测试)。Cases = occurrences of it( / test( in the source (static count; tests were not run).
交叉链接Cross-links#
viewState / watch / 任务图都是 chord 复制状态;每个调用带 chord ContextviewState / watch / the task graph are chord replicated states; every call takes a chord Context
coding-agent已完成Donesrc/experimental 里的服务(pi.transcript、pi.models …)the services in src/experimental (pi.transcript, pi.models …)
env已完成DoneRemoteExecutionEnv 的方法都带 chord ContextRemoteExecutionEnv methods take a chord Context