Base Runner

Base class for runner oriented functionalities. It handles the lifecycle of a runner so you just worry about the implementation of what is running.
The BaseRunner provides a state machine with events for the different phases: prepare → run → release → finally, with support for stopping, skipping, timeouts, and error handling.
Getting Started
npm install @universal-packages/base-runner
Usage
BaseRunner class
The BaseRunner class is designed to be extended. It provides a complete lifecycle management system for any kind of runner (workers, tasks, processes, etc.).
import { BaseRunner } from '@universal-packages/base-runner'
class MyWorker extends BaseRunner {
protected async internalPrepare(): Promise<void> {
console.log('Setting up worker...')
}
protected async internalRun(): Promise<string | Error | void> {
console.log('Working...')
await new Promise((resolve) => setTimeout(resolve, 1000))
return
}
protected async internalRelease(): Promise<void> {
console.log('Cleaning up...')
}
protected async internalStop(): Promise<void> {
console.log('Stopping work...')
}
protected async internalFinally(): Promise<void> {
console.log('Cleaning up always...')
}
}
const worker = new MyWorker({ timeout: 5000 })
worker.on('succeeded', (event) => {
console.log('Worker completed successfully!')
})
worker.on('failed', (event) => {
console.log('Worker failed:', event.payload.reason)
})
worker.on('timed-out', (event) => {
console.log('Worker timed out at:', event.payload.timedOutAt)
})
await worker.run()
console.log('Final status:', worker.status)
Constructor constructor
new BaseRunner(options?: BaseRunnerOptions)
Creates a new BaseRunner instance.
BaseRunnerOptions
All options from @universal-packages/event-emitter are supported.
timeout Number optional
The timeout in milliseconds. If the runner takes longer than this time, it will be stopped.
Timeout only applies to the running state. Preparation and releasing states are not affected.
runMode single | multi optional default: single
The run mode. When set to 'single', the runner can only be run once. When set to 'multi', the runner can be run multiple times and will reset to idle state after completion.
prepareOnMultiMode always | never | on-first-run optional default: on-first-run
Whether to prepare the runner on multi mode.
releaseOnMultiMode always | never optional default: never
Whether to release the runner on multi mode.
Status Lifecycle
The BaseRunner follows a specific state machine:
Idle - Initial state
Preparing - Running internalPrepare()
Running - Running internalRun()
Releasing - Running internalRelease()
Stopping - Trying to stop by running internalStop()
- Final states:
Succeeded, Failed, Error, Stopped, TimedOut, or Skipped
internalFinally() runs after reaching any final state, before emitting the final event
Getters
status
get status(): Status
Returns the current status of the runner.
const runner = new MyWorker()
console.log(runner.status)
await runner.run()
console.log(runner.status)
error
get error(): Error | null
Returns the error that occurred during execution, or null if no error occurred.
runner.on('error', () => {
console.log('Error occurred:', runner.error?.message)
})
failureReason
get failureReason(): string | null
Returns the failure reason or null if the runner didn't fail.
runner.on('failed', () => {
console.log('Failed because:', runner.failureReason)
})
skipReason
get skipReason(): string | null
Returns the reason the runner was skipped, or null if it wasn't skipped.
runner.skip('Not needed today')
console.log(runner.skipReason)
startedAt
get startedAt(): Date | null
Returns the date when the runner started execution, or null if it hasn't started or was skipped.
runner.on('running', () => {
console.log('Started at:', runner.startedAt)
})
finishedAt
get finishedAt(): Date | null
Returns the date when the runner finished execution, or null if it hasn't finished yet.
runner.on('succeeded', () => {
console.log('Finished at:', runner.finishedAt)
console.log('Duration:', runner.finishedAt.getTime() - runner.startedAt.getTime())
})
measurement
get measurement(): Measurement | null
Returns the time measurement of the entire execution, or null if the runner hasn't finished yet. This includes preparation, running, and release phases.
runner.on('succeeded', () => {
console.log('Total execution time:', runner.measurement?.toString())
})
The BaseRunner provides boolean getter methods for easy status checking:
isIdle
get isIdle(): boolean
Returns true if the runner is in the Idle state.
isPreparing
get isPreparing(): boolean
Returns true if the runner is in the Preparing state.
isRunning
get isRunning(): boolean
Returns true if the runner is in the Running state.
isStopping
get isStopping(): boolean
Returns true if the runner is in the Stopping state.
isReleasing
get isReleasing(): boolean
Returns true if the runner is in the Releasing state.
isStopped
get isStopped(): boolean
Returns true if the runner is in the Stopped state.
isFailed
get isFailed(): boolean
Returns true if the runner is in the Failed state.
isError
get isError(): boolean
Returns true if the runner is in the Error state.
isSucceeded
get isSucceeded(): boolean
Returns true if the runner is in the Succeeded state.
isTimedOut
get isTimedOut(): boolean
Returns true if the runner is in the TimedOut state.
isSkipped
get isSkipped(): boolean
Returns true if the runner is in the Skipped state.
isActive
get isActive(): boolean
Returns true if the runner is currently active (preparing, running, or stopping).
isFinished
get isFinished(): boolean
Returns true if the runner has reached a terminal state (succeeded, failed, stopped, timed out, skipped, or error).
Instance Methods
run
async run(): Promise<void>
Starts the lifecycle of the runner
const runner = new MyWorker()
await runner.run()
stop
async stop(reason?: string): Promise<void>
Attempts to stop the runner. The behavior depends on the current state.
const runner = new MyWorker()
const runPromise = runner.run()
setTimeout(() => runner.stop('User requested'), 2000)
await runPromise
skip
skip(reason?: string): void
Skips the runner execution. Can only be called when the runner is in Idle state.
const runner = new MyWorker()
runner.skip('Not needed')
fail
fail(reason: string | Error): void
Marks the runner as failed without going through the normal lifecycle. Can only be called when the runner is in Idle state. This is useful for pre-execution validation failures.
const runner = new MyWorker()
if (!isConfigurationValid()) {
runner.fail('Invalid configuration')
return
}
try {
validateInput()
} catch (error) {
runner.fail(error)
return
}
await runner.run()
waitForStatusLevel
async waitForStatusLevel(status: Status): Promise<void>
Waits for the runner to reach a certain status level. Useful for waiting for completion regardless of the final state.
const runner = new MyWorker()
const runPromise = runner.run()
await runner.waitForStatusLevel(Status.Succeeded)
Protected Methods (Override These)
internalPrepare
protected async internalPrepare(): Promise<void>
Override this method to implement your preparation logic. This runs before the main work.
protected async internalPrepare(): Promise<void> {
this.database = await connectToDatabase()
this.logger = new Logger('my-worker')
}
internalRun
protected async internalRun(): Promise<string | undefined>
Required override. Implement your main work logic here.
- Return
undefined or don't return anything for success
- Return a string to indicate failure with a reason
protected async internalRun(): Promise<string | undefined> {
try {
await this.processData()
return undefined
} catch (error) {
return `Processing failed: ${error.message}`
}
}
internalRelease
protected async internalRelease(): Promise<void>
Override this method to implement cleanup logic. This always runs after the work, regardless of success or failure.
protected async internalRelease(): Promise<void> {
await this.database?.close()
this.logger?.close()
}
internalStop
protected async internalStop(): Promise<void>
Override this method to handle stop requests. This should interrupt the current work.
protected async internalStop(): Promise<void> {
this.shouldStop = true
await this.currentTask?.cancel()
}
internalFinally
protected async internalFinally(): Promise<void>
Override this method to implement cleanup logic that should always run after the runner reaches any finished state, regardless of success, failure, error, timeout, stop, or skip. This is similar to a finally block in try-catch-finally.
This method is called after the final status is set but before the final event is emitted, giving you access to the final state while ensuring cleanup always happens.
Important Notes:
- This method runs for all terminal states:
Succeeded, Failed, Error, Stopped, TimedOut, and Skipped
- If this method throws an error, it will emit an
error event but won't prevent the final status event from being emitted
- This method has access to the final status, measurements, and timing information
- This method is called exactly once per runner lifecycle
protected async internalFinally(): Promise<void> {
await this.closeConnections()
await this.cleanupTempFiles()
this.logger?.info(`Runner finished with status: ${this.status}`)
if (this.error) {
this.logger?.error('Runner had error:', this.error)
}
if (this.measurement) {
this.logger?.info(`Total execution time: ${this.measurement.toString()}`)
}
}
Use Cases:
- Cleanup resources that should always be released
- Logging final status and metrics
- Sending notifications regardless of outcome
- Closing database connections or file handles
- Cleanup temporary files or directories
Events
The BaseRunner emits events for each phase of the lifecycle:
Status Events
preparing - Preparation phase started
prepared - Preparation phase completed
running - Running phase started
releasing - Release phase started
released - Release phase completed
stopping - Stopping phase started
Final Status Events
succeeded - Runner completed successfully
failed - Runner failed (returned failure reason or was marked as failed using fail())
stopped - Runner was stopped
timed-out - Runner exceeded timeout
skipped - Runner was skipped
error - An error occurred during execution
Other Events
warning - Warning event (e.g., invalid operations)
All events include timing information and relevant payloads:
worker.on('succeeded', (event) => {
console.log('Duration:', event.measurement.toString())
console.log('Started at:', event.payload.startedAt)
console.log('Finished at:', event.payload.finishedAt)
})
worker.on('failed', (event) => {
console.log('Failure reason:', event.payload.reason)
console.log('Duration:', event.measurement.toString())
})
worker.on('stopped', (event) => {
console.log('Stop reason:', event.payload.reason)
console.log('Stopped at:', event.payload.stoppedAt)
})
worker.on('timed-out', (event) => {
console.log('Runner timed out at:', event.payload.timedOutAt)
})
Example: File Processing Worker
import { BaseRunner, Status } from '@universal-packages/base-runner'
import { readFile, writeFile } from 'fs/promises'
class FileProcessor extends BaseRunner {
private inputFile: string
private outputFile: string
private data: string[]
private processedData: string[] = []
private shouldStop: boolean = false
constructor(inputFile: string, outputFile: string, options?: BaseRunnerOptions) {
super(options)
this.inputFile = inputFile
this.outputFile = outputFile
}
protected async internalPrepare(): Promise<void> {
this.data = (await readFile(this.inputFile, 'utf-8')).split('\n')
}
protected async internalRun(): Promise<string | undefined> {
try {
let processedData: string[] = []
for (const line of this.data) {
if (this.shouldStop) break
processedData.push(line.toUpperCase())
}
this.processedData = processedData
} catch (error) {
return `Processing failed: ${error.message}`
}
}
protected async internalRelease(): Promise<void> {
if (this.processedData) {
await writeFile(this.outputFile, this.processedData)
}
}
protected async internalStop(): Promise<void> {
console.log('Stopping file processing...')
this.shouldStop = true
}
protected async internalFinally(): Promise<void> {
console.log(`File processing completed with status: ${this.status}`)
if (this.measurement) {
console.log(`Total processing time: ${this.measurement.toString()}`)
}
this.processedData = []
}
}
const processor = new FileProcessor('input.txt', 'output.txt', { timeout: 10000 })
processor.on('succeeded', () => console.log('File processed successfully!'))
processor.on('failed', (event) => console.log('Processing failed:', event.payload.reason))
processor.on('timed-out', () => console.log('Processing timed out'))
await processor.run()
Typescript
This library is developed in TypeScript and shipped fully typed.
Contributing
The development of this library happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving this library.
License
MIT licensed.