@mcp-b/do-runtime 0.6.1 → 0.7.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 +18 -0
- package/README.md +251 -67
- package/dist/backends/sqlite-wasm.d.ts +6 -1
- package/dist/backends/sqlite-wasm.js +70 -5
- package/dist/backends/sqlite-wasm.js.map +1 -1
- package/dist/browser/alarm-coordinator.js +159 -0
- package/dist/browser/alarm-coordinator.js.map +1 -0
- package/dist/browser/message-port-websocket.js +243 -0
- package/dist/browser/message-port-websocket.js.map +1 -0
- package/dist/browser/offscreen-document.js +33 -0
- package/dist/browser/offscreen-document.js.map +1 -0
- package/dist/browser.js +1 -1
- package/dist/chunks/{web-socket-W63BdFn5.js → web-socket-PWFZxlBg.js} +6 -7
- package/dist/chunks/web-socket-PWFZxlBg.js.map +1 -0
- package/dist/gate.js +11 -7
- package/dist/gate.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/src/api/web-socket.d.ts +6 -0
- package/dist/src/browser/alarm-coordinator.d.ts +46 -0
- package/dist/src/browser/message-port-websocket.d.ts +62 -0
- package/dist/src/browser/offscreen-document.d.ts +17 -0
- package/dist/src/server/hibernation-mirror.d.ts +2 -0
- package/docs/assets/browser-agent-runtime.png +0 -0
- package/docs/assets/extension-agent-runtime.svg +178 -0
- package/docs/migrations.md +235 -0
- package/package.json +23 -3
- package/dist/chunks/web-socket-W63BdFn5.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3caa712: Move the reusable Chrome-host mechanics out of Rook: add a crash-safe browser alarm coordinator with durable transport recovery, package the MessagePort-backed WebSocket transport used between browser supervisors and actor workers, and share offscreen-document creation and stale-slot recovery.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- a2570d4: Ship a migration guide that separates Cloudflare Durable Object class lifecycle,
|
|
12
|
+
Drizzle application schemas, persisted Agents state, and runtime-owned storage.
|
|
13
|
+
- 41b6ccc: Keep transformed foreign awaits visible to lifecycle checks until their continuations are published, and preserve native synchronous-iterable behavior in transformed `for await` loops.
|
|
14
|
+
|
|
15
|
+
Restore WebSocket auto-response configuration and timestamps when a host recreates an actor from its hibernation mirror.
|
|
16
|
+
|
|
17
|
+
Roll back failed SQLite WASM snapshot replacements and direct storage copies. If rollback also fails, retain the original database images in `SqliteWasmRestoreError.recoverySnapshot` for host recovery. This rollback is in memory; hosts needing replacement to survive process loss should restore into a fresh prefix before switching placement.
|
|
18
|
+
|
|
19
|
+
Correct the minimal-host setup and verify its documented TypeScript configuration and code against the files included in the package.
|
|
20
|
+
|
|
3
21
|
## 0.6.1
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,109 +1,243 @@
|
|
|
1
1
|
# do-runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Cloudflare Durable Objects and Agents SDK code, running locally inside Chrome
|
|
6
|
+
extensions and browser tabs.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { Agent, callable } from "agents";
|
|
10
|
+
|
|
11
|
+
export class Counter extends Agent<Cloudflare.Env, { count: number }> {
|
|
12
|
+
initialState = { count: 0 };
|
|
13
|
+
|
|
14
|
+
@callable()
|
|
15
|
+
increment() {
|
|
16
|
+
this.setState({ count: this.state.count + 1 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@callable()
|
|
20
|
+
async armWake() {
|
|
21
|
+
await this.schedule(5, "scheduledIncrement");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
scheduledIncrement() {
|
|
25
|
+
this.setState({ count: this.state.count + 1 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The [Chrome MV3 example](examples/extension/README.md) runs this ordinary Agent
|
|
31
|
+
inside a module Worker. `setState()` persists to SQLite on OPFS; SDK calls and
|
|
32
|
+
state sync cross a `MessagePort`-backed WebSocket; `schedule()` stores the task
|
|
33
|
+
and projects its wake through `chrome.alarms`. Its end-to-end test destroys the
|
|
34
|
+
offscreen host before the alarm fires, then proves Chrome rebuilds the runtime
|
|
35
|
+
and calls `scheduledIncrement()` against the same state. Cloudflare runs the
|
|
36
|
+
same Agent source inside workerd.
|
|
4
37
|
|
|
5
38
|
[](https://github.com/WebMCP-org/do-runtime/actions)
|
|
6
39
|
[](LICENSE)
|
|
7
40
|
|
|
8
|
-
|
|
41
|
+
## The problem
|
|
42
|
+
|
|
43
|
+
An `Agent` or `DurableObject` class is TypeScript, but its safety comes from the
|
|
44
|
+
host around it. [Workerd](https://github.com/cloudflare/workerd) gives every named
|
|
45
|
+
object a private SQLite database, gates storage access and event continuations,
|
|
46
|
+
holds replies until their writes commit, delivers alarms, routes object-to-object
|
|
47
|
+
calls, and restores hibernatable WebSockets. Cloudflare's Agents SDK relies on
|
|
48
|
+
those rules.
|
|
49
|
+
|
|
50
|
+
A browser has the raw components needed to build that host: dedicated workers,
|
|
51
|
+
WebAssembly, OPFS, and `MessagePort`. It does not assemble them into a Durable
|
|
52
|
+
Object runtime. A storage adapter alone would leave concurrency, commit ordering,
|
|
53
|
+
wake-up, and lifecycle behavior undefined. Those differences surface when two
|
|
54
|
+
requests overlap, Chrome evicts an extension context, or a reply races the write
|
|
55
|
+
it reports.
|
|
56
|
+
|
|
57
|
+
`do-runtime` supplies that missing host. It ports the relevant Durable Object
|
|
58
|
+
behavior from workerd to TypeScript, runs each root actor in a Web Worker, stores
|
|
59
|
+
its SQLite database in OPFS, and carries capabilities between workers with
|
|
60
|
+
[Cap'n Web](https://github.com/cloudflare/capnweb). Supported `Agent` and
|
|
61
|
+
`DurableObject` classes can execute on-device and deploy to Cloudflare from the
|
|
62
|
+
same source. A `node:sqlite` backend provides a second local host and a fast test
|
|
63
|
+
lane.
|
|
64
|
+
|
|
65
|
+
The repository contains the runtime and the proofs needed to use it as an
|
|
66
|
+
execution target:
|
|
67
|
+
|
|
68
|
+
| Area | What it contains |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| [`@mcp-b/do-runtime`](src/) | Input and output gates, implicit transactions, Durable Object storage APIs, facets, alarms, hibernatable WebSockets, Worker Loader, and `cloudflare:workers`. |
|
|
71
|
+
| [`vendor/agents/`](vendor/agents/README.md) | Rook's Agents SDK fork. It consumes the runtime; the runtime package does not import the SDK. |
|
|
72
|
+
| [`conformance/`](conformance/) | One behavioral suite run against real workerd, the Node host, and the browser host in Chromium. |
|
|
73
|
+
| [`examples/extension/`](examples/extension/README.md) | A Chrome MV3 host with an offscreen supervisor, local Agents SDK actors and sub-agents, OPFS persistence, durable alarms, hibernatable sockets, and restart recovery. |
|
|
74
|
+
| [`examples/vibe-platform/`](examples/vibe-platform/README.md) | An in-tab editor that runs a user-authored Agent locally and exports the unchanged source as a Wrangler project. |
|
|
9
75
|
|
|
10
|
-
|
|
76
|
+
This work was extracted from Rook, SigVelo's AI agent for Chrome. Cloudflare,
|
|
77
|
+
Workers, Durable Objects, and workerd are Cloudflare's; this is an independent
|
|
78
|
+
port and is not affiliated with or endorsed by Cloudflare.
|
|
11
79
|
|
|
12
80
|
## Contents
|
|
13
81
|
|
|
14
|
-
- [The
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
82
|
+
- [The browser runtime](#the-browser-runtime)
|
|
83
|
+
- [Run it in a browser](#run-it-in-a-browser)
|
|
84
|
+
- [Durable Object semantics](#durable-object-semantics)
|
|
85
|
+
- [Minimal host](#minimal-host)
|
|
17
86
|
- [Hosting an actor](#hosting-an-actor)
|
|
18
87
|
- [Storage, alarms, facets, I/O](#storage)
|
|
88
|
+
- [Migrations](#migrations)
|
|
19
89
|
- [What is not supported, and stability](#what-is-not-supported)
|
|
20
90
|
- [Package layout](#package-layout)
|
|
21
91
|
- [Tests](#tests)
|
|
22
92
|
- [Development](#development)
|
|
23
93
|
- [Acknowledgements and license](#acknowledgements)
|
|
24
94
|
|
|
25
|
-
## The
|
|
95
|
+
## The browser runtime
|
|
26
96
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
| **Named identity** | `idFromName("alice")` always means the same actor, and the id names its storage. | `ActorContainerOptions.id` + `uniqueKey`, `src/server/actor-id-impl.ts` |
|
|
34
|
-
| **Private transactional storage** | A SQLite database only this actor can open. KV and SQL on the same file; writes coalesce into an implicit transaction that commits at the end of the event. | `src/io/actor-sqlite.ts`, `src/api/sql.ts`, `src/util/` |
|
|
35
|
-
| **Input and output gates** | The single-threaded illusion survives `await`. The input gate admits one event at a time (and re-admits a continuation only through a gated primitive); the output gate holds a reply until the write it could reveal is durable. | `src/io/io-gate.ts`, `src/io/io-context.ts` |
|
|
36
|
-
| **Alarms and facets** | `setAlarm()` wakes the actor later with retries and backoff. Facets are child actors under a root: own gates, own database, one tree index. | `src/server/alarm-scheduler.ts`, `src/server/facet-*.ts` |
|
|
37
|
-
|
|
38
|
-
Two consequences fall out of the gates and are the whole reason the runtime is more than a SQLite wrapper:
|
|
39
|
-
|
|
40
|
-
- **No interleaving.** If a method awaits storage, a second call on the same actor waits. Application code reads and writes state without locks and is still correct.
|
|
41
|
-
- **No phantom reads.** A reply that could expose a write does not leave until that write is committed. A crash between "returned" and "committed" cannot lie to a caller.
|
|
42
|
-
|
|
43
|
-
Everything else in the package serves those two lines.
|
|
44
|
-
|
|
45
|
-
## How it runs in the browser
|
|
46
|
-
|
|
47
|
-
Workerd gives every actor its own isolate, so `setTimeout`, `fetch`, and `scheduler.wait` can only ever mean the one actor in that isolate. The browser equivalent is **one root actor per Web Worker**, with the page acting as the supervisor workerd's `Server` is:
|
|
97
|
+
Workerd tracks each actor's I/O context inside a Worker isolate;
|
|
98
|
+
[multiple actors can share that isolate](https://developers.cloudflare.com/durable-objects/reference/in-memory-state/).
|
|
99
|
+
The reference browser host places one root actor in each Web Worker so its
|
|
100
|
+
ambient timers, `fetch`, and `scheduler` resolve to that root.
|
|
101
|
+
The page, or an extension's offscreen document, supervises placement and owns no
|
|
102
|
+
actor storage.
|
|
48
103
|
|
|
49
104
|
```mermaid
|
|
50
105
|
flowchart TB
|
|
51
|
-
subgraph page["Page
|
|
52
|
-
S["spawns workers
|
|
106
|
+
subgraph page["Page or offscreen document / supervisor"]
|
|
107
|
+
S["spawns workers; actor registry; routes actor calls; owns alarm delivery"]
|
|
53
108
|
end
|
|
54
109
|
|
|
55
|
-
subgraph wa["Web Worker
|
|
110
|
+
subgraph wa["Web Worker / actor alice"]
|
|
56
111
|
direction TB
|
|
57
|
-
Ca["ActorContainer<br/>input gate
|
|
58
|
-
Fa["facet containers<br/>(own gates
|
|
59
|
-
Pa[("
|
|
112
|
+
Ca["Agent or DurableObject<br/>inside ActorContainer<br/>input gate; output gate; globals"]
|
|
113
|
+
Fa["facet containers<br/>(own gates and database, same realm)"]
|
|
114
|
+
Pa[("sqlite-wasm<br/>OPFS SAH pool")]
|
|
60
115
|
Ca --- Fa
|
|
61
116
|
Ca --> Pa
|
|
62
117
|
Fa --> Pa
|
|
63
118
|
end
|
|
64
119
|
|
|
65
|
-
subgraph wb["Web Worker
|
|
120
|
+
subgraph wb["Web Worker / actor bob"]
|
|
66
121
|
direction TB
|
|
67
|
-
Cb["ActorContainer"]
|
|
68
|
-
Pb[("OPFS SAH pool")]
|
|
122
|
+
Cb["Agent or DurableObject<br/>inside ActorContainer"]
|
|
123
|
+
Pb[("sqlite-wasm<br/>OPFS SAH pool")]
|
|
69
124
|
Cb --> Pb
|
|
70
125
|
end
|
|
71
126
|
|
|
72
|
-
subgraph wal["Web Worker
|
|
127
|
+
subgraph wal["Web Worker / alarms"]
|
|
73
128
|
direction TB
|
|
74
|
-
A["AlarmScheduler<br/>_cf_ALARM
|
|
75
|
-
PA[("OPFS SAH pool")]
|
|
129
|
+
A["AlarmScheduler<br/>_cf_ALARM; retries; backoff"]
|
|
130
|
+
PA[("sqlite-wasm<br/>OPFS SAH pool")]
|
|
76
131
|
A --> PA
|
|
77
132
|
end
|
|
78
133
|
|
|
79
|
-
S <-- "MessagePort
|
|
80
|
-
S <-- "MessagePort
|
|
81
|
-
S <-- "MessagePort
|
|
134
|
+
S <-- "MessagePort + Cap'n Web" --> Ca
|
|
135
|
+
S <-- "MessagePort + Cap'n Web" --> Cb
|
|
136
|
+
S <-- "MessagePort + Cap'n Web" --> A
|
|
82
137
|
```
|
|
83
138
|
|
|
84
139
|
Why it is shaped this way:
|
|
85
140
|
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
141
|
+
- OPFS synchronous access handles exist only inside a dedicated worker, so the
|
|
142
|
+
supervisor creates workers, keeps the registry, and routes calls between them.
|
|
143
|
+
- Each root gets its own worker. `installActorScope()` can therefore install the
|
|
144
|
+
actor's gated timers, `fetch`, `crypto`, and `scheduler` as unambiguous ambient
|
|
145
|
+
globals for application and SDK code.
|
|
146
|
+
- Facets stay in their parent's worker, matching workerd's isolate layout. Each
|
|
147
|
+
facet still has its own container, gates, and database prefix.
|
|
148
|
+
- The reference host gives its alarm scheduler a separate worker because the
|
|
149
|
+
scheduler has durable SQLite state of its own. A single-root host can colocate
|
|
150
|
+
it with the actor worker, as the MV3 example does. Either layout can place a
|
|
151
|
+
sleeping actor when a stored alarm becomes due.
|
|
152
|
+
- Each cross-worker call is a capability-based RPC session over a transferred
|
|
153
|
+
`MessagePort`. The proxy exposed by `container.entry(instance)` turns each call
|
|
154
|
+
into one gated actor event.
|
|
91
155
|
|
|
92
|
-
|
|
156
|
+
`conformance/browser/` is that picture, runnable: [`host.ts`](conformance/browser/host.ts) is the page, [`actor.worker.ts`](conformance/browser/actor.worker.ts) is a worker hosting one actor tree over OPFS, [`alarms.worker.ts`](conformance/browser/alarms.worker.ts) is the scheduler, and [`protocol.ts`](conformance/browser/protocol.ts) is the three RPC surfaces between them.
|
|
93
157
|
|
|
94
|
-
|
|
95
|
-
2. Set `globalThis.sqlite3ApiConfig = { disable: { vfs: { opfs: true, "opfs-wl": true } } }` before touching sqlite — only the SAH pool is wanted, and the other two VFSes spawn workers and arm watchdogs of their own.
|
|
96
|
-
3. `sqlite3InitModule()` and `installOpfsSAHPoolVfs(...)` **before** `installActorScope` — the installer arms watchdogs through the global `setTimeout`, which must not yet be the actor's gate.
|
|
97
|
-
4. `installActorScope(globalThis, resolve)` with a `resolve` that throws when the container is gone, so a torn-down worker refuses instead of falling through to raw timers.
|
|
98
|
-
5. Application pool settings, not the conformance lane's test-only ones: a stable pool name (it becomes an OPFS directory name), `clearOnInit: false`, capacity sized to two databases per root plus journals. The pool takes exclusive sync access handles — one holder per pool; a second context fails to install.
|
|
158
|
+
## Run it in a browser
|
|
99
159
|
|
|
100
|
-
|
|
160
|
+
The Chrome MV3 extension is the primary reference host. From a fresh checkout:
|
|
101
161
|
|
|
102
|
-
|
|
162
|
+
```bash
|
|
163
|
+
pnpm sdk:setup
|
|
164
|
+
pnpm install
|
|
165
|
+
pnpm exec playwright install chromium
|
|
166
|
+
pnpm --filter do-runtime-example-extension e2e
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| Example | Browser shape | What the test proves |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| [Chrome MV3 extension](examples/extension/README.md) | Service worker -> offscreen document -> actor Worker -> sqlite-wasm/OPFS | An Agents SDK root and its sub-agents retain state across host teardown; alarms wake an evicted host; hibernatable sockets, state sync, callable RPC, queues, MCP, and email routing use the real SDK paths. |
|
|
172
|
+
| [In-tab coding platform](examples/vibe-platform/README.md) | Page -> workspace Worker and authored-Agent Worker -> sqlite-wasm/OPFS | A user-authored Agent runs against durable local state, survives code replacement, and exports from the browser as the same source in a Wrangler project. |
|
|
103
173
|
|
|
104
|
-
|
|
174
|
+
Run both browser end-to-end suites with `pnpm test:examples`.
|
|
175
|
+
|
|
176
|
+
The [in-tab coding platform](examples/vibe-platform/README.md) is the second
|
|
177
|
+
composition: an editor, local preview, persisted Agent state, and Wrangler
|
|
178
|
+
export, all inside a normal browser tab.
|
|
179
|
+
|
|
180
|
+

|
|
181
|
+
|
|
182
|
+
Start it at `http://localhost:5173` with
|
|
183
|
+
`pnpm --filter do-runtime-example-vibe-platform dev`.
|
|
184
|
+
|
|
185
|
+
## Durable Object semantics
|
|
186
|
+
|
|
187
|
+
An actor has an identity and private state. A Durable Object adds gates that
|
|
188
|
+
serialize event slices and storage access, plus storage and lifecycle behavior
|
|
189
|
+
that survives process loss:
|
|
190
|
+
|
|
191
|
+
| | What it means | Where it lives here |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| **Named identity** | `idFromName("alice")` always means the same actor, and the id names its storage. | `ActorContainerOptions.id` + `uniqueKey`, `src/server/actor-id-impl.ts` |
|
|
194
|
+
| **Private transactional storage** | A SQLite database only this actor can open. KV and SQL share the file; writes coalesce into implicit transactions that commit at gate-release boundaries. | `src/io/actor-sqlite.ts`, `src/api/sql.ts`, `src/util/` |
|
|
195
|
+
| **Input and output gates** | The input gate serializes event slices across `await`. The output gate holds a reply until the write it could reveal is durable. | `src/io/io-gate.ts`, `src/io/io-context.ts` |
|
|
196
|
+
| **Alarms and facets** | `setAlarm()` wakes the actor later with retries and backoff. Facets are child actors with their own gates and database under a root. | `src/server/alarm-scheduler.ts`, `src/server/facet-*.ts` |
|
|
197
|
+
|
|
198
|
+
With the default storage options, a method awaiting storage holds other calls
|
|
199
|
+
back, so a read-modify-write that only awaits storage needs no extra lock.
|
|
200
|
+
Awaiting external I/O such as `fetch()` releases the input gate: another event
|
|
201
|
+
can run before the method resumes. Use
|
|
202
|
+
[`blockConcurrencyWhile()`](https://developers.cloudflare.com/durable-objects/api/state/#blockconcurrencywhile)
|
|
203
|
+
when an async operation must exclude other events. A reply that exposes a write
|
|
204
|
+
waits for that write to commit, so a crash cannot leave a caller holding an
|
|
205
|
+
acknowledgement for data that never became durable.
|
|
206
|
+
|
|
207
|
+
## Minimal host
|
|
208
|
+
|
|
209
|
+
Install with `pnpm add @mcp-b/do-runtime`. The package ships ESM JavaScript and
|
|
210
|
+
declarations. The shortest complete host uses the `node:sqlite` backend, which
|
|
211
|
+
requires Node 24.11 or newer. Browser hosts place the same container inside a
|
|
212
|
+
Web Worker and supply the sqlite-wasm backend shown in the runnable examples.
|
|
213
|
+
|
|
214
|
+
For a standalone TypeScript host, install the ambient Node and Workers types:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pnpm add -D typescript @types/node @cloudflare/workers-types@5.20260820.1
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use the following `tsconfig.json`. An existing Workers project can keep its
|
|
221
|
+
generated Cloudflare types instead of loading `@cloudflare/workers-types` too.
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"compilerOptions": {
|
|
226
|
+
"target": "ESNext",
|
|
227
|
+
"module": "NodeNext",
|
|
228
|
+
"lib": ["ESNext"],
|
|
229
|
+
"types": ["node", "@cloudflare/workers-types"],
|
|
230
|
+
"strict": true,
|
|
231
|
+
"skipLibCheck": true,
|
|
232
|
+
"noEmit": true
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Save this as `host.mts` and run `pnpm exec tsc && node host.mts`:
|
|
105
238
|
|
|
106
239
|
```ts
|
|
240
|
+
import { mkdir } from "node:fs/promises";
|
|
107
241
|
import { DurableObject } from "@mcp-b/do-runtime/cloudflare-workers";
|
|
108
242
|
import { createActorContainer, DEFAULT_ALARM_OUTLET, noFacets, type Timer } from "@mcp-b/do-runtime";
|
|
109
243
|
import { createNodeSqlProvider } from "@mcp-b/do-runtime/backends/node-sqlite";
|
|
@@ -133,6 +267,7 @@ const timer: Timer = {
|
|
|
133
267
|
}),
|
|
134
268
|
};
|
|
135
269
|
|
|
270
|
+
await mkdir("./data", { recursive: true });
|
|
136
271
|
const container = await createActorContainer({
|
|
137
272
|
id: "counter-1",
|
|
138
273
|
uniqueKey: "my-app", // keep this stable forever: every DurableObjectId is derived from it
|
|
@@ -151,14 +286,12 @@ await counter.increment(); // 1
|
|
|
151
286
|
await counter.increment(); // 2
|
|
152
287
|
```
|
|
153
288
|
|
|
154
|
-
Open a second container over the same directory and `increment()` answers `3`:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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, 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
|
-
- [`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`.
|
|
289
|
+
Open a second container over the same directory and `increment()` answers `3`:
|
|
290
|
+
the instance was volatile, the storage was not. Inside an initialized actor
|
|
291
|
+
worker, the browser host supplies
|
|
292
|
+
`createSqliteWasmProvider({ pool, capi: sqlite3.capi }, { prefix: "/counter-1" })` from
|
|
293
|
+
`@mcp-b/do-runtime/backends/sqlite-wasm`. The supervisor, OPFS pool, and worker
|
|
294
|
+
bootstrapping are shown in the browser examples above.
|
|
162
295
|
|
|
163
296
|
## Hosting an actor
|
|
164
297
|
|
|
@@ -190,6 +323,27 @@ The lifecycle:
|
|
|
190
323
|
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
324
|
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.
|
|
192
325
|
|
|
326
|
+
### Browser worker boot order
|
|
327
|
+
|
|
328
|
+
The order inside an actor worker is load-bearing. Each inversion below has a
|
|
329
|
+
measured failure in the browser test lane:
|
|
330
|
+
|
|
331
|
+
1. Capture raw platform timers at module scope and build the `Timer` port on
|
|
332
|
+
them. Reading the installed globals from that port recurses after the actor
|
|
333
|
+
scope replaces them.
|
|
334
|
+
2. Set
|
|
335
|
+
`globalThis.sqlite3ApiConfig = { disable: { vfs: { opfs: true, "opfs-wl": true } } }`
|
|
336
|
+
before initializing sqlite. The host uses the SAH pool; the other OPFS VFSes
|
|
337
|
+
spawn workers and arm watchdogs of their own.
|
|
338
|
+
3. Run `sqlite3InitModule()` and `installOpfsSAHPoolVfs(...)` before
|
|
339
|
+
`installActorScope`. The pool installer uses global timers during startup.
|
|
340
|
+
4. Install the actor scope with a resolver that throws when its container is
|
|
341
|
+
gone. A torn-down worker must refuse new work instead of falling through to
|
|
342
|
+
ungated platform timers.
|
|
343
|
+
5. Use a stable pool name, preserve files with `clearOnInit: false`, and size the
|
|
344
|
+
pool for two databases per root plus journals. The pool owns exclusive sync
|
|
345
|
+
access handles, so another context cannot open it at the same time.
|
|
346
|
+
|
|
193
347
|
For a standard Durable Object binding, call
|
|
194
348
|
`createDurableObjectNamespace(uniqueKey, channel)` and put the result in `env`
|
|
195
349
|
and `ctx.exports`. The channel maps each routed id to a placed `Fetcher`; that
|
|
@@ -207,15 +361,31 @@ re-enters the owning input gate.
|
|
|
207
361
|
|
|
208
362
|
`SqlDatabaseProvider.open(name)` is the runtime execution seam. The runtime owns database names, tables, transactions, reset behaviour, facet metadata, and streaming `sql.ingest()` statement boundaries; the host chooses the physical provider and prefix. Stored KV values use structured-clone semantics across workerd, Node, and the browser; existing JSON rows remain readable. `_cf_` names are reserved to the runtime.
|
|
209
363
|
|
|
210
|
-
|
|
364
|
+
### Migrations
|
|
365
|
+
|
|
366
|
+
Wrangler class declarations, application SQL migrations, persisted `Agent.state`,
|
|
367
|
+
Agents SDK tables, and runtime-owned tables have separate owners. Application
|
|
368
|
+
SQL uses the same Drizzle migration bundle and constructor pattern on Cloudflare
|
|
369
|
+
and on this runtime; `do-runtime` adds no application migration registry. See
|
|
370
|
+
[`docs/migrations.md`](docs/migrations.md) for the Rook-facing workflow and the
|
|
371
|
+
upstream Cloudflare and Drizzle references.
|
|
372
|
+
|
|
373
|
+
The runtime's own `_cf_` tables are versioned per database file in
|
|
374
|
+
`PRAGMA user_version`. [`src/util/sqlite-migrations.ts`](src/util/sqlite-migrations.ts)
|
|
375
|
+
brings older files forward before any event enters and refuses storage written by
|
|
376
|
+
a newer package version. Application SQL cannot access that runtime-owned stamp.
|
|
211
377
|
|
|
212
378
|
The browser provider takes an already-installed OPFS SAH pool (`installOpfsSAHPoolVfs`; sync access handles in a dedicated worker — no cross-origin isolation or `SharedArrayBuffer` needed). One pool per worker; the root and each local facet get separate prefixes inside it. `SqliteWasmActorStorage` adds the close, physical delete, and clone operations a local placement host needs around one prefix. The Node provider uses in-memory databases by default and a directory when asked.
|
|
213
379
|
|
|
214
380
|
Both concrete providers also implement `SqlDatabaseSnapshotProvider`. After the host has stopped the actor, `provider.close()` releases every database handle; `exportSnapshot()` then returns the SQLite images for the whole actor storage scope, and `importSnapshot()` replaces an idle scope. The same snapshot can seed a cold local replica because SQLite images are portable between these providers. Node snapshots require a dedicated directory-backed provider. This is backup/restore and replica seeding, not Cloudflare's time-indexed PITR or continuously updated read replication.
|
|
215
381
|
|
|
382
|
+
The browser provider attempts to restore the original images when a snapshot import or direct `SqliteWasmActorStorage.copyFrom()` replacement fails; `SqliteWasmRestoreError.recoverySnapshot` retains those images if rollback also fails. This rollback lives in memory and cannot survive Worker or process loss during replacement; a host requiring that guarantee must import into a fresh prefix and durably switch placement after success.
|
|
383
|
+
|
|
216
384
|
### Alarms
|
|
217
385
|
|
|
218
|
-
Construct one `AlarmScheduler` per namespace over a `SqlDatabase` of its own. It owns `_cf_ALARM`, delivery, retry counts (`ALARM_RETRY_MAX_TRIES`), exponential backoff with jitter, and abandonment. Pass `scheduler.hooks(id)` as a root actor's `ports.alarms`, and give the scheduler a `getActor(id)` that places the actor if it is not running — an alarm is a reason to wake a Durable Object, not something that needs one awake already.
|
|
386
|
+
Construct one `AlarmScheduler` per namespace over a `SqlDatabase` of its own. It owns `_cf_ALARM`, delivery, retry counts (`ALARM_RETRY_MAX_TRIES`), exponential backoff with jitter, and abandonment. Pass `scheduler.hooks(id)` as a root actor's `ports.alarms`, and give the scheduler a `getActor(id)` that places the actor if it is not running — an alarm is a reason to wake a Durable Object, not something that needs one awake already.
|
|
387
|
+
|
|
388
|
+
A suspending browser host can project the scheduler's next wake through `BrowserAlarmCoordinator` from `@mcp-b/do-runtime/browser/alarm-coordinator`. The coordinator journals the physical hop, rejects stale projections, rearms a consumed watchdog, and reconciles after background-worker restart. The host supplies durable journal storage, the physical alarm calls, and delivery back into its scheduler; logical delivery policy remains in `AlarmScheduler`.
|
|
219
389
|
|
|
220
390
|
### Facets
|
|
221
391
|
|
|
@@ -246,6 +416,10 @@ replacement mirror with the prior socket snapshot and auto-response pair, then
|
|
|
246
416
|
pass that mirror to `ports.hibernation` and its `snapshot()` to `webSockets`.
|
|
247
417
|
Browser hosts can install the remaining Request/Response upgrade accommodation
|
|
248
418
|
from `@mcp-b/do-runtime/browser`; the runtime itself supplies `WebSocketPair`.
|
|
419
|
+
Hosts whose actor lives in another Worker can use `MessagePortWebSocket`,
|
|
420
|
+
`createMessagePortWebSocketConstructor`, and `serveMessagePortWebSockets` from
|
|
421
|
+
`@mcp-b/do-runtime/browser/message-port-websocket`; binary frames stay in
|
|
422
|
+
structured clone and each socket gets one dedicated `MessagePort`.
|
|
249
423
|
|
|
250
424
|
`container.quiescence()` reports armed timers, pending `waitUntil` work, input
|
|
251
425
|
lock state, and output-gate breakage without waiting. `drainWaitUntil()` is for
|
|
@@ -286,10 +460,12 @@ This is `0.x`. The public surface is what [`src/index.ts`](src/index.ts) and the
|
|
|
286
460
|
| `src/io/` | Gates, invocation context, actor storage engine, ids, Worker channels |
|
|
287
461
|
| `src/api/` | Workers-facing APIs: `DurableObjectState`, SQL, WebSocket, Worker Loader, `cloudflare:workers` |
|
|
288
462
|
| `src/server/` | Actor containers, facet lifecycle, deletion recovery, alarm scheduling |
|
|
463
|
+
| `src/browser/` | Physical alarm projection, offscreen-document recovery, and MessagePort-backed WebSockets for browser hosts |
|
|
289
464
|
| `src/transport/` | The one `MessagePort` Cap'n Web session adapter |
|
|
290
465
|
| `backends/` | `node:sqlite` and sqlite-wasm/OPFS `SqlDatabaseProvider`s |
|
|
291
466
|
| `conformance/` | One suite, three hosts: workerd, Node, browser; plus the probe fixture and benchmarks |
|
|
292
467
|
| `examples/` | Runnable browser hosts: an MV3 extension and an in-page vibe-coding platform |
|
|
468
|
+
| `docs/migrations.md` | How Rook evolves Wrangler declarations, application SQL, and persisted Agent state without duplicating Cloudflare's migration machinery |
|
|
293
469
|
| `docs/decisions.md` | The numbered invariants and decisions that source comments cite (`§1.2`, `decision 8`) |
|
|
294
470
|
|
|
295
471
|
The `util → io → api → server` direction follows workerd's own layering, enforced with TypeScript project references. Source comments cite the workerd file and line they port (`← io-gate.c++:142`), and every deliberate divergence is recorded beside its implementation and in a conformance row.
|
|
@@ -311,11 +487,19 @@ The workerd lane is what makes the others mean something: every row it passes is
|
|
|
311
487
|
```bash
|
|
312
488
|
git clone https://github.com/WebMCP-org/do-runtime
|
|
313
489
|
cd do-runtime
|
|
490
|
+
pnpm sdk:setup
|
|
314
491
|
pnpm install
|
|
315
492
|
pnpm exec playwright install chromium # browser lane only
|
|
316
493
|
pnpm typecheck && pnpm test
|
|
494
|
+
pnpm sdk:check && pnpm sdk:test # Rook's Agents SDK fork
|
|
317
495
|
```
|
|
318
496
|
|
|
497
|
+
The repository has two independently tested layers: the runtime at the root
|
|
498
|
+
and Rook's six-package Agents SDK fork in
|
|
499
|
+
[`vendor/agents/`](vendor/agents/README.md). The examples consume the fork's
|
|
500
|
+
built `agents` package through a `file:` dependency, which resolves their
|
|
501
|
+
own peer dependencies without installing a second SDK implementation.
|
|
502
|
+
|
|
319
503
|
Change runtime behaviour with the corresponding workerd source open (line citations use release `v1.20260713.1`; the conformance oracle is pinned to `v1.20260820.1`). Ask the workerd lane an observable question before inventing a local rule; record any intentional divergence in the table above and in a conformance row. Keep host seams small and typed, keep gates internal, and keep product knowledge out of the port. See [`docs/decisions.md`](docs/decisions.md) for the invariants the code cites.
|
|
320
504
|
|
|
321
505
|
## Acknowledgements
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* drives this file directly, and by the conformance suite, which runs the whole
|
|
24
24
|
* package over it.
|
|
25
25
|
*/
|
|
26
|
-
import { type SqlDatabase, type SqlDatabaseProvider, type SqlDatabaseSnapshotProvider, type SqlDatabaseStatement, type SqlResult, type SqlValue } from "../src/util/sqlite.js";
|
|
26
|
+
import { type SqlDatabase, type SqlDatabaseProvider, type SqlDatabaseSnapshot, type SqlDatabaseSnapshotProvider, type SqlDatabaseStatement, type SqlResult, type SqlValue } from "../src/util/sqlite.js";
|
|
27
27
|
/** ← `PreparedStatement`, the members used here. */
|
|
28
28
|
export interface SqliteWasmStatement {
|
|
29
29
|
readonly columnCount: number;
|
|
@@ -113,6 +113,11 @@ export declare class SqliteWasmActorStorage implements SqlDatabaseProvider {
|
|
|
113
113
|
copyFrom(source: SqliteWasmActorStorage): Promise<void>;
|
|
114
114
|
}
|
|
115
115
|
export declare function createSqliteWasmProvider(host: SqliteWasmHost, options: SqliteWasmProviderOptions): SqlDatabaseSnapshotProvider;
|
|
116
|
+
/** A failed rollback retains the original images so the host can recover to another prefix. */
|
|
117
|
+
export declare class SqliteWasmRestoreError extends AggregateError {
|
|
118
|
+
readonly recoverySnapshot: SqlDatabaseSnapshot;
|
|
119
|
+
constructor(errors: unknown[], recoverySnapshot: SqlDatabaseSnapshot);
|
|
120
|
+
}
|
|
116
121
|
export declare class SqliteWasmDatabase implements SqlDatabase {
|
|
117
122
|
#private;
|
|
118
123
|
private readonly onClose;
|
|
@@ -81,8 +81,11 @@ var SqliteWasmActorStorage = class {
|
|
|
81
81
|
bytes: new Uint8Array(await source.#host.pool.exportFile(file))
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
|
-
this.
|
|
85
|
-
|
|
84
|
+
this.close();
|
|
85
|
+
await replaceDatabases(this.#host.pool, this.#prefix, images.map(({ name, bytes }) => ({
|
|
86
|
+
name,
|
|
87
|
+
image: bytes
|
|
88
|
+
})));
|
|
86
89
|
}
|
|
87
90
|
#ownedFiles() {
|
|
88
91
|
return this.#host.pool.getFileNames().filter((name) => name.startsWith(`${this.#prefix}.`));
|
|
@@ -126,11 +129,73 @@ function createSqliteWasmProvider(host, options) {
|
|
|
126
129
|
requireClosed(openDatabases);
|
|
127
130
|
requireValidSqlDatabaseSnapshot(snapshot);
|
|
128
131
|
requireImportableRuntimeStorage(snapshot);
|
|
129
|
-
|
|
130
|
-
for (const { name, image } of snapshot.databases) await host.pool.importDb(`${prefix}.${name}.sqlite`, new Uint8Array(image));
|
|
132
|
+
await replaceDatabases(host.pool, prefix, snapshot.databases);
|
|
131
133
|
}
|
|
132
134
|
};
|
|
133
135
|
}
|
|
136
|
+
/** A failed rollback retains the original images so the host can recover to another prefix. */
|
|
137
|
+
var SqliteWasmRestoreError = class extends AggregateError {
|
|
138
|
+
recoverySnapshot;
|
|
139
|
+
constructor(errors, recoverySnapshot) {
|
|
140
|
+
super(errors, "SQLite replacement and rollback failed; restore recoverySnapshot to an idle provider.");
|
|
141
|
+
this.recoverySnapshot = recoverySnapshot;
|
|
142
|
+
this.name = "SqliteWasmRestoreError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
async function replaceDatabases(pool, prefix, databases) {
|
|
146
|
+
const replacement = databases.map(({ name, image }) => ({
|
|
147
|
+
name,
|
|
148
|
+
image: new Uint8Array(image)
|
|
149
|
+
}));
|
|
150
|
+
const ownedFiles = () => pool.getFileNames().filter((file) => file.startsWith(`${prefix}.`));
|
|
151
|
+
const files = ownedFiles();
|
|
152
|
+
requireNoRecoverySidecars(files);
|
|
153
|
+
const original = [];
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const name = file.slice(prefix.length + 1, -7);
|
|
156
|
+
requireSafeDatabaseName(name);
|
|
157
|
+
original.push({
|
|
158
|
+
name,
|
|
159
|
+
image: new Uint8Array(await pool.exportFile(file))
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const touched = /* @__PURE__ */ new Set();
|
|
163
|
+
try {
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
touched.add(file);
|
|
166
|
+
if (!pool.unlink(file)) throw new Error(`SAH pool did not unlink ${file}`);
|
|
167
|
+
}
|
|
168
|
+
for (const { name, image } of replacement) {
|
|
169
|
+
const file = `${prefix}.${name}.sqlite`;
|
|
170
|
+
touched.add(file);
|
|
171
|
+
await pool.importDb(file, image);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const errors = [error];
|
|
175
|
+
for (const file of ownedFiles()) {
|
|
176
|
+
if (!touched.has(file)) continue;
|
|
177
|
+
try {
|
|
178
|
+
if (!pool.unlink(file)) throw new Error(`SAH pool did not unlink ${file}`);
|
|
179
|
+
} catch (rollbackError) {
|
|
180
|
+
errors.push(rollbackError);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const { name, image } of original) {
|
|
184
|
+
const file = `${prefix}.${name}.sqlite`;
|
|
185
|
+
if (!touched.has(file)) continue;
|
|
186
|
+
try {
|
|
187
|
+
await pool.importDb(file, new Uint8Array(image));
|
|
188
|
+
} catch (rollbackError) {
|
|
189
|
+
errors.push(rollbackError);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (errors.length > 1) throw new SqliteWasmRestoreError(errors, {
|
|
193
|
+
version: 1,
|
|
194
|
+
databases: original
|
|
195
|
+
});
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
134
199
|
var SqliteWasmDatabase = class {
|
|
135
200
|
onClose;
|
|
136
201
|
#host;
|
|
@@ -255,6 +320,6 @@ function firstCompleteStatement(capi, sql) {
|
|
|
255
320
|
return sql;
|
|
256
321
|
}
|
|
257
322
|
//#endregion
|
|
258
|
-
export { SqliteWasmActorStorage, SqliteWasmDatabase, createSqliteWasmProvider };
|
|
323
|
+
export { SqliteWasmActorStorage, SqliteWasmDatabase, SqliteWasmRestoreError, createSqliteWasmProvider };
|
|
259
324
|
|
|
260
325
|
//# sourceMappingURL=sqlite-wasm.js.map
|