UNPKG

ipqueue

Version:

A inter-process task queue implementation for NodeJS.

193 lines (174 loc) 6.36 kB
import * as net from "net"; import { EventEmitter } from "events"; import first = require("lodash/first"); import { openChannel } from "open-channel"; import { encode, decode } from "bsp"; var taskId = 0; type Task = { id: number, socket: net.Socket }; const Tasks: { current: Task["id"]; queue: Task[]; timer?: NodeJS.Timer; } = { current: void 0, queue: [], timer: null }; enum QueueEvents { acquire, acquired, release, getLength, gotLength } function getTaskId() { let id = taskId++; if (taskId === Number.MAX_SAFE_INTEGER) taskId = 0; return id; } export class Queue { protected errorHandler: (err: Error) => void; protected tasks: { [id: number]: EventEmitter } = {}; private temp: Buffer[] = []; protected channel = openChannel(this.name, socket => { let temp = []; socket.on("data", (buf) => { let msg = decode<[number, number, any]>(buf, temp); for (let [code, id, extra] of msg) { socket.emit(QueueEvents[code], id, extra); } }).on(QueueEvents[0], (id: number) => { // if the queue is empty, run the task immediately Tasks.queue.length || this.respond(socket, id, true); // push task into the queue socket.destroyed || Tasks.queue.push({ id, socket }); }).on(QueueEvents[2], () => { Tasks.queue.shift(); // remove the running task clearTimeout(Tasks.timer); // run the next task let item = first(Tasks.queue); item && this.respond(item.socket, item.id); }).on(QueueEvents[3], (id: number) => { let length = Tasks.queue.length; socket.write(encode([ QueueEvents.gotLength, id, length && length - 1 ])); }).on("end", socket.destroy).on("close", socket.unref); }); protected socket = this.channel.connect().on("data", buf => { let msg = decode<[number, number, any]>(buf, this.temp); for (let [code, id, extra] of msg) { this.tasks[id].emit(QueueEvents[code], id, extra); } }); constructor(public name: string, private timeout: number) { } /** * Returns `true` if the queue is connected to the server, `false` the * otherwise. */ get connected() { return this.channel.connected; } /** Closes connection to the queue server. */ disconnect() { this.socket.destroyed || this.socket.destroy(); } /** Binds an error handler to catch errors whenever occurred. */ onError(handler: (err: Error) => void) { this.errorHandler = handler; this.socket.on("error", handler); return this; } /** * Pushes a task into the queue. The program will send a request to the * server acquiring for a lock, and wait until the lock has been acquired, * run the task automatically. */ push(task: (next: () => void) => void) { let id = getTaskId(), next = () => this.send(QueueEvents.release, id); this.tasks[id] = new EventEmitter(); this.tasks[id].once(QueueEvents[1], () => { try { delete this.tasks[id]; task(next); } catch (err) { if (this.errorHandler) this.errorHandler(err); } }); this.send(QueueEvents.acquire, id); return this; } /** Gets the length of remaining tasks in the queue. */ getLength(): Promise<number> { return new Promise((resolve, reject) => { if (!this.connected) return resolve(0); let id = getTaskId(), timer = setTimeout(() => { reject(new Error("failed to get queue length")); }, this.timeout); this.tasks[id] = new EventEmitter(); this.tasks[id].once(QueueEvents[4], (id: number, length: number) => { clearTimeout(timer); try { delete this.tasks[id]; resolve(length); } catch (err) { reject(err); } }); this.send(QueueEvents.getLength, id); }); } /** Sets a timeout to force release the queue for next task. */ setTimeout(timeout: number) { this.timeout = timeout; } private send(event: number, id?: number) { this.socket.write(encode([event, id])); } private respond(socket: net.Socket, id: number, immediate = false) { Tasks.current = id; if (!socket.destroyed) { return socket.write(encode([QueueEvents.acquired, id]), () => { // set a timer to force release when timeout. Tasks.timer = setTimeout(() => { socket.emit(QueueEvents[2]); }, this.timeout); }); } else if (!immediate) { // if the socket is destroyed whether normally or abnormally before // responding acquired queue lock, release it immediately. return socket.emit(QueueEvents[2]); } } } /** * Opens connection to the queue server and returns a client instance. * @param timeout Sets both connection timeout and max lock time, meaning if you * don't call `next()` in a task (or the process fails to call it, i.e. exited * unexpected), the next task will be run anyway when timeout. The default value * is `5000` ms. */ export function connect(timeout?: number): Queue /** * @param name A unique name to distinguish potential queues on the same * machine. */ export function connect(name: string, timeout?: number): Queue; export function connect(...args): Queue { var name: string, timeout: number; if (!args.length || typeof args[0] == "number") { name = "ipqueue"; timeout = args[0] || 5000; } else { name = args[0]; timeout = args[1] || 5000; } return new Queue(name, timeout); } export default connect;