Skip to content
Draft
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
7 changes: 7 additions & 0 deletions src/cloudflare/internal/workers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export class WorkflowEntrypoint {
env: unknown;
}

export class ContainerEntrypoint {
constructor(ctx: unknown, env: unknown);

ctx: unknown;
env: unknown;
}

export class RpcStub {
constructor(server: object);
}
Expand Down
10 changes: 10 additions & 0 deletions src/cloudflare/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import innerEnv from 'cloudflare-internal:env';

export const WorkerEntrypoint = entrypoints.WorkerEntrypoint;
export const DurableObject = entrypoints.DurableObject;

// Add ping method to ContainerEntrypoint prototype
const ContainerEntrypointBase = entrypoints.ContainerEntrypoint;
class ContainerEntrypointWithMethods extends ContainerEntrypointBase {
ping(): string {
return 'pong';
}
}
export const ContainerEntrypoint = ContainerEntrypointWithMethods;

export const RpcStub = entrypoints.RpcStub;
export const RpcPromise = entrypoints.RpcPromise;
export const RpcProperty = entrypoints.RpcProperty;
Expand Down
6 changes: 6 additions & 0 deletions src/workerd/api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,12 @@ wd_test(
data = ["actor-alarms-test.js"],
)

wd_test(
src = "container-entrypoint-test.wd-test",
args = ["--experimental"],
data = ["container-entrypoint-test.js"],
)

wd_test(
src = "tail-worker-test.wd-test",
args = [
Expand Down
27 changes: 27 additions & 0 deletions src/workerd/api/container-entrypoint-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2017-2025 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

import { ContainerEntrypoint } from 'cloudflare:workers';
import assert from 'node:assert';

// Example ContainerEntrypoint that extends the base class
export class MyContainer extends ContainerEntrypoint {
async customMethod() {
return 'custom response';
}
}

// Test that ContainerEntrypoint can be instantiated and ping method works
export const testPingMethod = {
async test(ctrl, env, ctx) {
// Get a stub to MyContainer
const id = env.MY_CONTAINER.idFromName('test-container');
const stub = env.MY_CONTAINER.get(id);

// Call the built-in ping method
const pingResult = await stub.ping();
assert.strictEqual(pingResult, 'pong', 'ping() should return "pong"');
},
};

27 changes: 27 additions & 0 deletions src/workerd/api/container-entrypoint-test.wd-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
services = [
(name = "main", worker = .mainWorker),
(name = "TEST_TMPDIR", disk = (writable = true)),
],
);

const mainWorker :Workerd.Worker = (
compatibilityDate = "2025-01-01",
compatibilityFlags = ["experimental", "nodejs_compat"],

modules = [
(name = "worker", esModule = embed "container-entrypoint-test.js"),
],

durableObjectNamespaces = [
(className = "MyContainer", uniqueKey = "container-test-unique-key-123"),
],

durableObjectStorage = (localDisk = "TEST_TMPDIR"),

bindings = [
(name = "MY_CONTAINER", durableObjectNamespace = "MyContainer"),
],
);
12 changes: 12 additions & 0 deletions src/workerd/api/workers-module.c++
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ jsg::Ref<WorkflowEntrypoint> WorkflowEntrypoint::constructor(
return js.alloc<WorkflowEntrypoint>();
}

jsg::Ref<ContainerEntrypoint> ContainerEntrypoint::constructor(
const v8::FunctionCallbackInfo<v8::Value>& args,
jsg::Ref<DurableObjectState> ctx,
jsg::JsObject env) {
jsg::Lock& js = jsg::Lock::from(args.GetIsolate());

jsg::JsObject self(args.This());
self.set(js, "ctx", jsg::JsValue(args[0]));
self.set(js, "env", jsg::JsValue(args[1]));
return js.alloc<ContainerEntrypoint>();
}

void EntrypointsModule::waitUntil(kj::Promise<void> promise) {
// No need to check if IoContext::hasCurrent since current() will throw
// if there is no active request.
Expand Down
24 changes: 23 additions & 1 deletion src/workerd/api/workers-module.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ class WorkflowEntrypoint: public jsg::Object {
JSG_RESOURCE_TYPE(WorkflowEntrypoint) {}
};

// Base class for Containers
//
// When the worker's top-level module exports a class that extends this class, it means that it
// is a Container entrypoint, similar to Durable Objects but with container-specific functionality.
//
// import { ContainerEntrypoint } from "cloudflare:workers";
// export class MyContainer extends ContainerEntrypoint {
// // ping() is provided by the base class
// }
//
// `env` and `ctx` are automatically available as `this.env` and `this.ctx`, without the need to
// define a constructor.
class ContainerEntrypoint: public jsg::Object {
public:
static jsg::Ref<ContainerEntrypoint> constructor(const v8::FunctionCallbackInfo<v8::Value>& args,
jsg::Ref<DurableObjectState> ctx,
jsg::JsObject env);

JSG_RESOURCE_TYPE(ContainerEntrypoint) {}
};

// The "cloudflare:workers" module, which exposes the WorkerEntrypoint, WorkflowEntrypoint and DurableObject types
// for extending.
class EntrypointsModule: public jsg::Object {
Expand All @@ -82,6 +103,7 @@ class EntrypointsModule: public jsg::Object {
JSG_RESOURCE_TYPE(EntrypointsModule) {
JSG_NESTED_TYPE(WorkerEntrypoint);
JSG_NESTED_TYPE(WorkflowEntrypoint);
JSG_NESTED_TYPE(ContainerEntrypoint);
JSG_NESTED_TYPE_NAMED(DurableObjectBase, DurableObject);
JSG_NESTED_TYPE_NAMED(JsRpcPromise, RpcPromise);
JSG_NESTED_TYPE_NAMED(JsRpcProperty, RpcProperty);
Expand All @@ -94,7 +116,7 @@ class EntrypointsModule: public jsg::Object {
};

#define EW_WORKERS_MODULE_ISOLATE_TYPES \
api::WorkerEntrypoint, api::WorkflowEntrypoint, api::DurableObjectBase, api::EntrypointsModule
api::WorkerEntrypoint, api::WorkflowEntrypoint, api::ContainerEntrypoint, api::DurableObjectBase, api::EntrypointsModule

template <class Registry>
void registerWorkersModule(Registry& registry, CompatibilityFlags::Reader flags) {
Expand Down
7 changes: 7 additions & 0 deletions src/workerd/io/worker.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1866,6 +1866,13 @@ void Worker::processEntrypointClass(jsg::Lock& js,
.missingSuperclass = false,
});
return;
} else if (handle == entrypointClasses.containerEntrypoint) {
impl->actorClasses.insert(kj::mv(handlerName),
ActorClassInfo{
.cls = kj::mv(cls),
.missingSuperclass = false,
});
return;
} else if (handle == entrypointClasses.workerEntrypoint) {
impl->statelessClasses.insert(kj::mv(handlerName), kj::mv(cls));
return;
Expand Down
3 changes: 3 additions & 0 deletions src/workerd/io/worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ struct EntrypointClasses {
// Class constructor for DurableObject (aka api::DurableObjectBase).
jsg::JsObject durableObject;

// Class constructor for ContainerEntrypoint
jsg::JsObject containerEntrypoint;

// Class constructor for WorkflowEntrypoint
jsg::JsObject workflowEntrypoint;
};
Expand Down
8 changes: 8 additions & 0 deletions src/workerd/server/tests/container-client/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ wd_test(
data = ["test.js"],
tags = ["off-by-default"],
)

wd_test(
size = "enormous",
src = "test-entrypoint.wd-test",
args = ["--experimental"],
data = ["test-entrypoint.js"],
tags = ["off-by-default"],
)
Loading
Loading