state-switch
Version:
State Switch is a Change Monitor/Guarder for Async Actions.
259 lines (214 loc) • 8.93 kB
text/typescript
/**
* Licenst: Apache-2.0
* https://github.com/huan/state-switch
*/
import { getLoggable } from 'brolog'
import type { Loggable } from 'brolog'
import { timeoutPromise } from 'gerror'
import {
ServiceCtlInterface,
StateSwitchOptions,
ServiceableAbstract,
StateSwitchInterface,
} from '../interfaces.js'
import { StateSwitch } from '../state-switch.js'
import { BusyIndicator } from '../busy-indicator.js'
import { VERSION } from '../version.js'
/**
* Wait from unknown state
*/
const TIMEOUT_SECONDS = 5
const RESET_TIMEOUT_SECONDS = TIMEOUT_SECONDS * 3
const serviceCtlMixin = (
serviceCtlName = 'ServiceCtl',
options? : StateSwitchOptions,
) => <SuperClass extends typeof ServiceableAbstract> (superClass: SuperClass) => {
abstract class ServiceCtlMixin extends superClass implements ServiceCtlInterface {
static VERSION = VERSION
state: StateSwitchInterface
__serviceCtlResettingIndicator : BusyIndicator
__serviceCtlLogger : Loggable
constructor (...args: any[]) {
super(...args)
this.__serviceCtlLogger = getLoggable(options?.log)
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'constructor()')
this.state = new StateSwitch(serviceCtlName, options)
this.__serviceCtlResettingIndicator = new BusyIndicator(serviceCtlName + 'Reset', options)
}
override async start () : Promise<void> {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'start()')
if (this.state.active()) {
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'start() found that is starting/statred ...')
await this.state.stable('active')
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'start() found that is starting/statred ... done')
return
}
if (this.state.inactive() === 'pending') {
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'start() found that is stopping, waiting stable ... (max %s seconds)', TIMEOUT_SECONDS)
try {
await timeoutPromise(
this.state.stable('inactive'),
TIMEOUT_SECONDS * 1000,
)
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'start() found that is stopping, waiting stable ... done')
} catch (e) {
this.emit('error', e)
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'start() found that is stopping, waiting stable ... timeout')
}
}
this.state.active('pending')
try {
/**
* Parent start()
*/
if (typeof super.start === 'function') {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'start() super.start() ...')
await super.start()
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'start() super.start() ... done')
}
/**
* Child onStart()
*/
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'start() this.onStart() ...')
await this.onStart()
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'start() this.onStart() ... done')
/**
* the service has been successfully started
*/
this.state.active(true)
} catch (e) {
this.emit('error', e)
await this.stop()
throw e
}
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'start() ... done')
}
override async stop (): Promise<void> {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'stop()')
/**
* Already in inactive/stop state: return directly
*/
if (this.state.inactive()) {
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'stop() found that is stopping/stopped, wait stable ...')
await this.state.stable('inactive')
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'stop() found that is stopping/stopped, wait stable ... done')
return
}
/**
* activing/starting: wait it to be finished first (with timeout)
*/
if (this.state.active() === 'pending') {
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'stop() found that is starting...')
try {
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'stop() found that is starting, waiting stable ... (max %s seconds)', TIMEOUT_SECONDS)
await timeoutPromise(
this.state.stable('active'),
TIMEOUT_SECONDS * 1000,
)
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'stop() found that is starting, waiting stable ... done')
} catch (e) {
this.emit('error', e)
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'stop() found that is starting, waiting stable ... timeout')
}
}
this.state.inactive('pending')
/**
* Child onStop()
*/
try {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'stop() this.onStop() ...')
await this.onStop()
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'stop() this.onStop() ... done')
} catch (e) {
this.emit('error', e)
}
/**
* Parent stop()
*/
if (typeof super.stop === 'function') {
try {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'stop() super.stop() ...')
await super.stop()
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'stop() super.stop() ... done')
} catch (e) {
this.emit('error', e)
}
}
/**
* no matter whether the `try {...}` code success or not
* set the service state to off(stopped) state
*/
this.state.inactive(true)
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'stop() ... done')
}
async reset (): Promise<void> {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset()')
/**
* Do not reset again if it's already resetting
*/
if (this.__serviceCtlResettingIndicator.busy()) {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() `resetBusy` is `busy`, wait `idle()` (max %d seconds) ...', RESET_TIMEOUT_SECONDS)
try {
await timeoutPromise(
this.__serviceCtlResettingIndicator.idle(),
RESET_TIMEOUT_SECONDS * 1000,
() => new Error('wait resetting timeout'),
)
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() `resetBusy` is `busy`, wait `idle()` (max %d seconds) ... done', RESET_TIMEOUT_SECONDS)
return
} catch (e) {
this.emit('error', e)
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() `resetBusy` is `busy`, wait `idle()` (max %d seconds) ... timeout')
}
}
/**
* Do not start Service if the stable state is OFF
*/
if (this.state.inactive()) {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() `state` is `off`, do nothing')
return
}
this.__serviceCtlResettingIndicator.busy(true)
/**
* If the Service is starting/stopping, wait for it
* The state will be `'active'` after await `stable()`
*/
try {
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() wait state ready() ...')
await timeoutPromise(
this.state.stable(),
3 * TIMEOUT_SECONDS * 1000,
() => new Error('state.ready() timeout'),
)
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() wait state ready() ... done')
} catch (e) {
this.emit('error', e)
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() wait state ready() ... timeout')
}
/**
* Do the stop() & start() job
*/
try {
await this.stop()
await this.start()
} catch (e) {
this.emit('error', e)
this.__serviceCtlLogger.warn(`ServiceCtl<${serviceCtlName}>`, 'reset() ... rejection: %s', (e as Error).message)
} finally {
this.__serviceCtlResettingIndicator.busy(false)
}
this.__serviceCtlLogger.verbose(`ServiceCtl<${serviceCtlName}>`, 'reset() ... done')
}
/**
* onStart & onStop must be implemented by the child class
*/
abstract onStart (): Promise<void>
abstract onStop (): Promise<void>
}
return ServiceCtlMixin
}
abstract class ServiceCtl extends serviceCtlMixin()(ServiceableAbstract) {}
export {
ServiceCtl,
serviceCtlMixin,
}