SuperAPI AI 工具集

effect-client-wrapper

编程与开发

查看项目

更新于 10-16 02:11

技能介绍

Effect Client Wrapper 是一种把第三方 SDK 客户端(例如 Stripe、Resend、AWS 等)封装成 Effect 服务的模式。它适合在需要创建 Effect 服务、并对外部库做统一包装时使用,核心是引入 use 方法,把外部 SDK 的调用统一收口,从而获得一致的错误处理、链路追踪和依赖注入能力。

用途

该模式的目标是让外部 SDK 的调用具备以下统一特性:集中化的错误处理、自动化的 tracing、基于 Config 的密钥读取,以及干净的依赖注入。通过把原始客户端隐藏在 use 接口之后,调用方无需直接接触第三方客户端实例。

模式结构

模式由四步组成。

  1. 定义带标签的错误类型,例如:
export class MyClientError extends Data.TaggedError("MyClientError")<{
  cause: unknown;
}> {}

export class MyClientInstantiationError extends Data.TaggedError(
  "MyClientInstantiationError"
)<{
  cause: unknown;
}> {}
  1. 定义包含 use 方法的服务接口:
export type IMyClient = Readonly<{
  client: ThirdPartyClient;
  use: <A>(
    fn: (client: ThirdPartyClient) => Promise<A>
  ) => Effect.Effect<A, MyClientError, never>;
}>;
  1. 创建服务实现,通过 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 };
});
  1. 以 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、集中管理密钥并避免在业务代码里直接实例化外部客户端时,可以采用此模式。

限制

本资料未提供关于该模式已知限制、版本兼容性要求或性能开销的说明,暂未提供相关信息。