express-long-polling
Version:
Library to implement task dispatching with long-polling approach on express server.
180 lines (179 loc) • 5.25 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LongPollingTaskQueue = exports.LongPollingTask = void 0;
const crypto_1 = require("crypto");
class LongPollingTask {
id;
input;
listeners = [];
constructor(id, input) {
this.id = id;
this.input = input;
}
toJSON() {
return {
id: this.id,
input: this.input,
};
}
dispatchResult(output) {
for (let listener of this.listeners) {
try {
listener(output);
}
catch (error) {
console.error(error);
}
}
this.listeners.length = 0;
this.addListener = listener => {
listener(output);
};
}
addListener(listener) {
this.listeners.push(listener);
}
removeListener(listener) {
let index = this.listeners.indexOf(listener);
if (index !== -1) {
this.listeners.splice(index, 1);
}
}
}
exports.LongPollingTask = LongPollingTask;
let defaultPollingInterval = 1000 * 30;
/** @description redirect with 307 to let client retry */
function defaultOnTimeout(req) {
req.res?.redirect(307, req.url);
}
class LongPollingTaskQueue {
pollingInterval;
/**
* @description list of pending request from workers
*/
pendingTaskListeners = [];
/**
* @description list of pending tasks for workers that are not completed
*/
pendingTasks = [];
/**
* @description list of all tasks for client, both completed or not completed tasks.
*/
allTasks = {};
constructor(options) {
this.pollingInterval = options?.pollingInterval || defaultPollingInterval;
}
/**
* @description create task from client
*/
addTask(options) {
let id = typeof options.id === 'string' ? options.id : (options.id || crypto_1.randomUUID)();
let task = new LongPollingTask(id, options.input);
this.pendingTasks.push(task);
this.allTasks[id] = task;
let listener = this.pendingTaskListeners.shift();
if (listener) {
listener(task);
}
return { id };
}
getFirstTask() {
if (this.pendingTasks.length == 0)
return null;
return this.pendingTasks[0];
}
getRandomTask() {
if (this.pendingTasks.length == 0)
return null;
let index = Math.floor(Math.random() * this.pendingTasks.length);
return this.pendingTasks[index];
}
popTaskById(id) {
let index = this.pendingTasks.findIndex(task => task.id == id);
if (index === -1)
return null;
let task = this.pendingTasks[index];
this.pendingTasks.splice(index, 1);
return task;
}
waitTask(req, onTask, onTimeout = defaultOnTimeout) {
this.pendingTaskListeners.push(onTask);
let remove = () => {
let index = this.pendingTaskListeners.indexOf(onTask);
if (index !== -1) {
this.pendingTaskListeners.splice(index, 1);
}
};
let timer = setTimeout(() => {
remove();
onTimeout(req);
}, this.pollingInterval);
req.on('end', () => {
remove();
clearTimeout(timer);
});
}
/**
* @description get task from worker
*/
getOrWaitTask(getTask, req, onTask, onTimeout = defaultOnTimeout) {
let task = null;
if (getTask == 'first') {
task = this.getFirstTask();
}
else if (getTask == 'random') {
task = this.getRandomTask();
}
if (task) {
onTask(task);
}
else {
this.waitTask(req, onTask, onTimeout);
}
}
/**
* @description dispatch result from worker
* @returns true if the task is found and deleted
* @returns false if the task is not found (maybe already deleted)
*/
dispatchResult(id, output) {
let task = this.popTaskById(id);
if (!task)
return false;
task.dispatchResult(output);
return true;
}
/**
* @description get result from client (dispatched from worker)
*/
getOrWaitResult(id, req, onOutput, onTimeout = defaultOnTimeout) {
let task = this.allTasks[id];
if (!task) {
throw new Error('Task not found by id: ' + id);
}
task.addListener(onOutput);
let timer = setTimeout(() => {
onTimeout(req);
task.removeListener(onOutput);
}, this.pollingInterval);
req.on('end', () => {
clearTimeout(timer);
task.removeListener(onOutput);
});
}
/**
* @description delete completed task from client (to release memory)
* @returns true if the task is found and deleted
* @returns false if the task is not found (maybe already deleted)
*/
deleteTask(id) {
if (id in this.allTasks) {
delete this.allTasks[id];
return true;
}
else {
return false;
}
}
}
exports.LongPollingTaskQueue = LongPollingTaskQueue;