🕳️

Cloudflare Durable Objects を TypeScript で使おうとしてハマった

2023/08/21に公開

起こった問題と解決策

環境

MacOS: Ventura 13.4.1(c)
Wrangler: wrangler 3.5.0
言語: TypeScript

問題

  • 公式のドキュメントを参考に Durable Objects を TypeScript で実装した
  • コード上特にエラーは起きていないが、wrangler dev/deploy 実行時にエラーが発生した

原因と解決方法

  • Durable Objects の定義クラスを別クラスとして切り出していたのが原因
  • 別クラスに切り出す場合は wrangler.toml 内の main = で指定されている root module から、切り出したクラスを export する必要がある

以下は実際のコードを交えた解説です

実装

公式の手順に沿って Durable Objects を使えるように定義

https://developers.cloudflare.com/durable-objects/get-started/#3-write-a-class-to-define-a-durable-object
https://developers.cloudflare.com/durable-objects/examples/durable-objects-typescript-rollup-es-modules/

TypeScript で実装したいので後者のページを参考に実装しました

Durable Objects class

counter.ts
export class Counter {
  constructor(private readonly state: DurableObjectState) {}
  async fetch(_request: Request): Promise<Response> {
      const id: number = await this.state.storage?.get("id") || 0;
      await this.state.storage?.put("id", id+1)
      return new Response(id.toString());
  }
}

worker.ts

worker.ts
export interface Env {
  COUNTER: DurableObjectNamespace;
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise<Response> {
    const doID = env.COUNTER.idFromName("id");
    const counter = env.COUNTER.get(doID);
    const res = await counter.fetch(request.url);
    return new Response(await res.text());
  },
};

wrangler.toml

wrangler.toml
name = "wrangler-test"
main = "src/worker.ts"
compatibility_date = "2023-08-07"

[durable_objects]
bindings = [
  { name = "COUNTER", class_name = "Counter" }
]

[[migrations]]
tag = "v1"
new_classes = ["Counter"]

wrangler dev/deploy の実行

wrangler dev

$ wrangler dev
⛅️ wrangler 3.5.0
------------------
wrangler dev now uses local mode by default, powered by 🔥 Miniflare and 👷 workerd.
To run an edge preview session for your Worker, use wrangler dev --remote
Your worker has access to the following bindings:
- Durable Objects:
  - Counter: Counter
⎔ Starting local server...
service core:user:wrangler-test: Uncaught TypeError: Class extends value undefined is not a constructor or null
  at worker.js:90:44 in maskDurableObjectDefinition
  at worker.js:99:16
✘ [ERROR] MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start. There is likely additional logging output above.

エラーが発生して起動できない

wrangler deploy

$ wangler deploy
⛅️ wrangler 3.5.0
------------------
Your worker has access to the following bindings:
- Durable Objects:
  - Counter: Counter
Total Upload: 0.33 KiB / gzip: 0.22 KiB

✘ [ERROR] A request to the Cloudflare API (/accounts/xxxx/workers/scripts/wrangler-test) failed.

  Cannot apply new-class migration to class Counter that is not exported by script [code: 10070]

  If you think this is a bug, please open an issue at:
  https://github.com/cloudflare/workers-sdk/issues/new/choose

エラーが発生してデプロイ出来ない

答え

原因

counter.tsexport class しているが wrangler.toml で定義している root module からは export していないため、定義した Durable Objects class を wrangler が認識できていなかった

解決策1. worker.ts に定義する

worker.ts に直接定義して export することでエラーが解消されます

worker.ts
+export class Counter {
+  constructor(private readonly state: DurableObjectState) {}
+  async fetch(_request: Request): Promise<Response> {
+      const id: number = await this.state.storage?.get("id") || 0;
+      await this.state.storage?.put("id", id+1)
+      return new Response(id.toString());
+  }
+}

解決策2. worker.ts 側からも export する

参考:
https://github.com/cloudflare/durable-objects-typescript-rollup-esm/blob/e1ae07aec30885dd3d604ca5b38fa6d6fac7f49e/src/index.ts#L3-L5
wrangler.tomlmain = で指定している root module から Durable Object クラスを export することでエラーが解消されます

worker.ts
+ export { Counter } from "./counter";

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise<Response> {
    const doID = env.COUNTER.idFromName("id");
    const counter = env.COUNTER.get(doID);
    const res = await counter.fetch(request.url);
    return new Response(await res.text());
  },
};

修正後の実行

wrangler dev

 ⛅️ wrangler 3.5.0
------------------
wrangler dev now uses local mode by default, powered by 🔥 Miniflare and 👷 workerd.
To run an edge preview session for your Worker, use wrangler dev --remote
Your worker has access to the following bindings:
- Durable Objects:
  - Counter: Counter
⎔ Starting local server...
[mf:inf] Ready on http://127.0.0.1:8787/

ローカルで起動成功

wrangler deploy

$ wrangler deploy
53.3s  月  8/14 22:12:22 2023
 ⛅️ wrangler 3.5.0
------------------
Your worker has access to the following bindings:
- Durable Objects:
  - Counter: Counter
Total Upload: 0.59 KiB / gzip: 0.31 KiB
Uploaded wrangler-test (1.63 sec)
Published wrangler-test (3.75 sec)
  https://xxx.workers.dev
Current Deployment ID: e3e4927d-7c39-466e-8147-46d29cc41287

デプロイ成功

備考

公式のドキュメントには別クラスとして Durable Objects を実装しているケースの情報が少なく、export の必要性に気づくのに時間がかかりました
以下のような issue も上がっているので、いずれ改善されるかもしれません
https://github.com/cloudflare/workers-sdk/issues/3738

EGSTOCK,Inc.

Discussion