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 {
6
11
EventObject ,
7
- StateMachine ,
8
- Typestate ,
9
- MachineConfig ,
10
- StateSchema ,
11
12
Interpreter ,
12
- State ,
13
+ MachineConfig ,
13
14
MachineOptions ,
14
- StateValue ,
15
- InterpreterOptions ,
15
+ State ,
16
+ StateMachine ,
17
+ StateSchema ,
18
+ Typestate ,
16
19
} 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
+ } ;
69
38
} ;
70
39
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
+ */
71
59
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 ( ) ;
105
81
}
106
82
107
- assert ( `Machine setup failed` , this . service ) ;
108
-
109
83
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,
113
102
} ;
114
103
}
115
104
116
- // used when this Resource is used as a helper
117
- get value ( ) {
118
- return this . state ;
119
- }
120
-
121
105
@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
+ }
125
112
126
- @action
127
- onTransition ( fn : StateListener < TContext , TEvent , TStateSchema , TTypestate > ) {
128
- this . _onTransition = fn ;
129
113
return this ;
130
114
}
131
115
132
116
@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
+ }
135
123
136
124
return this ;
137
125
}
138
126
139
127
@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
+ }
142
136
143
137
return this ;
144
138
}
145
139
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 ) ;
152
157
153
- this . service = interpret ( this . machine , {
158
+ this [ INTERPRETER ] = interpret ( this [ MACHINE ] , {
154
159
devTools : DEBUG ,
155
- ...this . interpreterOptions ,
156
160
clock : {
157
161
setTimeout ( fn , ms ) {
158
162
return later . call ( null , fn , ms ) ;
@@ -162,19 +166,30 @@ export class Statechart<
162
166
} ,
163
167
} ,
164
168
} ) . onTransition ( ( state ) => {
165
- this . _state = state ;
169
+ this . state = state ;
166
170
} ) ;
167
171
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 ) ;
171
186
172
- this . service . start ( setupOptions . initialState || state ) ;
187
+ this [ MACHINE ] = createMachine ( statechart ) ;
173
188
}
174
189
175
- teardown ( ) : void {
176
- if ( ! this . service ) return ;
190
+ protected teardown ( ) {
191
+ if ( ! this [ INTERPRETER ] ) return ;
177
192
178
- this . service . stop ( ) ;
193
+ this [ INTERPRETER ] . stop ( ) ;
179
194
}
180
195
}
0 commit comments