Last active
September 18, 2018 05:44
-
-
Save voodooattack/ce7e4429091fa45df8e2aeb6bec8a702 to your computer and use it in GitHub Desktop.
when-ts: first draft
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'reflect-metadata'; | |
export interface MachineState { | |
} | |
/** | |
* An activation condition, takes two arguments and must return true for the associated action to fire. | |
*/ | |
export type ActivationCond<State extends MachineState> = | |
(state: Readonly<State>, machine: EventMachine<State>) => boolean; | |
/** | |
* An activation action, takes two arguments and will only be executed during a tick | |
* when the associated conditon returns true. | |
*/ | |
export type ActivationAction<State extends MachineState> = | |
(state: Readonly<State>, machine: EventMachine<State>) => Partial<State> | undefined; | |
/** @internal */ | |
const programMetadataKey = Symbol('when-program'); | |
/** @internal */ | |
function getAllMethods(object: any) { | |
let current = object; | |
let props: string[] = []; | |
do { | |
props.push(...Object.getOwnPropertyNames(current)); | |
} while (current = Object.getPrototypeOf(current)); | |
return Array.from(new Set(props.map(p => typeof object[p] === 'function' ? object[p] : null) | |
.filter(p => p !== null))); | |
} | |
/** | |
* The HistoryManager class manages the state history of a program. | |
*/ | |
export class HistoryManager<S extends MachineState> { | |
private _records: S[] = []; | |
private _tick: number = 0; | |
private _nextState: Partial<S>; | |
private _maxHistory: number = Infinity; | |
/** | |
* Constrctor with an initial state. | |
* @param {S} _initialState The initial program state. | |
*/ | |
constructor(protected readonly _initialState: S) { | |
this._nextState = _initialState; | |
this._nextTick(); | |
} | |
/** | |
* Get the current tick number. | |
* @returns {number} | |
*/ | |
get tick() { | |
return this._tick; | |
} | |
/** | |
* Limit the number of recorded history states. | |
* @param {number} limit The maximum number of states to keep. | |
*/ | |
limit(limit: number) { | |
if (limit < 0) return; | |
if (limit < this._maxHistory) { | |
// trim back the record history. | |
this._records.splice(0, this._records.length - limit); | |
} | |
this._maxHistory = limit; | |
} | |
/** | |
* Rewind time by `n` ticks, the rest of the currently executing tick will be aborted. | |
* A partial state can be passed as the second argument to mutate the rewound state. | |
* @param {number} n The number of ticks to rewind, defaults to Infinity. | |
* @param {Partial<S extends MachineState>} mutate Any mutations to apply to the state after rewinding. | |
*/ | |
rewind(n: number = Infinity, mutate?: Partial<S>) { | |
if (n <= this._maxHistory && Number.isFinite(n)) { | |
this._records.splice(n, this.records.length - n); | |
this._tick -= n; | |
} else { | |
this._records.splice(0, this._records.length); | |
this._tick = 0; | |
this._records.push(this._initialState); | |
} | |
if (mutate) { | |
this._records[this._records.length - 1] = Object.assign(Object.create(null), | |
this.currentState, mutate); | |
} | |
this._resetTick(); | |
} | |
/** | |
* Clears the state history. Rewinds to the beginning, and the rest of the current tick will be aborted. | |
*/ | |
clear() { | |
this.rewind(Infinity); | |
} | |
/** | |
* Returns the entire state history. | |
* @returns {ReadonlyArray<S extends MachineState>} | |
*/ | |
get records(): ReadonlyArray<S> { | |
return this._records; | |
} | |
/** | |
* Returns the current state. | |
* @returns {Partial<S extends MachineState>} | |
*/ | |
get currentState(): Readonly<S> { | |
return this.records[this.records.length - 1] || this._initialState; | |
} | |
/** | |
* Returns the next state being updated. | |
* @returns {Partial<S extends MachineState>} | |
*/ | |
get nextState(): Readonly<Partial<S>> { | |
return this._nextState; | |
} | |
protected _resetTick() { | |
if (this._records.length) | |
this._nextState = Object.assign(Object.create(null), this.records[this.records.length - 1]); | |
else | |
this._nextState = Object.assign(Object.create(null), this._initialState); | |
} | |
/** @internal */ | |
_mutateTick(p: Partial<S>) { | |
return Object.assign(this._nextState, p); | |
} | |
/** @internal */ | |
_nextTick() { | |
const nextState = this.nextState as Readonly<S>; | |
this._records.push(nextState); | |
if (this._records.length > this._maxHistory) { | |
this._records.splice(this._maxHistory, this._records.length - this._maxHistory); | |
} | |
this._resetTick(); | |
this._tick++; | |
} | |
} | |
/** | |
* Your state machine should inherit the `EventMachine<YourStateInterface>` class. | |
*/ | |
export class EventMachine<S extends MachineState> { | |
/** | |
* The active state machine program. | |
* @type {Map} | |
* @private | |
*/ | |
private _program: Map<ActivationCond<S>, ActivationAction<S>> = new Map(); | |
/** | |
* | |
* @type {HistoryManager<S extends MachineState>} | |
* @private | |
*/ | |
private _history = new HistoryManager<S>(this._initialState); | |
private _exitState?: Readonly<S>; | |
/** | |
* Constructor, requires an initial state. | |
* @param {S} _initialState | |
*/ | |
constructor(protected readonly _initialState: S) { | |
const properties = getAllMethods(this); | |
for (let m of properties) { | |
if (Reflect.hasMetadata(programMetadataKey, m)) { | |
const cond = Reflect.getMetadata(programMetadataKey, m); | |
this._program.set(cond, m as any); | |
} | |
} | |
} | |
/** | |
* Returns the history manager object. | |
* @returns {HistoryManager<S extends MachineState>} | |
*/ | |
get history() { | |
return this._history; | |
} | |
/** | |
* The state at program exit. Returns `undefined` unless the program has ended. | |
* @returns {Readonly<S extends MachineState> | undefined} | |
*/ | |
get exitState() { | |
return this._exitState; | |
} | |
/** | |
* Advance a single tick and return. | |
* @returns {number} Number of actions fired during this tick. | |
*/ | |
step() { | |
if (this._exitState) | |
return false; | |
let fired = 0; | |
const current = this._history.currentState as Readonly<S>; | |
for (let [cond, body] of this._program) { | |
if (cond.call(this, current, this)) { | |
const newState = body.call(this, current, this); | |
if (this._exitState) break; | |
if (newState) { | |
this._history._mutateTick(newState); | |
} | |
fired++; | |
} | |
} | |
this._history._nextTick(); | |
return fired; | |
} | |
/** | |
* A blocking call that evaluates the state machine until it exits. | |
* @param {boolean} forever | |
* @returns {Readonly<S extends MachineState>} | |
*/ | |
run(forever: boolean = true) { | |
while (!this._exitState) | |
this.step(); | |
return this._exitState; | |
} | |
/** | |
* Resets the state machine to the initial state. | |
* @param {S} initialState (optional) Restart with a different initial state. | |
*/ | |
reset(initialState: S = this._initialState) { | |
this._exitState = undefined; | |
this.history.rewind(Infinity, initialState); | |
} | |
/** | |
* Call this from any action to signal program completion. | |
* @param {Readonly<S extends MachineState>} exitState The exit state to return from .run. | |
*/ | |
exit(exitState?: Readonly<S>) { | |
if (!this._exitState) | |
this._exitState = exitState || this._history.currentState as Readonly<S>; | |
} | |
} | |
/** | |
* A TypeScript decorator to declare a method as an action with an attached a condition. | |
* @param {ActivationCond<S extends MachineState> | boolean} cond The condition to check on every tick. | |
*/ | |
export function when<S extends MachineState = any>(cond: ActivationCond<S> | boolean): MethodDecorator { | |
if (typeof cond === 'boolean') | |
cond = ((v: boolean) => () => v)(cond); | |
return function <T>(type: T, _methodName: string | symbol, descriptor: PropertyDescriptor) { | |
Reflect.defineMetadata(programMetadataKey, cond, descriptor.value); | |
return descriptor; | |
}; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment