State Switch is a Monitor/Guard for Managing Your Async Operations.
StateSwitch
can manage state transition for you, by switching from the following four states:
INACTIVE
: state is inactivepending ACTIVE
: state is switching from INACTIVE to ACTIVEACTIVE
: state is activepending INACTIVE
: state is switch from ACTIVE to INACTIVE
You can set/get the state with the API, and you can also monite the state switch events by listening the 'active' and 'inactive' events.
There have another stable()
API return a Promise
so that you can wait the active
of inactive
events by:
await state.stable('active')
await state.stable('inactive')
await state.stable() // wait the current state
If the state is already ACTIVE when you await state.stable('active')
, then it will resolved immediatelly.
Talk is cheap, show me the code!
import { StateSwitch } from 'state-switch'
function doSlowConnect() {
console.log('> doSlowConnect() started')
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('> doSlowConnect() done')
resolve()
}, 1000)
})
}
function doSlowDisconnect() {
console.log('> doSlowDisconnect() started')
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('> doSlowDisconnect() done')
resolve()
}, 1000)
})
}
class MyConnection {
private state = new StateSwitch('MyConnection')
constructor() {
/* */
}
public connect() {
/**
* This is the only 1 Right State
*/
if (this.state.inactive() === true) {
this.state.active('pending')
doSlowConnect().then(() => {
this.state.active(true)
console.log(`> I'm now opened`)
})
console.log(`> I'm opening`)
return
}
/**
* These are the other 3 Error States
*/
if (this.state.inactive() === 'pending') {
console.error(`> I'm closing, please wait`)
} else if (this.state.active() === true) {
console.error(`> I'm already open. no need to connect again`)
} else if (this.state.active() === 'pending') {
console.error(`> I'm opening, please wait`)
}
}
public disconnect() {
/**
* This is the only one Right State
*/
if (this.state.active() === true) {
this.state.inactive('pending')
doSlowDisconnect().then(() => {
this.state.inactive(true)
console.log(`> I'm closed.`)
})
console.log(`> I'm closing`)
return
}
/**
* These are the other 3 Error States
*/
if (this.state.active() === 'pending') {
console.error(`> I'm opening, please wait`)
} else if (this.state.inactive() === true) {
console.error(`> I'm already close. no need to disconnect again`)
} else if (this.state.inactive() === 'pending') {
console.error(`> I'm closing, please wait`)
}
}
}
const conn = new MyConnection()
console.log('CALL: conn.connect(): should start to opening')
conn.connect()
console.log('CALL: conn.connect(): should not connect again while opening')
conn.connect()
console.log('CALL: conn.disconnect(): can not disconnect while opening')
conn.disconnect()
setTimeout(() => {
console.log('... 2 seconds later, should be already open ...')
console.log('CALL: conn.connect(): should not connect again if we are open')
conn.connect()
console.log('CALL: conn.disconnect(): should start to closing')
conn.disconnect()
console.log('CALL: conn.disconnect(): should not disconnect again while we are closing')
conn.disconnect()
console.log('CALL: conn.connect(): can not do connect while we are closing')
conn.connect()
setTimeout(() => {
console.log('... 2 seconds later, should be already closed ...')
console.log('CALL: conn.disconnect(): should not disconnect again if we are close')
conn.disconnect()
}, 2000)
}, 2000)
What's the meaning of the above code?
StateSwitch helps you manage the following four states easy:
$ npm run demo
> state-switch@0.1.3 demo /home/zixia/git/state-switch
> ts-node example/demo
CALL: conn.connect(): should start to opening
> doSlowConnect() started
> I'm opening
CALL: conn.connect(): should not connect again while opening
> I'm opening, please wait
CALL: conn.disconnect(): can not disconnect while opening
> I'm opening, please wait
> doSlowConnect() done
> I'm now opened
... 2 seconds later, should be already open ...
CALL: conn.connect(): should not connect again if we are open
> I'm already open. no need to connect again
CALL: conn.disconnect(): should start to closing
> doSlowDisconnect() started
> I'm closing
CALL: conn.disconnect(): should not disconnect again while we are closing
> I'm closing, please wait
CALL: conn.connect(): can not do connect while we are closing
> I'm closing, please wait
> doSlowDisconnect() done
> I'm closed.
... 2 seconds later, should be already closed ...
CALL: conn.disconnect(): should not disconnect again if we are close
> I'm already close. no need to disconnect again
That's the idea: we should always be able to know the state of our async operation.
Class StateSwitch
Create a new StateSwitch instance.
private state = new StateSwitch('MyConn')
Get the state for ACTIVE
: true
for ACTIVE(stable), pending
for ACTIVE(in-process). false
for not ACTIVE.
Set the state for ACTIVE
: true
for ACTIVE(stable), pending
for ACTIVE(in-process).
Get the state for INACTIVE
: true
for INACTIVE(stable), pending
for INACTIVE(in-process). false
for not INACTIVE.
Set the state for INACTIVE
: true
for INACTIVE(stable), pending
for INACTIVE(in-process).
Check if the state is pending
.
true
means there's some async operations we need to wait.
false
means no async active fly.
expectedState
:'active' | 'inactive'
, default is the current statenoCross
:boolean
, default isfalse
Wait the expected state to be stable.
If set noCross
to true
, then stable()
will throw if you are wait a state from it's opposite site, for example: you can expect an Exception
be thrown out when you call stable('active', true)
when the inactive() === true
.
Get the name from the constructor.
Enable log by set log to a Npmlog compatible instance.
Personaly I use Brolog, which is writen by my self, the same API with Npmlog but can also run inside Browser with Angular supported.
const log = Brolog.instance()
StateSwitch.setLog(log)
Set a true/false state.
const indicator = new BooleanIndicator()
- set
true
orfalse
- get
boolean
status
indicator.value(true)
indicator.value(false)
const value = indicator.value()
Return a Promise
that will resolved after the boolean state to be the value passed through v
.
If the current boolean state is the same as the
v
, then it will return aPromise
that will resolved immediately.
await indicator.ready(false)
assert (indicator.value() === false, 'value() should be false after await ready(false)')
interface ServiceCtlInterface {
state: StateSwitchInterface
reset : ServiceCtl['reset']
start : ServiceCtl['start']
stop : ServiceCtl['stop']
}
Use a Finite State Machine (FSM) to manage the state of your service.
import { ServiceCtlFsm } from 'state-switch'
class MyService extends ServiceCtlFsm {
async onStart (): Promise<void> {
// your start code
}
async onStop (): Promise<void> {
// your stop code
}
}
const service = new MyService()
await service.start() // this will call `onStart()`
await service.stop() // this will call `onStop()`
await service.start()
await service.reset() // this will call `onStop()` then `onStart()`
Learn more about the finite state machine design pattern inside our ServiceCtl
:
Implementes the same ServiceCtlInterface
, but using a StateSwitch
to manage the internal state.
The code is originally from Wechaty Puppet, then abstracted to a class.
- Add
BooleanIndicator
class to replace and deprecate theBusyIndicator
class for a more powerful and easy to use API.
StateSwitch#pending
->StateSwitch#pending()
StateSwitch#on()
->StateSwitch#active()
StateSwitch#off()
->StateSwitch#inactive()
emit('on')
->emit('active')
emit('off')
->emit('inactive')
TL;DR:
- state.on()
+ state.active()
- state.on(true)
+ state.active(true)
- state.off()
+ state.inactive()
- state.off(true)
+ staet.inactive(true)
- state.pending
+ state.pending()
- Oct 27: Add
ServiceCtl
/ServiceCtlFsm
abstract class andserviceCtlMixin
/serviceCtlFsmMixin
mixin - Oct 23: Add
BusyIndicator
class- Add
BusyIndicatorInterface
andStateSwitchInterface
- Add
- v0.15 (Sep 2021): Publish as ESM package.
- Add RxJS typing unit tests for making sure that the
fromEvent
typing inference is right.
Support for using RxJS:
const notPending = (state: true | 'pending') => state === true
const stateOn$ = fromEvent(stateSwitch, 'active').pipe(
filter(notPending)
)
- Support emit
on
andoff
events with the args of thestate
of two values:true
andpending
. - Add events unit tests
- DevOps for publishing to NPM@next for odd minor versions.
- Add State Diagram for easy understanding what state-switch do
BREAKING CHANGE: Change the ready()
parameter to the opposite side.
- Before:
ready(state, crossWait=false)
- AFTER:
ready(state, noCross=false
)
- add new method
ready()
to let user wait until the expected state is on(true).
BREAKING CHANGES: redesigned all APIs.
- delete all old APIs.
- add 4 new APIs: on() / off() / pending() / name()
Rename to StateSwitch
because the name StateMonitor on npmjs.com is taken.
- Make it a solo NPM Module. (#466)
Orignal name is StateMonitor
- Part of the Wechaty project
Huan LI zixia@zixia.net (http://linkedin.com/in/zixia)
- Code & Docs 2016-now© zixia
- Code released under the Apache-2.0 license
- Docs released under Creative Commons