Files
mesh-tools/node-tools/test/fixtures/state-keeper.mjs
T
jochen 6cba894f01 The runtime serves a bundle its module's state (novox/hq ADR 0201)
mesh/state.get, put, delete, keys and watch on the stdio channel, from the
buckets the membership issues. A watch hands the current values without
deletions, then every change, and is answered once the current values are
delivered. Refused with the reason: state not issued, a reader's write, a
value with a credential-named field — the bus alone would answer a refused
write with a timeout.
2026-10-04 02:45:07 +02:00

59 lines
2.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
// A bundle that keeps and reads state through the runtime (novox/hq ADR 0201), written against the
// protocol with no SDK: on start it watches STATE_NAME and writes every change it is handed to
// STATE_LOG; its tools put, get, delete and list keys of whichever state they name.
import { appendFileSync } from "node:fs";
import { createInterface } from "node:readline";
const say = (m) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...m }) + "\n");
let next = 1;
const waiting = new Map();
const ask = (method, params) => new Promise((resolve, reject) => {
const id = `keeper-${next++}`;
waiting.set(id, { resolve, reject });
say({ id, method, params });
});
const log = (line) => appendFileSync(process.env.STATE_LOG, line + "\n");
const text = (v) => ({ content: [{ type: "text", text: JSON.stringify(v ?? null) }] });
const tool = (name) => ({ name, description: name, inputSchema: { type: "object", properties: {} } });
const TOOLS = ["put", "get", "del", "keys"].map(tool);
createInterface({ input: process.stdin }).on("line", async (line) => {
const m = JSON.parse(line);
if (m.method === undefined && waiting.has(m.id)) {
const w = waiting.get(m.id); waiting.delete(m.id);
m.error ? w.reject(new Error(m.error.message)) : w.resolve(m.result);
return;
}
const p = m.params ?? {};
switch (m.method) {
case "initialize":
say({ id: m.id, result: { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "keeper", version: "1" } } });
// Watched once the runtime has the handshake: the current values, then every change.
ask("mesh/state.watch", { state: process.env.STATE_NAME })
.then(() => log("current"), (e) => log("watch refused: " + e.message));
return;
case "tools/list":
say({ id: m.id, result: { tools: TOOLS } });
return;
case "mesh/state": {
const c = p.change;
log(`${c.current ? "was" : "now"} ${c.op} ${c.key}${c.value !== undefined ? " " + JSON.stringify(c.value) : ""}`);
say({ id: m.id, result: {} });
return;
}
case "tools/call": {
const a = p.arguments ?? {};
const verb = { put: "put", get: "get", del: "delete", keys: "keys" }[p.name];
try {
say({ id: m.id, result: text(await ask(`mesh/state.${verb}`, { state: a.state, key: a.key, value: a.value })) });
} catch (e) {
say({ id: m.id, result: { content: [{ type: "text", text: e.message }], isError: true } });
}
return;
}
default:
if (m.id !== undefined) say({ id: m.id, error: { code: -32601, message: "no " + m.method } });
}
});