Implement AXIS parity UI and execution baseline
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
export const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
||||
export const PROTOCOL_SCHEMA = 1;
|
||||
let CLIENT_INSTANCE = 0;
|
||||
|
||||
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 } = {}) {
|
||||
constructor({ worker = null, workerFactory = null, contextId = CONTEXT_ID, sessionId = null, timeoutMs = 10000, maxPending = 64 } = {}) {
|
||||
this.contextId = contextId;
|
||||
this.sessionId = sessionId;
|
||||
this.sessionId = sessionId ?? `ui-${++CLIENT_INSTANCE}`;
|
||||
this.timeoutMs = timeoutMs;
|
||||
this.maxPending = maxPending;
|
||||
this.worker = worker ?? (workerFactory ? workerFactory() : new Worker("../worker/sim-worker.mjs", { type: "module" }));
|
||||
this.seq = 0;
|
||||
this.pending = new Map();
|
||||
@@ -37,7 +39,7 @@ export class WorkerClient {
|
||||
if (!pending) return;
|
||||
clearTimeout(pending.timer);
|
||||
this.pending.delete(operationId ?? `seq:${message.seq}`);
|
||||
this.completed.set(operationId, message);
|
||||
if (operationId) 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;
|
||||
@@ -55,6 +57,7 @@ export class WorkerClient {
|
||||
|
||||
send(kind, payload = {}, { operationId = null, timeoutMs = this.timeoutMs } = {}) {
|
||||
if (this.closed) return Promise.reject(new Error("worker_client_closed"));
|
||||
if (!Number.isSafeInteger(this.maxPending) || this.maxPending < 1) return Promise.reject(new Error("worker_client_backpressure"));
|
||||
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;
|
||||
@@ -70,19 +73,24 @@ export class WorkerClient {
|
||||
let resolvePromise;
|
||||
let rejectPromise;
|
||||
const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; });
|
||||
const pendingKey = operationId ?? `seq:${seq}`;
|
||||
const timer = setTimeout(() => {
|
||||
this.pending.delete(operationId);
|
||||
this.pending.delete(pendingKey);
|
||||
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 });
|
||||
if (this.pending.size >= this.maxPending) {
|
||||
clearTimeout(timer);
|
||||
rejectPromise(Object.assign(new Error("worker_backpressure"), { code: "worker_backpressure" }));
|
||||
return promise;
|
||||
}
|
||||
this.pending.set(pendingKey, { promise, resolve: resolvePromise, reject: rejectPromise, timer, seq, operationId, pendingKey });
|
||||
try {
|
||||
this.worker.postMessage(message);
|
||||
} catch (error) {
|
||||
clearTimeout(timer);
|
||||
this.pending.delete(operationId ?? `seq:${seq}`);
|
||||
this.pending.delete(pendingKey);
|
||||
rejectPromise(error);
|
||||
}
|
||||
return promise;
|
||||
@@ -93,7 +101,41 @@ export class WorkerClient {
|
||||
loadWorkspace(rootPath = "/workspace", configuration = undefined, hal = undefined, operationId = "workspace-load") {
|
||||
return this.send("LOAD_WORKSPACE", { root_path: rootPath, ...(configuration ? { configuration } : {}), ...(hal ? { hal } : {}) }, { operationId });
|
||||
}
|
||||
loadProgram(source, path = "program.ngc", operationId = `program-load-${this.seq + 1}`) {
|
||||
return this.send("PROGRAM_LOAD", { source, path }, { operationId });
|
||||
}
|
||||
runProgram(operationId = `program-run-${this.seq + 1}`) { return this.send("PROGRAM_RUN", {}, { operationId }); }
|
||||
pauseProgram(operationId = `program-pause-${this.seq + 1}`) { return this.send("PROGRAM_PAUSE", {}, { operationId }); }
|
||||
resumeProgram(operationId = `program-resume-${this.seq + 1}`) { return this.send("PROGRAM_RESUME", {}, { operationId }); }
|
||||
abortProgram(operationId = `program-abort-${this.seq + 1}`) { return this.send("PROGRAM_ABORT", {}, { operationId }); }
|
||||
restartProgram(operationId = `program-restart-${this.seq + 1}`) { return this.send("PROGRAM_RESTART", {}, { operationId }); }
|
||||
stepProgram(operationId = `program-step-${this.seq + 1}`) { return this.send("PROGRAM_STEP", {}, { operationId }); }
|
||||
seekProgram(line, operationId = `program-seek-${this.seq + 1}`) { return this.send("PROGRAM_SEEK", { line }, { operationId }); }
|
||||
mdi(block, operationId = `mdi-${this.seq + 1}`) { return this.send("MDI_EXECUTE", { block }, { operationId }); }
|
||||
step(cycles = 1, operationId = `step-${this.seq + 1}`) { return this.send("SIM_STEP", { cycles }, { operationId }); }
|
||||
async stepMany(cycles = 1, { chunk = 1, operationPrefix = "batch-step" } = {}) {
|
||||
if (!Number.isSafeInteger(cycles) || cycles < 0 || cycles > 1000000 || !Number.isSafeInteger(chunk) || chunk < 1 || chunk > 1000000) return Promise.reject(Object.assign(new Error("worker_step_batch_invalid"), { code: "worker_step_batch_invalid" }));
|
||||
const responses = [];
|
||||
let remaining = cycles;
|
||||
let index = 0;
|
||||
while (remaining > 0) {
|
||||
const amount = Math.min(chunk, remaining);
|
||||
responses.push(await this.step(amount, `${operationPrefix}-${index}`));
|
||||
remaining -= amount; index += 1;
|
||||
}
|
||||
return { status: "verified", cycles, chunks: responses.length, last: responses.at(-1) ?? null };
|
||||
}
|
||||
cancel(operationId, reason = "cancel_requested") {
|
||||
const pending = this.pending.get(operationId);
|
||||
if (!pending) return Promise.resolve({ status: "verified", cancelled: false, operation_id: operationId });
|
||||
clearTimeout(pending.timer);
|
||||
this.pending.delete(operationId);
|
||||
pending.reject(Object.assign(new Error("worker_cancelled"), { code: "worker_cancelled", reason }));
|
||||
const seq = ++this.seq;
|
||||
const message = { schema_version: PROTOCOL_SCHEMA, context_id: this.contextId, session_id: this.sessionId, seq, kind: "CANCEL", operation_id: `cancel:${operationId}`, payload: { operation_id: operationId, reason } };
|
||||
try { this.worker.postMessage(message); } catch { /* cancellation is local even if transport is closed */ }
|
||||
return Promise.resolve({ status: "verified", cancelled: true, operation_id: 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() {
|
||||
|
||||
Reference in New Issue
Block a user