219 lines
6.7 KiB
TypeScript
219 lines
6.7 KiB
TypeScript
/* Default `NvsimClient` implementation. Talks to the Web Worker that
|
|
* hosts the nvsim WASM module. ADR-092 §5.4 + §6.3. */
|
|
|
|
import {
|
|
type NvsimClient,
|
|
type SceneJson,
|
|
type PipelineConfigJson,
|
|
type RunOpts,
|
|
type MagFrameBatch,
|
|
type NvsimEvent,
|
|
type TransientRunResult,
|
|
parseFrameBatch,
|
|
} from './NvsimClient';
|
|
|
|
interface PendingRequest<T = unknown> {
|
|
resolve: (v: T) => void;
|
|
reject: (err: Error) => void;
|
|
}
|
|
|
|
export interface WasmBootInfo {
|
|
buildVersion: string;
|
|
frameMagic: number;
|
|
frameBytes: number;
|
|
expectedWitnessHex: string;
|
|
}
|
|
|
|
export class WasmClient implements NvsimClient {
|
|
private worker: Worker;
|
|
private nextId = 1;
|
|
private pending = new Map<number, PendingRequest<unknown>>();
|
|
private frameSubs = new Set<(b: MagFrameBatch) => void>();
|
|
private eventSubs = new Set<(e: NvsimEvent) => void>();
|
|
private bootInfo: WasmBootInfo | null = null;
|
|
|
|
constructor() {
|
|
this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
|
|
this.worker.addEventListener('message', (ev) => this.onMessage(ev));
|
|
this.worker.addEventListener('error', (e) =>
|
|
this.eventSubs.forEach((s) => s({ type: 'log', level: 'err', msg: String(e.message) })),
|
|
);
|
|
}
|
|
|
|
private onMessage(ev: MessageEvent): void {
|
|
const m = ev.data as { type: string; id?: number; [k: string]: unknown };
|
|
if (m.type === 'frames') {
|
|
const buf = m.batch as ArrayBuffer;
|
|
const bytes = new Uint8Array(buf);
|
|
const frames = parseFrameBatch(bytes);
|
|
const batch: MagFrameBatch = { frames, bytes };
|
|
this.frameSubs.forEach((s) => s(batch));
|
|
const fps = m.fps as number;
|
|
if (fps > 0) {
|
|
this.eventSubs.forEach((s) => s({ type: 'fps', value: fps }));
|
|
}
|
|
return;
|
|
}
|
|
if (m.type === 'state') {
|
|
this.eventSubs.forEach((s) =>
|
|
s({
|
|
type: 'state',
|
|
running: Boolean(m.running),
|
|
t: 0,
|
|
framesEmitted: Number(m.framesEmitted ?? 0),
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
if (m.type === 'ready') {
|
|
return;
|
|
}
|
|
if (m.type === 'err' && m.id == null) {
|
|
this.eventSubs.forEach((s) =>
|
|
s({ type: 'log', level: 'err', msg: String(m.msg) }),
|
|
);
|
|
return;
|
|
}
|
|
if (typeof m.id === 'number' && this.pending.has(m.id)) {
|
|
const p = this.pending.get(m.id)!;
|
|
this.pending.delete(m.id);
|
|
if (m.type === 'err') p.reject(new Error(String(m.msg)));
|
|
else p.resolve(m);
|
|
}
|
|
}
|
|
|
|
private rpc<T = unknown>(msg: Record<string, unknown>, transfer: Transferable[] = []): Promise<T> {
|
|
const id = this.nextId++;
|
|
return new Promise<T>((resolve, reject) => {
|
|
this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
|
|
this.worker.postMessage({ ...msg, id }, transfer);
|
|
});
|
|
}
|
|
|
|
async boot(): Promise<WasmBootInfo> {
|
|
if (this.bootInfo) return this.bootInfo;
|
|
// Pass Vite's resolved BASE_URL so the worker can locate /nvsim-pkg/
|
|
// under the same prefix the dashboard is served from (e.g. /RuView/nvsim/
|
|
// on GitHub Pages, "/" in dev).
|
|
const base = import.meta.env.BASE_URL ?? '/';
|
|
const r = await this.rpc<{ buildVersion: string; frameMagic: number; frameBytes: number; expectedWitnessHex: string }>(
|
|
{ type: 'boot', base },
|
|
);
|
|
this.bootInfo = {
|
|
buildVersion: r.buildVersion,
|
|
frameMagic: r.frameMagic,
|
|
frameBytes: r.frameBytes,
|
|
expectedWitnessHex: r.expectedWitnessHex,
|
|
};
|
|
return this.bootInfo;
|
|
}
|
|
|
|
async loadScene(scene: SceneJson): Promise<void> {
|
|
await this.rpc({ type: 'setScene', json: JSON.stringify(scene) });
|
|
}
|
|
|
|
async setConfig(cfg: PipelineConfigJson): Promise<void> {
|
|
await this.rpc({ type: 'setConfig', json: JSON.stringify(cfg) });
|
|
}
|
|
|
|
async setSeed(seed: bigint): Promise<void> {
|
|
await this.rpc({ type: 'setSeed', seed: Number(seed & 0xFFFFFFFFn) });
|
|
}
|
|
|
|
async reset(): Promise<void> {
|
|
await this.rpc({ type: 'reset' });
|
|
}
|
|
|
|
async run(_opts?: RunOpts): Promise<void> {
|
|
await this.rpc({ type: 'run' });
|
|
}
|
|
|
|
async pause(): Promise<void> {
|
|
await this.rpc({ type: 'pause' });
|
|
}
|
|
|
|
async step(_direction: 'fwd' | 'back', _dtMs: number): Promise<void> {
|
|
await this.rpc({ type: 'step' });
|
|
}
|
|
|
|
onFrames(cb: (batch: MagFrameBatch) => void): void { this.frameSubs.add(cb); }
|
|
onEvent(cb: (ev: NvsimEvent) => void): void { this.eventSubs.add(cb); }
|
|
|
|
async generateWitness(samples: number): Promise<Uint8Array> {
|
|
const r = await this.rpc<{ witness: ArrayBuffer; hex: string }>({ type: 'witnessGenerate', samples });
|
|
return new Uint8Array(r.witness);
|
|
}
|
|
|
|
async verifyWitness(expected: Uint8Array): Promise<{ ok: true } | { ok: false; actual: Uint8Array }> {
|
|
const buf = expected.slice().buffer;
|
|
const r = await this.rpc<{ ok: boolean; actual: ArrayBuffer; actualHex: string }>(
|
|
{ type: 'witnessVerify', samples: 256, expected: buf },
|
|
[buf],
|
|
);
|
|
if (r.ok) return { ok: true };
|
|
return { ok: false, actual: new Uint8Array(r.actual) };
|
|
}
|
|
|
|
async runTransient(
|
|
scene: SceneJson,
|
|
config: PipelineConfigJson,
|
|
seed: bigint,
|
|
samples: number,
|
|
): Promise<TransientRunResult> {
|
|
const r = await this.rpc<{
|
|
bRecoveredT: number[];
|
|
bMagT: number;
|
|
noiseFloorPtSqrtHz: number;
|
|
sigmaPt: number[];
|
|
nFrames: number;
|
|
witnessHex: string;
|
|
}>({
|
|
type: 'runTransient',
|
|
scene: JSON.stringify(scene),
|
|
config: JSON.stringify(config),
|
|
seed: Number(seed & 0xFFFFFFFFn),
|
|
samples,
|
|
});
|
|
return {
|
|
bRecoveredT: [r.bRecoveredT[0], r.bRecoveredT[1], r.bRecoveredT[2]],
|
|
bMagT: r.bMagT,
|
|
noiseFloorPtSqrtHz: r.noiseFloorPtSqrtHz,
|
|
sigmaPt: [r.sigmaPt[0], r.sigmaPt[1], r.sigmaPt[2]],
|
|
nFrames: r.nFrames,
|
|
witnessHex: r.witnessHex,
|
|
};
|
|
}
|
|
|
|
async exportProofBundle(): Promise<Blob> {
|
|
// Bundle = REFERENCE_SCENE_JSON + computed witness hex + version. Wraps
|
|
// the same artifacts `Proof::generate` produces natively. ADR-092 §6.1.
|
|
const w = await this.generateWitness(256);
|
|
const hex = Array.from(w).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
const info = this.bootInfo ?? (await this.boot());
|
|
const manifest = JSON.stringify(
|
|
{
|
|
kind: 'nvsim-proof-bundle',
|
|
version: info.buildVersion,
|
|
seed: '0x0000002A',
|
|
nSamples: 256,
|
|
witness: hex,
|
|
expected: info.expectedWitnessHex,
|
|
ok: hex === info.expectedWitnessHex,
|
|
ts: new Date().toISOString(),
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
return new Blob([manifest], { type: 'application/json' });
|
|
}
|
|
|
|
async buildId(): Promise<string> {
|
|
const r = await this.rpc<{ buildId: string }>({ type: 'buildId' });
|
|
return r.buildId;
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
this.worker.terminate();
|
|
}
|
|
}
|