ts-batch-processor
Version:
A batch processor with event emitter for browser and node.js
156 lines (155 loc) • 5.73 kB
JavaScript
import { CompleteEvent, ProgressEvent, StartEvent } from "./events";
import { CANNOT_ADD_TASK_DURING_PROCESSING_ERROR, CANNOT_RESET_DURING_PROCESSING_ERROR, NO_TASKS_ERROR, TASK_MUST_BE_FUNCTION_ERROR, } from "./constants/errors";
import { TaskResponseStatus } from "./types/task";
import { validateConcurrency } from "./utils";
export const getDefaultQueue = () => [];
export const getDefaultTotalTasks = () => 0;
export const getDefaultCompletedTasks = () => 0;
export const getDefaultProcessPromise = () => null;
export const getDefaultTaskResults = () => [];
export const getDefaultConcurrency = () => typeof navigator !== "undefined" && navigator.hardwareConcurrency
? navigator.hardwareConcurrency
: 10;
class Batch extends EventTarget {
constructor(config = {}) {
super();
this.queue = getDefaultQueue();
this.totalTasks = getDefaultTotalTasks();
this.completedTasks = getDefaultCompletedTasks();
this.processPromise = getDefaultProcessPromise();
this.taskResults = getDefaultTaskResults();
const concurrency = config.concurrency ?? getDefaultConcurrency();
validateConcurrency(concurrency);
this.config = {
concurrency,
// timeout: config.timeout || Infinity,
// taskTimeout: config.taskTimeout || Infinity,
// taskInterval: config.taskInterval || 0,
};
this.setToDefault();
}
add(task) {
if (this.isProcessing) {
throw CANNOT_ADD_TASK_DURING_PROCESSING_ERROR;
}
if (typeof task !== "function") {
throw TASK_MUST_BE_FUNCTION_ERROR;
}
this.queue.push({ task, index: this.totalTasks });
++this.totalTasks;
}
process() {
if (this.processPromise) {
return this.processPromise;
}
if (this.totalTasks === 0) {
return Promise.reject(NO_TASKS_ERROR);
}
this.taskResults = Array.from({ length: this.totalTasks });
// Test: store promise without .then() chain to see if that's the issue
this.processPromise = new Promise(async (resolve) => {
await this.startProcessing();
const results = this.taskResults.map((t) => Object.freeze(t));
// dispatch complete event
this.dispatchEvent(new CompleteEvent({ taskResults: results }));
resolve(results);
});
return this.processPromise;
}
async startProcessing() {
// dispatch start event
this.dispatchEvent(new StartEvent({ totalTasks: this.totalTasks }));
return new Promise((resolve) => {
// Handle edge case of empty queue
if (this.queue.length === 0) {
resolve();
return;
}
// Start initial batch of concurrent tasks
const initialTasks = Math.min(this.config.concurrency, this.queue.length);
for (let i = 0; i < initialTasks; i++) {
const task = this.queue.shift();
if (!task)
continue;
this.processTask(task).then((taskResult) => {
this.onTaskEnd(task, taskResult, resolve);
});
}
});
}
onTaskEnd(task, taskResult, resolve) {
// update completed tasks and store the result
++this.completedTasks;
this.taskResults[task.index] = Object.freeze(taskResult);
// dispatch progress event
this.dispatchEvent(new ProgressEvent({
totalTasks: this.totalTasks,
completedTasks: this.completedTasks,
pendingTasks: this.totalTasks - this.completedTasks,
progress: this.progress,
lastCompletedTaskResult: this.taskResults[task.index],
}));
// check for completion
if (this.completedTasks === this.totalTasks) {
resolve();
}
// get the next task from the queue
const newTask = this.queue.shift();
if (!newTask) {
return;
}
this.processTask(newTask).then((taskResult) => {
this.onTaskEnd(newTask, taskResult, resolve);
});
}
async processTask(task) {
let result = null;
let taskError = null;
try {
result = await task.task();
}
catch (caughtError) {
taskError = caughtError;
}
const taskResult = {
responseStatus: taskError !== null
? TaskResponseStatus.ERROR
: TaskResponseStatus.SUCCESS,
response: result,
error: taskError,
index: task.index,
};
return taskResult;
}
setToDefault() {
this.queue = getDefaultQueue();
this.totalTasks = getDefaultTotalTasks();
this.completedTasks = getDefaultCompletedTasks();
this.taskResults = getDefaultTaskResults();
this.processPromise = getDefaultProcessPromise();
}
reset() {
if (this.isProcessing) {
throw CANNOT_RESET_DURING_PROCESSING_ERROR;
}
this.setToDefault();
}
addEventListener(type, callback, options) {
super.addEventListener(type, callback, options);
}
get isProcessing() {
return (this.processPromise !== null && this.completedTasks < this.totalTasks);
}
get progress() {
return this.totalTasks === 0
? 0
: Math.round((this.completedTasks * 10000) / this.totalTasks) / 100;
}
get size() {
return this.totalTasks;
}
get concurrency() {
return this.config.concurrency;
}
}
export default Batch;