Skip to content

Commit 698e518

Browse files
committed
Version 1.0.0-alpha.5
Squashed commit of the following: commit ed1efa5 Author: Abdullah Ali <[email protected]> Date: Fri Nov 2 10:35:40 2018 +0200 build: Upgrade to latest Node.js LTS: 10.13.0 commit 188ff9d Author: Abdullah Ali <[email protected]> Date: Fri Nov 2 10:30:13 2018 +0200 chore: Version bump: 1.0.0-alpha5 commit 4d32b4e Author: Abdullah Ali <[email protected]> Date: Fri Nov 2 10:28:20 2018 +0200 feat: Action priorities A new decorator to specify which actions override others. A higher prioirity means an action gets to override all those below it when updating the state. commit b9110b8 Author: Abdullah Ali <[email protected]> Date: Sat Sep 29 10:49:50 2018 +0200 test: Fix unused variable/parameters in the tests. commit f77a4a6 Author: Abdullah Ali <[email protected]> Date: Sat Sep 29 10:48:45 2018 +0200 docs(Spec): Fix spec pseudocode examples. commit e536638 Merge: 452ec70 555f566 Author: Abdullah Ali <[email protected]> Date: Wed Sep 26 20:18:51 2018 +0200 Merge branch 'master' into devel commit 452ec70
1 parent 3269114 commit 698e518

10 files changed

+135
-47
lines changed

Diff for: .travis.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
language: nodejs
22

3-
node_js: 8.12.0
3+
node_js:
4+
- "8.12.0"
5+
- "10.13.0"
46

57
cache:
68
directories:
79
- node_modules
810

911
before_script:
12+
- nvm install stable
13+
- nvm use stable
1014
- npm install
1115
- npm run build
1216

Diff for: README.md

+4-5
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ When both traits are present in a single machine, the NPC will potentially exhib
121121

122122
Here are some abstract syntax examples for a full pseudo-language based on this pattern. In this theoretical language, the program itself is a state machine, variables of the `MachineState` are global variables, and all of the primitives described above are part of the language itself.
123123

124-
You can read about the original idea (slightly outdated) [in this proposal](https://gist.github.com/voodooattack/ccb1d18112720a8de5be660dbb80541c).
125-
126124
This is mostly pseudo-javascript with two extra `when` and `exit` keywords, and using a hypothetical decorator syntax to specify action metadata.
127125

128126
The decorators are completely optional, and the currently proposed ones are:
@@ -137,7 +135,7 @@ Action decorators may only precede a `when` block, and will only apply to that b
137135

138136
- `@inhibitedBy(action_name)` Prevents this action from triggering if another by `action_name` will execute during this tick. Can be used multiple times with the same action and different inhibitors.
139137

140-
- `@priority(number)` Sets a numeric priority for the action. This will influence the order of evaluation inside the main loop. Actions with higher priority values are evaluated last, meaning that they will take precedence if there's a conflict from multiple actions trying to update the same variable during the same tick.
138+
- `@priority(expression)` Sets a priority for the action. (Default is 0) This will influence the order of evaluation inside the main loop. Actions with lower priority values are evaluated last, while actions with higher priority values are evaluated first, meaning that they will take precedence if there's a conflict from multiple actions trying to update the same variable during the same tick. Can be a literal numeric value or an expression that returns a signed numeric value.
141139

142140
##### Control decorators:
143141

@@ -151,8 +149,9 @@ Action decorators may only precede a `when` block, and will only apply to that b
151149

152150
```typescript
153151
// maximum number of primes to brute-force before exiting,
154-
// note that this variable is a readonly external input
155-
@input('maxPrimes')
152+
// note that this variable is a readonly external input,
153+
// and is read only once on startup.
154+
@input('once')
156155
const maxPrimes: number = 10;
157156

158157
let counter = 2; // starting counting up from 2

Diff for: package-lock.json

+30-24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "when-ts",
3-
"version": "1.0.0-alpha.4",
3+
"version": "1.0.0-alpha.5",
44
"description": "When: A software design pattern for building event-based recombinant state machines.",
55
"main": "dist/lib/src/index.js",
66
"types": "dist/types/src/index.d.ts",
@@ -31,7 +31,7 @@
3131
"tslint": "^5.10.0",
3232
"tslint-config-prettier": "^1.13.0",
3333
"tslint-config-standard": "^8.0.1",
34-
"typedoc": "^0.12.0",
34+
"typedoc": "^0.13.0",
3535
"typescript": "^3.0.3"
3636
},
3737
"peerDependencies": {

Diff for: src/index.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'reflect-metadata';
2-
import { actionMetadataKey, inputMetadataKey } from './actionMetadataKey';
3-
import { ActivationCond, InputPolicy, MachineInputSource, MachineState } from './interfaces';
2+
import { actionMetadataKey, inputMetadataKey, priorityMetadataKey } from './metadataKeys';
3+
import { ActivationCond, InputPolicy, MachineInputSource, MachineState, PriorityExpression } from './interfaces';
44
import { StateMachine } from './stateMachine';
55
import { chainWhen, ConditionBuilder, ConstructorOf, InputMapping, WhenDecoratorWithChain } from './util';
66

@@ -111,5 +111,19 @@ export function input<S extends MachineState, I extends MachineInputSource,
111111
};
112112
}
113113

114-
export type StateObject<S extends MachineState, I extends MachineInputSource = any> =
115-
S & Readonly<I>;
114+
export function priority<
115+
S extends MachineState, I extends MachineInputSource = any,
116+
M extends StateMachine<S, I> = any>
117+
(
118+
priority: number| PriorityExpression<S, I>,
119+
chainedHistory: ConditionBuilder<S, I>[] = []
120+
): WhenDecoratorWithChain<S, I> {
121+
function definePriority(_: ConstructorOf<M>, __: string | symbol, descriptor: PropertyDescriptor) {
122+
Reflect.defineMetadata(priorityMetadataKey, priority, descriptor.value);
123+
}
124+
return chainWhen<S, I>([definePriority, ...chainedHistory]);
125+
}
126+
127+
export type StateObject<
128+
S extends MachineState,
129+
I extends MachineInputSource = any> = S & Readonly<I>;

Diff for: src/interfaces.ts

+7
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export type ActivationAction<State extends MachineState, InputSource extends Mac
4343
(state: Readonly<State & InputSource>, machine: StateMachine<State, InputSource>)
4444
=> Pick<State, fields> | void;
4545

46+
/**
47+
* An activation condition, takes two arguments and must return true for the associated action to fire.
48+
*/
49+
export type PriorityExpression<State extends MachineState, InputSource extends MachineInputSource> =
50+
(state: Readonly<State & InputSource>, machine: StateMachine<State, InputSource>) => number;
51+
52+
4653

4754
/**
4855
* The HistoryManager interface allows for state manipulation and the rewinding of a program.

Diff for: src/actionMetadataKey.ts renamed to src/metadataKeys.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
export const actionMetadataKey = Symbol('when-action');
33
/** @ignore */
44
export const inputMetadataKey = Symbol('when-input');
5+
/** @ignore */
6+
export const priorityMetadataKey = Symbol('when-priority');

Diff for: src/stateMachine.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { actionMetadataKey, inputMetadataKey } from './actionMetadataKey';
1+
import { actionMetadataKey, inputMetadataKey, priorityMetadataKey } from './metadataKeys';
22
import { HistoryManager } from './historyManager';
33
import { ActivationAction, ActivationCond, MachineInputSource, MachineState } from './index';
44
import { IHistory } from './interfaces';
@@ -40,12 +40,17 @@ export class StateMachine<S extends MachineState, I extends MachineInputSource =
4040
*/
4141
protected constructor(initialState: S, inputSource?: I) {
4242
const properties = getAllMethods(this);
43+
const program: [ActivationCond<S, I>, any, number][] = [];
4344
for (let m of properties) {
4445
if (Reflect.hasMetadata(actionMetadataKey, m)) {
4546
const cond = Reflect.getMetadata(actionMetadataKey, m);
46-
this._program.set(cond, m as any);
47+
const priority = Reflect.getMetadata(priorityMetadataKey, m);
48+
program.push([cond, m, typeof priority === 'number' ? priority : 0]);
4749
}
4850
}
51+
this._program = new Map(
52+
program.sort(([,,a], [,,b]) => a - b) as any
53+
);
4954
this._history = new HistoryManager<S, I, this>(this, initialState, inputSource,
5055
inputSource ? Reflect.getMetadata(inputMetadataKey, inputSource) : []
5156
);

Diff for: src/util.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { actionMetadataKey, inputMetadataKey } from './actionMetadataKey';
2-
import { inhibitedBy, InputPolicy, MachineInputSource, StateMachine, unless, when } from './index';
1+
import { actionMetadataKey, inputMetadataKey } from './metadataKeys';
2+
import { inhibitedBy, InputPolicy, MachineInputSource, priority, StateMachine, unless, when } from './index';
33
import { ActivationCond } from './interfaces';
44

55
/** @ignore */
@@ -37,13 +37,14 @@ export function getInheritanceTree<T>(entity: ConstructorOf<T>): Function[] {
3737

3838
/** @ignore */
3939
export type ConditionBuilder<S, I> = {
40-
(T: any, methodName: string | symbol, descriptor: PropertyDescriptor): ActivationCond<S, I>
40+
(T: any, methodName: string | symbol, descriptor: PropertyDescriptor): ActivationCond<S, I> | void
4141
}
4242

4343
export type WhenDecoratorChainResult<S, I> = {
4444
andWhen(cond: ActivationCond<S, I> | true): WhenDecoratorWithChain<S, I>;
4545
unless(condition: ActivationCond<S, I>): WhenDecoratorWithChain<S, I>;
4646
inhibitedBy<M>(inhibitor: keyof M): WhenDecoratorWithChain<S, I>;
47+
priority(p: number): WhenDecoratorWithChain<S, I>;
4748
}
4849
export type WhenDecoratorWithChain<S, I> = MethodDecorator & WhenDecoratorChainResult<S, I>;
4950

@@ -55,22 +56,24 @@ export function chainWhen<S, I>(chainedHistory: ConditionBuilder<S, I>[]): WhenD
5556
{
5657
andWhen: (...args: any[]) => (when as any)(...args, chainedHistory),
5758
unless: (...args: any[]) => (unless as any)(...args, chainedHistory),
58-
inhibitedBy: (...args: any[]) => (inhibitedBy as any)(...args, chainedHistory)
59+
inhibitedBy: (...args: any[]) => (inhibitedBy as any)(...args, chainedHistory),
60+
priority: (...args: any[]) => (priority as any)(...args, chainedHistory),
5961
}
6062
);
6163
}
6264

6365
/**
6466
* Build a decorator out of a list of conditions.
65-
* @param {ActivationCond<S>[]} builders
67+
* @param {ActivationCond[]} builders
6668
* @param {boolean} invert
6769
* @return {(_: any, _methodName: (string | symbol), descriptor: PropertyDescriptor) => void}
6870
*/
6971
/** @ignore */
7072
function buildDecorator<S, I>(builders: ConditionBuilder<S, I>[]) {
7173
return function decorator(Type: any, methodName: string | symbol, descriptor: PropertyDescriptor)
7274
{
73-
const built = builders.map(builder => builder(Type, methodName, descriptor));
75+
const built = builders.map(builder => builder(Type, methodName, descriptor))
76+
.filter(cond => typeof cond === 'function');
7477
const cond = built.length > 1 ? function () {
7578
for (let current of built) {
7679
// tell TS to ignore the next line because we specifically want a non-contextual `this`

Diff for: tests/stateMachine.test.ts

+51-3
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ describe('StateMachine', () => {
182182
const test = new TestMachine();
183183
const result = test.run(true);
184184

185+
expect(result).toEqual({ value: 3 });
185186
expect(series).toEqual([[0, 1], [1, 2], [2, 3], [3, 4], [1, 2], [2, 3], [3, 4]]);
186187

187188
});
@@ -503,12 +504,12 @@ describe('StateMachine', () => {
503504
}
504505

505506
@when<BlankState>(true)
506-
keepMe(_, m: BlankMachine) {
507+
keepMe(_: any, m: BlankMachine) {
507508
return { tick: m.history.tick };
508509
}
509510

510-
@when((_, m) => m.history.tick > 5)
511-
exitMachine(_, m: BlankMachine) {
511+
@when<BlankState>((_, m) => m.history.tick > 5)
512+
exitMachine(_: any, m: BlankMachine) {
512513
m.exit();
513514
}
514515
}
@@ -582,4 +583,51 @@ describe('StateMachine', () => {
582583
expect(test.history.records).toEqual(expectedHistory);
583584
});
584585

586+
it('Can handle priorities', () => {
587+
588+
type State = {
589+
value: number;
590+
}
591+
592+
class TestMachine extends StateMachine<State> {
593+
constructor() {
594+
super({ value: 0 });
595+
}
596+
597+
@when<State>(state => Math.abs(state.value) >= 5)
598+
exitWhenDone(_: State, m: TestMachine) {
599+
// this should execute on tick 6
600+
expect(m.history.tick).toEqual(6);
601+
m.exit();
602+
}
603+
604+
605+
// overrides the increment action via priority
606+
@when<State>(state => state.value > -5).priority(10)
607+
decrementOncePerTick(s: State) {
608+
return { value: s.value - 1 };
609+
}
610+
611+
@when<State>(state => state.value < 5)
612+
incrementOncePerTick(s: State) {
613+
return { value: s.value + 1 };
614+
}
615+
616+
update(tick: number): StateObject<BlankState, IBlankMachineInputs> {
617+
let old = this.snapshot(tick);
618+
this._fixed = 10;
619+
this._increments++;
620+
this._random = Math.round(Math.random() * 1000);
621+
if (old.tick % 2 !== 0) old.increments = old.increments === 0 ? 0 : old.increments - 1;
622+
return { ...old, tick };
623+
}
624+
}
625+
626+
const test = new TestMachine();
627+
const result = test.run();
628+
629+
expect(result).toEqual({ value: -5 });
630+
});
631+
632+
585633
});

0 commit comments

Comments
 (0)