@mcp-b/do-runtime 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +27 -4
- package/dist/chunks/{io-context-RmmjNtwm.js → io-context-BBgKEsdR.js} +12 -3
- package/dist/chunks/{io-context-RmmjNtwm.js.map → io-context-BBgKEsdR.js.map} +1 -1
- package/dist/conformance/host.d.ts +16 -2
- package/dist/conformance.js.map +1 -1
- package/dist/gate.js +1 -1
- package/dist/index.js +740 -206
- package/dist/index.js.map +1 -1
- package/dist/src/api/actor-state.d.ts +30 -21
- package/dist/src/api/global-scope.d.ts +12 -3
- package/dist/src/api/sql.d.ts +17 -6
- package/dist/src/api/web-socket.d.ts +88 -73
- package/dist/src/index.d.ts +4 -4
- package/dist/src/io/io-context.d.ts +1 -0
- package/dist/src/server/actor-container.d.ts +16 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0821d6e: Replace the fail-closed Durable Object WebSocket stubs with workerd-compatible hibernatable WebSockets.
|
|
8
|
+
|
|
9
|
+
`WebSocketPair`, `WebSocketRequestResponsePair`, all eight `DurableObjectState` WebSocket methods, tags, structured-clone attachments, auto-responses, event timeouts, close state, and `webSocketMessage`/`webSocketClose`/`webSocketError` dispatch now run through actor input and output gates. `installActorScope()` installs the three WebSocket globals alongside the existing actor-scoped primitives.
|
|
10
|
+
|
|
11
|
+
Embedders that evict live actors can mirror socket state through the new optional `ports.hibernation` callbacks and rehydrate it through `ActorContainerOptions.webSockets` before the next constructor runs. `container.quiescence()` exposes the non-blocking eviction signals, and `gateHooks` makes both gates observable.
|
|
12
|
+
|
|
13
|
+
This is a breaking replacement for the exported hibernation-unavailable error and the previous `never`-typed methods. Hosts should remove reconnect-only fallbacks; applications can use the Agents SDK and PartyServer hibernation defaults.
|
|
14
|
+
|
|
15
|
+
## 0.5.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- bef6bd8: `storage.put()` now reads its arguments the way JSG does, instead of silently dropping writes.
|
|
20
|
+
|
|
21
|
+
A non-string, non-object key previously fell into the multi-key overload, where `Object.entries` on a primitive is `[]` — so the call resolved having written nothing. It now stringifies the key as the `kj::String` alternative does: `put(123, v)` writes `"123"`, `put(null, v)` writes `"null"`, `put(undefined, v)` writes `"undefined"`, `put([["a", 1]], v)` writes `"a,1"`, and a symbol key throws V8's own conversion error.
|
|
22
|
+
|
|
23
|
+
Once the key does unwrap as a dictionary, a second argument that is a non-null primitive is now refused with upstream's own message rather than spread into the options — `put({a: 1}, "v")` wrote key `a` with `{0: "v"}` for options. The refusal models workerd's all-optional options struct exactly: `null`, arrays and functions all unwrap to default options and the write proceeds, as measured on real workerd. Functions take the dictionary alternative in the KEY position too, so `put(function f(){}, v)` is refused rather than writing a row keyed on the function's source text.
|
|
24
|
+
|
|
25
|
+
Measured on real workerd and pinned by a conformance row that runs on all three lanes.
|
|
26
|
+
|
|
27
|
+
- bef6bd8: `sql.exec()` and `sql.ingest()` now refuse the SQL forms workerd's authorizer refuses but its regulator never sees. SQL that uses them throws where it previously ran.
|
|
28
|
+
|
|
29
|
+
`ATTACH`, `DETACH`, the temp-schema creations, and virtual-table modules outside upstream's four reach SQLite action codes rather than `SqlStorageRegulator` callbacks, so porting the regulator whole left them unguarded. Each refuses with workerd's own message, `not authorized: SQLITE_AUTH`, except `VACUUM`, which carries SQLite's `cannot VACUUM from within a transaction: SQLITE_ERROR` because upstream refuses it by the transaction a Durable Object always has open.
|
|
30
|
+
|
|
31
|
+
`ATTACH` was an isolation boundary and not only a fidelity gap: both backends open a real database file, so application SQL could attach another actor's database and read every application table in it — the reserved-name scan sees only the submitted statement — and `VACUUM INTO` could write any file the process can. The temp schema is refused by both of its spellings, `CREATE TEMP TABLE` and `CREATE TABLE temp.t`; the second was the live gap, where the table was created, written and read back. Of the virtual-table modules, `dbstat` is the one that mattered: it reports a row per table, so it enumerated the runtime's own `_cf_` tables and their sizes without the statement ever naming them. The table name and the module accept every quoting SQLite does — double quotes, backticks, brackets, and the misquoting feature's single-quoted string — with the module resolved before the allowlist, exactly as workerd's authorizer sees it; a `CREATE VIRTUAL TABLE` whose module cannot be read is refused outright.
|
|
32
|
+
|
|
33
|
+
A leading `;` now counts as trivia for every leading-keyword refusal in the file, including the pre-existing transaction-control and pragma checks. `node:sqlite` reports an empty first statement as part of the span it compiled, so `;ATTACH …` read as a statement whose keyword was `;` and passed all three.
|
|
34
|
+
|
|
35
|
+
`PRAGMA page_size` is now allowed with no argument, as upstream's allowlist has it for the R\*Tree module's own internal read; `PRAGMA page_size = N` is still refused. That was a false refusal — code that runs on Cloudflare failed here.
|
|
36
|
+
|
|
37
|
+
Every form, refused and allowed alike, was measured on real workerd and is pinned by a conformance row that runs on all three lanes.
|
|
38
|
+
|
|
3
39
|
## 0.4.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ Open a second container over the same directory and `increment()` answers `3`: t
|
|
|
157
157
|
|
|
158
158
|
Two runnable browser hosts live in [`examples/`](examples/), each with its own README and Playwright e2e (`pnpm test:examples`):
|
|
159
159
|
|
|
160
|
-
- [`examples/extension/`](examples/extension/) — a Chrome MV3 compatibility harness: service worker → offscreen document (with corpse recovery) → worker hosting an Agents SDK `Counter` and local sub-agents. Proves persistent state, sibling and nested facet isolation, overlapping async work, abort/delete lifecycle, sub-agent scheduling across host recreation, exclusive host ownership,
|
|
160
|
+
- [`examples/extension/`](examples/extension/) — a Chrome MV3 compatibility harness: service worker → offscreen document (with corpse recovery) → worker hosting an Agents SDK `Counter` and local sub-agents. Proves persistent state, sibling and nested facet isolation, overlapping async work, abort/delete lifecycle, sub-agent scheduling across host recreation, exclusive host ownership, hibernating `AgentClient` WebSockets, state sync, callable and streaming RPC, SDK queues, stateless MCP, inbound email routing, the MV3 CSP story (`'wasm-unsafe-eval'`), and `chrome.alarms` recreation of an evicted host before durable alarm delivery.
|
|
161
161
|
- [`examples/vibe-platform/`](examples/vibe-platform/) — a self-contained vibe-coding page that authors both a front-end and an Agents SDK `Agent`, runs them in-tab with durable SQLite-backed state, and exports the unchanged sources as a Wrangler project that passes `wrangler deploy --dry-run`.
|
|
162
162
|
|
|
163
163
|
## Hosting an actor
|
|
@@ -175,6 +175,9 @@ The runtime owns semantics; the host owns placement and substrate. `createActorC
|
|
|
175
175
|
| `ports.facets` | A `FacetHost`: place a child container, abort it, copy or delete its storage. |
|
|
176
176
|
| `ports.timer` | `now()` and `afterDelay()`, captured below any installed actor scope. |
|
|
177
177
|
| `ports.fetch` | Optional global outbound. Absent means `fetch` refuses by name, as a Worker with `globalOutbound: null` does. |
|
|
178
|
+
| `ports.hibernation` | Optional mirror callbacks for accepted sockets, attachment bytes, auto-response changes, and closure. Omit it when the host never rebuilds a live socket placement. |
|
|
179
|
+
| `webSockets` | Socket references and mirrored tags/attachments to register before the new instance constructor runs. |
|
|
180
|
+
| `gateHooks` | Optional input/output gate instrumentation for an embedding host. |
|
|
178
181
|
| `facet` | Present when constructing a local child: its id, depth, and the root-owned `FacetTree`. |
|
|
179
182
|
|
|
180
183
|
The lifecycle:
|
|
@@ -185,6 +188,7 @@ The lifecycle:
|
|
|
185
188
|
4. Use `container.run(fn, signal?)` for events that are not method calls: a WebSocket frame, a host callback. Its signal likewise stops only a queued event, not one already running.
|
|
186
189
|
5. Reach the platform through `container.globals` (or install it with `installActorScope`). For a host-provided promise an actor must await, wrap it once in `container.awaitIo()`.
|
|
187
190
|
6. Watch `container.onBroken`; dispose the placement; recreate it on the next event over the same storage. A failed `blockConcurrencyWhile()` rejects its caller with `BrokenActorError` and breaks the placement with that same error.
|
|
191
|
+
7. Before evicting, inspect `container.quiescence()`. Mirror live sockets through `ports.hibernation`, then build the replacement with `webSockets`; do not reconnect or call `acceptWebSocket()` again.
|
|
188
192
|
|
|
189
193
|
For a standard Durable Object binding, call
|
|
190
194
|
`createDurableObjectNamespace(uniqueKey, channel)` and put the result in `env`
|
|
@@ -221,7 +225,25 @@ Construct one `AlarmScheduler` per namespace over a `SqlDatabase` of its own. It
|
|
|
221
225
|
|
|
222
226
|
On workerd every awaitable thing is an io-context primitive, so "resuming from an await re-enters with a fresh input lock" never needs saying. Here it does. A raw `setTimeout` resolves a promise the runtime does not own; the continuation resumes with an empty invocation stack and the next `ctx.storage` call throws `no input lock available in this context`. That is by design — the alternative is a continuation that silently writes outside the gate.
|
|
223
227
|
|
|
224
|
-
`container.globals` is the complete gated set, bound to that container: `setTimeout`/`clearTimeout`/`setInterval`/`clearInterval` capture the critical section when armed and re-enter when fired; `scheduler.wait()` and `scheduler.yield()` resume under the actor; `fetch()` waits for output locks and releases the input gate while in flight; `crypto` re-enters on async completion;
|
|
228
|
+
`container.globals` is the complete gated set, bound to that container: `setTimeout`/`clearTimeout`/`setInterval`/`clearInterval` capture the critical section when armed and re-enter when fired; `scheduler.wait()` and `scheduler.yield()` resume under the actor; `fetch()` waits for output locks and releases the input gate while in flight; `crypto` re-enters on async completion; and `WebSocketPair` creates runtime-owned socket halves. Install it as the worker's globals (`installActorScope`) when one worker hosts one root, or hand it to application code explicitly when it must not.
|
|
229
|
+
|
|
230
|
+
### Hibernatable WebSockets
|
|
231
|
+
|
|
232
|
+
`ctx.acceptWebSocket(socket, tags)` enables class-method dispatch and the full
|
|
233
|
+
Workers state API: `getWebSockets`, `getTags`, attachments, auto-response pairs
|
|
234
|
+
and timestamps, and the hibernatable event timeout. The runtime works without a
|
|
235
|
+
hibernation port for hosts that keep a container alive.
|
|
236
|
+
|
|
237
|
+
An evicting host implements `ports.hibernation` as a mirror. It retains the same
|
|
238
|
+
raw socket reference plus copied tags and attachment bytes, drops the old
|
|
239
|
+
placement, and supplies that snapshot as `webSockets` on the replacement. The
|
|
240
|
+
registry is populated before the constructor, so SDKs can lazily rebuild their
|
|
241
|
+
connection wrappers without another upgrade or connect hook. Closed sockets are
|
|
242
|
+
removed before `webSocketClose` runs.
|
|
243
|
+
|
|
244
|
+
`container.quiescence()` reports armed timers, pending `waitUntil` work, input
|
|
245
|
+
lock state, and output-gate breakage without waiting. `drainWaitUntil()` is for
|
|
246
|
+
shutdown and intentionally never settles while a live interval remains armed.
|
|
225
247
|
|
|
226
248
|
Actor bundles can also install `doRuntimeAwaitTransform()` from `@mcp-b/do-runtime/vite`. A production build checks the final module graph and fails with transformed/total counts for any included module with an uncovered await; the development transform warns once per module if a transformed await reaches its fail-open path without an actor lock.
|
|
227
249
|
|
|
@@ -231,14 +253,15 @@ The browser cannot reproduce every workerd facility. Where it cannot, the runtim
|
|
|
231
253
|
|
|
232
254
|
| Area | Contract here |
|
|
233
255
|
| --- | --- |
|
|
234
|
-
| Hibernatable WebSockets | Unsupported; named methods throw. Use memory-only sockets and reconnect. |
|
|
235
256
|
| Cloudflare point-in-time recovery and read replication | Unsupported by local SQLite; named methods throw. Bookmarks are development counters, not recovery points. |
|
|
236
257
|
| Actor-class stub serialization | Throws; needs workerd's serializer and channel tokens. |
|
|
237
258
|
| Module-scope `waitUntil`, `cache`, `abortIsolate`, Workers RPC stub constructors | Named `cloudflare:workers` boundaries throw. |
|
|
238
259
|
| `DurableObjectState.abort()` | Breaks later storage and re-entry; cannot synchronously terminate the calling JavaScript slice. |
|
|
239
260
|
| Stored value wire bytes | Browser-safe versioned structured-clone encoding rather than V8's private format; public value types align and legacy JSON rows remain readable. |
|
|
240
261
|
| SQL row counters | Local `rowsRead`/`rowsWritten`, including `sql.ingest()`, use returned rows and SQLite changes; workerd uses unavailable libsql billing counters. |
|
|
241
|
-
| Reserved SQL names | `_cf_` detected from tokenized SQL text, which can reject more than workerd's authorizer. |
|
|
262
|
+
| Reserved SQL names | `_cf_` detected from tokenized SQL text, which can reject more than workerd's authorizer. `ANALYZE` on a reserved table is refused where workerd allows it. |
|
|
263
|
+
| Authorizer-only SQL forms | `ATTACH`, `DETACH`, the temp-schema creations (both `CREATE TEMP …` and the `temp.` qualifier), `VACUUM`, and virtual-table modules outside upstream's four (`fts5`, `fts5vocab`, `rtree`, `rtree_i32`) are refused from the leading keyword, with workerd's own messages. These reach the authorizer's own decisions — action codes and its temp-schema rule — rather than `SqlStorageRegulator` callbacks, so porting the regulator did not carry them. The refused and allowed forms are matched through SQLite's identifier quoting, whitespace or none. `EXPLAIN` in front of a refused form still compiles here, where workerd's authorizer refuses it. |
|
|
264
|
+
| SQL function allowlist | Not enforced. Workerd's authorizer denies any function outside its 138-name `ALLOWED_SQLITE_FUNCTIONS` list; this runtime allows every function the backend compiled, including build-detail readers such as `sqlite_version()` and `sqlite_source_id()`. |
|
|
242
265
|
| PRAGMA allowlist | Workerd's allowlist enforced from tokenized SQL text. A `pragma_*` table-valued function with a string or bound argument is authorized by pragma name only, where workerd's authorizer also sees the resolved argument; the pinned conformance row is the contract. |
|
|
243
266
|
| Node SQLite length limit | Bound and returned strings and blobs are capped at 4 MiB; `node:sqlite` cannot cap an unreturned SQL-computed value. The browser backend uses SQLite's native limit. |
|
|
244
267
|
| Response BYOB readers | Refused; their continuation cannot be re-gated. Use a default reader or `arrayBuffer()`. |
|
|
@@ -995,13 +995,16 @@ var TimeoutManager = class {
|
|
|
995
995
|
const criticalSection = ctx.getCriticalSection();
|
|
996
996
|
const wake = new AbortController();
|
|
997
997
|
state.armed = wake;
|
|
998
|
-
const
|
|
998
|
+
const canceled = new Promise((resolve) => {
|
|
999
|
+
wake.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
1000
|
+
});
|
|
1001
|
+
const fired = Promise.race([this.#timer.afterDelay(state.params.msDelay, wake.signal).then(async () => {
|
|
999
1002
|
if (state.isCanceled) return;
|
|
1000
1003
|
state.armed = void 0;
|
|
1001
1004
|
await this.#fire(ctx, id, state, criticalSection);
|
|
1002
1005
|
}, (exception) => {
|
|
1003
1006
|
if (!state.isCanceled) throw exception;
|
|
1004
|
-
});
|
|
1007
|
+
}), canceled]);
|
|
1005
1008
|
ctx.addWaitUntil(fired);
|
|
1006
1009
|
}
|
|
1007
1010
|
/** ← the body of the `.then` at `io-context.c++:759-816`, which is one `context.run`. */
|
|
@@ -1042,6 +1045,9 @@ var TaskSet = class {
|
|
|
1042
1045
|
});
|
|
1043
1046
|
this.#tasks.add(task);
|
|
1044
1047
|
}
|
|
1048
|
+
size() {
|
|
1049
|
+
return this.#tasks.size;
|
|
1050
|
+
}
|
|
1045
1051
|
/** ← `kj::TaskSet::onEmpty()`. Re-checks, since a task can add another. */
|
|
1046
1052
|
async onEmpty() {
|
|
1047
1053
|
while (this.#tasks.size > 0) await Promise.all([...this.#tasks]);
|
|
@@ -1281,6 +1287,9 @@ var IoContext = class {
|
|
|
1281
1287
|
waitUntilStatus() {
|
|
1282
1288
|
return this.#waitUntilStatus?.exception;
|
|
1283
1289
|
}
|
|
1290
|
+
waitUntilTaskCount() {
|
|
1291
|
+
return this.#waitUntilTasks.size();
|
|
1292
|
+
}
|
|
1284
1293
|
/**
|
|
1285
1294
|
* ← `IncomingRequest::drain()`, actor branch. "For actors, all promises are canceled on
|
|
1286
1295
|
* actor shutdown, not on a fixed timeout, because work doesn't necessarily happen on a
|
|
@@ -1500,4 +1509,4 @@ var IoContext = class {
|
|
|
1500
1509
|
//#endregion
|
|
1501
1510
|
export { hasUserErrorDetail as a, setUserErrorDetail as c, tryCurrentSlice as d, CanceledError as f, captureGateStack as i, tryCurrentContinuation as l, OutputGate as m, IoContext as n, isExceptionFromInputGateBroken as o, InputGate as p, atCheckpointEnd as r, requireInputLock as s, BrokenActorError as t, tryCurrentIoContext as u };
|
|
1502
1511
|
|
|
1503
|
-
//# sourceMappingURL=io-context-
|
|
1512
|
+
//# sourceMappingURL=io-context-BBgKEsdR.js.map
|