@aws/durable-execution-sdk-js-eslint-plugin

中文文档
1.2.0发布于 2026/10/26Apache-2.0TypeScriptCommonJS零依赖构建来源已验证近 30 天下载 1.1万

ESLint plugin for AWS Lambda durable functions best practices

原始使用文档
文档目录18 节
  1. How replay works
  2. Requirements
  3. Installation
  4. Usage
  5. Choose the rules yourself
  6. Rule names in the recommended config
  7. Legacy eslintrc configuration
  8. Rules
  9. no-nested-durable-operations
  10. no-closure-in-durable-operations
  11. no-non-deterministic-outside-step
  12. require-durable-operation-in-child-context
  13. Why These Rules?
  14. No Non-Deterministic Code Outside a Step
  15. No Closure in Durable Operations
  16. No Nested Durable Operations
  17. Require a Durable Operation in a Child Context
  18. License

AWS Durable Execution ESLint Plugin

ESLint rules for AWS Lambda durable functions. They report code that breaks replay.

How replay works

A durable function works by replay. When an execution continues after a wait, a callback, a retry, or a failure, Lambda runs your handler again from the beginning. The SDK then walks through the code up to the point where the previous run stopped:

  • At a step that already finished, the SDK returns the step's saved result. It does not run the step's function again.
  • At the first operation that has not finished, the SDK runs it and saves its result.

So the code inside a step runs when the step executes, and the code outside the steps runs again on every replay.

The code outside a step must be deterministic. The SDK identifies each durable operation by its position: the first operation called on a context, the second, and so on. On replay it matches each call to the saved operation at the same position. Given the same saved results, your code must make the same calls in the same order. Anything that can give a different answer the second time belongs inside a step. That includes the current time, random numbers, UUIDs, and API calls whose results change.

Two rules in this plugin protect this requirement: no-non-deterministic-outside-step and no-closure-in-durable-operations. A third rule, no-nested-durable-operations, checks how durable operations are nested. A fourth rule, require-durable-operation-in-child-context, reports a child context that does no durable work. It is off by default.

Requirements

  • ESLint 8.57 or later, 9, or 10, using the flat config format (eslint.config.js). CI tests ESLint 8.57, the latest 9, and 10. @typescript-eslint/parser 8 needs the same ESLint versions.
  • Node.js 22 or later.
  • A parser for TypeScript files, such as @typescript-eslint/parser.

The plugin does not support the legacy eslintrc format. See Legacy eslintrc configuration.

Installation

npm install --save-dev @aws/durable-execution-sdk-js-eslint-plugin @typescript-eslint/parser

@typescript-eslint/parser lets ESLint read TypeScript. A JavaScript-only project does not need it. Remove the languageOptions.parser lines from the examples below.

Usage

Add the recommended config to your eslint.config.js. It enables the three rules that report code that breaks replay, as errors.

// eslint.config.js
import typescriptParser from "@typescript-eslint/parser";
import durableExecution from "@aws/durable-execution-sdk-js-eslint-plugin";

export default [
  {
    files: ["**/*.ts"],
    languageOptions: { parser: typescriptParser },
  },
  durableExecution.configs["flat/recommended"],
];

In a CommonJS project, use require instead:

// eslint.config.js
const typescriptParser = require("@typescript-eslint/parser");
const durableExecution = require("@aws/durable-execution-sdk-js-eslint-plugin");

module.exports = [
  {
    files: ["**/*.ts"],
    languageOptions: { parser: typescriptParser },
  },
  durableExecution.configs["flat/recommended"],
];

The recommended config has no files setting, so it applies to every file ESLint checks. To limit it to your durable functions, spread it into your own object:

{
  ...durableExecution.configs["flat/recommended"],
  files: ["src/handlers/**/*.ts"],
}

ESLint 8.57 uses flat config when it finds an eslint.config.js or eslint.config.mjs file. ESLint 9 and 10 always use it.

Choose the rules yourself

Register the plugin under a name you pick, then turn on the rules you want. This example uses the name durable:

// eslint.config.js
import durableExecution from "@aws/durable-execution-sdk-js-eslint-plugin";

export default [
  {
    files: ["**/*.ts"],
    plugins: { durable: durableExecution },
    rules: {
      "durable/no-nested-durable-operations": "error",
      "durable/no-closure-in-durable-operations": "error",
      "durable/no-non-deterministic-outside-step": "warn",
      "durable/require-durable-operation-in-child-context": "warn",
    },
    // languageOptions: { parser: ... } for TypeScript, as above
  },
];

The recommended config registers the plugin as @aws/durable-functions. ESLint shows that name in its output, and you use it in eslint-disable comments:

// eslint-disable-next-line @aws/durable-functions/no-nested-durable-operations

Legacy eslintrc configuration

The plugin cannot be loaded from an eslintrc file such as .eslintrc.json. ESLint adds the prefix eslint-plugin- to the name of a plugin in an eslintrc file and then looks for that package. For this package it looks for @aws/eslint-plugin-durable-execution-sdk-js-eslint-plugin, which does not exist. ESLint 10 removed eslintrc.

Earlier versions of this README showed extends: ["plugin:@aws/durable-execution-sdk-js-eslint-plugin/recommended"]. That setup never worked. If you have a project that still uses eslintrc, move it to flat config, or follow ESLint's migration guide.

The exported configs.recommended is the legacy form of the config. It is deprecated and remains only so that code which reads its rules keeps working.

Rules

The first three rules are in flat/recommended as errors. require-durable-operation-in-child-context is not, because the code it reports still replays correctly. To use it, turn it on as shown in Choose the rules yourself.

RuleWhat it reports
no-nested-durable-operationsA durable operation started on the context of the operation it runs in
no-closure-in-durable-operationsA variable declared outside a step that the step changes
no-non-deterministic-outside-stepNon-deterministic code outside a step, such as the time and random values
require-durable-operation-in-child-context (not in recommended)A child context whose function calls no durable operation on its context

no-nested-durable-operations

Prevents using the same context object inside its own durable operation.

A step is one unit of work, and it cannot contain other durable operations. To group operations, run them in a child context with runInChildContext, and call them on the child context.

Checked operations: step, wait, waitForCallback, waitForCondition, parallel, map and runInChildContext.

❌ Incorrect

const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    return await context.step("step1", async () => {
      // Error: Cannot use the same context object inside its own operation
      await context.wait({ seconds: 1 });

      const result = await context.step("step2", async () => {
        return "nested step";
      });

      return result;
    });
  },
);

✅ Correct

// Use runInChildContext for nested operations
const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    return await context.runInChildContext("block1", async (childCtx) => {
      await childCtx.wait({ seconds: 1 });

      const result = await childCtx.step("step2", async () => {
        return "nested step";
      });

      return result;
    });
  },
);

Known limitation

The rule finds the context by looking for the parameter of an arrow function or a function expression, or for a variable declared in the file. It does not recognize a context that is the parameter of a function declaration, such as async function handler(event, context) { ... }, so it reports nothing there. Write the handler as an arrow function, as in the examples above, to get the check.

no-closure-in-durable-operations

Prevents modifying closure variables inside durable operations. Reading closure variables is allowed.

The function of a step does not run again on replay, so a change it makes to a variable outside the step does not happen on replay. The code after the step then sees a different value than it saw on the first run, which makes that code non-deterministic. To pass a value out of a step, return it from the step.

Checked operations: step, runInChildContext, waitForCondition and waitForCallback.

❌ Incorrect

const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    let a = 2;
    const result = await context.runInChildContext(
      async (childContext: DurableContext) => {
        const stepResult = await childContext.step(async () => {
          // Error: this step does not run again on replay, so this change to 'a' is lost
          a = a + 1;
          return "child step completed";
        });
        return stepResult;
      },
    );
    return result;
  },
);

✅ Correct

const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    let a = 2;
    const result = await context.runInChildContext(
      async (childContext: DurableContext) => {
        const stepResult = await childContext.step(async () => {
          // Reading 'a' is OK
          const value = a + 1;
          return value;
        });
        return stepResult;
      },
    );
    return result;
  },
);

// Or use local variables
const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    const result = await context.runInChildContext(
      async (childContext: DurableContext) => {
        const stepResult = await childContext.step(async () => {
          let a = 2;
          a = a + 1;
          return "child step completed";
        });
        return stepResult;
      },
    );
    return result;
  },
);

no-non-deterministic-outside-step

Requires non-deterministic code to run inside a step. Code outside a step runs again on every replay, so it has to give the same answer each time. A step's result is saved, so a replay reads the saved value instead of running the call again.

The rule reports these calls when they are written outside a step callback:

  • Math.random()
  • Date.now() and new Date() with no arguments
  • performance.now()
  • crypto.randomBytes() and crypto.getRandomValues()
  • Any call whose name contains uuid, such as crypto.randomUUID() or uuidv4()

The rule checks where the call is written. That includes the body of a helper function that you define outside the step. It also reports a call to a helper that contains one of these calls, directly or through other functions, when the call to the helper is outside a step.

❌ Incorrect

const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Error: this code runs again on every replay, and both values change
    const orderId = crypto.randomUUID();
    const startedAt = Date.now();

    await context.step("charge", async () => chargeCard(orderId));
    return { orderId, startedAt };
  },
);

✅ Correct

const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // The step saves these values, so a replay returns the same ones
    const { orderId, startedAt } = await context.step(
      "create-order",
      async () => ({ orderId: crypto.randomUUID(), startedAt: Date.now() }),
    );

    await context.step("charge", async () => chargeCard(orderId));
    return { orderId, startedAt };
  },
);

require-durable-operation-in-child-context

Requires a child context to perform at least one durable operation on its context.

A child context is the context that runInChildContext passes to its function. map passes one to the function of each item, and parallel passes one to each branch. A child context saves the result of its function when the function finishes. A replay returns that saved result and does not call the function again. So a function that calls no durable operation on its context, such as step, invoke or wait, still replays correctly. It differs from a step in two ways:

  • A step retries its work when it throws. The default retry strategy makes up to six attempts. A child context fails at the first error.
  • A result larger than the checkpoint size limit does not fit in the child context's checkpoint. Then the SDK runs the function again on every replay to rebuild the result.

So the rule tells you to put the work in a step.

The rule reports these functions:

  • the function passed to runInChildContext
  • the function passed to map
  • each branch passed to parallel, as a function or as { name, func }

❌ Incorrect

const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Error: no durable operation runs on ctx
    await context.runInChildContext("transform", async (ctx) =>
      cheapTransform(event),
    );

    // Error: the same for each item
    await context.map("transform", event.items, async (ctx, item) =>
      cheapTransform(item),
    );
  },
);

✅ Correct

const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // A step saves the result, and it retries the work if it throws
    await context.step("transform", async () => cheapTransform(event));

    // A child context that contains a durable operation
    await context.map("transform", event.items, async (ctx, item) =>
      ctx.step("compute", async () => cheapTransform(item)),
    );
  },
);

How the rule follows the context

The rule follows the context into a function declared in the same file. It does this when the code passes the context as an argument, such as charge(ctx), and the function is declared as function charge() {} or const charge = () => {}. The rule does not follow a function declaration that the file assigns again.

The rule reports nothing when the context reaches code it cannot read. Examples are an imported function, a method call such as service.run(ctx), a return value, an object property, a rest parameter, a spread argument before the context, and arguments. That code may perform a durable operation.

Known limitations

map and parallel are common method names. Lodash has _.map, for example. So the rule checks a map or parallel call only when it knows the receiver is a durable context. The receiver must be a parameter that is one of these:

  • annotated with the type DurableContext, including an import alias such as import type { DurableContext as Ctx }
  • the second parameter of the function passed to withDurableExecution, inline or by name
  • the context parameter of a runInChildContext, map or parallel function

The rule checks runInChildContext on any receiver, because the name is specific to the SDK.

Why These Rules?

Replay runs the code outside the steps once on the first run, and again on every replay. The SDK can resume an execution correctly only if each run makes the same durable calls in the same order. The first two rules below protect that requirement. The third protects the structure of nested operations.

No Non-Deterministic Code Outside a Step

Code that reads the clock or generates a random value gives a different answer on each run. If it runs outside a step, a replay can compute a different value than the first run did. A later if statement can then take another branch, and the code makes different durable calls than the ones the SDK saved. Wrapping the call in a step saves its result, so every replay reads the same value.

No Closure in Durable Operations

A step's function does not run again on replay. If it changes a variable outside the step, the first run sees the change and a replay does not. The code after the step then works with different values on the two runs. Return the value from the step instead, because the SDK saves what a step returns.

Reading closure variables is safe because it doesn't change state. Only mutations (assignments, increments, decrements) are problematic.

No Nested Durable Operations

A durable operation that uses the same context as the operation it runs in can cause runtime errors and unexpected behavior. The SDK expects grouped work to run in a child context. This rule helps catch these issues at development time.

Require a Durable Operation in a Child Context

A child context groups durable operations. A child context with no durable operation still saves its result, so it replays correctly. That is why the rule is not in the recommended config. A step does the same work with retries, so the rule points to a step.

License

Apache-2.0

版本与发布记录

@aws/durable-execution-sdk-js-eslint-plugin 共发布 6 个版本,首次发布于 2025/11/28;过去 12 个月发布了 6 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.2.0latest2026/10/2612 天前
1.1.12026/08/152 个月前
1.1.02026/08/142 个月前
1.0.12026/08/112 个月前
1.0.02025/12/0211 个月前
0.0.12025/11/2811 个月前

关于 @aws/durable-execution-sdk-js-eslint-plugin 的常见问题

@aws/durable-execution-sdk-js-eslint-plugin 是什么?

ESLint plugin for AWS Lambda durable functions best practices

如何安装 @aws/durable-execution-sdk-js-eslint-plugin?

在项目目录运行 npm install @aws/durable-execution-sdk-js-eslint-plugin,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:eslint(^8.57.0 || ^9.0.0 || ^10.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@aws/durable-execution-sdk-js-eslint-plugin 支持 TypeScript 吗?

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

@aws/durable-execution-sdk-js-eslint-plugin 支持 ESM 还是 CommonJS?

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

@aws/durable-execution-sdk-js-eslint-plugin 需要什么版本的 Node.js?

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

@aws/durable-execution-sdk-js-eslint-plugin 可以商用吗?

@aws/durable-execution-sdk-js-eslint-plugin 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@aws/durable-execution-sdk-js-eslint-plugin 的下载量有多少?

根据 npm 官方下载统计,@aws/durable-execution-sdk-js-eslint-plugin 在 2026/10/08 – 2026/11/06 这 30 天内被下载 11,128 次,日均约 371 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@aws/durable-execution-sdk-js-eslint-plugin 还在维护吗?

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