From 74c3a0100d81bd83c353ae35f59382e5caa24a7b Mon Sep 17 00:00:00 2001 From: Sophie Wallace Date: Thu, 5 Mar 2026 16:45:25 +0000 Subject: [PATCH 1/2] Allow exposing ctx.version to workers with enable_version_api flag CustomEvents do not currently pass versionInfo to their entrypoints. --- src/workerd/api/global-scope.h | 79 ++++++++++++++++--- src/workerd/io/compatibility-date.capnp | 10 ++- src/workerd/io/worker-entrypoint.c++ | 53 ++++++++----- src/workerd/io/worker-entrypoint.h | 1 + src/workerd/io/worker.c++ | 11 ++- src/workerd/io/worker.h | 45 ++++++++++- src/workerd/server/server.c++ | 10 ++- .../experimental/index.d.ts | 8 ++ .../generated-snapshot/experimental/index.ts | 8 ++ 9 files changed, 187 insertions(+), 38 deletions(-) diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index 5a892ec2aad..3f59a4f53c9 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -17,6 +17,7 @@ #include #endif +#include #include #include @@ -193,11 +194,24 @@ class TestController: public jsg::Object { class ExecutionContext: public jsg::Object { public: ExecutionContext(jsg::Lock& js, jsg::JsValue exports) + : ExecutionContext(js, kj::mv(exports), /*props=*/js.obj(), /*versionInfo=*/kj::none) {} + + ExecutionContext(jsg::Lock& js, + jsg::JsValue exports, + jsg::JsValue props, + kj::Maybe versionInfo) : exports(js, exports), - props(js, js.obj()) {} - ExecutionContext(jsg::Lock& js, jsg::JsValue exports, jsg::JsValue props) - : exports(js, exports), - props(js, props) {} + props(js, props) { + auto featureFlags = FeatureFlags::get(js); + if (featureFlags.getEnableVersionApi()) { + version = kj::mv(versionInfo).map([&js, &featureFlags](auto v) { + return jsg::JsRef{js, + v.toJs(js, + featureFlags.getEnableCtxVersionMetadata() ? PopulateVersionInfoMetadata::YES + : PopulateVersionInfoMetadata::NO)}; + }); + } + } void waitUntil(kj::Promise promise); void passThroughOnException(); @@ -214,6 +228,17 @@ class ExecutionContext: public jsg::Object { return props.getHandle(js); } + jsg::JsValue getVersion(jsg::Lock& js) { + // TODO(soon): We should be able to assert for `version != kj::none` in the constructor when the + // `enable_version_api` compat flag is enabled, but currently dynamic workers and "reusable + // `ctx` object instantiation" do not pass information to populate `ctx.version` with, + // `ctx.version` is currently optional so we can return undefined for now. + KJ_IF_SOME(someVersion, version) { + return someVersion.getHandle(js); + } + return js.undefined(); + } + JSG_RESOURCE_TYPE(ExecutionContext, CompatibilityFlags::Reader flags) { JSG_METHOD(waitUntil); JSG_METHOD(passThroughOnException); @@ -221,6 +246,9 @@ class ExecutionContext: public jsg::Object { JSG_LAZY_INSTANCE_PROPERTY(exports, getExports); } JSG_LAZY_INSTANCE_PROPERTY(props, getProps); + if (flags.getEnableVersionApi()) { + JSG_LAZY_INSTANCE_PROPERTY(version, getVersion); + } if (flags.getWorkerdExperimental()) { // TODO(soon): Before making this generally available we need to: @@ -236,28 +264,57 @@ class ExecutionContext: public jsg::Object { JSG_METHOD(abort); } + // TODO(soon): This is getting unwieldy. if (flags.getEnableCtxExports()) { - JSG_TS_OVERRIDE( { - readonly props: Props; - readonly exports: Cloudflare.Exports; - }); + if (flags.getEnableVersionApi()) { + JSG_TS_OVERRIDE( { + readonly props: Props; + readonly exports: Cloudflare.Exports; + readonly version?: { + readonly metadata?: { readonly id: string; }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; + }); + } else { + JSG_TS_OVERRIDE( { + readonly props: Props; + readonly exports: Cloudflare.Exports; + }); + } } else { - JSG_TS_OVERRIDE( { - readonly props: Props; - }); + if (flags.getEnableVersionApi()) { + JSG_TS_OVERRIDE( { + readonly props: Props; + readonly version?: { + readonly metadata?: { readonly id: string; }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; + }); + } else { + JSG_TS_OVERRIDE( { + readonly props: Props; + }); + } } } void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("props", props); + tracker.trackField("version", version); } private: jsg::JsRef exports; jsg::JsRef props; + kj::Maybe> version; void visitForGc(jsg::GcVisitor& visitor) { visitor.visit(props); + visitor.visit(version); } }; diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index f31d42d0719..98dfaa3901c 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -1424,7 +1424,8 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { $compatEnableFlag("enable_version_api") $experimental; # Enables version-related APIs. This currently only enables the `version` option in loopback - # bindings to specify a requested version. The behaviour of this flag will change in the future. + # bindings to specify a requested version and exposes `ctx.version`. The behaviour of this flag + # will change in the future. websocketBinaryTypeDefault @166 :Bool $compatEnableFlag("websocket_standard_binary_type") @@ -1494,4 +1495,11 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { # WebSocket readyState is CLOSED (3) when the close event fires. Previously, # no close reply was sent and readyState was CLOSING (2). The WebSocket spec # requires readyState to be CLOSED when the close event is dispatched. + + enableCtxVersionMetadata @172 :Bool + $compatEnableFlag("enable_ctx_version_metadata") + $experimental; + # When paired with `enable_version_api`, also exposes `ctx.version.metadata`. This is a separate + # flag as we haven't decided on the exact behaviour of `ctx.version.metadata`, but the rest of + # `ctx.version` is much more well defined. The behaviour of this flag will change in the future. } diff --git a/src/workerd/io/worker-entrypoint.c++ b/src/workerd/io/worker-entrypoint.c++ index 1114fd38850..f57a268f210 100644 --- a/src/workerd/io/worker-entrypoint.c++ +++ b/src/workerd/io/worker-entrypoint.c++ @@ -59,6 +59,7 @@ class WorkerEntrypoint final: public WorkerInterface { bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan); kj::Promise request(kj::HttpMethod method, @@ -89,6 +90,7 @@ class WorkerEntrypoint final: public WorkerInterface { kj::Maybe entrypointName; Frankenvalue props; kj::Maybe cfBlobJson; + kj::Maybe versionInfo; // Hacky members used to hold some temporary state while processing a request. // See gory details in WorkerEntrypoint::request(). @@ -122,7 +124,8 @@ class WorkerEntrypoint final: public WorkerInterface { bool tunnelExceptions, kj::Maybe entrypointName, Frankenvalue props, - kj::Maybe cfBlobJson); + kj::Maybe cfBlobJson, + kj::Maybe versionInfo); }; // Simple wrapper around `HttpService::Response` to let us know if the response was sent @@ -175,11 +178,13 @@ kj::Own WorkerEntrypoint::construct(ThreadContext& threadContex bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan) { TRACE_EVENT("workerd", "WorkerEntrypoint::construct()"); - auto obj = kj::heap(kj::Badge(), threadContext, - waitUntilTasks, tunnelExceptions, entrypointName, kj::mv(props), kj::mv(cfBlobJson)); + auto obj = + kj::heap(kj::Badge(), threadContext, waitUntilTasks, + tunnelExceptions, entrypointName, kj::mv(props), kj::mv(cfBlobJson), kj::mv(versionInfo)); obj->init(kj::mv(worker), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency), kj::mv(ioChannelFactory), kj::addRef(*metrics), kj::mv(workerTracer), kj::mv(maybeTriggerInvocationSpan)); @@ -193,13 +198,15 @@ WorkerEntrypoint::WorkerEntrypoint(kj::Badge badge, bool tunnelExceptions, kj::Maybe entrypointName, Frankenvalue props, - kj::Maybe cfBlobJson) + kj::Maybe cfBlobJson, + kj::Maybe versionInfo) : threadContext(threadContext), waitUntilTasks(waitUntilTasks), tunnelExceptions(tunnelExceptions), entrypointName(entrypointName), props(kj::mv(props)), - cfBlobJson(kj::mv(cfBlobJson)) {} + cfBlobJson(kj::mv(cfBlobJson)), + versionInfo(kj::mv(versionInfo)) {} void WorkerEntrypoint::init(kj::Own worker, kj::Maybe> actor, @@ -321,7 +328,9 @@ kj::Promise WorkerEntrypoint::request(kj::HttpMethod method, return lock.getGlobalScope().request(method, url, headers, requestBody, wrappedResponse, cfBlobJson, lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()), kj::mv(signal)); + lock.getExportedHandler( + entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo)), + kj::mv(signal)); }) .then([this, &context, &wrappedResponse = *wrappedResponse, workerTracer]( api::DeferredProxy deferredProxy) { @@ -598,14 +607,16 @@ kj::Promise WorkerEntrypoint::runScheduled( incomingRequest->delivered(); // Scheduled handlers run entirely in waitUntil() tasks. - context.addWaitUntil(context.run( - [scheduledTime, cron, entrypointName = entrypointName, props = kj::mv(props), &context, - &metrics = incomingRequest->getMetrics()](Worker::Lock& lock) mutable { + context.addWaitUntil( + context.run([scheduledTime, cron, entrypointName = entrypointName, + versionInfo = kj::mv(versionInfo), props = kj::mv(props), &context, + &metrics = incomingRequest->getMetrics()](Worker::Lock& lock) mutable { TRACE_EVENT("workerd", "WorkerEntrypoint::runScheduled() run"); jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); lock.getGlobalScope().startScheduled(scheduledTime, cron, lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo))); })); static auto constexpr waitForFinished = [](IoContext& context, @@ -676,7 +687,8 @@ kj::Promise WorkerEntrypoint::runAlarmImpl( try { auto result = co_await context.run([scheduledTime, retryCount, entrypointName = entrypointName, - props = kj::mv(props), &context](Worker::Lock& lock) mutable { + versionInfo = kj::mv(versionInfo), props = kj::mv(props), + &context](Worker::Lock& lock) mutable { jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); // If we have an invalid timeout, set it to the default value of 15 minutes. @@ -686,7 +698,8 @@ kj::Promise WorkerEntrypoint::runAlarmImpl( timeout = 15 * kj::MINUTES; } - auto handler = lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()); + auto handler = lock.getExportedHandler( + entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo)); return lock.getGlobalScope().runAlarm(scheduledTime, timeout, retryCount, lock, handler); }); @@ -752,15 +765,17 @@ kj::Promise WorkerEntrypoint::test() { incomingRequest->delivered(); - context.addWaitUntil(context.run([entrypointName = entrypointName, props = kj::mv(props), - &context, &metrics = incomingRequest->getMetrics()]( - Worker::Lock& lock) mutable -> kj::Promise { + context.addWaitUntil( + context.run([entrypointName = entrypointName, versionInfo = kj::mv(versionInfo), + props = kj::mv(props), &context, &metrics = incomingRequest->getMetrics()]( + Worker::Lock& lock) mutable -> kj::Promise { TRACE_EVENT("workerd", "WorkerEntrypoint::test() run"); jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); return context.awaitJs(lock, - lock.getGlobalScope().test( - lock, lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()))); + lock.getGlobalScope().test(lock, + lock.getExportedHandler( + entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo)))); })); static auto constexpr waitForFinished = @@ -871,11 +886,13 @@ kj::Own newWorkerEntrypoint(ThreadContext& threadContext, bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan) { return WorkerEntrypoint::construct(threadContext, kj::mv(worker), kj::mv(entrypointName), kj::mv(props), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency), kj::mv(ioChannelFactory), kj::mv(metrics), waitUntilTasks, tunnelExceptions, - kj::mv(workerTracer), kj::mv(cfBlobJson), kj::mv(maybeTriggerInvocationSpan)); + kj::mv(workerTracer), kj::mv(cfBlobJson), kj::mv(versionInfo), + kj::mv(maybeTriggerInvocationSpan)); } } // namespace workerd diff --git a/src/workerd/io/worker-entrypoint.h b/src/workerd/io/worker-entrypoint.h index 6ed7ae38c5e..7bf41b1a06f 100644 --- a/src/workerd/io/worker-entrypoint.h +++ b/src/workerd/io/worker-entrypoint.h @@ -41,6 +41,7 @@ kj::Own newWorkerEntrypoint(ThreadContext& threadContext, bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, // The trigger invocation span may be propagated from other request. If it is provided, // the implication is that this worker entrypoint is being created as a subrequest or // subtask of another request. If it is kj::none, then this invocation is a top-level diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index 143c597ec72..7662c9aee99 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -2309,7 +2309,10 @@ static inline kj::Own fakeOwn(T& ref) { } kj::Maybe> Worker::Lock::getExportedHandler( - kj::Maybe name, Frankenvalue props, kj::Maybe actor) { + kj::Maybe name, + Frankenvalue props, + kj::Maybe actor, + kj::Maybe versionInfo) { KJ_IF_SOME(a, actor) { KJ_IF_SOME(h, a.getHandler()) { return fakeOwn(h); @@ -2323,7 +2326,8 @@ kj::Maybe> Worker::Lock::getExportedHandler( jsg::Lock& js = *this; auto handler = kj::heap(cls(js, js.alloc(js, - jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js)), + jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js), + kj::mv(versionInfo)), KJ_ASSERT_NONNULL(worker.impl->env).addRef(js))); // HACK: We set handler.env and handler.ctx to undefined because we already passed the real @@ -2340,7 +2344,8 @@ kj::Maybe> Worker::Lock::getExportedHandler( if (!FeatureFlags::get(js).getReuseCtxAcrossNonclassEvents()) { api::ExportedHandler constructedHandler = h.clone(js); constructedHandler.ctx = js.alloc(js, - jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js)); + jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js), + kj::mv(versionInfo)); return kj::heap(kj::mv(constructedHandler)); } return fakeOwn(h); diff --git a/src/workerd/io/worker.h b/src/workerd/io/worker.h index fac5362a67f..3a2b842c768 100644 --- a/src/workerd/io/worker.h +++ b/src/workerd/io/worker.h @@ -83,6 +83,8 @@ struct EntrypointClasses { jsg::JsObject workflowEntrypoint; }; +WD_STRONG_BOOL(PopulateVersionInfoMetadata); + // An instance of a Worker. // // Typically each worker script is loaded into a single Worker instance which is reused by @@ -154,6 +156,44 @@ class Worker: public kj::AtomicRefcounted { } }; + // Version information associated with a worker. These are made available through `ctx.version`. + struct VersionInfo { + kj::String id; + kj::Maybe cohort; + kj::Maybe key; + kj::Maybe versionOverride; + + VersionInfo clone() const { + return { + .id = kj::str(id), + .cohort = cohort.map([](const kj::String& s) { return kj::str(s); }), + .key = key.map([](const kj::String& s) { return kj::str(s); }), + .versionOverride = versionOverride.map([](const kj::String& s) { return kj::str(s); }), + }; + } + + jsg::JsValue toJs( + jsg::Lock& js, PopulateVersionInfoMetadata populateVersionInfoMetadata) const { + auto version = js.obj(); + if (populateVersionInfoMetadata) { + auto metadata = js.obj(); + metadata.set(js, "id"_kj, js.str(id)); + version.set(js, "metadata"_kj, metadata); + } + KJ_IF_SOME(someCohort, cohort) { + version.set(js, "cohort"_kj, js.str(someCohort)); + } + KJ_IF_SOME(someKey, key) { + version.set(js, "key"_kj, js.str(someKey)); + } + KJ_IF_SOME(someVersionOverride, versionOverride) { + version.set(js, "override"_kj, js.str(someVersionOverride)); + } + version.recursivelyFreeze(js); + return version; + }; + }; + explicit Worker(kj::Own script, kj::Own metrics, kj::FunctionParam> getExportedHandler( - kj::Maybe entrypointName, Frankenvalue props, kj::Maybe actor); + kj::Maybe entrypointName, + Frankenvalue props, + kj::Maybe actor, + kj::Maybe versionInfo = kj::none); // Get the C++ object representing the global scope. api::ServiceWorkerGlobalScope& getGlobalScope(); diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index e4d13c96403..7bb9429afc5 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -2188,14 +2188,16 @@ class Server::WorkerService final: public Service, kj::Own observer = kj::refcounted(mapAddRef(workerTracer), waitUntilTasks); - return newWorkerEntrypoint(threadContext, kj::atomicAddRef(*worker), entrypointName, - kj::mv(props), kj::mv(actor), kj::Own(this, kj::NullDisposer::instance), - {}, // ioContextDependency + return newWorkerEntrypoint( + threadContext, kj::atomicAddRef(*worker), entrypointName, kj::mv(props), kj::mv(actor), + kj::Own(this, kj::NullDisposer::instance), {}, // ioContextDependency kj::Own(this, kj::NullDisposer::instance), kj::mv(observer), waitUntilTasks, true, // tunnelExceptions kj::mv(workerTracer), // workerTracer - kj::mv(metadata.cfBlobJson)); + kj::mv(metadata.cfBlobJson), + kj::none // versionInfo + ); } class ActorNamespace final { diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 5eaf7c6d33f..cccaf66ea6a 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -492,6 +492,14 @@ interface ExecutionContext { passThroughOnException(): void; readonly exports: Cloudflare.Exports; readonly props: Props; + readonly version?: { + readonly metadata?: { + readonly id: string; + }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; abort(reason?: any): void; } type ExportedHandlerFetchHandler< diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index dfe677e59d5..0d146f69917 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -494,6 +494,14 @@ export interface ExecutionContext { passThroughOnException(): void; readonly exports: Cloudflare.Exports; readonly props: Props; + readonly version?: { + readonly metadata?: { + readonly id: string; + }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; abort(reason?: any): void; } export type ExportedHandlerFetchHandler< From 6cf8ef5f14eaf011e8c1746cd8a4500984bdec91 Mon Sep 17 00:00:00 2001 From: Sophie Wallace Date: Fri, 6 Mar 2026 18:46:44 +0000 Subject: [PATCH 2/2] Set VersionInfo when fetching CustomEvent entrypoints --- src/workerd/api/hibernatable-web-socket.c++ | 15 ++-- src/workerd/api/hibernatable-web-socket.h | 1 + src/workerd/api/queue.c++ | 7 +- src/workerd/api/queue.h | 2 + src/workerd/api/trace.c++ | 9 ++- src/workerd/api/trace.h | 1 + src/workerd/api/worker-rpc.c++ | 14 ++-- src/workerd/api/worker-rpc.h | 1 + src/workerd/io/trace-stream.c++ | 12 ++- src/workerd/io/trace-stream.h | 1 + src/workerd/io/worker-entrypoint.c++ | 12 +-- src/workerd/io/worker-interface.h | 2 + src/workerd/io/worker.c++ | 4 +- src/workerd/io/worker.h | 87 +++++++++++---------- src/workerd/tests/test-fixture.c++ | 5 +- 15 files changed, 104 insertions(+), 69 deletions(-) diff --git a/src/workerd/api/hibernatable-web-socket.c++ b/src/workerd/api/hibernatable-web-socket.c++ index d6613a2ee7f..564f1493017 100644 --- a/src/workerd/api/hibernatable-web-socket.c++ +++ b/src/workerd/api/hibernatable-web-socket.c++ @@ -60,6 +60,7 @@ jsg::Ref HibernatableWebSocketEvent::claimWebSocket( kj::Promise HibernatableWebSocketCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { // Mark the request as delivered because we're about to run some JS. @@ -82,29 +83,33 @@ kj::Promise HibernatableWebSocketCustomEve try { co_await context.run( [entrypointName = entrypointName, &context, eventParameters = kj::mv(eventParameters), - props = kj::mv(props)](Worker::Lock& lock) mutable { + versionInfo = kj::mv(versionInfo), props = kj::mv(props)](Worker::Lock& lock) mutable { KJ_SWITCH_ONEOF(eventParameters.eventType) { KJ_CASE_ONEOF(text, HibernatableSocketParams::Text) { return lock.getGlobalScope().sendHibernatableWebSocketMessage(context, kj::mv(text.message), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_CASE_ONEOF(data, HibernatableSocketParams::Data) { return lock.getGlobalScope().sendHibernatableWebSocketMessage(context, kj::mv(data.message), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_CASE_ONEOF(close, HibernatableSocketParams::Close) { return lock.getGlobalScope().sendHibernatableWebSocketClose(context, kj::mv(close), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_CASE_ONEOF(e, HibernatableSocketParams::Error) { return lock.getGlobalScope().sendHibernatableWebSocketError(context, kj::mv(e.error), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_UNREACHABLE; } diff --git a/src/workerd/api/hibernatable-web-socket.h b/src/workerd/api/hibernatable-web-socket.h index 9f7f5b13b67..84da6c6d9db 100644 --- a/src/workerd/api/hibernatable-web-socket.h +++ b/src/workerd/api/hibernatable-web-socket.h @@ -66,6 +66,7 @@ class HibernatableWebSocketCustomEvent final: public WorkerInterface::CustomEven kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/api/queue.c++ b/src/workerd/api/queue.c++ index c3d6c611396..e329f6a27c4 100644 --- a/src/workerd/api/queue.c++ +++ b/src/workerd/api/queue.c++ @@ -573,6 +573,7 @@ tracing::EventInfo QueueCustomEvent::getEventInfo() const { kj::Promise QueueCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { // This method has three main chunks of logic: @@ -594,14 +595,16 @@ kj::Promise QueueCustomEvent::run( // 2. This is where we call into the worker's queue event handler auto runProm = context.run( [this, entrypointName = entrypointName, &context, queueEvent = kj::addRef(*queueEventHolder), - &metrics = incomingRequest->getMetrics(), + &metrics = incomingRequest->getMetrics(), versionInfo = kj::mv(versionInfo), props = kj::mv(props)](Worker::Lock& lock) mutable { jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); auto& typeHandler = lock.getWorker().getIsolate().getApi().getQueueTypeHandler(lock); auto startResp = startQueueEvent(lock.getGlobalScope(), context, kj::mv(params), context.addObject(result), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()), typeHandler); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()), + typeHandler); queueEvent->event = kj::mv(startResp.event); queueEvent->exportedHandlerProm = kj::mv(startResp.exportedHandlerProm); queueEvent->isServiceWorkerHandler = startResp.isServiceWorkerHandler; diff --git a/src/workerd/api/queue.h b/src/workerd/api/queue.h index 23dd5913cb1..cd424871686 100644 --- a/src/workerd/api/queue.h +++ b/src/workerd/api/queue.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -341,6 +342,7 @@ class QueueCustomEvent final: public WorkerInterface::CustomEvent, public kj::Re kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/api/trace.c++ b/src/workerd/api/trace.c++ index 972e2cf9cdc..afd4eebe016 100644 --- a/src/workerd/api/trace.c++ +++ b/src/workerd/api/trace.c++ @@ -592,6 +592,7 @@ jsg::Ref UnsafeTraceMetrics::fromTrace(jsg::Lock& js, jsg::Ref sendTracesToExportedHandler(kj::Own incomingRequest, kj::Maybe entrypointNamePtr, + kj::Maybe versionInfo, Frankenvalue props, kj::ArrayPtr> traces) { // Mark the request as delivered because we're about to run some JS. @@ -614,10 +615,11 @@ kj::Promise sendTracesToExportedHandler(kj::Own incomingRequest, kj::Maybe entrypointNamePtr, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) -> kj::Promise { // Don't bother to wait around for the handler to run, just hand it off to the waitUntil tasks. waitUntilTasks.add(sendTracesToExportedHandler( - kj::mv(incomingRequest), entrypointNamePtr, kj::mv(props), traces)); + kj::mv(incomingRequest), entrypointNamePtr, kj::mv(versionInfo), kj::mv(props), traces)); // Reporting a proper outcome and return event here would be nice, but for that we'd need to await // running the tail handler... diff --git a/src/workerd/api/trace.h b/src/workerd/api/trace.h index 9cf9e6b50f0..ea3742854bd 100644 --- a/src/workerd/api/trace.h +++ b/src/workerd/api/trace.h @@ -618,6 +618,7 @@ class TraceCustomEvent final: public WorkerInterface::CustomEvent { kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/api/worker-rpc.c++ b/src/workerd/api/worker-rpc.c++ index 1b82db92909..a7c9d8f7475 100644 --- a/src/workerd/api/worker-rpc.c++ +++ b/src/workerd/api/worker-rpc.c++ @@ -1971,6 +1971,7 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { public: EntrypointJsRpcTarget(IoContext& ioCtx, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::Maybe wrapperModule, kj::Maybe> tracer) @@ -1979,6 +1980,7 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { // Most of the time we don't really have to clone this but it's hard to fully prove, so // let's be safe. entrypointName(entrypointName.map([](kj::StringPtr s) { return kj::str(s); })), + versionInfo(kj::mv(versionInfo)), props(kj::mv(props)), wrapperModule(kj::mv(wrapperModule)), tracer(kj::mv(tracer)) {} @@ -1997,9 +1999,9 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { TargetInfo getTargetInfo(Worker::Lock& lock, IoContext& ioCtx) override { jsg::Lock& js = lock; - auto handler = - KJ_REQUIRE_NONNULL(lock.getExportedHandler(entrypointName, kj::mv(props), ioCtx.getActor()), - "Failed to get handler to worker."); + auto handler = KJ_REQUIRE_NONNULL(lock.getExportedHandler(entrypointName, kj::mv(versionInfo), + kj::mv(props), ioCtx.getActor()), + "Failed to get handler to worker."); if (handler->missingSuperclass && wrapperModule == kj::none) { // JS RPC is not enabled on the server side, we cannot call any methods. @@ -2063,6 +2065,7 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { private: IoContext& ioCtx; kj::Maybe entrypointName; + kj::Maybe versionInfo; Frankenvalue props; kj::Maybe wrapperModule; kj::Maybe> tracer; @@ -2152,6 +2155,7 @@ class JsRpcSessionCustomEvent::ServerTopLevelMembrane final: public capnp::Membr kj::Promise JsRpcSessionCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { IoContext& ioctx = incomingRequest->getContext(); @@ -2164,8 +2168,8 @@ kj::Promise JsRpcSessionCustomEvent::run( waitUntilTasks.add(incomingRequest->drain().attach(kj::mv(incomingRequest))); }); - EntrypointJsRpcTarget target(ioctx, entrypointName, kj::mv(props), kj::mv(wrapperModule), - mapAddRef(incomingRequest->getWorkerTracer())); + EntrypointJsRpcTarget target(ioctx, entrypointName, kj::mv(versionInfo), kj::mv(props), + kj::mv(wrapperModule), mapAddRef(incomingRequest->getWorkerTracer())); capnp::RevocableServer revcableTarget(target); try { diff --git a/src/workerd/api/worker-rpc.h b/src/workerd/api/worker-rpc.h index b02a3095bce..afaa37cb2f9 100644 --- a/src/workerd/api/worker-rpc.h +++ b/src/workerd/api/worker-rpc.h @@ -468,6 +468,7 @@ class JsRpcSessionCustomEvent final: public WorkerInterface::CustomEvent { kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/io/trace-stream.c++ b/src/workerd/io/trace-stream.c++ index 5e6d6c98cdc..d743c3ac575 100644 --- a/src/workerd/io/trace-stream.c++ +++ b/src/workerd/io/trace-stream.c++ @@ -608,10 +608,12 @@ class TailStreamTarget final: public rpc::TailStreamTarget::Server { public: TailStreamTarget(IoContext& ioContext, kj::Maybe entrypointNamePtr, + kj::Maybe versionInfo, Frankenvalue props, kj::Own> doneFulfiller) : weakIoContext(ioContext.getWeakRef()), entrypointNamePtr(kj::mv(entrypointNamePtr)), + versionInfo(kj::mv(versionInfo)), props(kj::mv(props)), doneFulfiller(kj::mv(doneFulfiller)) {} @@ -723,8 +725,8 @@ class TailStreamTarget final: public rpc::TailStreamTarget::Server { events.size() == 1 && events[0].event.is(), "Expected only a single onset event"); auto& event = events[0]; - auto handler = KJ_REQUIRE_NONNULL( - lock.getExportedHandler(entrypointNamePtr, kj::mv(props), ioContext.getActor()), + auto handler = KJ_REQUIRE_NONNULL(lock.getExportedHandler(entrypointNamePtr, + kj::mv(versionInfo), kj::mv(props), ioContext.getActor()), "Failed to get handler to worker."); StringCache stringCache; @@ -916,6 +918,7 @@ class TailStreamTarget final: public rpc::TailStreamTarget::Server { kj::Own weakIoContext; kj::Maybe entrypointNamePtr; + kj::Maybe versionInfo; Frankenvalue props; // The done fulfiller is resolved when we receive the outcome event // or rejected if the capability is dropped before receiving the outcome @@ -939,14 +942,15 @@ EventInfo TailStreamCustomEvent::getEventInfo() const { kj::Promise TailStreamCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { IoContext& ioContext = incomingRequest->getContext(); incomingRequest->delivered(); auto [donePromise, doneFulfiller] = kj::newPromiseAndFulfiller(); - capFulfiller->fulfill(kj::heap( - ioContext, kj::mv(entrypointName), kj::mv(props), kj::mv(doneFulfiller))); + capFulfiller->fulfill(kj::heap(ioContext, kj::mv(entrypointName), + kj::mv(versionInfo), kj::mv(props), kj::mv(doneFulfiller))); donePromise = donePromise.attach(ioContext.registerPendingEvent()); diff --git a/src/workerd/io/trace-stream.h b/src/workerd/io/trace-stream.h index 740e9fba545..7d76c187dbf 100644 --- a/src/workerd/io/trace-stream.h +++ b/src/workerd/io/trace-stream.h @@ -28,6 +28,7 @@ class TailStreamCustomEvent final: public WorkerInterface::CustomEvent { kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/io/worker-entrypoint.c++ b/src/workerd/io/worker-entrypoint.c++ index f57a268f210..0a9458d007e 100644 --- a/src/workerd/io/worker-entrypoint.c++ +++ b/src/workerd/io/worker-entrypoint.c++ @@ -329,7 +329,7 @@ kj::Promise WorkerEntrypoint::request(kj::HttpMethod method, return lock.getGlobalScope().request(method, url, headers, requestBody, wrappedResponse, cfBlobJson, lock, lock.getExportedHandler( - entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo)), + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()), kj::mv(signal)); }) .then([this, &context, &wrappedResponse = *wrappedResponse, workerTracer]( @@ -616,7 +616,7 @@ kj::Promise WorkerEntrypoint::runScheduled( lock.getGlobalScope().startScheduled(scheduledTime, cron, lock, lock.getExportedHandler( - entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo))); + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); })); static auto constexpr waitForFinished = [](IoContext& context, @@ -699,7 +699,7 @@ kj::Promise WorkerEntrypoint::runAlarmImpl( } auto handler = lock.getExportedHandler( - entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo)); + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()); return lock.getGlobalScope().runAlarm(scheduledTime, timeout, retryCount, lock, handler); }); @@ -775,7 +775,7 @@ kj::Promise WorkerEntrypoint::test() { return context.awaitJs(lock, lock.getGlobalScope().test(lock, lock.getExportedHandler( - entrypointName, kj::mv(props), context.getActor(), kj::mv(versionInfo)))); + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()))); })); static auto constexpr waitForFinished = @@ -825,7 +825,9 @@ kj::Promise WorkerEntrypoint::customEvent( t.setEventInfo(*incomingRequest, event->getEventInfo()); } - auto promise = event->run(kj::mv(incomingRequest), entrypointName, kj::mv(props), waitUntilTasks) + auto promise = event + ->run(kj::mv(incomingRequest), entrypointName, kj::mv(versionInfo), + kj::mv(props), waitUntilTasks) .attach(kj::mv(event)); // TODO(cleanup): In theory `context` may have been destroyed by now if `event->run()` dropped diff --git a/src/workerd/io/worker-interface.h b/src/workerd/io/worker-interface.h index f68085d9382..43c66714c2a 100644 --- a/src/workerd/io/worker-interface.h +++ b/src/workerd/io/worker-interface.h @@ -17,6 +17,7 @@ namespace workerd { class Frankenvalue; class IoContext_IncomingRequest; +struct Worker_VersionInfo; // An interface representing the services made available by a worker/pipeline to handle a // request. @@ -115,6 +116,7 @@ class WorkerInterface: public kj::HttpService { // for this event. virtual kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) = 0; diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index 7662c9aee99..b343711b43e 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -2310,9 +2310,9 @@ static inline kj::Own fakeOwn(T& ref) { kj::Maybe> Worker::Lock::getExportedHandler( kj::Maybe name, + kj::Maybe versionInfo, Frankenvalue props, - kj::Maybe actor, - kj::Maybe versionInfo) { + kj::Maybe actor) { KJ_IF_SOME(a, actor) { KJ_IF_SOME(h, a.getHandler()) { return fakeOwn(h); diff --git a/src/workerd/io/worker.h b/src/workerd/io/worker.h index 3a2b842c768..05f180b901f 100644 --- a/src/workerd/io/worker.h +++ b/src/workerd/io/worker.h @@ -83,8 +83,6 @@ struct EntrypointClasses { jsg::JsObject workflowEntrypoint; }; -WD_STRONG_BOOL(PopulateVersionInfoMetadata); - // An instance of a Worker. // // Typically each worker script is loaded into a single Worker instance which is reused by @@ -97,6 +95,7 @@ WD_STRONG_BOOL(PopulateVersionInfoMetadata); // for a class name to end in "Instance". ("I have an instance of WorkerInstance...") class Worker: public kj::AtomicRefcounted { public: + using VersionInfo = Worker_VersionInfo; class Script; class Isolate; class Api; @@ -156,44 +155,6 @@ class Worker: public kj::AtomicRefcounted { } }; - // Version information associated with a worker. These are made available through `ctx.version`. - struct VersionInfo { - kj::String id; - kj::Maybe cohort; - kj::Maybe key; - kj::Maybe versionOverride; - - VersionInfo clone() const { - return { - .id = kj::str(id), - .cohort = cohort.map([](const kj::String& s) { return kj::str(s); }), - .key = key.map([](const kj::String& s) { return kj::str(s); }), - .versionOverride = versionOverride.map([](const kj::String& s) { return kj::str(s); }), - }; - } - - jsg::JsValue toJs( - jsg::Lock& js, PopulateVersionInfoMetadata populateVersionInfoMetadata) const { - auto version = js.obj(); - if (populateVersionInfoMetadata) { - auto metadata = js.obj(); - metadata.set(js, "id"_kj, js.str(id)); - version.set(js, "metadata"_kj, metadata); - } - KJ_IF_SOME(someCohort, cohort) { - version.set(js, "cohort"_kj, js.str(someCohort)); - } - KJ_IF_SOME(someKey, key) { - version.set(js, "key"_kj, js.str(someKey)); - } - KJ_IF_SOME(someVersionOverride, versionOverride) { - version.set(js, "override"_kj, js.str(someVersionOverride)); - } - version.recursivelyFreeze(js); - return version; - }; - }; - explicit Worker(kj::Own script, kj::Own metrics, kj::FunctionParam> getExportedHandler( kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, - kj::Maybe actor, - kj::Maybe versionInfo = kj::none); + kj::Maybe actor); // Get the C++ object representing the global scope. api::ServiceWorkerGlobalScope& getGlobalScope(); @@ -1071,6 +1033,47 @@ class Worker::Actor final: public kj::Refcounted { kj::Promise ensureConstructedImpl(IoContext&, ActorClassInfo& info); }; +WD_STRONG_BOOL(PopulateVersionInfoMetadata); + +// Version information associated with a worker. These are made available through `ctx.version`. +// This is also available through the preferred `Worker::VersionInfo` alias. `Worker_VersionInfo` +// exists to allow for forward declaration in a couple of niche places. +struct Worker_VersionInfo { + kj::String id; + kj::Maybe cohort; + kj::Maybe key; + kj::Maybe versionOverride; + + Worker_VersionInfo clone() const { + return { + .id = kj::str(id), + .cohort = cohort.map([](const kj::String& s) { return kj::str(s); }), + .key = key.map([](const kj::String& s) { return kj::str(s); }), + .versionOverride = versionOverride.map([](const kj::String& s) { return kj::str(s); }), + }; + } + + jsg::JsValue toJs(jsg::Lock& js, PopulateVersionInfoMetadata populateVersionInfoMetadata) const { + auto version = js.obj(); + if (populateVersionInfoMetadata) { + auto metadata = js.obj(); + metadata.set(js, "id"_kj, js.str(id)); + version.set(js, "metadata"_kj, metadata); + } + KJ_IF_SOME(someCohort, cohort) { + version.set(js, "cohort"_kj, js.str(someCohort)); + } + KJ_IF_SOME(someKey, key) { + version.set(js, "key"_kj, js.str(someKey)); + } + KJ_IF_SOME(someVersionOverride, versionOverride) { + version.set(js, "override"_kj, js.str(someVersionOverride)); + } + version.recursivelyFreeze(js); + return version; + } +}; + // ======================================================================================= // inline implementation details diff --git a/src/workerd/tests/test-fixture.c++ b/src/workerd/tests/test-fixture.c++ index 7e7fafe4cb0..9b2586ebb27 100644 --- a/src/workerd/tests/test-fixture.c++ +++ b/src/workerd/tests/test-fixture.c++ @@ -478,7 +478,10 @@ TestFixture::Response TestFixture::runRequest( runInIoContext([&](const TestFixture::Environment& env) { auto& globalScope = env.lock.getGlobalScope(); return globalScope.request(method, url, requestHeaders, *requestBody, response, "{}"_kj, - env.lock, env.lock.getExportedHandler(kj::none, {}, kj::none), /* abortSignal */ kj::none); + env.lock, + env.lock.getExportedHandler(/*entryPointName=*/kj::none, /*versionInfo=*/kj::none, + /*props=*/{}, /*actor=*/kj::none), + /*abortSignal=*/kj::none); }); return {.statusCode = response.statusCode, .body = response.body->str()};