@durability/transforms

中文文档
0.6.1发布于 2026/11/10ISCTypeScriptES Module构建来源已验证近 30 天下载 1.2万

Typed caller and callee transforms for Cloudflare RPC

原始使用文档
文档目录6 节
  1. Install
  2. Vite setup
  3. Restore broken Durable Object stubs
  4. Define and install a transform
  5. Built-in caller transforms
  6. Manual wrapping

@durability/transforms

Typed caller and callee transforms for Cloudflare Durable Object and Worker service-binding RPC.

Install

npm install @durability/transforms

Vite setup

The Vite plugin requires Wrangler, reads its validated binding configuration, wraps Durable Object stubs and every other object binding (services, KV, R2, Workflows, queues, and so on), and generates declarations that add .with(...) to their Wrangler-inferred types. Vars, secrets, and module blobs are left untouched. Bindings whose type Wrangler cannot infer, such as cross-Worker services, are wrapped at runtime but keep their hand-written declarations.

const value = await env.CACHE.with(timeout, 1_000).get('key');
await env.MEDIA.with(retry, 2).put('file.txt', body);
const instance = await env.JOBS.with(timeout, 5_000).create({ params });

Caller transforms run on the client only. Callee transforms and caller context still require a Durable Object or WorkerEntrypoint that you own.

Without transforms, wrapped bindings behave exactly like the originals, including synchronous methods and RPC promise pipelining. With transforms, every call returns a promise that still supports pipelined access such as stub.with(timeout, 1_000).counter().increment(); the chained call waits for the transformed result instead of sharing its round trip. Methods on returned objects, such as a WorkflowInstance, are not transformed.

For DurabilityRouting.client({...}) and DurabilitySemaphore.client({...}), the plugin injects exportName from the targeted Durable Object's same-module export. The target must be a local class exported by that module and declared as a local Durable Object class in Wrangler (including migration classes). Export aliases are supported:

import { DurableObject } from 'cloudflare:workers';
import { DurabilityRouting as Routing } from 'durability/routing';

class InternalMailbox extends DurableObject {
  enqueue(input: string) {}
}
export { InternalMailbox as Mailbox };

const router = Routing.client({
  target: InternalMailbox,
  invoke: (stub, input) => stub.enqueue(input),
  ...defaults,
});

The resulting exportName is appended after the other properties, so a spread cannot override it. Lookup is intentionally limited to class declarations and exports visible in the same Worker module; imported classes, re-exports, dynamic targets, and non-inline configs are left alone or rejected when the target is visible. Set exportName explicitly as the escape hatch for those cases. Explicit exportName values are never changed.

import { defineConfig } from 'vite';
import { doTransforms } from '@durability/transforms/vite';

export default defineConfig({
  plugins: [doTransforms({ wrangler: './wrangler.jsonc' })],
});

The generated declaration defaults to do-transforms.generated.d.ts beside the Wrangler main module. Include that file in the application's TypeScript project.

Restore broken Durable Object stubs

Many Durable Object errors leave a stub broken, so every later call through it fails with the same error. Enable restoreBrokenStubs to have the plugin rewrite configured namespace get and getByName calls so the resulting stub replaces itself after a call throws:

doTransforms({ wrangler: './wrangler.jsonc', restoreBrokenStubs: true });

The failed call still rejects and is not retried. The next call obtains a new stub from the namespace with the original arguments, which are evaluated only once, so env.MY_DO.get(env.MY_DO.newUniqueId()) keeps addressing the same object. The replacement happens at the end of the caller transform chain, so each attempt made by retry runs on a fresh stub. Without the plugin, use createRestorableStubFactory(env.MY_DO, 'getByName')(name).

Define and install a transform

import { DurableObject } from 'cloudflare:workers';
import {
  applyTransforms,
  createTransformContextTarget,
  defineTransform,
  registerTransform,
} from '@durability/transforms';

type RequestContext = { requestId?: string };

class ExampleObject extends DurableObject {
  setContext(context: RequestContext) {
    return createTransformContextTarget(this, context);
  }

  async greet(name: string) {
    return `Hello, ${name}`;
  }
}

const observability = defineTransform<ExampleObject, RequestContext>()
  .caller(
    (requestId: string) =>
      async ({ next }) =>
        next({ context: { requestId } })
  )
  .callee((metricName: string) => async ({ context, next }) => {
    console.log(metricName, context.requestId);
    return next();
  });

applyTransforms(ExampleObject, {
  methods: {
    greet: [registerTransform(observability, 'greet_calls')],
  },
});

const stub = env.EXAMPLE.getByName('example').with(
  observability,
  crypto.randomUUID()
);

await stub.greet('Ada');

applyTransforms mutates the class prototype and is cumulative. Call it once during module initialization, not per request or instance.

Caller transforms defined for a Durable Object class also receive actorId, the DurableObjectId of the object addressed by the stub. Transforms defined for other targets, such as defineTransform<object>(), do not see the field, and a Durable Object transform cannot be attached to a binding that is not a Durable Object stub. The ID stays on the caller and is not sent to the callee:

const actorLog = defineTransform<ExampleObject>().caller(
  () =>
    async ({ actorId, method, next }) => {
      console.log('rpc', method, actorId.toString());
      return next();
    }
);

await env.EXAMPLE.getByName('example').with(actorLog).greet('Ada');

Transform context is untrusted caller metadata. It is suitable for request IDs, tracing, and hints, but it must not be accepted as proof of identity, tenancy, roles, or authorization. Derive authorization from independently authenticated data on the callee.

Built-in caller transforms

import { retry, timeout } from '@durability/transforms';

const value = await stub
  .with(timeout, 5_000)
  .with(retry, {
    retries: 3,
    delay: ({ attempt }) => Math.min(1_000 * 2 ** (attempt - 1), 30_000),
  })
  .read();

A caller timeout stops waiting but does not cancel the remote operation. Each retry is a new RPC invocation, so retry only idempotent methods or pass an idempotency key. When delay is omitted, retries use capped exponential backoff with full jitter.

Other built-ins include:

  • betterResultCodec to serialize and rehydrate better-result values in a versioned envelope. Legacy 0.1.0 payloads are rejected by default because their shape is ambiguous; temporarily set acceptLegacy: true only while communicating with an older callee.
  • errorBoundary to convert caller-visible throws into Better Result errors.
  • abortAsSuccess to treat expected Durable Object resets as successful undefined results.
  • largeObjectStream to transfer large JSON object results. It requires a Standard Schema validator and enforces a caller decode limit, but it still buffers the complete serialized representation; use native streams for unbounded data.

Manual wrapping

When the Vite plugin is unavailable, wrap a target or namespace directly:

import {
  createTransformStub,
  timeout,
  withTransforms,
} from '@durability/transforms';

const service = createTransformStub(env.MY_SERVICE).with(timeout, 5_000);
const namespace = withTransforms(env.MY_DURABLE_OBJECT);
const id = namespace.idFromName('example');
const object = namespace.get(id).with(timeout, 5_000);

版本与发布记录

@durability/transforms 共发布 7 个版本,首次发布于 2026/07/14;过去 12 个月发布了 7 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.1latest2026/11/103 天前
0.6.02026/10/2123 天前
0.5.02026/10/1925 天前
0.4.02026/10/1826 天前
0.3.02026/10/051 个月前
0.2.02026/07/223 个月前

关于 @durability/transforms 的常见问题

@durability/transforms 是什么?

Typed caller and callee transforms for Cloudflare RPC

如何安装 @durability/transforms?

在项目目录运行 npm install @durability/transforms,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:better-result(^2.9.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@durability/transforms 支持 TypeScript 吗?

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

@durability/transforms 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@durability/transforms 可以商用吗?

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

@durability/transforms 的下载量有多少?

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

@durability/transforms 还在维护吗?

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