Effect service

Server 的功能都是 Effect service。呼叫它們的是各種傳輸層(transport):ws.ts 裡的 WebSocket RPC handler、HTTP route、MCP 工具、排程工作,以及 CLI。這一頁收錄撰寫 service 的規則。Effect Service Conventions 這個 review 檢查執行的是同一套規則;兩邊要保持一致。

功能該放在哪裡#

Server 的一項能力,就是它所屬領域資料夾(project/、workspace/、git/、provider/……)裡某個 service 上的一個方法。請擴充已經擁有該領域的 service;只有在沒有任何 service 擁有它時,才新增一個。

傳輸層的 handler 只做三件事:解碼請求、呼叫一個 service 方法、把 service 的具型別錯誤對應成該傳輸層的錯誤。除此之外什麼都不做。檔案系統、Git 或行程相關的工作、資料夾命名、多步驟的 dispatch、重試和 rollback,都屬於 service。

理由是「觸及範圍」。使用者從 WebSocket 觸發一項能力,agent 透過 MCP 工具使用它,排程工作和 CLI 也會執行它。寫在某一個 handler 裡的邏輯,其他入口就沒有,而且要測試它還需要一條 socket。純運算的工作(一個 slug、一張 SVG、一則訊息)用一般函式寫在 service 旁邊沒有問題;但能力本身就是那個方法。

// ws.ts: a thin handler
[WS_METHODS.projectsCreateNew]: (input) =>
  observeRpcEffect(
    WS_METHODS.projectsCreateNew,
    projectFolders.createNamedProject(input).pipe(
      Effect.mapError((cause) => new ProjectCreateNewError({ cause })),
    ),
    { "rpc.aggregate": "orchestration" },
  ),

Service 模組的樣子#

一個 service 一個模組,內容依照這個順序:import、錯誤與 schema、把介面直接寫在裡面的 Context.Service tag、make,最後是 layer。WorkspacePaths.ts 和 T3ProjectFileLoader.ts 是很好的參考。

export class FooWriteError extends Schema.TaggedError<FooWriteError>()("FooWriteError", {
  path: Schema.String,
  cause: Schema.Defect(),
}) {
  override get message(): string {
    return "Failed to write the foo file.";
  }
}

export class Foo extends Context.Service<
  Foo,
  { readonly write: (input: { readonly path: string }) => Effect.Effect<void, FooWriteError> }
>()("t3/area/Foo") {}

const make = Effect.gen(function* () {
  const fileSystem = yield* FileSystem.FileSystem;
  // ...
  return Foo.of({ write });
});

export const layer = Layer.effect(Foo, make);
  • Import。 使用端把模組當成一個 namespace 來用:import * as Foo from "./Foo.ts",然後寫 yield* Foo.Foo 和 Foo.layer。絕對不要寫 import { layer as fooLayer }。
  • 相依項目從 environment 取得(yield* FileSystem.FileSystem),絕對不要當成 make 的參數傳進來。
  • make 保持私有,除非有其他模組 import 它。Knip 遇到沒有被使用的 export 會讓 CI 失敗。
  • 錯誤是 Schema.TaggedError 類別,帶有結構化的屬性;包裝另一個失敗時要帶上 cause。訊息是固定的,或是由屬性組出來的,絕對不要從 cause 產生。在失敗發生的地方建立錯誤;只有在傳輸層才把它對應成傳輸層的錯誤。用 Effect.catchTags 捕捉已知的 tag。
  • 測試要透過 service 來驗證行為,只有外部相依項目才使用測試用的 layer。不要去 mock 正在被測試的邏輯。

Push 之前#

  • 你動過的 handler,有沒有哪一個做了解碼、呼叫、對應錯誤以外的事?
  • agent(MCP)或排程工作用得到這項能力嗎?如果用不到,那是刻意的嗎?
  • 新增 service 之前,你有沒有先擴充該領域既有的 service?
  • 你跑過 knip 了嗎?新的 export 沒有人 import 的話,它會失敗。

本頁譯自 docs/internals/effect-services.md(英文原文,版本 7ae4671)。標示「本站補充」的區塊不在原文裡。

非官方翻譯,與 T3 Code 的維護者無關。內容由 AI 翻譯並補充,未經逐頁人工校對,可能有錯誤或已經過時,請以英文原文為準。

原文 © T3 Tools Inc.,以 MIT 授權釋出;本站為其官方 repository 中 docs/ 的翻譯。本站原始碼與問題回報