Runtime - SergeiGolos/wod-wiki GitHub Wiki
This document provides comprehensive details about the runtime component architecture in wod.wiki, focusing on the block-based execution model, event handling system, and reactive data flow.
The wod.wiki runtime is built upon a few key architectural patterns:
- Block-based Component Model: Workout execution is divided into blocks, each responsible for a specific execution context
- Reactive Event Processing: Events flow through a reactive pipeline using RxJS Observables
- Stack-based Execution Trace: Maintains parent-child relationships between blocks in an execution stack
- Just-In-Time Compilation: Creates specialized blocks for different execution contexts as needed
graph TB
subgraph Frontend
WikiContainer --> ButtonRibbon
WikiContainer --> TimerDisplay
WikiContainer --> WodWiki[WodWiki Editor]
WikiContainer --> ResultsDisplay
end
subgraph RuntimeSystem
TimerRuntime --> RuntimeJit
TimerRuntime --> RuntimeTrace
TimerRuntime --> RuntimeStack
RuntimeJit --> RuntimeBlock
RuntimeBlock --> EventHandler
EventHandler --> IRuntimeAction
end
WodWiki -- onScriptCompiled --> TimerRuntime
ButtonRibbon -- Events --> TimerRuntime
TimerRuntime -- Actions --> TimerDisplay
TimerRuntime -- Results --> ResultsDisplay
RuntimeBlock
is the abstract base class for all execution blocks. It defines the common interface and behavior for handling events and generating actions.
export abstract class RuntimeBlock implements IRuntimeBlock {
constructor(
public blockId: number,
public blockKey: string,
public source?: StatementNode | undefined
) {}
// Parent-child relationship
public parent?: IRuntimeBlock | undefined;
// Results tracking
public laps: ResultSpan[] = [];
public metrics: RuntimeMetric[] = [];
public buttons: IActionButton[] = [];
// Event handling
protected handlers: EventHandler[] = [];
protected system: EventHandler[] = [];
// Abstracts that specialized blocks must implement
abstract next(runtime: ITimerRuntime): StatementNode | undefined;
abstract load(runtime: ITimerRuntime): IRuntimeEvent[];
// Standard event handling pipeline
public handle(runtime: ITimerRuntime, event: IRuntimeEvent): IRuntimeAction[] {
const result: IRuntimeAction[] = [];
for (const handler of [...this.system, ...this.handlers]) {
const actions = handler.apply(event, runtime);
for (const action of actions) {
result.push(action);
}
}
return result;
}
}
The system includes several specialized block types:
- RootBlock: Contains all top-level statements in the workout script
- SingleBlock: Handles execution of a single statement
- IdleRuntimeBlock: Represents the idle state of the runtime
- DoneRuntimeBlock: Represents the completed state of the runtime
Each specialized block type implements the abstract methods from RuntimeBlock
:
-
next()
: Determines the next statement to execute -
load()
: Initializes the block and generates any events needed at startup
The TimerRuntime
orchestrates the event flow using RxJS Observables:
export class TimerRuntime implements ITimerRuntimeIo {
// Observable streams
public tick$: Observable<IRuntimeEvent>;
public input$: Subject<IRuntimeEvent>;
public output$: Subject<OutputEvent>;
// State management
public trace: RuntimeTrace;
constructor(
public code: string,
public script: RuntimeStack,
public jit: RuntimeJit,
input$: Subject<IRuntimeEvent>,
output$: Subject<OutputEvent>,
) {
// Initialize the runtime
this.input$ = input$;
this.output$ = output$;
this.trace = new RuntimeTrace();
// Set up the initial blocks
this.next(this.jit.root(this));
this.next(this.jit.idle(this));
// Create tick stream for time-based events
this.tick$ = interval(100).pipe(map(() => new TickEvent()));
// Set up event processing pipeline
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$);
}
});
}
// ... methods for block navigation and state management ...
}
Events flow through the system in this pattern:
- External events enter through
input$
(e.g., start, stop, lap) - Time-based events enter through
tick$
(generated every 100ms) - Blocks handle events and generate actions
- Actions apply changes to the runtime state
- Output events are published through
output$
RuntimeTrace
manages the execution stack and event history:
export class RuntimeTrace {
// Execution history
public history: Array<IRuntimeLog> = [];
// Block execution stack
public stack: Array<IRuntimeBlock> = [];
// Get the currently active block
current(): IRuntimeBlock | undefined {
return this.stack.length == 0 ? undefined : this.stack[this.stack.length - 1];
}
// Record an event in the history
log(event: IRuntimeEvent) { /* ... */ }
// Add a block to the execution stack
push(block: IRuntimeBlock): IRuntimeBlock { /* ... */ }
// Remove the top block from the execution stack
pop(): IRuntimeBlock | undefined { /* ... */ }
}
The trace provides a complete picture of:
- The current execution context (active block)
- Parent-child relationships between blocks
- Event history for debugging and analytics
ResultSpan
captures execution results for a block:
export class ResultSpan {
blockKey?: string;
index?: number;
stack?: number[];
start?: IRuntimeEvent;
stop?: IRuntimeEvent;
metrics: RuntimeMetric[] = [];
label?: string;
// Calculate duration between start and stop
duration(timestamp?: Date): number { /* ... */ }
// Apply metric edits
edit(edits: RuntimeMetricEdit[]): ResultSpan { /* ... */ }
}
RuntimeMetric
captures specific measurements within a workout:
export type RuntimeMetric = {
effort: string; // The exercise name
repetitions?: MetricValue; // Count of repetitions
resistance?: MetricValue; // Weight/resistance used
distance?: MetricValue; // Distance covered
};
export type MetricValue = {
value: number;
unit: string;
};
These metrics are used throughout the application for results display, analytics, and performance tracking.
The runtime system integrates with the UI through:
- Input Events: Button clicks, editor commands, and other user interactions
- Output Actions: Display updates, timer changes, result updates
- ButtonRibbon: Sends control events (start, stop, lap) to the runtime
- TimerDisplay: Receives display updates from the runtime
- ResultsDisplay: Receives result metrics from the runtime
- WodWiki Editor: Provides the workout script to the runtime
By maintaining this separation of concerns, the runtime can focus on workout execution while the UI handles presentation.
This architectural approach provides a modular, testable, and extensible foundation for the wod.wiki runtime system, enabling complex workout script execution with real-time feedback and metrics tracking.