A guest SDK for OpenWorkers, source-compatible with Cloudflare's
worker crate 0.8.
An existing workers-rs application moves over by renaming one dependency:
# before
worker = { version = "0.8", features = ["d1"] }
# after
worker = { package = "openworkers-worker", version = "0.1", features = ["d1"] }The same use worker::*, the same #[event(fetch)] and #[event(scheduled)],
the same Request/Response/Headers/Env/D1Database. Underneath there is
no JavaScript, no wasm-bindgen and no V8: the crate targets wasm32-wasip2 and
speaks wasi:http/proxy@0.2.0 plus openworkers:bindings@0.1.0 to a wasmtime
host.
cargo build --target wasm32-wasip2 --release
The result is a component. Nothing else is needed: no worker-build, no
wrangler, no JS shim.
use worker::*;
#[event(fetch)]
async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
let db = env.d1("DB")?;
let rows: Vec<Row> = db.prepare("SELECT * FROM targets").all().await?.results()?;
Response::from_json(&rows)
}
#[event(scheduled)]
async fn tick(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
console_log!("cron at {}", event.schedule());
}A worker with only a fetch handler exports wasi:http/incoming-handler alone;
adding #[event(scheduled)] adds openworkers:worker/scheduled, which is how
the host tells a cron-capable worker from one that only serves HTTP.
Renaming the dependency is the whole SDK change. What else has to go is
anything that talks to JavaScript directly, because wasm-bindgen's imports
have no host on wasm32-wasip2 and panic with "cannot call wasm-bindgen
imported functions on non-wasm targets" the first time they run:
- Drop the
console_error_panic_hookdependency and adduse worker::console_error_panic_hook;. The call site stays as it is. - Drop the
wasm-bindgen-futuresdependency and reachspawn_localthroughworker::wasm_bindgen_futures::spawn_local. - Drop
wasmbindfromchrono's features, andwasm-bindgenfromtime's. Both read the clock throughstdonce the feature is off. - Replace any other
js_sys/wasm_bindgenuse.worker::js_sys::Dateandworker::wasm_bindgen::JsValuecover what a D1 or clock call site needs;Reflect,Promise,Uint8Arrayand the rest have no counterpart.
A real workers-rs status page (fetch handler, cron handler, two D1 bindings,
send_email, Fetch webhooks, a router rendering HTML) needed exactly those
four edits and then served its pages and ran its cron unchanged.
| Area | Status |
|---|---|
#[event(fetch)], #[event(scheduled)] |
yes, respond_with_errors included |
Request, Response, ResponseBuilder, Headers, Method, Url |
yes |
http::Request/http::Response conversions, Body |
yes |
Env, Var, Secret (from wasi:cli/environment) |
yes |
Fetch (outbound HTTP) |
yes, through wasi:http/outgoing-handler |
console_log! and friends, panic hook |
yes, to stdout and stderr |
D1 (d1 feature): prepare/bind/first/all/run/exec/batch, query! |
yes |
KV: get/put/delete/list |
yes |
R2: get/put/delete/head/list |
yes |
send_email |
types only; every send reports that the platform has no email operation |
| Queues, Durable Objects, WebSockets, Cache, Images | absent |
- Bodies are buffered. The host buffers request and response bodies at the
boundary, so
Response::from_streamcollects andResponseBodyhas no stream variant. ScheduledEvent::cron()is empty. The host's scheduled export carries only the trigger time.req.cf()is alwaysNone. There is no Cloudflare edge metadata.Bucket::headerrors on a missing key rather than returningOk(None): the host'sheadcannot separate absence from failure. Usegetwhen the two have to be told apart.- KV stores JSON documents. A plain string is stored quoted and unquoted on
the way back;
put_bytesstores a JSON array of bytes. worker::wasm_bindgen::JsValueis a plain tagged value, not a JS handle.from_str,from_f64,from_bool,NULLand theFromimpls work; the rest of wasm-bindgen does not exist.worker::js_syscarriesDate::now()only.#[event(start)],#[event(queue)],#[event(email)]and#[durable_object]are compile errors rather than silent no-ops.
WASI 0.2 exports are synchronous, so an async handler is driven to completion
inside handle by a poll loop over a no-op waker (src/rt.rs). Every host
call at 0.2 has a blocking form, so a guest future never parks waiting on the
host and no reactor is needed; futures::executor::block_on would park the
thread instead, and a wasm32-wasip2 guest has no second thread to unpark it.
ctx.wait_until and wasm_bindgen_futures::spawn_local queue tasks that the
same loop drains before the export returns.
.- the SDKwit/- verbatim copy of the runtime's contract; the SDK generates against itsfetch-workerandscheduled-onlyworldsmacros/-#[event]and friendsintegration/- runs the examples throughopenworkers-runtime-wasmexamples/hello- the smallest workerexamples/health-shapes- the shapes a real status page uses
cd examples/hello && cargo build --target wasm32-wasip2 --release
cd ../.. && cargo test -p openworkers-worker-integration
MIT.