mcpField
E
effect-client-wrapper

effect-client-wrapper

github.com/rhyssullivan開発10439 回インストール16 週間前に更新
リポジトリ

effect-client-wrapper について

Effect Client Wrapper Patternは、EffectでサードパーティSDKのクライアントを包むTypeScript向けのパターンです。エラー処理、トレース、依存性注入を統一し、`use`インターフェースで生のクライアントを隠します。名前付き操作やリトライ方針の例も含みます。

主な機能

  1. 01クライアントエラーの一元化と型付け
  2. 02呼び出しごとのトレースspan作成
  3. 03Config.redactedによる秘密情報の読み込み
  4. 04Effect Contextによる依存性注入
  5. 05生のクライアントインターフェースの隠蔽
  6. 06名前付き操作とリトライへの対応

活用シーン

  1. 01サードパーティSDKのエラー処理を統一
  2. 02クライアント呼び出しにトレースを追加
  3. 03依存性注入でクライアントを管理
  4. 04クライアント操作にリトライ方針を設定

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/effect-client-wrapperを入力して読み込まれたか確認します。

詳細

Effect クライアントラッパーパターン

一貫したエラー処理、トレース、依存性注入のために、「use」パターンを使ってサードパーティーの SDK クライアントを Effect でラップします。

パターンの構造

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 呼び出しで関数名を含むスパンを作成します
  3. Config ベースのシークレット - API キーを Config.redacted で読み込みます
  4. クリーンな DI - 利用側は 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;
});

コメント