blob: 5a8b03e9bb197f5f1596ab9d9b44ad7ad5ceecc6 [file] [log] [blame]
// Copyright 2017 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.
/**
* @template T
*/
export default class SourceMapManager extends Common.Object {
/**
* @param {!SDK.Target} target
*/
constructor(target) {
super();
this._target = target;
this._isEnabled = true;
/** @type {!Map<!T, string>} */
this._relativeSourceURL = new Map();
/** @type {!Map<!T, string>} */
this._relativeSourceMapURL = new Map();
/** @type {!Map<!T, string>} */
this._resolvedSourceMapId = new Map();
/** @type {!Map<string, !SDK.SourceMap>} */
this._sourceMapById = new Map();
/** @type {!Platform.Multimap<string, !T>} */
this._sourceMapIdToLoadingClients = new Platform.Multimap();
/** @type {!Platform.Multimap<string, !T>} */
this._sourceMapIdToClients = new Platform.Multimap();
SDK.targetManager.addEventListener(SDK.TargetManager.Events.InspectedURLChanged, this._inspectedURLChanged, this);
}
/**
* @param {boolean} isEnabled
*/
setEnabled(isEnabled) {
if (isEnabled === this._isEnabled) {
return;
}
this._isEnabled = isEnabled;
// We need this copy, because `this._resolvedSourceMapId` is getting modified
// in the loop body and trying to iterate over it at the same time leads to
// an infinite loop.
const clients = [...this._resolvedSourceMapId.keys()];
for (const client of clients) {
const relativeSourceURL = this._relativeSourceURL.get(client);
const relativeSourceMapURL = this._relativeSourceMapURL.get(client);
this.detachSourceMap(client);
this.attachSourceMap(client, relativeSourceURL, relativeSourceMapURL);
}
}
/**
* @param {!Common.Event} event
*/
_inspectedURLChanged(event) {
if (event.data !== this._target) {
return;
}
// We need this copy, because `this._resolvedSourceMapId` is getting modified
// in the loop body and trying to iterate over it at the same time leads to
// an infinite loop.
const prevSourceMapIds = new Map(this._resolvedSourceMapId);
for (const [client, prevSourceMapId] of prevSourceMapIds) {
const relativeSourceURL = this._relativeSourceURL.get(client);
const relativeSourceMapURL = this._relativeSourceMapURL.get(client);
const {sourceMapId} = this._resolveRelativeURLs(relativeSourceURL, relativeSourceMapURL);
if (prevSourceMapId !== sourceMapId) {
this.detachSourceMap(client);
this.attachSourceMap(client, relativeSourceURL, relativeSourceMapURL);
}
}
}
/**
* @param {!T} client
* @return {?SDK.SourceMap}
*/
sourceMapForClient(client) {
const sourceMapId = this._resolvedSourceMapId.get(client);
if (!sourceMapId) {
return null;
}
return this._sourceMapById.get(sourceMapId) || null;
}
/**
* @param {!SDK.SourceMap} sourceMap
* @return {!Array<!T>}
*/
clientsForSourceMap(sourceMap) {
const sourceMapId = this._getSourceMapId(sourceMap.compiledURL(), sourceMap.url());
if (this._sourceMapIdToClients.has(sourceMapId)) {
return this._sourceMapIdToClients.get(sourceMapId).valuesArray();
}
return this._sourceMapIdToLoadingClients.get(sourceMapId).valuesArray();
}
/**
* @param {string} sourceURL
* @param {string} sourceMapURL
*/
_getSourceMapId(sourceURL, sourceMapURL) {
return `${sourceURL}:${sourceMapURL}`;
}
/**
* @param {string} sourceURL
* @param {string} sourceMapURL
* @return {?{sourceURL: string, sourceMapURL: string, sourceMapId: string}}
*/
_resolveRelativeURLs(sourceURL, sourceMapURL) {
// |sourceURL| can be a random string, but is generally an absolute path.
// Complete it to inspected page url for relative links.
const resolvedSourceURL = Common.ParsedURL.completeURL(this._target.inspectedURL(), sourceURL);
if (!resolvedSourceURL) {
return null;
}
const resolvedSourceMapURL = Common.ParsedURL.completeURL(resolvedSourceURL, sourceMapURL);
if (!resolvedSourceMapURL) {
return null;
}
return {
sourceURL: resolvedSourceURL,
sourceMapURL: resolvedSourceMapURL,
sourceMapId: this._getSourceMapId(resolvedSourceURL, resolvedSourceMapURL)
};
}
/**
* @param {!T} client
* @param {string} relativeSourceURL
* @param {?string} relativeSourceMapURL
*/
attachSourceMap(client, relativeSourceURL, relativeSourceMapURL) {
if (!relativeSourceMapURL) {
return;
}
console.assert(!this._resolvedSourceMapId.has(client), 'SourceMap is already attached to client');
const resolvedURLs = this._resolveRelativeURLs(relativeSourceURL, relativeSourceMapURL);
if (!resolvedURLs) {
return;
}
this._relativeSourceURL.set(client, relativeSourceURL);
this._relativeSourceMapURL.set(client, relativeSourceMapURL);
const {sourceURL, sourceMapURL, sourceMapId} = resolvedURLs;
this._resolvedSourceMapId.set(client, sourceMapId);
if (!this._isEnabled) {
return;
}
this.dispatchEventToListeners(Events.SourceMapWillAttach, client);
if (this._sourceMapById.has(sourceMapId)) {
attach.call(this, sourceMapId, client);
return;
}
if (!this._sourceMapIdToLoadingClients.has(sourceMapId)) {
const sourceMapPromise = sourceMapURL === SDK.WasmSourceMap.FAKE_URL ?
SDK.WasmSourceMap.load(client, sourceURL) :
SDK.TextSourceMap.load(sourceMapURL, sourceURL);
sourceMapPromise
.catch(error => {
Common.console.warn(ls`DevTools failed to load SourceMap: ${error.message}`);
})
.then(onSourceMap.bind(this, sourceMapId));
}
this._sourceMapIdToLoadingClients.set(sourceMapId, client);
/**
* @param {string} sourceMapId
* @param {?SDK.SourceMap} sourceMap
* @this {SourceMapManager}
*/
function onSourceMap(sourceMapId, sourceMap) {
this._sourceMapLoadedForTest();
const clients = this._sourceMapIdToLoadingClients.get(sourceMapId);
this._sourceMapIdToLoadingClients.deleteAll(sourceMapId);
if (!clients.size) {
return;
}
if (!sourceMap) {
for (const client of clients) {
this.dispatchEventToListeners(Events.SourceMapFailedToAttach, client);
}
return;
}
this._sourceMapById.set(sourceMapId, sourceMap);
for (const client of clients) {
attach.call(this, sourceMapId, client);
}
}
/**
* @param {string} sourceMapId
* @param {!T} client
* @this {SourceMapManager}
*/
function attach(sourceMapId, client) {
this._sourceMapIdToClients.set(sourceMapId, client);
const sourceMap = this._sourceMapById.get(sourceMapId);
this.dispatchEventToListeners(Events.SourceMapAttached, {client: client, sourceMap: sourceMap});
}
}
/**
* @param {!T} client
*/
detachSourceMap(client) {
const sourceMapId = this._resolvedSourceMapId.get(client);
this._relativeSourceURL.delete(client);
this._relativeSourceMapURL.delete(client);
this._resolvedSourceMapId.delete(client);
if (!sourceMapId) {
return;
}
if (!this._sourceMapIdToClients.hasValue(sourceMapId, client)) {
if (this._sourceMapIdToLoadingClients.delete(sourceMapId, client)) {
this.dispatchEventToListeners(Events.SourceMapFailedToAttach, client);
}
return;
}
this._sourceMapIdToClients.delete(sourceMapId, client);
const sourceMap = this._sourceMapById.get(sourceMapId);
this.dispatchEventToListeners(Events.SourceMapDetached, {client: client, sourceMap: sourceMap});
if (!this._sourceMapIdToClients.has(sourceMapId)) {
sourceMap.dispose();
this._sourceMapById.delete(sourceMapId);
}
}
_sourceMapLoadedForTest() {
}
dispose() {
for (const sourceMap of this._sourceMapById.values()) {
sourceMap.dispose();
}
SDK.targetManager.removeEventListener(
SDK.TargetManager.Events.InspectedURLChanged, this._inspectedURLChanged, this);
}
}
export const Events = {
SourceMapWillAttach: Symbol('SourceMapWillAttach'),
SourceMapFailedToAttach: Symbol('SourceMapFailedToAttach'),
SourceMapAttached: Symbol('SourceMapAttached'),
SourceMapDetached: Symbol('SourceMapDetached'),
SourceMapChanged: Symbol('SourceMapChanged')
};
/* Legacy exported object */
self.SDK = self.SDK || {};
/* Legacy exported object */
SDK = SDK || {};
/** @constructor */
SDK.SourceMapManager = SourceMapManager;
SDK.SourceMapManager.Events = Events;