UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

189 lines (168 loc) 5.25 kB
/** * Worker pool for parallel page building using true multicore. */ import { Worker } from 'node:worker_threads' import { cpus } from 'node:os' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) interface PendingTask { id: number resolve: (result: any) => void reject: (error: Error) => void } export class BuildWorkerPool { private workers: Worker[] = [] private available: Worker[] = [] private taskQueue: Array<{ msg: any; pending: PendingTask }> = [] private pendingById = new Map<number, PendingTask>() private nextId = 0 private readyCount = 0 private initCount = 0 private _ready: Promise<void> private _resolveReady!: () => void private _initialized: Promise<void> private _resolveInitialized!: () => void private _terminated = false constructor(size = Math.max(1, cpus().length - 1)) { this._ready = new Promise((resolve) => { this._resolveReady = resolve }) this._initialized = new Promise((resolve) => { this._resolveInitialized = resolve }) // use .mjs for proper ESM module resolution in worker threads const workerPath = join(__dirname, 'buildPageWorker.mjs') for (let i = 0; i < size; i++) { const worker = new Worker(workerPath) worker.on('message', (msg: any) => { if (msg.type === 'ready') { this.readyCount++ this.available.push(worker) if (this.readyCount === size) { this._resolveReady() } } else if (msg.type === 'init-done') { this.initCount++ if (this.initCount === size) { this._resolveInitialized() } this.dispatch() } else if (msg.type === 'done' || msg.type === 'error') { const pending = this.pendingById.get(msg.id) if (pending) { this.pendingById.delete(msg.id) if (msg.type === 'done') { pending.resolve(msg.result) } else { pending.reject(new Error(msg.error)) } } this.available.push(worker) this.dispatch() } }) worker.on('error', (err) => { console.error('[BuildWorkerPool] Worker error:', err) }) this.workers.push(worker) } } get size() { return this.workers.length } // initialize all workers - they load config themselves from vite.config async initialize() { await this._ready // send init message to all workers // workers load the vite config themselves to avoid serialization issues for (const worker of this.workers) { worker.postMessage({ type: 'init', id: this.nextId++ }) } // wait for all workers to be initialized await this._initialized } private dispatch() { while (this.available.length > 0 && this.taskQueue.length > 0) { const worker = this.available.shift()! const { msg, pending } = this.taskQueue.shift()! this.pendingById.set(pending.id, pending) worker.postMessage(msg) } } async buildPage(args: { serverEntry: string path: string relativeId: string params: any foundRoute: any clientManifestEntry: any staticDir: string clientDir: string builtMiddlewares: Record<string, string> serverJsPath: string preloads: string[] allCSS: string[] routePreloads: Record<string, string> allCSSContents?: string[] criticalPreloads?: string[] deferredPreloads?: string[] useAfterLCP?: boolean useAfterLCPAggressive?: boolean }): Promise<any> { if (this._terminated) { throw new Error('Worker pool has been terminated') } // serialize foundRoute to only include plain data (no functions) // workers can't receive non-serializable data like functions const serializedRoute = { type: args.foundRoute.type, file: args.foundRoute.file, // only keep serializable layout data layouts: args.foundRoute.layouts?.map((layout: any) => ({ contextKey: layout.contextKey, loaderServerPath: layout.loaderServerPath, layoutRenderMode: layout.layoutRenderMode, })), // only keep contextKey from middlewares middlewares: args.foundRoute.middlewares?.map((mw: any) => ({ contextKey: mw.contextKey, })), } const id = this.nextId++ const msg = { type: 'build', id, args: { ...args, foundRoute: serializedRoute, }, } return new Promise((resolve, reject) => { const pending: PendingTask = { id, resolve, reject } this.taskQueue.push({ msg, pending }) this.dispatch() }) } async terminate() { this._terminated = true await Promise.all(this.workers.map((w) => w.terminate())) this.workers = [] this.available = [] } } // singleton pool instance let pool: BuildWorkerPool | null = null export function getWorkerPool(size?: number): BuildWorkerPool { if (!pool) { pool = new BuildWorkerPool(size) } return pool } export async function terminateWorkerPool() { if (pool) { await pool.terminate() pool = null } }