// Runs a lab's MATLAB layer in a numbl session. // // One session per (driver, script) pair, booted on first use and then reused: // changing a control only rewrites params.json and re-runs main.m, which is // fast enough to drive sliders. Editing the script needs a fresh session, // because the script is compiled into main.m at boot. import { createNumblSession, type BootFile, type NumblSession } from 'numbl/browser' import type { Params, RunResult } from './types.ts' import { LIB_FILES } from './lib.ts' function bootFiles(driver: string, script: string): BootFile[] { return [ ...LIB_FILES, { path: 'main.m', content: `${driver}\n${script}` }, // params.json is overwritten before every run; it only needs to exist so // that the first fileread has something to find { path: 'params.json', content: '{}' }, ] } export class Engine { private session: NumblSession | null = null private bootedFor: string | null = null private queue: Promise = Promise.resolve() private chunks: string[] = [] /** Resolves when the run finishes; runs are serialised in call order. */ run(driver: string, script: string, params: Params): Promise> { const task = this.queue.then( () => this.exec(driver, script, params), () => this.exec(driver, script, params), ) this.queue = task.catch(() => undefined) return task } private async exec(driver: string, script: string, params: Params): Promise> { const t0 = performance.now() const fail = (error: string): RunResult => ({ ok: false, error, output: this.chunks.join(''), ms: performance.now() - t0, }) const key = `${driver}\n---\n${script}` try { if (!this.session || this.bootedFor !== key) { this.session?.dispose() this.session = null this.bootedFor = null this.chunks = [] const session = await createNumblSession({ files: bootFiles(driver, script), mip: false, persistSystem: false, optimization: '1', onOutput: (text) => { this.chunks.push(text) }, }) this.session = session this.bootedFor = key } this.chunks = [] this.session.writeFile('params.json', JSON.stringify(params)) // `run` rather than a bare `main;`: the file is a script with local // functions, and this says so at the call site. const res = await this.session.execute("run('main.m');") if (!res.ok) return fail(res.error ?? 'the script failed') const bytes = await this.session.readFile('out.json') return { ok: true, data: JSON.parse(new TextDecoder().decode(bytes)) as T, output: this.chunks.join(''), ms: performance.now() - t0, } } catch (err) { // a boot failure leaves nothing usable behind this.session?.dispose() this.session = null this.bootedFor = null return fail(err instanceof Error ? err.message : String(err)) } } dispose() { this.session?.dispose() this.session = null this.bootedFor = null } }