UNPKG

chrome-devtools-frontend

Version:
156 lines (133 loc) 6.36 kB
// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../common/common.js'; import * as ProtocolClient from '../protocol_client/protocol_client.js'; // eslint-disable-line no-unused-vars import * as SDK from '../sdk/sdk.js'; import * as ReportRenderer from './LighthouseReporterTypes.js'; // eslint-disable-line no-unused-vars let lastId = 1; export class ProtocolService extends Common.ObjectWrapper.ObjectWrapper { private rawConnection?: ProtocolClient.InspectorBackend.Connection; private lighthouseWorkerPromise?: Promise<Worker>; private lighthouseMessageUpdateCallback?: ((arg0: string) => void); async attach(): Promise<void> { await SDK.SDKModel.TargetManager.instance().suspendAllTargets(); const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget(); if (!mainTarget) { throw new Error('Unable to find main target required for LightHouse'); } const childTargetManager = mainTarget.model(SDK.ChildTargetManager.ChildTargetManager); if (!childTargetManager) { throw new Error('Unable to find child target manager required for LightHouse'); } this.rawConnection = await childTargetManager.createParallelConnection(message => { if (typeof message === 'string') { message = JSON.parse(message); } this.dispatchProtocolMessage(message); }); } getLocales(): readonly string[] { return navigator.languages; } startLighthouse(auditURL: string, categoryIDs: string[], flags: Object): Promise<ReportRenderer.RunnerResult> { return this.sendWithResponse('start', {url: auditURL, categoryIDs, flags, locales: this.getLocales()}); } async detach(): Promise<void> { const oldLighthouseWorker = this.lighthouseWorkerPromise; const oldRawConnection = this.rawConnection; // When detaching, make sure that we remove the old promises, before we // perform any async cleanups. That way, if there is a message coming from // lighthouse while we are in the process of cleaning up, we shouldn't deliver // them to the backend. this.lighthouseWorkerPromise = undefined; this.rawConnection = undefined; if (oldLighthouseWorker) { (await oldLighthouseWorker).terminate(); } if (oldRawConnection) { await oldRawConnection.disconnect(); } await SDK.SDKModel.TargetManager.instance().resumeAllTargets(); } registerStatusCallback(callback: (arg0: string) => void): void { this.lighthouseMessageUpdateCallback = callback; } private dispatchProtocolMessage(message: Object): void { // A message without a sessionId is the main session of the main target (call it "Main session"). // A parallel connection and session was made that connects to the same main target (call it "Lighthouse session"). // Messages from the "Lighthouse session" have a sessionId. // Without some care, there is a risk of sending the same events for the same main frame to Lighthouse–the backend // will create events for the "Main session" and the "Lighthouse session". // The workaround–only send message to Lighthouse if: // * the message has a sessionId (is not for the "Main session") // * the message does not have a sessionId (is for the "Main session"), but only for the Target domain // (to kickstart autoAttach in LH). const protocolMessage = message as { sessionId?: string, method?: string, }; if (protocolMessage.sessionId || (protocolMessage.method && protocolMessage.method.startsWith('Target'))) { this.sendWithoutResponse('dispatchProtocolMessage', {message: JSON.stringify(message)}); } } private initWorker(): Promise<Worker> { this.lighthouseWorkerPromise = new Promise<Worker>(resolve => { const worker = new Worker(new URL('../lighthouse_worker.js', import.meta.url), {type: 'module'}); worker.addEventListener('message', event => { if (event.data === 'workerReady') { resolve(worker); return; } const lighthouseMessage = JSON.parse(event.data); if (lighthouseMessage.method === 'statusUpdate') { if (this.lighthouseMessageUpdateCallback && lighthouseMessage.params && 'message' in lighthouseMessage.params) { this.lighthouseMessageUpdateCallback(lighthouseMessage.params.message as string); } } else if (lighthouseMessage.method === 'sendProtocolMessage') { if (lighthouseMessage.params && 'message' in lighthouseMessage.params) { this.sendProtocolMessage(lighthouseMessage.params.message as string); } } }); }); return this.lighthouseWorkerPromise; } private async ensureWorkerExists(): Promise<Worker> { let worker: Worker; if (!this.lighthouseWorkerPromise) { worker = await this.initWorker(); } else { worker = await this.lighthouseWorkerPromise; } return worker; } private sendProtocolMessage(message: string): void { if (this.rawConnection) { this.rawConnection.sendRawMessage(message); } } private async sendWithoutResponse(method: string, params: {[x: string]: string|string[]|Object} = {}): Promise<void> { const worker = await this.ensureWorkerExists(); const messageId = lastId++; worker.postMessage(JSON.stringify({id: messageId, method, params: {...params, id: messageId}})); } private async sendWithResponse(method: string, params: {[x: string]: string|string[]|Object} = {}): Promise<ReportRenderer.RunnerResult> { const worker = await this.ensureWorkerExists(); const messageId = lastId++; const messageResult = new Promise<ReportRenderer.RunnerResult>(resolve => { const workerListener = (event: MessageEvent): void => { const lighthouseMessage = JSON.parse(event.data); if (lighthouseMessage.id === messageId) { worker.removeEventListener('message', workerListener); resolve(lighthouseMessage.result); } }; worker.addEventListener('message', workerListener); }); worker.postMessage(JSON.stringify({id: messageId, method, params: {...params, id: messageId}})); return messageResult; } }