opnet
Version:
The perfect library for building Bitcoin-based applications.
187 lines (186 loc) • 6.61 kB
JavaScript
import { Logger } from '@btc-vision/logger';
import { createWorker, isNode } from './WorkerCreator.js';
export class BaseThreader extends Logger {
logColor = '#FF5733';
workers = [];
available = [];
pending = new Map();
queue = [];
idCounter = 0;
poolSize;
initialized = false;
initializing = null;
tasksProcessed = 0;
tasksFailed = 0;
lastStatsLog = 0;
statsInterval = 30_000;
cleanupBound = false;
constructor(options = {}) {
super();
this.poolSize =
options.poolSize ??
(isNode
? 6
: Math.max(Math.max(1, Math.ceil((navigator?.hardwareConcurrency ?? 8) / 2)), 6));
}
get stats() {
return {
pending: this.pending.size,
queued: this.queue.length,
available: this.available.length,
total: this.workers.length,
processed: this.tasksProcessed,
failed: this.tasksFailed,
};
}
async terminate() {
if (!this.initialized && !this.initializing)
return;
const queuedCount = this.queue.length;
const pendingCount = this.pending.size;
for (const task of this.queue) {
task.reject(new Error('Threader terminated'));
}
for (const [, handler] of this.pending) {
handler.reject(new Error('Threader terminated'));
}
for (const worker of this.workers) {
await worker.terminate();
}
this.queue = [];
this.pending.clear();
this.workers = [];
this.available = [];
this.initialized = false;
this.initializing = null;
if (queuedCount > 0 || pendingCount > 0) {
this.info(`Terminated. Rejected ${queuedCount} queued and ${pendingCount} pending tasks. Total processed: ${this.tasksProcessed}, failed: ${this.tasksFailed}`);
}
}
async drain() {
if (!this.initialized)
return;
const queuedCount = this.queue.length;
const pendingCount = this.pending.size;
this.info(`Draining. Rejecting ${queuedCount} queued, waiting for ${pendingCount} pending...`);
for (const task of this.queue) {
task.reject(new Error('Threader draining'));
}
this.queue = [];
if (this.pending.size > 0) {
await new Promise((resolve) => {
const checkDone = () => {
if (this.pending.size === 0) {
resolve();
}
};
const originalPending = new Map(this.pending);
for (const [id, handler] of originalPending) {
const origResolve = handler.resolve;
const origReject = handler.reject;
handler.resolve = (v) => {
origResolve(v);
checkDone();
};
handler.reject = (e) => {
origReject(e);
checkDone();
};
this.pending.set(id, handler);
}
checkDone();
});
}
await this.terminate();
}
runWithTransfer(op, data, transferables) {
return new Promise(async (resolve, reject) => {
await this.init();
this.queue.push({ resolve, reject, op, data, transferables });
this.processQueue();
});
}
run(op, data) {
return new Promise(async (resolve, reject) => {
await this.init();
this.queue.push({ resolve, reject, op, data });
this.processQueue();
});
}
bindCleanupHandlers() {
if (this.cleanupBound)
return;
this.cleanupBound = true;
const cleanup = () => {
this.terminate().catch(() => { });
};
if (isNode) {
process.once('beforeExit', cleanup);
process.once('SIGINT', cleanup);
process.once('SIGTERM', cleanup);
}
else if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', cleanup);
window.addEventListener('unload', cleanup);
}
else if (typeof self !== 'undefined') {
self.addEventListener('beforeunload', cleanup);
}
}
async init() {
if (this.initialized)
return;
if (this.initializing)
return this.initializing;
this.bindCleanupHandlers();
this.initializing = (async () => {
const startTime = Date.now();
const workers = await Promise.all(Array.from({ length: this.poolSize }, () => createWorker(this.workerScript)));
for (const worker of workers) {
worker.onMessage((msg) => {
const handler = this.pending.get(msg.id);
if (handler) {
this.pending.delete(msg.id);
if (msg.error) {
this.tasksFailed++;
handler.reject(new Error(msg.error));
}
else {
this.tasksProcessed++;
handler.resolve(msg.result);
}
}
this.available.push(worker);
this.logStatsIfNeeded();
this.processQueue();
});
this.workers.push(worker);
this.available.push(worker);
}
this.initialized = true;
})();
return this.initializing;
}
logStatsIfNeeded() {
const now = Date.now();
if (now - this.lastStatsLog < this.statsInterval)
return;
this.lastStatsLog = now;
}
processQueue() {
if (this.queue.length > 100 && this.available.length === 0) {
this.warn(`Queue backing up: ${this.queue.length} tasks waiting, no workers available`);
}
while (this.queue.length > 0 && this.available.length > 0) {
const task = this.queue.shift();
if (!task)
break;
const worker = this.available.pop();
if (!worker)
break;
const id = this.idCounter++;
this.pending.set(id, task);
worker.postMessage({ id, op: task.op, data: task.data }, task.transferables);
}
}
}