pi-wasm-kit 0.0.0-stage → 0.2.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +11 -0
  3. package/README.md +206 -2
  4. package/dist/engine.d.ts +30 -0
  5. package/dist/engine.js +246 -0
  6. package/dist/guest/bridge.d.ts +1 -0
  7. package/dist/guest/bridge.js +14 -0
  8. package/dist/guest/entry.d.ts +1 -0
  9. package/dist/guest/entry.js +134 -0
  10. package/dist/guest/journal.d.ts +6 -0
  11. package/dist/guest/journal.js +21 -0
  12. package/dist/guest/platform.d.ts +1 -0
  13. package/dist/guest/platform.js +64 -0
  14. package/dist/guest-meta.json +1 -0
  15. package/dist/guest-source.d.ts +2 -0
  16. package/dist/guest-source.js +3 -0
  17. package/dist/guest.js +21929 -0
  18. package/dist/index.d.ts +104 -0
  19. package/dist/index.js +84 -0
  20. package/dist/licenses/@earendil-works__chord__LICENSE +21 -0
  21. package/dist/licenses/@earendil-works__pi-ai__LICENSE +21 -0
  22. package/dist/licenses/@earendil-works__pi-durable__LICENSE +21 -0
  23. package/dist/licenses/@jitl__quickjs-wasmfile-release-sync__LICENSE +47 -0
  24. package/dist/licenses/abort-controller__LICENSE +21 -0
  25. package/dist/licenses/event-target-shim__LICENSE +22 -0
  26. package/dist/licenses/manifest.json +47 -0
  27. package/dist/licenses/partial-json__LICENSE +21 -0
  28. package/dist/licenses/quickjs-emscripten-core__LICENSE +21 -0
  29. package/dist/licenses/typebox__license +23 -0
  30. package/dist/providers/openai.d.ts +8 -0
  31. package/dist/providers/openai.js +41 -0
  32. package/dist/storage/browser.d.ts +2 -0
  33. package/dist/storage/browser.js +75 -0
  34. package/dist/storage/node.d.ts +2 -0
  35. package/dist/storage/node.js +105 -0
  36. package/dist/types.d.ts +66 -0
  37. package/dist/types.js +1 -0
  38. package/package.json +75 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 leepokai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,11 @@
1
+ pi-wasm-kit is an independent project by leepokai.
2
+
3
+ Inspired by Finn Voorhees's PiDurableKit:
4
+ https://github.com/finnvoor/PiDurableKit
5
+ PiDurableKit demonstrates embedding Pi Durable in a host runtime with native
6
+ capability bridges and an idiomatic API. This project applies that approach to
7
+ QuickJS WebAssembly and JavaScript hosts. No affiliation or endorsement is implied.
8
+
9
+ Powered by Earendil Works' Pi Durable, Pi AI, and Chord (MIT), and
10
+ QuickJS / quickjs-emscripten (MIT). Upstream runtime code is bundled at build time,
11
+ not reimplemented. Third-party license texts are distributed in dist/licenses.
package/README.md CHANGED
@@ -1,3 +1,207 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/leepokai/pi-wasm-kit/main/logos/export/logo-192.png" width="112" height="112" alt="Pi WASM Kit — continuation pi icon">
3
+ </p>
4
+ <h1 align="center">Pi WASM Kit</h1>
5
+ <p align="center"><strong>Durable agents. A portable runtime.</strong><br>Run Pi in WebAssembly. Pick up from the last committed checkpoint.</p>
6
+ <p align="center">
7
+ <a href="docs/install.md">Install</a> · <a href="#try-it">Quick start</a> · <a href="docs/api.md">API</a> · <a href="docs/features.md">Features</a> · <a href="docs/mobile.md">Mobile guide</a> · <a href="https://github.com/leepokai/pi-wasm-kit/releases">Releases</a>
8
+ </p>
9
+ <p align="center">
10
+ <a href="https://github.com/leepokai/pi-wasm-kit/releases"><img alt="Download SDK" src="https://img.shields.io/badge/DOWNLOAD-SDK-91ad60?style=for-the-badge&labelColor=22271f"></a>
11
+ <a href="examples/ios"><img alt="iOS example" src="https://img.shields.io/badge/iOS-EXAMPLE-91ad60?style=for-the-badge&logo=apple&logoColor=white&labelColor=22271f"></a>
12
+ <a href="examples/android"><img alt="Android example" src="https://img.shields.io/badge/ANDROID-EXAMPLE-91ad60?style=for-the-badge&logo=android&logoColor=white&labelColor=22271f"></a>
13
+ </p>
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/pi-wasm-kit"><img alt="npm version" src="https://img.shields.io/npm/v/pi-wasm-kit?color=627d3c"></a>
16
+ <a href="https://github.com/leepokai/pi-wasm-kit/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/leepokai/pi-wasm-kit/actions/workflows/ci.yml/badge.svg"></a>
17
+ <a href="https://github.com/leepokai/pi-wasm-kit/actions/workflows/mobile.yml"><img alt="Mobile examples CI" src="https://github.com/leepokai/pi-wasm-kit/actions/workflows/mobile.yml/badge.svg"></a>
18
+ <a href="https://github.com/leepokai/pi-wasm-kit/releases"><img alt="Release including experimental prereleases" src="https://img.shields.io/github/v/release/leepokai/pi-wasm-kit?include_prereleases&color=627d3c"></a>
19
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/github/license/leepokai/pi-wasm-kit?color=627d3c"></a>
20
+ <a href="https://github.com/leepokai/pi-wasm-kit/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/leepokai/pi-wasm-kit?style=flat&color=627d3c"></a>
21
+ </p>
22
+ <p align="center">
23
+ <img alt="WebAssembly runtime" src="https://img.shields.io/badge/runtime-WebAssembly-654ff0?logo=webassembly&logoColor=white">
24
+ <img alt="Node.js 22.19 or newer" src="https://img.shields.io/badge/Node.js-22.19%2B-5f843d?logo=nodedotjs&logoColor=white">
25
+ <img alt="Browser IndexedDB storage" src="https://img.shields.io/badge/browser-IndexedDB-627d3c">
26
+ <img alt="iOS example requires iOS 26 or newer" src="https://img.shields.io/badge/iOS_example-26%2B-555?logo=apple&logoColor=white">
27
+ <img alt="Android example minimum API 26" src="https://img.shields.io/badge/Android_example-API_26%2B-627d3c?logo=android&logoColor=white">
28
+ </p>
29
+ <p align="center">Open source · MIT licensed · Experimental<br>
30
+ Inspired by <a href="https://github.com/finnvoor/PiDurableKit">PiDurableKit</a> by <a href="https://github.com/finnvoor">Finn Voorhees</a>.</p>
2
31
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
32
+ ![Browser playground](https://raw.githubusercontent.com/leepokai/pi-wasm-kit/main/docs/playground.png)
33
+
34
+ The **actual upstream Pi Durable engine** runs inside QuickJS WASM. The host supplies a model transport, an exclusive durable journal, and explicitly granted tools. Stop the process or terminate the Worker, reopen the same journal, and Pi resumes unfinished tasks from committed checkpoints.
35
+
36
+ **Experimental, v0.2.** Browser and Node hosts, plus [iOS and Android WebView examples](docs/mobile.md). Mobile examples run the same WASM engine on-device and call an actual Swift/Java tool. They load assets from a development server; they are not standalone offline apps. PostgreSQL and standalone WASI execution are not implemented.
37
+
38
+ This is an independent project inspired by PiDurableKit’s Apple embedding design, not an official port or an endorsed project. PiDurableKit uses JavaScriptCore; this project uses QuickJS compiled to WebAssembly.
39
+
40
+ ## Install
41
+
42
+ | Platform | Get started |
43
+ | --- | --- |
44
+ | macOS / Windows / Linux · Node.js | [Install the SDK from npm](docs/install.md#nodejs-sdk) |
45
+ | Browser | [Run the playground](docs/install.md#browser-playground) |
46
+ | iOS 26+ | [Build the Swift example](docs/install.md#ios) on macOS |
47
+ | Android API 26+ | [Build and install the Android example](docs/install.md#android) |
48
+
49
+ ```sh
50
+ npm install pi-wasm-kit
51
+ ```
52
+
53
+ Requires Node.js 22.19+. This installs an SDK in your application, not a desktop/mobile app or a global CLI. See the [platform installation guide](docs/install.md) for prerequisites, Windows PowerShell commands, device setup and checksums.
54
+
55
+ ## Try it
56
+
57
+ Requires Node.js 22.19+ and a modern browser with IndexedDB and Web Locks (HTTPS or localhost).
58
+
59
+ ```sh
60
+ git clone https://github.com/leepokai/pi-wasm-kit.git
61
+ cd pi-wasm-kit
62
+ npm ci
63
+ npm run build
64
+ npm run dev
65
+ ```
66
+
67
+ Open the local URL. The demo uses a **deterministic local model**, not an LLM: every prompt demonstrates multiplying 21 by 2, with delayed model responses so you can interrupt it. The Pi engine, tool execution, WebAssembly, storage, and recovery are real.
68
+
69
+ 1. Click **Run**.
70
+ 2. While running, click **Interrupt runtime**. This actually terminates the Worker.
71
+ 3. Click **Reopen & resume**. The existing journal is reopened.
72
+ 4. Refresh the page. The saved conversation remains.
73
+
74
+ Optionally enter an OpenAI Chat Completions-compatible endpoint and model in **Model connection**, then interrupt and reopen. The endpoint must allow your origin through CORS. Use your own authenticated backend proxy; this demo has no login and does not store API keys. The endpoint and model selection are retained in localStorage so refreshing uses the same transport.
75
+
76
+ ## Mobile examples
77
+
78
+ - **[iOS / Swift](examples/ios)** — WKWebView, native tool, app-relaunch recovery, and iOS 26+ `BGContinuedProcessingTask`.
79
+ - **[Android / Java](examples/android)** — Android WebView, origin-scoped native tool, process-restart recovery.
80
+
81
+ The [mobile guide](docs/mobile.md) explains asset hosting, platform limits, and how to reproduce the native E2E checks. Background execution is separate from durability: a saved task can survive an interruption without receiving more background CPU time.
82
+
83
+ ## Node example with a real model
84
+
85
+ ```sh
86
+ PI_MODEL=your-model PI_API_KEY=your-key \
87
+ PI_BASE_URL=https://your-compatible-provider.example/v1 \
88
+ node examples/node/chat.mjs 'Hello from WASM'
89
+ ```
90
+
91
+ `PI_BASE_URL` defaults to `https://api.openai.com/v1`. Credentials stay in the host transport and are not passed to the WASM guest. Journals contain prompts, model responses, tool arguments/results and application state; handle them as sensitive data.
92
+
93
+ ## Use the SDK
94
+
95
+ Install from [npm](https://www.npmjs.com/package/pi-wasm-kit) with `npm install pi-wasm-kit`. For an exact version, use `npm install pi-wasm-kit@0.2.0`. You can also build from source with `npm pack`, or install the tarball from the GitHub release.
96
+
97
+ ```ts
98
+ import { PiWasmAgent } from 'pi-wasm-kit';
99
+ import { openNodeJournal } from 'pi-wasm-kit/node';
100
+ import { openAICompatible } from 'pi-wasm-kit/openai';
101
+
102
+ const agent = await PiWasmAgent.open({
103
+ journal: await openNodeJournal('./agent.jsonl'),
104
+ model: openAICompatible({
105
+ baseURL: process.env.PI_BASE_URL!,
106
+ model: process.env.PI_MODEL!,
107
+ apiKey: process.env.PI_API_KEY,
108
+ }),
109
+ tools: [{
110
+ name: 'multiply',
111
+ description: 'Multiply two numbers',
112
+ parameters: {
113
+ type: 'object',
114
+ properties: { a: { type: 'number' }, b: { type: 'number' } },
115
+ required: ['a', 'b'],
116
+ },
117
+ replay: 'safe', // Only for operations safe to repeat after interruption.
118
+ execute: ({ a, b }, { signal, output, taskId }) => {
119
+ signal.throwIfAborted();
120
+ output('Calculating…');
121
+ return String(Number(a) * Number(b));
122
+ },
123
+ }],
124
+ onEvent(event) {
125
+ if (event.type === 'events') console.log(event.events);
126
+ },
127
+ });
128
+
129
+ try {
130
+ const { id } = await agent.submit('Multiply 21 by 2', 'request-001');
131
+ await agent.wait(id);
132
+ console.log(await agent.view());
133
+ } finally {
134
+ await agent.close();
135
+ }
136
+ ```
137
+
138
+ In a browser Worker, replace `openNodeJournal` with `openBrowserJournal` from `pi-wasm-kit/browser`. Use a bundler such as Vite to serve the QuickJS WASM assets. The included demo shows the complete Worker bridge. Do not run CPU-bound guest code on the UI thread.
139
+
140
+ `model` can be any host function returning a Pi `AssistantMessage`. Its third argument accepts Pi `AssistantMessageEvent` values for incremental text, thinking and tool-call updates. The supplied HTTP adapter currently **buffers Chat Completions responses**; use a streaming `ModelHandler` for token-level events. The final message is returned from the handler; the bridge emits its terminal event.
141
+
142
+ ## What is preserved from PiDurableKit?
143
+
144
+ The goal is to retain Pi’s capabilities rather than replace its scheduler with a simplified loop. See the [feature matrix](docs/features.md) for exact boundaries.
145
+
146
+ | Capability | Available here |
147
+ | --- | --- |
148
+ | Durable conversations and interrupted-run recovery | Upstream Pi + persisted journal |
149
+ | Input deduplication, steering, follow-ups | `submit`, `submitDraft`, `whenBusy` |
150
+ | Configuration, reset, fork and compaction | First-class host methods |
151
+ | Host tools, schema validation, progress and cancellation | JSON bridge, explicit host capabilities |
152
+ | Safe/unsafe tool replay | Upstream Pi policy, safe requires opt-in |
153
+ | Events, usage, task inspection and graph | `onEvent`, `watch`, `view`, `inspect`, `taskGraph` |
154
+ | JS extensions, tools, prompt sections, hooks and wrappers | CommonJS extension sources inside WASM |
155
+ | Documents, transactions, custom/child tasks and subagents | Upstream API inside extensions and `evaluate` |
156
+ | Apple Keychain, OAuth helpers, native filesystem/shell | Host integration required; not bundled |
157
+ | Android / iOS app integration | Origin-scoped native tool bridges, persistent WebView journals; [examples](docs/mobile.md) |
158
+ | iOS continued background processing | iOS 26+ adapter, progress, expiration and durable cancellation; device scheduling remains system-controlled |
159
+
160
+ ## Extensions and advanced Pi APIs
161
+
162
+ Install a CommonJS source string at open time. Supported `require` names are `@earendil-works/pi-durable`, `@earendil-works/pi-ai` (**Type only**) and `@earendil-works/chord/context`.
163
+
164
+ ```ts
165
+ const agent = await PiWasmAgent.open({
166
+ journal,
167
+ model,
168
+ extensions: [counterExtensionSource],
169
+ });
170
+ ```
171
+
172
+ See [the counter extension](examples/extensions/counter.cjs): a tool, prompt section, wrapper, transactionally deduplicated document mutation, and custom durable task. Definitions must be installed again on recovery; the journal stores task kinds and checkpoints, **not executable source code**. `install(source)` and `uninstall(name)` change the live registry.
173
+
174
+ For advanced integration, `evaluate(source, args, conversationId?)` runs a trusted async function body inside WASM. It receives `pi`, `Type`, `harness`, `conversation`, `registry`, `context`, and JSON `args`. This exposes the upstream document, task, hook and transaction APIs without attempting to serialize functions across the boundary.
175
+
176
+ ```ts
177
+ const taskId = await agent.evaluate<number>(`
178
+ const task = registry.snapshot().task('example.double');
179
+ return conversation.commit(tx => tx.createTask(task, args, {
180
+ ownership: { kind: 'conversation' }
181
+ }), context);
182
+ `, { value: 21 });
183
+ const result = await agent.waitForTask(taskId);
184
+ ```
185
+
186
+ Extension source and `evaluate` are **trusted application code**, not a multitenant plugin security boundary. Extensions share one guest and its granted capabilities. Do not evaluate arbitrary user/model-generated code with powerful host tools. See [architecture and durability](docs/architecture.md) and the [API guide](docs/api.md).
187
+
188
+ ## Validation
189
+
190
+ ```sh
191
+ npm test # Real WASM integration tests + local HTTP transport
192
+ npx playwright install chromium
193
+ npm run build:demo # Production browser bundle
194
+ npm run test:e2e # Production Worker termination, reload, exclusion
195
+ npm run typecheck
196
+ npm run test:package # Install the tarball into a clean consumer
197
+ ```
198
+
199
+ Tests use synthetic model responses and a local HTTP fixture. They do not require paid credentials or claim to validate a live provider’s service. CI repeats the suite on Linux. Recovery is at committed task boundaries, not a snapshot of a JavaScript stack or an in-flight HTTP connection.
200
+
201
+ ## Credits and license
202
+
203
+ - **Inspired by [PiDurableKit](https://github.com/finnvoor/PiDurableKit), by [Finn Voorhees](https://github.com/finnvoor).** Its Apple embedding design motivated this WASM host adaptation. No PiDurableKit Swift implementation is copied.
204
+ - Powered by [Pi Durable, Pi AI and Chord](https://github.com/earendil-works/pi), by Earendil Works and contributors; the upstream MIT notice credits Mario Zechner.
205
+ - Executes JavaScript using [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) and [QuickJS](https://bellard.org/quickjs/).
206
+
207
+ This project is MIT licensed. See [LICENSE](LICENSE), [NOTICE](NOTICE), and [third-party notices](third-party/README.md). Builds include `dist/licenses` with the license texts for bundled dependencies. Upstream licenses remain applicable.
@@ -0,0 +1,30 @@
1
+ type Handler = (method: string, data: any, signal: AbortSignal, update: (value: unknown) => void) => Promise<unknown>;
2
+ /** JSON-only boundary. No host objects or ambient filesystem/network enter the VM. */
3
+ export declare class Engine {
4
+ private handler;
5
+ private emit;
6
+ private cpuMs;
7
+ private vm;
8
+ private runtime;
9
+ private stopped;
10
+ private queued;
11
+ private pumps;
12
+ private deadline;
13
+ private timers;
14
+ private nextTimer;
15
+ private pending;
16
+ private controller;
17
+ private activeCalls;
18
+ private disposal?;
19
+ private effects;
20
+ private constructor();
21
+ static create(handler: Handler, emit: (event: any) => void, cpuMs?: number, memory?: number): Promise<Engine>;
22
+ private run;
23
+ private expose;
24
+ private install;
25
+ private settle;
26
+ private pump;
27
+ call<T>(method: string, data?: unknown): Promise<T>;
28
+ dispose(): Promise<void>;
29
+ }
30
+ export {};
package/dist/engine.js ADDED
@@ -0,0 +1,246 @@
1
+ import { newQuickJSWASMModuleFromVariant } from 'quickjs-emscripten-core';
2
+ import variant from '@jitl/quickjs-wasmfile-release-sync';
3
+ // The upstream variant uses a CJS declaration file for its ESM default export.
4
+ let modulePromise;
5
+ const getQuickJS = () => modulePromise ??= newQuickJSWASMModuleFromVariant(variant);
6
+ import source from './guest-source.js';
7
+ /** JSON-only boundary. No host objects or ambient filesystem/network enter the VM. */
8
+ export class Engine {
9
+ handler;
10
+ emit;
11
+ cpuMs;
12
+ vm;
13
+ runtime;
14
+ stopped = false;
15
+ queued = false;
16
+ pumps = 0;
17
+ deadline = Infinity;
18
+ timers = new Map();
19
+ nextTimer = 1;
20
+ pending = new Set();
21
+ controller = new AbortController();
22
+ activeCalls = new Set();
23
+ disposal;
24
+ effects = new Map();
25
+ constructor(module, handler, emit, cpuMs, memory) {
26
+ this.handler = handler;
27
+ this.emit = emit;
28
+ this.cpuMs = cpuMs;
29
+ this.runtime = module.newRuntime();
30
+ this.runtime.setMemoryLimit(memory);
31
+ this.runtime.setMaxStackSize(1024 * 1024);
32
+ this.runtime.setInterruptHandler(() => this.stopped || performance.now() > this.deadline);
33
+ this.vm = this.runtime.newContext();
34
+ try {
35
+ this.install();
36
+ this.run(() => this.vm.unwrapResult(this.vm.evalCode(source, 'pi-durable.guest.js')).dispose());
37
+ }
38
+ catch (error) {
39
+ this.vm.dispose();
40
+ this.runtime.dispose();
41
+ throw new Error(error instanceof Error ? error.message : String(error));
42
+ }
43
+ }
44
+ static async create(handler, emit, cpuMs = 1000, memory = 64 * 1024 * 1024) {
45
+ return new Engine(await getQuickJS(), handler, emit, cpuMs, memory);
46
+ }
47
+ run(fn) {
48
+ this.deadline = performance.now() + this.cpuMs;
49
+ try {
50
+ return fn();
51
+ }
52
+ finally {
53
+ this.deadline = Infinity;
54
+ }
55
+ }
56
+ expose(name, fn) {
57
+ const handle = this.vm.newFunction(name, fn);
58
+ this.vm.setProp(this.vm.global, name, handle);
59
+ handle.dispose();
60
+ }
61
+ install() {
62
+ this.expose('__cancel', id => { this.effects.get(this.vm.getNumber(id))?.controller.abort(); });
63
+ this.expose('__host', (method, raw, callback, requestId) => {
64
+ const name = this.vm.getString(method);
65
+ const data = JSON.parse(this.vm.getString(raw));
66
+ const promise = this.vm.newPromise();
67
+ this.pending.add(promise);
68
+ const id = requestId ? this.vm.getNumber(requestId) : 0;
69
+ const effect = { controller: new AbortController(), callback: callback && this.vm.typeof(callback) === 'function' ? callback.dup() : undefined };
70
+ if (id)
71
+ this.effects.set(id, effect);
72
+ const signal = AbortSignal.any([this.controller.signal, effect.controller.signal]);
73
+ const update = (value) => {
74
+ if (this.stopped || signal.aborted || !effect.callback?.alive)
75
+ return;
76
+ const raw = this.vm.newString(JSON.stringify(value));
77
+ try {
78
+ this.run(() => this.vm.unwrapResult(this.vm.callFunction(effect.callback, this.vm.undefined, raw)).dispose());
79
+ }
80
+ finally {
81
+ raw.dispose();
82
+ this.pump();
83
+ }
84
+ };
85
+ const finish = (value, failed) => {
86
+ if (effect.callback?.alive)
87
+ effect.callback.dispose();
88
+ if (id)
89
+ this.effects.delete(id);
90
+ this.settle(promise, value, failed);
91
+ };
92
+ void Promise.resolve().then(() => this.handler(name, data, signal, update)).then(value => finish(value, false), error => finish(error instanceof Error ? error.message : String(error), true));
93
+ return promise.handle;
94
+ });
95
+ this.expose('__emit', raw => { this.emit(JSON.parse(this.vm.getString(raw))); });
96
+ this.expose('__hostSync', (name, arg) => {
97
+ const method = this.vm.getString(name);
98
+ const value = this.vm.getString(arg);
99
+ switch (method) {
100
+ case 'url': {
101
+ const [input, base] = JSON.parse(value);
102
+ const url = new URL(input, base ?? undefined);
103
+ return this.vm.newString(JSON.stringify({ href: url.href, origin: url.origin, protocol: url.protocol, username: url.username, password: url.password, host: url.host, hostname: url.hostname, port: url.port, pathname: url.pathname, search: url.search, hash: url.hash }));
104
+ }
105
+ case 'now': return this.vm.newString(String(performance.now()));
106
+ case 'uuid': return this.vm.newString(JSON.stringify(crypto.randomUUID()));
107
+ case 'random': return this.vm.newString(JSON.stringify(Array.from(crypto.getRandomValues(new Uint8Array(Number(value))))));
108
+ case 'encode': return this.vm.newString(JSON.stringify(Array.from(new TextEncoder().encode(value))));
109
+ case 'decode': return this.vm.newString(JSON.stringify(new TextDecoder().decode(Uint8Array.from(JSON.parse(value)))));
110
+ default: throw new Error('Unknown synchronous capability');
111
+ }
112
+ });
113
+ this.expose('__timer', (fn, delay) => {
114
+ const id = this.nextTimer++;
115
+ const callback = fn.dup();
116
+ const timer = setTimeout(() => {
117
+ this.timers.delete(id);
118
+ if (this.stopped)
119
+ return;
120
+ try {
121
+ this.run(() => this.vm.unwrapResult(this.vm.callFunction(callback, this.vm.undefined)).dispose());
122
+ }
123
+ catch (error) {
124
+ this.emit({ type: 'error', message: String(error) });
125
+ }
126
+ finally {
127
+ callback.dispose();
128
+ this.pump();
129
+ }
130
+ }, Math.min(Math.max(0, this.vm.getNumber(delay)), 2147483647));
131
+ this.timers.set(id, { timer, callback });
132
+ return this.vm.newNumber(id);
133
+ });
134
+ this.expose('__clearTimer', id => {
135
+ const key = this.vm.getNumber(id);
136
+ const item = this.timers.get(key);
137
+ if (item) {
138
+ clearTimeout(item.timer);
139
+ item.callback.dispose();
140
+ this.timers.delete(key);
141
+ }
142
+ });
143
+ }
144
+ settle(promise, value, failed) {
145
+ if (this.stopped)
146
+ return;
147
+ const handle = failed ? this.vm.newError(String(value)) : this.vm.newString(JSON.stringify(value ?? null));
148
+ try {
149
+ this.run(() => failed ? promise.reject(handle) : promise.resolve(handle));
150
+ }
151
+ finally {
152
+ handle.dispose();
153
+ promise.dispose();
154
+ this.pending.delete(promise);
155
+ }
156
+ this.pump();
157
+ }
158
+ pump() {
159
+ if (this.queued || this.stopped)
160
+ return;
161
+ this.queued = true;
162
+ const schedule = ++this.pumps % 16 === 0 ? (fn) => setTimeout(fn, 0) : queueMicrotask;
163
+ schedule(() => {
164
+ this.queued = false;
165
+ if (this.stopped)
166
+ return;
167
+ const result = this.run(() => this.runtime.executePendingJobs(100));
168
+ if (result.error) {
169
+ const message = JSON.stringify(this.vm.dump(result.error));
170
+ result.error.dispose();
171
+ this.emit({ type: 'error', message });
172
+ }
173
+ if (this.runtime.hasPendingJob())
174
+ this.pump();
175
+ });
176
+ }
177
+ async call(method, data = {}) {
178
+ if (this.stopped)
179
+ throw new Error('WASM runtime is closed');
180
+ const fn = this.vm.getProp(this.vm.global, '__dispatch');
181
+ const name = this.vm.newString(method);
182
+ const raw = this.vm.newString(JSON.stringify(data));
183
+ let promise;
184
+ try {
185
+ promise = this.run(() => this.vm.unwrapResult(this.vm.callFunction(fn, this.vm.undefined, name, raw)));
186
+ }
187
+ finally {
188
+ fn.dispose();
189
+ name.dispose();
190
+ raw.dispose();
191
+ }
192
+ let release;
193
+ const released = new Promise(resolve => { release = resolve; });
194
+ this.activeCalls.add(released);
195
+ const signal = this.controller.signal;
196
+ let onAbort;
197
+ try {
198
+ const stopped = new Promise((_, reject) => { onAbort = () => reject(new Error('WASM runtime was terminated')); signal.addEventListener('abort', onAbort, { once: true }); });
199
+ const resolved = this.vm.resolvePromise(promise).then(result => {
200
+ if (this.stopped) {
201
+ if (result.error)
202
+ result.error.dispose();
203
+ else
204
+ result.value.dispose();
205
+ throw new Error('WASM runtime was terminated');
206
+ }
207
+ const value = this.vm.unwrapResult(result);
208
+ try {
209
+ return JSON.parse(this.vm.getString(value));
210
+ }
211
+ finally {
212
+ value.dispose();
213
+ }
214
+ });
215
+ this.pump();
216
+ return await Promise.race([resolved, stopped]);
217
+ }
218
+ finally {
219
+ signal.removeEventListener('abort', onAbort);
220
+ promise.dispose();
221
+ this.activeCalls.delete(released);
222
+ release();
223
+ }
224
+ }
225
+ dispose() {
226
+ if (this.disposal)
227
+ return this.disposal;
228
+ this.stopped = true;
229
+ this.controller.abort();
230
+ for (const effect of this.effects.values())
231
+ if (effect.callback?.alive)
232
+ effect.callback.dispose();
233
+ this.effects.clear();
234
+ for (const { timer, callback } of this.timers.values()) {
235
+ clearTimeout(timer);
236
+ callback.dispose();
237
+ }
238
+ this.timers.clear();
239
+ for (const promise of this.pending)
240
+ promise.dispose();
241
+ this.pending.clear();
242
+ // Rejected call() waiters must release their handles before freeing the context.
243
+ this.disposal = Promise.all([...this.activeCalls]).then(() => { this.vm.dispose(); this.runtime.dispose(); });
244
+ return this.disposal;
245
+ }
246
+ }
@@ -0,0 +1 @@
1
+ export declare function hostCall(name: string, value: unknown, signal?: AbortSignal, update?: (value: any) => void): Promise<any>;
@@ -0,0 +1,14 @@
1
+ let nextId = 1;
2
+ export async function hostCall(name, value, signal, update) {
3
+ signal?.throwIfAborted();
4
+ const id = nextId++;
5
+ const abort = () => __cancel(id);
6
+ const pending = __host(name, JSON.stringify(value), raw => update?.(JSON.parse(raw)), id);
7
+ signal?.addEventListener('abort', abort, { once: true });
8
+ try {
9
+ return JSON.parse(await pending);
10
+ }
11
+ finally {
12
+ signal?.removeEventListener('abort', abort);
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ import './platform.js';