diff --git a/src/index.ts b/src/index.ts index 1079cc0..5ef0ced 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ class CdpSession { private ws!: WebSocket; private msgId = 0; private pending = new Map void; reject: (e: Error) => void }>(); + private eventListeners = new Map void>>(); static async connect(port: number): Promise { const resp = await fetch(`http://localhost:${port}/json`); @@ -45,13 +46,20 @@ class CdpSession { this.ws.onopen = () => resolve(); this.ws.onerror = (e: any) => reject(new Error(`WebSocket error: ${e.message || e}`)); this.ws.onmessage = (ev: MessageEvent) => { - const msg: CdpResult = JSON.parse(typeof ev.data === 'string' ? ev.data : ev.data.toString()); + const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : ev.data.toString()); + // Handle command responses if (msg.id != null && this.pending.has(msg.id)) { const p = this.pending.get(msg.id)!; this.pending.delete(msg.id); if (msg.error) p.reject(new Error(msg.error.message)); else p.resolve(msg.result); } + // Handle events + if (msg.method && this.eventListeners.has(msg.method)) { + for (const fn of this.eventListeners.get(msg.method)!) { + fn(msg.params); + } + } }; }); } @@ -64,6 +72,29 @@ class CdpSession { }); } + waitForEvent(event: string, timeoutMs: number = 30000): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup(); + reject(new Error(`Timeout waiting for ${event}`)); + }, timeoutMs); + const handler = (params: any) => { + cleanup(); + resolve(params); + }; + const cleanup = () => { + clearTimeout(timer); + const listeners = this.eventListeners.get(event); + if (listeners) { + const idx = listeners.indexOf(handler); + if (idx >= 0) listeners.splice(idx, 1); + } + }; + if (!this.eventListeners.has(event)) this.eventListeners.set(event, []); + this.eventListeners.get(event)!.push(handler); + }); + } + async evaluate(expression: string): Promise { const res = await this.send('Runtime.evaluate', { expression, returnByValue: true }); return res?.result?.value; @@ -202,40 +233,47 @@ export async function run( mobile: false, }); + // Navigate and wait for page load event + const loadPromise = cdp.waitForEvent('Page.loadEventFired', 30000); await cdp.send('Page.navigate', { url }); + await loadPromise; - // Wait for window.context.result.data to be populated (poll up to 15s) + // Wait for networkIdle — poll until no pending requests for 1s + await cdp.evaluate(` + new Promise(resolve => { + let timer; + const reset = () => { clearTimeout(timer); timer = setTimeout(resolve, 1000); }; + const observer = new PerformanceObserver(() => reset()); + observer.observe({ entryTypes: ['resource'] }); + reset(); + }) + `); + + // Extract window.context.result.data let productPackInfo: unknown = null; let windowContext: unknown = null; - for (let i = 0; i < 30; i++) { - await new Promise(r => setTimeout(r, 500)); - const ctx = await cdp.evaluate(` - (function() { - try { - const d = window.context && window.context.result && window.context.result.data; - if (d && d.productPackInfo) { - return JSON.stringify({ - productPackInfo: d.productPackInfo, - productTitle: d.productTitle || null, - productAttributes: d.productAttributes || null, - skuSelection: d.skuSelection || null, - }); - } - } catch(e) {} - return null; - })() - `); - if (ctx) { - const parsed = JSON.parse(ctx); - productPackInfo = parsed.productPackInfo; - windowContext = parsed; - break; - } + const ctx = await cdp.evaluate(` + (function() { + try { + const d = window.context && window.context.result && window.context.result.data; + if (d && d.productPackInfo) { + return JSON.stringify({ + productPackInfo: d.productPackInfo, + productTitle: d.productTitle || null, + productAttributes: d.productAttributes || null, + skuSelection: d.skuSelection || null, + }); + } + } catch(e) {} + return null; + })() + `); + if (ctx) { + const parsed = JSON.parse(ctx); + productPackInfo = parsed.productPackInfo; + windowContext = parsed; } - // Extra wait for remaining dynamic content (images etc) - await new Promise(r => setTimeout(r, 2000)); - const outputDir = path.join('/tmp', '1688-logistics', offerId); // Capture full-page screenshots (scrolling)