One web surface, shared by every OpenWorkers runtime, written against a native contract and nothing else.
Before this repo the surface existed in five diverging copies, one per runtime,
as JavaScript inlined in Rust string literals. openworkers-conformance
measured what that costs: of the 95 WinterTC Minimum Common
API tests the v8 backend fails,
77 fail on all four JavaScript backends. Five copies mean five corrections
for one gap.
js/ is the surface, one classic script per API, assigning onto
globalThis. It is written against the contract, the engine intrinsics and the
rest of the surface, never against an embedding: nothing in a module knows which
runtime is running it.
src/ is the contract. Each module carries the ops it reads out of the
native namespace, and the crate hands a host both the script and that list
through include_str!, so a host cannot pick up a module without picking up what
it needs.
test/ is the inner loop: the contract mocked in pure JavaScript, so the
whole surface runs under Bun with no engine embedding at all. Each sandbox is a
real node:vm context, so a bare new Headers() inside a module finds the one
this surface installed rather than Bun's, and a module that asks for a global
the host never installs fails here instead of in production. The cost is a realm
boundary: an assertion about what the surface built compares against the
sandbox's constructors, which intrinsics() hands back.
openworkers-conformance stays the integration judge, with 448 tests and a
streaming battery through a real runtime, but it is not where you want to find
out that a signature is wrong.
bun test # the surface against the mocked contract, no engine
cargo test # the crate| module | ops it asks of its host |
|---|---|
events |
none |
abort |
none |
streams |
none |
url |
urlParse, urlUpdate |
url-pattern |
urlPatternParse, urlPatternProcessInput |
headers |
none |
request |
none |
response |
none |
A module may build on another, and on what the host installs around them
(setTimeout, console, queueMicrotask, ReadableStream); SURFACE is the
order that makes that true. required_ops covers the native namespace alone.
streams extends the prototype of the host's ReadableStream rather than
replacing it: the host backs that one with its own channel, and the streaming
battery in openworkers-conformance measures what happens on it.
DISPATCH is not in SURFACE. It is the dispatch between the host and the
guest's handlers: addEventListener, export default, respondWith,
waitUntil and the task results. The script evaluates to a function; the host
calls it after the surface and before the guest script, with its engine's
streamBody(response, ended) and disconnect(response), and keeps the
{ fetch, task } it answers. A second argument, { strict: true }, makes
respondWith throw InvalidStateError once the dispatch has ended, as the
Service Worker spec has it. Each of these answers a handle whose answer,
done and streamed promises always fulfil, so the host reads how far an
event got from them and not from globals. A fetch handle also carries
marks: late when a listener called respondWith after the dispatch, and
afterSettle when the async listeners had already ended then. The Service
Worker spec refuses both; the dispatch accepts them and lets the host count
them. The dispatch ends when the host calls the handle's endDispatch(),
after the microtask checkpoint that follows its call. Every listener added
for a type runs, in order, until one calls respondWith.
A module never captures a host function; it reads it out of the namespace at call time, every time. Native functions cannot be serialised into a V8 startup snapshot, so a snapshotting host installs the JavaScript once at build time and the native functions again per context at run time. A class body that captured an op while being snapshotted would capture nothing, and every later context would call into the hole. Reading late is the only shape that serves a snapshotting host and an eval-at-boot host at once.
The cost is that the namespace stays reachable from guest code. A host installs it non-enumerable, neither writable nor configurable, and freezes it once every op is registered, so a guest can neither find it by enumeration nor replace the namespace or an op under the surface's feet.
A module reads it as globalThis.__ow.<op>, never as a bare __ow. The global
belongs to the guest, and a guest's top-level let __ow would be a global
lexical binding that shadows the global-object property for every bare lookup
that follows, this surface's included.
A module is evaluated as a classic script in the guest's own global, where a
top-level const becomes a global lexical binding: readable by guest code, and a
duplicate-declaration SyntaxError for any guest that declares the same name at
its own top level. A module that needs a private helper wraps itself in an IIFE
and touches nothing but globalThis.
url-pattern is the clearest case. The pattern syntax is a grammar with a
tokenizer, a parser and a compilation step, and the conformance suite has two
tests for it: an implementation written here could be wrong in a dozen ways and
still pass both. So the grammar crosses to the host, which answers with the
eight components, each carrying a matcher and a regexp source in ECMAScript
syntax, and the matching stays here on the engine's own RegExp.
Nothing is held on the host side between calls: both ops are pure functions, and a compiled pattern is plain data the guest owns. A resource table would have been a leak waiting for a guest that builds patterns in a loop.
A host is free to answer any op with a native fast path, and encouraged to: the
v8 backend took URL from 46 to 68 of 68 by handing parsing to the url crate.
The contract is what makes that safe, since a fast path has to honour the same
signature and the conformance suite says whether it does. What the contract ends
is five engines each reimplementing Headers.