mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
294 lines
8.3 KiB
TypeScript
294 lines
8.3 KiB
TypeScript
import { app } from 'electron';
|
|
import { randomUUID } from 'crypto';
|
|
import * as path from 'path';
|
|
import { pathToFileURL } from 'url';
|
|
import { Worker } from 'worker_threads';
|
|
import type {
|
|
DbOperationEvent,
|
|
DbWorkerMessage,
|
|
DbWorkerOperation,
|
|
} from '../workers/database-worker.types';
|
|
import {
|
|
isDbTraceEnabled,
|
|
roundTraceDuration,
|
|
summarizeForTrace,
|
|
trace,
|
|
} from './debug-trace';
|
|
import { resolveWorkerRuntimeBootstrap } from '../workers/worker-runtime-paths';
|
|
|
|
type PendingRequest = {
|
|
resolve: (value: unknown) => void;
|
|
reject: (reason?: unknown) => void;
|
|
onEvent?: (event: DbOperationEvent) => void;
|
|
operation: DbWorkerOperation;
|
|
startedAt: number;
|
|
};
|
|
|
|
type RequestOptions = {
|
|
onEvent?: (event: DbOperationEvent) => void;
|
|
};
|
|
|
|
function createWorkerError(error: {
|
|
message: string;
|
|
name?: string;
|
|
stack?: string;
|
|
}): Error {
|
|
const workerError = new Error(error.message);
|
|
workerError.name = error.name || 'DatabaseWorkerError';
|
|
workerError.stack = error.stack || workerError.stack;
|
|
return workerError;
|
|
}
|
|
|
|
export class DatabaseWorkerClient {
|
|
private worker: Worker | null = null;
|
|
private readyPromise: Promise<void> | null = null;
|
|
private readyResolve: (() => void) | null = null;
|
|
private readyReject: ((reason?: unknown) => void) | null = null;
|
|
private pendingRequests = new Map<string, PendingRequest>();
|
|
private shuttingDown = false;
|
|
|
|
async request<TResult>(
|
|
operation: DbWorkerOperation,
|
|
payload: unknown,
|
|
options?: RequestOptions
|
|
): Promise<TResult> {
|
|
await this.ensureWorker();
|
|
|
|
const requestId = randomUUID();
|
|
const startedAt = Date.now();
|
|
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-request', 'dispatch', {
|
|
operation,
|
|
payload: summarizeForTrace(payload),
|
|
requestId,
|
|
});
|
|
}
|
|
|
|
return new Promise<TResult>((resolve, reject) => {
|
|
this.pendingRequests.set(requestId, {
|
|
resolve: resolve as (value: unknown) => void,
|
|
reject,
|
|
onEvent: options?.onEvent,
|
|
operation,
|
|
startedAt,
|
|
});
|
|
|
|
this.worker?.postMessage({
|
|
type: 'request',
|
|
requestId,
|
|
operation,
|
|
payload,
|
|
});
|
|
});
|
|
}
|
|
|
|
async cancel(operationId: string): Promise<{ success: boolean }> {
|
|
if (!operationId) {
|
|
return { success: false };
|
|
}
|
|
|
|
await this.ensureWorker();
|
|
this.worker?.postMessage({
|
|
type: 'cancel',
|
|
operationId,
|
|
});
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
async shutdown(): Promise<void> {
|
|
this.shuttingDown = true;
|
|
this.readyResolve = null;
|
|
this.readyReject = null;
|
|
this.readyPromise = null;
|
|
|
|
if (!this.worker) {
|
|
return;
|
|
}
|
|
|
|
const currentWorker = this.worker;
|
|
this.worker = null;
|
|
this.rejectAllPending(new Error('Database worker shut down'));
|
|
|
|
try {
|
|
await currentWorker.terminate();
|
|
} finally {
|
|
this.shuttingDown = false;
|
|
}
|
|
}
|
|
|
|
private async ensureWorker(): Promise<void> {
|
|
if (this.worker && this.readyPromise) {
|
|
return this.readyPromise;
|
|
}
|
|
|
|
this.createWorker();
|
|
return this.readyPromise!;
|
|
}
|
|
|
|
private createWorker(): void {
|
|
const bootstrap = resolveWorkerRuntimeBootstrap({
|
|
isPackaged: app.isPackaged,
|
|
workerFilename: 'database.worker.js',
|
|
developmentWorkerDir: path.join(__dirname, 'workers'),
|
|
resourcesPath: (
|
|
process as NodeJS.Process & { resourcesPath?: string }
|
|
).resourcesPath,
|
|
appPath: app.getAppPath(),
|
|
});
|
|
|
|
this.readyPromise = new Promise<void>((resolve, reject) => {
|
|
this.readyResolve = resolve;
|
|
this.readyReject = reject;
|
|
});
|
|
|
|
try {
|
|
const workerURL = pathToFileURL(bootstrap.workerPath);
|
|
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-worker', 'create', {
|
|
nativeModuleSearchPaths:
|
|
bootstrap.nativeModuleSearchPaths?.length ?? 0,
|
|
workerPath: bootstrap.workerPath,
|
|
});
|
|
}
|
|
|
|
this.worker = new Worker(workerURL, {
|
|
workerData: {
|
|
nativeModuleSearchPaths: bootstrap.nativeModuleSearchPaths,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
this.readyReject?.(error);
|
|
this.resetWorkerState();
|
|
throw error;
|
|
}
|
|
|
|
this.worker.on('message', (message: DbWorkerMessage) => {
|
|
this.handleMessage(message);
|
|
});
|
|
this.worker.on('error', (error) => {
|
|
this.handleWorkerFailure(error);
|
|
});
|
|
this.worker.on('exit', (code) => {
|
|
this.handleWorkerExit(code);
|
|
});
|
|
}
|
|
|
|
private handleMessage(message: DbWorkerMessage): void {
|
|
if (message.type === 'ready') {
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-worker', 'ready');
|
|
}
|
|
this.readyResolve?.();
|
|
this.readyResolve = null;
|
|
this.readyReject = null;
|
|
return;
|
|
}
|
|
|
|
if (message.type === 'event') {
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-event', 'worker-event', {
|
|
event: message.event,
|
|
requestId: message.requestId,
|
|
});
|
|
}
|
|
this.pendingRequests.get(message.requestId)?.onEvent?.(message.event);
|
|
return;
|
|
}
|
|
|
|
const pendingRequest = this.pendingRequests.get(message.requestId);
|
|
if (!pendingRequest) {
|
|
return;
|
|
}
|
|
|
|
this.pendingRequests.delete(message.requestId);
|
|
|
|
if (message.success) {
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-request', 'resolved', {
|
|
durationMs: roundTraceDuration(
|
|
Date.now() - pendingRequest.startedAt
|
|
),
|
|
operation: pendingRequest.operation,
|
|
requestId: message.requestId,
|
|
result: summarizeForTrace(message.result),
|
|
});
|
|
}
|
|
pendingRequest.resolve(message.result);
|
|
return;
|
|
}
|
|
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-request', 'failed', {
|
|
durationMs: roundTraceDuration(
|
|
Date.now() - pendingRequest.startedAt
|
|
),
|
|
error: message.error,
|
|
operation: pendingRequest.operation,
|
|
requestId: message.requestId,
|
|
});
|
|
}
|
|
|
|
pendingRequest.reject(
|
|
createWorkerError(
|
|
message.error ?? { message: 'Database worker request failed' }
|
|
)
|
|
);
|
|
}
|
|
|
|
private handleWorkerFailure(error: Error): void {
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-worker', 'error', error);
|
|
}
|
|
|
|
if (this.readyReject) {
|
|
this.readyReject(error);
|
|
}
|
|
|
|
this.resetWorkerState();
|
|
this.rejectAllPending(error);
|
|
}
|
|
|
|
private handleWorkerExit(code: number): void {
|
|
if (this.shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
if (isDbTraceEnabled()) {
|
|
trace('db-worker', 'exit', { code });
|
|
}
|
|
|
|
const error =
|
|
code === 0
|
|
? new Error('Database worker exited unexpectedly')
|
|
: new Error(`Database worker stopped with exit code ${code}`);
|
|
|
|
if (this.readyReject) {
|
|
this.readyReject(error);
|
|
}
|
|
|
|
this.resetWorkerState();
|
|
this.rejectAllPending(error);
|
|
}
|
|
|
|
private resetWorkerState(): void {
|
|
this.worker = null;
|
|
this.readyPromise = null;
|
|
this.readyResolve = null;
|
|
this.readyReject = null;
|
|
}
|
|
|
|
private rejectAllPending(error: Error): void {
|
|
const pendingRequests = [...this.pendingRequests.values()];
|
|
this.pendingRequests.clear();
|
|
|
|
pendingRequests.forEach((request) => {
|
|
request.reject(error);
|
|
});
|
|
}
|
|
}
|
|
|
|
export const databaseWorkerClient = new DatabaseWorkerClient();
|