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> {
// Setup your worker
console.log('Setting up worker...')
}
protected async internalRun(): Promise<string | Error | void> {
// Do the actual work
console.log('Working...')
await new Promise((resolve) => setTimeout(resolve, 1000))
// Return for success, or a string for failure reason, or an error for failure
return
}
protected async internalRelease(): Promise<void> {
// Cleanup after work
console.log('Cleaning up...')
}
protected async internalStop(): Promise<void> {
// Handle stopping the work
console.log('Stopping work...')
}
protected async internalFinally(): Promise<void> {
// Always cleanup, regardless of outcome
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()
// Check final status
console.log('Final status:', worker.status) // 'succeeded', 'failed', 'timed-out', etc.
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.
Status Lifecycle
The BaseRunner follows a specific state machine:
Idle
- Initial statePreparing
- RunninginternalPrepare()
Running
- RunninginternalRun()
Releasing
- RunninginternalRelease()
Stopping
- Trying to stop by runninginternalStop()
- Final states:
Succeeded
,Failed
,Error
,Stopped
,TimedOut
, orSkipped
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) // Status.Idle
await runner.run()
console.log(runner.status) // Status.Succeeded, Status.Failed, etc.
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) // "Not needed today"
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()
// Stop after 2 seconds
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')
// Runner will be in 'Skipped' state
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()
// Check some precondition
if (!isConfigurationValid()) {
runner.fail('Invalid configuration')
// Runner will be in 'Failed' state immediately
return
}
// Or fail with an Error object
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()
// Wait for any final status (succeeded, failed, stopped, etc.)
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 // Success
} catch (error) {
return `Processing failed: ${error.message}` // Failure
}
}
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
, andSkipped
- 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> {
// Always runs regardless of outcome
await this.closeConnections()
await this.cleanupTempFiles()
// You have access to final state
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 startedprepared
- Preparation phase completedrunning
- Running phase startedreleasing
- Release phase startedreleased
- Release phase completedstopping
- Stopping phase started
Final Status Events
succeeded
- Runner completed successfullyfailed
- Runner failed (returned failure reason or was marked as failed usingfail()
)stopped
- Runner was stoppedtimed-out
- Runner exceeded timeoutskipped
- Runner was skippederror
- 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)
// Note: Final status will be TimedOut, not Stopped
})
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> {
// Validate files exist, setup temp directories, etc.
this.data = (await readFile(this.inputFile, 'utf-8')).split('\n')
}
protected async internalRun(): Promise<string | undefined> {
try {
// Process the data
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> {
// Write output file if we have processed data
if (this.processedData) {
await writeFile(this.outputFile, this.processedData)
}
}
protected async internalStop(): Promise<void> {
// Cancel any ongoing operations
console.log('Stopping file processing...')
this.shouldStop = true
}
protected async internalFinally(): Promise<void> {
// Always cleanup, regardless of success or failure
console.log(`File processing completed with status: ${this.status}`)
if (this.measurement) {
console.log(`Total processing time: ${this.measurement.toString()}`)
}
// Any cleanup that should always happen
this.processedData = []
}
}
// Usage
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.