Workflow - SergeiGolos/wod-wiki GitHub Wiki
This document details the workflow of the wod.wiki runtime, focusing on the execution flow and state transitions.
The runtime follows a stack-based execution model, processing blocks in sequence and responding to events.
flowchart TB
Start([Start]) --> Root[RootBlock]
Root --> Idle[IdleRuntimeBlock]
Idle --> |User starts| Execution
subgraph Execution
Block1[Block based on strategy] --> |"next()"| Block2[Next block]
Block2 --> |"next()"| Block3[...]
Block3 --> |Complete| Done
end
Done[DoneRuntimeBlock] --> |Save/Reset| Restart
Restart --> |Reset| Idle
classDef blockClass fill:#f9f,stroke:#333,stroke-width:2px
class Root,Idle,Done blockClass
The runtime moves through these primary states:
- Initialization: Setting up the runtime with compiled script
- Idle: Waiting for user to start the workout
- Executing: Processing blocks according to the workout script
- Done: Workout completed, displaying results
- Reset: Returning to Idle state for potential restart
// Create a new runtime with the compiled script
const runtime = new TimerRuntime(
code, // Original source code
new RuntimeStack(), // Empty execution stack
new RuntimeJit(script), // JIT compiler with the script
new Subject<IRuntimeEvent>(), // Input event stream
new Subject<OutputEvent>() // Output event stream
);
During initialization:
- The RuntimeJit is configured with the script
- Event streams are established
- RootBlock containing all statements is pushed onto the stack
- IdleRuntimeBlock is pushed on top of the RootBlock
The IdleRuntimeBlock:
- Displays the workout overview
- Sets up the "Start" button
- Waits for the start event
// Inside IdleRuntimeBlock.onNext
if (event instanceof StartEvent) {
return [new PopBlockAction()]; // Remove idle block, exposing root
}
When the user clicks Start:
- The IdleBlock is popped from the stack
- The RootBlock becomes active
- The RootBlock's
enter()
method is called - The RootBlock processes its first child statement
// Inside RootBlock.onEnter
protected onEnter(runtime: ITimerRuntime): IRuntimeAction[] {
const actions: IRuntimeAction[] = [];
// Start tracking results for this block
this.onStart(runtime);
// If we have statements, push the first one
if (this.children.length > 0) {
actions.push(new PushStatementAction(this.children[0]));
}
return actions;
}
As the workout progresses, each block:
- Processes events via
handle()
- Updates its state
- Generates actions via
next()
- Eventually completes and calls
leave()
sequenceDiagram
participant RT as TimerRuntime
participant Block as Current Block
participant Stack as RuntimeStack
RT->>Block: handle(event)
Block->>Block: Process event
Block->>RT: Return actions
RT->>RT: Apply actions
Note over RT,Block: Block decides to advance
Block->>RT: next()
RT->>Stack: pop()
RT->>Stack: push(newBlock)
RT->>Block: enter()
Most blocks follow this general pattern:
// Example pattern for a timed block
class TimedBlock extends RuntimeBlock {
protected onEnter(runtime: ITimerRuntime): IRuntimeAction[] {
// Start timer
return [new StartTimerAction(this.duration)];
}
protected onNext(runtime: ITimerRuntime): IRuntimeAction[] {
if (event instanceof CompleteEvent) {
// User clicked "complete"
return [new PopBlockAction()];
}
if (event instanceof TimerCompleteEvent) {
// Timer finished
return [new PopBlockAction()];
}
return [];
}
protected onLeave(runtime: ITimerRuntime): IRuntimeAction[] {
// Stop timer if still running
return [new StopTimerAction()];
}
}
The runtime uses a stack to navigate between blocks:
- Push: Adding a new block on top of the stack, making it active
- Pop: Removing the top block, returning to the previous block
- Replace: Popping the current block and pushing a new one
// Example of block navigation
runtime.push(new EffortBlock(statement)); // Push a new block
runtime.pop(); // Return to previous block
When all blocks are processed:
// Inside RootBlock when all children are complete
return [new PushEndBlockAction()]; // Push the Done block
The DoneRuntimeBlock:
- Displays final workout results
- Provides options to save or reset
- Handles the end of the workout flow
When the user resets the workout:
// Reset handler
if (event instanceof ResetEvent) {
return [
new PopBlockAction(), // Remove DoneBlock
new PushIdleBlockAction() // Return to idle state
];
}
Events flow through a reactive pipeline:
flowchart LR
Input[Input Events] --> Merge[Merge]
Tick[Tick Events] --> Merge
Merge --> Process[Process Events]
Process --> Actions[Generate Actions]
Actions --> Apply[Apply Actions]
Apply --> Output[Output Events]
// Event processing pipeline in TimerRuntime
this.dispose = merge(this.input$.pipe(tap(logEvent)), this.tick$)
.subscribe(event => {
// Log the event to the trace
this.trace.log(event);
// Get the current active block
const block = this.trace.current();
// Handle the event and collect actions
const actions = block?.handle(this, event)
.filter(actions => actions !== undefined)
.flat() ?? [];
// Apply each action
for (const action of actions) {
action.apply(this, this.input$, this.output$);
}
});
For a simple :30
timer:
- User clicks Start
- IdleBlock pops
- RootBlock gets
:30
statement - TimedBlock for
:30
is pushed - Timer starts counting down
- When timer completes or user clicks Complete
- TimedBlock pops
- DoneBlock is pushed
For (3) 10 Push-ups
:
- RepeatingBlock for 3 rounds is pushed
- EffortBlock for 10 Push-ups is pushed
- User completes the exercise
- EffortBlock pops
- RepeatingBlock checks round count (1 of 3)
- EffortBlock is pushed again for round 2
- Process repeats until all 3 rounds complete
- RepeatingBlock pops
- DoneBlock is pushed
The RuntimeTrace maintains a complete history of events and block transitions, which can be used for debugging:
// Accessing event history
const history = runtime.trace.history;
// Viewing the current block stack
const stack = runtime.trace.stack;
// Finding the active block
const current = runtime.trace.current();
This workflow provides a comprehensive view of how the wod.wiki runtime executes workout scripts, from initialization through completion.