跳转到内容

错误码 ​

Dougong 的所有结构化错误都带一个稳定的 code 字符串。应用代码应该 switch (error.code) 分流,而不是匹配消息文本——消息会变,code 不会。

命名规则 ​

错误码指向哪个对象的不变量被违反,而不是笼统地说「插件出错了」:

前缀违反不变量的对象属于
SERVICE_* / CONTRACT_* / CONFIG_*Contract 身份、依赖图、配置声明Core
LIFETIME_*结构化资源所有权Core
INSTALLATION_*一次已存在的安装Core
GROUP_*一棵安装所有权子树Core
ARTIFACT_*一份外部制品内部不自洽Platform
REGISTRATION_*注册身份与 Manifest 声明的注册依赖图Platform
MANIFEST_* / MODULE_* / API_* / PERMISSION_* / PLATFORM_*信任边界与加载边界Platform

所以看到 INSTALLATION_REMOVED 就知道是 Core 的一次安装,看到 REGISTRATION_REMOVED 就知道是 Platform 的一条注册——不用点进实现确认层级。

错误类型 ​

ts
class DougongError extends Error {
  readonly code: string
}

class ConfigValidationError extends DougongError {
  readonly issues: ReadonlyArray<StandardSchemaV1.Issue>
}

class PlatformError extends DougongError {}

class PermissionDeniedError extends PlatformError {
  readonly manifestName: string
  readonly denied: ReadonlyArray<string>
}

多个失败会被聚合成标准的 AggregateError,每一条原因都保留在 errors 数组里。

TypeError 与 Error 的分工

除了带 code 的错误,Dougong 还用两种原生类型:

  • TypeError —— 调用者传错了东西(不是函数、不是 Contract、key 冲突,或继续使用已经撤销其局部能力的对象)
  • Error —— 内部不变量被违反,属于框架 bug,正常使用碰不到

所以你可以按构造函数分流:DougongError 是可预期的操作失败,TypeError 是你的用法问题。

Core(@dougongjs/core) ​

构图期 ​

这些错误在任何 Instance 启动之前抛出。执行图一动没动。

Code触发条件
SERVICE_CYCLE依赖成环。消息带真实环路径:app.a:1 -> app.b:2 -> app.a:1。插件 requires 自己 provides 的 Service 也算
SERVICE_CONFLICT两个插件提供同一个 Service
SERVICE_MISSING必需 Service 没有提供者(optional() 声明的不算)
CONTRACT_CONFLICT同一个 Contract ID 被当作两种 kind 使用
CONFIG_INVALID配置未通过 Standard Schema。error.issues 是逐字段的问题列表

校验先于停机

所有受影响 Installation 的 Plugin 配置会在停止任何活动 Instance 之前全部校验完毕。一个拼错的字段不会让应用停在半路。

启动与活动执行 ​

Code触发条件
SERVICE_NOT_RETURNEDprovides 声明了某个 key,但 setup 的返回值里没有
SERVICE_UNAVAILABLEhost.get() 在非 active 状态调用;或依赖的 Service 所属安装未处于活动状态
LIFETIME_DISPOSEDLifetime 已开始释放,因而拒绝新的监听、贡献、任务、子 Lifetime、cleanup 或 Event 发送
INSTALLATION_UNAVAILABLE安装处于 failed 状态;或 setup / 配置校验器抛出了非 Error 的值(原值在 cause 里);或在未提交的 draft 上操作
INSTALLATION_REMOVED在已移除的 Installation 上操作
INSTALLATION_IDENTITYupdate() 试图更换 Plugin 名称。更新可以换实现和配置,不能换身份

Group ​

Code触发条件
GROUP_REMOVED在已移除的 Group 上操作,或继续使用该 Group 删除前创建的旧 ChangeSet
GROUP_UNAVAILABLEGroup 尚未成功建立;或 Group 操作失败时抛出的是非 Error 值

Platform(@dougongjs/platform) ​

信任边界 ​

在加载任何外部模块代码之前抛出。

Code触发条件
MANIFEST_INVALIDManifest 形状非法,或声明了重复的激活事件 / 权限
API_INCOMPATIBLEManifest 要求的 apiVersion 不满足应用版本
PERMISSION_DENIEDAuthorizer 拒绝。error.denied 是被拒的权限列表
REGISTRATION_DUPLICATE两份 Artifact 的 Manifest 名称落入同一个注册身份

Manifest 依赖解析 ​

这些错误来自 Manifest 声明在候选 Registration 图中的解析结果;错误码指向实际被验证的对象,而不是尚未加载的 Plugin。

Code触发条件
REGISTRATION_DEPENDENCY_MISSINGManifest 依赖没有对应 Registration
REGISTRATION_DEPENDENCY_INCOMPATIBLE依赖 Registration 的版本不满足范围
REGISTRATION_DEPENDENCY_INACTIVE依赖 Registration 存在但未激活
REGISTRATION_CYCLE候选 Registration 图中的 Manifest 依赖成环;消息带真实环路径

加载与激活 ​

Code触发条件
MODULE_LOAD_FAILEDLoader 抛异常。原始错误在 cause 里
MODULE_INVALID模块加载成功但没有导出合法的 Plugin
ARTIFACT_IDENTITY同一份 Artifact 内部不自洽:Manifest 名称与 placeholder 或加载出的 Plugin 名称不一致
REGISTRATION_BUSY该 Registration 正在变更,或 Platform 结构变更已关闭新的根激活入口
REGISTRATION_UNAVAILABLERegistration 不可用;激活或 admission 抛出了非 Error 的值(原值在 cause 里);或在未提交的注册上操作
REGISTRATION_REMOVED在已移除的 Registration 上操作
REGISTRATION_IDENTITY更新 Registration 时,新 Artifact 的 Manifest 名称与原名称不同
PLATFORM_UNAVAILABLEPlatform 已释放,或处于不允许该操作的状态

三种 IDENTITY 的区别

它们描述的是三个不同对象的身份不变量:

  • INSTALLATION_IDENTITY —— 已存在的 Installation 想换 Plugin 名称(Core)
  • REGISTRATION_IDENTITY —— 已存在的 Registration 想换 Manifest 名称(Platform)
  • ARTIFACT_IDENTITY —— 一份 Artifact 自身的 Manifest 和它加载出的 Plugin 名字对不上(Platform,此时可能还不存在 Registration)

怎么处理 ​

按 code 分流 ​

ts
try {
  await installation.ready()
} catch (error) {
  if (!(error instanceof DougongError)) throw error

  switch (error.code) {
    case "CONFIG_INVALID":
      showFieldErrors((error as ConfigValidationError).issues)
      break
    case "SERVICE_MISSING":
      suggestInstallDependency(error.message)
      break
    case "INSTALLATION_UNAVAILABLE":
      offerRetry()
      break
    default:
      report(error)
  }
}

处理聚合错误 ​

ts
try {
  await host.stop()
} catch (error) {
  if (error instanceof AggregateError) {
    for (const cause of error.errors) report(cause)
  }
}

接收后台错误 ​

后台任务和诊断订阅者的异常通过 Host 的上报通道送出,不改变它们正在观察的命令。Event 监听器失败则由 emit() 聚合并拒绝,交给发送方处理:

ts
const host = createHost({
  name: "app",
  onError: (error) => reportToSentry(error),
  logger: myLogger,
})

上报通道本身是 fail-safe 的:onError 抛出或异步拒绝都会退到 logger,logger 自己抛出或拒绝则作为终端 sink 被观察后静默——错误观察永远不会改变它正在观察的 Host 命令。

诊断失败记录 ​

Core 提供 isError(value) 作为外部拒绝值的统一识别边界。不可读取的原型返回 false,不抛出替代原始失败的反射异常。Platform 复用此判定,再选择自己的错误码与处置。Manifest 声明检查的原样传播权限只属于内部真实创建且冻结的错误;外部抛出的 PlatformError 或同名 code 不能绕过 MANIFEST_INVALID 边界。

Installation 与 Registration 的诊断错误始终使用 RecordedFailure,被丢弃句柄的后续 ready() 也会拒绝为该类型。它的 name 固定为 RecordedFailure,可选 code 与冻结的 snapshot: ErrorSnapshot 描述原始失败;它不声称自己是 TypeError、DougongError 或 PermissionDeniedError 子类。失败命令本身和仍可恢复的活动失败继续交付原始 Error。SnapshotPublisher 最终读取失败后,首次 dispose 抛原始值,终态 get 抛 RecordedFailure,且不再保留旧快照载荷或失败 reader。

快照保留原始名称、消息、错误码和栈文本,以及有界的 cause 与 errors 树。权限失败保留 manifestName 和 denied;配置失败保留 issue 消息与路径。任意对象、回调和自定义载荷会被省略。只读 snapshot getter 返回 RecordedFailure 私有拥有的记录,不信任外部同名字段或原型继承;枚举和序列化记录应显式使用 error.snapshot,而非错误实例的可枚举字段。栈文本复制为字符串;原始错误缺少 stack 时也清除记录构造器的原生栈,历史错误不会通过原错误对象图保活 Host、Installer、Loader 或 Platform。

记录最多展开 32 个错误节点,递归最多四条边,每个错误列表最多八项。消息上限为 4,096 字符,栈文本为 16,384,名称、错误码与权限字段为 256;校验路径最多 16 段。truncated 标记发生截断的记录与错误链。这是诊断记录协议,不是应用状态序列化器。

异常字段的反射与读取也经过记录边界:不可访问的 cause 对象记录为省略的 NonError,不可访问的数组或 path 字段不进入快照。数组长度只捕获一次,随后按有界长度读取条目;撤销的 Proxy 或抛错的访问器不能替换原始操作失败,也不能阻止终态资源释放。

RecordedFailure 的构造参数为 unknown;它直接记录任意拒绝值,每个错误节点最多检查一次原型,不再要求提前识别 Error。基本类型拒绝值使用 NonError 名称和有界文本,无法识别的对象只保存省略信息。错误归类与 Manifest 声明失败的来源由各自的模块私有集合拥有,只能由对应内部路径登记。清理结果由本次激活的内部结果拥有;外部错误,包括重抛另一次激活的真实清理失败,只作为错误数据,不能推进本次清理事实。

Core 创建的非 Error 包装错误冻结自身,保住用于后续归类的原始 cause 身份与 code;不冻结 cause 引用的应用对象,也不冻结应用显式抛出的 Error。

相关 ​

基于 MIT 许可证发布。