@synsema/wasm 0.6.1
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/README.md +90 -0
- package/bridge.mjs +134 -0
- package/index.d.ts +141 -0
- package/index.mjs +371 -0
- package/package.json +44 -0
- package/synsema.wasm +0 -0
- package/worker.mjs +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @synsema/wasm
|
|
2
|
+
|
|
3
|
+
[Synsema](https://github.com/kitecosmic/synsema) — the programming language for AI agents —
|
|
4
|
+
embedded as WebAssembly. Run `.syn` programs inside a browser, Node/Bun/Deno or an edge
|
|
5
|
+
runtime, and lend them exactly the capabilities your app decides to: `http`, a `kv`
|
|
6
|
+
store, an `llm`, a `log`. No Synsema backend, nothing to install on the host.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { Synsema } from "@synsema/wasm";
|
|
10
|
+
|
|
11
|
+
const syn = await Synsema.load(new URL("@synsema/wasm/synsema.wasm", import.meta.url));
|
|
12
|
+
await syn.ready();
|
|
13
|
+
|
|
14
|
+
const r = syn.run(`print(keccak256("hola"))`);
|
|
15
|
+
r.output; // ["…"] — the program's print lines
|
|
16
|
+
r.errors; // [] — parse/runtime errors are data, never exceptions
|
|
17
|
+
r.audit; // every capability check the program made
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Lending capabilities (the host)
|
|
21
|
+
|
|
22
|
+
The program keeps its manifest (`require net("api.example")`, `require llm`,
|
|
23
|
+
`require memory("agenda")`) and your app decides what it actually gets. A missing hook
|
|
24
|
+
makes the builtin fail with a clear error; a `ceiling` denies above what the host offers.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const host = {
|
|
28
|
+
http: (req) => ({ status: 200, headers: [["content-type", "application/json"]], body: "{}" }),
|
|
29
|
+
kv: { get: (ns, k) => store.get(ns + "/" + k) ?? null, set: (ns, k, v) => store.set(ns + "/" + k, v) },
|
|
30
|
+
llm: (op, prompt) => ({ content: "…", tokens: 12 }),
|
|
31
|
+
log: (line) => console.log(line),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
syn.run(`require memory("agenda")\nremember("preference", "dark mode")`, { host, filename: "agenda.syn" });
|
|
35
|
+
syn.run(`require llm\nprint(reason about "the weather")`, { host, ceiling: "stdout" }); // denied: ceiling wins
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `kv` backs persistent agent memory (`remember`/`recall`, rules, progress — namespace
|
|
39
|
+
`memory:<declared name>`) and `state_*` (namespace `state`).
|
|
40
|
+
- `llm` backs `reason`/`decide`/`analyze`/`generate`; `llm_usage()` sums the tokens you report.
|
|
41
|
+
- `http` backs `fetch`/`http_*` and the blockchain read-side RPC — after the `net(host)`
|
|
42
|
+
gate, with the same URL canonicalization as the native binary.
|
|
43
|
+
|
|
44
|
+
## Async hosts (browser `fetch`, IndexedDB, LLM SDKs)
|
|
45
|
+
|
|
46
|
+
The interpreter is synchronous. `runAsync`/`handleAsync` run it in a Worker and block on
|
|
47
|
+
`Atomics.wait` while your Promises resolve on the main thread:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const r = await syn.runAsync(program, { host: { async http(req) { const res = await fetch(req.url); return { status: res.status, headers: res.headers, body: await res.text() }; } } });
|
|
51
|
+
await syn.close();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Node/Bun/Deno: works out of the box. Browsers: needs cross-origin isolation
|
|
55
|
+
(`Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy: require-corp`)
|
|
56
|
+
for `SharedArrayBuffer`. Without it, use the sync API with a sync host.
|
|
57
|
+
|
|
58
|
+
## `serve` without sockets (edge handler)
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const app = `require serve(8080)
|
|
62
|
+
serve on 8080
|
|
63
|
+
route "GET /hello/:name"
|
|
64
|
+
give {"hi": params.name, "visits": state_incr("visits")}`;
|
|
65
|
+
const res = syn.handle(app, { method: "GET", path: "/hello/ana", headers: {} }, { host });
|
|
66
|
+
res.status; res.content_type; res.headers; res.body;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Routes, params, query, `auth with`, `errors with`, `content()` negotiation, `redirect`,
|
|
70
|
+
pagination and `state_*` (durable through your `kv`) work as in the native server. Not in
|
|
71
|
+
handler mode: `stream` (SSE), `proxy to`, rate limits, `static` mounts, vhosts, TLS —
|
|
72
|
+
your edge platform does those before calling the handler.
|
|
73
|
+
|
|
74
|
+
## Also: `test`, `check`, `version`
|
|
75
|
+
|
|
76
|
+
`syn.test(source)` runs the file's `test` blocks; `syn.check(source)` parses and
|
|
77
|
+
validates without running; `syn.version()` is the artifact's release tag.
|
|
78
|
+
|
|
79
|
+
## Files, time, randomness
|
|
80
|
+
|
|
81
|
+
This artifact has **no filesystem** (`read_file` says so; pass data through `env` or the
|
|
82
|
+
source). `now()` comes from `Date.now()`; `random()`, `token()` and every signature nonce
|
|
83
|
+
come from `crypto.getRandomValues`. For file-based confidential jobs use the wasip1 build
|
|
84
|
+
under wasmtime (see the Synsema docs, *WebAssembly*).
|
|
85
|
+
|
|
86
|
+
## Other languages
|
|
87
|
+
|
|
88
|
+
The `.wasm` has a plain JSON ABI (`synsema_call`) and three imports (`synsema_host`),
|
|
89
|
+
so it embeds anywhere: see `examples/embed/python` (wasmtime-py) and `examples/embed/go`
|
|
90
|
+
(wazero) in the repository.
|
package/bridge.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Bridge asíncrono: el intérprete (síncrono) corre en un Worker; cuando necesita al
|
|
2
|
+
// host (http/kv/llm/log/sleep) manda el pedido al main thread y se bloquea con
|
|
3
|
+
// Atomics.wait sobre un SharedArrayBuffer hasta que la Promise del host resuelve.
|
|
4
|
+
//
|
|
5
|
+
// Layout del SAB: Int32Array ctrl = [estado, len_chunk, restante]; datos desde el
|
|
6
|
+
// byte 16. Estados: 0 = esperando, 1 = último chunk listo, 2 = chunk listo (hay más),
|
|
7
|
+
// 3 = el host no ofrece ese hook. Respuestas más grandes que el SAB viajan en chunks:
|
|
8
|
+
// el worker copia, pone 0, avisa con `ack` y el main escribe el siguiente.
|
|
9
|
+
//
|
|
10
|
+
// Navegador: SharedArrayBuffer requiere cross-origin isolation (headers
|
|
11
|
+
// Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp).
|
|
12
|
+
// Node/Bun/Deno: funciona sin más.
|
|
13
|
+
|
|
14
|
+
import { dispatchHostAsync } from "./index.mjs";
|
|
15
|
+
|
|
16
|
+
const te = new TextEncoder();
|
|
17
|
+
const IS_NODE = typeof process !== "undefined" && process.versions && process.versions.node;
|
|
18
|
+
const DATA_OFFSET = 16;
|
|
19
|
+
|
|
20
|
+
async function spawnWorker() {
|
|
21
|
+
const url = new URL("./worker.mjs", import.meta.url);
|
|
22
|
+
if (IS_NODE) {
|
|
23
|
+
const { Worker } = await import("node:worker_threads");
|
|
24
|
+
const w = new Worker(url);
|
|
25
|
+
return {
|
|
26
|
+
post: (m) => w.postMessage(m),
|
|
27
|
+
onMessage: (fn) => w.on("message", fn),
|
|
28
|
+
onError: (fn) => w.on("error", fn),
|
|
29
|
+
terminate: () => w.terminate(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const w = new Worker(url, { type: "module" });
|
|
33
|
+
return {
|
|
34
|
+
post: (m) => w.postMessage(m),
|
|
35
|
+
onMessage: (fn) => (w.onmessage = (e) => fn(e.data)),
|
|
36
|
+
onError: (fn) => (w.onerror = (e) => fn(e.error ?? new Error(e.message))),
|
|
37
|
+
terminate: () => w.terminate(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createBridge(module, { bridgeBytes = 4 * 1024 * 1024 } = {}) {
|
|
42
|
+
if (typeof SharedArrayBuffer === "undefined") {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"synsema: the async mode needs SharedArrayBuffer (in browsers, serve the page cross-origin isolated: COOP same-origin + COEP require-corp); use the sync API with a sync host instead",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const sab = new SharedArrayBuffer(DATA_OFFSET + bridgeBytes);
|
|
48
|
+
const ctrl = new Int32Array(sab, 0, 4);
|
|
49
|
+
const data = new Uint8Array(sab, DATA_OFFSET);
|
|
50
|
+
const w = await spawnWorker();
|
|
51
|
+
|
|
52
|
+
let pending = null; // { resolve, reject, host }
|
|
53
|
+
let sending = null; // { bytes, offset }
|
|
54
|
+
|
|
55
|
+
function writeChunk() {
|
|
56
|
+
const { bytes } = sending;
|
|
57
|
+
const n = Math.min(bytes.length - sending.offset, data.length);
|
|
58
|
+
data.set(bytes.subarray(sending.offset, sending.offset + n));
|
|
59
|
+
sending.offset += n;
|
|
60
|
+
const remaining = bytes.length - sending.offset;
|
|
61
|
+
Atomics.store(ctrl, 1, n);
|
|
62
|
+
Atomics.store(ctrl, 2, remaining);
|
|
63
|
+
Atomics.store(ctrl, 0, remaining > 0 ? 2 : 1);
|
|
64
|
+
Atomics.notify(ctrl, 0);
|
|
65
|
+
if (remaining === 0) sending = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function reply(result) {
|
|
69
|
+
if (result === undefined) {
|
|
70
|
+
Atomics.store(ctrl, 0, 3);
|
|
71
|
+
Atomics.notify(ctrl, 0);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
sending = { bytes: te.encode(JSON.stringify(result ?? null)), offset: 0 };
|
|
75
|
+
writeChunk();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ready = new Promise((resolve, reject) => {
|
|
79
|
+
w.onError((e) => {
|
|
80
|
+
const p = pending;
|
|
81
|
+
pending = null;
|
|
82
|
+
if (p) p.reject(e);
|
|
83
|
+
else reject(e);
|
|
84
|
+
});
|
|
85
|
+
w.onMessage(async (m) => {
|
|
86
|
+
switch (m.type) {
|
|
87
|
+
case "ready":
|
|
88
|
+
resolve();
|
|
89
|
+
break;
|
|
90
|
+
case "host": {
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await dispatchHostAsync(pending?.host, m.kind, m.payload);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
result = { error: String(e && e.message ? e.message : e) };
|
|
96
|
+
}
|
|
97
|
+
reply(result);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "ack":
|
|
101
|
+
if (sending) writeChunk();
|
|
102
|
+
break;
|
|
103
|
+
case "result": {
|
|
104
|
+
const p = pending;
|
|
105
|
+
pending = null;
|
|
106
|
+
p?.resolve(m.response);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "error": {
|
|
110
|
+
const p = pending;
|
|
111
|
+
pending = null;
|
|
112
|
+
p?.reject(new Error(m.message));
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
w.post({ type: "init", module, sab });
|
|
119
|
+
await ready;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
call(request, host) {
|
|
123
|
+
if (pending) return Promise.reject(new Error("synsema: one call at a time per instance"));
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
pending = { resolve, reject, host };
|
|
126
|
+
Atomics.store(ctrl, 0, 0);
|
|
127
|
+
w.post({ type: "call", request });
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
async close() {
|
|
131
|
+
await w.terminate();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Tipos de @synsema/wasm — la frontera JSON de synsema-wasm-web.
|
|
2
|
+
|
|
3
|
+
export type HeaderPairs = Array<[string, string]>;
|
|
4
|
+
|
|
5
|
+
export interface HttpRequestPayload {
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
headers: HeaderPairs;
|
|
9
|
+
/** Body como texto (o `body_base64` si era binario). */
|
|
10
|
+
body?: string | null;
|
|
11
|
+
body_base64?: string;
|
|
12
|
+
/** Segundos. */
|
|
13
|
+
timeout: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HttpResponsePayload {
|
|
17
|
+
status: number;
|
|
18
|
+
headers?: HeaderPairs | Record<string, string> | Headers;
|
|
19
|
+
body?: string | Uint8Array | ArrayBuffer | null;
|
|
20
|
+
/** Error de transporte (DNS, timeout…): el programa lo ve en `{error: …}`. */
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface KvStore {
|
|
25
|
+
get(ns: string, key: string): string | null | undefined | Promise<string | null | undefined>;
|
|
26
|
+
set(ns: string, key: string, value: string): void | Promise<void>;
|
|
27
|
+
delete?(ns: string, key: string): void | Promise<void>;
|
|
28
|
+
/** Claves del namespace (para `state_all`). */
|
|
29
|
+
list?(ns: string): string[] | Promise<string[]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LlmResult {
|
|
33
|
+
content: string;
|
|
34
|
+
/** Tokens consumidos (alimenta `llm_usage()`). */
|
|
35
|
+
tokens?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lo que tu app le PRESTA al programa. Cada hook es opcional; lo que falta hace que el
|
|
40
|
+
* builtin correspondiente falle con un error claro. El programa igual debe declarar
|
|
41
|
+
* `require net("host")` / `require llm` / `require memory("x")`, y `ceiling` manda.
|
|
42
|
+
* En la API síncrona los hooks deben ser síncronos; en `*Async` pueden devolver Promise.
|
|
43
|
+
*/
|
|
44
|
+
export interface Host {
|
|
45
|
+
http?(req: HttpRequestPayload): HttpResponsePayload | Promise<HttpResponsePayload>;
|
|
46
|
+
kv?: KvStore;
|
|
47
|
+
llm?(op: string, prompt: string): string | LlmResult | Promise<string | LlmResult>;
|
|
48
|
+
log?(line: string): void | Promise<void>;
|
|
49
|
+
/** Una función, o `true` para la pausa por defecto (Atomics.wait / setTimeout). */
|
|
50
|
+
sleep?: ((secs: number) => void | Promise<void>) | true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RunOptions {
|
|
54
|
+
filename?: string;
|
|
55
|
+
/** Reemplaza al `.env`: `secret("KEY")`/`env("KEY")` resuelven de acá. */
|
|
56
|
+
env?: Record<string, string>;
|
|
57
|
+
/** `"sandbox"` (stdout + time) o la lista de `--cap-set`: `"stdout,secret=ETH_*"`. */
|
|
58
|
+
ceiling?: string;
|
|
59
|
+
host?: Host;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AuditEntry {
|
|
63
|
+
capability: string;
|
|
64
|
+
granted: boolean;
|
|
65
|
+
source: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RunResult {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
/** Las líneas de `print`. */
|
|
72
|
+
output: string[];
|
|
73
|
+
errors: string[];
|
|
74
|
+
audit: AuditEntry[];
|
|
75
|
+
llm_tokens: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TestResult {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
passed: number;
|
|
81
|
+
failed: number;
|
|
82
|
+
lines: string[];
|
|
83
|
+
audit: AuditEntry[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CheckResult {
|
|
87
|
+
ok: boolean;
|
|
88
|
+
errors: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface HandleRequest {
|
|
92
|
+
method?: string;
|
|
93
|
+
/** Path, con `?query` opcional. */
|
|
94
|
+
path?: string;
|
|
95
|
+
headers?: HeaderPairs | Record<string, string> | Headers;
|
|
96
|
+
body?: string | Uint8Array | ArrayBuffer | null;
|
|
97
|
+
ip?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface HandleResult {
|
|
101
|
+
status: number;
|
|
102
|
+
content_type: string;
|
|
103
|
+
headers: HeaderPairs;
|
|
104
|
+
body?: string;
|
|
105
|
+
/** Cuerpo binario (cuando no es UTF-8). */
|
|
106
|
+
body_base64?: string;
|
|
107
|
+
/** Líneas de `print` del handler. */
|
|
108
|
+
log: string[];
|
|
109
|
+
/** Errores de preparación del programa (parse/top-level); el status es 500. */
|
|
110
|
+
errors: string[];
|
|
111
|
+
audit: AuditEntry[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type ModuleSource = WebAssembly.Module | Uint8Array | ArrayBuffer | URL | string | Response;
|
|
115
|
+
|
|
116
|
+
export declare function readModule(src: ModuleSource): Promise<WebAssembly.Module>;
|
|
117
|
+
export declare function offersOf(host?: Host): Record<string, boolean>;
|
|
118
|
+
export declare function sleepSync(secs: number): void;
|
|
119
|
+
export declare function instantiate(
|
|
120
|
+
module: WebAssembly.Module,
|
|
121
|
+
hostCall: (kind: string, payload: any) => any,
|
|
122
|
+
): Promise<{ exports: WebAssembly.Exports; call(request: object): any }>;
|
|
123
|
+
|
|
124
|
+
export declare class Synsema {
|
|
125
|
+
constructor(module: WebAssembly.Module);
|
|
126
|
+
static load(src: ModuleSource): Promise<Synsema>;
|
|
127
|
+
/** Prepara la instancia síncrona (obligatorio antes de run/test/check/handle). */
|
|
128
|
+
ready(): Promise<this>;
|
|
129
|
+
version(): string;
|
|
130
|
+
run(source: string, opts?: RunOptions): RunResult;
|
|
131
|
+
test(source: string, opts?: RunOptions): TestResult;
|
|
132
|
+
check(source: string, opts?: { filename?: string }): CheckResult;
|
|
133
|
+
handle(source: string, request: HandleRequest, opts?: RunOptions): HandleResult;
|
|
134
|
+
/** Modo asíncrono (Worker + Atomics): los hooks del host pueden ser async. */
|
|
135
|
+
runAsync(source: string, opts?: RunOptions): Promise<RunResult>;
|
|
136
|
+
testAsync(source: string, opts?: RunOptions): Promise<TestResult>;
|
|
137
|
+
handleAsync(source: string, request: HandleRequest, opts?: RunOptions): Promise<HandleResult>;
|
|
138
|
+
close(): Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default Synsema;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// @synsema/wasm — Synsema embebido en JS/TS (navegador, Node ≥ 18, Bun, Deno).
|
|
2
|
+
//
|
|
3
|
+
// Glue de la ABI cruda de `synsema-wasm-web` (ver engine/crates/synsema-wasm-web):
|
|
4
|
+
// exports synsema_alloc / synsema_free / synsema_call(json) -> [u32 len][json]
|
|
5
|
+
// imports synsema_host.{host_call, host_random_fill, host_now_ms}
|
|
6
|
+
//
|
|
7
|
+
// Dos modos:
|
|
8
|
+
// - SÍNCRONO (`Synsema.run/test/check/handle`): el host que le pasás (http/kv/llm/log/
|
|
9
|
+
// sleep) debe responder de forma síncrona. Ideal en Node con un KV en memoria o un
|
|
10
|
+
// archivo JSON, o sin host.
|
|
11
|
+
// - ASÍNCRONO (`Synsema.runAsync/handleAsync`): el intérprete corre en un Worker y
|
|
12
|
+
// bloquea con Atomics.wait sobre un SharedArrayBuffer mientras tu `http`/`kv`/`llm`
|
|
13
|
+
// async resuelven en el main thread (fetch del navegador, IndexedDB, un SDK de LLM).
|
|
14
|
+
// En el navegador requiere cross-origin isolation (COOP/COEP) para SharedArrayBuffer.
|
|
15
|
+
//
|
|
16
|
+
// No hay estado global: cada `load()` es una instancia (memoria lineal propia).
|
|
17
|
+
|
|
18
|
+
const te = new TextEncoder();
|
|
19
|
+
const td = new TextDecoder();
|
|
20
|
+
|
|
21
|
+
const IS_NODE = typeof process !== "undefined" && process.versions && process.versions.node;
|
|
22
|
+
|
|
23
|
+
/** Lee el .wasm desde bytes, Module, URL/path o Response. */
|
|
24
|
+
export async function readModule(src) {
|
|
25
|
+
if (src instanceof WebAssembly.Module) return src;
|
|
26
|
+
if (src instanceof Uint8Array || src instanceof ArrayBuffer) return WebAssembly.compile(src);
|
|
27
|
+
if (typeof Response !== "undefined" && src instanceof Response) {
|
|
28
|
+
return WebAssembly.compile(await src.arrayBuffer());
|
|
29
|
+
}
|
|
30
|
+
const isFileLike =
|
|
31
|
+
IS_NODE && (typeof src === "string" ? !/^https?:/.test(src) : src instanceof URL && src.protocol === "file:");
|
|
32
|
+
if (isFileLike) {
|
|
33
|
+
const { readFile } = await import("node:fs/promises");
|
|
34
|
+
const { fileURLToPath } = await import("node:url");
|
|
35
|
+
const p = typeof src === "string" ? src : fileURLToPath(src);
|
|
36
|
+
return WebAssembly.compile(await readFile(p));
|
|
37
|
+
}
|
|
38
|
+
const res = await fetch(src);
|
|
39
|
+
if (!res.ok) throw new Error(`synsema: cannot fetch ${src}: ${res.status}`);
|
|
40
|
+
if (WebAssembly.compileStreaming) return WebAssembly.compileStreaming(res);
|
|
41
|
+
return WebAssembly.compile(await res.arrayBuffer());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function randomFill(bytes) {
|
|
45
|
+
const c = globalThis.crypto;
|
|
46
|
+
if (!c || !c.getRandomValues) throw new Error("synsema: no crypto.getRandomValues in this host");
|
|
47
|
+
// getRandomValues acepta hasta 65536 bytes por llamada.
|
|
48
|
+
for (let i = 0; i < bytes.length; i += 65536) c.getRandomValues(bytes.subarray(i, Math.min(i + 65536, bytes.length)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Qué ofrece un host: derivado de las funciones presentes. */
|
|
52
|
+
export function offersOf(host) {
|
|
53
|
+
if (!host) return {};
|
|
54
|
+
const o = {};
|
|
55
|
+
if (typeof host.http === "function") o.http = true;
|
|
56
|
+
if (host.kv && typeof host.kv.get === "function") o.kv = true;
|
|
57
|
+
if (typeof host.llm === "function") o.llm = true;
|
|
58
|
+
if (typeof host.log === "function") o.log = true;
|
|
59
|
+
if (typeof host.sleep === "function" || host.sleep === true) o.sleep = true;
|
|
60
|
+
return o;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Un hook async usado desde la API síncrona devolvería una Promise donde el intérprete
|
|
64
|
+
* espera un valor: en vez de un `{status: 0}` mudo, un error que nombra el fix. */
|
|
65
|
+
function syncOnly(kind, r) {
|
|
66
|
+
if (r && typeof r.then === "function") {
|
|
67
|
+
throw new Error(`synsema: the host \`${kind}\` hook returned a Promise — use runAsync/handleAsync (Worker + Atomics) or make the hook synchronous`);
|
|
68
|
+
}
|
|
69
|
+
return r;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Despacha un `host_call` (kind, payload) a un host SÍNCRONO. Devuelve un objeto
|
|
73
|
+
* JSON-serializable, o `undefined` si el host no ofrece ese hook. Un hook que devuelve
|
|
74
|
+
* una Promise es un error (lo ve el programa como `{error}` / lo lanza `synsema_call`). */
|
|
75
|
+
export function dispatchHostSync(host, kind, payload) {
|
|
76
|
+
if (!host) return undefined;
|
|
77
|
+
switch (kind) {
|
|
78
|
+
case "http":
|
|
79
|
+
return typeof host.http === "function" ? normalizeHttp(syncOnly("http", host.http(payload))) : undefined;
|
|
80
|
+
case "kv_get":
|
|
81
|
+
return host.kv ? { value: syncOnly("kv.get", host.kv.get(payload.ns, payload.key)) ?? null } : undefined;
|
|
82
|
+
case "kv_set":
|
|
83
|
+
if (!host.kv) return undefined;
|
|
84
|
+
syncOnly("kv.set", host.kv.set(payload.ns, payload.key, payload.value));
|
|
85
|
+
return {};
|
|
86
|
+
case "kv_delete":
|
|
87
|
+
if (!host.kv) return undefined;
|
|
88
|
+
syncOnly("kv.delete", host.kv.delete?.(payload.ns, payload.key));
|
|
89
|
+
return {};
|
|
90
|
+
case "kv_list":
|
|
91
|
+
return host.kv ? { keys: host.kv.list ? syncOnly("kv.list", host.kv.list(payload.ns)) : [] } : undefined;
|
|
92
|
+
case "llm":
|
|
93
|
+
return typeof host.llm === "function" ? normalizeLlm(syncOnly("llm", host.llm(payload.op, payload.prompt))) : undefined;
|
|
94
|
+
case "log":
|
|
95
|
+
if (typeof host.log !== "function") return undefined;
|
|
96
|
+
host.log(payload.line);
|
|
97
|
+
return {};
|
|
98
|
+
case "sleep":
|
|
99
|
+
if (typeof host.sleep === "function") {
|
|
100
|
+
host.sleep(payload.secs);
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
if (host.sleep === true) {
|
|
104
|
+
sleepSync(payload.secs);
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
default:
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Igual que dispatchHostSync pero el host puede devolver Promises. */
|
|
114
|
+
export async function dispatchHostAsync(host, kind, payload) {
|
|
115
|
+
if (!host) return undefined;
|
|
116
|
+
switch (kind) {
|
|
117
|
+
case "http":
|
|
118
|
+
return typeof host.http === "function" ? normalizeHttp(await host.http(payload)) : undefined;
|
|
119
|
+
case "kv_get":
|
|
120
|
+
return host.kv ? { value: (await host.kv.get(payload.ns, payload.key)) ?? null } : undefined;
|
|
121
|
+
case "kv_set":
|
|
122
|
+
if (!host.kv) return undefined;
|
|
123
|
+
await host.kv.set(payload.ns, payload.key, payload.value);
|
|
124
|
+
return {};
|
|
125
|
+
case "kv_delete":
|
|
126
|
+
if (!host.kv) return undefined;
|
|
127
|
+
await host.kv.delete?.(payload.ns, payload.key);
|
|
128
|
+
return {};
|
|
129
|
+
case "kv_list":
|
|
130
|
+
return host.kv ? { keys: host.kv.list ? await host.kv.list(payload.ns) : [] } : undefined;
|
|
131
|
+
case "llm":
|
|
132
|
+
return typeof host.llm === "function" ? normalizeLlm(await host.llm(payload.op, payload.prompt)) : undefined;
|
|
133
|
+
case "log":
|
|
134
|
+
if (typeof host.log !== "function") return undefined;
|
|
135
|
+
await host.log(payload.line);
|
|
136
|
+
return {};
|
|
137
|
+
case "sleep":
|
|
138
|
+
if (typeof host.sleep === "function") {
|
|
139
|
+
await host.sleep(payload.secs);
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
if (host.sleep === true) {
|
|
143
|
+
await new Promise((r) => setTimeout(r, payload.secs * 1000));
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
default:
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeHttp(r) {
|
|
153
|
+
if (!r || typeof r !== "object") return { error: "host http returned nothing" };
|
|
154
|
+
const out = { status: r.status | 0, headers: [] };
|
|
155
|
+
if (r.error) out.error = String(r.error);
|
|
156
|
+
if (r.headers) {
|
|
157
|
+
if (Array.isArray(r.headers)) out.headers = r.headers;
|
|
158
|
+
else if (typeof r.headers.forEach === "function" && !(r.headers instanceof Array)) {
|
|
159
|
+
// Headers de fetch
|
|
160
|
+
r.headers.forEach((v, k) => out.headers.push([k, v]));
|
|
161
|
+
} else out.headers = Object.entries(r.headers);
|
|
162
|
+
}
|
|
163
|
+
if (r.body instanceof Uint8Array || r.body instanceof ArrayBuffer) {
|
|
164
|
+
out.body_base64 = b64(r.body instanceof Uint8Array ? r.body : new Uint8Array(r.body));
|
|
165
|
+
} else if (r.body != null) out.body = String(r.body);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeLlm(r) {
|
|
170
|
+
if (typeof r === "string") return { content: r, tokens: 0 };
|
|
171
|
+
if (r && typeof r === "object") return { content: String(r.content ?? ""), tokens: r.tokens | 0 };
|
|
172
|
+
return { content: "", tokens: 0 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function b64(bytes) {
|
|
176
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
177
|
+
let s = "";
|
|
178
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
179
|
+
return btoa(s);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function sleepSync(secs) {
|
|
183
|
+
try {
|
|
184
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, secs * 1000));
|
|
185
|
+
} catch {
|
|
186
|
+
// Main thread del navegador: Atomics.wait no está permitido. Espera activa acotada.
|
|
187
|
+
const until = Date.now() + Math.min(secs, 5) * 1000;
|
|
188
|
+
while (Date.now() < until) { /* busy */ }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Instancia el módulo con un `hostCall(kind, payload) -> objeto | undefined` SÍNCRONO
|
|
194
|
+
* (el bridge async lo implementa con Atomics). Devuelve `{ call(request) -> response }`.
|
|
195
|
+
*/
|
|
196
|
+
export async function instantiate(module, hostCall) {
|
|
197
|
+
let exports = null;
|
|
198
|
+
const mem = () => new Uint8Array(exports.memory.buffer);
|
|
199
|
+
const writeBytes = (bytes) => {
|
|
200
|
+
const ptr = exports.synsema_alloc(bytes.length);
|
|
201
|
+
mem().set(bytes, ptr);
|
|
202
|
+
return ptr;
|
|
203
|
+
};
|
|
204
|
+
const imports = {
|
|
205
|
+
synsema_host: {
|
|
206
|
+
host_call(kp, kl, pp, pl) {
|
|
207
|
+
const kind = td.decode(mem().subarray(kp, kp + kl));
|
|
208
|
+
const payload = JSON.parse(td.decode(mem().subarray(pp, pp + pl)));
|
|
209
|
+
let result;
|
|
210
|
+
try {
|
|
211
|
+
result = hostCall(kind, payload);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
result = { error: String(e && e.message ? e.message : e) };
|
|
214
|
+
}
|
|
215
|
+
if (result === undefined) return 0;
|
|
216
|
+
const bytes = te.encode(JSON.stringify(result ?? null));
|
|
217
|
+
const ptr = exports.synsema_alloc(4 + bytes.length);
|
|
218
|
+
const m = mem();
|
|
219
|
+
new DataView(m.buffer).setUint32(ptr, bytes.length, true);
|
|
220
|
+
m.set(bytes, ptr + 4);
|
|
221
|
+
return ptr;
|
|
222
|
+
},
|
|
223
|
+
host_random_fill(ptr, len) {
|
|
224
|
+
try {
|
|
225
|
+
randomFill(mem().subarray(ptr, ptr + len));
|
|
226
|
+
return 0;
|
|
227
|
+
} catch {
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
host_now_ms() {
|
|
232
|
+
return Date.now();
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
237
|
+
exports = instance.exports;
|
|
238
|
+
return {
|
|
239
|
+
exports,
|
|
240
|
+
call(request) {
|
|
241
|
+
const req = te.encode(JSON.stringify(request));
|
|
242
|
+
const rp = writeBytes(req);
|
|
243
|
+
let out;
|
|
244
|
+
try {
|
|
245
|
+
out = exports.synsema_call(rp, req.length);
|
|
246
|
+
} finally {
|
|
247
|
+
exports.synsema_free(rp, req.length);
|
|
248
|
+
}
|
|
249
|
+
const m = mem();
|
|
250
|
+
const len = new DataView(m.buffer).getUint32(out, true);
|
|
251
|
+
const json = td.decode(m.subarray(out + 4, out + 4 + len));
|
|
252
|
+
exports.synsema_free(out, 4 + len);
|
|
253
|
+
return JSON.parse(json);
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function requestOf(op, source, opts = {}) {
|
|
259
|
+
const r = { op, source, filename: opts.filename ?? "<embedded>" };
|
|
260
|
+
if (opts.env) r.env = opts.env;
|
|
261
|
+
if (opts.ceiling) r.ceiling = opts.ceiling;
|
|
262
|
+
if (opts.host) r.host = offersOf(opts.host);
|
|
263
|
+
return r;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeRequest(request) {
|
|
267
|
+
const headers = Array.isArray(request.headers)
|
|
268
|
+
? request.headers
|
|
269
|
+
: request.headers && typeof request.headers.forEach === "function"
|
|
270
|
+
? (() => { const h = []; request.headers.forEach((v, k) => h.push([k, v])); return h; })()
|
|
271
|
+
: Object.entries(request.headers ?? {});
|
|
272
|
+
const out = { method: request.method ?? "GET", path: request.path ?? "/", headers, ip: request.ip ?? "" };
|
|
273
|
+
if (request.body instanceof Uint8Array || request.body instanceof ArrayBuffer) {
|
|
274
|
+
out.body_base64 = b64(request.body instanceof Uint8Array ? request.body : new Uint8Array(request.body));
|
|
275
|
+
} else out.body = request.body == null ? "" : String(request.body);
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export class Synsema {
|
|
280
|
+
/** @param {WebAssembly.Module} module */
|
|
281
|
+
constructor(module) {
|
|
282
|
+
this.module = module;
|
|
283
|
+
this._inst = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Carga el .wasm (bytes, Module, URL/path, Response). */
|
|
287
|
+
static async load(src) {
|
|
288
|
+
return new Synsema(await readModule(src));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async _instance() {
|
|
292
|
+
if (!this._inst) {
|
|
293
|
+
// Cada llamada síncrona lleva su host; la instancia es una y despacha al host
|
|
294
|
+
// de la llamada en curso.
|
|
295
|
+
const self = this;
|
|
296
|
+
this._inst = await instantiate(this.module, (kind, payload) => dispatchHostSync(self._host, kind, payload));
|
|
297
|
+
}
|
|
298
|
+
return this._inst;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_callSync(request, host) {
|
|
302
|
+
const inst = this._instSync();
|
|
303
|
+
this._host = host;
|
|
304
|
+
try {
|
|
305
|
+
return inst.call(request);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
// Un trap deja la instancia inservible: se reinstancia en la próxima llamada.
|
|
308
|
+
this._inst = null;
|
|
309
|
+
this._ready = null;
|
|
310
|
+
throw new Error(`synsema: the module trapped (${e && e.message ? e.message : e}); the instance was discarded`);
|
|
311
|
+
} finally {
|
|
312
|
+
this._host = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_instSync() {
|
|
317
|
+
if (!this._inst) throw new Error("synsema: call `await syn.ready()` (or use the *Async methods) before the first call");
|
|
318
|
+
return this._inst;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Prepara la instancia síncrona. Idempotente. */
|
|
322
|
+
async ready() {
|
|
323
|
+
if (!this._ready) this._ready = this._instance();
|
|
324
|
+
await this._ready;
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
version() {
|
|
329
|
+
return this._callSync({ op: "version" }).version;
|
|
330
|
+
}
|
|
331
|
+
run(source, opts = {}) {
|
|
332
|
+
return this._callSync(requestOf("run", source, opts), opts.host);
|
|
333
|
+
}
|
|
334
|
+
test(source, opts = {}) {
|
|
335
|
+
return this._callSync(requestOf("test", source, opts), opts.host);
|
|
336
|
+
}
|
|
337
|
+
check(source, opts = {}) {
|
|
338
|
+
return this._callSync({ op: "check", source, filename: opts.filename ?? "<embedded>" });
|
|
339
|
+
}
|
|
340
|
+
handle(source, request, opts = {}) {
|
|
341
|
+
return this._callSync({ ...requestOf("handle", source, opts), request: normalizeRequest(request) }, opts.host);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---- modo asíncrono (Worker + Atomics) ----
|
|
345
|
+
|
|
346
|
+
async _bridge() {
|
|
347
|
+
if (!this._worker) {
|
|
348
|
+
const { createBridge } = await import("./bridge.mjs");
|
|
349
|
+
this._worker = await createBridge(this.module);
|
|
350
|
+
}
|
|
351
|
+
return this._worker;
|
|
352
|
+
}
|
|
353
|
+
async runAsync(source, opts = {}) {
|
|
354
|
+
return (await this._bridge()).call(requestOf("run", source, opts), opts.host);
|
|
355
|
+
}
|
|
356
|
+
async testAsync(source, opts = {}) {
|
|
357
|
+
return (await this._bridge()).call(requestOf("test", source, opts), opts.host);
|
|
358
|
+
}
|
|
359
|
+
async handleAsync(source, request, opts = {}) {
|
|
360
|
+
return (await this._bridge()).call({ ...requestOf("handle", source, opts), request: normalizeRequest(request) }, opts.host);
|
|
361
|
+
}
|
|
362
|
+
/** Termina el Worker del modo asíncrono (si se creó). */
|
|
363
|
+
async close() {
|
|
364
|
+
if (this._worker) {
|
|
365
|
+
await this._worker.close();
|
|
366
|
+
this._worker = null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export default Synsema;
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synsema/wasm",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Synsema — the programming language for AI agents — embedded as WebAssembly: run .syn programs inside browsers, Node/Bun/Deno and edge runtimes, lending them http/kv/llm from your app",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./index.mjs",
|
|
8
|
+
"module": "./index.mjs",
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"import": "./index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./synsema.wasm": "./synsema.wasm",
|
|
16
|
+
"./bridge.mjs": "./bridge.mjs",
|
|
17
|
+
"./worker.mjs": "./worker.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.mjs",
|
|
21
|
+
"bridge.mjs",
|
|
22
|
+
"worker.mjs",
|
|
23
|
+
"index.d.ts",
|
|
24
|
+
"synsema.wasm",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/kitecosmic/synsema.git",
|
|
34
|
+
"directory": "packages/synsema-wasm"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"synsema",
|
|
38
|
+
"wasm",
|
|
39
|
+
"webassembly",
|
|
40
|
+
"agents",
|
|
41
|
+
"llm",
|
|
42
|
+
"edge"
|
|
43
|
+
]
|
|
44
|
+
}
|
package/synsema.wasm
ADDED
|
Binary file
|
package/worker.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Lado Worker del bridge asíncrono (ver bridge.mjs). Corre en Node (worker_threads)
|
|
2
|
+
// y en el navegador (module Worker). Instancia el .wasm con un hostCall que manda el
|
|
3
|
+
// pedido al main thread y se bloquea con Atomics.wait hasta la respuesta.
|
|
4
|
+
|
|
5
|
+
import { instantiate } from "./index.mjs";
|
|
6
|
+
|
|
7
|
+
const td = new TextDecoder();
|
|
8
|
+
const DATA_OFFSET = 16;
|
|
9
|
+
const IS_NODE = typeof process !== "undefined" && process.versions && process.versions.node;
|
|
10
|
+
|
|
11
|
+
let post;
|
|
12
|
+
let onMessage;
|
|
13
|
+
if (IS_NODE) {
|
|
14
|
+
const { parentPort } = await import("node:worker_threads");
|
|
15
|
+
post = (m) => parentPort.postMessage(m);
|
|
16
|
+
onMessage = (fn) => parentPort.on("message", fn);
|
|
17
|
+
} else {
|
|
18
|
+
post = (m) => self.postMessage(m);
|
|
19
|
+
onMessage = (fn) => (self.onmessage = (e) => fn(e.data));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let module = null;
|
|
23
|
+
let ctrl = null;
|
|
24
|
+
let data = null;
|
|
25
|
+
let inst = null;
|
|
26
|
+
|
|
27
|
+
function hostCall(kind, payload) {
|
|
28
|
+
Atomics.store(ctrl, 0, 0);
|
|
29
|
+
post({ type: "host", kind, payload });
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (;;) {
|
|
32
|
+
Atomics.wait(ctrl, 0, 0);
|
|
33
|
+
const state = Atomics.load(ctrl, 0);
|
|
34
|
+
if (state === 3) {
|
|
35
|
+
Atomics.store(ctrl, 0, 0);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const len = Atomics.load(ctrl, 1);
|
|
39
|
+
parts.push(data.slice(0, len));
|
|
40
|
+
if (state === 2) {
|
|
41
|
+
Atomics.store(ctrl, 0, 0);
|
|
42
|
+
post({ type: "ack" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
Atomics.store(ctrl, 0, 0);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
49
|
+
const bytes = new Uint8Array(total);
|
|
50
|
+
let off = 0;
|
|
51
|
+
for (const p of parts) {
|
|
52
|
+
bytes.set(p, off);
|
|
53
|
+
off += p.length;
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(td.decode(bytes));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onMessage(async (m) => {
|
|
59
|
+
if (m.type === "init") {
|
|
60
|
+
module = m.module;
|
|
61
|
+
ctrl = new Int32Array(m.sab, 0, 4);
|
|
62
|
+
data = new Uint8Array(m.sab, DATA_OFFSET);
|
|
63
|
+
inst = await instantiate(module, hostCall);
|
|
64
|
+
post({ type: "ready" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (m.type === "call") {
|
|
68
|
+
try {
|
|
69
|
+
const response = inst.call(m.request);
|
|
70
|
+
post({ type: "result", response });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
post({ type: "error", message: `synsema: the module trapped (${e && e.message ? e.message : e}); the instance was recreated` });
|
|
73
|
+
inst = await instantiate(module, hostCall);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|