Download pool/BrowserPool.js from stnh70/vid: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/spaces/stnh70/vid/resolve/main/pool/BrowserPool.js
- Command line
-
hf download hf://spaces/stnh70/vid/pool/BrowserPool.js
-
curl -L -o BrowserPool.js https://huggingface.co/spaces/stnh70/vid/resolve/main/pool/BrowserPool.js
10.8 kB
| import { EventEmitter } from 'events'; | |
| import { randomUUID } from 'crypto'; | |
| export default class BrowserPool extends EventEmitter { | |
| constructor(options = {}) { | |
| super(); | |
| this.chromium = options.chromium; | |
| if (!this.chromium) throw new Error('Chromium instance must be provided'); | |
| this.maxSize = options.maxSize || 5; | |
| this.minSize = options.minSize || 1; | |
| this.maxUsage = options.maxUsage || 100; | |
| this.maxIdleTime = options.maxIdleTime || 300000; | |
| this.createTimeout = options.createTimeout || 30000; | |
| this.validateInterval = options.validateInterval || 60000; | |
| this.getTimeout = options.getTimeout || 30000; | |
| this.availableInstances = []; | |
| this.busyInstances = new Set(); | |
| this.totalInstances = 0; | |
| this.waitingQueue = []; | |
| this.stats = { created: 0, destroyed: 0, borrowed: 0, returned: 0, timeouts: 0, errors: 0 }; | |
| this.isShuttingDown = false; | |
| this.healthCheckInterval = null; | |
| } | |
| async initialize() { | |
| console.log(`[BrowserPool] Initializing pool with min: ${this.minSize}, max: ${this.maxSize}`); | |
| const createPromises = Array.from({ length: this.minSize }, () => this.createInstance()); | |
| try { | |
| await Promise.all(createPromises); | |
| console.log(`[BrowserPool] Pool initialized with ${this.availableInstances.length} instances.`); | |
| } catch (error) { | |
| console.error('[BrowserPool] Failed to initialize pool:', error.message); | |
| await this.shutdown(); | |
| throw error; | |
| } | |
| this.startHealthCheck(); | |
| } | |
| async createInstance() { | |
| if (this.isShuttingDown) { throw new Error('Pool is shutting down'); } | |
| if (this.totalInstances >= this.maxSize) { return null; } | |
| const startTime = Date.now(); | |
| let browser = null; | |
| this.totalInstances++; | |
| try { | |
| console.log('[BrowserPool] Creating new browser instance...'); | |
| browser = await Promise.race([ | |
| this.chromium.launch({ | |
| headless: true, | |
| args: [ | |
| // '--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', | |
| // '--disable-web-security', '--autoplay-policy=no-user-gesture-required', | |
| // '--disable-dev-shm-usage', '--disable-gpu', '--single-process', '--no-zygote', | |
| // '--disable-infobars', '--disable-notifications', '--disable-extensions', | |
| '--no-sandbox', | |
| '--disable-setuid-sandbox', | |
| '--disable-blink-features=AutomationControlled', | |
| '--disable-web-security', | |
| '--autoplay-policy=no-user-gesture-required', | |
| '--disable-dev-shm-usage', // 避免共享内存问题 | |
| '--disable-gpu', // 减少资源使用 | |
| '--no-first-run', // 跳过首次运行设置 | |
| '--disable-default-apps' // 禁用默认应用 | |
| ] | |
| }), | |
| new Promise((_, reject) => setTimeout(() => reject(new Error('Browser creation timeout')), this.createTimeout)) | |
| ]); | |
| const instance = { | |
| browser, id: `browser_${randomUUID().slice(0, 8)}`, createdAt: Date.now(), | |
| lastUsed: Date.now(), usageCount: 0, isHealthy: true | |
| }; | |
| browser.on('disconnected', () => { | |
| console.log(`[BrowserPool] Browser ${instance.id} disconnected unexpectedly.`); | |
| this.destroyInstance(instance, true); | |
| }); | |
| this.stats.created++; | |
| console.log(`[BrowserPool] Browser instance created: ${instance.id} (${Date.now() - startTime}ms)`); | |
| this.emit('instanceCreated', instance); | |
| this.availableInstances.push(instance); | |
| this.processWaitingQueue(); | |
| return instance; | |
| } catch (error) { | |
| this.totalInstances--; | |
| this.stats.errors++; | |
| console.error('[BrowserPool] Failed to create browser instance:', error.message); | |
| if (browser) await browser.close().catch(e => console.error(`Error closing failed browser: ${e.message}`)); | |
| throw error; | |
| } | |
| } | |
| get() { | |
| if (this.isShuttingDown) return Promise.reject(new Error('Pool is shutting down')); | |
| return new Promise((resolve, reject) => { | |
| const timeoutId = setTimeout(() => { | |
| this.waitingQueue = this.waitingQueue.filter(w => w.resolve !== resolve); | |
| this.stats.timeouts++; | |
| reject(new Error(`Browser acquisition timeout after ${this.getTimeout}ms`)); | |
| }, this.getTimeout); | |
| const tryAcquire = async () => { | |
| while (this.availableInstances.length > 0) { | |
| const instance = this.availableInstances.shift(); | |
| if (await this.validateInstance(instance)) { | |
| this.busyInstances.add(instance); | |
| instance.lastUsed = Date.now(); | |
| instance.usageCount++; | |
| this.stats.borrowed++; | |
| clearTimeout(timeoutId); | |
| return resolve(instance); | |
| } else { | |
| await this.destroyInstance(instance); | |
| } | |
| } | |
| if (this.totalInstances < this.maxSize) { | |
| this.createInstance().catch(err => console.error(`[BrowserPool] Background instance creation failed: ${err.message}`)); | |
| } | |
| this.waitingQueue.push({ resolve, reject, timeoutId }); | |
| console.log(`[BrowserPool] No available instances, request added to waiting queue (size: ${this.waitingQueue.length})`); | |
| }; | |
| tryAcquire(); | |
| }); | |
| } | |
| async release(instance) { | |
| if (!instance || !this.busyInstances.has(instance)) { return; } | |
| this.busyInstances.delete(instance); | |
| this.stats.returned++; | |
| if (this.isShuttingDown || !await this.validateInstance(instance) || instance.usageCount >= this.maxUsage) { | |
| await this.destroyInstance(instance); | |
| } else { | |
| try { | |
| const contexts = instance.browser.contexts(); | |
| // 默认上下文是第一个,我们不关闭它,只关闭后续创建的 | |
| for (let i = 1; i < contexts.length; i++) { | |
| await contexts[i].close(); | |
| } | |
| this.availableInstances.push(instance); | |
| } catch (e) { | |
| console.warn(`[BrowserPool] Error cleaning up contexts for ${instance.id}, destroying it.`, e.message); | |
| await this.destroyInstance(instance); | |
| } | |
| } | |
| this.processWaitingQueue(); | |
| } | |
| async destroyInstance(instance, isDisconnected = false) { | |
| if (!instance) return; | |
| this.busyInstances.delete(instance); | |
| const index = this.availableInstances.indexOf(instance); | |
| if (index > -1) this.availableInstances.splice(index, 1); | |
| if (!isDisconnected && instance.browser && instance.browser.isConnected()) { | |
| await instance.browser.close().catch(e => console.error(`[BrowserPool] Error closing browser on destroy: ${e.message}`)); | |
| } | |
| if (this.totalInstances > 0) this.totalInstances--; | |
| this.stats.destroyed++; | |
| console.log(`[BrowserPool] Instance ${instance.id} destroyed.`); | |
| this.emit('instanceDestroyed', instance); | |
| if (!this.isShuttingDown && this.totalInstances < this.minSize) { | |
| this.createInstance().catch(err => console.error(`[BrowserPool] Failed to replenish instance: ${err.message}`)); | |
| } | |
| } | |
| processWaitingQueue() { | |
| if (this.waitingQueue.length > 0 && this.availableInstances.length > 0) { | |
| console.log('[BrowserPool] Processing waiting queue...'); | |
| const waiter = this.waitingQueue.shift(); | |
| const instance = this.availableInstances.shift(); | |
| this.busyInstances.add(instance); | |
| instance.lastUsed = Date.now(); | |
| instance.usageCount++; | |
| this.stats.borrowed++; | |
| clearTimeout(waiter.timeoutId); | |
| waiter.resolve(instance); | |
| } | |
| } | |
| async validateInstance(instance) { | |
| if (!instance || !instance.browser || !instance.browser.isConnected()) return false; | |
| try { | |
| // 一个更轻量级的检查,创建一个新的 incognito context | |
| const context = await instance.browser.newContext(); | |
| await context.close(); | |
| return true; | |
| } catch (error) { | |
| console.warn(`[BrowserPool] Instance ${instance.id} validation failed.`); | |
| return false; | |
| } | |
| } | |
| startHealthCheck() { | |
| this.healthCheckInterval = setInterval(() => this.performHealthCheck(), this.validateInterval); | |
| } | |
| async performHealthCheck() { | |
| if (this.isShuttingDown) return; | |
| const now = Date.now(); | |
| const idleInstances = this.availableInstances.slice(); // 创建副本以安全遍历 | |
| for (const instance of idleInstances) { | |
| if (this.totalInstances > this.minSize && now - instance.lastUsed > this.maxIdleTime) { | |
| console.log(`[BrowserPool] Removing idle instance ${instance.id}`); | |
| await this.destroyInstance(instance); | |
| } | |
| } | |
| } | |
| getStats() { | |
| return { | |
| ...this.stats, | |
| pool: { | |
| available: this.availableInstances.length, | |
| busy: this.busyInstances.size, | |
| total: this.totalInstances, | |
| waiting: this.waitingQueue.length | |
| } | |
| }; | |
| } | |
| async shutdown() { | |
| console.log('[BrowserPool] Shutting down...'); | |
| this.isShuttingDown = true; | |
| if (this.healthCheckInterval) clearInterval(this.healthCheckInterval); | |
| for (const waiter of this.waitingQueue) { | |
| clearTimeout(waiter.timeoutId); | |
| waiter.reject(new Error('Pool is shutting down')); | |
| } | |
| this.waitingQueue = []; | |
| const allInstances = [...this.availableInstances, ...Array.from(this.busyInstances)]; | |
| await Promise.all(allInstances.map(instance => instance.browser.close())); | |
| console.log('[BrowserPool] All browser instances closed.'); | |
| } | |
| } | |
| // 使用 module.exports 导出类 | |
| // module.exports = { BrowserPool }; | |