mcpField
E
effect-client-wrapper

effect-client-wrapper

github.com/rhyssullivan开发1469 次安装38 周前更新
代码仓库

关于 effect-client-wrapper

一种用 Effect 封装第三方 SDK 客户端的 TypeScript 模式,面向需要统一错误处理、追踪、配置和依赖注入的开发者。

主要功能

  1. 01带类型的客户端错误
  2. 02为每次 use 调用创建追踪
  3. 03通过 Redacted 配置 API 密钥
  4. 04依赖注入
  5. 05封装底层客户端访问

适用场景

  1. 01统一 SDK 调用的错误处理
  2. 02追踪客户端操作
  3. 03通过配置加载 API 密钥

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rhyssullivan/effect-client-wrapper-skill --skill effect-client-wrapper

安装后在 Claude Code 中输入/effect-client-wrapper确认已加载。

详细介绍

Effect 客户端封装模式

使用 Effect 的“use”模式封装第三方 SDK 客户端,实现一致的错误处理、跟踪和依赖注入。

模式结构

import { Context, Data, Effect, Layer, Config, Redacted } from "effect";

// 1. Define tagged errors
export class MyClientError extends Data.TaggedError("MyClientError")<{
  cause: unknown;
}> {}

export class MyClientInstantiationError extends Data.TaggedError(
  "MyClientInstantiationError"
)<{
  cause: unknown;
}> {}

// 2. Define service interface with `use` method
export type IMyClient = Readonly<{
  client: ThirdPartyClient;
  use: <A>(
    fn: (client: ThirdPartyClient) => Promise<A>
  ) => Effect.Effect<A, MyClientError, never>;
}>;

// 3. Create the service implementation
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 };
});

// 4. Export as Context.Tag with Default layer
export class MyClient extends Context.Tag("MyClient")<MyClient, IMyClient>() {
  static Default = Layer.effect(this, make).pipe(
    Layer.annotateSpans({ module: "MyClient" })
  );
}

用法

const program = Effect.gen(function* () {
  const myClient = yield* MyClient;

  const result = yield* myClient.use((client) =>
    client.someMethod({ param: "value" })
  );

  return result;
});

// Run with layer
program.pipe(Effect.provide(MyClient.Default));

主要优势

  1. 集中式错误处理 - 所有客户端错误都封装为类型化的 MyClientError
  2. 自动跟踪 - 每次 use 调用都会创建一个包含函数名称的 span
  3. 基于配置的密钥 - 通过 Config.redacted 加载 API 密钥
  4. 简洁的依赖注入 - 消费者通过 yield* MyClient 注入
  5. 封装 - 原始客户端隐藏在 use 接口之后

变体

多种错误类型
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:

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")),
  };
});
使用重试策略
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" })
  );
}

// Usage
const createCustomer = Effect.gen(function* () {
  const stripe = yield* StripeClient;

  const customer = yield* stripe.use((client) =>
    client.customers.create({ email: "[email protected]" })
  );

  return customer;
});

评论

支持 Markdown