@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
331 lines (273 loc) • 7.75 kB
JavaScript
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;