From 76ac8422e17323ac5cdb58d466ac4c031bb3dd57 Mon Sep 17 00:00:00 2001 From: Max McDonnell Date: Wed, 25 Mar 2026 14:26:13 -0700 Subject: [PATCH] Support ctx.id.name in local dev for Durable Objects --- src/workerd/server/server-test.c++ | 151 ++++++++++++++++++++++++++++- src/workerd/server/server.c++ | 13 ++- 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/src/workerd/server/server-test.c++ b/src/workerd/server/server-test.c++ index 8bef22e439d..1edc8047138 100644 --- a/src/workerd/server/server-test.c++ +++ b/src/workerd/server/server-test.c++ @@ -1998,9 +1998,9 @@ KJ_TEST("Server: Durable Objects (in memory)") { ` throw new Error("durable ID should be type DurableObjectId, " + ` `got: ${this.id.constructor.name}`); ` } - ` if (this.id.name) { - ` throw new Error("ctx.id for Durable Object should not have a .name " + - ` `property, got: ${this.id.name}`); + ` if (typeof this.id.name !== "string" || this.id.name.length === 0) { + ` throw new Error("ctx.id.name should be a non-empty string for " + + ` `named DOs, got: ${JSON.stringify(this.id.name)}`); ` } ` } ` async fetch(request) { @@ -2047,6 +2047,151 @@ KJ_TEST("Server: Durable Objects (in memory)") { "/bar", "02b496f65dd35cbac90e3e72dc5a398ee93926ea4a3821e26677082d2e6f9b79: http://foo/bar 2"); } +KJ_TEST("Server: Durable Objects keep ctx.id.name undefined for unique IDs") { + TestServer test(R"(( + services = [ + ( name = "hello", + worker = ( + compatibilityDate = "2022-08-17", + modules = [ + ( name = "main.js", + esModule = + `export default { + ` async fetch(request, env) { + ` let actor = env.ns.get(env.ns.newUniqueId()) + ` return await actor.fetch(request) + ` } + `} + `export class MyActorClass { + ` constructor(state, env) { + ` this.name = state.id.name; + ` } + ` async fetch(request) { + ` return new Response(String(this.name)); + ` } + `} + ) + ], + bindings = [(name = "ns", durableObjectNamespace = "MyActorClass")], + durableObjectNamespaces = [ + ( className = "MyActorClass", + uniqueKey = "mykey", + ) + ], + durableObjectStorage = (inMemory = void) + ) + ), + ], + sockets = [ + ( name = "main", + address = "test-addr", + service = "hello" + ) + ] + ))"_kj); + + test.start(); + auto conn = test.connect("test-addr"); + conn.httpGet200("/", "undefined"); +} + +KJ_TEST("Server: Durable Objects retain ctx.id.name for short names") { + TestServer test(R"(( + services = [ + ( name = "hello", + worker = ( + compatibilityDate = "2022-08-17", + modules = [ + ( name = "main.js", + esModule = + `const name = "retained-name-123" + `export default { + ` async fetch(request, env) { + ` let actor = env.ns.get(env.ns.idFromName(name)) + ` return await actor.fetch(request) + ` } + `} + `export class MyActorClass { + ` constructor(state, env) { + ` this.name = state.id.name; + ` } + ` async fetch(request) { + ` return new Response(String(this.name)); + ` } + `} + ) + ], + bindings = [(name = "ns", durableObjectNamespace = "MyActorClass")], + durableObjectNamespaces = [ + ( className = "MyActorClass", + uniqueKey = "mykey", + ) + ], + durableObjectStorage = (inMemory = void) + ) + ), + ], + sockets = [ + ( name = "main", + address = "test-addr", + service = "hello" + ) + ] + ))"_kj); + + test.start(); + auto conn = test.connect("test-addr"); + conn.httpGet200("/", "retained-name-123"); +} + +KJ_TEST("Server: Durable Objects drop ctx.id.name for long names") { + TestServer test(R"(( + services = [ + ( name = "hello", + worker = ( + compatibilityDate = "2022-08-17", + modules = [ + ( name = "main.js", + esModule = + `export default { + ` async fetch(request, env) { + ` let actor = env.ns.get(env.ns.idFromName("a".repeat(1025))) + ` return await actor.fetch(request) + ` } + `} + `export class MyActorClass { + ` constructor(state, env) { + ` this.name = state.id.name; + ` } + ` async fetch(request) { + ` return new Response(String(this.name)); + ` } + `} + ) + ], + bindings = [(name = "ns", durableObjectNamespace = "MyActorClass")], + durableObjectNamespaces = [ + ( className = "MyActorClass", + uniqueKey = "mykey", + ) + ], + durableObjectStorage = (inMemory = void) + ) + ), + ], + sockets = [ + ( name = "main", + address = "test-addr", + service = "hello" + ) + ] + ))"_kj); + + test.start(); + auto conn = test.connect("test-addr"); + conn.httpGet200("/", "undefined"); +} + KJ_TEST("Server: Simultaneous requests to a DO that hasn't started don't cause split brain") { TestServer test(R"(( services = [ diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index 085d38602ea..c1c9581b566 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -2277,11 +2277,14 @@ class Server::WorkerService final: public Service, kj::Own getActorChannel(Worker::Actor::Id id) { KJ_IF_SOME(doId, id.tryGet>()) { - // To emulate production, we have to recreate this ID. - ActorIdFactoryImpl::ActorIdImpl* idImpl = - dynamic_cast(doId.get()); - KJ_ASSERT(idImpl != nullptr, "Unexpected ActorId type?"); - idImpl->clearName(); + KJ_IF_SOME(name, doId->getName()) { + // To emulate production, we preserve the name on the id, but only if it's <= 1024 bytes. + if (name.size() > 1024) { + auto* idImpl = dynamic_cast(doId.get()); + KJ_ASSERT(idImpl != nullptr, "Unexpected ActorId type?"); + idImpl->clearName(); + } + } } return kj::refcounted(getActorContainer(kj::mv(id)));