@blade47/saga-ts

中文文档
1.0.4发布于 2025/12/09MITTypeScriptCommonJS零依赖近 30 天下载 4894

A lightweight, type-safe saga pattern implementation for TypeScript with automatic rollback support

原始使用文档
文档目录23 节
  1. Features
  2. Installation
  3. Quick Start
  4. API Reference
  5. createSaga()
  6. .step(name, input, execute, rollback?)
  7. .run()
  8. Examples
  9. Payment Processing with Rollback
  10. Complex User Onboarding
  11. How It Works
  12. Type Safety
  13. Error Handling Best Practices
  14. Known Limitations
  15. Sequential Execution Only
  16. No Built-in Retry Logic
  17. Limited Rollback Observability
  18. In-Memory Only
  19. No Conditional Logic
  20. No Timeout or Cancellation
  21. When to Use Sagas
  22. License
  23. Contributing

saga-ts

A lightweight, type-safe saga pattern implementation for TypeScript with automatic rollback support.

Features

  • Type-safe: Full TypeScript support with intelligent type inference
  • Automatic rollback: Failed steps trigger automatic compensation in reverse order
  • Zero dependencies: Lightweight and production-ready
  • Simple API: Intuitive builder pattern for defining sagas
  • Flexible: Works with any async operations (database, API calls, etc.)

Installation

npm install saga-ts

Quick Start

import { createSaga } from 'saga-ts';

const result = await createSaga()
  .step(
    'createUser',
    { email: '[email protected]', name: 'John' },
    async (input, ctx) => {
      const user = await db.users.create(input);
      return { userId: user.id, email: input.email };
    },
    async (output) => {
      // Rollback: delete the user if something fails later
      await db.users.delete(output.userId);
    }
  )
  .step(
    'sendEmail',
    { template: 'welcome' },
    async (input, ctx) => {
      // Access previous step results with full type safety
      const { email } = ctx.results.createUser;

      const emailId = await emailService.send({
        to: email,
        template: input.template,
      });
      return { emailId, sentAt: new Date() };
    },
    async (output) => {
      await emailService.cancel(output.emailId);
    }
  )
  .run();

if (result.status === 'success') {
  console.log('User created:', result.results.createUser.userId);
  console.log('Email sent:', result.results.sendEmail.emailId);
} else {
  console.error('Failed at step:', result.failedAt);
  console.error('Error:', result.error);
}

API Reference

createSaga()

Creates a new saga builder instance.

.step(name, input, execute, rollback?)

Adds a step to the saga.

  • name: Unique identifier for the step (used for accessing results)
  • input: Input data for the step
  • execute: Async function that performs the step's action
    • Receives input and ctx (containing results from previous steps)
    • Must return the step's output
  • rollback (optional): Async function to undo the step's action
    • Receives the step's output
    • Called automatically if a later step fails

.run()

Executes the saga and returns a promise with the result.

Success result:

{
  status: 'success',
  data: TLastOutput,      // Output of the last step
  results: TResults       // All step results by name
}

Failure result:

{
  status: 'failed',
  error: Error,           // The error that occurred
  failedAt: string        // Name of the step that failed
}

Examples

Payment Processing with Rollback

const result = await createSaga()
  .step(
    'validateCard',
    { cardToken: 'tok_visa' },
    async (input) => {
      const card = await stripe.tokens.retrieve(input.cardToken);
      return { cardId: card.id, last4: card.card.last4 };
    }
  )
  .step(
    'calculateTotal',
    { items: [{ id: '1', price: 100 }, { id: '2', price: 50 }] },
    async (input) => {
      const total = input.items.reduce((sum, item) => sum + item.price, 0);
      return { total, items: input.items };
    }
  )
  .step(
    'createCharge',
    { currency: 'usd' },
    async (input, ctx) => {
      const charge = await stripe.charges.create({
        amount: ctx.results.calculateTotal.total,
        currency: input.currency,
        source: ctx.results.validateCard.cardId,
      });
      return { chargeId: charge.id };
    },
    async (output) => {
      // Rollback: refund the charge
      await stripe.refunds.create({ charge: output.chargeId });
    }
  )
  .step(
    'sendReceipt',
    { recipientEmail: '[email protected]' },
    async (input, ctx) => {
      await emailService.send({
        to: input.recipientEmail,
        template: 'receipt',
        data: {
          chargeId: ctx.results.createCharge.chargeId,
          amount: ctx.results.calculateTotal.total,
        },
      });
      return { sent: true };
    }
  )
  .run();

// If sendReceipt fails, the charge will be automatically refunded

Complex User Onboarding

const result = await createSaga()
  .step(
    'createAccount',
    { email: '[email protected]', password: 'secure123' },
    async (input) => {
      const account = await db.accounts.create(input);
      return { accountId: account.id };
    },
    async (output) => {
      await db.accounts.delete(output.accountId);
    }
  )
  .step(
    'createProfile',
    { name: 'John Doe', avatar: 'default.png' },
    async (input, ctx) => {
      const profile = await db.profiles.create({
        ...input,
        accountId: ctx.results.createAccount.accountId,
      });
      return { profileId: profile.id };
    },
    async (output) => {
      await db.profiles.delete(output.profileId);
    }
  )
  .step(
    'assignRole',
    { role: 'user' },
    async (input, ctx) => {
      await db.roles.assign({
        accountId: ctx.results.createAccount.accountId,
        role: input.role,
      });
      return { role: input.role };
    },
    async (output, ctx) => {
      await db.roles.revoke({
        accountId: ctx.results.createAccount.accountId,
        role: output.role,
      });
    }
  )
  .step(
    'sendWelcomeEmail',
    { template: 'onboarding' },
    async (input, ctx) => {
      const emailId = await emailService.send({
        to: ctx.results.createAccount.email,
        template: input.template,
      });
      return { emailId };
    }
  )
  .run();

How It Works

  1. Step Execution: Steps are executed sequentially in the order they're defined
  2. Context Propagation: Each step receives results from all previous steps via ctx.results
  3. Type Safety: TypeScript automatically infers and validates the types of step inputs and outputs
  4. Error Handling: If any step throws an error, execution stops immediately
  5. Automatic Rollback: Compensation functions are called in reverse order for all successfully executed steps
  6. Rollback Resilience: If a rollback fails, an error is logged but other rollbacks continue

Type Safety

saga-ts provides full type inference:

const result = await createSaga()
  .step('step1', { value: 10 }, async (input) => {
    return { doubled: input.value * 2 };
  })
  .step('step2', { multiplier: 3 }, async (input, ctx) => {
    // TypeScript knows ctx.results.step1.doubled is a number
    const result = ctx.results.step1.doubled * input.multiplier;
    return { final: result };
  })
  .run();

if (result.status === 'success') {
  // TypeScript knows the exact shape of results
  const doubled: number = result.results.step1.doubled;
  const final: number = result.results.step2.final;
}

Error Handling Best Practices

  1. Always provide rollback functions for steps that modify state
  2. Keep rollbacks idempotent - they may be called multiple times
  3. Log rollback failures - the library logs them but continues
  4. Test your rollback logic - ensure compensations work correctly

Known Limitations

This library is designed for simplicity and type safety. For more complex orchestration needs, consider these limitations:

Sequential Execution Only

Steps execute one at a time in order. If you have independent steps that could run in parallel, they will still wait for each other to complete.

// These steps run sequentially even though they're independent
.step('fetchUserProfile', {}, async () => { /* ... */ })
.step('fetchUserPreferences', {}, async () => { /* ... */ })

Workaround: Run independent sagas concurrently using Promise.all().

No Built-in Retry Logic

If a step fails due to transient issues (network timeout, rate limiting), the entire saga fails and rolls back. There's no automatic retry with exponential backoff.

Workaround: Implement retry logic inside your step functions or wrap the saga execution in a retry handler.

Limited Rollback Observability

When rollback functions fail, errors are logged to console.error but not included in the saga result. You won't know if compensation partially failed.

Workaround: Implement your own error tracking inside rollback functions if you need detailed compensation audit logs.

In-Memory Only

All step results are stored in memory during execution. For long-running sagas or steps that return large payloads, this could cause memory issues. There's no persistence layer.

Implication: If your process crashes mid-saga, there's no way to resume. For critical workflows, consider workflow engines like Temporal or Conductor.

No Conditional Logic

Every step runs unless a previous step fails. You can't skip steps based on conditions.

// Can't do: "if user is premium, skip payment step"

Workaround: Use conditional logic inside step functions to return early, or split into separate sagas.

No Timeout or Cancellation

Steps can run indefinitely. There's no built-in timeout mechanism or AbortSignal support.

Workaround: Implement timeouts within your step functions using Promise.race() or AbortController.

When to Use Sagas

Sagas are ideal for:

  • Multi-step business processes that need to be atomic
  • Distributed transactions across services
  • Complex workflows with compensation logic
  • Operations that need to maintain consistency across failures

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

版本与发布记录

@blade47/saga-ts 共发布 5 个版本,首次发布于 2025/11/18;过去 12 个月发布了 5 个版本。

版本发布日期距今
1.0.4latest2025/12/0910 个月前
1.0.32025/11/1811 个月前
1.0.22025/11/1811 个月前
1.0.12025/11/1811 个月前
1.0.02025/11/1811 个月前

关于 @blade47/saga-ts 的常见问题

@blade47/saga-ts 是什么?

A lightweight, type-safe saga pattern implementation for TypeScript with automatic rollback support

如何安装 @blade47/saga-ts?

在项目目录运行 npm install @blade47/saga-ts,或使用 pnpm add / yarn add / bun add。

@blade47/saga-ts 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@blade47/saga-ts 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@blade47/saga-ts 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=16.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@blade47/saga-ts 可以商用吗?

@blade47/saga-ts 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@blade47/saga-ts 的下载量有多少?

根据 npm 官方下载统计,@blade47/saga-ts 在 2026/10/09 – 2026/11/07 这 30 天内被下载 4,894 次,日均约 163 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@blade47/saga-ts 还在维护吗?

最新版本 1.0.4 发布于 2025/12/09(10 个月前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2025/11/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。