diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cf6fff --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.idea diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..2839dd9 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,4 @@ +semi: false # 不添加分号 +singleQuote: true # js 内使用单引号 +trailingComma: 'all' # 末尾总是添加逗号 +arrowParens: 'always' # 参数总是用括号包裹 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2f9c91 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# wow-state-machine + +像多线程那样去轮询多个状态,不同的状态满足后去执行不同的定时任务。 + +该状态机基本是为写游戏自动脚本量身定做,它就是整个脚本的"调度中心"。即使是基于 Node.js 的单线程,你也能够实现"同时"检测角色血条,掉落物品,游戏状态等等各种来触发不同的操作,搭配 [dm.dll](/documents/dm.dll/) 食用更佳! + +[文档地址](https://aweiu.com/documents/wow-state-machine/) diff --git a/dist/promise-interval.d.ts b/dist/promise-interval.d.ts new file mode 100644 index 0000000..ca9281c --- /dev/null +++ b/dist/promise-interval.d.ts @@ -0,0 +1,9 @@ +declare type PromiseFun = () => Promise; +export default class PromiseInterval { + private ms; + private timer?; + constructor(ms: number); + start(promiseFun: PromiseFun, onError?: (e: Error) => any): void; + stop(): void; +} +export {}; diff --git a/dist/promise-interval.js b/dist/promise-interval.js new file mode 100644 index 0000000..1f9fcfc --- /dev/null +++ b/dist/promise-interval.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const set_promise_interval_1 = require("set-promise-interval"); +class PromiseInterval { + constructor(ms) { + this.ms = ms; + } + start(promiseFun, onError) { + if (this.timer === undefined) { + this.timer = set_promise_interval_1.default(async () => { + try { + await promiseFun(); + } + catch (e) { + this.stop(); + if (onError) + onError(e); + else + throw e; + } + }, this.ms); + } + } + stop() { + set_promise_interval_1.clearPromiseInterval(this.timer); + this.timer = undefined; + } +} +exports.default = PromiseInterval; diff --git a/dist/state-machine.d.ts b/dist/state-machine.d.ts new file mode 100644 index 0000000..1f927c8 --- /dev/null +++ b/dist/state-machine.d.ts @@ -0,0 +1,32 @@ +declare type DataOrPromiseData = T | Promise; +declare type Publisher = () => DataOrPromiseData; +declare type Subscriber = () => any; +declare type OnTick = (state: T, lastState: T | undefined, isFirstTick: boolean) => any; +declare type OnError = (e: Error) => any; +declare type OnTimeout = (state: T) => any; +export default class StateMachine { + private publisher; + private onTickCallback?; + private onErrorCallback?; + private onTimeoutCallback?; + private mainLoop?; + private lastState?; + private states; + private runners; + private timeoutChecker; + constructor(publisher: Publisher); + private errorHandle; + private timeoutHandle; + private startTimeoutChecker; + private stopTimeoutChecker; + private startRunner; + private stopRunner; + private publish; + on(state: T, stateMachineOrSubscriber: StateMachine | Subscriber, timeout?: number, tick?: number): this; + onTick(callback: OnTick): this; + onError(callback: OnError): this; + onTimeout(callback: OnTimeout): this; + start(tick?: number): void; + stop(): void; +} +export {}; diff --git a/dist/state-machine.js b/dist/state-machine.js new file mode 100644 index 0000000..8aefbe6 --- /dev/null +++ b/dist/state-machine.js @@ -0,0 +1,128 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const promise_interval_1 = require("./promise-interval"); +class StateMachine { + constructor(publisher) { + this.states = {}; + this.runners = []; + this.timeoutChecker = []; + this.publisher = publisher; + } + errorHandle(e) { + this.stop(); + if (this.onErrorCallback) + this.onErrorCallback(e); + else + throw e; + } + timeoutHandle(state) { + this.stop(); + if (this.onTimeoutCallback) + this.onTimeoutCallback(state); + else + throw Error(`state: ${state}超时`); + } + startTimeoutChecker(state, ms) { + if (ms) { + this.timeoutChecker.push(setTimeout(() => this.timeoutHandle(state), ms)); + } + } + stopTimeoutChecker() { + for (let checker of this.timeoutChecker) + clearTimeout(checker); + this.timeoutChecker = []; + } + startRunner(stateMachineOrSubscriber, tick) { + if (stateMachineOrSubscriber instanceof StateMachine) { + try { + stateMachineOrSubscriber.onError((e) => this.errorHandle(e)); + } + catch (e) { + /* tslint:disable:no-empty */ + } + try { + stateMachineOrSubscriber.onTimeout((state) => this.onTimeout(state)); + } + catch (e) { + /* tslint:disable:no-empty */ + } + stateMachineOrSubscriber.start(tick); + this.runners.push(stateMachineOrSubscriber); + } + else { + const runner = new promise_interval_1.default(tick); + runner.start(stateMachineOrSubscriber, (e) => this.errorHandle(e)); + this.runners.push(runner); + } + } + stopRunner() { + for (let runner of this.runners) + runner.stop(); + this.runners = []; + } + async publish(isFirstTick) { + let state = await this.publisher(); + if (this.onTickCallback) + await this.onTickCallback(state, this.lastState, isFirstTick); + if (this.lastState !== state) { + this.lastState = state; + this.stopRunner(); + this.stopTimeoutChecker(); + if (this.states.hasOwnProperty(state)) { + const subscribers = this + .states[state]; + for (let [subscriber, timeout, tick] of subscribers) { + this.startTimeoutChecker(state, timeout); + if (tick === -Infinity) { + await subscriber(); + this.stopTimeoutChecker(); + } + else + this.startRunner(subscriber, tick); + } + } + } + } + on(state, stateMachineOrSubscriber, timeout = 0, tick = 200) { + if (!this.states.hasOwnProperty(state)) + this.states[state] = []; + this.states[state].push([stateMachineOrSubscriber, timeout, tick]); + return this; + } + onTick(callback) { + if (this.onTickCallback) + throw Error('just allow one TickHandler'); + this.onTickCallback = callback; + return this; + } + onError(callback) { + if (this.onErrorCallback) + throw Error('just allow one ErrorHandler'); + this.onErrorCallback = callback; + return this; + } + onTimeout(callback) { + if (this.onTimeoutCallback) + throw Error('just allow one TimeoutHandler'); + this.onTimeoutCallback = callback; + return this; + } + start(tick = 200) { + let isFirstTick = true; + this.lastState = undefined; + this.mainLoop = new promise_interval_1.default(tick); + this.mainLoop.start(() => { + const _isFirstTick = isFirstTick; + if (isFirstTick) + isFirstTick = false; + return this.publish(_isFirstTick); + }, (e) => this.errorHandle(e)); + } + stop() { + if (this.mainLoop) + this.mainLoop.stop(); + this.stopRunner(); + this.stopTimeoutChecker(); + } +} +exports.default = StateMachine; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1396c9a --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "wow-state-machine", + "version": "1.0.1", + "description": "像多线程那样去轮询多个状态,不同的状态满足后去执行不同的任务", + "keywords": [ + "state-machine", + "async", + "thread", + "interval", + "timer" + ], + "main": "dist/wow-state-machine.js", + "typings": "types/wow-state-machine.d.ts", + "author": "aweiu", + "license": "ISC", + "repository": { + "type": "git", + "url": "git+https://github.com/aweiu/wow-state-machine" + }, + "dependencies": { + "set-promise-interval": "^1.0.2" + }, + "devDependencies": { + "typescript": "^3.5.3" + } +} diff --git a/src/promise-interval.ts b/src/promise-interval.ts new file mode 100644 index 0000000..c363a2a --- /dev/null +++ b/src/promise-interval.ts @@ -0,0 +1,31 @@ +import setPromiseInterval, { clearPromiseInterval } from 'set-promise-interval' + +type PromiseFun = () => Promise + +export default class PromiseInterval { + private ms: number + private timer?: number + + constructor(ms: number) { + this.ms = ms + } + + start(promiseFun: PromiseFun, onError?: (e: Error) => any) { + if (this.timer === undefined) { + this.timer = setPromiseInterval(async () => { + try { + await promiseFun() + } catch (e) { + this.stop() + if (onError) onError(e) + else throw e + } + }, this.ms) + } + } + + stop() { + clearPromiseInterval(this.timer) + this.timer = undefined + } +} diff --git a/src/state-machine.ts b/src/state-machine.ts new file mode 100644 index 0000000..59b331e --- /dev/null +++ b/src/state-machine.ts @@ -0,0 +1,151 @@ +import PromiseInterval from './promise-interval' + +type DataOrPromiseData = T | Promise +type Publisher = () => DataOrPromiseData +type Subscriber = () => any +type OnTick = ( + state: T, + lastState: T | undefined, + isFirstTick: boolean, +) => any +type OnError = (e: Error) => any +type OnTimeout = (state: T) => any + +export default class StateMachine { + private publisher: Publisher + private onTickCallback?: OnTick + private onErrorCallback?: OnError + private onTimeoutCallback?: OnTimeout + private mainLoop?: PromiseInterval + private lastState?: T + private states: any = {} + private runners: Array | PromiseInterval> = [] + private timeoutChecker: number[] = [] + + constructor(publisher: Publisher) { + this.publisher = publisher + } + + private errorHandle(e: Error) { + this.stop() + if (this.onErrorCallback) this.onErrorCallback(e) + else throw e + } + + private timeoutHandle(state: T) { + this.stop() + if (this.onTimeoutCallback) this.onTimeoutCallback(state) + else throw Error(`state: ${state}超时`) + } + + private startTimeoutChecker(state: T, ms: number) { + if (ms) { + this.timeoutChecker.push(setTimeout(() => this.timeoutHandle(state), ms)) + } + } + + private stopTimeoutChecker() { + for (let checker of this.timeoutChecker) clearTimeout(checker) + this.timeoutChecker = [] + } + + private startRunner( + stateMachineOrSubscriber: StateMachine | Subscriber, + tick: number, + ) { + if (stateMachineOrSubscriber instanceof StateMachine) { + try { + stateMachineOrSubscriber.onError((e) => this.errorHandle(e)) + } catch (e) { + /* tslint:disable:no-empty */ + } + try { + stateMachineOrSubscriber.onTimeout((state) => this.onTimeout(state)) + } catch (e) { + /* tslint:disable:no-empty */ + } + stateMachineOrSubscriber.start(tick) + this.runners.push(stateMachineOrSubscriber) + } else { + const runner = new PromiseInterval(tick) + runner.start(stateMachineOrSubscriber, (e) => this.errorHandle(e)) + this.runners.push(runner) + } + } + + private stopRunner() { + for (let runner of this.runners) runner.stop() + this.runners = [] + } + + private async publish(isFirstTick: boolean) { + let state: T = await this.publisher() + if (this.onTickCallback) + await this.onTickCallback(state, this.lastState, isFirstTick) + if (this.lastState !== state) { + this.lastState = state + this.stopRunner() + this.stopTimeoutChecker() + if (this.states.hasOwnProperty(state)) { + const subscribers: [Subscriber, number, number][] = (this + .states as any)[state] + for (let [subscriber, timeout, tick] of subscribers) { + this.startTimeoutChecker(state, timeout) + if (tick === -Infinity) { + await subscriber() + this.stopTimeoutChecker() + } else this.startRunner(subscriber, tick) + } + } + } + } + + on( + state: T, + stateMachineOrSubscriber: StateMachine | Subscriber, + timeout = 0, + tick = 200, + ) { + if (!this.states.hasOwnProperty(state)) this.states[state] = [] + this.states[state].push([stateMachineOrSubscriber, timeout, tick]) + return this + } + + onTick(callback: OnTick) { + if (this.onTickCallback) throw Error('just allow one TickHandler') + this.onTickCallback = callback + return this + } + + onError(callback: OnError) { + if (this.onErrorCallback) throw Error('just allow one ErrorHandler') + this.onErrorCallback = callback + return this + } + + onTimeout(callback: OnTimeout) { + if (this.onTimeoutCallback) throw Error('just allow one TimeoutHandler') + this.onTimeoutCallback = callback + return this + } + + start(tick: number = 200) { + let isFirstTick = true + this.lastState = undefined + this.mainLoop = new PromiseInterval(tick) + this.mainLoop.start( + () => { + const _isFirstTick = isFirstTick + if (isFirstTick) isFirstTick = false + return this.publish(_isFirstTick) + }, + (e) => this.errorHandle(e), + ) + } + + stop() { + if (this.mainLoop) this.mainLoop.stop() + this.stopRunner() + this.stopTimeoutChecker() + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..7affabe --- /dev/null +++ b/test.js @@ -0,0 +1,59 @@ +const { default: StateMachine } = require('./dist/state-machine') + +let num = 0 +setInterval(() => { + // 随机生成数字 1-10 + num = Math.ceil(Math.random() * 10) +}, 1000) + +// 这是一个用于测试的始终返回 state1 的状态机 +const otherStateMachine = new StateMachine(() => 'state1').on('state1', () => + console.log('otherStateMachine', 'state1'), +) + +const stateMachine = new StateMachine(() => { + // 模拟十分之一概率的错误 + if (num === 1) throw Error('发生错误了') + // 模拟十分之二的概率返回 state1 + else if (num < 4) return 'state1' + // 模拟十分之二的概率返回 state2 + else if (num < 6) return 'state2' + // 模拟十分之二的概率返回 state3 + else if (num < 8) return 'state3' + // 模拟十分之一的概率返回 state4 + else if (num === 8) return 'state4' + // 模拟剩下十分之二的概率返回 unknown + else return 'unknown' +}) + // 任意状态发生的时候都会触发 onTick + .onTick((state, lastState, isFirstTick) => + console.log(state, lastState, isFirstTick), + ) + // 当 state1 发生的时候,启动一个每 100 毫秒输出一次 'state1' 的定时任务(如果发生了其他事情,该定时任务会停止 + .on('state1', () => console.log('state1'), 0, 100) + // 当 state2 发生的时候,启动一个每 100 毫秒(tick 默认 200)输出一次 'state2' 的定时任务。如果 state2 持续了超过 10 秒钟,则触发超时 + .on('state2', () => console.log('state2'), 10 * 1000) + // 当 state3 发生的时候启动 otherStateMachine 状态机(如果没发生,该状态机会被自动停止 + .on('state3', otherStateMachine, 0, 500) + // 当 state4 发生的时候,"阻塞"整个状态机(tick 被设置成了 -Infinity),直到 state4 的任务执行完毕后,状态机才会继续工作 + .on( + 'state4', + () => { + console.log('state4 开始了') + return new Promise((resolve) => { + setTimeout(() => resolve(console.log('state4 结束了')), 3 * 1000) + }) + }, + 0, + -Infinity, + ) + // 如果超过了 2 秒钟啥事情都没有发生(unknown 状态),则触发超时 + .on('unknown', () => console.log('没有事情发生...'), 2 * 1000) + // 捕获状态机执行期间的所有超时,当超时发生时状态机会终止 + .onTimeout((state) => console.log(state, '超时了')) + // 捕获状态机执行期间的所有异常,当异常发生时状态机会终止 + .onError((e) => console.log(e)) + +// 启动状态机,让其每 500 毫秒检测一次状态(tick 默认 200) +stateMachine.start(500) +// stateMachine.stop() // 终止状态机 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..24e4af0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "node", + "module": "commonjs", + "target": "es2017", + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..8bbbd12 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,13 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +set-promise-interval@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/set-promise-interval/-/set-promise-interval-1.0.2.tgz#9a45d4dd608c62e1dd560e28cff2375c7708b6ac" + integrity sha512-s7EylxOLlqahgCAMHhjeZGwQNZUYTX9iy6qSL7qG7dc6uVgMhe9SJJNPUHf1Qf1yn3Bxru65t3iqr09+dcNyaA== + +typescript@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" + integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==