Add pause-resume-stop functionality and UI
This commit is contained in:
@ -14,6 +14,8 @@ class ExecutionController<RS> {
|
||||
private _engine: LanguageEngine<RS>;
|
||||
private _breakpoints: number[] = [];
|
||||
private _result: StepExecutionResult<RS> | null;
|
||||
private _resolvePause: (() => void) | null = null;
|
||||
private _isPaused: boolean = false;
|
||||
|
||||
/**
|
||||
* Create a new ExecutionController.
|
||||
@ -51,16 +53,75 @@ class ExecutionController<RS> {
|
||||
this._breakpoints = points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the ongoing execution.
|
||||
* - If already paused, returns immediately
|
||||
* - Queues up with any existing pause calls
|
||||
* @returns Promise that resolves when execution is paused
|
||||
*/
|
||||
async pauseExecution(): Promise<void> {
|
||||
// If already paused, return immediately
|
||||
if (this._isPaused) return;
|
||||
|
||||
// If there's another "pause" call waiting, chain up with older resolver.
|
||||
// This kinda creates a linked list of resolvers, with latest resolver at head.
|
||||
if (this._resolvePause) {
|
||||
console.log("Chaining pause calls");
|
||||
return new Promise((resolve) => {
|
||||
// Keep a reference to the existing resolver
|
||||
const oldResolve = this._resolvePause;
|
||||
// Replace resolver with new chained resolver
|
||||
this._resolvePause = () => {
|
||||
oldResolve && oldResolve();
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Else, create a callback to be called by the execution loop
|
||||
// when it finishes current execution step.
|
||||
return new Promise(
|
||||
(resolve) =>
|
||||
(this._resolvePause = () => {
|
||||
this._resolvePause = null;
|
||||
this._isPaused = true;
|
||||
resolve();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the loaded program until stopped.
|
||||
* @param param0.interval Interval between two execution steps
|
||||
* @param param0.onResult Callback called with result on each execution step
|
||||
* @returns Returns last (already used) execution result
|
||||
*/
|
||||
async executeAll({ interval, onResult }: ExecuteAllArgs<RS>) {
|
||||
while (true) {
|
||||
this._result = this._engine.executeStep();
|
||||
onResult && onResult(this._result);
|
||||
if (!this._result.nextStepLocation) break;
|
||||
await this.sleep(interval || 0);
|
||||
console.log("Result: ", this._result);
|
||||
if (!this._result.nextStepLocation) {
|
||||
// End of program
|
||||
onResult && onResult(this._result);
|
||||
this._resolvePause && this._resolvePause(); // In case pause happens on same cycle
|
||||
break;
|
||||
} else if (this._resolvePause) {
|
||||
// Execution has been paused/stopped
|
||||
this._result.signal = "paused";
|
||||
onResult && onResult(this._result);
|
||||
this._resolvePause();
|
||||
break;
|
||||
} else {
|
||||
onResult && onResult(this._result);
|
||||
// Sleep for specified interval
|
||||
await this.sleep(interval || 0);
|
||||
}
|
||||
}
|
||||
|
||||
return this._result;
|
||||
}
|
||||
|
||||
/** Asynchronously sleep for a period of time */
|
||||
private async sleep(millis: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
|
@ -35,6 +35,9 @@ export type StepExecutionResult<RS> = {
|
||||
* Passing `null` indicates reaching the end of program.
|
||||
*/
|
||||
nextStepLocation: DocumentRange | null;
|
||||
|
||||
/** Signal if execution has been paused/stopped */
|
||||
signal?: "paused";
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -21,10 +21,19 @@ export type WorkerRequestData =
|
||||
| {
|
||||
type: "Execute";
|
||||
params: { interval?: number };
|
||||
}
|
||||
| {
|
||||
type: "Pause";
|
||||
params?: null;
|
||||
};
|
||||
|
||||
/** Kinds of acknowledgement responses the worker can send */
|
||||
export type WorkerAckType = "init" | "reset" | "bp-update" | "prepare";
|
||||
export type WorkerAckType =
|
||||
| "init" // on initialization
|
||||
| "reset" // on state reset
|
||||
| "bp-update" // on updating breakpoints
|
||||
| "prepare" // on preparing for execution
|
||||
| "pause"; // on pausing execution
|
||||
|
||||
/** Types of responses the worker can send */
|
||||
export type WorkerResponseData<RS> =
|
||||
|
@ -71,11 +71,20 @@ const execute = (interval?: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
addEventListener("message", (ev: MessageEvent<WorkerRequestData>) => {
|
||||
/**
|
||||
* Trigger pause in program execution
|
||||
*/
|
||||
const pauseExecution = async () => {
|
||||
await _controller!.pauseExecution();
|
||||
postMessage(ackMessage("pause"));
|
||||
};
|
||||
|
||||
addEventListener("message", async (ev: MessageEvent<WorkerRequestData>) => {
|
||||
if (ev.data.type === "Init") return initController();
|
||||
if (ev.data.type === "Reset") return resetController();
|
||||
if (ev.data.type === "Prepare") return prepare(ev.data.params);
|
||||
if (ev.data.type === "Execute") return execute(ev.data.params.interval);
|
||||
if (ev.data.type === "Pause") return await pauseExecution();
|
||||
if (ev.data.type === "UpdateBreakpoints")
|
||||
return updateBreakpoints(ev.data.params.points);
|
||||
throw new Error("Invalid worker message type");
|
||||
|
Reference in New Issue
Block a user