jssm 5.44.0 → 5.45.0

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.
package/jssm.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  declare type StateType = string;
2
2
  import { JssmGenericState, JssmGenericConfig, JssmTransition, JssmTransitionList, // JssmTransitionRule,
3
- JssmMachineInternalState, JssmParseTree, JssmStateDeclaration, JssmArrow, JssmArrowDirection, JssmArrowKind, JssmLayout, FslDirection, FslTheme } from './jssm_types';
3
+ JssmMachineInternalState, JssmParseTree, JssmStateDeclaration, JssmArrow, JssmArrowDirection, JssmArrowKind, JssmLayout, FslDirection, FslTheme, HookDescription } from './jssm_types';
4
4
  import { seq, weighted_rand_select, weighted_sample_select, histograph, weighted_histo_key } from './jssm_util';
5
5
  import { version } from './version';
6
6
  declare function arrow_direction(arrow: JssmArrow): JssmArrowDirection;
@@ -37,6 +37,9 @@ declare class Machine<mDT> {
37
37
  _arrange_end_declaration: Array<Array<StateType>>;
38
38
  _theme: FslTheme;
39
39
  _flow: FslDirection;
40
+ _has_hooks: boolean;
41
+ _hooks: Map<string, Function>;
42
+ _named_hooks: Map<string, Function>;
40
43
  constructor({ start_states, complete, transitions, machine_author, machine_comment, machine_contributor, machine_definition, machine_language, machine_license, machine_name, machine_version, state_declaration, fsl_version, dot_preamble, arrange_declaration, arrange_start_declaration, arrange_end_declaration, theme, flow, graph_layout }: JssmGenericConfig<mDT>);
41
44
  _new_state(state_config: JssmGenericState): StateType;
42
45
  state(): StateType;
@@ -86,6 +89,7 @@ declare class Machine<mDT> {
86
89
  is_complete(): boolean;
87
90
  state_is_complete(whichState: StateType): boolean;
88
91
  has_completes(): boolean;
92
+ set_hook(HookDesc: HookDescription): void;
89
93
  action(name: StateType, newData?: mDT): boolean;
90
94
  transition(newState: StateType, newData?: mDT): boolean;
91
95
  force_transition(newState: StateType, newData?: mDT): boolean;
package/jssm_types.d.ts CHANGED
@@ -148,4 +148,28 @@ declare type JssmCompileSeStart<DataType> = {
148
148
  };
149
149
  declare type JssmParseTree = Array<JssmCompileSeStart<StateType>>;
150
150
  declare type JssmParseFunctionType = (string: any) => JssmParseTree;
151
- export { JssmColor, JssmTransition, JssmTransitions, JssmTransitionList, JssmTransitionRule, JssmArrow, JssmArrowKind, JssmArrowDirection, JssmGenericConfig, JssmGenericState, JssmGenericMachine, JssmParseTree, JssmCompileSe, JssmCompileSeStart, JssmCompileRule, JssmPermitted, JssmPermittedOpt, JssmResult, JssmStateDeclaration, JssmStateDeclarationRule, JssmLayout, JssmParseFunctionType, JssmMachineInternalState, FslDirection, FslTheme };
151
+ declare type BasicHookDescription = {
152
+ kind: 'hook';
153
+ from: string;
154
+ to: string;
155
+ handler: Function;
156
+ };
157
+ declare type HookDescriptionWithAction = {
158
+ kind: 'named';
159
+ from: string;
160
+ to: string;
161
+ action: string;
162
+ handler: Function;
163
+ };
164
+ declare type EntryHook = {
165
+ kind: 'entry';
166
+ to: string;
167
+ handler: Function;
168
+ };
169
+ declare type ExitHook = {
170
+ kind: 'exit';
171
+ from: string;
172
+ handler: Function;
173
+ };
174
+ declare type HookDescription = BasicHookDescription | HookDescriptionWithAction | EntryHook | ExitHook;
175
+ export { JssmColor, JssmTransition, JssmTransitions, JssmTransitionList, JssmTransitionRule, JssmArrow, JssmArrowKind, JssmArrowDirection, JssmGenericConfig, JssmGenericState, JssmGenericMachine, JssmParseTree, JssmCompileSe, JssmCompileSeStart, JssmCompileRule, JssmPermitted, JssmPermittedOpt, JssmResult, JssmStateDeclaration, JssmStateDeclarationRule, JssmLayout, JssmParseFunctionType, JssmMachineInternalState, FslDirection, FslTheme, HookDescription };
package/jssm_util.d.ts CHANGED
@@ -5,4 +5,6 @@ declare const seq: Function;
5
5
  declare const histograph: Function;
6
6
  declare const weighted_sample_select: Function;
7
7
  declare const weighted_histo_key: Function;
8
- export { seq, arr_uniq_p, histograph, weighted_histo_key, weighted_rand_select, weighted_sample_select, array_box_if_string };
8
+ declare const hook_name: (from: string, to: string) => string;
9
+ declare const named_hook_name: (from: string, to: string, action: string) => string;
10
+ export { seq, arr_uniq_p, histograph, weighted_histo_key, weighted_rand_select, weighted_sample_select, array_box_if_string, hook_name, named_hook_name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jssm",
3
- "version": "5.44.0",
3
+ "version": "5.45.0",
4
4
  "engines": {
5
5
  "node": ">=10.0.0"
6
6
  },
package/src/ts/jssm.ts CHANGED
@@ -19,7 +19,8 @@ import {
19
19
  JssmCompileSe, JssmCompileSeStart, JssmCompileRule,
20
20
  JssmArrow, JssmArrowDirection, JssmArrowKind,
21
21
  JssmLayout,
22
- FslDirection, FslTheme
22
+ FslDirection, FslTheme,
23
+ HookDescription
23
24
 
24
25
  } from './jssm_types';
25
26
 
@@ -29,7 +30,7 @@ import {
29
30
 
30
31
  import {
31
32
  seq, weighted_rand_select, weighted_sample_select, histograph,
32
- weighted_histo_key, array_box_if_string
33
+ weighted_histo_key, array_box_if_string, hook_name, named_hook_name
33
34
  } from './jssm_util';
34
35
 
35
36
 
@@ -475,11 +476,15 @@ class Machine<mDT> {
475
476
  _theme : FslTheme;
476
477
  _flow : FslDirection;
477
478
 
479
+ _has_hooks : boolean;
480
+ _hooks : Map<string, Function>;
481
+ _named_hooks : Map<string, Function>;
482
+
478
483
 
479
484
  // whargarbl this badly needs to be broken up, monolith master
480
485
  constructor({
481
486
  start_states,
482
- complete = [],
487
+ complete = [],
483
488
  transitions,
484
489
  machine_author,
485
490
  machine_comment,
@@ -530,6 +535,10 @@ class Machine<mDT> {
530
535
  this._flow = flow;
531
536
  this._graph_layout = graph_layout;
532
537
 
538
+ this._has_hooks = false;
539
+ this._hooks = new Map();
540
+ this._named_hooks = new Map();
541
+
533
542
 
534
543
  if (state_declaration) {
535
544
  state_declaration.map( (state_decl: JssmStateDeclaration) => {
@@ -975,14 +984,73 @@ class Machine<mDT> {
975
984
 
976
985
 
977
986
 
987
+ // basic toolable hook call. convenience wrappers will follow, like
988
+ // hook(from, to, handler) and exit_hook(from, handler) and etc
989
+ set_hook(HookDesc: HookDescription) {
990
+
991
+ switch (HookDesc.kind) {
992
+
993
+ case 'hook':
994
+ this._hooks.set(hook_name(HookDesc.from, HookDesc.to), HookDesc.handler);
995
+ this._has_hooks = true;
996
+ break;
997
+
998
+ case 'named':
999
+ this._named_hooks.set(named_hook_name(HookDesc.from, HookDesc.to, HookDesc.action), HookDesc.handler);
1000
+ this._has_hooks = true;
1001
+ break;
1002
+
1003
+ // case 'entry':
1004
+ // console.log('TODO: Should add entry hook here');
1005
+ // throw 'TODO: Should add entry hook here';
1006
+
1007
+ // case 'exit':
1008
+ // console.log('TODO: Should add exit hook here');
1009
+ // throw 'TODO: Should add exit hook here';
1010
+
1011
+ default:
1012
+ console.log(`Unknown hook type ${(HookDesc as any).kind}, should be impossible`);
1013
+ throw new RangeError(`Unknown hook type ${(HookDesc as any).kind}, should be impossible`);
1014
+
1015
+ }
1016
+ }
1017
+
1018
+ // remove_hook(HookDesc: HookDescription) {
1019
+ // throw 'TODO: Should remove hook here';
1020
+ // }
1021
+
1022
+
1023
+
978
1024
  action(name: StateType, newData?: mDT): boolean {
979
1025
  // todo whargarbl implement hooks
980
1026
  // todo whargarbl implement data stuff
981
1027
  // todo major incomplete whargarbl comeback
982
1028
  if (this.valid_action(name, newData)) {
983
- const edge: JssmTransition<mDT> = this.current_action_edge_for(name);
984
- this._state = edge.to;
985
- return true;
1029
+
1030
+ const edge : JssmTransition<mDT> = this.current_action_edge_for(name);
1031
+
1032
+ if (this._has_hooks) {
1033
+
1034
+ let hook_permits : boolean | undefined = undefined;
1035
+
1036
+ const nhn : string = named_hook_name(this._state, edge.to, name),
1037
+ maybe_hook = this._named_hooks.get(nhn);
1038
+
1039
+ if (maybe_hook === undefined) { hook_permits = true; }
1040
+ else { hook_permits = maybe_hook( { from: this._state, to: edge.to, action: name } ); }
1041
+
1042
+ if (hook_permits !== false) {
1043
+ this._state = edge.to;
1044
+ return true;
1045
+ } else {
1046
+ return false;
1047
+ }
1048
+
1049
+ } else {
1050
+ this._state = edge.to;
1051
+ return true;
1052
+ }
1053
+
986
1054
  } else {
987
1055
  return false;
988
1056
  }
@@ -993,11 +1061,35 @@ class Machine<mDT> {
993
1061
  // todo whargarbl implement data stuff
994
1062
  // todo major incomplete whargarbl comeback
995
1063
  if (this.valid_transition(newState, newData)) {
996
- this._state = newState;
997
- return true;
1064
+
1065
+ if (this._has_hooks) {
1066
+
1067
+ let hook_permits : boolean | undefined = undefined;
1068
+
1069
+ const hn : string = hook_name(this._state, newState),
1070
+ maybe_hook : Function | undefined = this._hooks.get(hn);
1071
+
1072
+ if (maybe_hook === undefined) { hook_permits = true; }
1073
+ else { hook_permits = maybe_hook( { from: this._state, to: newState } ); }
1074
+
1075
+ if (hook_permits !== false) {
1076
+ this._state = newState;
1077
+ return true;
1078
+ } else {
1079
+ return false;
1080
+ }
1081
+
1082
+ } else {
1083
+
1084
+ this._state = newState;
1085
+ return true;
1086
+
1087
+ }
1088
+
998
1089
  } else {
999
1090
  return false;
1000
1091
  }
1092
+
1001
1093
  }
1002
1094
 
1003
1095
  // can leave machine in inconsistent state. generally do not use
@@ -1006,18 +1098,44 @@ class Machine<mDT> {
1006
1098
  // todo whargarbl implement data stuff
1007
1099
  // todo major incomplete whargarbl comeback
1008
1100
  if (this.valid_force_transition(newState, newData)) {
1009
- this._state = newState;
1010
- return true;
1101
+
1102
+ if (this._has_hooks) {
1103
+
1104
+ let hook_permits : boolean | undefined = undefined;
1105
+
1106
+ const hn : string = hook_name(this._state, newState),
1107
+ maybe_hook : Function | undefined = this._hooks.get(hn);
1108
+
1109
+ if (maybe_hook === undefined) { hook_permits = true; }
1110
+ else { hook_permits = maybe_hook({ from: this._state, to: newState, forced: true }); }
1111
+
1112
+ if (hook_permits !== false) {
1113
+ this._state = newState;
1114
+ return true;
1115
+ } else {
1116
+ return false;
1117
+ }
1118
+
1119
+ } else {
1120
+
1121
+ this._state = newState;
1122
+ return true;
1123
+
1124
+ }
1125
+
1011
1126
  } else {
1012
1127
  return false;
1013
1128
  }
1129
+
1014
1130
  }
1015
1131
 
1016
1132
 
1017
1133
 
1018
1134
  current_action_for(action: StateType): number {
1019
1135
  const action_base: Map<StateType, number> = this._actions.get(action);
1020
- return action_base? action_base.get(this.state()): undefined;
1136
+ return action_base
1137
+ ? action_base.get(this.state())
1138
+ : undefined;
1021
1139
  }
1022
1140
 
1023
1141
  current_action_edge_for(action: StateType): JssmTransition<mDT> {
@@ -1081,7 +1199,8 @@ function sm<mDT>(template_strings: TemplateStringsArray, ... remainder /* , argu
1081
1199
  // string notation, as designed, it's not really worth the hassle
1082
1200
 
1083
1201
  /* eslint-disable prefer-rest-params */
1084
- (acc, val, idx): string => `${acc}${remainder[idx-1]}${val}` // arguments[0] is never loaded, so args doesn't need to be gated
1202
+ (acc, val, idx): string =>
1203
+ `${acc}${remainder[idx-1]}${val}` // arguments[0] is never loaded, so args doesn't need to be gated
1085
1204
  /* eslint-enable prefer-rest-params */
1086
1205
 
1087
1206
  )));
@@ -305,6 +305,43 @@ type JssmParseFunctionType =
305
305
 
306
306
 
307
307
 
308
+ type BasicHookDescription = {
309
+ kind : 'hook'
310
+ from : string,
311
+ to : string,
312
+ handler : Function
313
+ };
314
+
315
+ type HookDescriptionWithAction = {
316
+ kind : 'named',
317
+ from : string,
318
+ to : string,
319
+ action : string,
320
+ handler : Function
321
+ };
322
+
323
+ type EntryHook = {
324
+ kind : 'entry',
325
+ to : string,
326
+ handler : Function
327
+ };
328
+
329
+ type ExitHook = {
330
+ kind : 'exit',
331
+ from : string,
332
+ handler : Function
333
+ };
334
+
335
+ type HookDescription
336
+ = BasicHookDescription
337
+ | HookDescriptionWithAction
338
+ | EntryHook
339
+ | ExitHook;
340
+
341
+
342
+
343
+
344
+
308
345
  export {
309
346
 
310
347
  JssmColor,
@@ -341,6 +378,8 @@ export {
341
378
  JssmMachineInternalState,
342
379
 
343
380
  FslDirection,
344
- FslTheme
381
+ FslTheme,
382
+
383
+ HookDescription
345
384
 
346
385
  };
@@ -90,11 +90,28 @@ const weighted_histo_key: Function = (n: number, opts: Array<any>, prob_prop: st
90
90
 
91
91
 
92
92
 
93
+ const hook_name = (from: string, to: string): string =>
94
+
95
+ JSON.stringify([from, to]);
96
+
97
+
98
+
99
+
100
+
101
+ const named_hook_name = (from: string, to: string, action: string): string =>
102
+
103
+ JSON.stringify([from, to, action]);
104
+
105
+
106
+
107
+
108
+
93
109
  export {
94
110
  seq,
95
111
  arr_uniq_p,
96
112
  histograph, weighted_histo_key,
97
113
  weighted_rand_select, weighted_sample_select,
98
- array_box_if_string
114
+ array_box_if_string,
115
+ hook_name, named_hook_name
99
116
  };
100
117
 
@@ -5,9 +5,7 @@ import { sm } from '../jssm';
5
5
 
6
6
 
7
7
 
8
- describe('Hooks', () => {
9
-
10
-
8
+ describe('Hooks open and closed in grammar', () => {
11
9
 
12
10
  test.todo('Hooks open doesn\'t throw' /*, () => {
13
11
 
@@ -23,6 +21,189 @@ describe('Hooks', () => {
23
21
 
24
22
  } */);
25
23
 
24
+ });
25
+
26
+
27
+
28
+
29
+
30
+ describe('Basic hooks on API callpoint', () => {
31
+
32
+
33
+ test('Setting a regular hook doesn\'t throw', () => {
34
+
35
+ expect( () => {
36
+ const _foo = sm`a -> b;`;
37
+ _foo.set_hook({ from: 'a', to: 'b', handler: () => console.log('hi'), kind: 'hook' })
38
+ })
39
+ .not.toThrow();
40
+
41
+ } );
42
+
43
+
44
+ test('Setting a named hook doesn\'t throw', () => {
45
+
46
+ expect( () => {
47
+ const _foo = sm`a 'foo' -> b;`;
48
+ _foo.set_hook({ from: 'a', to: 'b', handler: () => console.log('hi'), kind: 'named', action: 'foo' })
49
+ })
50
+ .not.toThrow();
51
+
52
+ } );
53
+
54
+
55
+ test('Setting a kind of hook that doesn\'t exist throws', () => {
56
+
57
+ expect( () => {
58
+ const _foo = sm`a 'foo' -> b;`;
59
+ _foo.set_hook({ from: 'a', to: 'b', handler: () => console.log('hi'), kind: 'Smaug the Merciless', action: 'foo' } as any)
60
+ })
61
+ .toThrow();
62
+
63
+ } );
64
+
65
+ });
66
+
67
+
68
+
69
+
70
+
71
+ test('Basic hook rejection works', () => {
72
+
73
+ const foo = sm`a => b;`;
74
+
75
+ foo.set_hook({ from: 'a', to: 'b', kind: 'hook', handler: () => false });
76
+ expect(foo.transition('b')).toBe(false);
77
+ expect(foo.state()).toBe('a');
78
+
79
+ foo.set_hook({ from: 'a', to: 'b', kind: 'hook', handler: () => true });
80
+ expect(foo.transition('b')).toBe(true);
81
+ expect(foo.state()).toBe('b');
82
+
83
+ });
84
+
85
+
86
+
87
+
88
+
89
+ test('Basic hook rejection works on forced edges', () => {
90
+
91
+ const foo = sm`a ~> b ~> c;`;
92
+
93
+ foo.set_hook({ from: 'a', to: 'b', kind: 'hook', handler: () => false });
94
+ expect(foo.force_transition('b')).toBe(false);
95
+ expect(foo.state()).toBe('a');
96
+
97
+ foo.set_hook({ from: 'a', to: 'b', kind: 'hook', handler: () => true });
98
+ expect(foo.force_transition('b')).toBe(true);
99
+ expect(foo.state()).toBe('b');
100
+
101
+ // line completion for when a hook lookup finds nothing
102
+ expect(foo.force_transition('c')).toBe(true);
103
+ expect(foo.state()).toBe('c');
104
+
105
+ });
106
+
107
+
108
+
109
+
110
+
111
+ test('Named hook rejection works', () => {
112
+
113
+ const foo = sm`a 'foo' => b;`;
114
+
115
+ foo.set_hook({ from: 'a', to: 'b', action: 'foo', kind: 'named', handler: () => false });
116
+ expect(foo.action('foo')).toBe(false);
117
+ expect(foo.state()).toBe('a');
118
+
119
+ foo.set_hook({ from: 'a', to: 'b', action: 'foo', kind: 'named', handler: () => true });
120
+ expect(foo.action('foo')).toBe(true);
121
+ expect(foo.state()).toBe('b');
122
+
123
+ });
124
+
125
+
126
+
127
+
128
+
129
+ test('Named hook rejection doesn\'t block transitions', () => {
130
+
131
+ const foo = sm`a 'foo' => b;`;
132
+
133
+ foo.set_hook({ from: 'a', to: 'b', action: 'foo', kind: 'named', handler: () => false });
134
+ expect(foo.action('foo')).toBe(false);
135
+ expect(foo.state()).toBe('a');
136
+
137
+ expect(foo.transition('b')).toBe(true);
138
+ expect(foo.state()).toBe('b');
139
+
140
+ });
141
+
142
+
143
+
144
+
145
+
146
+ describe('Basic hooks on API callpoint', () => {
147
+
148
+ test('Basic hooks call their handler', () => {
149
+
150
+ const handler = jest.fn(x => true),
151
+ uncalled = jest.fn(x => true);
152
+
153
+ expect( () => {
154
+ const _foo = sm`a -> b -> c;`;
155
+ _foo.set_hook({ from: 'a', to: 'b', handler, kind: 'hook' });
156
+ _foo.set_hook({ from: 'b', to: 'a', handler: uncalled, kind: 'hook' });
157
+ _foo.set_hook({ from: 'b', to: 'c', handler: uncalled, kind: 'hook' });
158
+ _foo.transition('b');
159
+ })
160
+ .not.toThrow();
161
+
162
+ // should hook from first, but not from second
163
+ expect(handler.mock.calls.length).toBe(1);
164
+ expect(uncalled.mock.calls.length).toBe(0);
165
+
166
+ } );
167
+
168
+ test('Named hooks call their handler', () => {
169
+
170
+ const handler = jest.fn(x => true),
171
+ uncalled = jest.fn(x => true);
172
+
173
+ expect( () => {
174
+ const _foo = sm`a 'next' -> b 'next' -> c;`;
175
+ _foo.set_hook({ from: 'a', to: 'b', handler, kind: 'named', action: 'next' });
176
+ _foo.set_hook({ from: 'a', to: 'b', handler: uncalled, kind: 'named', action: 'borg' });
177
+ _foo.set_hook({ from: 'b', to: 'a', handler: uncalled, kind: 'named', action: 'next' });
178
+ _foo.action('next');
179
+ _foo.action('next');
180
+ })
181
+ .not.toThrow();
182
+
183
+ // should hook from first, but not from second
184
+ expect(handler.mock.calls.length).toBe(1);
185
+ expect(uncalled.mock.calls.length).toBe(0);
186
+
187
+ } );
188
+
189
+ test('Forced hooks call their handler', () => {
190
+
191
+ const handler = jest.fn(x => true),
192
+ uncalled = jest.fn(x => true);
193
+
194
+ expect( () => {
195
+ const _foo = sm`a ~> b ~> c;`;
196
+ _foo.set_hook({ from: 'a', to: 'b', handler, kind: 'hook' });
197
+ _foo.set_hook({ from: 'b', to: 'a', handler: uncalled, kind: 'hook' });
198
+ _foo.set_hook({ from: 'b', to: 'c', handler: uncalled, kind: 'hook' });
199
+ _foo.force_transition('b');
200
+ })
201
+ .not.toThrow();
202
+
203
+ // should hook from first, but not from second
204
+ expect(handler.mock.calls.length).toBe(1);
205
+ expect(uncalled.mock.calls.length).toBe(0);
26
206
 
207
+ } );
27
208
 
28
209
  });
package/src/ts/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- const version: string = "5.44.0";
2
+ const version: string = "5.45.0";
3
3
  export { version };