Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 29 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useEffect, useReducer } from "react";
import { UseStateMachine, Machine, $$t } from "./types";
import { UseStateMachine, Machine, $$t, O } from "./types";
import { assertNever, R, useConstant } from "./extras";


const useStateMachineImpl = (definition: Machine.Definition.Impl) => {
const [state, dispatch] = useReducer(createReducer(definition), createInitialState(definition));
const [machineInstant, dispatch] = useReducer(createReducer(definition), createInitialState(definition));

const send = useConstant(() => (sendable: Machine.Sendable.Impl) => dispatch({ type: "SEND", sendable }));

Expand All @@ -13,30 +14,33 @@ const useStateMachineImpl = (definition: Machine.Definition.Impl) => {
};

useEffect(() => {
const entry = R.get(definition.states, state.value)!.effect;
const entry = R.get(definition.states, machineInstant.state)!.effect;
let exit = entry?.({
send,
setContext,
event: state.event,
context: state.context,
event: machineInstant.event,
context: machineInstant.context,
});

return typeof exit === "function"
? () => exit?.({ send, setContext, event: state.event, context: state.context })
? () => exit?.({ send, setContext, event: machineInstant.event, context: machineInstant.context })
: undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.value, state.event]);
}, [machineInstant.state, machineInstant.event]);

return [state, send];
return { ...machineInstant, send };
};

const createInitialState = (definition: Machine.Definition.Impl): Machine.State.Impl => {
type MachineInstant =
O.OmitKey<Machine.Impl, "send">

const createInitialState = (definition: Machine.Definition.Impl): MachineInstant => {
let nextEvents = R.keys(R.concat(
R.fromMaybe(R.get(definition.states, definition.initial)!.on),
R.fromMaybe(definition.on)
))
return {
value: definition.initial,
state: definition.initial,
context: definition.context as Machine.Context.Impl,
event: { type: "$$initial" } as Machine.Event.Impl,
nextEvents: nextEvents,
Expand All @@ -46,56 +50,56 @@ const createInitialState = (definition: Machine.Definition.Impl): Machine.State.

const createReducer = (definition: Machine.Definition.Impl) => {
let log = createLogger(definition);
return (machineState: Machine.State.Impl, internalEvent: InternalEvent): Machine.State.Impl => {
return (machineInstant: MachineInstant, internalEvent: InternalEvent): MachineInstant => {
if (internalEvent.type === "SET_CONTEXT") {
let nextContext = internalEvent.updater(machineState.context);
log("Context update", ["Previous Context", machineState.context], ["Next Context", nextContext]);
let nextContext = internalEvent.updater(machineInstant.context);
log("Context update", ["Previous Context", machineInstant.context], ["Next Context", nextContext]);

return { ...machineState, context: nextContext };
return { ...machineInstant, context: nextContext };
}

if (internalEvent.type === "SEND") {
let sendable = internalEvent.sendable;
let event = typeof sendable === "string" ? { type: sendable } : sendable;
let context = machineState.context;
let stateNode = R.get(definition.states, machineState.value)!;
let context = machineInstant.context;
let stateNode = R.get(definition.states, machineInstant.state)!;
let resolvedTransition =
R.get(R.fromMaybe(stateNode.on), event.type) ?? R.get(R.fromMaybe(definition.on), event.type);

if (!resolvedTransition) {
log(
`Current state doesn't listen to event type "${event.type}".`,
["Current State", machineState],
["Current State", machineInstant],
["Event", event]
);
return machineState;
return machineInstant;
}

let [nextStateValue, didGuardDeny = false] = (() => {
let [nextState, didGuardDeny = false] = (() => {
if (typeof resolvedTransition === "string") return [resolvedTransition];
if (resolvedTransition.guard === undefined) return [resolvedTransition.target];
if (resolvedTransition.guard({ context, event })) return [resolvedTransition.target];
return [resolvedTransition.target, true]
})() as [Machine.StateValue.Impl, true?]
})() as [Machine.State.Impl, true?]

if (didGuardDeny) {
log(
`Transition from "${machineState.value}" to "${nextStateValue}" denied by guard`,
`Transition from "${machineInstant.state}" to "${nextState}" denied by guard`,
["Event", event],
["Context", context]
);
return machineState;
return machineInstant;
}
log(`Transition from "${machineState.value}" to "${nextStateValue}"`, ["Event", event]);
log(`Transition from "${machineInstant.state}" to "${nextState}"`, ["Event", event]);

let resolvedStateNode = R.get(definition.states, nextStateValue)!;
let resolvedStateNode = R.get(definition.states, nextState)!;

let nextEvents = R.keys(R.concat(
R.fromMaybe(resolvedStateNode.on),
R.fromMaybe(definition.on)
));
return {
value: nextStateValue,
state: nextState,
context,
event,
nextEvents,
Expand Down
132 changes: 73 additions & 59 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,46 @@ import { R } from "./extras"

export type UseStateMachine =
<D extends Machine.Definition<D>>(definition: A.InferNarrowestObject<D>) =>
[ state: A.Instantiated<Machine.State<Machine.Definition.FromTypeParamter<D>>>
, send: A.Instantiated<Machine.Send<Machine.Definition.FromTypeParamter<D>>>
]
Machine<Machine.Definition.FromTypeParameter<D>>

export const $$t = Symbol("$$t");
type $$t = typeof $$t;
export type CreateType = <T>() => { [$$t]: T }

export type Machine<D,
State = Machine.State<D>,
NextEvents =
( State extends any
? A.Get<Machine.ExitEventForState<D, State>, "type">
: never
)[]
> =
& A.Instantiated<
{ nextEvents: NextEvents
, send: Machine.Send<D>
}>
& ( State extends any
? A.Instantiated<
{ state: State
, context: Machine.Context<D>
, event: Machine.EntryEventForState<D, State>
, nextEventsT: A.Get<Machine.ExitEventForState<D, State>, "type">[]
}>
: never
)

interface MachineImpl
{ state: Machine.State.Impl
, context: Machine.Context.Impl
, event: Machine.Event.Impl
, nextEvents: Machine.Event.Impl["type"][]
, nextEventsT: Machine.Event.Impl["type"][]
, send: Machine.Send.Impl
}

export namespace Machine {
export type Impl = MachineImpl

export type Definition<
Self,
States = A.Get<Self, "states">,
Expand Down Expand Up @@ -52,8 +83,8 @@ export namespace Machine {
)

interface DefinitionImp
{ initial: StateValue.Impl
, states: R.Of<StateValue.Impl, Definition.StateNode.Impl>
{ initial: State.Impl
, states: R.Of<State.Impl, Definition.StateNode.Impl>
, on?: Definition.On.Impl
, schema?: { context?: null, events?: R.Of<Event.Impl["type"], null> }
, verbose?: boolean
Expand All @@ -64,7 +95,7 @@ export namespace Machine {
export namespace Definition {
export type Impl = DefinitionImp

export type FromTypeParamter<D> =
export type FromTypeParameter<D> =
"$$internalIsConstraint" extends keyof D
? D extends infer X ? X extends Definition<infer X> ? X : never : never
: D
Expand Down Expand Up @@ -120,7 +151,7 @@ export namespace Machine {
}

export type Transition<D, P,
TargetString = Machine.StateValue<D>,
TargetString = Machine.State<D>,
Event = { type: L.Pop<P> }
> =
| TargetString
Expand All @@ -135,12 +166,12 @@ export namespace Machine {
}

type TransitionImpl =
| State.Impl["value"]
| { target: State.Impl["value"]
| Machine.Impl["state"]
| { target: Machine.Impl["state"]
, guard?:
( parameter:
{ context: State.Impl["context"]
, event: State.Impl["event"]
{ context: Machine.Impl["context"]
, event: Machine.Impl["event"]
}
) => boolean
}
Expand All @@ -149,10 +180,10 @@ export namespace Machine {
}


export type Effect<D, P, StateValue = L.Pop<L.Popped<P>>> =
(parameter: A.Instantiated<EffectParameterForStateValue<D, StateValue>>) =>
export type Effect<D, P, State = L.Pop<L.Popped<P>>> =
(parameter: A.Instantiated<EffectParameterForState<D, State>>) =>
| void
| ((parameter: A.Instantiated<EffectCleanupParameterForStateValue<D, StateValue>>) => void)
| ((parameter: A.Instantiated<EffectCleanupParameterForState<D, State>>) => void)

type EffectImpl =
(parameter: EffectParameter.Impl) =>
Expand Down Expand Up @@ -218,15 +249,15 @@ export namespace Machine {
export type InitialEventType = "$$initial";
}

export type StateValue<D> =
export type State<D> =
keyof A.Get<D, "states">

export type InitialStateValue<D> =
export type InitialState<D> =
A.Get<D, "initial">

type StateValueImpl = string & A.Tag<"Machine.StateValue">
export namespace StateValue {
export type Impl = StateValueImpl;
type StateImpl = string & A.Tag<"Machine.State">
export namespace State {
export type Impl = StateImpl;
}

export type Context<D> =
Expand All @@ -240,7 +271,7 @@ export namespace Machine {
export type Event<D, EventsSchema = A.Get<D, ["schema", "events"], {}>> =
| O.Value<{ [T in U.Exclude<keyof EventsSchema, Definition.ExhaustiveIdentifier>]:
A.Get<EventsSchema, [T, $$t]> extends infer P
? P extends any ? O.ShallowMerge<{ type: T } & P> : never
? P extends any ? O.ShallowClean<{ type: T } & P> : never
: never
}>
| ( A.Get<EventsSchema, Definition.ExhaustiveIdentifier, false> extends true ? never :
Expand Down Expand Up @@ -271,7 +302,17 @@ export namespace Machine {
}

export namespace EffectParameter {
export interface EffectParameterForState<D, State>
extends BaseEffectParameter<D>
{ event: Machine.EntryEventForState<D, State>
}

export namespace Cleanup {
export interface ForState<D, State>
extends BaseEffectParameter<D>
{ event: Machine.ExitEventForState<D, State>
}

export type Impl = EffectParameter.Impl
}

Expand All @@ -284,14 +325,14 @@ export namespace Machine {
, setContext: SetContext.Impl
}

export interface EffectParameterForStateValue<D, StateValue>
export interface EffectParameterForState<D, State>
extends BaseEffectParameter<D>
{ event: A.Uninstantiated<Machine.EntryEventForStateValue<D, StateValue>>
{ event: A.Uninstantiated<Machine.EntryEventForState<D, State>>
}

export interface EffectCleanupParameterForStateValue<D, StateValue>
export interface EffectCleanupParameterForState<D, State>
extends BaseEffectParameter<D>
{ event: A.Uninstantiated<Machine.ExitEventForStateValue<D, StateValue>>
{ event: A.Uninstantiated<Machine.ExitEventForState<D, State>>
}

export interface BaseEffectParameter<D>
Expand All @@ -300,8 +341,8 @@ export namespace Machine {
, setContext: Machine.SetContext<D>
}

export type EntryEventForStateValue<D, StateValue> =
| ( StateValue extends InitialStateValue<D>
export type EntryEventForState<D, State> =
| ( State extends InitialState<D>
? { type: Definition.InitialEventType }
: never
)
Expand All @@ -311,27 +352,27 @@ export namespace Machine {
| O.Value<{ [S in keyof A.Get<D, "states">]:
O.Value<{ [E in keyof A.Get<D, ["states", S, "on"]>]:
A.Get<D, ["states", S, "on", E]> extends infer T
? (T extends A.String ? T : A.Get<T, "target">) extends StateValue
? (T extends A.String ? T : A.Get<T, "target">) extends State
? E
: never
: never
}>
}>
| O.Value<{ [E in keyof A.Get<D, ["on"]>]:
A.Get<D, ["on", E]> extends infer T
? (T extends A.String ? T : A.Get<T, "target">) extends StateValue
? (T extends A.String ? T : A.Get<T, "target">) extends State
? E
: never
: never
}>
}
>

export type ExitEventForStateValue<D, StateValue> =
export type ExitEventForState<D, State> =
U.Extract<
Event<D>,
{ type:
| keyof A.Get<D, ["states", StateValue, "on"], {}>
| keyof A.Get<D, ["states", State, "on"], {}>
| keyof A.Get<D, "on", {}>
}
>
Expand Down Expand Up @@ -378,34 +419,6 @@ export namespace Machine {
export namespace ContextUpdater {
export type Impl = ContextUpdaterImpl;
}

export type State<D,
Value = StateValue<D>,
NextEvents =
( Value extends any
? A.Get<ExitEventForStateValue<D, Value>, "type">
: never
)[]
> =
Value extends any
? { value: Value
, context: A.Uninstantiated<Context<D>>
, event: A.Uninstantiated<EntryEventForStateValue<D, Value>>
, nextEventsT: A.Get<ExitEventForStateValue<D, Value>, "type">[]
, nextEvents: NextEvents
}
: never

interface StateImpl
{ value: StateValue.Impl
, context: Context.Impl
, event: Event.Impl
, nextEvents: Event.Impl["type"][]
, nextEventsT: Event.Impl["type"][]
}
export namespace State {
export type Impl = StateImpl
}
}

export namespace L {
Expand Down Expand Up @@ -439,7 +452,8 @@ export namespace U {

export namespace O {
export type Value<T> = T[keyof T];
export type ShallowMerge<T> = { [K in keyof T]: T[K] } & unknown
export type ShallowClean<T> = { [K in keyof T]: T[K] }
export type OmitKey<T, K extends keyof T> = { [P in U.Exclude<keyof T, K>]: T[P] }
}

export namespace A {
Expand Down
Loading