mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-28 00:15:27 -07:00
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { createLogger } from '../../../logger';
|
|
|
|
interface WorkerResponse {
|
|
ok?: boolean;
|
|
error?: unknown;
|
|
}
|
|
|
|
interface WorkerHandle {
|
|
once(event: 'message', listener: (message: WorkerResponse) => void): this;
|
|
once(event: 'error', listener: (error: Error) => void): this;
|
|
once(event: 'exit', listener: (code: number) => void): this;
|
|
terminate(): Promise<number>;
|
|
}
|
|
|
|
interface LexicalRollupWorkerRuntimeOptions {
|
|
resolveWorkerPath?: () => string | null;
|
|
createWorker?: (workerPath: string, workerData: { dbPath: string }) => Promise<WorkerHandle>;
|
|
timeoutMs?: number;
|
|
warn?: (message: string, ...meta: unknown[]) => void;
|
|
}
|
|
|
|
const logger = createLogger('main:immersion-tracker:lexical-rollup-worker');
|
|
const DEFAULT_WORKER_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
|
|
export function resolveLexicalRollupWorkerPath(): string | null {
|
|
const fileName = __filename.endsWith('.ts')
|
|
? 'lexical-rollup-worker-thread.ts'
|
|
: 'lexical-rollup-worker-thread.js';
|
|
const workerPath = path.join(__dirname, fileName);
|
|
return fs.existsSync(workerPath) ? workerPath : null;
|
|
}
|
|
|
|
export class LexicalRollupWorkerRuntime {
|
|
private readonly activeWorkers = new Set<WorkerHandle>();
|
|
private destroyed = false;
|
|
|
|
constructor(private readonly options: LexicalRollupWorkerRuntimeOptions = {}) {}
|
|
|
|
async run(dbPath: string): Promise<void> {
|
|
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
|
let worker: WorkerHandle;
|
|
try {
|
|
const workerPath = (this.options.resolveWorkerPath ?? resolveLexicalRollupWorkerPath)();
|
|
if (!workerPath) throw new Error('Emitted lexical rollup worker module was not found');
|
|
const createWorker =
|
|
this.options.createWorker ??
|
|
(async (resolvedPath, workerData) => {
|
|
const { Worker } = await import('node:worker_threads');
|
|
return new Worker(resolvedPath, { workerData });
|
|
});
|
|
worker = await createWorker(workerPath, { dbPath });
|
|
} catch (error) {
|
|
if (this.destroyed) throw new Error('Lexical rollup worker is shut down');
|
|
(this.options.warn ?? logger.warn)(
|
|
'Lexical rollup worker unavailable; leaving backfill pending for a later startup',
|
|
error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this.destroyed) {
|
|
await worker.terminate().catch(() => undefined);
|
|
throw new Error('Lexical rollup worker is shut down');
|
|
}
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
let settled = false;
|
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
this.activeWorkers.add(worker);
|
|
const settle = (error?: Error) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
if (timeout) clearTimeout(timeout);
|
|
this.activeWorkers.delete(worker);
|
|
void worker.terminate().catch(() => undefined);
|
|
if (error) reject(error);
|
|
else resolve();
|
|
};
|
|
timeout = setTimeout(
|
|
() => settle(new Error('Lexical rollup worker timed out')),
|
|
this.options.timeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS,
|
|
);
|
|
worker.once('message', (message) => {
|
|
if (message.ok) settle();
|
|
else
|
|
settle(
|
|
new Error(
|
|
`Lexical rollup backfill failed: ${String(message.error ?? 'unknown error')}`,
|
|
),
|
|
);
|
|
});
|
|
worker.once('error', (error) => settle(error));
|
|
worker.once('exit', (code) => {
|
|
if (!settled) {
|
|
settle(
|
|
new Error(
|
|
code === 0
|
|
? 'Lexical rollup worker exited without a response'
|
|
: `Lexical rollup worker exited with code ${code}`,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
destroy(): void {
|
|
if (this.destroyed) return;
|
|
this.destroyed = true;
|
|
for (const worker of this.activeWorkers) {
|
|
void worker.terminate().catch(() => undefined);
|
|
}
|
|
this.activeWorkers.clear();
|
|
}
|
|
}
|