diff --git a/package-lock.json b/package-lock.json index 1856ce7..d0b6942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "solid-trpc": "^0.1.0-ssr.1", "solidstart-17": "file:", "undici": "5.15.1", + "xstate": "^4.35.2", "zod": "^3.20.2" }, "devDependencies": { @@ -7937,6 +7938,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xstate": { + "version": "4.35.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.35.2.tgz", + "integrity": "sha512-5X7EyJv5OHHtGQwN7DsmCAbSnDs3Mxl1cXQ4PVaLwi+7p/RRapERnd1dFyHjYin+KQoLLfuXpl1dPBThgyIGNg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -12705,6 +12715,7 @@ "typescript": "^4.9.4", "undici": "5.15.1", "vite": "^3.1.8", + "xstate": "*", "zod": "^3.20.2" }, "dependencies": { @@ -17959,6 +17970,11 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "xstate": { + "version": "4.35.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.35.2.tgz", + "integrity": "sha512-5X7EyJv5OHHtGQwN7DsmCAbSnDs3Mxl1cXQ4PVaLwi+7p/RRapERnd1dFyHjYin+KQoLLfuXpl1dPBThgyIGNg==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -18607,6 +18623,11 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "xstate": { + "version": "4.35.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.35.2.tgz", + "integrity": "sha512-5X7EyJv5OHHtGQwN7DsmCAbSnDs3Mxl1cXQ4PVaLwi+7p/RRapERnd1dFyHjYin+KQoLLfuXpl1dPBThgyIGNg==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 614c5ff..b39df5c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "solid-trpc": "^0.1.0-ssr.1", "solidstart-17": "file:", "undici": "5.15.1", + "xstate": "^4.35.2", "zod": "^3.20.2" }, "engines": { diff --git a/src/deus-ex/checkoutMachine.ts b/src/deus-ex/checkoutMachine.ts new file mode 100644 index 0000000..f137800 --- /dev/null +++ b/src/deus-ex/checkoutMachine.ts @@ -0,0 +1,96 @@ +import { ActorRefFrom, interpret } from "xstate"; +import { actions } from "xstate"; +import { assign } from "xstate"; +import { createMachine } from "xstate" + +export const checkoutMachine = createMachine({ + "id": "Checkout", + context: { + vipCode: '', + }, + "initial": "cart", + "states": { + "cart": { + "on": { + "CHECKOUT": { + "target": "shipping" + }, + "PAYPAL": { + "target": "payment" + }, + "ENTER_VIP_CODE": { + "actions": "setVipCode" + // "actions": [ + // "setVipCode" + // ] + } + } + }, + "shipping": { + "on": { + "NEXT": { + "target": "contact" + }, + "SELECT_SHIPPING_ADDRESS": {}, + "SELECT_SHIPPING_METHOD": {} + } + }, + "contact": { + "on": { + "NEXT": { + "target": "payment" + }, + "ENTER_EMAIL": {}, + "ENTER_MOBILE": {} + } + }, + "payment": { + "on": { + "ORDER": { + "target": "confirmation" + }, + "SELECT_PAYMENT_METHOD": {}, + "AGREE": {} + } + }, + "confirmation": { + "type": "final" + } + }, + // actions: { + // setVipCode: (context, event) => { + // console.log(`counterMachine.ts setVipCode context: ${JSON.stringify(context)} `); + // console.log(`counterMachine.ts setVipCode event: ${JSON.stringify(event)} `); + // console.log(`counterMachine.ts incrementCounter event: ${JSON.stringify(event)} `); + + // console.log(`counterMachine.ts incrementCounter context: ${JSON.stringify(context)} `); + + // context.count++; + // context.incrementCount++; + // }, + // }, + + schema: { + // eslint-disable-next-line @typescript-eslint/ban-types + context: {} as { + vipCode: string; + + }, + events: {} as { "type": "CHECKOUT" } | { "type": "NEXT" } | { "type": "ORDER" } | { "type": "PAYPAL" } | { "type": "ENTER_VIP_CODE" } | { "type": "SELECT_SHIPPING_ADDRESS" } | { "type": "SELECT_SHIPPING_METHOD" } | { "type": "ENTER_EMAIL" } | { "type": "ENTER_MOBILE" } | { "type": "SELECT_PAYMENT_METHOD" } | { "type": "AGREE" } + }, + predictableActionArguments: true, + preserveActionOrder: true, + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + tsTypes: {} as import("./checkoutMachine.typegen").Typegen0, + + + +}) + + + +// export default checkoutMachine + +export const checkoutMachineActor = interpret(checkoutMachine).start() + + diff --git a/src/deus-ex/checkoutMachine.typegen.ts b/src/deus-ex/checkoutMachine.typegen.ts new file mode 100644 index 0000000..eef50de --- /dev/null +++ b/src/deus-ex/checkoutMachine.typegen.ts @@ -0,0 +1,33 @@ + + // This file was automatically generated. Edits will be overwritten + + export interface Typegen0 { + '@@xstate/typegen': true; + internalEvents: { + "xstate.init": { type: "xstate.init" }; + }; + invokeSrcNameMap: { + + }; + missingImplementations: { + actions: "setVipCode"; + delays: never; + guards: never; + services: never; + }; + eventsCausingActions: { + "setVipCode": "ENTER_VIP_CODE"; + }; + eventsCausingDelays: { + + }; + eventsCausingGuards: { + + }; + eventsCausingServices: { + + }; + matchesStates: "cart" | "confirmation" | "contact" | "payment" | "shipping"; + tags: never; + } + \ No newline at end of file diff --git a/src/routes/xstate/index.tsx b/src/routes/xstate/index.tsx new file mode 100644 index 0000000..99a2563 --- /dev/null +++ b/src/routes/xstate/index.tsx @@ -0,0 +1,128 @@ +import type { VoidComponent } from "solid-js" +import { Title } from "solid-start" +import type { ActorRefFrom} from "xstate" +import { interpret } from "xstate" +import Layout from "~/layouts/Layout" +import { checkoutMachine } from "~/deus-ex/checkoutMachine" +import { checkoutMachineActor } from "~/deus-ex/checkoutMachine" +import { useMachine } from "./useMachine" +import { createSignal } from "solid-js" +import XCOMInfoPanel from "~/components/XCOMInfoPanel" + +// const actorHoisted = interpret(checkoutMachine).start() + +// console.log(`xstate | index.tsx | actorHoisted`, actorHoisted) + + +type CheckoutActor = ActorRefFrom + + + +const XStatePage: VoidComponent = () => { + + const actorPage = useMachine(checkoutMachine) + + const [theState, setTheState] = createSignal(checkoutMachineActor.initialState) + + // const stateValue = () => state().value.toString() + + + + checkoutMachineActor.onTransition((newState) => { + console.log(`xstate | index.tsx | actorHoisted.onTransition | newState`, newState) + // console.log(`\nxstate | index.tsx | actorHoisted.onTransition | newState.context\n`, newState.context) + setTheState(newState) + }) + + + + + return ( + + XState +

XState

+ +

theState().value:{theState().value.toString()}

+ + + {theState().value === "cart" && ( + +

Cart

+ + +
+ )} + + {theState().value === "shipping" && ( + +

Shipping

+ + { + const fixTarget = e.target as HTMLInputElement + + console.log(`\nxstate | index.tsx | fixTarget.value\n`, fixTarget.value) + checkoutMachineActor.send({ + // actorPage.send({ + type: "ENTER_VIP_CODE", + value: fixTarget.value + }) + } + } + /> + +

asdf{theState().context.vipCode}qwer

+ +
+ )} + + {theState().value === "contact" && ( + +

Contact

+
+ )} + + {theState().value === "payment" && ( + +

Payment

+
+ )} + + {theState().value === "confirmation" && ( + +

Confirmation

+
+ )} + + + +

actorHoisted = interpret(checkoutMachine).start()

+
+                {JSON.stringify(checkoutMachineActor, null, 4)}
+            
+ + + +

actorPage = useMachine(checkoutMachine)

+
+                {JSON.stringify(actorPage, null, 4)}
+            
+ + + +

theState() = createSignal(actorHoisted.initialState)

+
+                {JSON.stringify(theState(), null, 4)}
+            
+ + + + + +
+ ) +} + +export default XStatePage diff --git a/src/routes/xstate/useMachine.ts b/src/routes/xstate/useMachine.ts new file mode 100644 index 0000000..4adabfd --- /dev/null +++ b/src/routes/xstate/useMachine.ts @@ -0,0 +1,66 @@ +/* eslint-disable solid/reactivity */ +import type { + AnyStateMachine, + EventFrom, + InterpreterFrom, + StateFrom, + StateValueFrom +} from "xstate"; +import { + interpret +} from "xstate"; +import type { Accessor} from "solid-js"; +import { createSignal, onCleanup, onMount } from "solid-js"; + +interface UseMachineApi { + can: (event: EventFrom | EventFrom["type"]) => boolean; + matches: (state: StateValueFrom) => boolean; + select: ( + selector: (state: StateFrom) => Result + ) => Accessor; + send: InterpreterFrom["send"]; +} + +export const useMachine = ( + machine: M +): UseMachineApi => { + const service = interpret(machine); + const unsubscribes: Array<() => void> = []; + onMount(() => { + service.start(); + }); + onCleanup(() => { + service.stop(); + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }); + return { + can: (event) => { + const [can, setCan] = createSignal(service.getSnapshot().can(event)); + unsubscribes.push( + service.subscribe((s) => setCan(s.can(event))).unsubscribe + ); + return can(); + }, + matches: (state) => { + const [matches, setMatches] = createSignal( + service.getSnapshot().matches(state) + ); + unsubscribes.push( + service.subscribe((s) => setMatches(s.matches(state))).unsubscribe + ); + return matches(); + }, + select: (selector) => { + const [result, setResult] = createSignal( + selector(service.getSnapshot() as StateFrom) + ); + unsubscribes.push( + service.subscribe((state) => + setResult(() => selector(state as StateFrom)) + ).unsubscribe + ); + return result; + }, + send: service.send + }; +};