106 lines
4.3 KiB
JavaScript
106 lines
4.3 KiB
JavaScript
export const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
|
export const PROTOCOL_SCHEMA = 1;
|
|
|
|
function isObject(value) {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
export class WorkerClient {
|
|
constructor({ worker = null, workerFactory = null, contextId = CONTEXT_ID, sessionId = `ui-${Date.now().toString(36)}`, timeoutMs = 10000 } = {}) {
|
|
this.contextId = contextId;
|
|
this.sessionId = sessionId;
|
|
this.timeoutMs = timeoutMs;
|
|
this.worker = worker ?? (workerFactory ? workerFactory() : new Worker("../worker/sim-worker.mjs", { type: "module" }));
|
|
this.seq = 0;
|
|
this.pending = new Map();
|
|
this.completed = new Map();
|
|
this.listeners = new Set();
|
|
this.closed = false;
|
|
this.worker.onmessage = (event) => this.#receive(event.data);
|
|
this.worker.onerror = (event) => this.#failAll(new Error(event?.message ?? "worker_exception"));
|
|
}
|
|
|
|
onMessage(listener) {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
}
|
|
|
|
#emit(message) {
|
|
for (const listener of this.listeners) listener(message);
|
|
}
|
|
|
|
#receive(message) {
|
|
this.#emit(message);
|
|
if (!isObject(message)) return;
|
|
const operationId = message.operation_id ?? message.payload?.operation_id;
|
|
const pending = operationId ? this.pending.get(operationId) : this.pending.get(`seq:${message.seq}`);
|
|
if (!pending) return;
|
|
clearTimeout(pending.timer);
|
|
this.pending.delete(operationId ?? `seq:${message.seq}`);
|
|
this.completed.set(operationId, message);
|
|
if (message.kind === "ERROR" || message.payload?.status === "error" || message.payload?.status === "blocked") {
|
|
const error = new Error(message.payload?.message_key ?? message.payload?.code ?? "worker_error");
|
|
error.response = message;
|
|
pending.reject(error);
|
|
} else pending.resolve(message);
|
|
}
|
|
|
|
#failAll(error) {
|
|
for (const pending of this.pending.values()) {
|
|
clearTimeout(pending.timer);
|
|
pending.reject(error);
|
|
}
|
|
this.pending.clear();
|
|
}
|
|
|
|
send(kind, payload = {}, { operationId = null, timeoutMs = this.timeoutMs } = {}) {
|
|
if (this.closed) return Promise.reject(new Error("worker_client_closed"));
|
|
if (operationId && this.completed.has(operationId)) return Promise.resolve(this.completed.get(operationId));
|
|
if (operationId && this.pending.has(operationId)) return this.pending.get(operationId).promise;
|
|
const seq = ++this.seq;
|
|
const message = {
|
|
schema_version: PROTOCOL_SCHEMA,
|
|
context_id: this.contextId,
|
|
session_id: this.sessionId,
|
|
seq,
|
|
kind,
|
|
...(operationId ? { operation_id: operationId } : {}),
|
|
payload,
|
|
};
|
|
let resolvePromise;
|
|
let rejectPromise;
|
|
const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; });
|
|
const timer = setTimeout(() => {
|
|
this.pending.delete(operationId);
|
|
const error = new Error("worker_timeout");
|
|
error.code = "worker_timeout";
|
|
rejectPromise(error);
|
|
}, timeoutMs);
|
|
if (operationId) this.pending.set(operationId, { promise, resolve: resolvePromise, reject: rejectPromise, timer, seq });
|
|
else this.pending.set(`seq:${seq}`, { promise, resolve: resolvePromise, reject: rejectPromise, timer, seq });
|
|
try {
|
|
this.worker.postMessage(message);
|
|
} catch (error) {
|
|
clearTimeout(timer);
|
|
this.pending.delete(operationId ?? `seq:${seq}`);
|
|
rejectPromise(error);
|
|
}
|
|
return promise;
|
|
}
|
|
|
|
hello() { return this.send("HELLO", {}); }
|
|
attach(operationId = "session-attach") { return this.send("SESSION_ATTACH", {}, { operationId }); }
|
|
loadWorkspace(rootPath = "/workspace", configuration = undefined, hal = undefined, operationId = "workspace-load") {
|
|
return this.send("LOAD_WORKSPACE", { root_path: rootPath, ...(configuration ? { configuration } : {}), ...(hal ? { hal } : {}) }, { operationId });
|
|
}
|
|
step(cycles = 1, operationId = `step-${this.seq + 1}`) { return this.send("SIM_STEP", { cycles }, { operationId }); }
|
|
snapshot(operationId = `snapshot-${this.seq + 1}`) { return this.send("SNAPSHOT_REQUEST", {}, { operationId }); }
|
|
checkpoint(operationId = `checkpoint-${this.seq + 1}`) { return this.send("CHECKPOINT", {}, { operationId }); }
|
|
close() {
|
|
if (this.closed) return;
|
|
this.closed = true;
|
|
this.#failAll(new Error("worker_client_closed"));
|
|
this.worker.terminate?.();
|
|
}
|
|
}
|