Coordinates worker-based and main-thread preparation of data table slices.

export class DataTableWorkerPool {
private isInitialized = false;
private readonly maxWorkers: number;

constructor(maxWorkers?: number) {
this.maxWorkers = maxWorkers ?? 2;
}

/**
* Initializes the shared worker pool or enables main-thread fallback.
* @source
*/
async initialize(): Promise<void> {
if (this.isInitialized) {
return;
}

try {
const pool = getGenericWorkerPool();
await pool.initialize();
this.isInitialized = true;
console.info("[DataTableWorkerPool] Pool initialized");
} catch (error) {
console.warn("[DataTableWorkerPool] Failed to initialize pool:", error);
// Still mark as initialized to use main thread fallback
this.isInitialized = true;
}
}

/**
* Prepares a table data slice, preferring workers and falling back to the main thread.
* @param data - Full manga dataset to slice from.
* @param startIndex - Inclusive start index of the slice.
* @param endIndex - Exclusive end index of the slice.
* @param itemsPerPage - Number of items targeted per page/viewport.
* @param columnVisibility - Flags controlling which column values are precomputed.
* @returns Preparation result with formatted rows and metadata.
* @source
*/
async prepareTableSlice(
data: KenmeiMangaItem[],
startIndex: number,
endIndex: number,
itemsPerPage: number,
columnVisibility: {
score: boolean;
chapters: boolean;
volumes: boolean;
lastRead: boolean;
},
): Promise<DataTablePreparationResult> {
const taskId = generateTaskId();

// Ensure pool is initialized
if (!this.isInitialized) {
await this.initialize();
}

try {
const pool = getGenericWorkerPool();

// Check if workers are available
if (!pool.isAvailable()) {
console.debug(
"[DataTableWorkerPool] No workers available, using main thread",
);
return this.executeOnMainThread(
data,
startIndex,
endIndex,
columnVisibility,
);
}

// Try to use worker
return await this.executeOnWorker(
pool,
taskId,
data,
startIndex,
endIndex,
itemsPerPage,
columnVisibility,
);
} catch (error) {
console.warn(
"[DataTableWorkerPool] Worker execution failed, falling back to main thread:",
error,
);
return this.executeOnMainThread(
data,
startIndex,
endIndex,
columnVisibility,
);
}
}

/**
* Executes table preparation on a worker from the generic pool.
* Rejects and triggers fallback on worker errors or timeouts.
* @source
*/
private async executeOnWorker(
pool: ReturnType<typeof getGenericWorkerPool>,
taskId: string,
data: KenmeiMangaItem[],
startIndex: number,
endIndex: number,
itemsPerPage: number,
columnVisibility: {
score: boolean;
chapters: boolean;
volumes: boolean;
lastRead: boolean;
},
): Promise<DataTablePreparationResult> {
try {
// Get a worker from the pool
const workerIndex = pool.selectWorker();
if (workerIndex === -1) {
throw new Error("No workers available from pool");
}

const worker = pool.getWorker(workerIndex);
if (!worker) {
throw new Error("Failed to get worker from pool");
}

// Register task with the pool
const task = {
taskId,
type: "data_table_preparation" as const,
data,
startIndex,
endIndex,
itemsPerPage,
columnVisibility,
resolve: null as unknown as (result: unknown) => void,
reject: null as unknown as (error: Error) => void,
isCancelled: false,
workerIndex,
};

const taskPromise = new Promise<DataTablePreparationResult>(
(resolve, reject) => {
task.resolve = (result: unknown) => {
const typedResult = result as Record<string, unknown>;
resolve({
preparedData:
typedResult.preparedData as PreparedTableRow<KenmeiMangaItem>[],
indexInfo:
typedResult.indexInfo as DataTablePreparationResult["indexInfo"],
timing:
typedResult.timing as DataTablePreparationResult["timing"],
ranOnWorker: true,
});
};
task.reject = reject;
},
);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
pool.registerTask(taskId, task as unknown as any);

// Send message to the worker
const message: DataTablePreparationMessage = {
type: "DATA_TABLE_PREPARATION",
payload: {
taskId,
data,
viewport: {
startIndex,
endIndex,
itemsPerPage,
},
columnVisibility,
},
};

worker.postMessage(message);

console.debug(
`[DataTableWorkerPool] Dispatched preparation task ${taskId}: ${endIndex - startIndex} items`,
);

// Set timeout for task completion (10 seconds)
const timeout = setTimeout(() => {
pool.cancelTask(taskId);
task.reject(
new Error(
`[DataTableWorkerPool] Preparation task ${taskId} timed out after 10s`,
),
);
console.warn(
`[DataTableWorkerPool] Preparation task ${taskId} timed out after 10s`,
);
}, 10000);

// Wait for result and clear timeout
try {
const result = await taskPromise;
clearTimeout(timeout);
return result;
} catch (error) {
clearTimeout(timeout);
throw error;
}
} catch (error) {
console.error(
"[DataTableWorkerPool] Error executing preparation on worker:",
error,
);
throw error;
}
}

/**
* Prepares table rows on the main thread as a safe fallback when workers are unavailable.
* @source
*/
private executeOnMainThread(
data: KenmeiMangaItem[],
startIndex: number,
endIndex: number,
columnVisibility: {
score: boolean;
chapters: boolean;
volumes: boolean;
lastRead: boolean;
},
): DataTablePreparationResult {
const startTime = performance.now();
const formattingStartTime = performance.now();

// Extract and format the viewport slice
const slice = data.slice(startIndex, endIndex);

// Precompute formatted values for all visible rows using shared formatter
const preparedData = prepareTableSlice(
slice,
0,
slice.length,
columnVisibility,
);

const formattingTimeMs = performance.now() - formattingStartTime;

// No additional metadata computation needed since row heights are already computed in preparedData
const metadataStartTime = performance.now();
const metadataComputationTimeMs = performance.now() - metadataStartTime;
const totalTimeMs = performance.now() - startTime;

return {
preparedData,
indexInfo: {
startIndex,
endIndex,
totalCount: data.length,
},
timing: {
formattingTimeMs,
metadataComputationTimeMs,
totalTimeMs,
},
ranOnWorker: false,
};
}

/**
* Returns basic internal pool state used for diagnostics.
* @source
*/
getStats(): {
isInitialized: boolean;
} {
return {
isInitialized: this.isInitialized,
};
}

/**
* Returns the number of available workers in the generic pool.
* @source
*/
getAvailableWorkerCount(): number {
const pool = getGenericWorkerPool();
return this.isInitialized ? pool.getAvailableWorkerCount() : 0;
}

/**
* Shuts down the underlying worker pool and clears initialization state.
* @source
*/
terminate(): void {
if (this.isInitialized) {
try {
const pool = getGenericWorkerPool();
pool.terminate();
this.isInitialized = false;
} catch (error) {
console.warn("[DataTableWorkerPool] Error terminating pool:", error);
}
}
}
}

Constructors

Methods

  • Initializes the shared worker pool or enables main-thread fallback.

    Returns Promise<void>

      async initialize(): Promise<void> {
    if (this.isInitialized) {
    return;
    }

    try {
    const pool = getGenericWorkerPool();
    await pool.initialize();
    this.isInitialized = true;
    console.info("[DataTableWorkerPool] Pool initialized");
    } catch (error) {
    console.warn("[DataTableWorkerPool] Failed to initialize pool:", error);
    // Still mark as initialized to use main thread fallback
    this.isInitialized = true;
    }
    }
  • Prepares a table data slice, preferring workers and falling back to the main thread.

    Parameters

    • data: KenmeiMangaItem[]

      Full manga dataset to slice from.

    • startIndex: number

      Inclusive start index of the slice.

    • endIndex: number

      Exclusive end index of the slice.

    • itemsPerPage: number

      Number of items targeted per page/viewport.

    • columnVisibility: { score: boolean; chapters: boolean; volumes: boolean; lastRead: boolean }

      Flags controlling which column values are precomputed.

    Returns Promise<DataTablePreparationResult>

    Preparation result with formatted rows and metadata.

      async prepareTableSlice(
    data: KenmeiMangaItem[],
    startIndex: number,
    endIndex: number,
    itemsPerPage: number,
    columnVisibility: {
    score: boolean;
    chapters: boolean;
    volumes: boolean;
    lastRead: boolean;
    },
    ): Promise<DataTablePreparationResult> {
    const taskId = generateTaskId();

    // Ensure pool is initialized
    if (!this.isInitialized) {
    await this.initialize();
    }

    try {
    const pool = getGenericWorkerPool();

    // Check if workers are available
    if (!pool.isAvailable()) {
    console.debug(
    "[DataTableWorkerPool] No workers available, using main thread",
    );
    return this.executeOnMainThread(
    data,
    startIndex,
    endIndex,
    columnVisibility,
    );
    }

    // Try to use worker
    return await this.executeOnWorker(
    pool,
    taskId,
    data,
    startIndex,
    endIndex,
    itemsPerPage,
    columnVisibility,
    );
    } catch (error) {
    console.warn(
    "[DataTableWorkerPool] Worker execution failed, falling back to main thread:",
    error,
    );
    return this.executeOnMainThread(
    data,
    startIndex,
    endIndex,
    columnVisibility,
    );
    }
    }
  • Returns basic internal pool state used for diagnostics.

    Returns { isInitialized: boolean }

      getStats(): {
    isInitialized: boolean;
    } {
    return {
    isInitialized: this.isInitialized,
    };
    }
  • Returns the number of available workers in the generic pool.

    Returns number

      getAvailableWorkerCount(): number {
    const pool = getGenericWorkerPool();
    return this.isInitialized ? pool.getAvailableWorkerCount() : 0;
    }
  • Shuts down the underlying worker pool and clears initialization state.

    Returns void

      terminate(): void {
    if (this.isInitialized) {
    try {
    const pool = getGenericWorkerPool();
    pool.terminate();
    this.isInitialized = false;
    } catch (error) {
    console.warn("[DataTableWorkerPool] Error terminating pool:", error);
    }
    }
    }