Skip to content

Commit 0b4cde6

Browse files
committed
copy current edition of useMachine from emberclear/pinochel
1 parent 728f4e6 commit 0b4cde6

File tree

1 file changed

+148
-133
lines changed

1 file changed

+148
-133
lines changed

addon/usables/use-machine.ts

Lines changed: 148 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,162 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import {
3-
interpret,
4-
createMachine,
5-
StateNode,
1+
import { DEBUG } from '@glimmer/env';
2+
import { tracked } from '@glimmer/tracking';
3+
import { assert } from '@ember/debug';
4+
import { action } from '@ember/object';
5+
import { cancel, later } from '@ember/runloop';
6+
7+
import { Resource } from 'ember-could-get-used-to-this';
8+
import { createMachine, interpret } from 'xstate';
9+
10+
import type {
611
EventObject,
7-
StateMachine,
8-
Typestate,
9-
MachineConfig,
10-
StateSchema,
1112
Interpreter,
12-
State,
13+
MachineConfig,
1314
MachineOptions,
14-
StateValue,
15-
InterpreterOptions,
15+
State,
16+
StateMachine,
17+
StateSchema,
18+
Typestate,
1619
} from 'xstate';
17-
import { StateListener } from 'xstate/lib/interpreter';
18-
19-
import { tracked } from '@glimmer/tracking';
20-
import { DEBUG } from '@glimmer/env';
21-
import { later, cancel } from '@ember/runloop';
22-
import { getOwner, setOwner } from '@ember/application';
23-
import { assert, warn } from '@ember/debug';
24-
import { action } from '@ember/object';
25-
26-
import { use, Resource } from 'ember-could-get-used-to-this';
27-
28-
export const ARGS_STATE_CHANGE_WARNING =
29-
'A change to passed `args` or a local state change triggered an update to a `useMachine`-usable. You can send a dedicated event to the machine or restart it so this is handled. This is done via the `.update`-hook of the `useMachine`-usable.';
30-
31-
export type Send<
32-
TContext,
33-
TStateSchema extends StateSchema,
34-
TEvent extends EventObject,
35-
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
36-
> = Interpreter<TContext, TStateSchema, TEvent, TTypestate>['send'];
37-
38-
export type UpdateFunction<
39-
TContext,
40-
TStateSchema extends StateSchema,
41-
TEvent extends EventObject,
42-
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
43-
> = (args: {
44-
machine: StateMachine<TContext, TStateSchema, TEvent, TTypestate>;
45-
context?: TContext;
46-
config?: Partial<MachineOptions<TContext, TEvent>>;
47-
send: Send<TContext, TStateSchema, TEvent, TTypestate>;
48-
restart: (initialState?: State<TContext, TEvent, TStateSchema, TTypestate> | StateValue) => void;
49-
}) => void;
50-
51-
export type UsableStatechart<
52-
TContext,
53-
TStateSchema extends StateSchema,
54-
TEvent extends EventObject,
55-
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
56-
> =
57-
| MachineConfig<TContext, TStateSchema, TEvent>
58-
| StateMachine<TContext, TStateSchema, TEvent, TTypestate>;
59-
60-
type Args<
61-
TContext,
62-
TStateSchema,
63-
TEvent extends EventObject,
64-
TTypestate extends Typestate<TContext>
65-
> = {
66-
machine: UsableStatechart<TContext, TStateSchema, TEvent, TTypestate>;
67-
interpreterOptions: Partial<InterpreterOptions>;
68-
onTransition?: StateListener<TContext, TEvent, TStateSchema, TTypestate>;
20+
import type { StateListener } from 'xstate/lib/interpreter';
21+
22+
const INTERPRETER = Symbol('interpreter');
23+
const CONFIG = Symbol('config');
24+
const MACHINE = Symbol('machine');
25+
26+
const ERROR_CANT_RECONFIGURE = `Cannot re-invoke withContext after the interpreter has been initialized`;
27+
const ERROR_CHART_MISSING = `A statechart was not passed`;
28+
29+
type Args<Context, Schema extends StateSchema, Event extends EventObject> = {
30+
positional?: [MachineConfig<Context, Schema, Event>];
31+
named?: {
32+
chart: MachineConfig<Context, Schema, Event>;
33+
config?: Partial<MachineOptions<Context, Event>>;
34+
context?: Context;
35+
initialState?: State<Context, Event>;
36+
onTransition?: StateListener<Context, Event, Schema, Typestate<Context>>;
37+
};
6938
};
7039

40+
type SendArgs<Context, Schema extends StateSchema, Event extends EventObject> = Parameters<
41+
Interpreter<Context, Schema, Event>['send']
42+
>;
43+
44+
/**
45+
*
46+
@use
47+
interpreter = new Statechart(() => [statechart])
48+
.withContext(this.context).withConfig({
49+
actions: {
50+
returnSelectedToHand: this._returnSelectedToHand,
51+
showSelected: this._showSelected,
52+
closeHand: this._closeHand,
53+
fanOpen: this._fanOpen,
54+
adjustHand: this._adjustHand,
55+
},
56+
guards: {},
57+
});
58+
*/
7159
export class Statechart<
72-
TContext,
73-
TStateSchema extends StateSchema,
74-
TEvent extends EventObject,
75-
TTypestate extends Typestate<TContext>
76-
> extends Resource<Args<TContext, TStateSchema, TEvent, TTypestate>> {
77-
@tracked service?: Interpreter<TContext, TStateSchema, TEvent, TTypestate> = undefined;
78-
// current state of the machine,
79-
// set onTransition, which is configured during setup
80-
@tracked _state?: State<TContext, TEvent, TStateSchema, TTypestate> = undefined;
81-
82-
machine: StateMachine<TContext, TStateSchema, TEvent, TTypestate>;
83-
interpreterOptions: Partial<InterpreterOptions>;
84-
85-
declare _onTransition: StateListener<TContext, TEvent, TStateSchema, TTypestate> | undefined;
86-
declare _config: Partial<MachineOptions<TContext, TEvent>>;
87-
declare _context: TContext;
88-
89-
constructor(owner: unknown, args: Args<TContext, TStateSchema, TEvent, TTypestate>) {
90-
super(owner, args);
91-
92-
let { machine } = args;
93-
const { interpreterOptions, onTransition } = args;
94-
95-
machine = machine instanceof StateNode ? machine : createMachine(machine);
96-
97-
this.machine = machine;
98-
this.interpreterOptions = interpreterOptions || {};
99-
this._onTransition = onTransition;
100-
}
101-
102-
get state() {
103-
if (!this.service) {
104-
this.setup();
60+
Context,
61+
Schema extends StateSchema,
62+
Event extends EventObject
63+
> extends Resource<Args<Context, Schema, Event>> {
64+
declare [MACHINE]: StateMachine<Context, Schema, Event>;
65+
declare [INTERPRETER]: Interpreter<Context, Schema, Event>;
66+
67+
@tracked state?: State<Context, Event>;
68+
69+
/**
70+
* This is the return value of `new Statechart(() => ...)`
71+
*/
72+
get value(): {
73+
state?: State<Context, Event>;
74+
send: Statechart<Context, Schema, Event>['send'];
75+
// withContext: Statechart<Context, Schema, Event>['withContext'];
76+
// withConfig: Statechart<Context, Schema, Event>['withConfig'];
77+
// onTransition: Statechart<Context, Schema, Event>['onTransition'];
78+
} {
79+
if (!this[INTERPRETER]) {
80+
this._setupMachine();
10581
}
10682

107-
assert(`Machine setup failed`, this.service);
108-
10983
return {
110-
state: this._state,
111-
send: this.service.send,
112-
service: this.service,
84+
// For TypeScript, this is tricky because this is what is accessible at the call site
85+
// but typescript thinks the context is the class instance.
86+
//
87+
// To remedy, each property has to also exist on the class body under the same name
88+
state: this.state,
89+
send: this.send,
90+
91+
/**
92+
* One Time methods
93+
* Currently disabled due to issues with the use/resource transform not allowing
94+
* the builder pattern
95+
*
96+
* If the transform is fixed, we can remove the protected visibility modifier
97+
* and uncomment out these three lines in a back-compat way for existing users
98+
*/
99+
// withContext: this.withContext,
100+
// withConfig: this.withConfig,
101+
// onTransition: this.onTransition,
113102
};
114103
}
115104

116-
// used when this Resource is used as a helper
117-
get value() {
118-
return this.state;
119-
}
120-
121105
@action
122-
send(...args: Parameters<Interpreter<TContext, TStateSchema, TEvent, TTypestate>['send']>) {
123-
this.state.service.send(...args);
124-
}
106+
protected withContext(context?: Context) {
107+
assert(ERROR_CANT_RECONFIGURE, !this[INTERPRETER]);
108+
109+
if (context) {
110+
this[MACHINE] = this[MACHINE].withContext(context);
111+
}
125112

126-
@action
127-
onTransition(fn: StateListener<TContext, TEvent, TStateSchema, TTypestate>) {
128-
this._onTransition = fn;
129113
return this;
130114
}
131115

132116
@action
133-
withContext(context: TContext) {
134-
this._context = context;
117+
protected withConfig(config?: Partial<MachineOptions<Context, Event>>) {
118+
assert(ERROR_CANT_RECONFIGURE, !this[INTERPRETER]);
119+
120+
if (config) {
121+
this[MACHINE] = this[MACHINE].withConfig(config);
122+
}
135123

136124
return this;
137125
}
138126

139127
@action
140-
withConfig(config: Partial<MachineOptions<TContext, TEvent>>) {
141-
this._config = config;
128+
protected onTransition(fn?: StateListener<Context, Event, Schema, Typestate<Context>>) {
129+
if (!this[INTERPRETER]) {
130+
this._setupMachine();
131+
}
132+
133+
if (fn) {
134+
this[INTERPRETER].onTransition(fn);
135+
}
142136

143137
return this;
144138
}
145139

146-
setup(
147-
setupOptions: {
148-
initialState?: State<TContext, TEvent, TStateSchema, TTypestate> | StateValue;
149-
} = {}
150-
): void {
151-
const { state } = this.interpreterOptions;
140+
/**
141+
* Private
142+
*/
143+
144+
private get [CONFIG]() {
145+
return this.args.named;
146+
}
147+
148+
@action
149+
send(...args: SendArgs<Context, Schema, Event>) {
150+
return this[INTERPRETER].send(...args);
151+
}
152+
153+
@action
154+
private _setupMachine() {
155+
this.withContext(this[CONFIG]?.context);
156+
this.withConfig(this[CONFIG]?.config);
152157

153-
this.service = interpret(this.machine, {
158+
this[INTERPRETER] = interpret(this[MACHINE], {
154159
devTools: DEBUG,
155-
...this.interpreterOptions,
156160
clock: {
157161
setTimeout(fn, ms) {
158162
return later.call(null, fn, ms);
@@ -162,19 +166,30 @@ export class Statechart<
162166
},
163167
},
164168
}).onTransition((state) => {
165-
this._state = state;
169+
this.state = state;
166170
});
167171

168-
if (this._onTransition) {
169-
this.service.onTransition(this._onTransition);
170-
}
172+
this.onTransition(this[CONFIG]?.onTransition);
173+
174+
this[INTERPRETER].start(this[CONFIG]?.initialState);
175+
}
176+
177+
/**
178+
* Lifecycle methods on Resource
179+
*
180+
*/
181+
@action
182+
const statechart = this.args.positional?.[0] || this.args.named?.chart;
183+
let statechart = this.args.positional?.[0] || this.args.named?.chart;
184+
185+
assert(ERROR_CHART_MISSING, statechart);
171186

172-
this.service.start(setupOptions.initialState || state);
187+
this[MACHINE] = createMachine(statechart);
173188
}
174189

175-
teardown(): void {
176-
if (!this.service) return;
190+
protected teardown() {
191+
if (!this[INTERPRETER]) return;
177192

178-
this.service.stop();
193+
this[INTERPRETER].stop();
179194
}
180195
}

0 commit comments

Comments
 (0)