t3code 的 Effect 库开发指南从 Effect.gen、Schema 到服务与可观测性的完整实践【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code导读本文以本仓库.repos/effect-smol/LLMS.md中收录的 Effect 官方开发文档为骨架系统讲解在 t3code一个以 TypeScript 为主、横跨桌面端、服务端与移动端的 monorepo中如何规范地编写 Effect 代码包括Effect.gen/Effect.fn的写法、Schema领域建模、Context.Service服务与Layer组合、错误处理、资源管理、流处理、可观测性与测试等全链路实践。读完本文你将掌握 Effect 推荐的核心编码范式并能在 t3code 的packages/shared、apps/desktop等目录中看到这些范式在生产代码中的真实落地。说明.repos/effect-smol是 t3code 仓库内置的一份 Effect 源码与文档快照。官方建议在开发中直接使用这份文档与该快照中的 Effect 源码避免引用环境中其他可能过时或错误的 Effect 资料副本见 LLMS.md。文档中的示例均含教学注释实际编码时无需保留这些注释。一、文档背景LLMS.md 从何而来.repos/effect-smol/LLMS.md并非手写文档而是由.repos/effect-smol/ai-docs/src目录下的示例源码与各小节index.md自动生成的一份面向 LLM/Agent 的库文档详见 ai-docs/README.md每个主题一节正文来自各目录下的index.md完整示例以.ts文件放在同目录文件名以数字前缀控制顺序如10_、20_通过pnpm ai-docgen一次性重新生成pnpm ai-docgen:watch进入监听模式示例要求必须代表真实世界用法与最佳实践优先采用 service 风格的代码组织。因此在阅读本指南时你可以随时打开.repos/effect-smol/ai-docs/src/下对应的.ts示例文件对照学习。二、编写 Effect 代码Effect.gen 与 Effect.fn 双范式官方文档给出的核心写作原则是优先使用Effect.gen与Effect.fn(name)编写 Effect 代码再用组合子combinator附加额外行为。这种风格比单纯堆叠组合子更易读、更易维护。2.1 使用 Effect.gen 编写命令式代码Effect.gen让你以类似async/await的命令式风格编写 Effect 代码用yield*取出一个 effect 的结果import { Effect, Schema } from effect Effect.gen(function*() { yield* Effect.log(Starting the file processing...) yield* Effect.log(Reading file...) // 抛出错误时务必显式 return以确保 TypeScript 理解函数不会继续执行。 return yield* new FileProcessingError({ message: Failed to read the file }) }).pipe( // 用 .pipe 附加额外功能 Effect.catch((error) Effect.logError(An error occurred: ${error})), Effect.withSpan(fileProcessing, { attributes: { method: Effect.gen } }) ) // 使用 Schema.TaggedError 定义自定义错误 export class FileProcessingError extends Schema.TaggedErrorFileProcessingError()(FileProcessingError, { message: Schema.String }) {}关键点yield*一个 effect 即可解包其结果失败会沿错误通道传播抛错分支必须return yield* new ...Error(...)这既构造了错误又让 TypeScript 的收窄逻辑生效后续代码不可达额外行为错误捕获、追踪 span、日志标注统一通过.pipe(...)挂接。对应源码示例见 01_effect-gen.ts。2.2 使用 Effect.fn 定义返回 Effect 的函数凡是返回 Effect 的函数都用Effect.fn而不是返回一个 Effect.gen 的函数这是文档明确强调的纪律import { Effect, Schema } from effect // 传给 Effect.fn 的字符串会改善堆栈信息并自动附加一个追踪 span // 内部使用 Effect.withSpan。 // 该名字应与函数名保持一致。 export const effectFunction Effect.fn(effectFunction)( // 用 Effect.fn.Return 指定返回类型它接受与 Effect.Effect 相同的类型参数。 function*(n: number): Effect.fn.Returnstring, SomeError { yield* Effect.logInfo(Received number:, n) // 抛出错误时务必显式 return以确保 TypeScript 理解函数不会继续执行。 return yield* new SomeError({ message: Failed to read the file }) }, // 通过额外参数附加功能**不要**对 Effect.fn 使用 .pipe。 Effect.catch((error) Effect.logError(An error occurred: ${error})), Effect.annotateLogs({ method: effectFunction }) ) // 使用 Schema.TaggedError 定义自定义错误 export class SomeError extends Schema.TaggedErrorSomeError()(SomeError, { message: Schema.String }) {}要点字符串名称同时改善堆栈可读性并自动挂载追踪 span附加行为作为额外参数传入而不是.pipeEffect.fn.Return成功值, 错误, 依赖接受与Effect.Effect相同的类型参数成功、错误、环境依赖三通道。对应源码示例见 02_effect-fn.ts。2.3 从常见数据源创建 Effect在实际代码里Effect 往往不是凭空出现的而是从各种外部来源包装而来。文档归纳了如下常见构造器完整示例见 10_creating-effects.ts构造器适用场景示例Effect.succeed包装内存中已有的值Effect.succeed({ env: prod, retries: 3 })Effect.sync包装不会抛出异常的同步副作用Effect.sync(() Date.now())Effect.try包装可能抛异常的同步代码Effect.try({ try: () JSON.parse(input), catch: (cause) new InvalidPayload({ input, cause }) })Effect.tryPromise包装可能 reject/throw 的 Promise APIEffect.tryPromise({ async try() {...}, catch: (cause) new UserLookupError({ userId, cause }) })Effect.fromNullishOr把可空值转为类型化的 effectEffect.fromNullishOr(headers.get(x-workspace-id)).pipe(Effect.mapError(() new MissingWorkspaceId()))Effect.callback包装回调风格异步 APIEffect.callbacknumber((resume) { ... resume(Effect.succeed(200)) })注意Effect.callback可以返回一个 finalizer使中断interruption能取消底层的回调源例如清除setTimeout。2.4 t3code 生产代码中的印证这些范式不是纸面规范而是 t3code 的真实编码风格shell.ts 中resolveCommandPathForPlatform直接使用Effect.fn(shell.resolveCommandPathForPlatform)(...)返回类型标注为Effect.fn.Returnstring, CommandResolutionError, FileSystem.FileSystem | Path.Path第三个类型参数清晰声明了所需的环境依赖httpReadiness.ts 中waitForHttpReady同样以Effect.fn(shared.httpReadiness.waitForHttpReady)定义内部通过HttpClient.HttpClient、Ref、Schedule完成对 HTTP 服务就绪状态的轮询探测nodeSqliteClient.ts 中makeWithDatabase也遵循Effect.fn命名风格并把依赖声明为Scope.Scope | Reactivity.Reactivity。三、用 Schema 定义领域模型与校验文档规定Effect 中所有校验与领域建模都使用Schema避免使用谓词predicate或手工解析来处理不可信数据。详细指南可参阅快照内 SCHEMA.mdLLMS.md 中给出了官方链接并提醒文档很大请分段阅读。基础用法示例见 10_schema-basics.tsimport { Effect, Schema } from effect // Schema.Class 同时定义运行时校验器与 TypeScript 类 // 适合只能从合法数据构造的领域模型。 export class User extends Schema.ClassUser(path/to/module/User)({ id: Schema.Int, name: Schema.NonEmptyString, email: Schema.String, role: Schema.Literals([admin, member]) }) {} // 校验后的类型就是 User 类本身 export type UserType typeof User[Type] // 外部表示编码后的类型 export type UserEncoded typeof User[Encoded] // 在应用边界复用解析器而不是每次请求重建。 // 在 Effect 代码内部请使用返回 Effect 的 API让校验错误保留在错误通道中。 export const decodeUser Schema.decodeUnknownEffect(User) export const encodeUser Schema.encodeEffect(User) export class InvalidUserPayload extends Schema.TaggedErrorInvalidUserPayload()(InvalidUserPayload, { message: Schema.String }) {} export const parseUserPayload Effect.fn(parseUserPayload)((input: unknown) decodeUser(input).pipe( Effect.mapError((error) new InvalidUserPayload({ message: error.message })) ) )要点Schema.ClassClassName(...)生成一个既是类型又是运行时守卫的类Schema.Int、Schema.NonEmptyString、Schema.Literals([...])等内建 schema 直接表达约束decodeUnknownEffect/encodeEffect在应用边界HTTP 请求、外部消息等完成未知输入 → 强类型值和强类型值 → 外部表示的双向转换自定义错误统一用Schema.TaggedError声明天然携带结构化字段并可被catchTag按 tag 捕获。在 t3code 中这一模式被大量复用例如 nodeSqliteClient.ts 用Schema.TaggedErrorUnsupportedNodeSqliteOperationError()(UnsupportedNodeSqliteOperationError, {})定义Node SQLite 不支持 executeStream的领域错误Net.ts 则用Data.TaggedError(NetError)声明带message与可选cause的错误类型。四、编写 Effect Services模块化、可测试、可维护文档指出Service 是组织 Effect 代码最常见的方式优先用服务封装行为以保证代码模块化、可测试、可维护。4.1 默认方式Context.Service// file: src/db/Database.ts import { Context, Effect, Layer, Schema } from effect // 第一个类型参数传服务类名第二个传服务接口。 export class Database extends Context.ServiceDatabase, { query(sql: string): Effect.EffectArrayunknown, DatabaseError }()( // 服务标识字符串应包含包名与服务文件所在子目录路径。 myapp/db/Database ) { // 为服务挂一个静态 Layer用于提供服务实现。 static readonly layer Layer.effect( Database, Effect.gen(function*() { // 用 Effect.fn 定义服务方法 const query Effect.fn(Database.query)(function*(sql: string) { yield* Effect.log(Executing SQL query:, sql) return [{ id: 1, name: Alice }, { id: 2, name: Bob }] }) // 用 Database.of 返回实现了服务接口的实例 return Database.of({ query }) }) ) } export class DatabaseError extends Schema.TaggedErrorDatabaseError()(DatabaseError, { cause: Schema.Defect() }) {} // 需要访问服务类型时使用 Database[Service] export type DatabaseService Database[Service]要点Context.ServiceClassName, Interface()(包名/子目录/服务名)是推荐的服务声明方式字符串标识具备全局唯一性与可追踪性静态layer字段让服务自带默认实现消费者无需关心装配细节方法用Effect.fn定义具备良好堆栈与 spanDatabase[Service]可随时取出服务接口类型。t3code 中的实例Net.ts 声明class NetService extends Context.ServiceNetService, NetServiceShape()(t3tools/shared/Net/NetService)服务形状NetServiceShape定义了端口可用性探测、回环端口预留、可用端口查找等一组方法。4.2 配置项与特性开关Context.Reference对于带默认值的配置值、特性开关等服务使用Context.Referenceimport { Context } from effect export const FeatureFlag Context.Referenceboolean(myapp/FeatureFlag, { defaultValue: () false })t3code 中 hostProcess.ts 连续定义了HostProcessPlatform、HostProcessArchitecture、HostProcessHostname三个Context.Reference分别以process.platform、process.arch、NodeOS.hostname()作为默认值——这正是可测试的当前环境这一设计目标测试中可覆盖这些 Reference 以模拟任意平台。4.3 Layer 的组合与动态装配组合先构建聚焦的 service layer再用Layer.provide/Layer.provideMerge组合按需决定对外暴露哪些服务见 20_layer-composition.ts动态装配用Layer.unwrap从 Effect / Config 动态构建 layer见 20_layer-unwrap.ts。t3code 的 nodeSqliteClient.ts 展示了Layer.provide的组合用法Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer))把 SQLite 客户端层挂到 Reactivity 依赖之上。五、错误处理类型化错误 按 tag 捕获文档给出了错误处理的完整范式示例见 01_error-handling.tsimport { Effect, Schema } from effect // 用 Schema.TaggedError 定义自定义错误 export class ParseError extends Schema.TaggedErrorParseError()(ParseError, { input: Schema.String, message: Schema.String }) {} export class ReservedPortError extends Schema.TaggedErrorReservedPortError()(ReservedPortError, { port: Schema.Int }) {} declare const loadPort: (input: string) Effect.Effectnumber, ParseError | ReservedPortError export const recovered loadPort(80).pipe( // 用 Effect.catchTag 一次捕获多个错误返回默认端口号 Effect.catchTag([ParseError, ReservedPortError], (_) Effect.succeed(3000)) ) export const withFinalFallback loadPort(invalid).pipe( // 先捕获特定错误 Effect.catchTag(ReservedPortError, (_) Effect.succeed(3000)), // 再兜底捕获所有错误 Effect.catch((_) Effect.succeed(3000)) )文档还提供了两个进阶示例10_catch-tags.ts用Effect.catchTags在一个位置集中处理多个 tagged 错误20_reason-errors.ts为错误定义一个带 tag 的reason字段然后用Effect.catchReason、Effect.catchReasons捕获或用Effect.unwrapReason把 reason 解包进错误通道——适合需要按失败原因分类处理的业务。六、资源管理与 ScopeacquireRelease 与后台任务Effect 用Scope与 finalizer 安全管理资源生命周期10_acquire-release.ts用Effect.acquireRelease管理资源生命周期——Layer 构建时创建资源如 SMTP transporterLayer 拆除时自动释放transporter.close()保证资源始终被正确清理。示例还展示了Config.string/Config.redacted读取配置以及Redacted.value安全取用密钥20_layer-side-effects.ts用Layer.effectDiscard封装没有服务接口的后台任务如常驻 worker30_layer-map.ts用LayerMap.Service按标识如租户 ID动态构建与管理多个资源实例。t3code 中 preview/Manager.ts 大量使用Effect.acquireRelease管理浏览器实例等长生命周期资源nodeSqliteClient.ts 的依赖签名Scope.Scope | Reactivity.Reactivity也印证了资源生命周期绑定 Scope的实践。七、运行 Effect 程序runMain 与 Layer.launch文档给出了两种进程入口模式10_run-main.tsNodeRuntime.runMain(program)作为进程入口自动安装 SIGINT/SIGTERM 处理器并在退出时优雅中断 fiberBunRuntime.runMain提供完全相同的 API 形态。可用disableErrorReporting: true关闭自动错误上报若你的应用已自行集中处理错误20_layer-launch.ts当整个应用都由 layer 表示HTTP 服务器 后台 worker时用Layer.launch把 layer 变成长期运行的Effectnever示例中配合HttpRouter.serve与NodeHttpServer.layer起一个带/health、/healthz健康检查的 HTTP 服务。// 摘要示例Layer.launch 作为入口 export const HttpServerLive HttpRouter.serve(HealthRoutes).pipe( Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) ) export const main Layer.launch(HttpServerLive) NodeRuntime.runMain(main)八、PubSub 事件广播一个生产者多个消费者当需要一个生产者向多个消费者扇出消息时使用PubSub示例见 10_pubsub.ts。文档示例构建了一个订单事件总线服务// 关键片段 const pubsub yield* PubSub.boundedOrderEvent({ capacity: 256, // 有界 背压不需要背压可用 PubSub.unbounded replay: 50 // 可选重放缓冲区让迟到订阅者补收最近的旧事件 }) yield* Effect.addFinalizer(() PubSub.shutdown(pubsub)) // 服务拆除时关闭 const subscribe Stream.fromPubSub(pubsub) // 把 PubSub 变成 Stream服务接口暴露publish、publishAll与只读的subscribe: Stream.StreamOrderEventStream.fromPubSub让每个订阅者收到订阅后发布的所有事件配置 replay 时还能收到订阅前的最近若干条。t3code 的 updates/DesktopUpdates.ts 同样用到了PubSub相关能力。九、Stream拉取式、可组合的数据流Effect Stream 表示基于 effect 的、拉取式的、随时间产生的值序列可建模有限或无限数据源。文档归纳了从常见数据源创建流的 API示例见 10_creating-streams.tsStream.fromIterable数组等可迭代对象Stream.fromEffectSchedule轮询 effect适用于指标、健康检查、缓存刷新示例用Schedule.spaced(30 seconds)配合Stream.take(3)Stream.paginate分页 API函数返回当前页值 可选下一页游标Stream.fromAsyncIterable异步可迭代对象Stream.fromEventListenerDOM 事件Stream.callback任意回调风格 APINodeStream.fromReadableNode.js readable 流。其余示例20_consuming-streams.ts 讲解map、flatMap、filter、mapEffect及各类run*消费方法30_encoding.ts 演示用Stream.pipeThroughChannel配合Ndjson与Msgpack模块对流式结构化数据编解码。十、集成既有应用ManagedRuntimeManagedRuntime是 Effect 程序与非 Effect 代码之间的桥梁从应用 Layer 构建一个runtime然后在任意需要命令式执行的地方使用——Web handler、框架钩子、worker 队列、遗留回调 API 均可见 10_managed-runtime.ts 的 Hono 示例TodoRepo服务 TodoSchema 模型 ManagedRuntime.make路由 handler 中runtime.runPromise(...)调用领域逻辑。这让你在既有框架中逐步引入 Effect同时保持领域逻辑全部收敛在 service 与 layer 中。十一、批量请求RequestResolver文档展示了如何把多个请求合并为更少的外部调用见 10_request-resolver.ts用Request.Class定义请求类型再用RequestResolver成批解析。这在高频查询外部 API 时能显著减少往返次数。十二、Schedule重试、重复与轮询的模式Schedule定义重试、重复与轮询的重复模式见 10_schedules.ts可组合后用于Effect.retry与Effect.repeat。t3code 中的真实用例httpReadiness.ts 用Schedule.spaced(Duration.millis(intervalMs)).pipe(Schedule.upTo({ times: Math.ceil(timeoutMs / intervalMs) }))构造每 100ms 探测一次、至多若干次的轮询策略——这正是Schedule模块在就绪探测场景的典型应用preview/Manager.ts 中也有Effect.retry({ times: 2 })等重试调用。十三、DateTime测试友好、类型安全的时间处理文档建议处理日期时间时使用DateTime模块替代Date与Date.now()当程序需要可测试的当前时间、安全解析、稳定的 ISO 格式化、时区转换、日历运算时优先使用它。10_creating-and-formatting.ts安全解析输入日期、使用 Clock 驱动的当前时间、为 API 载荷或用户界面格式化 instants20_time-zones.ts给 instants 附加 IANA 时区、渲染带时区的 ISO 字符串并提供CurrentTimeZone服务供需要工作区/用户时区的代码使用。十四、可观测性结构化日志、分布式追踪与指标Effect 内建对结构化日志、分布式追踪和指标的支持。导出遥测数据时文档建议新项目使用effect/unstable/observability下轻量的 Otlp 模块已有 OpenTelemetry 集成时使用effect/opentelemetry的NodeSdk。相关示例10_logging.ts 配置生产环境的 logger 与日志级别过滤20_otlp-tracing.ts 用可复用的 observability layer 配置 OTLP 追踪与日志导出。t3code 的落地情况与文档建议完全一致observability.ts 与 relayTracing.ts 均直接导入effect/unstable/observabilityOtlpResource、OtlpTracer、OtlpSerializationpreview/Manager.ts 与 BrowserSession.ts 使用Effect.withSpan(PreviewManager.make)自动创建追踪 spanDesktopBackendConfiguration.ts 与 preview/Manager.ts 使用Effect.annotateLogs({...})为日志附加结构化字段如tabId、webContentsId。十五、测试effect/vitest 与共享 Layer10_effect-tests.ts用it.effect编写基于 Effect 的测试20_layer-tests.ts测试依赖其他服务的 Effect 服务通过共享 layer 装配被测系统。t3code 中 Net.test.ts 直接从effect/vitest导入assert, describe, it并用Effect.callback包装 Node 网络 API 来构造测试服务器——与文档推荐的测试范式一致。十六、Predicate不要重复造轮子文档强调永远不要自己写isRecord、isString之类的辅助函数直接使用Predicate模块。谓词可通过Predicate.and、Predicate.or、Predicate.not、Predicate.compose组合import { Predicate } from effect const thing: unknown { a: 1 } if (Predicate.isObject(thing)) { if (Predicate.isNumber(thing.a)) { console.log(number, thing.a) } }t3code 中 Net.ts 用Predicate.isObject(cause) Predicate.hasProperty(cause, code) Predicate.isString(cause.code)收窄NodeJS.ErrnoException的code字段正是该模块的典型组合用法。十七、扩展主题速览SQL / HttpClient / HttpApi / 子进程 / CLI / AI / ClusterLLMS.md后半部分还覆盖了 Effect 生态的完整拼图每项都配有可运行的示例文件SQL40_sql用effect/unstable/sql配合驱动如effect/sql-sqlite-node用Model.Class为数据库与 JSON 边界派生 schema、运行迁移、写类型安全查询。t3code 的 nodeSqliteClient.ts 是这一方向在仓库内的完整实现HttpClient50_http-client定义一个服务用HttpClient模块请求外部 APIHttpApi51_http-serverschema-first 的类型安全 HTTP API——运行时校验、类型化客户端、OpenAPI 文档均从一份定义生成20_testing.ts 演示用HttpApiTest通过内存类型化客户端测试 handler无需起 HTTP 服务或连真实数据库子进程60_child-process用effect/unstable/process定义子进程并用ChildProcessSpawner运行收集输出、组合管道、流式读取长命令输出CLI70_cli用effect/unstable/cli解析命令行参数、处理用户输入、管理 CLI 流程把子命令 handler 装配成单一可执行命令AI 模块71_aiprovider 无关的语言模型接口——LanguageModel生成纯文本、用Schema解码结构化对象、流式输出10_language-model.ts用 schema 定义 AI 工具并分组为 toolkit20_tools.tsChat模块自动维护会话历史适合构建 agent 或聊天助手30_chat.ts。示例中还展示了ExecutionPlan定义先试便宜模型、失败回退昂贵模型的多 provider 策略Cluster80_cluster把有状态服务建模为实体并在多台机器间分布实现分布式应用。十八、结语把文档变成可复用的编码规范回顾.repos/effect-smol/LLMS.md的全文可以提炼出 t3code 项目团队在 Effect 使用上的几条铁律它们同样适用于任何采用 Effect 的新代码写法统一所有返回 Effect 的函数用Effect.fn(name)自动获得堆栈与 span流程用Effect.genyield*附加行为用组合子领域建模用 Schema拒绝手工解析与自定义谓词Schema.ClassSchema.TaggedError是唯一正道行为封装进 ServiceContext.Service 静态Layer配置项用Context.Reference用Layer.provide/provideMerge/unwrap组装错误类型化所有失败都是类型化、带 tag 的错误用catchTag/catchTags/catchReason恢复资源交给 ScopeacquireRelease管理生命周期后台任务用Layer.effectDiscbegin▁of▁sentencerd可观测性与测试是标配Effect.withSpan/annotateLogs从开发第一天就挂上测试用effect/vitest的it.effect。这些规则在 t3code 的packages/shared、apps/desktop等目录中均有大量真实代码可以对照学习建议在阅读本文后结合 LLMS.md 原文与 ai-docs/src 下的示例文件逐一验证从而把这套文档转化为团队内可落地的编码规范。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
