@koordinates/xstate-tree 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ import { useMachine } from "@xstate/react";
2
+ import memoize from "fast-memoize";
3
+ import { reduce } from "lodash";
4
+ import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
5
+ import { TinyEmitter } from "tiny-emitter";
6
+ import { handleLocationChange, RoutingContext, } from "./routing";
7
+ import { useActiveRouteEvents } from "./routing/providers";
8
+ import { useConstant } from "./useConstant";
9
+ import { useService } from "./useService";
10
+ export const emitter = new TinyEmitter();
11
+ /**
12
+ * @public
13
+ */
14
+ export function broadcast(event) {
15
+ console.debug("[xstate-tree] broadcasting event ", event.type);
16
+ emitter.emit("event", event);
17
+ }
18
+ /**
19
+ * @public
20
+ */
21
+ export function onBroadcast(handler) {
22
+ emitter.on("event", handler);
23
+ return () => {
24
+ emitter.off("event", handler);
25
+ };
26
+ }
27
+ function cacheKeyForInterpreter(
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ interpreter) {
30
+ return interpreter.sessionId;
31
+ }
32
+ const getViewForInterpreter = memoize((interpreter) => {
33
+ return React.memo(function InterpreterView() {
34
+ const activeRouteEvents = useActiveRouteEvents();
35
+ useEffect(() => {
36
+ if (activeRouteEvents) {
37
+ activeRouteEvents.forEach((event) => {
38
+ if (interpreter.state.nextEvents.includes(event.type)) {
39
+ interpreter.send(event);
40
+ }
41
+ });
42
+ }
43
+ }, []);
44
+ return React.createElement(XstateTreeView, { interpreter: interpreter });
45
+ });
46
+ },
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ { serializer: cacheKeyForInterpreter });
49
+ const getMultiSlotViewForChildren = memoize((parent, slot) => {
50
+ return React.memo(function MultiSlotView() {
51
+ const [_, children] = useService(parent);
52
+ const interpreters = [...children.values()];
53
+ // Once the interpreter is stopped, initialized gets set to false
54
+ // We don't want to render stopped interpreters
55
+ const interpretersWeCareAbout = interpreters.filter((i) => i.id.includes(slot) && i.initialized);
56
+ return (React.createElement(XstateTreeMultiSlotView, { childInterpreters: interpretersWeCareAbout }));
57
+ });
58
+ }, {
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ serializer: ((interpreter, slot) =>
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ `${cacheKeyForInterpreter(interpreter)}-${slot}`),
63
+ });
64
+ function useSlots(interpreter, slots) {
65
+ return useConstant(() => {
66
+ return reduce(slots, (views, slot) => {
67
+ return {
68
+ ...views,
69
+ [slot]: () => {
70
+ // eslint-disable-next-line react-hooks/rules-of-hooks
71
+ const [__, children] = useService(interpreter);
72
+ if (slot.toString().endsWith("s")) {
73
+ const MultiView = getMultiSlotViewForChildren(interpreter, slot.toLowerCase());
74
+ return React.createElement(MultiView, null);
75
+ }
76
+ else {
77
+ const interpreterForSlot = children.get(`${slot.toLowerCase()}-slot`);
78
+ if (interpreterForSlot) {
79
+ const View = getViewForInterpreter(interpreterForSlot);
80
+ return React.createElement(View, null);
81
+ }
82
+ else {
83
+ // Waiting for the interpreter for this slot to be invoked
84
+ return null;
85
+ }
86
+ }
87
+ },
88
+ };
89
+ }, {});
90
+ });
91
+ }
92
+ function XstateTreeMultiSlotView({ childInterpreters, }) {
93
+ return (React.createElement(React.Fragment, null, childInterpreters.map((i) => (React.createElement(XstateTreeView, { key: i.id, interpreter: i })))));
94
+ }
95
+ /**
96
+ * @internal
97
+ */
98
+ export function XstateTreeView({ interpreter }) {
99
+ const [current] = useService(interpreter);
100
+ const currentRef = useRef(current);
101
+ currentRef.current = current;
102
+ const { view: View, actions: actionsFactory, selectors: selectorsFactory, slots: interpreterSlots, } = interpreter.machine.meta;
103
+ const slots = useSlots(interpreter, interpreterSlots.map((x) => x.name));
104
+ const canHandleEvent = useCallback((e) => {
105
+ return interpreter.nextState(e).changed ?? false;
106
+ }, [interpreter]);
107
+ const inState = useCallback((state) => {
108
+ return currentRef.current?.matches(state) ?? false;
109
+ },
110
+ // This is needed because the inState function needs to be recreated if the
111
+ // current state the machine is in changes. But _only_ then
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ [current.value]);
114
+ if (!current) {
115
+ return null;
116
+ }
117
+ const selectors = selectorsFactory(current.context, canHandleEvent, inState, current.value);
118
+ const actions = actionsFactory(interpreter.send, selectors);
119
+ return (React.createElement(View, { selectors: selectors, actions: actions, slots: slots, inState: inState }));
120
+ }
121
+ /**
122
+ * @internal
123
+ */
124
+ export function recursivelySend(service, event) {
125
+ const children = ([...service.children.values()] || []).filter((s) => s.id.includes("-slot"));
126
+ // If the service can't handle the event, don't send it
127
+ if (service.state.nextEvents.includes(event.type)) {
128
+ try {
129
+ service.send(event);
130
+ }
131
+ catch (e) {
132
+ console.error("Error sending event ", event, " to machine ", service.machine.id, e);
133
+ }
134
+ }
135
+ children.forEach((child) => recursivelySend(child, event));
136
+ }
137
+ /**
138
+ * @public
139
+ */
140
+ export function buildRootComponent(machine, routing) {
141
+ if (!machine.meta) {
142
+ throw new Error("Root machine has no meta");
143
+ }
144
+ if (!machine.meta.view) {
145
+ throw new Error("Root machine has no associated view");
146
+ }
147
+ const RootComponent = function XstateTreeRootComponent() {
148
+ const [_, __, interpreter] = useMachine(machine, { devTools: true });
149
+ const [activeRouteEvents, setActiveRouteEvents] = useState(undefined);
150
+ const [forceRenderValue, forceRender] = useState(false);
151
+ useEffect(() => {
152
+ function handler(event) {
153
+ recursivelySend(interpreter, event);
154
+ }
155
+ emitter.on("event", handler);
156
+ return () => {
157
+ emitter.off("event", handler);
158
+ };
159
+ }, [interpreter]);
160
+ useEffect(() => {
161
+ if (routing) {
162
+ const { getPathName = () => window.location.pathname, getQueryString = () => window.location.search, } = routing;
163
+ const queryString = getQueryString();
164
+ handleLocationChange(routing.routes, routing.basePath, getPathName(), getQueryString(), setActiveRouteEvents);
165
+ // Hack to ensure the initial location doesn't have undefined state
166
+ // It's not supposed to, but it does for some reason
167
+ // And the history library ignores popstate events with undefined state
168
+ routing.history.replace(`${getPathName()}${queryString}`, {});
169
+ }
170
+ }, []);
171
+ useEffect(() => {
172
+ if (routing) {
173
+ const unsub = routing.history.listen((location) => {
174
+ handleLocationChange(routing.routes, routing.basePath, location.pathname, location.search, setActiveRouteEvents, location.state?.meta);
175
+ });
176
+ return () => {
177
+ unsub();
178
+ };
179
+ }
180
+ return undefined;
181
+ }, []);
182
+ const routingProviderValue = useMemo(() => {
183
+ if (!routing) {
184
+ return null;
185
+ }
186
+ return {
187
+ activeRouteEvents,
188
+ };
189
+ }, [activeRouteEvents]);
190
+ if (!interpreter.initialized) {
191
+ setTimeout(() => forceRender(!forceRenderValue), 0);
192
+ return null;
193
+ }
194
+ if (routingProviderValue) {
195
+ return (React.createElement(RoutingContext.Provider, { value: routingProviderValue },
196
+ React.createElement(XstateTreeView, { interpreter: interpreter })));
197
+ }
198
+ else {
199
+ return React.createElement(XstateTreeView, { interpreter: interpreter });
200
+ }
201
+ };
202
+ RootComponent.rootMachine = machine;
203
+ return RootComponent;
204
+ }
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@koordinates/xstate-tree",
3
+ "main": "lib/index.js",
4
+ "types": "lib/xstate-tree.d.ts",
5
+ "version": "1.0.0-beta.1",
6
+ "dependencies": {
7
+ "@xstate/react": "3.0.0",
8
+ "fast-memoize": "2.5.2",
9
+ "history": "4.10.1",
10
+ "lodash": "4.17.21",
11
+ "path-to-regexp": "6.2.0",
12
+ "query-string": "6.12.1",
13
+ "react": "18.1.0",
14
+ "react-dom": "18.1.0",
15
+ "rxjs": "6.6.2",
16
+ "tiny-emitter": "2.1.0",
17
+ "typescript": "4.7.3",
18
+ "xstate": "4.32.0",
19
+ "zod": "3.17.3"
20
+ },
21
+ "devDependencies": {
22
+ "@microsoft/api-extractor": "7.28.4",
23
+ "@rushstack/eslint-config": "2.6.0",
24
+ "@testing-library/dom": "8.14.0",
25
+ "@testing-library/jest-dom": "5.16.1",
26
+ "@testing-library/react": "10.4.8",
27
+ "@testing-library/user-event": "13.5.0",
28
+ "@types/history": "4.7.7",
29
+ "@types/jest": "28.1.4",
30
+ "@types/lodash": "4.14.180",
31
+ "@types/react": "17.0.29",
32
+ "@types/react-dom": "18.0.6",
33
+ "@types/testing-library__jest-dom": "5.14.1",
34
+ "@typescript-eslint/eslint-plugin": "5.30.5",
35
+ "classnames": "2.3.1",
36
+ "cz-conventional-changelog": "^3.3.0",
37
+ "eslint": "7.32.0",
38
+ "eslint-import-resolver-typescript": "2.7.1",
39
+ "eslint-plugin-import": "2.26.0",
40
+ "eslint-plugin-prettier": "4.2.1",
41
+ "eslint-plugin-react-hooks": "4.3.0",
42
+ "jest": "28.0.3",
43
+ "jest-environment-jsdom": "28.0.1",
44
+ "rimraf": "3.0.2",
45
+ "semantic-release": "19.0.3",
46
+ "ts-jest": "28.0.5"
47
+ },
48
+ "scripts": {
49
+ "lint": "eslint 'src/**/*'",
50
+ "test": "jest",
51
+ "build": "rimraf dist && tsc -p tsconfig.build.json",
52
+ "build:watch": "tsc -p tsconfig.build.json -w",
53
+ "api-extractor": "api-extractor run"
54
+ },
55
+ "files": [
56
+ "lib/**/*.js",
57
+ "lib/xstate-tree.d.ts",
58
+ "!lib/**/*.spec.js"
59
+ ],
60
+ "config": {
61
+ "commitizen": {
62
+ "path": "./node_modules/cz-conventional-changelog",
63
+ "types": {
64
+ "build": {
65
+ "description": "Changes that affect the build system or external dependencies (example scopes: rollup, npm)"
66
+ },
67
+ "ci": {
68
+ "description": "Changes to our CI configuration files and scripts"
69
+ },
70
+ "docs": {
71
+ "description": "Changes to documentation"
72
+ },
73
+ "feat": {
74
+ "description": "A new feature"
75
+ },
76
+ "fix": {
77
+ "description": "Bug fixes"
78
+ },
79
+ "perf": {
80
+ "description": "A performance improvement"
81
+ },
82
+ "refactor": {
83
+ "description": "A code change that neither fixes a bug nor adds a feature"
84
+ },
85
+ "test": {
86
+ "description": "Adding or correcting tests"
87
+ },
88
+ "chore": {
89
+ "description": "Other changes that don't modify src or test files"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }