UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

331 lines (273 loc) • 7.75 kB
import { assert } from "../../assert.js"; import { array_push_if_unique } from "../../collection/array/array_push_if_unique.js"; import Signal from "../../events/signal/Signal.js"; import { noop } from "../../function/noop.js"; import ObservedInteger from "../../model/ObservedInteger.js"; import { TaskSignal } from "./TaskSignal.js"; import TaskState from "./TaskState.js"; /** * * @type {number} */ let id_counter = 0; /** * Represents a computation task, where computation is performed in cycles. * Each cycle is intended to complete very quickly, and be executed by {@link ConcurrentExecutor} */ export class Task { /** * @readonly * @type {number} */ id = id_counter++; /** * @readonly */ on = { /** * @readonly * @type {Signal} */ started: new Signal(), /** * @readonly * @type {Signal} */ completed: new Signal(), /** * @readonly * @type {Signal} */ failed: new Signal(), }; /** * * @type {ObservedInteger} */ state = new ObservedInteger(TaskState.INITIAL); /** * amount of time spent running this task in milliseconds * @type {number} * @public */ __executedCpuTime = 0; /** * number of time task's cycle function was executed * @type {number} * @public */ __executedCycleCount = 0; /** * * @param {string} [name] useful for identifying the task later on, for various UI and debug purposes * @param {function(Task, executor:ConcurrentExecutor)} [initializer] function to be executed just before task starts * @param {function():TaskSignal} cycleFunction * @param {function():number} [computeProgress] * @param {Task[]} [dependencies=[]] * @param {number} [estimatedDuration=1] in seconds * @constructor */ constructor( { name = "Unnamed", initializer = noop, cycleFunction, computeProgress, dependencies = [], estimatedDuration = 1 } ) { assert.isString(name, 'name'); assert.isFunction(cycleFunction, 'cycleFunction'); assert.isFunction(initializer, 'initializer'); assert.isNumber(estimatedDuration, 'estimatedDuration'); /** * * @type {Task[]} */ this.dependencies = dependencies; /** * * @type {number} */ this.estimatedDuration = estimatedDuration; /** * * @type {string} */ this.name = name; /** * * @type {function(): TaskSignal} */ this.cycle = cycleFunction; /** * * @type {function(Task, executor:ConcurrentExecutor)} */ this.initialize = initializer; if (computeProgress !== undefined) { // override progress function this.computeProgress = computeProgress; } } computeProgress() { const cycles = this.__executedCycleCount; if (cycles === 0) { return 0; } // inverse logarithmic progression, never reaches 1 return 1 - (1 / cycles); } /** * Time in milliseconds that the task has been executing for, suspended time does not count * @returns {number} */ getExecutedCpuTime() { return this.__executedCpuTime; } getEstimatedDuration() { return this.estimatedDuration; } /** * * @param {Task|TaskGroup} task * @returns Task */ addDependency(task) { assert.notEqual(task, undefined, 'task is undefined'); assert.notEqual(task, null, 'task is null'); if (task.isTaskGroup) { //is a task group, add all children instead this.addDependencies(task.children); } else if (task.isTask) { //check that the dependency is not registered yet array_push_if_unique(this.dependencies, task); } else { throw new Error('Expected a Task or a TaskGroup, got something else'); } return this; } /** * * @param {Array<(Task|TaskGroup)>} tasks */ addDependencies(tasks) { assert.isArray(tasks, 'tasks'); const task_count = tasks.length; for (let i = 0; i < task_count; i++) { const task = tasks[i]; this.addDependency(task); } } toString() { return `Task{name:'${this.name}'}`; } /** * * @param {function} resolve * @param {function} reject */ join(resolve, reject) { Task.join(this, resolve, reject); } /** * Run entire task synchronously to completion */ executeSync() { this.initialize(this, null); this.on.started.send0(); let s = this.cycle(); for (; s !== TaskSignal.EndSuccess && s !== TaskSignal.EndFailure; s = this.cycle()) { //keep running } if (s === TaskSignal.EndSuccess) { this.on.completed.send0(); } else if (s === TaskSignal.EndFailure) { this.on.failed.send0(); } return s; } /** * * @returns {Promise} */ promise() { return Task.promise(this); } /** * * @param {Array<(Task|TaskGroup)>} tasks * @return {Promise} */ static promiseAll(tasks) { const promises = tasks.map(Task.promise); return Promise.all(promises); } /** * * @param {Task|TaskGroup} task */ static promise(task) { return new Promise((resolve, reject) => Task.join(task, resolve, reject)); } /** * * @param {Task} task * @param {function} resolve * @param {function} reject */ static join(task, resolve, reject) { assert.isFunction(resolve, 'resolve'); assert.isFunction(reject, 'reject'); const state = task.state.getValue(); if (state === TaskState.SUCCEEDED) { resolve(); } else if (state === TaskState.FAILED) { if (reject !== undefined) { reject(); } } else { task.on.completed.addOne(resolve); if (reject !== undefined) { task.on.failed.addOne(reject); } } } /** * * @param {Task[]} tasks * @param {function} resolve * @param {function} reject */ static joinAll(tasks, resolve, reject) { let liveCount = tasks.length; if (liveCount === 0) { //empty input resolve(); return; } let failedDispatched = false; function cbOK() { liveCount--; if (liveCount <= 0 && !failedDispatched) { resolve(); } } function cbFailed() { if (!failedDispatched) { failedDispatched = true; reject(arguments); } } for (let i = 0; i < tasks.length; i++) { Task.join(tasks[i], cbOK, cbFailed); } } } /** * @readonly * @type {boolean} */ Task.prototype.isTask = true; export default Task;