Skip to content

Instantly share code, notes, and snippets.

@arthur-fontaine
Created January 8, 2025 21:15
Show Gist options
  • Save arthur-fontaine/608a6e04d4e600779be2a3b8f89bd11c to your computer and use it in GitHub Desktop.
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.
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