拓冰建站拓冰建站
首页 / 资讯中心 / 正文

effect 的 Context 分层存储优化:O(1) 的 `Context.add`、HTTP 服务器每请求零克隆与 docgen 签名精简

effect 的 Context 分层存储优化O(1) 的Context.add、HTTP 服务器每请求零克隆与 docgen 签名精简【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect导读本文基于 effect 仓库.changeset/pre/layered-context-storage.md这一变更说明展开剖析 effectTypeScript 生产级应用框架对依赖注入核心数据结构Context的一次底层存储重构通过引入基表 overlay 覆盖链的分层存储将Context.add的复杂度降为 O(1)并据此消除了 Node/Bun/Deno HTTP 服务器在每请求建立上下文时的服务 map 克隆开销同时梳理了effect/docgen在生成 API 签名时省略internal选项属性的配套改动。读完本文你将理解 effect 新版 Context 的内部布局、查找与惰性展平机制、其与 fiber 缓存的协作关系以及这次 patch 级变更对日常使用的影响。变更速览一次涉及五个包的 patch该 changeset 位于 .changeset/pre/layered-context-storage.md是一次面向 4.0 系列预发布版本的 patch 级变更涉及以下包包版本类型变更点effectpatchContext 改为分层存储Context.add变为 O(1)effect/platform-nodepatchHTTP 服务器消除每请求的服务 map 克隆effect/platform-bunpatch同上effect/platform-denopatch同上effect/docgenpatch生成的 API 签名省略internal选项属性变更原文概括为三点分层存储layered storage、Context.addO(1)、HTTP 服务器不再每请求克隆服务 map外加 docgen 对内部选项属性的签名裁剪。下面逐一结合源码印证。Context 分层存储ContextImpl的内部结构在 packages/effect/src/Context.ts 中Context 的运行时载体是ContextImpl它不再是一份孤立的Map而是由以下几个字段组成的混合结构见 Context.tsinterface ContextImplin Services extends ContextServices { cacheRoot: ContextImplany | undefined base: ReadonlyMapstring, any baseHits: number overlay: Overlay | undefined depth: number _flat: ReadonlyMapstring, any | undefined } interface Overlay { readonly key: string readonly value: unknown readonly parent: Overlay | undefined }各字段职责如下base一份共享的、不可变的基表普通Map存放基础服务集合overlay一个单向链表parent指向上一层链上的每个节点记录一次add写入的key与valuedepth当前 overlay 链的深度用于触发链的重置rebasebaseHits查找时落到 base 的命中计数用于触发惰性展平cacheRoot与 fiber 缓存共享相关的根标记_flat惰性计算并缓存的扁平化结果。这种共享基表 追加覆盖链的布局正是 changeset 所说的layered storage新增服务不再复制已有内容而是追加一个 overlay 节点。Context.add为何能做到 O(1)Context.add是dual(3, ...)的双参/三参调用形式内部委托给addUnsafe见 Context.tsexport const addUnsafe Services, I, S( self: ContextServices, key: string, service: Types.NoInferS ): ContextServices | I { const impl self as ContextImplServices const cacheRoot cacheKeys.has(key) ? undefined : impl.cacheRoot if (impl.depth MaxDepth) { // Rebase the overlay chain into a flat map, keeping the cacheRoot so a // rebase on an ordinary key does not invalidate fiber caches const map new Map(impl.mapUnsafe) map.set(key, service) return makeImpl(cacheRoot, map, undefined, 0) } return makeImpl( cacheRoot, impl.base, { key, value: service, parent: impl.overlay }, impl.depth 1 ) }关键点有二常规路径未达到MaxDepth仅创建一个新节点把新(key, service)挂到 overlay 链头base原样共享深度 1。整个过程不遍历、不复制已有服务因此单次add是常数时间 O(1)。链过长路径depth MaxDepth为避免 overlay 链无限增长导致查找退化为 O(n)将 overlay 链整体rebase成一张扁平Mapdepth重置为 0。MaxDepth 8定义在 Context.ts。测试 packages/effect/test/Context.test.ts 中专门验证了这一点连续添加 20 个服务后context.mapUnsafe.size仍为 20、顺序不变、取值正确且 rebase 后通过Context.hasSameCache确认不会破坏 fiber 缓存——这正是注释中rebase on an ordinary key does not invalidate fiber caches的行为约束。惰性展平lookup与flatten分层存储的代价是查找需要先走 overlay 链、再查 base。为此实现了两层优化见 Context.tsconst flatten (self: ContextImplany): ReadonlyMapstring, any { if (self._flat) return self._flat if (!self.overlay) return self._flat self.base const map new Map(self.base) applyOverlays(map, self.overlay) return self._flat map } const lookup (self: Contextany, key: string): unknown { const impl self as ContextImplany for (let overlay impl.overlay; overlay; overlay overlay.parent) { if (overlay.key key) return overlay.value } const value impl.base.get(key) // Misses must not advance the counter: reference-default lookups miss the // base on every fiber cache refresh, which would flatten every short-lived // request context and reintroduce the O(services) per-request cost if (value undefined !impl.base.has(key)) return notFound if (impl.overlay impl.baseHits FlattenAfterBaseHits) { impl.base flatten(impl) impl.overlay undefined impl.depth 0 } return value }flatten惰性只有真正需要完整扁平视图例如外部读取mapUnsafe、序列化、rebase时才拼合 base 与全部 overlay结果缓存进_flat下次直接复用lookup先链后表命中 overlay 立即返回绝不触碰 base只有链上未命中才回落到 base命中自适应展平当 base 的命中计数达到FlattenAfterBaseHits 8时就地展平、清空 overlay 与深度让后续查找直接走扁平表未命中不计入计数注释明确说明如果让 miss例如 Reference 默认值在 fiber 缓存刷新时的回落也推进计数器会把每个短生命周期请求的 Context 都过早展平重新引入每请求 O(services) 的开销——这正是 changeset 要根除的成本。测试 Context.test.ts 验证了展平时机连续 7 次命中 base 时_flat仍为undefined第 8 次命中后才变为Map且overlay、depth归零在此之后新增服务得到的上下文_flat又回到undefined、baseHits归零。cacheRoot与 fiber 缓存的关系ContextImpl.cacheRoot与 effect 的 fiber 缓存机制如fiberCached服务见测试中Context.Serviceundefined(..., { fiberCached: true })的用法紧密相关cacheKeys见 Context.ts记录了哪些 key 与缓存相关addUnsafe中若新增的 key 属于cacheKeys则新上下文的cacheRoot被置为undefined即不再与旧链共享缓存根否则沿用原cacheRootrebase 时也刻意保留cacheRoot保证普通 key 的展平不会使 fiber 缓存失效Context.hasSameCache见 Context.ts正是通过比较cacheRoot是否同一来判断两个 Context 是否共享缓存。从源码结构看这套设计的目标是让为每个请求派生一个新 Context这件事本身尽可能廉价同时尽量不打断已有的 fiber 级缓存。HTTP 服务器从每请求克隆到每请求 O(1) 添加changeset 中eliminating per-request service map clones in the HTTP servers的落点在 Node 平台实现中清晰可见。以 packages/platform/node/src/NodeHttpServer.ts 为例每个到达的请求都会基于父 fiber 的服务上下文派生出一个新 Context并把请求对象作为服务注入return Effect.withFiber((parent) { const services parent.context return Effect.succeed(function handler( nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse ) { const context Context.add(services, HttpServerRequest, new ServerRequestImpl(nodeRequest, nodeResponse)) const fiber Fiber.runIn(Effect.runForkWith(context as Context.Contextany)(handled), options.scope) // ... }) })在旧实现可以推断每次都需要复制父上下文的服务 map 再追加新服务下每个请求都要对整张服务表做一次 O(services) 的克隆而采用分层存储后这里的Context.add只追加一个 overlay 节点services基表被共享每个请求的派生成本从 O(服务数) 降为 O(1)。这正是 changeset 标题 layered-context-storage 与 HTTP 服务器优化之间的直接因果链。Bunpackages/platform/bun与 Denopackages/platform/deno平台的 HTTP 服务器同步享受这一收益因此也被列入同一 changeset 的 patch 名单。docgen生成的签名省略internal选项属性配套改动落在effect/docgen的解析器上。packages/tools/docgen/src/Parser.ts 中的shouldIgnore决定了哪些声明不进入文档生成流程const shouldIgnore (doc: Domain.Doc): boolean { return Record.has(doc.tags, internal) || Record.has(doc.tags, ignore) }在parseInterfaceDeclaration见 Parser.ts中凡是 JSDoc 带internal或ignore标记的接口/声明都会被直接过滤不参与签名生成options 对象的属性同样经过这套过滤于是带internal标记的选项属性不会再出现在 docgen 输出的 API 签名中对应 changeset 中 Docgen now omitsinternaloption properties from generated signatures。这样既保证公开 API 文档的整洁又不会影响库内实际使用这些内部属性的运行时行为。迁移影响与使用建议由于本次变更是 patch 级别的语义兼容优化日常使用Context.empty、Context.make、Context.add、Context.merge、Context.get等 API 的用户无需改动任何调用代码。值得留意的仅是以下内部行为特征add语义不变但成本变化新增服务的返回值依旧是不可变的新 Context原 Context 不受影响只是内部从复制再写入变为共享 base 追加 overlay且链深超过MaxDepth 8或 base 命中超过FlattenAfterBaseHits 8时会自动展平保证查找效率有界mapUnsafe读取会触发惰性展平它是内部低层 APIContext.ts 的makeUnsafe相关用法普通业务代码应优先使用Context.get/Context.getOption/Context.getOrElsefiber 缓存服务不受影响rebase 与常规add都会保留cacheRoot不会因普通 key 的添加而使fiberCached服务失效。如果需要深入验证或阅读实现细节建议按以下路径继续探索当前仓库Context 分层存储核心实现packages/effect/src/Context.ts重点看addUnsafe、lookup、flatten、MaxDepth相关段落行为与性能约束的测试佐证packages/effect/test/Context.test.ts每请求 O(1) 注入的 HTTP 服务器实现packages/platform/node/src/NodeHttpServer.ts、packages/platform/bun与packages/platform/deno对应文件docgen 的internal过滤逻辑packages/tools/docgen/src/Parser.ts。小结layered-context-storage是一次典型的内部结构优化、外部语义不变的工程实践把Context从扁平 map 每次复制改造成共享基表 overlay 覆盖链的分层存储使Context.add达到 O(1)配合MaxDepth/FlattenAfterBaseHits双阈值与惰性展平保持查找效率有界并让 Node/Bun/Deno HTTP 服务器得以零克隆地为每个请求派生上下文同时effect/docgen通过internal标签过滤让生成的 API 签名只暴露公开选项属性。理解这套分层存储的取舍有助于你在高吞吐服务场景下更自信地依赖 effect 的依赖注入机制。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门