尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

集成指南:用 ManagedRuntime 桥接 Effect 与外部框架(t3code 实战)

集成指南:用 ManagedRuntime 桥接 Effect 与外部框架(t3code 实战) 集成指南用 ManagedRuntime 桥接 Effect 与外部框架t3code 实战【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3codeManagedRuntime是 Effect 生态中连接「纯函数式 Effect 程序」与「命令式外部世界」的桥梁只需从应用的 Layer 构建一个 runtime就能在 Web 处理器、框架 Hook、工作队列或遗留回调 API 中按需执行 Effect 程序。本文以 t3code 仓库内的官方集成文档与完整 Hono 示例为主线结合仓库中 Web、Mobile、Server 端对ManagedRuntime的真实使用讲清楚它的构建、运行、共享与销毁机制让你在现有应用里放心地引入 Effect 而不必重写全部业务代码。为什么需要ManagedRuntimeEffect 程序EffectA, E, R的天然形态是惰性的它只是对计算的描述只有在被「运行」时才会真正执行。在纯 Effect 应用里Effect.runMain或Layer.launch就足够但现实项目往往同时存在命令式代码——Express/Hono/Fastify 的路由、React 的事件回调、setTimeout定时器、原生回调 API 等。这些地方需要一个统一的、可复用的执行入口这就是ManagedRuntime的定位用应用自身的 Layer 构建一个runtime然后在任何需要命令式执行的地方复用它例如 Web 处理器、框架钩子、工作队列或遗留回调 API。它同时接管了服务依赖Layer的生命周期启动时构建服务图dispose时统一释放资源你不再需要手写「初始化/清理」样板代码。核心概念一次构建随处运行ManagedRuntime.make接受一个或多个 Layer返回一个持有完整服务图实例的 runtime 对象import { Layer, ManagedRuntime } from effect const runtime ManagedRuntime.make(Layer.mergeAll(httpClientLayer, cryptoLayer, persistenceLayer))构建完成后runtime 提供四种典型运行方式覆盖不同边界场景方法适用场景返回类型runPromiseasync/await 的 Web 处理器、Promise 边界PromiseA成功或 rejectionrunPromiseExit需要显式检查失败原因、不抛异常的边界PromiseExitA, ErunSync同步边界事件回调、同步校验逻辑直接返回A遇到异步/中断会抛错runCallback纯回调式 API如旧版 Node 回调、事件监听器通过回调接收结果在 t3code 的 Web 运行时中可以看到同样的模式先合并多个 LayerHTTP 客户端、浏览器加密、WebSocket、中继客户端、追踪再一次性ManagedRuntime.make(runtimeLayer)之后runPrimaryHttp通过primaryHttpRuntime.runPromise(effect)执行任意需要主环境 HTTP 服务的 Effect。Mobile 端 runtime.ts 结构完全一致只是 Layer 清单换成移动端专属的持久化层、加密层与追踪层。完整实战Hono 中的 Todo API官方集成示例 10_managed-runtime.ts 展示了从零到一的全过程领域逻辑全部封装在服务与 Layer 中路由处理器只负责「取结果、返回响应」。示例依赖effect与hono两个包t3code 的多个子包同样通过pnpmcatalog 统一引入effect依赖见 packages/client-runtime/package.json。第一步用 Schema 定义领域模型import { Schema } from effect class Todo extends Schema.ClassTodo(Todo)({ id: Schema.Int, title: Schema.String, completed: Schema.Boolean }) {} class CreateTodoPayload extends Schema.ClassCreateTodoPayload(CreateTodoPayload)({ title: Schema.String }) {} class TodoNotFound extends Schema.TaggedErrorTodoNotFound()(TodoNotFound, { id: Schema.Int }) {}Schema.Class同时充当运行时校验器与静态类型Schema.TaggedError生成带判别标签的错误类型便于后续用catchTag精确匹配。第二步把业务逻辑放进 Service Layerimport { Context, Effect, Layer, Ref } from effect export class TodoRepo extends Context.ServiceTodoRepo, { readonly getAll: Effect.EffectReadonlyArrayTodo getById(id: number): Effect.EffectTodo, TodoNotFound create(payload: CreateTodoPayload): Effect.EffectTodo }()(app/TodoRepo) { static readonly layer Layer.effect( TodoRepo, Effect.gen(function*() { const store new Mapnumber, Todo() const nextId yield* Ref.make(1) const getAll Effect.gen(function*() { return Array.from(store.values()) }).pipe( Effect.withSpan(TodoRepo.getAll) // 给每个操作附加追踪 Span ) const getById Effect.fn(TodoRepo.getById)(function*(id: number) { const todo store.get(id) if (todo undefined) { return yield* new TodoNotFound({ id }) } return todo }) const create Effect.fn(TodoRepo.create)(function*(payload: CreateTodoPayload) { const id yield* Ref.getAndUpdate(nextId, (current) current 1) const todo new Todo({ id, title: payload.title, completed: false }) store.set(id, todo) return todo }) return TodoRepo.of({ getAll, getById, create }) }) ) }要点Context.Service声明服务接口Layer.effect描述如何构建实现可变状态用Ref.make封装自增 ID 用Ref.getAndUpdate完全不用let/varEffect.fn与Effect.withSpan自动为方法附加追踪名称是后面可观测性章节的基础Map作为进程内存储的「玩具持久化」真实场景替换为数据库 Layer 即可路由代码零改动。第三步构建全局共享的 ManagedRuntimeimport { Layer, ManagedRuntime } from effect // 全局 memo map跨 ManagedRuntime 实例共享记忆化确保 memo 正确生效 export const appMemoMap Layer.makeMemoMapUnsafe() // 整个应用共享同一个 runtime生命周期由它统一管理 export const runtime ManagedRuntime.make(TodoRepo.layer, { memoMap: appMemoMap })这里传入Layer.makeMemoMapUnsafe()是示例特意强调的细节如果应用在多个ManagedRuntime实例之间共享同一组 Layer例如测试环境与生产环境各建一个 runtime默认的 memo map 会各自独立导致依赖被重复构建显式共享 memo map 才能让构建结果跨实例复用。第四步在 Hono 路由中命令式执行import { Effect } from effect import { Hono } from hono export const app new Hono() app.get(/todos, async (context) { const todos await runtime.runPromise( TodoRepo.use((repo) repo.getAll) ) return context.json(todos) }) app.get(/todos/:id, async (context) { const id Number(context.req.param(id)) if (!Number.isFinite(id)) { return context.json({ message: Todo id must be a number }, 400) } const todo await runtime.runPromise( TodoRepo.use((repo) repo.getById(id)).pipe( Effect.catchTag(TodoNotFound, () Effect.succeed(null)) ) ) if (todo null) { return context.json({ message: Todo not found }, 404) } return context.json(todo) }) const decodeCreateTodoPayload Schema.decodeUnknownSync(CreateTodoPayload) app.post(/todos, async (context) { const body await context.req.json() let payload: CreateTodoPayload try { payload decodeCreateTodoPayload(body) } catch { return context.json({ message: Invalid request body }, 400) } const todo await runtime.runPromise( TodoRepo.use((repo) repo.create(payload)) ) return context.json(todo, 201) })值得学习的写法错误即控制流TodoNotFound不靠 try/catch而是用Effect.catchTag在 Effect 内部转换为null再在命令式层映射成 HTTP 404路由层因此没有异常风暴入参校验与业务分离Schema.decodeUnknownSync在进入 Effect 之前完成请求体校验非法请求直接返回 400TodoRepo.use模式不把TodoRepo塞进EffectA, E, TodoRepo的环境参数里而是用use取出后直接传给每个 handler让runPromise的返回类型更干净。第五步进程退出时销毁 runtimeconst shutdown () { void runtime.dispose() } process.once(SIGINT, shutdown) process.once(SIGTERM, shutdown)dispose会按依赖逆序关闭所有资源关闭连接池、刷盘、释放文件句柄等确保优雅退出。t3code 的 Mobile 运行时更进一步它把runtime.dispose()挂在 React Native 的 Fast Refresh 钩子上——每次模块热替换HMR时先销毁旧 runtime 再构建新 runtime防止开发模式下资源泄漏。t3code 仓库中的四种集成范式官方示例只是起点。t3code 仓库在真实代码里把ManagedRuntime用在了不同层级可以作为更复杂的参考1. 全局共享运行时Web/Mobile 客户端apps/web/src/lib/runtime.ts 与 apps/mobile/src/lib/runtime.ts 都把整个客户端的依赖图HTTP、加密、WebSocket、持久化、追踪、中继合并进单一 runtime导出后供所有 UI 层命令式调用并把runtime.contextEffect包装成runtimeContextLayer供 React/组件体系注入环境。2. 动态重建的追踪运行时可观测性clientTracing.ts 展示了「运行时即服务」的高级用法configureClientTracing在配置变化时用新 Layer 重建ManagedRuntime通过runSync(Scope.make())开辟独立 Scope用runPromiseExit启动 OTLP Tracer再在替换前disposeTracerRuntime关闭旧 Scope 与旧 runtime——配置热更新与资源回收都交给ManagedRuntime编排。3. 测试 HarnessServer 集成测试Server 端的编排引擎集成测试同样用ManagedRuntime.make构建被测系统再以runPromise驱动断言例如 OrchestrationEngine.test.ts 中的createOrchestrationSystem构建一次 runtimerunPromise(Effect.service(...))取出引擎与查询服务测试结束时统一dispose。4. 测试边界上的强制约束lint 规则t3code 在 oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.ts 中定义了一条专门规则测试文件里禁止手写ManagedRuntime.make以及Effect.run*系列runPromise、runSync、runCallback等统一改用effect/vitest的it.effect(...)与测试 Layer。这说明仓库的工程约定是生产边界用ManagedRuntime桥接测试边界交给框架托管运行时。阅读这条规则可以顺便看到runSync、runCallback、runFork等全部运行方法的清单。迁移到其他框架同样的桥接模式官方示例在结尾明确指出同一套桥接模式可直接套用到 Express、Fastify、Koa 等框架只需按边界的同步/异步性质选择运行方法async 处理器Express/Fastify/Koa 的 async handler→runtime.runPromise(effect)同步回调/中间件→runtime.runSync(effect)要求 Effect 不含异步操作否则会抛出AsyncFiberException之类的错误纯回调 API如事件监听器、旧式流回调→runtime.runCallback(effect)。配套地把「构建 runtime」放在应用启动阶段如bootstrap.ts、入口文件把「dispose」挂在进程退出信号SIGINT/SIGTERM或框架关闭钩子上就完成了与任意既有应用的集成。最佳实践清单结合官方示例与 t3code 的落地经验集成ManagedRuntime时建议遵循全局单例整个应用只构建一个 runtime共享 memo map避免重复初始化服务与连接业务逻辑不进路由handler 里只做runPromise 响应映射领域逻辑留在 Service/Layer用 Effect 表达错误TaggedErrorcatchTag/catchAll在 Effect 内部消化预期错误命令式层只见成功值入参校验前置Schema.decodeUnknownSync放在边界入口非法输入不进 Effect必须 dispose进程退出、HMR 替换、配置热更新时调用runtime.dispose()否则连接与资源不会自动释放测试别手搓 runtime优先使用effect/vitest的it.effect与 Layer 注入ManagedRuntime留给生产边界。小结ManagedRuntime解决了 Effect 融入既有应用时的最后一公里问题以应用 Layer 为蓝图构建一个可复用的执行器用runPromise/runSync/runCallback适配任意命令式边界用dispose统一回收生命周期。官方 Hono 示例给出了一套可复制的完整骨架Schema 模型 → Service 业务 → 全局 runtime → 路由桥接 → 优雅退出t3code 仓库则进一步示范了它在客户端全局依赖图、动态重建的追踪运行时、服务端集成测试中的规模化用法——这正是把 Effect 引入老项目时最值得优先掌握的能力。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表