#AI#TypeScript#DevOps

TypeScript 运行时信任边界:版本化解码、资源上限与双读回滚

类型声明不会验证网络、Worker、URL、IndexedDB 或跨标签页消息。本文把 unknown 输入转换为规范内部类型,并设计 Schema 演进、双读发布和旧数据可回滚路径。

本文要点

  • 主题聚焦:AI、TypeScript、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年1月1日·更新于 2026年7月20日

fetch<User>()、event.data as Result 和 JSON.parse(raw) as Draft 不会检查运行时字节。服务端灰度、旧标签页、损坏的 IndexedDB 或 URL 参数都可能送来错误形状。TypeScript 高级类型 约束受控代码,本篇处理外部信任边界。

所有入口先得到 unknown

网络、Worker、BroadcastChannel、URL、postMessage 和本地记录都从 unknown 开始。入口适配器限制资源、识别版本、调用 decoder,再转换成稳定内部类型;业务组件不接触原始对象。

ts
type CanonicalTask = {
  id: string;
  prompt: string;
  priority: "interactive" | "background";
};

const taskV2 = z.object({
  schemaVersion: z.literal(2),
  id: z.string().regex(/^tsk_[a-z0-9]{12}$/),
  input: z.object({ prompt: z.string().min(1).max(20_000) }).strict(),
  priority: z.enum(["interactive", "background"]),
}).strict();

function decodeTask(raw: unknown): CanonicalTask {
  const parsed = taskV2.parse(raw);
  return { id: parsed.id, prompt: parsed.input.prompt, priority: parsed.priority };
}

Zod 适合就地声明,JSON Schema 适合跨语言协议。安全性来自运行时调用、严格字段策略和语义检查,不来自类型生成。模型输出即使用了 JSON mode,也要在完整记录后验证;参见 结构化输出。

按入口能控制的时点限制资源

Schema validator 不是流量整形器。对网络字节流,可以在 JSON 分配前限制状态码、Content-Type、解压后的实际读取字节和超时,不能只信 Content-Length;URL 也能在解析后立即限制总长度、重复参数数和允许键。这些是应用真正能执行的前置硬上限。

Worker message、BroadcastChannel 和 IndexedDB 读取不同:浏览器在 handler 或 decoder 得到值之前已经完成结构化克隆或反序列化,接收端检查无法撤销这次分配。把 event.data 再序列化来“估算字节”还会重复遍历和分配,并可能遇到循环结构。接收端的数组元素、字符串长度、嵌套深度和 deadline 仍有价值,但它们只限制后续 decoder、业务 CPU 与日志,不应宣传成初始内存防线。

这类入口在发送或写入侧使用窄 envelope,优先传 ID、digest 和版本;发送端按 outstanding task/bytes 使用 credit,IndexedDB 写入时限制可验证的编码大小。无法信任发送方时,把后续解析放入可终止 Worker,并限制来源、频率和并发。隔离可减少持续占用和主线程阻塞,但不能阻止已经发生的 clone 分配。

结构校验后还要验证跨字段不变量:时间顺序、游标归属、模型维度及 tenant。.strict() 或 additionalProperties: false 能发现意外字段,但兼容政策必须统一决定拒绝或安全丢弃。Agent 工具输出净化 的有界解析也适用于浏览器消息。

错误只记录 boundary、schema 版本、code、字段路径、字节区间和 payload hash,不记录完整 prompt、URL token 或 IndexedDB 记录。拒绝率按应用版本与来源分组。

每个外部版本拥有专属 decoder

不要写全是 optional 字段的“万能 Schema”。例如 v1 用 text、v2 用 input.prompt,应保留两个严格 decoder,再映射到 CanonicalTask:

ts
function decodeAnyTask(raw: unknown): CanonicalTask {
  const version = readBoundedVersionDiscriminator(raw);
  if (version === 1) return fromV1(taskV1.parse(raw));
  if (version === 2) return fromV2(taskV2.parse(raw));
  throw new DecodeError("UNSUPPORTED_SCHEMA_VERSION");
}

readBoundedVersionDiscriminator 只读取普通对象的整数自有属性,不遍历原型。转换函数建立默认值、枚举和内部不变量;领域层不再携带 v1/v2 联合类型。这与 REST API 版本管理 一致,但浏览器还要兼容缓存 bundle 和离线记录。

IndexedDB 保存 schemaVersion 与写入应用版本。onupgradeneeded 只做结构升级;数据迁移用可续跑游标和小事务。BroadcastChannel 消息带 protocol、sender 和 sequence,解码后再判断代际。

用先扩展、后写入、最后收缩发布

第一阶段 reader 同时支持 v1/v2,但只写 v1。第二阶段小流量写 v2,网络、Worker 和 IndexedDB 继续双读,离线记录惰性迁移并记录失败 hash。兼容窗口、旧标签页周期和离线保留期都结束后,才移除 v1 decoder。

回滚先把 writer 切回 v1,再恢复上一版逻辑;回滚 bundle 必须保留 v2 decoder,因为新格式已写入本地或由其他标签页发送。无法表达的字段只能降级到批准值,不能无声丢失权限、金额或执行状态。

用边界夹具证明不存在类型幻觉

测试覆盖版本缺失/未知、额外字段、错误枚举、超长字符串、巨型数组、深层对象、原型污染键、循环 clone、非法 URL、截断 JSON、旧 IndexedDB 记录和乱序消息。循环对象可合法到达 handler;浅层门禁或隔离 Worker 必须在 deadline 内拒绝且不再次序列化原载荷。decoder 只能返回满足不变量的对象或有限错误,不能部分写 UI。

端到端演练让新旧页面并存,写入 v2 后加载回滚 bundle,确认可读且不会回写损坏版本;再让 Worker 和网络返回同一 id 的不同 generation,验证并发隔离。输入只有通过版本专属验证并规范转换后才可信;对已 clone 的对象,这只约束后续使用,不保证此前未分配内存。

延伸阅读

Related

相关推荐