Files
jcoffey-dev 136fc2540b
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m45s
Sieve playground: the server's interpreter in the browser
tools/sieve-playground builds the vendored sieve-rs 0.7.3 (vnd.inbuxa.*
names included) to WebAssembly, with a static page around it, for inbuxa
Admin's Debug button on a Sieve script: until now that button sent the
script to an outside site whose interpreter knows only upstream's
extension names, so any script that requires vnd.inbuxa.* failed to
compile there.

Ported from the playground in the upstream sieve-rs repository's web/
directory (commit 549a2d57f7), which targets sieve-rs 1.x:

- run.rs and handler.rs drive 0.7's event loop the way the server's
  delivery does, carrying on after a runtime error; llm_prompt compiles
  and answers false, as on a server without a model.
- The expression functions are the server's own modules, included by
  path; tests fail if the list, ApplyString or the few functions copied
  from misc.rs/unicode.rs stop matching crates/common.
- Built for wasm32-wasip1 rather than with wasm-bindgen: sieve-rs and
  mail-builder read the clock, which panics on wasm32-unknown-unknown.
  The page's worker answers the six WASI calls, giving the module the
  playground's current-time setting. shim/gethostname stands in for a
  crate with no WASI build. Nothing in vendor/ or crates/ changes.
- The page carries no upstream names or links; storage keys and the
  samples are renamed to match.

build.sh --admin <dir> stages the result into an inbuxa-admin checkout.
2026-10-09 01:13:53 -07:00

153 lines
4.7 KiB
JavaScript

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
// inbuxa: the engine is the server's sieve-rs built for wasm32-wasip1 (see
// build.sh), so this worker gives it the few WASI calls it makes. The clock
// is the point: currentdate, and the Date headers of the replies a script
// writes, read the playground's current time from it.
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let memory;
let engine;
let panicMessage = null;
// Seconds since the epoch the clock reports, or null for the real clock.
let clockSeconds = null;
const view = () => new DataView(memory.buffer);
const bytes = (ptr, len) => new Uint8Array(memory.buffer, ptr, len);
const SUCCESS = 0;
const CLOCK_MONOTONIC = 1;
const wasi = {
clock_time_get(clockId, _precision, timePtr) {
const ms =
clockId === CLOCK_MONOTONIC ? performance.now() : clockSeconds !== null ? clockSeconds * 1000 : Date.now();
view().setBigUint64(timePtr, BigInt(Math.round(ms)) * 1000000n, true);
return SUCCESS;
},
random_get(ptr, len) {
for (let offset = 0; offset < len; offset += 65536) {
crypto.getRandomValues(bytes(ptr + offset, Math.min(65536, len - offset)));
}
return SUCCESS;
},
environ_sizes_get(countPtr, sizePtr) {
view().setUint32(countPtr, 0, true);
view().setUint32(sizePtr, 0, true);
return SUCCESS;
},
environ_get() {
return SUCCESS;
},
fd_write(_fd, iovs, iovsLen, writtenPtr) {
const v = view();
let written = 0;
let text = "";
for (let i = 0; i < iovsLen; i++) {
const ptr = v.getUint32(iovs + i * 8, true);
const len = v.getUint32(iovs + i * 8 + 4, true);
text += decoder.decode(bytes(ptr, len));
written += len;
}
v.setUint32(writtenPtr, written, true);
if (text.trim()) console.warn(text.trim());
return SUCCESS;
},
proc_exit(code) {
throw new WebAssembly.RuntimeError(`The engine exited with code ${code}`);
},
};
const playground = {
panicked(ptr, len) {
panicMessage = decoder.decode(bytes(ptr, len).slice());
},
};
async function load() {
const url = new URL("./sieve_playground.wasm", import.meta.url);
const imports = { wasi_snapshot_preview1: wasi, playground };
let instance;
try {
({ instance } = await WebAssembly.instantiateStreaming(fetch(url), imports));
} catch {
// Served without application/wasm: compile from the bytes instead.
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status} loading the engine`);
({ instance } = await WebAssembly.instantiate(await response.arrayBuffer(), imports));
}
engine = instance.exports;
memory = engine.memory;
engine.playground_init();
}
function put(data) {
const ptr = engine.playground_alloc(data.length);
bytes(ptr, data.length).set(data);
return [ptr, data.length];
}
function call(op, payload) {
const [opPtr, opLen] = put(encoder.encode(op));
const [payloadPtr, payloadLen] = put(encoder.encode(payload === undefined ? "null" : JSON.stringify(payload)));
const packed = BigInt.asUintN(64, engine.playground_call(opPtr, opLen, payloadPtr, payloadLen));
const ptr = Number(packed >> 32n);
const len = Number(packed & 0xffffffffn);
const text = decoder.decode(bytes(ptr, len).slice());
engine.playground_free(ptr, len);
const reply = JSON.parse(text);
if ("error" in reply) throw new Error(reply.error);
return reply.ok;
}
const ops = {
version: () => call("version"),
capabilities: () => call("capabilities"),
defaults: () => call("defaults"),
compile: (request) => call("compile", request),
run: (request) => {
const currentTime = request?.settings?.currentTime;
clockSeconds = Number.isFinite(currentTime) ? currentTime : null;
try {
return call("run", request);
} finally {
clockSeconds = null;
}
},
};
const ready = load().then(
() => {
self.postMessage({ type: "ready" });
return true;
},
(err) => {
self.postMessage({ type: "init-error", error: String(err && err.message ? err.message : err) });
return false;
},
);
self.onmessage = async (event) => {
const { id, op, payload } = event.data;
if (!(await ready)) return;
try {
const fn = ops[op];
if (!fn) throw new Error(`Unknown operation ${op}`);
const result = fn(payload);
self.postMessage({ id, result });
} catch (err) {
const fatal = err instanceof WebAssembly.RuntimeError;
const message = fatal && panicMessage ? panicMessage : String(err && err.message ? err.message : err);
self.postMessage({ id, error: message, fatal });
}
};