From 6caa936adadf81b8a766b5ab541d0764a27d6794 Mon Sep 17 00:00:00 2001 From: Kenton Varda Date: Fri, 31 Jan 2025 17:52:03 -0600 Subject: [PATCH 1/2] Implement automatic self-referential bindings via ctx.exports. For every top-level export, `ctx.exports` will contain a corresponding property which acts as a binding to that export. If the export is a simple object or `WorkerEntrypoint`, then the `ctx.exports` property is a service binding. If the export is a Durable Object class (and it has been configured with storage), the `ctx.exports` property is a DO namespace binding. So you can do: `ctx.exports.MyEntrypoint.someRpc()` And it calls `someRpc()` on the entrypoint named `MyEntrypoint`, as if you had called it over a service binding. This is marked experimental for now since it won't work on the edge until further changes are made to our control plane. --- src/workerd/api/global-scope.h | 18 ++- src/workerd/io/worker.c++ | 18 ++- src/workerd/io/worker.h | 10 +- src/workerd/server/server-test.c++ | 75 ++++++++++++ src/workerd/server/server.c++ | 114 ++++++++++++++++-- src/workerd/tests/test-fixture.c++ | 2 +- .../experimental/index.d.ts | 1 + .../generated-snapshot/experimental/index.ts | 1 + 8 files changed, 216 insertions(+), 23 deletions(-) diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index 91fed62e223..8a09ffb705c 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -204,8 +204,12 @@ class TestController: public jsg::Object { class ExecutionContext: public jsg::Object { public: - ExecutionContext(jsg::Lock& js): props(js, js.obj()) {} - ExecutionContext(jsg::Lock& js, jsg::JsValue props): props(js, props) {} + ExecutionContext(jsg::Lock& js, jsg::JsValue exports) + : exports(js, exports), + props(js, js.obj()) {} + ExecutionContext(jsg::Lock& js, jsg::JsValue exports, jsg::JsValue props) + : exports(js, exports), + props(js, props) {} void waitUntil(kj::Promise promise); void passThroughOnException(); @@ -214,6 +218,10 @@ class ExecutionContext: public jsg::Object { // and throwing an error at the client. void abort(jsg::Lock& js, jsg::Optional reason); + jsg::JsValue getExports(jsg::Lock& js) { + return exports.getHandle(js); + } + jsg::JsValue getProps(jsg::Lock& js) { return props.getHandle(js); } @@ -221,6 +229,11 @@ class ExecutionContext: public jsg::Object { JSG_RESOURCE_TYPE(ExecutionContext, CompatibilityFlags::Reader flags) { JSG_METHOD(waitUntil); JSG_METHOD(passThroughOnException); + if (flags.getWorkerdExperimental()) { + // TODO(soon): Remove experimental gate as soon as we've wired up the control plane so that + // this works in production. + JSG_LAZY_INSTANCE_PROPERTY(exports, getExports); + } JSG_LAZY_INSTANCE_PROPERTY(props, getProps); if (flags.getWorkerdExperimental()) { @@ -243,6 +256,7 @@ class ExecutionContext: public jsg::Object { } private: + jsg::JsRef exports; jsg::JsRef props; void visitForGc(jsg::GcVisitor& visitor) { diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index 967d5f69349..739c0e95154 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -481,6 +481,7 @@ struct Worker::Impl { // The environment blob to pass to handlers. kj::Maybe env; + kj::Maybe ctxExports; struct ActorClassInfo { EntrypointClass cls; @@ -1550,8 +1551,10 @@ kj::Maybe tryResolveMainModule(jsg::Lock& js, Worker::Worker(kj::Own scriptParam, kj::Own metricsParam, - kj::FunctionParam target)> - compileBindings, + kj::FunctionParam target, + v8::Local ctxExports)> compileBindings, IsolateObserver::StartType startType, TraceParentContext spans, LockType lockType, @@ -1639,7 +1642,9 @@ Worker::Worker(kj::Own scriptParam, lock.v8Set(bindingsScope, global.name, global.value); } - compileBindings(lock, script->isolate->getApi(), bindingsScope); + v8::Local ctxExports = v8::Object::New(lock.v8Isolate); + + compileBindings(lock, script->isolate->getApi(), bindingsScope, ctxExports); // Execute script. currentSpan = maybeMakeSpan("lw:top_level_execution"_kjc); @@ -1656,6 +1661,7 @@ Worker::Worker(kj::Own scriptParam, KJ_IF_SOME(ns, tryResolveMainModule(lock, mainModule, *jsContext, *script, limitErrorOrTime)) { impl->env = lock.v8Ref(bindingsScope.As()); + impl->ctxExports = lock.v8Ref(ctxExports.As()); auto& api = script->isolate->getApi(); auto handlers = api.unwrapExports(lock, ns); @@ -1670,7 +1676,7 @@ Worker::Worker(kj::Own scriptParam, // requests. This is weird and obviously wrong but changing it probably // requires a compat flag. Until then, connection properties will not be // available for non-class handlers. - obj.ctx = jsg::alloc(lock); + obj.ctx = jsg::alloc(lock, jsg::JsValue(ctxExports)); impl->namedHandlers.insert(kj::mv(handler.name), kj::mv(obj)); } @@ -1974,7 +1980,9 @@ kj::Maybe> Worker::Lock::getExportedHandler( return fakeOwn(h); } else KJ_IF_SOME(cls, worker.impl->statelessClasses.find(n)) { jsg::Lock& js = *this; - auto handler = kj::heap(cls(js, jsg::alloc(js, props.toJs(js)), + auto handler = kj::heap(cls(js, + jsg::alloc(js, + jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js)), KJ_ASSERT_NONNULL(worker.impl->env).addRef(js))); // HACK: We set handler.env and handler.ctx to undefined because we already passed the real diff --git a/src/workerd/io/worker.h b/src/workerd/io/worker.h index 6b59e092df8..0a29e22ec68 100644 --- a/src/workerd/io/worker.h +++ b/src/workerd/io/worker.h @@ -99,15 +99,19 @@ class Worker: public kj::AtomicRefcounted { explicit Worker(kj::Own script, kj::Own metrics, - kj::FunctionParam target)> - compileBindings, + kj::FunctionParam target, + v8::Local ctxExports)> compileBindings, IsolateObserver::StartType startType, TraceParentContext spans, LockType lockType, kj::Maybe errorReporter = kj::none, kj::Maybe startupTime = kj::none); // `compileBindings()` is a callback that constructs all of the bindings and adds them as - // properties to `target`. + // properties to `target`. It also compiles the `ctx.exports` object and writes it to + // `ctxExports`. Note that it is permissible for this callback to save a handle to `ctxExports` + // and fill it in later if needed, as long as it is filled in before any requests are started. ~Worker() noexcept(false); KJ_DISALLOW_COPY_AND_MOVE(Worker); diff --git a/src/workerd/server/server-test.c++ b/src/workerd/server/server-test.c++ index da8b53915f2..6c73b6a3293 100644 --- a/src/workerd/server/server-test.c++ +++ b/src/workerd/server/server-test.c++ @@ -4006,6 +4006,81 @@ KJ_TEST("Server: Entrypoint binding with props") { conn.httpGet200("/", "got: 123"); } +KJ_TEST("Server: ctx.exports self-referential bindings") { + TestServer test(R"(( + services = [ + ( name = "hello", + worker = ( + compatibilityDate = "2024-02-23", + compatibilityFlags = ["experimental"], + modules = [ + ( name = "main.js", + esModule = + `import { WorkerEntrypoint, DurableObject } from "cloudflare:workers"; + `export default { + ` async fetch(request, env, ctx) { + ` // First set the actor state the old fashion way, to make sure we get + ` // reconnected to the same actor when using self-referential bindings. + ` { + ` let bindingActor = env.NS.get(env.NS.idFromName("qux")); + ` await bindingActor.setValue(234); + ` } + ` + ` let actor = ctx.exports.MyActor.get(ctx.exports.MyActor.idFromName("qux")); + ` return new Response([ + ` await ctx.exports.MyEntrypoint.foo(123), + ` await ctx.exports.AnotherEntrypoint.bar(321), + ` await actor.baz(), + ` await ctx.exports.default.corge(555), + ` "UnconfiguredActor" in ctx.exports, // should be false + ` ].join(", ")); + ` }, + ` corge(i) { return `corge: ${i}` } + `} + `export class MyEntrypoint extends WorkerEntrypoint { + ` foo(i) { return `foo: ${i}` } + `} + `export class AnotherEntrypoint extends WorkerEntrypoint { + ` bar(i) { return `bar: ${i}` } + `} + `export class MyActor extends DurableObject { + ` setValue(i) { this.value = i; } + ` baz() { return `baz: ${this.value}` } + `} + `export class UnconfiguredActor extends DurableObject { + ` qux(i) { return `qux: ${i}` } + `} + ) + ], + bindings = [ + # A regular binding, just here to make sure it doesn't mess up self-referential + # channel numbers. + ( name = "INTERNET", service = "internet" ), + + # Similarly, an actor namespace binding. + (name = "NS", durableObjectNamespace = "MyActor") + ], + durableObjectNamespaces = [ + ( className = "MyActor", + uniqueKey = "mykey", + ) + ], + durableObjectStorage = (inMemory = void) + ) + ), + ], + sockets = [ + ( name = "main", address = "test-addr", service = "hello" ), + ] + ))"_kj); + + test.server.allowExperimental(); + test.start(); + + auto conn = test.connect("test-addr"); + conn.httpGet200("/", "foo: 123, bar: 321, baz: 234, corge: 555, false"); +} + // ======================================================================================= // TODO(beta): Test TLS (send and receive) diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index a95949650b9..d3f8cb7e43b 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -1856,6 +1856,10 @@ class Server::WorkerService final: public Service, return kj::heap(*this, name, propsJson, *handlers); } + bool hasDefaultEntrypoint() { + return defaultEntrypointHandlers != kj::none; + } + kj::Array getEntrypointNames() { return KJ_MAP(e, namedEntrypoints) -> kj::StringPtr { return e.key; }; } @@ -3430,25 +3434,83 @@ kj::Own Server::makeWorker(kj::StringPtr name, } } + jsg::V8Ref ctxExportsHandle = nullptr; auto worker = kj::atomicRefcounted(kj::mv(script), kj::atomicRefcounted(), - [&](jsg::Lock& lock, const Worker::Api& api, v8::Local target) { + [&](jsg::Lock& lock, const Worker::Api& api, v8::Local target, + v8::Local ctxExports) { + // We can't fill in ctx.exports yet because we need to run the validator first to discover + // entrypoints, which we cannot do until after the Worker constructor completes. We are + // permitted to hold a handle until then, though. + ctxExportsHandle = lock.v8Ref(ctxExports); + return WorkerdApi::from(api).compileGlobals(lock, globals, target, 1); - }, IsolateObserver::StartType::COLD, + }, + IsolateObserver::StartType::COLD, TraceParentContext(nullptr, nullptr), // systemTracer -- TODO(beta): factor out Worker::Lock::TakeSynchronously(kj::none), errorReporter); { - worker->runInLockScope(Worker::Lock::TakeSynchronously(kj::none), - [&](Worker::Lock& lock) { lock.validateHandlers(errorReporter); }); + worker->runInLockScope(Worker::Lock::TakeSynchronously(kj::none), [&](Worker::Lock& lock) { + lock.validateHandlers(errorReporter); + + // Build `ctx.exports` based on the entrypoints reported by `validateHandlers()`. + kj::Vector ctxExports( + errorReporter.namedEntrypoints.size() + localActorConfigs.size()); + + // Start numbering loopback channels for stateless entrypoints after the last subrequest + // channel used by bindings. + uint nextSubrequestChannel = + subrequestChannels.size() + IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; + if (errorReporter.defaultEntrypoint != kj::none) { + ctxExports.add(Global{.name = kj::str("default"), + .value = Global::Fetcher{ + .channel = nextSubrequestChannel++, .requiresHost = true, .isInHouse = false}}); + } + for (auto& ep: errorReporter.namedEntrypoints) { + ctxExports.add(Global{.name = kj::str(ep.key), + .value = Global::Fetcher{ + .channel = nextSubrequestChannel++, .requiresHost = true, .isInHouse = false}}); + } + + // Start numbering loopback channels for actor classes after the last actor channel used by + // bindings. + uint nextActorChannel = actorChannels.size(); + for (auto& ns: localActorConfigs) { + decltype(Global::value) value; + KJ_SWITCH_ONEOF(ns.value) { + KJ_CASE_ONEOF(durable, Durable) { + value = Global::DurableActorNamespace{ + .actorChannel = nextActorChannel++, .uniqueKey = durable.uniqueKey}; + } + KJ_CASE_ONEOF(ephemeral, Ephemeral) { + value = Global::EphemeralActorNamespace{ + .actorChannel = nextActorChannel++, + }; + } + } + ctxExports.add(Global{.name = kj::str(ns.key), .value = kj::mv(value)}); + } + + JSG_WITHIN_CONTEXT_SCOPE(lock, lock.getContext(), [&](jsg::Lock& js) { + WorkerdApi::from(worker->getIsolate().getApi()) + .compileGlobals(lock, ctxExports, ctxExportsHandle.getHandle(js), 1); + }); + + // As an optimization, drop this now while we have the lock. + { auto drop = kj::mv(ctxExportsHandle); } + }); } auto linkCallback = [this, name, conf, subrequestChannels = kj::mv(subrequestChannels), - actorChannels = kj::mv(actorChannels)]( - WorkerService& workerService) mutable { + actorChannels = kj::mv(actorChannels), + &localActorConfigs](WorkerService& workerService) mutable { WorkerService::LinkedIoChannels result{.alarmScheduler = *alarmScheduler}; - auto services = kj::heapArrayBuilder>( - subrequestChannels.size() + IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT); + auto entrypointNames = workerService.getEntrypointNames(); + + auto services = kj::heapArrayBuilder>(subrequestChannels.size() + + IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT + entrypointNames.size() + + workerService.hasDefaultEntrypoint()); kj::Own globalService = lookupService(conf.getGlobalOutbound(), kj::str("Worker \"", name, "\"'s globalOutbound")); @@ -3469,26 +3531,54 @@ kj::Own Server::makeWorker(kj::StringPtr name, services.add(lookupService(channel.designator, kj::mv(channel.errorContext))); } + // Link the ctx.exports self-referential channels. Note that it's important these are added + // in exactyl the same order as the channels were allocated earlier when we compiled the + // ctx.exports bindings. + if (workerService.hasDefaultEntrypoint()) { + services.add(KJ_ASSERT_NONNULL(workerService.getEntrypoint(kj::none, kj::none))); + } + for (auto& ep: entrypointNames) { + services.add(KJ_ASSERT_NONNULL(workerService.getEntrypoint(ep, kj::none))); + } + result.subrequest = services.finish(); - result.actor = KJ_MAP(channel, actorChannels) -> kj::Maybe { + auto linkedActorChannels = kj::heapArrayBuilder>( + actorChannels.size() + localActorConfigs.size()); + + for (auto& channel: actorChannels) { WorkerService* targetService = &workerService; if (channel.designator.hasServiceName()) { auto& svc = KJ_UNWRAP_OR(this->services.find(channel.designator.getServiceName()), { // error was reported earlier - return kj::none; + linkedActorChannels.add(kj::none); + continue; }); targetService = dynamic_cast(svc.get()); if (targetService == nullptr) { // error was reported earlier - return kj::none; + linkedActorChannels.add(kj::none); + continue; } } // (If getActorNamespace() returns null, an error was reported earlier.) - return targetService->getActorNamespace(channel.designator.getClassName()); + linkedActorChannels.add(targetService->getActorNamespace(channel.designator.getClassName())); }; + // Link the ctx.exports self-referential actor channels. Again, it's important that these + // be added in the same order as before. kj::HashMap iteration order is deterministic, and + // is exactly insertion order as long as no entries have been removed, so we can expect that + // `workerService.getActorNamespaces()` iterates in the same order as `localActorConfigs` did + // earlier. + auto& selfActorNamespaces = workerService.getActorNamespaces(); + KJ_ASSERT(selfActorNamespaces.size() == localActorConfigs.size()); + for (auto& ns: selfActorNamespaces) { + linkedActorChannels.add(*ns.value); + } + + result.actor = linkedActorChannels.finish(); + if (conf.hasCacheApiOutbound()) { result.cache = lookupService( conf.getCacheApiOutbound(), kj::str("Worker \"", name, "\"'s cacheApiOutbound")); diff --git a/src/workerd/tests/test-fixture.c++ b/src/workerd/tests/test-fixture.c++ index ae45a721449..066cb84097a 100644 --- a/src/workerd/tests/test-fixture.c++ +++ b/src/workerd/tests/test-fixture.c++ @@ -346,7 +346,7 @@ TestFixture::TestFixture(SetupParams&& params) nullptr)), worker(kj::atomicRefcounted(kj::atomicAddRef(*workerScript), kj::atomicRefcounted(), - [](jsg::Lock&, const Worker::Api&, v8::Local) { + [](jsg::Lock&, const Worker::Api&, v8::Local, v8::Local) { // no bindings, nothing to do }, IsolateObserver::StartType::COLD, diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index d21de1e685e..f0e68fc2372 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -400,6 +400,7 @@ interface TestController {} interface ExecutionContext { waitUntil(promise: Promise): void; passThroughOnException(): void; + exports: any; props: any; abort(reason?: any): void; } diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index 9de197c953c..8ae44012cca 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -402,6 +402,7 @@ export interface TestController {} export interface ExecutionContext { waitUntil(promise: Promise): void; passThroughOnException(): void; + exports: any; props: any; abort(reason?: any): void; } From 5078e8e1ee8f14f3656c85ec342f623348d4fe24 Mon Sep 17 00:00:00 2001 From: Kenton Varda Date: Thu, 6 Feb 2025 12:11:32 -0600 Subject: [PATCH 2/2] Also add `ctx.exports` to Durable Objects' `ctx`. --- src/workerd/api/actor-state.c++ | 2 ++ src/workerd/api/actor-state.h | 11 +++++++++++ src/workerd/io/worker.c++ | 5 ++++- src/workerd/server/server-test.c++ | 5 ++++- types/generated-snapshot/experimental/index.d.ts | 1 + types/generated-snapshot/experimental/index.ts | 1 + 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/workerd/api/actor-state.c++ b/src/workerd/api/actor-state.c++ index 3052c90c99f..60ec4fa5ce7 100644 --- a/src/workerd/api/actor-state.c++ +++ b/src/workerd/api/actor-state.c++ @@ -826,9 +826,11 @@ kj::OneOf, kj::StringPtr> ActorState::getId() { } DurableObjectState::DurableObjectState(Worker::Actor::Id actorId, + jsg::JsRef exports, kj::Maybe> storage, kj::Maybe container) : id(kj::mv(actorId)), + exports(kj::mv(exports)), storage(kj::mv(storage)), container(container.map( [&](rpc::Container::Client& cap) { return jsg::alloc(kj::mv(cap)); })) {} diff --git a/src/workerd/api/actor-state.h b/src/workerd/api/actor-state.h index 3c21f7019be..0a8cdf28aa2 100644 --- a/src/workerd/api/actor-state.h +++ b/src/workerd/api/actor-state.h @@ -464,11 +464,16 @@ class WebSocketRequestResponsePair: public jsg::Object { class DurableObjectState: public jsg::Object { public: DurableObjectState(Worker::Actor::Id actorId, + jsg::JsRef exports, kj::Maybe> storage, kj::Maybe container); void waitUntil(kj::Promise promise); + jsg::JsValue getExports(jsg::Lock& js) { + return exports.getHandle(js); + } + kj::OneOf, kj::StringPtr> getId(); jsg::Optional> getStorage() { @@ -541,6 +546,11 @@ class DurableObjectState: public jsg::Object { JSG_RESOURCE_TYPE(DurableObjectState, CompatibilityFlags::Reader flags) { JSG_METHOD(waitUntil); + if (flags.getWorkerdExperimental()) { + // TODO(soon): Remove experimental gate as soon as we've wired up the control plane so that + // this works in production. + JSG_LAZY_INSTANCE_PROPERTY(exports, getExports); + } JSG_LAZY_INSTANCE_PROPERTY(id, getId); JSG_LAZY_INSTANCE_PROPERTY(storage, getStorage); JSG_LAZY_INSTANCE_PROPERTY(container, getContainer); @@ -581,6 +591,7 @@ class DurableObjectState: public jsg::Object { private: Worker::Actor::Id id; + jsg::JsRef exports; kj::Maybe> storage; kj::Maybe> container; diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index 739c0e95154..bf2983ab66e 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -3454,7 +3454,10 @@ void Worker::Actor::ensureConstructed(IoContext& context) { storage = impl->makeStorage(lock, worker->getIsolate().getApi(), *c); } auto handler = info.cls(lock, - jsg::alloc(cloneId(), kj::mv(storage), kj::mv(impl->container)), + jsg::alloc(cloneId(), + jsg::JsRef( + js, KJ_ASSERT_NONNULL(lock.getWorker().impl->ctxExports).addRef(js)), + kj::mv(storage), kj::mv(impl->container)), KJ_ASSERT_NONNULL(lock.getWorker().impl->env).addRef(js)); // HACK: We set handler.env to undefined because we already passed the real env into the diff --git a/src/workerd/server/server-test.c++ b/src/workerd/server/server-test.c++ index 6c73b6a3293..92ee1f5126b 100644 --- a/src/workerd/server/server-test.c++ +++ b/src/workerd/server/server-test.c++ @@ -4032,6 +4032,7 @@ KJ_TEST("Server: ctx.exports self-referential bindings") { ` await ctx.exports.AnotherEntrypoint.bar(321), ` await actor.baz(), ` await ctx.exports.default.corge(555), + ` await actor.grault(456), ` "UnconfiguredActor" in ctx.exports, // should be false ` ].join(", ")); ` }, @@ -4039,6 +4040,7 @@ KJ_TEST("Server: ctx.exports self-referential bindings") { `} `export class MyEntrypoint extends WorkerEntrypoint { ` foo(i) { return `foo: ${i}` } + ` grault(i) { return `grault: ${i}` } `} `export class AnotherEntrypoint extends WorkerEntrypoint { ` bar(i) { return `bar: ${i}` } @@ -4046,6 +4048,7 @@ KJ_TEST("Server: ctx.exports self-referential bindings") { `export class MyActor extends DurableObject { ` setValue(i) { this.value = i; } ` baz() { return `baz: ${this.value}` } + ` grault(i) { return this.ctx.exports.MyEntrypoint.grault(i); } `} `export class UnconfiguredActor extends DurableObject { ` qux(i) { return `qux: ${i}` } @@ -4078,7 +4081,7 @@ KJ_TEST("Server: ctx.exports self-referential bindings") { test.start(); auto conn = test.connect("test-addr"); - conn.httpGet200("/", "foo: 123, bar: 321, baz: 234, corge: 555, false"); + conn.httpGet200("/", "foo: 123, bar: 321, baz: 234, corge: 555, grault: 456, false"); } // ======================================================================================= diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index f0e68fc2372..f6a5e67fdd2 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -565,6 +565,7 @@ interface DurableObjectNamespaceGetDurableObjectOptions { } interface DurableObjectState { waitUntil(promise: Promise): void; + exports: any; readonly id: DurableObjectId; readonly storage: DurableObjectStorage; container?: Container; diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index 8ae44012cca..b76063b5042 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -570,6 +570,7 @@ export interface DurableObjectNamespaceGetDurableObjectOptions { } export interface DurableObjectState { waitUntil(promise: Promise): void; + exports: any; readonly id: DurableObjectId; readonly storage: DurableObjectStorage; container?: Container;