From 3203cf27ff82172f44566e5e928ea487fee1a4a8 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 4 Aug 2021 00:11:58 +0530 Subject: [PATCH 1/5] api changes for `sendT` machine = { ...machineInstant, send } Machine.State -> Machine machineState -> machineInstant machineState.value -> machineInstant.state Machine.StateValue -> Machine.State --- src/index.ts | 54 ++++++++-------- src/types.ts | 122 ++++++++++++++++++------------------ test/index.test.ts | 89 +++++++++++++++----------- test/types.twoslash-test.ts | 102 ++++++++++++++---------------- 4 files changed, 189 insertions(+), 178 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7ab82e3..3f97a4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 })); @@ -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 + +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, @@ -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, diff --git a/src/types.ts b/src/types.ts index 5fb5090..5d1ad94 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,16 +3,45 @@ import { R } from "./extras" export type UseStateMachine = >(definition: A.InferNarrowestObject) => - [ state: Machine.State> - , send: Machine.Send> - ] + Machine> export const $$t = Symbol("$$t"); type $$t = typeof $$t; export type CreateType = () => { [$$t]: T } +export type Machine, + NextEvents = + ( State extends any + ? A.Get, "type"> + : never + )[] + > = + & { nextEvents: NextEvents + , send: Machine.Send + } + & ( State extends any + ? { state: State + , context: Machine.Context + , event: Machine.EntryEventForState + , nextEventsT: A.Get, "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, @@ -53,8 +82,8 @@ export namespace Machine { ) interface DefinitionImp - { initial: StateValue.Impl - , states: R.Of + { initial: State.Impl + , states: R.Of , on?: Definition.On.Impl , schema?: { context?: null, events?: R.Of } , verbose?: boolean @@ -121,7 +150,7 @@ export namespace Machine { } export type Transition, + TargetString = Machine.State, Event = { type: L.Pop

} > = | TargetString @@ -135,12 +164,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 } @@ -149,10 +178,10 @@ export namespace Machine { } - export type Effect>> = - (parameter: EffectParameterForStateValue) => + export type Effect>> = + (parameter: EffectParameterForState) => | void - | ((parameter: EffectCleanupParameterForStateValue) => void) + | ((parameter: EffectCleanupParameterForState) => void) type EffectImpl = (parameter: EffectParameter.Impl) => @@ -218,15 +247,15 @@ export namespace Machine { export type InitialEventType = "$$initial"; } - export type StateValue = + export type State = keyof A.Get - export type InitialStateValue = + export type InitialState = A.Get - 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 = @@ -271,15 +300,15 @@ export namespace Machine { } export namespace EffectParameter { - export interface EffectParameterForStateValue + export interface EffectParameterForState extends Base - { event: Machine.EntryEventForStateValue + { event: Machine.EntryEventForState } export namespace Cleanup { - export interface ForStateValue + export interface ForState extends Base - { event: Machine.ExitEventForStateValue + { event: Machine.ExitEventForState } export type Impl = EffectParameter.Impl @@ -300,14 +329,14 @@ export namespace Machine { , setContext: SetContext.Impl } - export interface EffectParameterForStateValue + export interface EffectParameterForState extends BaseEffectParameter - { event: Machine.EntryEventForStateValue + { event: Machine.EntryEventForState } - export interface EffectCleanupParameterForStateValue + export interface EffectCleanupParameterForState extends BaseEffectParameter - { event: Machine.ExitEventForStateValue + { event: Machine.ExitEventForState } export interface BaseEffectParameter @@ -316,8 +345,8 @@ export namespace Machine { , setContext: Machine.SetContext } - export type EntryEventForStateValue = - | ( StateValue extends InitialStateValue + export type EntryEventForState = + | ( State extends InitialState ? { type: Definition.InitialEventType } : never ) @@ -327,7 +356,7 @@ export namespace Machine { | O.Value<{ [S in keyof A.Get]: O.Value<{ [E in keyof A.Get]: A.Get extends infer T - ? (T extends A.String ? T : A.Get) extends StateValue + ? (T extends A.String ? T : A.Get) extends State ? E : never : never @@ -335,7 +364,7 @@ export namespace Machine { }> | O.Value<{ [E in keyof A.Get]: A.Get extends infer T - ? (T extends A.String ? T : A.Get) extends StateValue + ? (T extends A.String ? T : A.Get) extends State ? E : never : never @@ -343,11 +372,11 @@ export namespace Machine { } > - export type ExitEventForStateValue = + export type ExitEventForState = U.Extract< Event, { type: - | keyof A.Get + | keyof A.Get | keyof A.Get } > @@ -392,34 +421,6 @@ export namespace Machine { export namespace ContextUpdater { export type Impl = ContextUpdaterImpl; } - - export type State, - NextEvents = - ( Value extends any - ? A.Get, "type"> - : never - )[] - > = - Value extends any - ? { value: Value - , context: Context - , event: EntryEventForStateValue - , nextEventsT: A.Get, "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 { @@ -459,6 +460,7 @@ export namespace U { export namespace O { export type Value = T[keyof T]; export type ShallowClean = { [K in keyof T]: T[K] } + export type OmitKey = { [P in U.Exclude]: T[P] } } export namespace A { diff --git a/test/index.test.ts b/test/index.test.ts index 570dbcf..387229f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -32,12 +32,13 @@ describe("useStateMachine", () => { }) ); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "$$initial" }, - value: "inactive", + state: "inactive", nextEvents: ["ACTIVATE"], nextEventsT: ["ACTIVATE"], + send: expect.any(Function) }); }); @@ -57,17 +58,18 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("ACTIVATE"); + result.current.send("ACTIVATE"); }); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "ACTIVATE", }, - value: "active", + state: "active", nextEvents: ["DEACTIVATE"], nextEventsT: ["DEACTIVATE"], + send: expect.any(Function) }); }); @@ -90,17 +92,18 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("FORCE_ACTIVATE"); + result.current.send("FORCE_ACTIVATE"); }); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "FORCE_ACTIVATE", }, - value: "active", + state: "active", nextEvents: ["DEACTIVATE", "FORCE_ACTIVATE"], nextEventsT: ["DEACTIVATE", "FORCE_ACTIVATE"], + send: expect.any(Function), }); }); @@ -120,17 +123,18 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]({ type: "ACTIVATE" }); + result.current.send({ type: "ACTIVATE" }); }); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "ACTIVATE", }, - value: "active", + state: "active", nextEvents: ["DEACTIVATE"], nextEventsT: ["DEACTIVATE"], + send: expect.any(Function), }); }); @@ -152,15 +156,16 @@ describe("useStateMachine", () => { act(() => { // TypeScript won"t allow me to type "ON" because it knows it"s not a valid event // @ts-expect-error - result.current[1]("ON"); + result.current.send("ON"); }); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "$$initial" }, - value: "inactive", + state: "inactive", nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); @@ -188,17 +193,18 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("TOGGLE"); + result.current.send("TOGGLE"); }); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "TOGGLE", }, - value: "active", + state: "active", nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); it("should invoke effect callbacks", () => { @@ -227,7 +233,7 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("TOGGLE"); + result.current.send("TOGGLE"); }); expect(entry.mock.calls.length).toBe(2); @@ -260,14 +266,15 @@ describe("useStateMachine", () => { }) ); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "TOGGLE", }, - value: "active", + state: "active", nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); @@ -295,7 +302,7 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]({ type: "ACTIVATE", number: 10 }); + result.current.send({ type: "ACTIVATE", number: 10 }); }); expect(effect.mock.calls[0][0]["event"]).toStrictEqual({ type: "ACTIVATE", number: 10 }); }); @@ -352,16 +359,17 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("TOGGLE"); + result.current.send("TOGGLE"); }); expect(guard).toHaveBeenCalled(); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "$$initial" }, - value: "inactive", + state: "inactive", nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); @@ -388,18 +396,19 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("TOGGLE"); + result.current.send("TOGGLE"); }); expect(guard).toHaveBeenCalled(); - expect(result.current[0]).toStrictEqual({ + expect(result.current).toStrictEqual({ context: undefined, event: { type: "TOGGLE", }, - value: "active", + state: "active", nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); }); @@ -420,12 +429,13 @@ describe("useStateMachine", () => { }) ); - expect(result.current[0]).toStrictEqual({ - value: "inactive", + expect(result.current).toStrictEqual({ + state: "inactive", context: { foo: "bar" }, event: { type: "$$initial" }, nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); @@ -453,12 +463,13 @@ describe("useStateMachine", () => { }) ); - expect(result.current[0]).toStrictEqual({ - value: "inactive", + expect(result.current).toStrictEqual({ + state: "inactive", context: { foo: "bar" }, event: { type: "$$initial" }, nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); @@ -482,17 +493,18 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("TOGGLE"); + result.current.send("TOGGLE"); }); - expect(result.current[0]).toStrictEqual({ - value: "active", + expect(result.current).toStrictEqual({ + state: "active", context: { toggleCount: 1 }, event: { type: "TOGGLE", }, nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); it("should update context on exit", () => { @@ -515,17 +527,18 @@ describe("useStateMachine", () => { ); act(() => { - result.current[1]("TOGGLE"); + result.current.send("TOGGLE"); }); - expect(result.current[0]).toStrictEqual({ - value: "active", + expect(result.current).toStrictEqual({ + state: "active", context: { toggleCount: 1 }, event: { type: "TOGGLE", }, nextEvents: ["TOGGLE"], nextEventsT: ["TOGGLE"], + send: expect.any(Function), }); }); }); @@ -592,7 +605,7 @@ describe("useStateMachine", () => { if (result.all[0] instanceof Error) throw result.all[0]; else if (result.all[1] instanceof Error) throw result.all[1]; - else expect(result.all[0][1]).toBe(result.all[1][1]); + else expect(result.all[0].send).toBe(result.all[1].send); }); }); }); diff --git a/test/types.twoslash-test.ts b/test/types.twoslash-test.ts index f45be5e..7aa0954 100644 --- a/test/types.twoslash-test.ts +++ b/test/types.twoslash-test.ts @@ -432,13 +432,13 @@ describe("Machine.Definition", () => { }) it("doesn't infer narrowest", () => { - let [state] = useStateMachine({ + let machine = useStateMachine({ schema: {}, context: { foo: "hello" }, initial: "a", states: { a: {} } }) - A.test(A.areEqual()) + A.test(A.areEqual()) }) }) @@ -1123,8 +1123,8 @@ describe("Machine.Definition", () => { }) }) -describe("UseStateMachine", () => { - let [state, send] = useStateMachine({ +describe("Machine", () => { + let machine = useStateMachine({ schema: { events: { X: t<{ foo: number }>(), @@ -1152,46 +1152,40 @@ describe("UseStateMachine", () => { } }) - describe("Machine.State", () => { - A.test(A.areEqual< - typeof state, - | { value: "a" + A.test(A.areEqual< + typeof machine, + & { nextEvents: ("X" | "Y" | "Z")[] + , send: + { ( sendable: + | { type: "X", foo: number } + | { type: "Y", bar?: number } + | { type: "Z" } + ): void + , ( sendable: + | "Y" + | "Z" + ): void + } + } + & ( { state: "a" , context: { foo?: number } , event: | { type: "$$initial" } | { type: "Y", bar?: number } | { type: "Z" } , nextEventsT: ("X" | "Z")[] - , nextEvents: ("X" | "Y" | "Z")[] } - | { value: "b" + | { state: "b" , context: { foo?: number } , event: { type: "X", foo: number } , nextEventsT: ("Y" | "Z")[] - , nextEvents: ("X" | "Y" | "Z")[] } - >()) - }) - - describe("Machine.Send", () => { - A.test(A.areEqual< - typeof send, - { ( sendable: - | { type: "X", foo: number } - | { type: "Y", bar?: number } - | { type: "Z" } - ): void - , ( sendable: - | "Y" - | "Z" - ): void - } - >()) - }) + ) + >()) }) describe("Machine.Definition.FromTypeParamter", () => { - let [state, send] = useStateMachine({ + let machine = useStateMachine({ context: { toggleCount: 0 }, initial: "inactive", states: { @@ -1208,33 +1202,31 @@ describe("Machine.Definition.FromTypeParamter", () => { }) A.test(A.areEqual< - typeof state, - | { value: "inactive" - , context: { toggleCount: number } - , event: - | { type: "$$initial" } - | { type: "TOGGLE" } - , nextEventsT: "TOGGLE"[] - , nextEvents: "TOGGLE"[] - } - | { value: "active" - , context: { toggleCount: number } - , event: { type: "TOGGLE" } - , nextEventsT: "TOGGLE"[] - , nextEvents: "TOGGLE"[] + typeof machine, + & { nextEvents: "TOGGLE"[] + , send: + { (sendable: { type: "TOGGLE" }): void + , (sendable: "TOGGLE"): void + } } - >()) - - A.test(A.areEqual< - typeof send, - { (sendable: { type: "TOGGLE" }): void - , (sendable: "TOGGLE"): void - } + & ( { state: "inactive" + , context: { toggleCount: number } + , event: + | { type: "$$initial" } + | { type: "TOGGLE" } + , nextEventsT: "TOGGLE"[] + } + | { state: "active" + , context: { toggleCount: number } + , event: { type: "TOGGLE" } + , nextEventsT: "TOGGLE"[] + } + ) >()) }) describe("fix(Machine.State['nextEvents']): only normalize don't widen", () => { - let [state] = useStateMachine({ + let machine = useStateMachine({ schema: { events: { Y: t<{}>() } }, @@ -1246,11 +1238,11 @@ describe("fix(Machine.State['nextEvents']): only normalize don't widen", () => { } }) - A.test(A.areEqual()) + A.test(A.areEqual()) }) describe("workaround for #65", () => { - let [_, send] = useStateMachine({ + let machine = useStateMachine({ schema: { events: { A: t<{ value: string }>() @@ -1267,7 +1259,7 @@ describe("workaround for #65", () => { }) A.test(A.areEqual< - typeof send, + typeof machine.send, { (sendable: { type: "A", value: string } | { type: "B" }): void , (sendable: "B"): void } From 49ff6f6e34fd37a095b781086aecd8a00cea33eb Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 4 Aug 2021 00:14:08 +0530 Subject: [PATCH 2/5] make test logger add linebreaks --- test/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/index.test.ts b/test/index.test.ts index 387229f..7980817 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -6,7 +6,7 @@ const logger: Console["log"] = (...xs) => log += xs.reduce( (a, x) => a + (typeof x === "string" ? x : JSON.stringify(x)), "" - ) + ) + "\n" const clearLog = () => log = ""; From 32048925763e3e0ebecf4e9cbd800cd98e37bceb Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Wed, 4 Aug 2021 06:38:20 +0530 Subject: [PATCH 3/5] heirarchical poc --- src/core/index.test.ts | 32 ++++++ src/core/index.ts | 248 +++++++++++++++++++++++++++++++++++++++++ src/extras.ts | 30 ++++- src/index.ts | 2 +- src/types.ts | 26 ++--- tsconfig.json | 1 + 6 files changed, 320 insertions(+), 19 deletions(-) create mode 100644 src/core/index.test.ts create mode 100644 src/core/index.ts diff --git a/src/core/index.test.ts b/src/core/index.test.ts new file mode 100644 index 0000000..c1d8628 --- /dev/null +++ b/src/core/index.test.ts @@ -0,0 +1,32 @@ +import { createMachine } from "." + +describe("createMachine", () => { + it("works", () => { + let machine = createMachine({ + initial: "b", + states: { + a: { + initial: "a1", + states: { + a1: {}, + a2: {} + } + }, + b: { + initial: "b1", + states: { + b1: {}, + b2: {} + }, + on: { + X: "a2" + } + } + } + } as any) + + expect(machine.state).toBe("b.b1") + machine.send("X" as any); + expect(machine.state).toBe("a.a2") + }) +}) \ No newline at end of file diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..57dda48 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,248 @@ +import { R, use } from "../extras"; +import { F, Machine as MachineT, U } from "../types"; + +const $$root = "$$root" as MachineT.StateIdentifier.Impl; +const $$initial = "$$initial" as MachineT.Event.Impl["type"]; + +export const createMachine = (definition: MachineT.Definition.Impl) => { + let rootNode = Node.from(definition); + let context = definition.context!; + let lastEvent: MachineT.Event.Impl; + let eventQueue = [{ type: $$initial }] as MachineT.Event.Impl[]; + let contextUpdaterQueue = [] as MachineT.ContextUpdater.Impl[] + let effects = new Map(Node.effectEntries(rootNode, $$root)) + let effectCleanups = new Map< + MachineT.StateIdentifier.Impl, + (p: MachineT.EffectParameter.Cleanup.Impl) => void + >(); + let isTransiting = false; + let listeners = [] as (() => void)[]; + + const send = (sendable: MachineT.Sendable.Impl) => { + eventQueue.push(typeof sendable === "string" ? { type: sendable } : sendable); + if (!isTransiting) transition(); + } + + const setContext = (updater: MachineT.ContextUpdater.Impl) => { + contextUpdaterQueue.push(updater) + return { send } + } + + const pureParameter = () => ({ context, event: lastEvent }); + const effectParameter = () => ({ ...pureParameter(), setContext, send }); + + const transition = () => { + isTransiting = true; + let event = eventQueue.shift(); + if (!event) throw new Error("Invariant: `transition` called with no events in queue"); + lastEvent = event; + + let [newRootNode, entries, exits] = Node.transition(rootNode, pureParameter()) + + for (let identifier of exits.slice().reverse()) { + let cleanup = effectCleanups.get(identifier); + if (!cleanup) throw new Error(`Invariant: Expected a cleanup for "${identifier}", found none`); + cleanup(effectParameter()); + } + + for (let identifier of entries) { + let effect = effects.get(identifier) + if (!effect) throw new Error(`Invariant: Expected an effect for "${identifier}", found none`); + let cleanup = effect(effectParameter()); + effectCleanups.set(identifier, cleanup); + } + + rootNode = newRootNode; + context = contextUpdaterQueue.reduce((c, f) => f(c), context) + if (eventQueue.length > 0) { + transition(); + } else { + isTransiting = false; + for (let l of listeners) l(); + } + } + transition(); + + return { + get state() { return Node.state(rootNode).join(".") }, + get context() { return context }, + get event() { return lastEvent }, + send, + subscribe: (f: () => void) => { + listeners.push(f) + return () => void listeners.splice(listeners.indexOf(f), 1); + } + } +} +type PureParameter = + { context: MachineT.Context.Impl + , event: MachineT.Event.Impl + } + +type Node = + & ( { state: MachineT.StateIdentifier.Impl + , initialState: MachineT.StateIdentifier.Impl + } + | { state: undefined + , initialState: undefined + } + ) + & { children: R.Of + , definition: MachineT.Definition.StateNode.Impl + } +type AtomicNode = Node & { state: undefined, initialState: undefined } +type CompoundNode = U.Exclude +namespace Node { + export const from = (definition: MachineT.Definition.StateNode.Impl): Node => + ({ + ...( + definition.initial === undefined + ? { state: undefined, initialState: undefined } + : { state: definition.initial, initialState: definition.initial } + ), + children: R.map(R.fromMaybe(definition.states), from), + definition, + }) + + export const state = (node: Node): State => + isAtomic(node) ? [] : + [node.state, ...state(currentChild(node))] + + export const initialState = (node: Node): State => + isAtomic(node) ? [] : + [node.initialState, ...state(initialChild(node))] + + export const transition = (node: Node, pureParameter: PureParameter): [Node, State, State] => { + let [entries, exits] = Node.entriesAndExitsForEvent(node, pureParameter) + return [Node.doEntries(Node.doExits(node), entries), entries, exits] + } + + export const entriesAndExitsForEvent = (node: Node, pureParameter: PureParameter) => { + let nextStateIdentifier = Node.nextStateIdentifier(node, $$root, pureParameter); + if (!nextStateIdentifier) return [[], []]; + + let nextState = Node.nextStateFromNextStateIdentifier(node, nextStateIdentifier); + if (!nextState) throw new Error(`Invariant: Could not resolve path for ${nextStateIdentifier}`); + let currentState = pureParameter.event.type === $$initial ? [] : state(node) + + return State.entriesAndExits(currentState, nextState); + } + + export const nextStateFromNextStateIdentifier = ( + node: Node, + nextStateIdentifier: MachineT.StateIdentifier.Impl + ): State | undefined => + isAtomic(node) + ? undefined : + use(R.find(node.children, (_, k) => k === nextStateIdentifier)) + .as(foundNode => + foundNode ? [nextStateIdentifier, ...initialState(foundNode)] : + R.find( + R.map(node.children, (n, k) => + use(nextStateFromNextStateIdentifier(n, nextStateIdentifier)) + .as(foundState => + !foundState ? undefined : [k, ...foundState] + ) + ), + Boolean + ) + ) + + export const nextStateIdentifier = ( + node: Node, + identifier: MachineT.StateIdentifier.Impl, + pureParameter: PureParameter + ): MachineT.StateIdentifier.Impl | undefined => + isAtomic(node) + ? identifier : + pureParameter.event.type === $$initial + ? nextStateIdentifier(initialChild(node), node.initialState, pureParameter) : + use( + R.get( + R.fromMaybe(node.definition.on), + pureParameter.event.type + ) + ).as(rootTransition => + !rootTransition ? nextStateIdentifier(currentChild(node), node.state, pureParameter) : + typeof rootTransition === "string" ? rootTransition : + !rootTransition.guard ? rootTransition.target : + rootTransition.guard(pureParameter) ? rootTransition.target : + undefined + ) + + export const doExits = (node: Node): Node => + isAtomic(node) ? node : + ({ + ...node, + state: node.initialState, + children: R.map(node.children, (n, i) => i !== node.state ? n : doExits(n)) + }) + + export const doEntries = (node: Node, entries: State): Node => { + if (isAtomic(node)) { + if (entries.length > 0) { + throw new Error("Invariant: Attempt to enter states deeper than possible") + } + return node; + } + let [nextIdentifier, ...tailState] = entries as [ + MachineT.StateIdentifier.Impl?, + ...MachineT.StateIdentifier.Impl[] + ] + if (!nextIdentifier) { + throw new Error("Invariant: Attempt to enter states shallower than possible") + } + return { + ...node, + state: nextIdentifier, + children: R.map(node.children, (n, i) => i !== nextIdentifier ? n : doEntries(n, tailState)) + } + } + + export const currentChild = (compoundNode: CompoundNode) => + R.get(compoundNode.children, compoundNode.state)! + + export const initialChild = (compoundNode: CompoundNode) => + R.get(compoundNode.children, compoundNode.initialState)! + + export const isAtomic = (node: Node): node is AtomicNode => + R.isEmpty(node.children) + + + export const effectEntries = ( + node: Node, + identifier: MachineT.StateIdentifier.Impl + ): [MachineT.StateIdentifier.Impl, Effect][] => + [ + [identifier, Effect.from(node.definition.effect)], + ...( + isAtomic(node) ? []: + R.reduce( + node.children, + (es, v, k) => [...es, ...effectEntries(v, k)], + [] as [MachineT.StateIdentifier.Impl, Effect][] + ) + ) + ] +} + +type State = MachineT.StateIdentifier.Impl[]; +namespace State { + export const entriesAndExits = (from: State, to: State): [State, State] => + from.length === 0 ? [to, []] : + to.length === 0 ? [[], from] : + from[1] === to[1] ? entriesAndExits(from.slice(1), to.slice(1)) : + [to, from] +} + +type Effect = F.Call +namespace Effect { + export const from = (effect?: MachineT.Definition.Effect.Impl) => + (p: MachineT.EffectParameter.Impl) => { + let cleanup = effect?.(p) + + return (p: MachineT.EffectParameter.Cleanup.Impl) => { + cleanup?.(p); + } + } +} \ No newline at end of file diff --git a/src/extras.ts b/src/extras.ts index d09cfe7..9529a70 100644 --- a/src/extras.ts +++ b/src/extras.ts @@ -1,11 +1,28 @@ import { useRef } from "react"; export const R = { - get: (r: R, k: R.Key) => (r as any)[k] as R.Value | undefined, - concat: (r1: R1, r2: R2) => - (({ ...r1, ...r2 } as any) as R.Concat), - fromMaybe: (r: R | undefined) => r ?? ({} as R), - keys: (r: R) => Object.keys(r) as R.Key[] + get: (r: T, k: R.Key) => + (r as any)[k] as R.Value | undefined, + + concat: (r1: T, r2: U) => + (({ ...r1, ...r2 } as any) as R.Concat), + + fromMaybe: (r: T | undefined) => + r ?? ({} as T), + + keys: (r: T) => + Object.keys(r) as R.Key[], + + map: (r: T, f: (v: R.Value, k: R.Key) => Uv) => + Object.fromEntries(Object.entries(r).map(([k, v]) => [k, f(v, k)])) as R.Of, Uv>, + + find: (r: T, f: (v: R.Value, k: R.Key) => boolean) => + Object.entries(r).find(([k, v]) => f(v, k))?.[1] as R.Value | undefined, + + reduce: (r: T, f: (a: A, v: R.Value, k: R.Key) => A, seed: A) => + Object.entries(r).reduce((a, [k, v]) => f(a, v, k), seed) as A, + + isEmpty: (r: R.Unknown) => R.keys(r).length === 0 }; const $$K = Symbol("R.$$K"); @@ -34,3 +51,6 @@ export const useConstant = (compute: () => T): T => { export const assertNever = (_value: never): never => { throw new Error("Invariant: assertNever was called"); }; + +// yes this codebase is written by a hipster-fp-wannabe, apologies. +export const use = (t: T) => ({ as: (f: (t: T) => R) => f(t) }) diff --git a/src/index.ts b/src/index.ts index 3f97a4f..86af866 100644 --- a/src/index.ts +++ b/src/index.ts @@ -80,7 +80,7 @@ const createReducer = (definition: Machine.Definition.Impl) => { if (resolvedTransition.guard === undefined) return [resolvedTransition.target]; if (resolvedTransition.guard({ context, event })) return [resolvedTransition.target]; return [resolvedTransition.target, true] - })() as [Machine.State.Impl, true?] + })() as [Machine.StateIdentifier.Impl, true?] if (didGuardDeny) { log( diff --git a/src/types.ts b/src/types.ts index 5d1ad94..4b6ad24 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,7 +11,7 @@ export type CreateType = () => { [$$t]: T } export type Machine, + State = Machine.StateIdentifier, NextEvents = ( State extends any ? A.Get, "type"> @@ -31,7 +31,7 @@ export type Machine - , on?: Definition.On.Impl + interface DefinitionImpl extends Definition.StateNode.Impl + { on?: Definition.On.Impl , schema?: { context?: null, events?: R.Of } , verbose?: boolean , console?: Console @@ -92,7 +90,7 @@ export namespace Machine { } export namespace Definition { - export type Impl = DefinitionImp + export type Impl = DefinitionImpl export type FromTypeParamter = "$$internalIsConstraint" extends keyof D @@ -105,7 +103,9 @@ export namespace Machine { } interface StateNodeImpl - { on?: On.Impl + { initial?: StateIdentifier.Impl + , states?: R.Of + , on?: On.Impl , effect?: Effect.Impl } export namespace StateNode { @@ -150,7 +150,7 @@ export namespace Machine { } export type Transition, + TargetString = Machine.StateIdentifier, Event = { type: L.Pop

} > = | TargetString @@ -247,15 +247,15 @@ export namespace Machine { export type InitialEventType = "$$initial"; } - export type State = + export type StateIdentifier = keyof A.Get export type InitialState = A.Get - type StateImpl = string & A.Tag<"Machine.State"> - export namespace State { - export type Impl = StateImpl; + type StateIdentifierImpl = string & A.Tag<"Machine.StateIdentifier"> + export namespace StateIdentifier { + export type Impl = StateIdentifierImpl; } export type Context = diff --git a/tsconfig.json b/tsconfig.json index 85dfcd2..3460d11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "include": ["src", "test"], "compilerOptions": { "module": "esnext", + "target": "ES2015", "lib": ["dom", "esnext"], "importHelpers": true, "declaration": true, From 1d10bb18abc28a9da77fcbba963193512f167489 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Thu, 5 Aug 2021 05:06:30 +0530 Subject: [PATCH 4/5] renaming node.state ->node.current node.initialState -> node.initial Node.nextStateFromNextStateIdentifier -> Node.nextState Node.entriesAndExitsForEvent -> node.entriesAndExits --- src/core/index.ts | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index 57dda48..be5b41b 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -80,25 +80,25 @@ type PureParameter = } type Node = - & ( { state: MachineT.StateIdentifier.Impl - , initialState: MachineT.StateIdentifier.Impl + & ( { current: MachineT.StateIdentifier.Impl + , initial: MachineT.StateIdentifier.Impl } - | { state: undefined - , initialState: undefined + | { current: undefined + , initial: undefined } ) & { children: R.Of , definition: MachineT.Definition.StateNode.Impl } -type AtomicNode = Node & { state: undefined, initialState: undefined } +type AtomicNode = Node & { current: undefined, initial: undefined } type CompoundNode = U.Exclude namespace Node { export const from = (definition: MachineT.Definition.StateNode.Impl): Node => ({ ...( definition.initial === undefined - ? { state: undefined, initialState: undefined } - : { state: definition.initial, initialState: definition.initial } + ? { current: undefined, initial: undefined } + : { current: definition.initial, initial: definition.initial } ), children: R.map(R.fromMaybe(definition.states), from), definition, @@ -106,29 +106,29 @@ namespace Node { export const state = (node: Node): State => isAtomic(node) ? [] : - [node.state, ...state(currentChild(node))] + [node.current, ...state(currentChild(node))] export const initialState = (node: Node): State => isAtomic(node) ? [] : - [node.initialState, ...state(initialChild(node))] + [node.initial, ...state(initialChild(node))] export const transition = (node: Node, pureParameter: PureParameter): [Node, State, State] => { - let [entries, exits] = Node.entriesAndExitsForEvent(node, pureParameter) + let [entries, exits] = Node.entriesAndExits(node, pureParameter) return [Node.doEntries(Node.doExits(node), entries), entries, exits] } - export const entriesAndExitsForEvent = (node: Node, pureParameter: PureParameter) => { + export const entriesAndExits = (node: Node, pureParameter: PureParameter) => { let nextStateIdentifier = Node.nextStateIdentifier(node, $$root, pureParameter); if (!nextStateIdentifier) return [[], []]; - let nextState = Node.nextStateFromNextStateIdentifier(node, nextStateIdentifier); + let nextState = Node.nextState(node, nextStateIdentifier); if (!nextState) throw new Error(`Invariant: Could not resolve path for ${nextStateIdentifier}`); let currentState = pureParameter.event.type === $$initial ? [] : state(node) return State.entriesAndExits(currentState, nextState); } - export const nextStateFromNextStateIdentifier = ( + export const nextState = ( node: Node, nextStateIdentifier: MachineT.StateIdentifier.Impl ): State | undefined => @@ -139,7 +139,7 @@ namespace Node { foundNode ? [nextStateIdentifier, ...initialState(foundNode)] : R.find( R.map(node.children, (n, k) => - use(nextStateFromNextStateIdentifier(n, nextStateIdentifier)) + use(nextState(n, nextStateIdentifier)) .as(foundState => !foundState ? undefined : [k, ...foundState] ) @@ -156,14 +156,14 @@ namespace Node { isAtomic(node) ? identifier : pureParameter.event.type === $$initial - ? nextStateIdentifier(initialChild(node), node.initialState, pureParameter) : + ? nextStateIdentifier(initialChild(node), node.initial, pureParameter) : use( R.get( R.fromMaybe(node.definition.on), pureParameter.event.type ) ).as(rootTransition => - !rootTransition ? nextStateIdentifier(currentChild(node), node.state, pureParameter) : + !rootTransition ? nextStateIdentifier(currentChild(node), node.current, pureParameter) : typeof rootTransition === "string" ? rootTransition : !rootTransition.guard ? rootTransition.target : rootTransition.guard(pureParameter) ? rootTransition.target : @@ -174,8 +174,8 @@ namespace Node { isAtomic(node) ? node : ({ ...node, - state: node.initialState, - children: R.map(node.children, (n, i) => i !== node.state ? n : doExits(n)) + current: node.initial, + children: R.map(node.children, (n, i) => i !== node.current ? n : doExits(n)) }) export const doEntries = (node: Node, entries: State): Node => { @@ -194,16 +194,16 @@ namespace Node { } return { ...node, - state: nextIdentifier, + current: nextIdentifier, children: R.map(node.children, (n, i) => i !== nextIdentifier ? n : doEntries(n, tailState)) } } export const currentChild = (compoundNode: CompoundNode) => - R.get(compoundNode.children, compoundNode.state)! + R.get(compoundNode.children, compoundNode.current)! export const initialChild = (compoundNode: CompoundNode) => - R.get(compoundNode.children, compoundNode.initialState)! + R.get(compoundNode.children, compoundNode.initial)! export const isAtomic = (node: Node): node is AtomicNode => R.isEmpty(node.children) From d3860f25bb9d6ebd3d90c38fb00eac281cbc6da9 Mon Sep 17 00:00:00 2001 From: Devansh Jethmalani Date: Thu, 5 Aug 2021 05:16:26 +0530 Subject: [PATCH 5/5] refactor: use throw expressions --- src/core/index.ts | 69 ++++++++++++++++++++++++----------------------- src/extras.ts | 1 + 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index be5b41b..982ade2 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,4 @@ -import { R, use } from "../extras"; +import { doThrow, R, use } from "../extras"; import { F, Machine as MachineT, U } from "../types"; const $$root = "$$root" as MachineT.StateIdentifier.Impl; @@ -112,21 +112,26 @@ namespace Node { isAtomic(node) ? [] : [node.initial, ...state(initialChild(node))] - export const transition = (node: Node, pureParameter: PureParameter): [Node, State, State] => { - let [entries, exits] = Node.entriesAndExits(node, pureParameter) - return [Node.doEntries(Node.doExits(node), entries), entries, exits] - } - - export const entriesAndExits = (node: Node, pureParameter: PureParameter) => { - let nextStateIdentifier = Node.nextStateIdentifier(node, $$root, pureParameter); - if (!nextStateIdentifier) return [[], []]; + export const transition = (node: Node, pureParameter: PureParameter): [Node, State, State] => + use(Node.entriesAndExits(node, pureParameter)) + .as(([entries, exits]) => + [Node.doEntries(Node.doExits(node), entries), entries, exits] + ) - let nextState = Node.nextState(node, nextStateIdentifier); - if (!nextState) throw new Error(`Invariant: Could not resolve path for ${nextStateIdentifier}`); - let currentState = pureParameter.event.type === $$initial ? [] : state(node) - - return State.entriesAndExits(currentState, nextState); - } + export const entriesAndExits = (node: Node, pureParameter: PureParameter): [State, State] => + use(Node.nextStateIdentifier(node, $$root, pureParameter)) + .as(nextStateIdentifier => + !nextStateIdentifier ? [[], []] : + use(Node.nextState(node, nextStateIdentifier)) + .as(nextState => + !nextState + ? doThrow(new Error(`Invariant: Could not resolve path for ${nextStateIdentifier}`)) + : State.entriesAndExits( + pureParameter.event.type === $$initial ? [] : state(node), + nextState + ) + ) + ) export const nextState = ( node: Node, @@ -178,26 +183,24 @@ namespace Node { children: R.map(node.children, (n, i) => i !== node.current ? n : doExits(n)) }) - export const doEntries = (node: Node, entries: State): Node => { - if (isAtomic(node)) { - if (entries.length > 0) { - throw new Error("Invariant: Attempt to enter states deeper than possible") - } - return node; - } - let [nextIdentifier, ...tailState] = entries as [ + export const doEntries = (node: Node, entries: State): Node => + isAtomic(node) + ? entries.length > 0 + ? doThrow(new Error("Invariant: Attempt to enter states deeper than possible")) + : node : + use(entries as [ MachineT.StateIdentifier.Impl?, ...MachineT.StateIdentifier.Impl[] - ] - if (!nextIdentifier) { - throw new Error("Invariant: Attempt to enter states shallower than possible") - } - return { - ...node, - current: nextIdentifier, - children: R.map(node.children, (n, i) => i !== nextIdentifier ? n : doEntries(n, tailState)) - } - } + ]) + .as(([nextIdentifier, ...tailState]) => + !nextIdentifier + ? doThrow(new Error("Invariant: Attempt to enter states shallower than possible")) + : ({ + ...node, + current: nextIdentifier, + children: R.map(node.children, (n, i) => i !== nextIdentifier ? n : doEntries(n, tailState)) + }) + ) export const currentChild = (compoundNode: CompoundNode) => R.get(compoundNode.children, compoundNode.current)! diff --git a/src/extras.ts b/src/extras.ts index 9529a70..d3e726d 100644 --- a/src/extras.ts +++ b/src/extras.ts @@ -54,3 +54,4 @@ export const assertNever = (_value: never): never => { // yes this codebase is written by a hipster-fp-wannabe, apologies. export const use = (t: T) => ({ as: (f: (t: T) => R) => f(t) }) +export const doThrow = (error: unknown): never => { throw error } \ No newline at end of file