`getDurableObjectClass()` refuses a plain class as a facet: `no DO class …`
zsilbi opened ·
### Summary
On celld 0.6.1, a facet starts only when the loaded Worker's class extends `DurableObject` from `cloudflare:workers`. A plain class (a constructor that takes `(state, env)` and a `fetch` method) fails with `no DO class App`. workerd accepts both forms. The way the class is exported does not matter: a plain class fails when it is exported directly, re-exported from a second module, or exported beside a default export. A `DurableObject` subclass works in each of these forms.
A top-level Durable Object class that is a plain class works on celld. Only the facet path refuses it.
### Reproduction
`wrangler.json`:
```json { "name": "facet-class", "main": "index.js", "compatibility_date": "2026-01-01", "worker_loaders": [{ "binding": "LOADER" }], "durable_objects": { "bindings": [{ "name": "SUPERVISOR", "class_name": "Supervisor" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Supervisor"] }] } ```
`index.js`:
```js import { DurableObject } from "cloudflare:workers";
const VARIANTS = { plain: { mainModule: "app.js", modules: { "app.js": `export class App { constructor(state, env) { this.state = state; } fetch() { return new Response("plain"); } }` } }, reexported: { mainModule: "main.js", modules: { "main.js": `export { App } from "./facet.js";`, "facet.js": `export class App { constructor(state, env) { this.state = state; } fetch() { return new Response("reexported"); } }` } }, plainWithDefault: { mainModule: "app.js", modules: { "app.js": `export default { fetch() { return new Response("default"); } }; export class App { constructor(state) { this.state = state; } fetch() { return new Response("plain-with-default"); } }` } }, durableReexported: { mainModule: "main.js", modules: { "main.js": `export { App } from "./facet.js";`, "facet.js": `import { DurableObject } from "cloudflare:workers"; export class App extends DurableObject { fetch() { return new Response("durable-reexported"); } }` } } };
export class Supervisor extends DurableObject { async fetch(request) { const variant = new URL(request.url).pathname.slice(1); try { const worker = this.env.LOADER.get(variant, () => ({ compatibilityDate: "2026-01-01", globalOutbound: null, ...VARIANTS[variant] })); const facet = this.ctx.facets.get(variant, () => ({ class: worker.getDurableObjectClass("App") })); return await facet.fetch(request); } catch (error) { return new Response(`ERROR: ${String(error)}`, { status: 500 }); } } }
export default { fetch(request, env) { return env.SUPERVISOR.getByName("only").fetch(request); } }; ```
Run `celld dev .`, then `curl http://127.0.0.1:9876/plain` (and `/reexported`, `/plainWithDefault`, `/durableReexported`).
### Expected
Each path answers its variant's text, as on workerd 1.20260907.1:
```text /plain -> plain /reexported -> reexported /plainWithDefault -> plain-with-default /durableReexported -> durable-reexported ```
### Actual
```text /plain -> ERROR: Error: rejected: Error: no DO class App [at _instance (<anonymous>:2871:21) <- at _readyInstance (<anonymous>:2885:16) <- at <anonymous>:5349:26] /reexported -> ERROR: Error: rejected: Error: no DO class App [at _instance (<anonymous>:2871:21) <- at _readyInstance (<anonymous>:2885:16) <- at <anonymous>:5349:26] /plainWithDefault -> ERROR: Error: rejected: Error: no DO class App [at _instance (<anonymous>:2871:21) <- at _readyInstance (<anonymous>:2885:16) <- at <anonymous>:5349:26] /durableReexported -> durable-reexported ```
### Environment
- celld 0.6.1, release binary `celld-x86_64-unknown-linux-gnu`, `celld dev` - Linux x86-64 - Compared with workerd 1.20260907.1 (`workerd serve --experimental`)
### Possible fix
Accept any exported class (a constructor function) from `getDurableObjectClass()` for a facet, as the top-level Durable Object path already does.
bartlomiejureferenced 655ff6caf8b6last month on GitHub
bartlomiejureferenced 90b43017241flast month on GitHub
- BA
Fixed in v0.6.2: `getDurableObjectClass()` accepts a plain class that extends no runtime base.