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.
145 lines
3.8 KiB
JavaScript
145 lines
3.8 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.
|
|
*/
|
|
|
|
const RUN_TIMEOUT_MS = 8000;
|
|
const MAX_RESTARTS = 3;
|
|
const RESTART_WINDOW_MS = 60000;
|
|
const BASE_BACKOFF_MS = 500;
|
|
|
|
export class EngineUnavailable extends Error {}
|
|
|
|
export class Engine {
|
|
constructor() {
|
|
this.pending = new Map();
|
|
this.nextId = 1;
|
|
this.listeners = new Set();
|
|
this.restarts = [];
|
|
this.failed = null;
|
|
this.spawn();
|
|
}
|
|
|
|
onStatus(listener) {
|
|
this.listeners.add(listener);
|
|
}
|
|
|
|
emit(state, detail) {
|
|
for (const listener of this.listeners) listener(state, detail);
|
|
}
|
|
|
|
spawn() {
|
|
const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
|
|
this.worker = worker;
|
|
this.ready = new Promise((resolve, reject) => {
|
|
this.resolveReady = resolve;
|
|
this.rejectReady = reject;
|
|
});
|
|
this.ready.catch(() => {});
|
|
worker.onmessage = (event) => {
|
|
if (worker !== this.worker) return;
|
|
const data = event.data;
|
|
if (data.type === "ready") {
|
|
this.resolveReady();
|
|
} else if (data.type === "init-error") {
|
|
this.crash(`Engine failed to load: ${data.error}`, { loadFailure: true });
|
|
} else {
|
|
this.settle(data);
|
|
}
|
|
};
|
|
worker.onerror = (event) => {
|
|
event.preventDefault();
|
|
if (worker === this.worker) {
|
|
this.crash(`Engine failed to load: ${event.message || "the worker script could not be loaded"}`, { loadFailure: true });
|
|
}
|
|
};
|
|
}
|
|
|
|
crash(reason, { loadFailure = false } = {}) {
|
|
this.worker.terminate();
|
|
this.rejectReady(new EngineUnavailable(reason));
|
|
for (const { reject, timer } of this.pending.values()) {
|
|
clearTimeout(timer);
|
|
reject(new EngineUnavailable(reason));
|
|
}
|
|
this.pending.clear();
|
|
|
|
let delay = 0;
|
|
if (loadFailure) {
|
|
const now = Date.now();
|
|
this.restarts = this.restarts.filter((time) => now - time < RESTART_WINDOW_MS);
|
|
if (this.restarts.length >= MAX_RESTARTS) {
|
|
this.failed = reason;
|
|
this.emit("failed", reason);
|
|
return;
|
|
}
|
|
this.restarts.push(now);
|
|
delay = BASE_BACKOFF_MS * 2 ** (this.restarts.length - 1);
|
|
}
|
|
this.emit("restarting", reason);
|
|
const failedReady = this.ready;
|
|
this.ready = new Promise((resolve, reject) => {
|
|
setTimeout(() => {
|
|
this.spawn();
|
|
this.ready.then(resolve, reject);
|
|
}, delay);
|
|
});
|
|
this.ready.catch(() => {});
|
|
failedReady.catch(() => {});
|
|
}
|
|
|
|
settle({ id, result, error, fatal }) {
|
|
const entry = this.pending.get(id);
|
|
if (!entry) return;
|
|
this.pending.delete(id);
|
|
clearTimeout(entry.timer);
|
|
if (error) {
|
|
entry.reject(new Error(error));
|
|
if (fatal) this.crash(`The engine crashed: ${error}`);
|
|
} else {
|
|
entry.resolve(result);
|
|
}
|
|
}
|
|
|
|
async call(op, payload, timeout) {
|
|
if (this.failed) throw new EngineUnavailable(this.failed);
|
|
await this.ready;
|
|
const id = this.nextId++;
|
|
return new Promise((resolve, reject) => {
|
|
const timer = timeout
|
|
? setTimeout(() => {
|
|
this.pending.delete(id);
|
|
reject(new Error(`The script did not finish within ${timeout / 1000} seconds and was stopped.`));
|
|
this.crash("timeout");
|
|
}, timeout)
|
|
: null;
|
|
this.pending.set(id, { resolve, reject, timer });
|
|
this.worker.postMessage({ id, op, payload });
|
|
});
|
|
}
|
|
|
|
version() {
|
|
return this.call("version");
|
|
}
|
|
|
|
capabilities() {
|
|
return this.call("capabilities");
|
|
}
|
|
|
|
defaults() {
|
|
return this.call("defaults");
|
|
}
|
|
|
|
compile(request) {
|
|
return this.call("compile", request, RUN_TIMEOUT_MS);
|
|
}
|
|
|
|
run(request) {
|
|
return this.call("run", request, RUN_TIMEOUT_MS);
|
|
}
|
|
}
|