Created
January 8, 2025 21:15
-
-
Save arthur-fontaine/608a6e04d4e600779be2a3b8f89bd11c to your computer and use it in GitHub Desktop.
`nanomachine` is a type-safe state machine builder based on Nanostores and inspired by XState.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as nanostores from "nanostores"; | |
class StateMachineContextBuilder { | |
context<CONTEXT_TYPE>(value: CONTEXT_TYPE) { | |
const atom = nanostores.atom(value); | |
return new StateMachineEventsBuilder(atom); | |
} | |
} | |
class StateMachineEventsBuilder<CONTEXT_TYPE> { | |
constructor( | |
private atom: nanostores.PreinitializedWritableAtom<CONTEXT_TYPE>, | |
) {} | |
events< | |
EVENTS extends { | |
[key: string]: unknown; | |
}, | |
>() { | |
return new StateMachineStatesBuilder<CONTEXT_TYPE, EVENTS>(this.atom); | |
} | |
} | |
class StateMachineStatesBuilder< | |
CONTEXT_TYPE, | |
EVENTS extends { | |
[key: string]: unknown; | |
}, | |
> { | |
constructor( | |
private atom: nanostores.PreinitializedWritableAtom<CONTEXT_TYPE>, | |
) {} | |
states<STATE_TYPES extends string>( | |
states: { | |
[key in STATE_TYPES]: ( | |
stateBuilder: StateMachineStateBuilder< | |
CONTEXT_TYPE, | |
STATE_TYPES, | |
EVENTS | |
>, | |
) => StateMachineStateBuilder<CONTEXT_TYPE, STATE_TYPES, EVENTS>; | |
}, | |
) { | |
return new StateMachineInitialBuilder<CONTEXT_TYPE, STATE_TYPES, EVENTS>( | |
this.atom, | |
states, | |
); | |
} | |
} | |
class StateMachineStateBuilder< | |
CONTEXT_TYPE, | |
STATE_TYPES extends string, | |
EVENTS extends { | |
[key: string]: unknown; | |
}, | |
> { | |
constructor( | |
private atom: nanostores.PreinitializedWritableAtom<CONTEXT_TYPE>, | |
private stateAtom: nanostores.PreinitializedWritableAtom<STATE_TYPES>, | |
private event: keyof EVENTS, | |
private payload: EVENTS[keyof EVENTS], | |
) {} | |
onEntry( | |
action: ( | |
state: CONTEXT_TYPE, | |
set: (value: Partial<CONTEXT_TYPE>) => void, | |
) => void, | |
): typeof this { | |
action(this.atom.get(), (value) => | |
this.atom.set({ ...this.atom.get(), ...value }), | |
); | |
return this; | |
} | |
onReceive( | |
events: { | |
[key in keyof EVENTS]?: ( | |
...[context, set, payload]: EVENTS[key] extends undefined | |
? [CONTEXT_TYPE, (value: Partial<CONTEXT_TYPE>) => void] | |
: [CONTEXT_TYPE, (value: Partial<CONTEXT_TYPE>) => void, EVENTS[key]] | |
) => STATE_TYPES; | |
}, | |
options?: { | |
guard?: (state: CONTEXT_TYPE) => boolean; | |
}, | |
): typeof this { | |
if (options?.guard && !options.guard(this.atom.get())) { | |
return this; | |
} | |
const newState = ( | |
events[this.event] as ((...args: unknown[]) => void) | undefined | |
)?.(this.atom.get(), (value: CONTEXT_TYPE) => this.atom.set({ ...this.atom.get(), ...value }), this.payload); | |
if (newState) { | |
this.stateAtom.set(newState); | |
} | |
return this; | |
} | |
after(timeout: number, stateType: STATE_TYPES): typeof this { | |
const expectedState = this.stateAtom.get(); | |
setTimeout(() => { | |
if (this.stateAtom.get() === expectedState) { | |
this.stateAtom.set(stateType); | |
} | |
}, timeout); | |
return this; | |
} | |
} | |
class StateMachineInitialBuilder< | |
CONTEXT_TYPE, | |
STATE_TYPES extends string, | |
EVENTS extends { | |
[key: string]: unknown; | |
}, | |
> { | |
constructor( | |
private atom: nanostores.PreinitializedWritableAtom<CONTEXT_TYPE>, | |
private states: { | |
[key in STATE_TYPES]: ( | |
stateBuilder: StateMachineStateBuilder< | |
CONTEXT_TYPE, | |
STATE_TYPES, | |
EVENTS | |
>, | |
) => void; | |
}, | |
) {} | |
initial(initialState: STATE_TYPES) { | |
return new StateMachineFinalBuilder<CONTEXT_TYPE, STATE_TYPES, EVENTS>( | |
this.atom, | |
this.states, | |
initialState, | |
); | |
} | |
} | |
class StateMachineFinalBuilder< | |
CONTEXT_TYPE, | |
STATE_TYPES extends string, | |
EVENTS extends { | |
[key: string]: unknown; | |
}, | |
> { | |
constructor( | |
private atom: nanostores.PreinitializedWritableAtom<CONTEXT_TYPE>, | |
private states: { | |
[key in STATE_TYPES]: ( | |
stateBuilder: StateMachineStateBuilder< | |
CONTEXT_TYPE, | |
STATE_TYPES, | |
EVENTS | |
>, | |
) => void; | |
}, | |
private initialState: STATE_TYPES, | |
) {} | |
get stateMachine() { | |
return new StateMachine<CONTEXT_TYPE, STATE_TYPES, EVENTS>( | |
this.atom, | |
this.states, | |
this.initialState, | |
); | |
} | |
} | |
class StateMachine< | |
CONTEXT_TYPE, | |
STATE_TYPES extends string, | |
EVENTS extends { | |
[key: string]: unknown; | |
}, | |
> { | |
private $stateAtom: nanostores.PreinitializedWritableAtom<STATE_TYPES>; | |
private started = false; | |
constructor( | |
private atom: nanostores.PreinitializedWritableAtom<CONTEXT_TYPE>, | |
private states: { | |
[key in STATE_TYPES]: ( | |
stateBuilder: StateMachineStateBuilder< | |
CONTEXT_TYPE, | |
STATE_TYPES, | |
EVENTS | |
>, | |
) => void; | |
}, | |
initialState: STATE_TYPES, | |
) { | |
this.$stateAtom = nanostores.atom(initialState); | |
} | |
start() { | |
if (this.started) { | |
return this; | |
} | |
this.started = true; | |
this.$stateAtom.subscribe((state) => { | |
this.states[state]?.( | |
// biome-ignore lint/style/noNonNullAssertion: <explanation> | |
new StateMachineStateBuilder(this.atom, this.$stateAtom, null!, null!), | |
); | |
}); | |
return this; | |
} | |
get() { | |
return this.atom.get(); | |
} | |
set(value: CONTEXT_TYPE) { | |
this.atom.set(value); | |
} | |
send<K extends keyof EVENTS>( | |
...[event, payload]: EVENTS[K] extends undefined ? [K] : [K, EVENTS[K]] | |
) { | |
if (!this.started) { | |
return; | |
} | |
const state = this.$stateAtom.get(); | |
this.states[state]?.( | |
new StateMachineStateBuilder( | |
this.$atom, | |
this.$stateAtom, | |
event, | |
payload as never, | |
), | |
); | |
} | |
subscribe(listener: (value: CONTEXT_TYPE) => void): () => void { | |
return this.atom.subscribe(listener); | |
} | |
get $atom() { | |
return this.atom; | |
} | |
} | |
export function createStateMachine() { | |
return new StateMachineContextBuilder(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment