Manage async complexity with states
import { States, PickState } from 'class-states'
type SomeConnectionState =
{
state: 'DISCONNECTED'
} | {
state: 'CONNECTING',
transition: Promise<
PickState<SomeConnectionState, 'CONNECTED' | 'DISCONNECTED'>
>
} | {
state: 'CONNECTED',
connection: Connection
}
class SomeConnection {
state: States<SomeConnectionState>
constructor() {
this.state = new States({
state: 'DISCONNECTED'
})
this.state.onTransition((state, prevState) => {})
this.state.onTransition('DISCONNECTED', (disconnectedState, prevState) => {})
}
private _connect() {
return this.state.set({
state: 'CONNECTING',
transition: someConnectionCreator()
.then((connection) => ({
state: 'CONNECTED',
connection
}))
.catch(() => ({
state: 'DISCONNECTED'
}))
})
}
async connect() {
// We first narrow down to a possible "CONNECTED" or "DISCONNECTED"
// state
const connectState = await this.state.matches({
DISCONNECTED: () => this._connect().transition,
CONNECTING: ({ transition }) => transition,
CONNECTED: (state) => state
})
// Then we use the narrowed state to evaluate what this method
// returns
return this.state.matches(connectState, {
CONNECTED: ({ connection }) => connection,
DISCONNECTED: () => {
throw new Error("Could not connect")
}
})
}
disconnect() {
this.state.matches({
CONNECTED: ({ connection }) => {
connection.dispose()
},
_: () => {
// Do nothing
}
})
}
}
When you work with complex asynchronous code you have some challenges:
-
Async function/method calls can be called when they are already running, which is easy to ignore or forget to evaluate
-
You have multiple flags that has a relationship (isConnecting, isConnected), but needs to be manually orchestrated, risking invalid states
-
Values can often also be
null
orundefined
as they are asynchronously initialized
class-states
gives you a tiny abstraction of explicit states and a matches
utility which:
-
Forces you to evaluate how any function/method runs when consuming the state of the class
-
Explicit single states instead of manually orchestrating multiple flags
-
Values tied to states are consumed on the specific state, avoiding
null
andundefined
unions