Effect Client Wrapper 是一种把第三方 SDK 客户端(例如 Stripe、Resend、AWS 等)封装成 Effect 服务的模式。它适合在需要创建 Effect 服务、并对外部库做统一包装时使用,核心是引入 use 方法,把外部 SDK 的调用统一收口,从而获得一致的错误处理、链路追踪和依赖注入能力。
用途
该模式的目标是让外部 SDK 的调用具备以下统一特性:集中化的错误处理、自动化的 tracing、基于 Config 的密钥读取,以及干净的依赖注入。通过把原始客户端隐藏在 use 接口之后,调用方无需直接接触第三方客户端实例。
模式结构
模式由四步组成。
- 定义带标签的错误类型,例如:
export class MyClientError extends Data.TaggedError("MyClientError")<{
cause: unknown;
}> {}
export class MyClientInstantiationError extends Data.TaggedError(
"MyClientInstantiationError"
)<{
cause: unknown;
}> {}
- 定义包含 use 方法的服务接口:
export type IMyClient = Readonly<{
client: ThirdPartyClient;
use: <A>(
fn: (client: ThirdPartyClient) => Promise<A>
) => Effect.Effect<A, MyClientError, never>;
}>;
- 创建服务实现,通过
Config.redacted读取 API Key,用Effect.try实例化客户端,用Effect.tryPromise包装调用并附加 span:
const make = Effect.gen(function* () {
const apiKey = yield* Config.redacted("MY_CLIENT_API_KEY");
const client = yield* Effect.try({
try: () => new ThirdPartyClient(Redacted.value(apiKey)),
catch: (cause) => new MyClientInstantiationError({ cause }),
});
const use = <A>(fn: (client: ThirdPartyClient) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => new MyClientError({ cause }),
}).pipe(Effect.withSpan(`my_client.${fn.name ?? "use"}`));
return { client, use };
});
- 以
Context.Tag加 Default layer 的形式导出:
export class MyClient extends Context.Tag("MyClient")<MyClient, IMyClient>() {
static Default = Layer.effect(this, make).pipe(
Layer.annotateSpans({ module: "MyClient" })
);
}
使用方式
在 Effect 程序里通过 yield* MyClient 注入服务,并调用 use:
const program = Effect.gen(function* () {
const myClient = yield* MyClient;
const result = yield* myClient.use((client) =>
client.someMethod({ param: "value" })
);
return result;
});
program.pipe(Effect.provide(MyClient.Default));
主要能力与优势
- 集中式错误处理:所有客户端错误都被包装为带类型的 MyClientError。
- 自动 tracing:每次 use 调用都会以函数名创建 span。
- 基于配置的密钥:API Key 通过 Config.redacted 加载。
- 干净的依赖注入:使用方通过 yield* MyClient 注入。
- 封装性:原始客户端被隐藏在 use 接口之后。
变体
多种错误类型
可以定义多个 TaggedError,并在 use 中根据原因分类返回,例如网络错误映射为 MyClientNetworkError,其余映射为 MyClientError:
export class MyClientNetworkError extends Data.TaggedError("MyClientNetworkError")<{
cause: unknown;
}> {}
export class MyClientValidationError extends Data.TaggedError("MyClientValidationError")<{
message: string;
}> {}
const use = <A>(fn: (client: ThirdPartyClient) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => {
if (cause instanceof NetworkError) {
return new MyClientNetworkError({ cause });
}
return new MyClientError({ cause });
},
}).pipe(Effect.withSpan(`my_client.${fn.name ?? "use"}`));
具名操作
也可以不暴露通用 use,而是暴露具体方法,例如 sendEmail 和 getEmail,并在内部复用其他服务:
export type IEmailClient = Readonly<{
sendEmail: (params: SendEmailParams) => Effect.Effect<EmailResult, EmailError>;
getEmail: (id: string) => Effect.Effect<Email, EmailError>;
}>;
const make = Effect.gen(function* () {
const resend = yield* ResendClient;
return {
sendEmail: (params) =>
resend
.use((client) => client.emails.send(params))
.pipe(Effect.withSpan("email_client.send")),
getEmail: (id) =>
resend
.use((client) => client.emails.get(id))
.pipe(Effect.withSpan("email_client.get")),
};
});
带重试策略
可以为 use 添加重试:
import { Schedule } from "effect";
const retryPolicy = Schedule.exponential(100).pipe(
Schedule.intersect(Schedule.recurs(3)),
Schedule.jittered
);
const use = <A>(fn: (client: ThirdPartyClient) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => new MyClientError({ cause }),
}).pipe(
Effect.retry(retryPolicy),
Effect.withSpan(`my_client.${fn.name ?? "use"}`)
);
真实示例:Stripe
import Stripe from "stripe";
import { Context, Data, Effect, Layer, Config, Redacted } from "effect";
export class StripeError extends Data.TaggedError("StripeError")<{
cause: unknown;
}> {}
export type IStripeClient = Readonly<{
use: <A>(fn: (stripe: Stripe) => Promise<A>) => Effect.Effect<A, StripeError>;
}>;
const make = Effect.gen(function* () {
const secretKey = yield* Config.redacted("STRIPE_SECRET_KEY");
const client = new Stripe(Redacted.value(secretKey));
const use = <A>(fn: (stripe: Stripe) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => new StripeError({ cause }),
}).pipe(Effect.withSpan(`stripe.${fn.name ?? "use"}`));
return { use };
});
export class StripeClient extends Context.Tag("StripeClient")<
StripeClient,
IStripeClient
>() {
static Default = Layer.effect(this, make).pipe(
Layer.annotateSpans({ module: "StripeClient" })
);
}
const createCustomer = Effect.gen(function* () {
const stripe = yield* StripeClient;
const customer = yield* stripe.use((client) =>
client.customers.create({ email: "user@example.com" })
);
return customer;
});
适用场景
适用于在 Effect 应用中需要接入第三方 SDK 的场景,例如支付(Stripe)、邮件(Resend)、云服务(AWS)等。当希望统一错误类型、统一 tracing、集中管理密钥并避免在业务代码里直接实例化外部客户端时,可以采用此模式。
限制
本资料未提供关于该模式已知限制、版本兼容性要求或性能开销的说明,暂未提供相关信息。