Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/spring-cleaning-browser-host.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
"@mcp-b/do-runtime": minor
---

Ship the Worker half of the browser alarm protocol. `createBrowserAlarmProjector()`
supplies the `AlarmScheduler`'s `projectWake` and an `acknowledge()` for the
`BrowserAlarmCoordinator`'s `deliver()`. Projections leave one at a time, and each draws
its generation only after the previous one was sent. A consumed wake is acknowledged only
after the latest projection is accepted, no delivery or cleanup is active, and the next
wake is absent or later. If the latest projection failed, `acknowledge()` sends it again
first, so a scheduler with nothing new to project cannot leave the wake retrying forever.
`nextGeneration()` must be durable across Worker restarts: the coordinator silently drops
any projection older than the generation it journaled, so an in-memory counter would stall
every wake after a restart. `parseBrowserAlarmProjection()` is now exported.

Add `connectMessagePortWebSocket()` to `@mcp-b/do-runtime/browser`. It routes one
MessagePort socket through a Workers-style `fetch` such as the Agents SDK's
`routeAgentRequest()`. A socket nothing routes closes with 1011, and a refused upgrade
closes with 1008 and the refusal's text when it is printable ASCII of at most 123 bytes.
Previously every failure closed with a generic 1011 that dropped the Agent's reason.
`serveMessagePortWebSockets()` now takes `(bridge, url) => Promise<void>`, such as
`(bridge, url) => connectMessagePortWebSocket(bridge, url, route)`, instead of a function
resolving a URL to a socket. It reports a connection failure after closing the client,
and a socket that finishes connecting after `stop()` now closes with
"MessagePort transport closed" instead of "host stopped".

Gate a hibernatable socket that is a host transport, such as a `MessagePortWebSocket`
rehydrated after a Worker restart. The actor used to receive the transport itself, so its
`send()` and `close()` could leave before a preceding storage write was confirmed. It now
receives one stable socket per transport that waits for the output gate like a
`WebSocketPair` half; hibernation hosts still see the transport.

`OffscreenDocumentAdapter` gains optional `ready()` and `replaceUnready()` hooks. The
coordinator runs readiness in the same single flight as creation, for new and existing
documents, and replaces a document that fails it at most once.

Fix two `MessagePortWebSocket` hangs. A throwing `onmessage`, `onopen` or `onclose`
handler skipped the `addEventListener` listeners behind it; during the open flush it also
dropped the queued frames and held every later frame in the queue. Handler errors are now
reported the way `EventTarget` reports a throwing listener. A throw while bridging a
connected socket, such as a second `accept()`, left the brokered client connecting; it now
closes with 1011.
24 changes: 24 additions & 0 deletions .changeset/spring-cleaning-maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@mcp-b/do-runtime": patch
---

Validate both `withEnvAndExports()` scopes before installing either. A non-object `exports`
argument previously left the `env` scope installed for the whole realm, so every later `env`
read resolved against it.

Treat a `Timer.afterDelay` that rejects on abort, as `node:timers/promises` does, as
cancellation. Replacing or deleting a waiting alarm previously raised an unhandled
`AbortError`, which terminates a Node host by default. The `Timer` contract now states that
an aborted wait may stay pending or reject, but must not resolve.

Close the databases `createActorContainer()` opened when an open-time check refuses, such as
storage written by a newer release. Each refused attempt previously leaked its handles: a
retry opened another connection to the same OPFS file, and on Node the provider's
`exportSnapshot()` refused from then on. A `FacetHost.abort` that throws is now recorded like a
failed facet deletion instead of becoming an unhandled rejection.

Report `rowsWritten` as 0 for statements without result columns that write nothing, in both
SQLite backends. DDL previously repeated the last write's count, because SQLite does not
reset `sqlite3_changes()` for it. `SqliteWasmActorStorage.copyFrom()` now reads every source
file in the same task as its recovery-sidecar check, so a source that is still running cannot
open a write transaction between the two.
15 changes: 15 additions & 0 deletions .changeset/spring-cleaning-opfs-pool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@mcp-b/do-runtime": minor
---

Add `installSqliteWasmHost()` to `@mcp-b/do-runtime/backends/sqlite-wasm`. It installs an OPFS SAH
pool through sqlite-wasm's `installOpfsSAHPoolVfs()` with the `name`, `directory`, `clearOnInit`
and `initialCapacity` options, and returns the `{ pool, capi }` host the sqlite-wasm provider
takes. Concurrent calls for one pool share a single install. Install pools through it:

- It waits up to 10 seconds for a terminated worker to release the pool before installing, then
rejects with a `NoModificationAllowedError`. Installing while the previous worker was still
shutting down could delete the pool's directory and every database in it.
- A transaction interrupted by worker termination is now rolled back when its database is
reopened. Previously, reopening could expose the interrupted transaction's writes in place of
committed rows and left the recovery journal behind, so snapshot export stayed refused.
14 changes: 14 additions & 0 deletions .changeset/spring-cleaning-scope-and-vite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@mcp-b/do-runtime": minor
---

Export `ACTOR_SCOPE_GLOBALS` from `@mcp-b/do-runtime` and `facetScopeBanner()` from
`@mcp-b/do-runtime/vite`. The banner binds every name `installActorScope()` writes to a
facet bundle's own scope. Banners that bound only timers, `fetch` and `crypto` left
`WebSocketPair` on the root actor's global, so a facet's socket frames waited on the root's
output gate and could leave before the facet's own write committed.

`doRuntimeAwaitTransform()` now resolves the imports it injects to the package's own files,
ahead of any host alias for them, so hosts no longer alias `@mcp-b/do-runtime/gate` or
`@mcp-b/do-runtime/browser/async-hooks`. Add a `@mcp-b/do-runtime/cloudflare-email` export,
a data-only `EmailMessage` for hosts to alias `cloudflare:email` to.
21 changes: 21 additions & 0 deletions .changeset/spring-cleaning-transformed-await-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@mcp-b/do-runtime": patch
---

Keep the input lock and the implicit transaction across transformed awaits that settle while
the actor still holds its lock. `doRuntimeAwaitTransform()` previously resumed every await
through a macrotask hop and a fresh input lock queued behind waiting events, including awaits
of storage calls and plain values. Another event could then run between `await
storage.get()` and the following `put()`, so two concurrent read-modify-writes lost an update.
The implicit transaction also committed at the await, so writes survived an abort that workerd
rolls back. A transformed `await ctx.blockConcurrencyWhile()` let queued events run before its
caller resumed. Every consumer that transforms the Agents SDK was exposed on its storage
sequences.

A transformed await now continues in the same checkpoint when its promise settles while the
actor holds its lock: storage calls, plain values, and resumptions the runtime already admitted.
Settled awaits no longer pay a macrotask each. An await on foreign I/O still re-enters through
a fresh lock. When another actor's continuation owns the checkpoint, the await waits for a later
task without releasing its lock, and its implicit transaction commits at that hand-off. Code that
relied on a storage-only await to let other events in now blocks them, as on workerd. The Node
and browser conformance lanes now also run the suite with the probe compiled by the transform.
5 changes: 0 additions & 5 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,5 @@ updates:
actions:
patterns:
- "*"
ignore:
# changesets/action v2 requires Changesets CLI v3; we are on CLI v2.
# Lift this when the CLI is upgraded.
- dependency-name: changesets/action
update-types: ["version-update:semver-major"]
commit-message:
prefix: "ci(deps)"
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,15 @@ jobs:
- name: Test browser conformance
run: pnpm test:conformance-browser

- name: Build examples
run: pnpm examples:build
- name: Test transformed Node conformance
run: pnpm test:conformance-node-transformed

- name: Test transformed browser conformance
run: pnpm test:conformance-browser-transformed

# The extension e2e builds the extension itself; vibe's e2e uses the dev server.
- name: Build vibe-platform for production
run: pnpm --filter do-runtime-example-vibe-platform build

- name: Test examples
run: pnpm test:examples
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
.sdk-pack/
.tsbuild/
.wrangler/
__screenshots__/
Expand Down
Loading
Loading