Spaces:
Running
Running
| // Run/Stop lifecycle around executeProgram: one run at a time, abort on | |
| // stop, and a guaranteed cleanup path (stopAll → returnHome → unhighlight) | |
| // no matter how the run ended. | |
| import type * as Blockly from "blockly"; | |
| import { AbortError, type RobotAdapter } from "../robot/adapter"; | |
| import type { EmotionSource, SoundSource } from "./commands"; | |
| import { executeProgram, findStart } from "./interpreter"; | |
| export type RunResult = "finished" | "stopped" | "error"; | |
| export interface RunnerHooks { | |
| onHighlight?(blockId: string | null): void; | |
| onRunStateChange?(running: boolean): void; | |
| } | |
| export interface RunnerOptions { | |
| emotions: EmotionSource; | |
| sounds: SoundSource; | |
| random?: () => number; | |
| maxSteps?: number; | |
| hooks?: RunnerHooks; | |
| } | |
| export class Runner { | |
| private controller: AbortController | null = null; | |
| private current: Promise<RunResult> | null = null; | |
| constructor( | |
| private workspace: Blockly.Workspace, | |
| private robot: RobotAdapter, | |
| private opts: RunnerOptions, | |
| ) {} | |
| get running(): boolean { | |
| return this.current !== null; | |
| } | |
| run(): Promise<RunResult> { | |
| if (this.current) return this.current; | |
| const controller = new AbortController(); | |
| this.controller = controller; | |
| this.opts.hooks?.onRunStateChange?.(true); | |
| this.current = (async (): Promise<RunResult> => { | |
| try { | |
| if (!findStart(this.workspace)) { | |
| // No start hat: a friendly shrug so the tap still "does something". | |
| await this.robot.goto({ headRoll: 8 }, 0.2); | |
| await this.robot.goto({ headRoll: 0 }, 0.25); | |
| return "finished"; | |
| } | |
| await executeProgram(this.workspace, { | |
| robot: this.robot, | |
| signal: controller.signal, | |
| random: this.opts.random, | |
| emotions: this.opts.emotions, | |
| sounds: this.opts.sounds, | |
| maxSteps: this.opts.maxSteps, | |
| hooks: { onHighlight: (id) => this.opts.hooks?.onHighlight?.(id) }, | |
| }); | |
| return "finished"; | |
| } catch (err) { | |
| if (err instanceof AbortError) return "stopped"; | |
| console.error("[reachy-blocks] run failed", err); | |
| return "error"; | |
| } finally { | |
| try { | |
| await this.robot.stopAll(); | |
| } catch { | |
| /* cleanup is best-effort */ | |
| } | |
| try { | |
| await this.robot.returnHome(); | |
| } catch { | |
| /* cleanup is best-effort */ | |
| } | |
| this.opts.hooks?.onHighlight?.(null); | |
| this.controller = null; | |
| this.current = null; | |
| this.opts.hooks?.onRunStateChange?.(false); | |
| } | |
| })(); | |
| return this.current; | |
| } | |
| /** Abort the active run and wait for its cleanup to complete. */ | |
| async stop(): Promise<void> { | |
| const current = this.current; | |
| if (!current) return; | |
| this.controller?.abort(); | |
| await current; | |
| } | |
| } | |