Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement automatic self-referential bindings via ctx.exports. #3447

Merged
merged 2 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/workerd/api/actor-state.c++
Original file line number Diff line number Diff line change
Expand Up @@ -826,9 +826,11 @@ kj::OneOf<jsg::Ref<DurableObjectId>, kj::StringPtr> ActorState::getId() {
}

DurableObjectState::DurableObjectState(Worker::Actor::Id actorId,
jsg::JsRef<jsg::JsValue> exports,
kj::Maybe<jsg::Ref<DurableObjectStorage>> storage,
kj::Maybe<rpc::Container::Client> container)
: id(kj::mv(actorId)),
exports(kj::mv(exports)),
storage(kj::mv(storage)),
container(container.map(
[&](rpc::Container::Client& cap) { return jsg::alloc<Container>(kj::mv(cap)); })) {}
Expand Down
11 changes: 11 additions & 0 deletions src/workerd/api/actor-state.h
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,16 @@ class WebSocketRequestResponsePair: public jsg::Object {
class DurableObjectState: public jsg::Object {
public:
DurableObjectState(Worker::Actor::Id actorId,
jsg::JsRef<jsg::JsValue> exports,
kj::Maybe<jsg::Ref<DurableObjectStorage>> storage,
kj::Maybe<rpc::Container::Client> container);

void waitUntil(kj::Promise<void> promise);

jsg::JsValue getExports(jsg::Lock& js) {
return exports.getHandle(js);
}

kj::OneOf<jsg::Ref<DurableObjectId>, kj::StringPtr> getId();

jsg::Optional<jsg::Ref<DurableObjectStorage>> getStorage() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -581,6 +591,7 @@ class DurableObjectState: public jsg::Object {

private:
Worker::Actor::Id id;
jsg::JsRef<jsg::JsValue> exports;
kj::Maybe<jsg::Ref<DurableObjectStorage>> storage;
kj::Maybe<jsg::Ref<Container>> container;

Expand Down
18 changes: 16 additions & 2 deletions src/workerd/api/global-scope.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> promise);
void passThroughOnException();
Expand All @@ -214,13 +218,22 @@ class ExecutionContext: public jsg::Object {
// and throwing an error at the client.
void abort(jsg::Lock& js, jsg::Optional<jsg::Value> reason);

jsg::JsValue getExports(jsg::Lock& js) {
return exports.getHandle(js);
}

jsg::JsValue getProps(jsg::Lock& js) {
return props.getHandle(js);
}

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()) {
Expand All @@ -243,6 +256,7 @@ class ExecutionContext: public jsg::Object {
}

private:
jsg::JsRef<jsg::JsValue> exports;
jsg::JsRef<jsg::JsValue> props;

void visitForGc(jsg::GcVisitor& visitor) {
Expand Down
23 changes: 17 additions & 6 deletions src/workerd/io/worker.c++
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ struct Worker::Impl {

// The environment blob to pass to handlers.
kj::Maybe<jsg::Value> env;
kj::Maybe<jsg::Value> ctxExports;

struct ActorClassInfo {
EntrypointClass cls;
Expand Down Expand Up @@ -1550,8 +1551,10 @@ kj::Maybe<jsg::JsObject> tryResolveMainModule(jsg::Lock& js,

Worker::Worker(kj::Own<const Script> scriptParam,
kj::Own<WorkerObserver> metricsParam,
kj::FunctionParam<void(jsg::Lock& lock, const Api& api, v8::Local<v8::Object> target)>
compileBindings,
kj::FunctionParam<void(jsg::Lock& lock,
const Api& api,
v8::Local<v8::Object> target,
v8::Local<v8::Object> ctxExports)> compileBindings,
IsolateObserver::StartType startType,
TraceParentContext spans,
LockType lockType,
Expand Down Expand Up @@ -1639,7 +1642,9 @@ Worker::Worker(kj::Own<const Script> scriptParam,
lock.v8Set(bindingsScope, global.name, global.value);
}

compileBindings(lock, script->isolate->getApi(), bindingsScope);
v8::Local<v8::Object> ctxExports = v8::Object::New(lock.v8Isolate);

compileBindings(lock, script->isolate->getApi(), bindingsScope, ctxExports);

// Execute script.
currentSpan = maybeMakeSpan("lw:top_level_execution"_kjc);
Expand All @@ -1656,6 +1661,7 @@ Worker::Worker(kj::Own<const Script> scriptParam,
KJ_IF_SOME(ns,
tryResolveMainModule(lock, mainModule, *jsContext, *script, limitErrorOrTime)) {
impl->env = lock.v8Ref(bindingsScope.As<v8::Value>());
impl->ctxExports = lock.v8Ref(ctxExports.As<v8::Value>());

auto& api = script->isolate->getApi();
auto handlers = api.unwrapExports(lock, ns);
Expand All @@ -1670,7 +1676,7 @@ Worker::Worker(kj::Own<const Script> 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<api::ExecutionContext>(lock);
obj.ctx = jsg::alloc<api::ExecutionContext>(lock, jsg::JsValue(ctxExports));

impl->namedHandlers.insert(kj::mv(handler.name), kj::mv(obj));
}
Expand Down Expand Up @@ -1974,7 +1980,9 @@ kj::Maybe<kj::Own<api::ExportedHandler>> 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<api::ExecutionContext>(js, props.toJs(js)),
auto handler = kj::heap(cls(js,
jsg::alloc<api::ExecutionContext>(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
Expand Down Expand Up @@ -3446,7 +3454,10 @@ void Worker::Actor::ensureConstructed(IoContext& context) {
storage = impl->makeStorage(lock, worker->getIsolate().getApi(), *c);
}
auto handler = info.cls(lock,
jsg::alloc<api::DurableObjectState>(cloneId(), kj::mv(storage), kj::mv(impl->container)),
jsg::alloc<api::DurableObjectState>(cloneId(),
jsg::JsRef<jsg::JsValue>(
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
Expand Down
10 changes: 7 additions & 3 deletions src/workerd/io/worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,19 @@ class Worker: public kj::AtomicRefcounted {

explicit Worker(kj::Own<const Script> script,
kj::Own<WorkerObserver> metrics,
kj::FunctionParam<void(jsg::Lock& lock, const Api& api, v8::Local<v8::Object> target)>
compileBindings,
kj::FunctionParam<void(jsg::Lock& lock,
const Api& api,
v8::Local<v8::Object> target,
v8::Local<v8::Object> ctxExports)> compileBindings,
IsolateObserver::StartType startType,
TraceParentContext spans,
LockType lockType,
kj::Maybe<ValidationErrorReporter&> errorReporter = kj::none,
kj::Maybe<kj::Duration&> 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);
Expand Down
78 changes: 78 additions & 0 deletions src/workerd/server/server-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -4006,6 +4006,84 @@ 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),
` await actor.grault(456),
` "UnconfiguredActor" in ctx.exports, // should be false
` ].join(", "));
` },
` corge(i) { return `corge: ${i}` }
`}
`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}` }
`}
`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}` }
`}
)
],
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, grault: 456, false");
}

// =======================================================================================

// TODO(beta): Test TLS (send and receive)
Expand Down
Loading
Loading