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

148 lines
3.0 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 DB_NAME = "inbuxa-sieve-playground";
const DB_VERSION = 1;
const STORE = "workspaces";
const PREFS_KEY = "inbuxa-sieve.prefs";
const FALLBACK_KEY = "inbuxa-sieve.workspaces";
const WELCOME_KEY = "inbuxa-sieve.welcome-dismissed";
function openDb() {
return new Promise((resolve, reject) => {
if (!("indexedDB" in self)) {
reject(new Error("IndexedDB unavailable"));
return;
}
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
request.result.createObjectStore(STORE, { keyPath: "id" });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
function wrap(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
class IdbStore {
constructor(db) {
this.db = db;
}
tx(mode) {
return this.db.transaction(STORE, mode).objectStore(STORE);
}
list() {
return wrap(this.tx("readonly").getAll());
}
get(id) {
return wrap(this.tx("readonly").get(id));
}
put(workspace) {
return wrap(this.tx("readwrite").put(workspace));
}
delete(id) {
return wrap(this.tx("readwrite").delete(id));
}
}
class MemoryStore {
constructor() {
this.items = new Map();
try {
for (const ws of JSON.parse(localStorage.getItem(FALLBACK_KEY) || "[]")) {
this.items.set(ws.id, ws);
}
} catch (_) {
this.items.clear();
}
}
persist() {
try {
localStorage.setItem(FALLBACK_KEY, JSON.stringify([...this.items.values()]));
} catch (_) {
return;
}
}
async list() {
return [...this.items.values()];
}
async get(id) {
return this.items.get(id);
}
async put(workspace) {
this.items.set(workspace.id, workspace);
this.persist();
}
async delete(id) {
this.items.delete(id);
this.persist();
}
}
export async function openStore() {
try {
return new IdbStore(await openDb());
} catch (_) {
return new MemoryStore();
}
}
export function loadPrefs() {
try {
return JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
} catch (_) {
return {};
}
}
export function savePrefs(prefs) {
try {
localStorage.setItem(PREFS_KEY, JSON.stringify({ ...loadPrefs(), ...prefs }));
} catch (_) {
return;
}
}
export function isWelcomeDismissed() {
try {
return localStorage.getItem(WELCOME_KEY) !== null;
} catch (_) {
return false;
}
}
export function dismissWelcome() {
try {
localStorage.setItem(WELCOME_KEY, new Date().toISOString());
} catch (_) {
return;
}
}
export function newId() {
if (self.crypto && crypto.randomUUID) return crypto.randomUUID();
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}