├── .travis.yml ├── src ├── index.ts ├── enum.ts ├── task.ts ├── util.ts ├── event.ts ├── scheduler.ts └── driver.ts ├── tsconfig.build.json ├── .prettierrc ├── .vscode └── launch.json ├── tsconfig.json ├── __tests__ ├── __snapshots__ │ └── driver.test.ts.snap └── driver.test.ts ├── LICENSE ├── package.json └── .gitignore /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | 6 | script: 7 | - npm run ci 8 | 9 | cache: 10 | npm: false -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './driver'; 2 | export * from './scheduler'; 3 | export * from './task'; 4 | export * from './event'; 5 | export * from './util'; 6 | export * from './enum'; 7 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "sourceMap": false 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "printWidth": 100, 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // [VSCode 调试中 launch.json 配置不完全指南 | 小胡子哥的个人网站](https://www.barretlee.com/blog/2019/03/18/debugginginvscodetutorial/) 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Attach to node", 7 | "type": "node", 8 | "request": "attach", 9 | "processId": "${command:PickProcess}" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/enum.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | export enum DriverStageEnum { 3 | init = 'init', 4 | stopping = 'stopping', 5 | done = 'done', 6 | running = 'running', 7 | paused = 'paused', 8 | error = 'error', 9 | } 10 | 11 | // eslint-disable-next-line 12 | export enum TaskStageEnum { 13 | init = 'init', 14 | running = 'running', 15 | error = 'error', 16 | dropped = 'dropped', 17 | done = 'done', 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "allowSyntheticDefaultImports": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "noUnusedLocals": true, 13 | "declaration": true, 14 | "downlevelIteration": true, 15 | "strict": true, 16 | "lib": ["es5", "es2015", "dom"] 17 | }, 18 | "includes": ["src", "__tests__"] 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/driver.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`__tests__/driver.test.ts driver 事件流 1`] = ` 4 | Array [ 5 | "DriverStageChangeEvent-init->running", 6 | "TaskStageChangeEvent-BaseTask-1-init->running", 7 | "YieldEvent-1.1", 8 | "YieldEvent-1.2", 9 | "TaskStageChangeEvent-BaseTask-1-running->done", 10 | "TaskStageChangeEvent-BaseTask-2-init->running", 11 | "YieldEvent-2.1", 12 | "YieldEvent-2.2", 13 | "TaskStageChangeEvent-BaseTask-2-running->dropped", 14 | "DriverStageChangeEvent-running->done", 15 | ] 16 | `; 17 | -------------------------------------------------------------------------------- /src/task.ts: -------------------------------------------------------------------------------- 1 | import { TaskStageEnum } from './enum'; 2 | import { getUUid } from './util'; 3 | 4 | type ITaskInitProps = { 5 | readonly iter: IterableIterator; 6 | priority?: number; 7 | minorPriority?: number; 8 | }; 9 | 10 | export class BaseTask { 11 | // 初始化 task 状态 12 | public stage = TaskStageEnum.init; 13 | 14 | /** 运行 ms 数 */ 15 | public ms?: number; 16 | public sendValue?: any; 17 | public error?: Error; 18 | 19 | constructor( 20 | private readonly data: ITaskInitProps, 21 | readonly name: string = getUUid('BaseTask-') 22 | ) {} 23 | 24 | public get iter() { 25 | return this.data.iter; 26 | } 27 | 28 | public get priority() { 29 | return this.data.priority || 0; 30 | } 31 | 32 | public set priority(p: number) { 33 | this.data.priority = p; 34 | } 35 | 36 | public get minorPriority() { 37 | return this.data.minorPriority || 0; 38 | } 39 | 40 | public set minorPriority(p: number) { 41 | this.data.minorPriority = p; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 黄文鉴 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iterator-driver", 3 | "version": "6.0.9", 4 | "description": "Tiny 迭代器驱动", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "dist/index.js", 9 | "scripts": { 10 | "ci": "ah-workflow ci", 11 | "build": "ah-workflow build", 12 | "prepublishOnly": "npm run ci && npm run build", 13 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag v$PACKAGE_VERSION && git push origin --all && git push origin --tags" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/concefly/iterator-driver.git" 18 | }, 19 | "author": "concefly@foxmail.com", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/concefly/iterator-driver/issues" 23 | }, 24 | "homepage": "https://github.com/concefly/iterator-driver#readme", 25 | "devDependencies": { 26 | "@types/node": "^12.7.1", 27 | "ah-workflow": "^1.0.5", 28 | "typescript": "^3.5.2" 29 | }, 30 | "dependencies": { 31 | "ah-event-bus": "^1.0.7" 32 | }, 33 | "sideEffects": false 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | /dist -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { DriverStageEnum, TaskStageEnum } from './enum'; 2 | 3 | export function runtimeMs any>(fn: F): [ReturnType, number] { 4 | const a = new Date(); 5 | const res = fn(); 6 | const b = new Date(); 7 | 8 | const delta = b.valueOf() - a.valueOf(); 9 | 10 | return [res, delta]; 11 | } 12 | 13 | /** 把任意值变成 promise */ 14 | export async function toPromise(data: any): Promise { 15 | // null, undefined .... 16 | if (!data) return Promise.resolve(data); 17 | 18 | // promise 19 | if (typeof data.then === 'function') { 20 | return data; 21 | } 22 | 23 | // array 24 | if (Array.isArray(data)) { 25 | return Promise.all(data.map(d => toPromise(d))); 26 | } 27 | 28 | // 其他都直接返回 29 | return Promise.resolve(data); 30 | } 31 | 32 | let uuid = 0; 33 | export function getUUid(prefix = '') { 34 | return `${prefix}${uuid++}`; 35 | } 36 | 37 | export function enumCond( 38 | spec: { [code in S]: ((ctx: T) => R) | 'error' | 'skip' } 39 | ) { 40 | return (code: S, ctx: T) => { 41 | const fn = spec[code]; 42 | if (fn === 'skip') { 43 | // do nothing 44 | } 45 | if (fn === 'error') throw new Error(`状态错误 ${code}`); 46 | if (typeof fn === 'function') return (fn as any)(ctx) as R; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/event.ts: -------------------------------------------------------------------------------- 1 | import { DriverStageEnum, TaskStageEnum } from './enum'; 2 | import { BaseTask } from './task'; 3 | import { BaseEvent } from 'ah-event-bus'; 4 | 5 | /** 每个 yield 事件 */ 6 | export class YieldEvent extends BaseEvent { 7 | static displayName = 'Yield'; 8 | constructor(public readonly value: any, public readonly task: BaseTask) { 9 | super(); 10 | } 11 | } 12 | 13 | /** driver stage 变化 */ 14 | export class DriverStageChangeEvent extends BaseEvent { 15 | static displayName = 'DriverStageChangeEvent'; 16 | constructor( 17 | public readonly stage: DriverStageEnum, 18 | public readonly extra: { lastStage: DriverStageEnum } 19 | ) { 20 | super(); 21 | } 22 | 23 | public isRunning() { 24 | return this.stage === DriverStageEnum.running; 25 | } 26 | 27 | public isDone() { 28 | return this.stage === DriverStageEnum.done; 29 | } 30 | 31 | public isError() { 32 | return this.stage === DriverStageEnum.error; 33 | } 34 | } 35 | 36 | /** 任务 stage 变化 */ 37 | export class TaskStageChangeEvent extends BaseEvent { 38 | static displayName = 'TaskStageChangeEvent'; 39 | constructor( 40 | public readonly stage: TaskStageEnum, 41 | public readonly extra: { task: BaseTask; lastStage: TaskStageEnum } 42 | ) { 43 | super(); 44 | } 45 | 46 | public isRunning() { 47 | return this.stage === TaskStageEnum.running; 48 | } 49 | 50 | public isDone() { 51 | return this.stage === TaskStageEnum.done; 52 | } 53 | 54 | public isError() { 55 | return this.stage === TaskStageEnum.error; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/scheduler.ts: -------------------------------------------------------------------------------- 1 | export class BaseScheduler { 2 | schedule(callback: () => void): () => void { 3 | void callback; 4 | return () => {}; 5 | } 6 | } 7 | 8 | export class TimeoutScheduler extends BaseScheduler { 9 | constructor(private readonly timeout = 0) { 10 | super(); 11 | } 12 | 13 | schedule(callback: () => void): () => void { 14 | const tid = setTimeout(() => callback(), this.timeout); 15 | 16 | return () => { 17 | clearTimeout(tid); 18 | }; 19 | } 20 | } 21 | 22 | /** idle 调度器 */ 23 | export class IdleScheduler extends BaseScheduler { 24 | private _global = (window || global) as any; 25 | 26 | constructor() { 27 | super(); 28 | if (!this._global.requestIdleCallback) throw new Error('requestIdleCallback 不存在'); 29 | } 30 | 31 | schedule(callback: () => void) { 32 | const cancelId = this._global.requestIdleCallback(callback); 33 | 34 | return () => { 35 | this._global.cancelIdleCallback(cancelId); 36 | }; 37 | } 38 | } 39 | 40 | /** 组合调度器 */ 41 | export function composeScheduler(list: S[]): typeof BaseScheduler { 42 | const ComposedScheduler = class extends BaseScheduler { 43 | schedule(callback: () => void): () => void { 44 | const calledFlagMap = new Set(); 45 | 46 | const disposeList = list.map(s => 47 | s.schedule(() => { 48 | calledFlagMap.add(s); 49 | 50 | if (list.every(_s => calledFlagMap.has(_s))) { 51 | callback(); 52 | } 53 | }) 54 | ); 55 | 56 | return () => { 57 | disposeList.forEach(fn => fn()); 58 | }; 59 | } 60 | }; 61 | 62 | return ComposedScheduler; 63 | } 64 | -------------------------------------------------------------------------------- /src/driver.ts: -------------------------------------------------------------------------------- 1 | import { YieldEvent, TaskStageChangeEvent, DriverStageChangeEvent } from './event'; 2 | import { BaseTask } from './task'; 3 | import { BaseScheduler } from './scheduler'; 4 | import { runtimeMs, toPromise, enumCond } from './util'; 5 | import { DriverStageEnum, TaskStageEnum } from './enum'; 6 | import { EventBus, EventClass } from 'ah-event-bus'; 7 | 8 | /** 创建切片任务驱动器 */ 9 | export class TaskDriver { 10 | public eventBus = new EventBus(); 11 | private readonly stage: DriverStageEnum = DriverStageEnum.init; 12 | private error?: Error; 13 | 14 | constructor( 15 | private readonly tasks: T[], 16 | private readonly scheduler: BaseScheduler, 17 | private readonly callback?: (value: T) => void, 18 | private readonly config?: { 19 | /** 添加任务时自动启动 */ 20 | autoStart?: boolean; 21 | autoOverwrite?: boolean; 22 | } 23 | ) {} 24 | 25 | private async waitEvOnce( 26 | evType: ET, 27 | tester: (ev: InstanceType) => boolean = () => true 28 | ) { 29 | const clear = () => this.eventBus.off(evType, tester); 30 | 31 | const evPromise = new Promise>(resolve => { 32 | this.eventBus.on(evType, ev => { 33 | if (!tester(ev)) return; 34 | clear(); 35 | resolve(ev); 36 | }); 37 | }); 38 | 39 | const timeoutPromise = new Promise(resolve => setTimeout(resolve, 10e3)); 40 | 41 | await Promise.race([evPromise, timeoutPromise]).catch(err => { 42 | console.error(err); 43 | clear(); 44 | }); 45 | } 46 | 47 | private changeTaskStage(task: T, newStage: TaskStageEnum) { 48 | if (task.stage === newStage) return; 49 | 50 | const lastStage = task.stage; 51 | task.stage = newStage; 52 | 53 | this.eventBus.emit(new TaskStageChangeEvent(task.stage, { task, lastStage })); 54 | } 55 | 56 | private changeStage(ns: DriverStageEnum) { 57 | if (this.stage === ns) return; 58 | 59 | const lastStage = this.stage; 60 | (this.stage as any) = ns; 61 | 62 | this.eventBus.emit(new DriverStageChangeEvent(this.stage, { lastStage })); 63 | } 64 | 65 | /** @override 自定义选取 task */ 66 | protected pickTask(tasks: T[]): T | undefined { 67 | if (tasks.length === 0) return; 68 | 69 | // 优先级大的排后面 70 | tasks.sort((a, b) => { 71 | return ( 72 | // 优先级排序 73 | a.priority - b.priority || 74 | // 次优先级排序 75 | a.minorPriority - b.minorPriority || 76 | // 运行时间排序 77 | (() => { 78 | const aMs = a.ms || 0; 79 | const bMs = b.ms || 0; 80 | // 耗时越长,优先级约低 81 | return bMs - aMs; 82 | })() 83 | ); 84 | }); 85 | 86 | return tasks.pop(); 87 | } 88 | 89 | /** @override 自定义判断任务是否执行 */ 90 | protected shouldTaskRun(_task: T): boolean { 91 | return true; 92 | } 93 | 94 | /** @override 判断是否要进行此次调度 */ 95 | protected shouldRunCallLoop(): boolean { 96 | return true; 97 | } 98 | 99 | /** 开始 */ 100 | public async start() { 101 | // float promise 102 | this.doLoop(); 103 | } 104 | 105 | /** 暂停 */ 106 | public async pause() { 107 | enumCond({ 108 | init: 'skip', 109 | running: () => this.changeStage(DriverStageEnum.paused), 110 | paused: 'skip', 111 | stopping: 'skip', 112 | done: 'skip', 113 | error: 'skip', 114 | })(this.stage); 115 | } 116 | 117 | /** 恢复 */ 118 | public async resume() { 119 | enumCond({ 120 | init: 'skip', 121 | paused: () => this.changeStage(DriverStageEnum.running), 122 | running: 'skip', 123 | stopping: 'skip', 124 | done: 'skip', 125 | error: 'skip', 126 | })(this.stage); 127 | } 128 | 129 | /** 卸载任务 */ 130 | public async drop(tasks: T[]) { 131 | const stageHandler = enumCond>({ 132 | init: async ctx => this.changeTaskStage(ctx.task, TaskStageEnum.dropped), 133 | running: async ctx => { 134 | this.changeTaskStage(ctx.task, TaskStageEnum.dropped); 135 | }, 136 | error: 'skip', 137 | dropped: 'skip', 138 | done: 'skip', 139 | }); 140 | 141 | await Promise.all(tasks.map(async task => stageHandler(task.stage, { task }))); 142 | } 143 | 144 | public async dropAll() { 145 | const tasks = this.getUnFinishTaskQueue(); 146 | await this.drop(tasks); 147 | } 148 | 149 | /** 150 | * 停止 151 | * - 清理各种定时器 152 | * - 重置状态 153 | */ 154 | public async stop() { 155 | // 卸掉所有任务 156 | await this.dropAll(); 157 | 158 | // 清除任务池 159 | this.tasks.length = 0; 160 | 161 | const doStop = async () => { 162 | this.changeStage(DriverStageEnum.stopping); 163 | await this.waitEvOnce(DriverStageChangeEvent, () => this.stage === DriverStageEnum.done); 164 | }; 165 | 166 | await enumCond>({ 167 | init: async () => this.changeStage(DriverStageEnum.done), 168 | running: doStop, 169 | paused: doStop, 170 | stopping: 'skip', 171 | done: 'skip', 172 | error: 'skip', 173 | })(this.stage); 174 | } 175 | 176 | public async waitStop() { 177 | await this.waitEvOnce(DriverStageChangeEvent, () => this.stage === DriverStageEnum.done); 178 | } 179 | 180 | /** 181 | * 销毁 182 | * - stop & 清空事件监听 183 | */ 184 | public async dispose() { 185 | await this.stop(); 186 | this.eventBus.off(); 187 | } 188 | 189 | public addTask(task: T) { 190 | if (!this.config?.autoOverwrite) { 191 | if (this.tasks.some(t => t.name === task.name)) { 192 | throw new Error('当前任务已存在 ' + task.name); 193 | } 194 | } 195 | 196 | this.tasks.push(task); 197 | 198 | if (this.config?.autoStart) { 199 | const shouldStart = enumCond({ 200 | init: () => true, 201 | done: () => true, 202 | error: () => true, 203 | running: 'skip', 204 | stopping: 'error', 205 | paused: 'skip', 206 | })(this.stage); 207 | 208 | if (shouldStart) this.start(); 209 | } 210 | 211 | return this; 212 | } 213 | 214 | /** 获取未完成的任务队列 */ 215 | public getUnFinishTaskQueue(): T[] { 216 | return this.tasks.filter( 217 | d => d.stage === TaskStageEnum.init || d.stage === TaskStageEnum.running 218 | ); 219 | } 220 | 221 | public get isRunning(): boolean { 222 | return this.stage === DriverStageEnum.running; 223 | } 224 | 225 | public getStage(): DriverStageEnum { 226 | return this.stage; 227 | } 228 | 229 | public getError() { 230 | return this.error; 231 | } 232 | 233 | private async doLoop() { 234 | // 启动前检查状态 235 | enumCond({ 236 | // 部分状态允许重启 237 | init: 'skip', 238 | done: 'skip', 239 | error: 'skip', 240 | running: 'error', 241 | stopping: 'error', 242 | paused: 'error', 243 | })(this.stage); 244 | 245 | // 开始 246 | this.error = undefined; 247 | this.changeStage(DriverStageEnum.running); 248 | 249 | try { 250 | while (1) { 251 | // 每个循环开始都要等待调度 252 | await new Promise(r => this.scheduler.schedule(r)); 253 | 254 | // 执行前检查 255 | const loopStartAction = enumCond({ 256 | init: 'error', 257 | running: 'skip', 258 | // 停止中 259 | stopping: () => { 260 | this.changeStage(DriverStageEnum.done); 261 | return 'break'; 262 | }, 263 | done: 'error', 264 | paused: () => 'continue', 265 | error: 'skip', 266 | })(this.stage); 267 | 268 | if (loopStartAction === 'continue') continue; 269 | 270 | // 自定义检查 271 | if (!this.shouldRunCallLoop()) continue; 272 | 273 | const unfinishedTasks = this.getUnFinishTaskQueue(); 274 | if (unfinishedTasks.length === 0) { 275 | this.changeStage(DriverStageEnum.done); 276 | break; 277 | } 278 | 279 | const shouldRunTasks = unfinishedTasks.filter(d => this.shouldTaskRun(d)); 280 | if (shouldRunTasks.length === 0) continue; 281 | 282 | // 优先级排序 283 | const toRunTask = this.pickTask(shouldRunTasks); 284 | if (!toRunTask) continue; 285 | 286 | // 变更 task stage 287 | this.changeTaskStage(toRunTask, TaskStageEnum.running); 288 | const { sendValue } = toRunTask; 289 | 290 | // 求值 291 | let resolvedValue: any; 292 | let isDone = false; 293 | let invokeMs = 0; 294 | 295 | try { 296 | const [{ value, done }, ms] = runtimeMs(() => toRunTask.iter.next(sendValue)); 297 | invokeMs = ms; 298 | isDone = !!done; 299 | 300 | resolvedValue = await toPromise(value); 301 | 302 | // 走到这里的时候,toRunTask 的状态可能会发生变化(await) 303 | if (toRunTask.stage !== TaskStageEnum.running) continue; 304 | } catch (taskError) { 305 | toRunTask.error = taskError; 306 | this.changeTaskStage(toRunTask, TaskStageEnum.error); 307 | continue; 308 | } 309 | 310 | // 累加运行时间 311 | toRunTask.ms = (toRunTask.ms || 0) + invokeMs; 312 | 313 | // 记录 sendValue 314 | toRunTask.sendValue = resolvedValue; 315 | 316 | if (isDone) { 317 | this.changeTaskStage(toRunTask, TaskStageEnum.done); 318 | } else { 319 | this.callback?.(resolvedValue); 320 | this.eventBus.emit(new YieldEvent(resolvedValue, toRunTask)); 321 | } 322 | } 323 | } catch (driverError) { 324 | this.error = driverError; 325 | this.changeStage(DriverStageEnum.error); 326 | 327 | throw driverError; 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /__tests__/driver.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TaskDriver, 3 | TimeoutScheduler, 4 | BaseTask, 5 | YieldEvent, 6 | TaskStageChangeEvent, 7 | DriverStageChangeEvent, 8 | DriverStageEnum, 9 | } from '../src'; 10 | 11 | describe('__tests__/driver.test.ts', () => { 12 | it('单任务', done => { 13 | const i1 = (function* () { 14 | yield 'x'; 15 | })(); 16 | const t1 = new BaseTask({ iter: i1 }); 17 | 18 | const d = new TaskDriver([t1], new TimeoutScheduler(), value => { 19 | expect(value).toBe('x'); 20 | }); 21 | 22 | let startFlag = 0; 23 | 24 | d.eventBus.on(DriverStageChangeEvent, ev => { 25 | if (ev.stage === DriverStageEnum.running) { 26 | startFlag++; 27 | } else if (ev.stage === DriverStageEnum.done) { 28 | expect(startFlag).toBe(1); 29 | done(); 30 | } 31 | }); 32 | 33 | d.start(); 34 | }); 35 | 36 | it('driver 事件流', done => { 37 | const i1 = (function* () { 38 | yield '1.1'; 39 | yield '1.2'; 40 | return '1.3'; 41 | })(); 42 | const i2 = (function* () { 43 | yield '2.1'; 44 | yield '2.2'; 45 | return '2.3'; 46 | })(); 47 | const t1 = new BaseTask({ iter: i1, priority: 2 }, 'BaseTask-1'); 48 | const t2 = new BaseTask({ iter: i2, priority: 1 }, 'BaseTask-2'); 49 | 50 | const driver = new TaskDriver([t1, t2], new TimeoutScheduler()); 51 | 52 | let flag: string[] = []; 53 | 54 | driver.eventBus 55 | .on(DriverStageChangeEvent, ev => { 56 | flag.push(`DriverStageChangeEvent-${ev.extra.lastStage}->${ev.stage}`); 57 | 58 | if (ev.isDone()) { 59 | expect(flag).toMatchSnapshot(); 60 | done(); 61 | } 62 | }) 63 | .on(TaskStageChangeEvent, ev => { 64 | flag.push( 65 | `TaskStageChangeEvent-${ev.extra.task.name}-${ev.extra.lastStage}->${ev.extra.task.stage}` 66 | ); 67 | }) 68 | .on(YieldEvent, e => { 69 | flag.push(`YieldEvent-${e.value}`); 70 | if (e.value === '2.2') driver.drop([t2]); 71 | }); 72 | 73 | driver.start(); 74 | }); 75 | 76 | it('可以 yield 各种值', done => { 77 | const i1 = (function* () { 78 | yield null; 79 | yield undefined; 80 | yield 'a'; 81 | yield new Promise(resolve => setTimeout(() => resolve('b'), 100)); 82 | yield [1, new Promise(resolve => setTimeout(() => resolve('1.1'), 100))]; 83 | yield { c: 'c' }; 84 | })(); 85 | const t1 = new BaseTask({ iter: i1 }); 86 | 87 | let cnt = 0; 88 | const d = new TaskDriver([t1], new TimeoutScheduler(), value => { 89 | cnt++; 90 | cnt === 1 && expect(value).toBeNull(); 91 | cnt === 2 && expect(value).toBeUndefined(); 92 | cnt === 3 && expect(value).toEqual('a'); 93 | cnt === 4 && expect(value).toEqual('b'); 94 | cnt === 5 && expect(value).toEqual([1, '1.1']); 95 | cnt === 6 && expect(value).toEqual({ c: 'c' }); 96 | }); 97 | 98 | d.eventBus.on(DriverStageChangeEvent, ev => { 99 | if (ev.isDone()) done(); 100 | }); 101 | 102 | d.start(); 103 | }); 104 | 105 | it('yield 可以拿到 send 的值', done => { 106 | const i1 = (function* () { 107 | let res: any; 108 | 109 | res = yield new Promise(resolve => setTimeout(() => resolve('1.1'), 100)); 110 | expect(res).toEqual('1.1'); 111 | 112 | res = yield '1.2'; 113 | expect(res).toEqual('1.2'); 114 | })(); 115 | 116 | const i2 = (function* () { 117 | let res: any; 118 | 119 | res = yield new Promise(resolve => setTimeout(() => resolve('2.1'), 100)); 120 | expect(res).toEqual('2.1'); 121 | 122 | res = yield '2.2'; 123 | expect(res).toEqual('2.2'); 124 | })(); 125 | 126 | const t1 = new BaseTask({ iter: i1 }); 127 | const t2 = new BaseTask({ iter: i2 }); 128 | 129 | const d = new TaskDriver([t1, t2], new TimeoutScheduler()); 130 | 131 | d.eventBus.on(DriverStageChangeEvent, ev => { 132 | if (ev.isDone()) done(); 133 | }); 134 | 135 | d.start(); 136 | }); 137 | 138 | it('yield 可以 catch', done => { 139 | const i1 = (function* () { 140 | try { 141 | yield Promise.reject('err'); 142 | } catch (e) { 143 | expect(e).toEqual('err'); 144 | } 145 | })(); 146 | const t1 = new BaseTask({ iter: i1 }); 147 | 148 | const d = new TaskDriver([t1], new TimeoutScheduler(), () => {}); 149 | 150 | d.eventBus.on(DriverStageChangeEvent, ev => { 151 | if (ev.isDone()) done(); 152 | }); 153 | 154 | d.start(); 155 | }); 156 | 157 | it('.start() 之后等待调度再开始任务', done => { 158 | let flag = 'init'; 159 | 160 | const i1 = (function* () { 161 | flag = 'run i1'; 162 | yield 'x'; 163 | })(); 164 | const t1 = new BaseTask({ iter: i1 }); 165 | const d = new TaskDriver([t1], new TimeoutScheduler()); 166 | 167 | d.eventBus.on(TaskStageChangeEvent, ev => { 168 | if (ev.isDone()) done(); 169 | }); 170 | 171 | d.start(); 172 | 173 | // .start() 之后,flag 依然是 `init`,表示没有执行过 i1 174 | expect(flag).toBe('init'); 175 | }); 176 | 177 | it('shouldTaskRun test', done => { 178 | class TestTaskDriver extends TaskDriver { 179 | private cnt = 10; 180 | 181 | shouldTaskRun(task: BaseTask) { 182 | if (this.cnt-- === 0) d.stop(); 183 | 184 | if (task.name === 'skip') return false; 185 | else return true; 186 | } 187 | } 188 | 189 | const flag: string[] = []; 190 | 191 | const t1 = new BaseTask( 192 | { 193 | iter: (function* () { 194 | let cnt = 3; 195 | while (cnt--) { 196 | flag.push('i1'); 197 | yield; 198 | } 199 | })(), 200 | priority: 1, 201 | }, 202 | 'run' 203 | ); 204 | 205 | const t2 = new BaseTask( 206 | { 207 | iter: (function* () { 208 | let cnt = 3; 209 | while (cnt--) { 210 | flag.push('i2'); 211 | yield; 212 | } 213 | })(), 214 | priority: 1, 215 | }, 216 | 'skip' 217 | ); 218 | 219 | const d = new TestTaskDriver([t1, t2], new TimeoutScheduler()); 220 | 221 | d.eventBus.on(DriverStageChangeEvent, ev => { 222 | if (ev.isDone()) { 223 | expect(flag).toEqual(['i1', 'i1', 'i1']); 224 | done(); 225 | } 226 | }); 227 | 228 | d.start(); 229 | }); 230 | 231 | describe('优先级任务', () => { 232 | it('priority 调度', done => { 233 | const i1 = (function* () { 234 | yield 'i1'; 235 | })(); 236 | const i2 = (function* () { 237 | yield 'i2'; 238 | })(); 239 | const i3 = (function* () { 240 | yield 'i3'; 241 | })(); 242 | 243 | const t1 = new BaseTask({ iter: i1, priority: 1 }); 244 | const t2 = new BaseTask({ iter: i2, priority: 2 }); 245 | const t3 = new BaseTask({ iter: i3, priority: 3 }); 246 | 247 | let cnt = 3; 248 | const d = new TaskDriver([t1, t3, t2], new TimeoutScheduler(), value => { 249 | expect(value).toBe(`i${cnt}`); 250 | cnt--; 251 | }); 252 | 253 | d.eventBus.on(TaskStageChangeEvent, ev => { 254 | if (ev.isDone()) done(); 255 | }); 256 | 257 | d.start(); 258 | }); 259 | 260 | it('runtime ms 调度', done => { 261 | const i1 = (function* () { 262 | yield 'i1.1'; 263 | yield 'i1.2'; 264 | })(); 265 | const i2 = (function* () { 266 | for (let i = 0; i < 1e8; i++) {} 267 | yield 'i2.1'; 268 | yield 'i2.2'; 269 | })(); 270 | 271 | const t1 = new BaseTask({ iter: i1 }); 272 | const t2 = new BaseTask({ iter: i2 }); 273 | 274 | let cnt = 0; 275 | const d = new TaskDriver([t1, t2], new TimeoutScheduler(), value => { 276 | cnt++; 277 | 278 | cnt === 1 && expect(value).toBe(`i2.1`); 279 | // i2.1 耗时较长,所以 i1 任务先执行了 280 | cnt === 2 && expect(value).toBe(`i1.1`); 281 | cnt === 3 && expect(value).toBe(`i1.2`); 282 | cnt === 4 && expect(value).toBe(`i2.2`); 283 | if (cnt === 5) throw new Error(); 284 | }); 285 | 286 | d.eventBus.on(TaskStageChangeEvent, ev => { 287 | if (ev.isDone()) done(); 288 | }); 289 | 290 | d.start(); 291 | }); 292 | 293 | it('动态 priority', done => { 294 | const i1 = (function* () { 295 | yield 'i1.1'; 296 | yield 'i1.2'; 297 | })(); 298 | const i2 = (function* () { 299 | yield 'i2.1'; 300 | yield 'i2.2'; 301 | })(); 302 | 303 | const t1 = new BaseTask({ iter: i1 }); 304 | const t2 = new BaseTask({ iter: i2, priority: 1 }); 305 | 306 | let cnt = 0; 307 | const d = new TaskDriver([t1, t2], new TimeoutScheduler(), value => { 308 | cnt++; 309 | 310 | if (cnt === 1) t1.priority = 2; 311 | 312 | cnt === 1 && expect(value).toBe('i2.1'); 313 | cnt === 2 && expect(value).toBe('i1.1'); 314 | cnt === 3 && expect(value).toBe('i1.2'); 315 | cnt === 4 && expect(value).toBe('i2.2'); 316 | }); 317 | 318 | d.eventBus.on(TaskStageChangeEvent, ev => { 319 | if (ev.isDone()) done(); 320 | }); 321 | 322 | d.start(); 323 | }); 324 | }); 325 | 326 | describe('错误堆栈还原', () => { 327 | it('同步栈 & 异步栈', done => { 328 | const invokeCnt = { i1: 0, i2: 0, i3: 0 }; 329 | const invokeErrorEvents: { name: string; message: string }[] = []; 330 | 331 | // 同步栈 332 | const i1 = (function* () { 333 | invokeCnt.i1++; 334 | yield 1; 335 | throw new Error('fake error1'); 336 | })(); 337 | 338 | // 异步栈 339 | const i2 = (function* () { 340 | invokeCnt.i2++; 341 | yield new Promise((_, reject) => { 342 | setTimeout(() => reject(new Error('fake error2')), 0); 343 | }); 344 | })(); 345 | 346 | // 正常任务 347 | const i3 = (function* () { 348 | yield invokeCnt.i3++; 349 | yield invokeCnt.i3++; 350 | })(); 351 | 352 | const t1 = new BaseTask({ iter: i1, priority: 10 }); 353 | const t2 = new BaseTask({ iter: i2 }); 354 | const t3 = new BaseTask({ iter: i3 }); 355 | 356 | const d = new TaskDriver([t1, t2, t3], new TimeoutScheduler()); 357 | 358 | d.eventBus 359 | .on(DriverStageChangeEvent, ev => { 360 | if (ev.isDone()) { 361 | expect(invokeCnt).toStrictEqual({ i1: 1, i2: 1, i3: 2 }); 362 | 363 | expect(invokeErrorEvents[0].message).toContain('fake error1'); 364 | expect(invokeErrorEvents[0].name).toBe(t1.name); 365 | 366 | expect(invokeErrorEvents[1].message).toContain('fake error2'); 367 | expect(invokeErrorEvents[1].name).toBe(t2.name); 368 | 369 | done(); 370 | } 371 | }) 372 | .on(TaskStageChangeEvent, ev => { 373 | if (ev.isError()) { 374 | invokeErrorEvents.push({ 375 | name: ev.extra.task.name, 376 | message: ev.extra.task.error!.message, 377 | }); 378 | } 379 | }); 380 | 381 | d.start(); 382 | }); 383 | }); 384 | 385 | describe('配置项', () => { 386 | it('autoStart', done => { 387 | let startCnt = 0; 388 | let flag = 'init'; 389 | 390 | const i1 = (function* () { 391 | yield 'x'; 392 | flag = 'i1'; 393 | })(); 394 | const t1 = new BaseTask({ iter: i1, priority: 999 }); 395 | 396 | const i2 = (function* () { 397 | yield 'x'; 398 | flag = 'i2'; 399 | })(); 400 | const t2 = new BaseTask({ iter: i2, priority: 0 }); 401 | 402 | const d = new TaskDriver([], new TimeoutScheduler(), undefined, { 403 | autoStart: true, 404 | }); 405 | 406 | d.eventBus 407 | .on(DriverStageChangeEvent, ev => { 408 | if (ev.isRunning()) { 409 | startCnt++; 410 | } 411 | }) 412 | .on(TaskStageChangeEvent, ev => { 413 | if (ev.isDone()) { 414 | expect(flag).toBe('i1'); 415 | expect(startCnt).toBe(1); 416 | done(); 417 | } 418 | }); 419 | 420 | d.addTask(t1); 421 | d.addTask(t2); 422 | }); 423 | }); 424 | }); 425 | --------------------------------------------------------------------------------