@mcp-b/do-runtime 0.5.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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### 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, non-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.
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; accepted WebSocket frames enter through the captured context. 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.
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,7 +253,6 @@ 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. |
@@ -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 fired = this.#timer.afterDelay(state.params.msDelay, wake.signal).then(async () => {
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-RmmjNtwm.js.map
1512
+ //# sourceMappingURL=io-context-BBgKEsdR.js.map