@percy/core
Version:
The core component of Percy's CLI and SDKs that handles creating builds, discovering snapshot assets, uploading snapshots, and finalizing builds. Uses `@percy/client` for API communication, a Chromium browser for asset discovery, and starts a local API se
103 lines (98 loc) • 3.65 kB
JavaScript
import EventEmitter from 'events';
import logger from '@percy/logger';
export class Session extends EventEmitter {
#callbacks = new Map();
log = logger('core:session');
children = new Map();
constructor(browser, {
params,
sessionId: parentId
}) {
var _this$parent;
super();
this.browser = browser;
this.sessionId = params.sessionId;
this.targetId = params.targetInfo.targetId;
this.type = params.targetInfo.type;
this.isDocument = this.type === 'page' || this.type === 'iframe';
this.parent = browser.sessions.get(parentId);
(_this$parent = this.parent) === null || _this$parent === void 0 || _this$parent.children.set(this.sessionId, this);
this.on('Inspector.targetCrashed', this._handleTargetCrashed);
}
async close() {
var _this$browser;
// Check for the new closeBrowser option
if (((_this$browser = this.browser) === null || _this$browser === void 0 || (_this$browser = _this$browser.percy.config.discovery) === null || _this$browser === void 0 || (_this$browser = _this$browser.launchOptions) === null || _this$browser === void 0 ? void 0 : _this$browser.closeBrowser) === false) {
this.log.debug('Skipping session close due to closeBrowser:false option');
return true;
}
if (!this.browser || this.closing) return;
this.closing = true;
await this.browser.send('Target.closeTarget', {
targetId: this.targetId
}).catch(this._handleClosedError);
}
async send(method, params) {
/* istanbul ignore next: race condition paranoia */
if (this.closedReason) {
throw new Error(`Protocol error (${method}): ${this.closedReason}`);
}
// send a raw message to the browser so we can provide a sessionId
let id = await this.browser.send({
sessionId: this.sessionId,
method,
params
});
// will resolve or reject when a matching response is received
return new Promise((resolve, reject) => {
this.#callbacks.set(id, {
error: new Error(),
resolve,
reject,
method
});
});
}
_handleMessage(data) {
if (data.id && this.#callbacks.has(data.id)) {
// resolve or reject a pending promise created with #send()
let callback = this.#callbacks.get(data.id);
this.#callbacks.delete(data.id);
/* istanbul ignore next: races with browser._handleMessage() */
if (data.error) {
callback.reject(Object.assign(callback.error, {
message: `Protocol error (${callback.method}): ${data.error.message}` + ('data' in data.error ? `: ${data.error.data}` : '')
}));
} else {
callback.resolve(data.result);
}
} else {
// emit the message as an event
this.emit(data.method, data.params);
}
}
_handleClose() {
var _this$parent2;
this.closedReason || (this.closedReason = 'Session closed.');
// reject any pending callbacks
for (let callback of this.#callbacks.values()) {
callback.reject(Object.assign(callback.error, {
message: `Protocol error (${callback.method}): ${this.closedReason}`
}));
}
this.#callbacks.clear();
(_this$parent2 = this.parent) === null || _this$parent2 === void 0 || _this$parent2.children.delete(this.sessionId);
this.browser = null;
}
_handleTargetCrashed = () => {
this.closedReason = 'Session crashed!';
this.close();
};
/* istanbul ignore next: encountered during closing races */
_handleClosedError = error => {
if (!(error.message ?? error).endsWith(this.closedReason)) {
this.log.debug(error, this.meta);
}
};
}
export default Session;