/** * Pure sandbox message handler — no Worker APIs, engine injected. The worker * entry wires it to self.onmessage; the Node test harness calls it directly, * so the exact code path that runs in production is what gets tested. * * State model: one compiled script + accumulated bars per worker instance * (SandboxRunner spawns one worker per user script on the chart). */ import type { Bar, CompiledScript, IScriptEngine, ScriptCompileError } from "../engine/types"; import type { FromWorker, ToWorker } from "./protocol"; export type SandboxState = { compiled: CompiledScript | null; bars: Bar[]; }; export function createState(): SandboxState { return { compiled: null, bars: [] }; } /** Merge incoming tail bars into state (append new, replace same-time updates). */ export function mergeBars(existing: Bar[], incoming: Bar[]): Bar[] { if (existing.length === 0) return [...incoming]; const out = [...existing]; for (const b of incoming) { const last = out[out.length - 1]; if (b.time > last.time) out.push(b); else if (b.time === last.time) out[out.length - 1] = b; else { // Out-of-order update inside history — locate and replace. const idx = out.findIndex((x) => x.time === b.time); if (idx >= 0) out[idx] = b; } } return out; } export async function handleMessage( engine: IScriptEngine, state: SandboxState, msg: ToWorker, ): Promise { switch (msg.type) { case "compile": { try { state.compiled = await engine.compile(msg.source); state.bars = []; return { type: "compiled", reqId: msg.reqId, meta: state.compiled.meta, capabilities: { incremental: engine.capabilities.incremental }, }; } catch (e) { const err = e as ScriptCompileError; return { type: "compileError", reqId: msg.reqId, line: err?.line, message: err?.message ?? "خطای ناشناخته در کامپایل", }; } } case "run": { if (!state.compiled) return { type: "runError", reqId: msg.reqId, message: "اسکریپت هنوز کامپایل نشده است" }; state.bars = [...msg.bars]; return execute(engine, state, msg.reqId, msg.inputs); } case "runIncremental": { if (!state.compiled) return { type: "runError", reqId: msg.reqId, message: "اسکریپت هنوز کامپایل نشده است" }; state.bars = mergeBars(state.bars, msg.bars); if (engine.capabilities.incremental && engine.runIncremental) { try { const r = await engine.runIncremental(state.compiled, msg.bars, msg.inputs); return { type: "result", reqId: msg.reqId, ...r }; } catch { /* fall through to full recompute */ } } // Fallback: full recompute over accumulated bars (throttling lives on the // main thread — the worker just does what it is told). return execute(engine, state, msg.reqId, msg.inputs); } } } async function execute( engine: IScriptEngine, state: SandboxState, reqId: number, inputs: Record, ): Promise { try { const r = await engine.run(state.compiled!, state.bars, inputs); return { type: "result", reqId, ...r }; } catch (e) { return { type: "runError", reqId, message: (e as Error)?.message ?? "خطای اجرا" }; } }