Skip to content

Commit 019b0e3

Browse files
committed
Add first-class WorkerdApi binding
1 parent 08f318d commit 019b0e3

File tree

5 files changed

+165
-89
lines changed

5 files changed

+165
-89
lines changed

src/workerd/api/workerd.c++

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#include "workerd.h"
2+
#include <kj/compat/http.h>
3+
#include <capnp/compat/json.h>
4+
#include <workerd/util/http-util.h>
5+
6+
namespace workerd::api {
7+
8+
kj::Promise<uint> doCreateWorkerRequest(kj::Own<kj::HttpClient> client, kj::String serializedArgs) {
9+
auto& context = IoContext::current();
10+
auto headers = kj::HttpHeaders(context.getHeaderTable());
11+
auto req = client->request(kj::HttpMethod::POST, "http://workerd.local/workers"_kjc, headers, serializedArgs.size());
12+
co_await req.body->write(serializedArgs.begin(), serializedArgs.size());
13+
auto res = co_await req.response;
14+
auto resBody = co_await res.body->readAllText();
15+
if (res.statusCode >= 400) {
16+
JSG_FAIL_REQUIRE(Error, resBody);
17+
}
18+
co_return atoi(resBody.cStr());
19+
}
20+
21+
jsg::Promise<jsg::Ref<Fetcher>> WorkerdApi::newWorker(jsg::Lock& js, jsg::JsValue args) {
22+
auto& context = IoContext::current();
23+
kj::String serializedArgs = args.toJson(js);
24+
auto client = context.getHttpClient(serviceChannel, true, kj::none, "create_worker"_kjc);
25+
auto promise = doCreateWorkerRequest(kj::mv(client), kj::mv(serializedArgs));
26+
return context.awaitIo(js, kj::mv(promise), [](jsg::Lock& js, uint chan) {
27+
return jsg::alloc<Fetcher>(chan, Fetcher::RequiresHostAndProtocol::NO, true);
28+
});
29+
}
30+
31+
} // namespace workerd::api

src/workerd/api/workerd.h

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
3+
#include <workerd/jsg/jsg.h>
4+
#include <workerd/api/http.h>
5+
6+
namespace workerd::api {
7+
8+
// A special binding object that allows for dynamic evaluation.
9+
class WorkerdApi: public jsg::Object {
10+
public:
11+
explicit WorkerdApi(uint serviceChannel): serviceChannel(serviceChannel) {}
12+
13+
jsg::Promise<jsg::Ref<Fetcher>> newWorker(jsg::Lock& js, jsg::JsValue args);
14+
15+
JSG_RESOURCE_TYPE(WorkerdApi) {
16+
JSG_METHOD(newWorker);
17+
}
18+
19+
private:
20+
uint serviceChannel;
21+
};
22+
#define EW_WORKERD_API_ISOLATE_TYPES \
23+
api::WorkerdApi
24+
} // namespace workerd::api

src/workerd/server/server.c++

+92-88
Original file line numberDiff line numberDiff line change
@@ -2334,14 +2334,15 @@ kj::Array<kj::byte> measureConfig(config::Worker::Reader& config) {
23342334
return digest;
23352335
}
23362336

2337-
class Server::WorkerdApiService final: public Service, private WorkerInterface {
2337+
class Server::WorkerdApiService final: public Service {
23382338
// Service used when the service is configured as network service.
23392339

23402340
public:
23412341
WorkerdApiService(Server& server): server(server) {}
23422342

23432343
kj::Own<WorkerInterface> startRequest(IoChannelFactory::SubrequestMetadata metadata) override {
2344-
return { this, kj::NullDisposer::instance };
2344+
auto& context = IoContext::current();
2345+
return kj::heap<WorkerdApiRequestHandler>(*this, context.getWorker());
23452346
}
23462347

23472348
bool hasHandler(kj::StringPtr handlerName) override {
@@ -2351,102 +2352,102 @@ public:
23512352
private:
23522353
Server& server;
23532354

2354-
kj::Promise<void> request(
2355-
kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
2356-
kj::AsyncInputStream& requestBody, kj::HttpService::Response& response) override {
2357-
if (url == "http://workerd.local/workers") {
2358-
return requestBody.readAllText().then([this, &headers, &response](auto confJson) {
2359-
capnp::MallocMessageBuilder confArena;
2360-
capnp::JsonCodec json;
2361-
json.handleByAnnotation<config::NewWorker>();
2362-
auto conf = confArena.initRoot<config::NewWorker>();
2363-
json.decode(confJson, conf);
2355+
class WorkerdApiRequestHandler final: public WorkerInterface {
2356+
public:
2357+
WorkerdApiRequestHandler(WorkerdApiService& parent, const Worker& requester)
2358+
: parent(parent), requester(requester) {}
23642359

2365-
kj::String id = workerd::randomUUID(kj::none);
2360+
kj::Promise<void> request(
2361+
kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
2362+
kj::AsyncInputStream& requestBody, kj::HttpService::Response& response) override {
2363+
if (url != "http://workerd.local/workers") {
2364+
auto out = response.send(404, "Not Found", headers, kj::none);
2365+
auto errMsg = "Unknown workerd API endpoint"_kjc.asBytes();
2366+
co_await out->write(errMsg.begin(), errMsg.size());
2367+
co_return;
2368+
}
23662369

2367-
server.actorConfigs.insert(kj::str(id), {});
2370+
auto confJson = co_await requestBody.readAllText();
23682371

2369-
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement = kj::none;
2370-
if (conf.hasExpectedMeasurement()) {
2371-
auto res = kj::decodeHex(conf.getExpectedMeasurement());
2372-
if (res.hadErrors) {
2373-
auto out = response.send(400, "Bad Request", headers, kj::none);
2374-
auto errMsg = "invalid expected measurement"_kjc.asBytes();
2375-
return out->write(errMsg.begin(), errMsg.size());
2376-
}
2377-
expectedMeasurement = kj::mv(res);
2378-
}
2372+
capnp::MallocMessageBuilder confArena;
2373+
capnp::JsonCodec json;
2374+
json.handleByAnnotation<config::NewWorker>();
2375+
auto conf = confArena.initRoot<config::NewWorker>();
2376+
json.decode(confJson, conf);
23792377

2380-
kj::Maybe<kj::String> configError = kj::none;
2381-
auto workerService = server.makeWorker(
2382-
id, conf.getWorker().asReader(), {},
2383-
[&configError](auto err) { configError = kj::mv(err); },
2384-
kj::mv(expectedMeasurement)
2385-
);
2386-
KJ_IF_SOME(err, configError) {
2387-
throw KJ_EXCEPTION(FAILED, err);
2388-
}
2389-
auto& worker = server.services.insert(kj::str(id), kj::mv(workerService)).value;
2390-
worker->link();
2378+
WorkerService* requesterService;
2379+
KJ_IF_SOME(svc, parent.server.services.find(requester.getName())) {
2380+
requesterService = &kj::downcast<WorkerService>(*svc);
2381+
} else {
2382+
auto out = response.send(500, "Internal Server Error", headers, kj::none);
2383+
auto errMsg = "unable to locate requester service"_kjc.asBytes();
2384+
co_await out->write(errMsg.begin(), errMsg.size());
2385+
co_return;
2386+
}
23912387

2392-
auto resMessage = kj::heap<capnp::MallocMessageBuilder>(); // TODO: not alloc
2393-
auto res = resMessage->initRoot<config::ServiceDesignator>();
2394-
res.setName(id);
2395-
auto resJson = json.encode(res);
2388+
kj::String id = workerd::randomUUID(kj::none);
23962389

2397-
auto out = response.send(201, "Created", headers, kj::none);
2398-
return out->write(resJson.begin(), resJson.size()).attach(kj::mv(out), kj::mv(resJson));
2399-
});
2400-
} else if (url.startsWith("http://workerd.local/workers/"_kjc) &&
2401-
url.endsWith("/events/scheduled")) {
2402-
auto workerId = url.slice(29, 29 + 36);
2403-
return requestBody.readAllText().then([this, workerId = kj::mv(workerId),
2404-
&headers, &response](auto confJson) {
2405-
capnp::MallocMessageBuilder confArena;
2406-
capnp::JsonCodec json;
2407-
json.handleByAnnotation<rpc::Trace::ScheduledEventInfo>();
2408-
auto event = confArena.initRoot<rpc::Trace::ScheduledEventInfo>();
2409-
json.decode(confJson, event);
2410-
2411-
KJ_IF_SOME(svc, server.services.find(workerId)) {
2412-
IoChannelFactory::SubrequestMetadata metadata;
2413-
auto worker = svc->startRequest(kj::mv(metadata));
2414-
kj::Date scheduledTime = kj::UNIX_EPOCH +
2415-
static_cast<long long>(event.getScheduledTime()) * kj::MILLISECONDS;
2416-
auto cron = event.getCron();
2417-
return worker->runScheduled(scheduledTime, cron)
2418-
.then([&response, &headers](auto scheduledResult) {
2419-
return response.send(204, "No Content", headers, kj::none)->write(nullptr, 0);
2420-
}).attach(kj::mv(cron));
2421-
} else {
2422-
return response.send(404, "Not Found", headers, kj::none)->write(nullptr, 0);
2423-
}
2424-
});
2425-
} else {
2426-
return response.send(404, "Not Found", headers, kj::none)->write(nullptr, 0);
2390+
parent.server.actorConfigs.insert(kj::str(id), {});
2391+
2392+
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement = kj::none;
2393+
if (conf.hasExpectedMeasurement()) {
2394+
auto res = kj::decodeHex(conf.getExpectedMeasurement());
2395+
if (res.hadErrors) {
2396+
auto out = response.send(400, "Bad Request", headers, kj::none);
2397+
auto errMsg = "invalid expected measurement"_kjc.asBytes();
2398+
co_await out->write(errMsg.begin(), errMsg.size());
2399+
co_return;
2400+
}
2401+
expectedMeasurement = kj::mv(res);
2402+
}
2403+
2404+
kj::Maybe<kj::String> configError = kj::none;
2405+
auto workerService = parent.server.makeWorker(
2406+
id, conf.getWorker().asReader(), {},
2407+
[&configError](auto err) { configError = kj::mv(err); },
2408+
kj::mv(expectedMeasurement)
2409+
);
2410+
KJ_IF_SOME(err, configError) {
2411+
auto out = response.send(400, "Bad Request", headers, kj::none);
2412+
auto errMsg = kj::str(err);
2413+
co_await out->write(errMsg.begin(), errMsg.size());
2414+
co_return;
2415+
}
2416+
auto& worker = parent.server.services.insert(kj::str(id), kj::mv(workerService)).value;
2417+
worker->link();
2418+
2419+
uint newWorkerChannel = requesterService->addChannel(worker);
2420+
2421+
auto resMsg = kj::str(newWorkerChannel);
2422+
auto out = response.send(201, "Created", headers, kj::none);
2423+
co_await out->write(resMsg.begin(), resMsg.size()).attach(kj::mv(out), kj::mv(resMsg));
2424+
co_return;
24272425
}
2428-
}
24292426

2430-
kj::Promise<void> connect(
2431-
kj::StringPtr host, const kj::HttpHeaders& headers, kj::AsyncIoStream& connection,
2432-
ConnectResponse& tunnel, kj::HttpConnectSettings settings) override {
2433-
throwUnsupported();
2434-
}
2427+
kj::Promise<void> connect(
2428+
kj::StringPtr host, const kj::HttpHeaders& headers, kj::AsyncIoStream& connection,
2429+
ConnectResponse& tunnel, kj::HttpConnectSettings settings) override {
2430+
throwUnsupported();
2431+
}
2432+
void prewarm(kj::StringPtr url) override {}
2433+
kj::Promise<ScheduledResult> runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override {
2434+
throwUnsupported();
2435+
}
2436+
kj::Promise<AlarmResult> runAlarm(kj::Date scheduledTime, uint32_t retryCount) override {
2437+
throwUnsupported();
2438+
}
2439+
kj::Promise<CustomEvent::Result> customEvent(kj::Own<CustomEvent> event) override {
2440+
throwUnsupported();
2441+
}
24352442

2436-
void prewarm(kj::StringPtr url) override {}
2437-
kj::Promise<ScheduledResult> runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override {
2438-
throwUnsupported();
2439-
}
2440-
kj::Promise<AlarmResult> runAlarm(kj::Date scheduledTime, uint32_t retryCount) override {
2441-
throwUnsupported();
2442-
}
2443-
kj::Promise<CustomEvent::Result> customEvent(kj::Own<CustomEvent> event) override {
2444-
throwUnsupported();
2445-
}
2443+
[[noreturn]] void throwUnsupported() {
2444+
JSG_FAIL_REQUIRE(Error, "WorkerdApiService does not support this event type.");
2445+
}
24462446

2447-
[[noreturn]] void throwUnsupported() {
2448-
JSG_FAIL_REQUIRE(Error, "WorkerdApiService does not support this event type.");
2449-
}
2447+
private:
2448+
WorkerdApiService& parent;
2449+
const Worker& requester;
2450+
};
24502451
};
24512452

24522453
// =======================================================================================
@@ -2609,6 +2610,9 @@ static kj::Maybe<WorkerdApi::Global> createBinding(
26092610
binding.getService(),
26102611
kj::mv(errorContext)
26112612
});
2613+
if (binding.getService().getName() == "@workerd") {
2614+
return makeGlobal(Global::WorkerdApi { .subrequestChannel = channel });
2615+
}
26122616
return makeGlobal(Global::Fetcher {
26132617
.channel = channel,
26142618
.requiresHost = true,

src/workerd/server/workerd-api.c++

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <workerd/api/trace.h>
3232
#include <workerd/api/unsafe.h>
3333
#include <workerd/api/urlpattern.h>
34+
#include <workerd/api/workerd.h>
3435
#include <workerd/api/node/node.h>
3536
#include <workerd/io/promise-wrapper.h>
3637
#include <workerd/util/thread-scopes.h>
@@ -70,6 +71,7 @@ JSG_DECLARE_ISOLATE_TYPE(JsgWorkerdIsolate,
7071
EW_CACHE_ISOLATE_TYPES,
7172
EW_CRYPTO_ISOLATE_TYPES,
7273
EW_NSM_ISOLATE_TYPES,
74+
EW_WORKERD_API_ISOLATE_TYPES,
7375
EW_ENCODING_ISOLATE_TYPES,
7476
EW_FORMDATA_ISOLATE_TYPES,
7577
EW_HTML_REWRITER_ISOLATE_TYPES,
@@ -699,6 +701,9 @@ static v8::Local<v8::Value> createBindingValue(
699701
KJ_CASE_ONEOF(nsm, Global::NitroSecureModule) {
700702
value = lock.wrap(context, jsg::alloc<api::NitroSecureModule>());
701703
}
704+
KJ_CASE_ONEOF(workerdApi, Global::WorkerdApi) {
705+
value = lock.wrap(context, jsg::alloc<api::WorkerdApi>(workerdApi.subrequestChannel));
706+
}
702707
}
703708

704709
return value;
@@ -785,6 +790,9 @@ WorkerdApi::Global WorkerdApi::Global::clone() const {
785790
KJ_CASE_ONEOF(nsm, Global::NitroSecureModule) {
786791
result.value = Global::NitroSecureModule {};
787792
}
793+
KJ_CASE_ONEOF(workerdApi, Global::WorkerdApi) {
794+
result.value = workerdApi.clone();
795+
}
788796
}
789797

790798
return result;

src/workerd/server/workerd-api.h

+10-1
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,19 @@ class WorkerdApi final: public Worker::Api {
176176
};
177177
struct UnsafeEval {};
178178
struct NitroSecureModule {};
179+
struct WorkerdApi {
180+
uint subrequestChannel;
181+
182+
WorkerdApi clone() const {
183+
return WorkerdApi {
184+
.subrequestChannel = subrequestChannel,
185+
};
186+
}
187+
};
179188
kj::String name;
180189
kj::OneOf<Json, Fetcher, KvNamespace, R2Bucket, R2Admin, CryptoKey, EphemeralActorNamespace,
181190
DurableActorNamespace, QueueBinding, kj::String, kj::Array<byte>, Wrapped,
182-
AnalyticsEngine, Hyperdrive, UnsafeEval, NitroSecureModule> value;
191+
AnalyticsEngine, Hyperdrive, UnsafeEval, WorkerdApi, NitroSecureModule> value;
183192

184193
Global clone() const;
185194
};

0 commit comments

Comments
 (0)