Skip to content

Commit

Permalink
core(tsc): update debugger protocol type checking (#5836)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendankenny authored and paulirish committed Sep 1, 2018
1 parent 97dce2b commit e07a94b
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 2,377 deletions.
65 changes: 28 additions & 37 deletions lighthouse-core/gather/connections/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ const EventEmitter = require('events').EventEmitter;
const log = require('lighthouse-logger');
const LHError = require('../../lib/errors');

// TODO(bckenny): CommandCallback properties should be tied by command type after
// https://github.com/Microsoft/TypeScript/pull/22348. See driver.js TODO.
/**
* @typedef {LH.StrictEventEmitter<{'protocolevent': LH.Protocol.RawEventMessage}>} CrdpEventMessageEmitter
* @typedef {{'protocolevent': [LH.Protocol.RawEventMessage]}} ProtocolEventRecord
* @typedef {LH.Protocol.StrictEventEmitter<ProtocolEventRecord>} CrdpEventMessageEmitter
* @typedef {LH.CrdpCommands[keyof LH.CrdpCommands]} CommandInfo
* @typedef {{resolve: function(Promise<CommandInfo['returnType']>): void, method: keyof LH.CrdpCommands, options: {silent?: boolean}}} CommandCallback
* @typedef {{resolve: function(Promise<CommandInfo['returnType']>): void, method: keyof LH.CrdpCommands}} CommandCallback
*/

class Connection {
Expand All @@ -21,8 +24,7 @@ class Connection {
/** @type {Map<number, CommandCallback>} */
this._callbacks = new Map();

/** @type {?CrdpEventMessageEmitter} */
this._eventEmitter = new EventEmitter();
this._eventEmitter = /** @type {?CrdpEventMessageEmitter} */ (new EventEmitter());
}

/**
Expand All @@ -46,6 +48,27 @@ class Connection {
return Promise.reject(new Error('Not implemented'));
}

/**
* Call protocol methods
* @template {keyof LH.CrdpCommands} C
* @param {C} method
* @param {LH.CrdpCommands[C]['paramsType']} paramArgs,
* @return {Promise<LH.CrdpCommands[C]['returnType']>}
*/
sendCommand(method, ...paramArgs) {
// Reify params since we need it as a property so can't just spread again.
const params = paramArgs.length ? paramArgs[0] : undefined;

log.formatProtocol('method => browser', {method, params}, 'verbose');
const id = ++this._lastCommandId;
const message = JSON.stringify({id, method, params});
this.sendRawMessage(message);

return new Promise(resolve => {
this._callbacks.set(id, {method, resolve});
});
}

/**
* Bind listeners for connection events.
* @param {'protocolevent'} eventName
Expand Down Expand Up @@ -94,12 +117,9 @@ class Connection {
if (callback) {
this._callbacks.delete(object.id);

// @ts-ignore since can't convince compiler that callback.resolve's return
// type and object.result are matching since only linked by object.id.
return callback.resolve(Promise.resolve().then(_ => {
if (object.error) {
const logLevel = callback.options.silent ? 'verbose' : 'error';
log.formatProtocol('method <= browser ERR', {method: callback.method}, logLevel);
log.formatProtocol('method <= browser ERR', {method: callback.method}, 'error');
throw LHError.fromProtocolMessage(callback.method, object.error);
}

Expand Down Expand Up @@ -138,33 +158,4 @@ class Connection {
}
}

// Declared outside class body because function expressions can be typed via coercive @type
/**
* Looser-typed internal implementation of `Connection.sendCommand` which is
* strictly typed externally on exposed Connection interface. See
* `Driver.sendCommand` for explanation.
* @this {Connection}
* @param {keyof LH.CrdpCommands} method
* @param {CommandInfo['paramsType']=} params,
* @param {{silent?: boolean}=} cmdOpts
* @return {Promise<CommandInfo['returnType']>}
*/
function _sendCommand(method, params, cmdOpts = {}) {
/* eslint-disable no-invalid-this */
log.formatProtocol('method => browser', {method, params}, 'verbose');
const id = ++this._lastCommandId;
const message = JSON.stringify({id, method, params});
this.sendRawMessage(message);
return new Promise(resolve => {
this._callbacks.set(id, {resolve, method, options: cmdOpts});
});
/* eslint-enable no-invalid-this */
}

/**
* Call protocol methods.
* @type {LH.Protocol.SendCommand}
*/
Connection.prototype.sendCommand = _sendCommand;

module.exports = Connection;
88 changes: 39 additions & 49 deletions lighthouse-core/gather/connections/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,45 @@ class ExtensionConnection extends Connection {
}).then(_ => this._detachCleanup());
}

/**
* Call protocol methods.
* @template {keyof LH.CrdpCommands} C
* @param {C} method
* @param {LH.CrdpCommands[C]['paramsType']} paramArgs,
* @return {Promise<LH.CrdpCommands[C]['returnType']>}
*/
sendCommand(method, ...paramArgs) {
// Reify params since we need it as a property so can't just spread again.
const params = paramArgs.length ? paramArgs[0] : undefined;

return new Promise((resolve, reject) => {
log.formatProtocol('method => browser', {method, params}, 'verbose');
if (!this._tabId) {
log.error('ExtensionConnection', 'No tabId set for sendCommand');
return reject(new Error('No tabId set for sendCommand'));
}

chrome.debugger.sendCommand({tabId: this._tabId}, method, params || {}, result => {
if (chrome.runtime.lastError) {
// The error from the extension has a `message` property that is the
// stringified version of the actual protocol error object.
const message = chrome.runtime.lastError.message || '';
let errorMessage;
try {
errorMessage = JSON.parse(message).message;
} catch (e) {}
errorMessage = errorMessage || message || 'Unknown debugger protocol error.';

log.formatProtocol('method <= browser ERR', {method}, 'error');
return reject(new Error(`Protocol error (${method}): ${errorMessage}`));
}

log.formatProtocol('method <= browser OK', {method, params: result}, 'verbose');
resolve(result);
});
});
}

/**
* @return {Promise<chrome.tabs.Tab>}
* @private
Expand Down Expand Up @@ -172,53 +211,4 @@ class ExtensionConnection extends Connection {
}
}

/**
* @typedef {LH.CrdpCommands[keyof LH.CrdpCommands]} CommandInfo
*/
// Declared outside class body because function expressions can be typed via coercive @type
/**
* Looser-typed internal implementation of `ExtensionConnection.sendCommand`
* which is strictly typed externally on exposed ExtensionConnection interface.
* See `Driver.sendCommand` for explanation.
* @this {ExtensionConnection}
* @param {keyof LH.CrdpCommands} method
* @param {CommandInfo['paramsType']=} params,
* @return {Promise<CommandInfo['returnType']>}
*/
function _sendCommand(method, params) {
return new Promise((resolve, reject) => {
log.formatProtocol('method => browser', {method, params}, 'verbose');
if (!this._tabId) { // eslint-disable-line no-invalid-this
log.error('ExtensionConnection', 'No tabId set for sendCommand');
return reject(new Error('No tabId set for sendCommand'));
}

// eslint-disable-next-line no-invalid-this
chrome.debugger.sendCommand({tabId: this._tabId}, method, params || {}, result => {
if (chrome.runtime.lastError) {
// The error from the extension has a `message` property that is the
// stringified version of the actual protocol error object.
const message = chrome.runtime.lastError.message || '';
let errorMessage;
try {
errorMessage = JSON.parse(message).message;
} catch (e) {}
errorMessage = errorMessage || message || 'Unknown debugger protocol error.';

log.formatProtocol('method <= browser ERR', {method}, 'error');
return reject(new Error(`Protocol error (${method}): ${errorMessage}`));
}

log.formatProtocol('method <= browser OK', {method, params: result}, 'verbose');
resolve(result);
});
});
}

/**
* Call protocol methods.
* @type {LH.Protocol.SendCommand}
*/
ExtensionConnection.prototype.sendCommand = _sendCommand;

module.exports = ExtensionConnection;
Loading

0 comments on commit e07a94b

Please sign in to comment.