blob: d24a01601ed5be9110625f9fccb788bf1af467f2 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
export default class ServiceWorkerManager extends SDK.SDKModel {
/**
* @param {!SDK.Target} target
*/
constructor(target) {
super(target);
target.registerServiceWorkerDispatcher(new ServiceWorkerDispatcher(this));
this._lastAnonymousTargetId = 0;
this._agent = target.serviceWorkerAgent();
/** @type {!Map.<string, !ServiceWorkerRegistration>} */
this._registrations = new Map();
this.enable();
this._forceUpdateSetting = Common.settings.createSetting('serviceWorkerUpdateOnReload', false);
if (this._forceUpdateSetting.get()) {
this._forceUpdateSettingChanged();
}
this._forceUpdateSetting.addChangeListener(this._forceUpdateSettingChanged, this);
new ServiceWorkerContextNamer(target, this);
}
enable() {
if (this._enabled) {
return;
}
this._enabled = true;
this._agent.enable();
}
disable() {
if (!this._enabled) {
return;
}
this._enabled = false;
this._registrations.clear();
this._agent.disable();
}
/**
* @return {!Map.<string, !ServiceWorkerRegistration>}
*/
registrations() {
return this._registrations;
}
/**
* @param {!Array<string>} urls
* @return {boolean}
*/
hasRegistrationForURLs(urls) {
for (const registration of this._registrations.values()) {
if (urls.filter(url => url && url.startsWith(registration.scopeURL)).length === urls.length) {
return true;
}
}
return false;
}
/**
* @param {string} versionId
* @return {?ServiceWorkerVersion}
*/
findVersion(versionId) {
for (const registration of this.registrations().values()) {
const version = registration.versions.get(versionId);
if (version) {
return version;
}
}
return null;
}
/**
* @param {string} registrationId
*/
deleteRegistration(registrationId) {
const registration = this._registrations.get(registrationId);
if (!registration) {
return;
}
if (registration._isRedundant()) {
this._registrations.delete(registrationId);
this.dispatchEventToListeners(Events.RegistrationDeleted, registration);
return;
}
registration._deleting = true;
for (const version of registration.versions.values()) {
this.stopWorker(version.id);
}
this._unregister(registration.scopeURL);
}
/**
* @param {string} registrationId
*/
updateRegistration(registrationId) {
const registration = this._registrations.get(registrationId);
if (!registration) {
return;
}
this._agent.updateRegistration(registration.scopeURL);
}
/**
* @param {string} registrationId
* @param {string} data
*/
deliverPushMessage(registrationId, data) {
const registration = this._registrations.get(registrationId);
if (!registration) {
return;
}
const origin = Common.ParsedURL.extractOrigin(registration.scopeURL);
this._agent.deliverPushMessage(origin, registrationId, data);
}
/**
* @param {string} registrationId
* @param {string} tag
* @param {boolean} lastChance
*/
dispatchSyncEvent(registrationId, tag, lastChance) {
const registration = this._registrations.get(registrationId);
if (!registration) {
return;
}
const origin = Common.ParsedURL.extractOrigin(registration.scopeURL);
this._agent.dispatchSyncEvent(origin, registrationId, tag, lastChance);
}
/**
* @param {string} registrationId
* @param {string} tag
*/
dispatchPeriodicSyncEvent(registrationId, tag) {
const registration = this._registrations.get(registrationId);
if (!registration) {
return;
}
const origin = Common.ParsedURL.extractOrigin(registration.scopeURL);
this._agent.dispatchPeriodicSyncEvent(origin, registrationId, tag);
}
/**
* @param {string} scope
*/
_unregister(scope) {
this._agent.unregister(scope);
}
/**
* @param {string} scope
*/
startWorker(scope) {
this._agent.startWorker(scope);
}
/**
* @param {string} scope
*/
skipWaiting(scope) {
this._agent.skipWaiting(scope);
}
/**
* @param {string} versionId
*/
stopWorker(versionId) {
this._agent.stopWorker(versionId);
}
/**
* @param {string} versionId
*/
inspectWorker(versionId) {
this._agent.inspectWorker(versionId);
}
/**
* @param {!Array.<!Protocol.ServiceWorker.ServiceWorkerRegistration>} registrations
*/
_workerRegistrationUpdated(registrations) {
for (const payload of registrations) {
let registration = this._registrations.get(payload.registrationId);
if (!registration) {
registration = new ServiceWorkerRegistration(payload);
this._registrations.set(payload.registrationId, registration);
this.dispatchEventToListeners(Events.RegistrationUpdated, registration);
continue;
}
registration._update(payload);
if (registration._shouldBeRemoved()) {
this._registrations.delete(registration.id);
this.dispatchEventToListeners(Events.RegistrationDeleted, registration);
} else {
this.dispatchEventToListeners(Events.RegistrationUpdated, registration);
}
}
}
/**
* @param {!Array.<!Protocol.ServiceWorker.ServiceWorkerVersion>} versions
*/
_workerVersionUpdated(versions) {
/** @type {!Set.<!ServiceWorkerRegistration>} */
const registrations = new Set();
for (const payload of versions) {
const registration = this._registrations.get(payload.registrationId);
if (!registration) {
continue;
}
registration._updateVersion(payload);
registrations.add(registration);
}
for (const registration of registrations) {
if (registration._shouldBeRemoved()) {
this._registrations.delete(registration.id);
this.dispatchEventToListeners(Events.RegistrationDeleted, registration);
} else {
this.dispatchEventToListeners(Events.RegistrationUpdated, registration);
}
}
}
/**
* @param {!Protocol.ServiceWorker.ServiceWorkerErrorMessage} payload
*/
_workerErrorReported(payload) {
const registration = this._registrations.get(payload.registrationId);
if (!registration) {
return;
}
registration.errors.push(payload);
this.dispatchEventToListeners(Events.RegistrationErrorAdded, {registration: registration, error: payload});
}
/**
* @return {!Common.Setting}
*/
forceUpdateOnReloadSetting() {
return this._forceUpdateSetting;
}
_forceUpdateSettingChanged() {
this._agent.setForceUpdateOnPageLoad(this._forceUpdateSetting.get());
}
}
/** @enum {symbol} */
export const Events = {
RegistrationUpdated: Symbol('RegistrationUpdated'),
RegistrationErrorAdded: Symbol('RegistrationErrorAdded'),
RegistrationDeleted: Symbol('RegistrationDeleted')
};
/**
* @implements {Protocol.ServiceWorkerDispatcher}
* @unrestricted
*/
class ServiceWorkerDispatcher {
/**
* @param {!ServiceWorkerManager} manager
*/
constructor(manager) {
this._manager = manager;
}
/**
* @override
* @param {!Array.<!Protocol.ServiceWorker.ServiceWorkerRegistration>} registrations
*/
workerRegistrationUpdated(registrations) {
this._manager._workerRegistrationUpdated(registrations);
}
/**
* @override
* @param {!Array.<!Protocol.ServiceWorker.ServiceWorkerVersion>} versions
*/
workerVersionUpdated(versions) {
this._manager._workerVersionUpdated(versions);
}
/**
* @override
* @param {!Protocol.ServiceWorker.ServiceWorkerErrorMessage} errorMessage
*/
workerErrorReported(errorMessage) {
this._manager._workerErrorReported(errorMessage);
}
}
/**
* @unrestricted
*/
export class ServiceWorkerVersion {
/**
* @param {!ServiceWorkerRegistration} registration
* @param {!Protocol.ServiceWorker.ServiceWorkerVersion} payload
*/
constructor(registration, payload) {
this.registration = registration;
this._update(payload);
}
/**
* @param {!Protocol.ServiceWorker.ServiceWorkerVersion} payload
*/
_update(payload) {
this.id = payload.versionId;
this.scriptURL = payload.scriptURL;
const parsedURL = new Common.ParsedURL(payload.scriptURL);
this.securityOrigin = parsedURL.securityOrigin();
this.runningStatus = payload.runningStatus;
this.status = payload.status;
this.scriptLastModified = payload.scriptLastModified;
this.scriptResponseTime = payload.scriptResponseTime;
this.controlledClients = [];
for (let i = 0; i < payload.controlledClients.length; ++i) {
this.controlledClients.push(payload.controlledClients[i]);
}
this.targetId = payload.targetId || null;
}
/**
* @return {boolean}
*/
isStartable() {
return !this.registration.isDeleted && this.isActivated() && this.isStopped();
}
/**
* @return {boolean}
*/
isStoppedAndRedundant() {
return this.runningStatus === Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Stopped &&
this.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Redundant;
}
/**
* @return {boolean}
*/
isStopped() {
return this.runningStatus === Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Stopped;
}
/**
* @return {boolean}
*/
isStarting() {
return this.runningStatus === Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Starting;
}
/**
* @return {boolean}
*/
isRunning() {
return this.runningStatus === Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Running;
}
/**
* @return {boolean}
*/
isStopping() {
return this.runningStatus === Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Stopping;
}
/**
* @return {boolean}
*/
isNew() {
return this.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.New;
}
/**
* @return {boolean}
*/
isInstalling() {
return this.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installing;
}
/**
* @return {boolean}
*/
isInstalled() {
return this.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installed;
}
/**
* @return {boolean}
*/
isActivating() {
return this.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activating;
}
/**
* @return {boolean}
*/
isActivated() {
return this.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activated;
}
/**
* @return {boolean}
*/
isRedundant() {
return this.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Redundant;
}
/**
* @return {string}
*/
mode() {
if (this.isNew() || this.isInstalling()) {
return ServiceWorkerVersion.Modes.Installing;
} else if (this.isInstalled()) {
return ServiceWorkerVersion.Modes.Waiting;
} else if (this.isActivating() || this.isActivated()) {
return ServiceWorkerVersion.Modes.Active;
}
return ServiceWorkerVersion.Modes.Redundant;
}
}
/**
* @type {!Object<string, string>}
*/
ServiceWorkerVersion.RunningStatus = {
[Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Running]: ls`running`,
[Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Starting]: ls`starting`,
[Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Stopped]: ls`stopped`,
[Protocol.ServiceWorker.ServiceWorkerVersionRunningStatus.Stopping]: ls`stopping`,
};
/**
* @type {!Object<string, string>}
*/
ServiceWorkerVersion.Status = {
[Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activated]: ls`activated`,
[Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activating]: ls`activating`,
[Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installed]: ls`installed`,
[Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installing]: ls`installing`,
[Protocol.ServiceWorker.ServiceWorkerVersionStatus.New]: ls`new`,
[Protocol.ServiceWorker.ServiceWorkerVersionStatus.Redundant]: ls`redundant`,
};
/**
* @enum {string}
*/
ServiceWorkerVersion.Modes = {
Installing: 'installing',
Waiting: 'waiting',
Active: 'active',
Redundant: 'redundant'
};
/**
* @unrestricted
*/
export class ServiceWorkerRegistration {
/**
* @param {!Protocol.ServiceWorker.ServiceWorkerRegistration} payload
*/
constructor(payload) {
this._update(payload);
/** @type {!Map.<string, !ServiceWorkerVersion>} */
this.versions = new Map();
this._deleting = false;
/** @type {!Array<!Protocol.ServiceWorker.ServiceWorkerErrorMessage>} */
this.errors = [];
}
/**
* @param {!Protocol.ServiceWorker.ServiceWorkerRegistration} payload
*/
_update(payload) {
this._fingerprint = Symbol('fingerprint');
this.id = payload.registrationId;
this.scopeURL = payload.scopeURL;
const parsedURL = new Common.ParsedURL(payload.scopeURL);
this.securityOrigin = parsedURL.securityOrigin();
this.isDeleted = payload.isDeleted;
this.forceUpdateOnPageLoad = payload.forceUpdateOnPageLoad;
}
/**
* @return {symbol}
*/
fingerprint() {
return this._fingerprint;
}
/**
* @return {!Map<string, !ServiceWorkerVersion>}
*/
versionsByMode() {
/** @type {!Map<string, !ServiceWorkerVersion>} */
const result = new Map();
for (const version of this.versions.values()) {
result.set(version.mode(), version);
}
return result;
}
/**
* @param {!Protocol.ServiceWorker.ServiceWorkerVersion} payload
* @return {!ServiceWorkerVersion}
*/
_updateVersion(payload) {
this._fingerprint = Symbol('fingerprint');
let version = this.versions.get(payload.versionId);
if (!version) {
version = new ServiceWorkerVersion(this, payload);
this.versions.set(payload.versionId, version);
return version;
}
version._update(payload);
return version;
}
/**
* @return {boolean}
*/
_isRedundant() {
for (const version of this.versions.values()) {
if (!version.isStoppedAndRedundant()) {
return false;
}
}
return true;
}
/**
* @return {boolean}
*/
_shouldBeRemoved() {
return this._isRedundant() && (!this.errors.length || this._deleting);
}
/**
* @return {boolean}
*/
canBeRemoved() {
return this.isDeleted || this._deleting;
}
clearErrors() {
this._fingerprint = Symbol('fingerprint');
this.errors = [];
}
}
/**
* @unrestricted
*/
class ServiceWorkerContextNamer {
/**
* @param {!SDK.Target} target
* @param {!ServiceWorkerManager} serviceWorkerManager
*/
constructor(target, serviceWorkerManager) {
this._target = target;
this._serviceWorkerManager = serviceWorkerManager;
/** @type {!Map<string, !ServiceWorkerVersion>} */
this._versionByTargetId = new Map();
serviceWorkerManager.addEventListener(Events.RegistrationUpdated, this._registrationsUpdated, this);
serviceWorkerManager.addEventListener(Events.RegistrationDeleted, this._registrationsUpdated, this);
SDK.targetManager.addModelListener(
SDK.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextCreated, this._executionContextCreated, this);
}
/**
* @param {!Common.Event} event
*/
_registrationsUpdated(event) {
this._versionByTargetId.clear();
const registrations = this._serviceWorkerManager.registrations().valuesArray();
for (const registration of registrations) {
const versions = registration.versions.valuesArray();
for (const version of versions) {
if (version.targetId) {
this._versionByTargetId.set(version.targetId, version);
}
}
}
this._updateAllContextLabels();
}
/**
* @param {!Common.Event} event
*/
_executionContextCreated(event) {
const executionContext = /** @type {!SDK.ExecutionContext} */ (event.data);
const serviceWorkerTargetId = this._serviceWorkerTargetId(executionContext.target());
if (!serviceWorkerTargetId) {
return;
}
this._updateContextLabel(executionContext, this._versionByTargetId.get(serviceWorkerTargetId) || null);
}
/**
* @param {!SDK.Target} target
* @return {?string}
*/
_serviceWorkerTargetId(target) {
if (target.parentTarget() !== this._target || target.type() !== SDK.Target.Type.ServiceWorker) {
return null;
}
return target.id();
}
_updateAllContextLabels() {
for (const target of SDK.targetManager.targets()) {
const serviceWorkerTargetId = this._serviceWorkerTargetId(target);
if (!serviceWorkerTargetId) {
continue;
}
const version = this._versionByTargetId.get(serviceWorkerTargetId) || null;
const runtimeModel = target.model(SDK.RuntimeModel);
const executionContexts = runtimeModel ? runtimeModel.executionContexts() : [];
for (const context of executionContexts) {
this._updateContextLabel(context, version);
}
}
}
/**
* @param {!SDK.ExecutionContext} context
* @param {?ServiceWorkerVersion} version
*/
_updateContextLabel(context, version) {
if (!version) {
context.setLabel('');
return;
}
const parsedUrl = Common.ParsedURL.fromString(context.origin);
const label = parsedUrl ? parsedUrl.lastPathComponentWithFragment() : context.name;
const localizedStatus = ServiceWorkerVersion.Status[version.status];
context.setLabel(ls`${label} #${version.id} (${localizedStatus})`);
}
}
/* Legacy exported object */
self.SDK = self.SDK || {};
/* Legacy exported object */
SDK = SDK || {};
/** @constructor */
SDK.ServiceWorkerManager = ServiceWorkerManager;
/** @enum {symbol} */
SDK.ServiceWorkerManager.Events = Events;
/** @constructor */
SDK.ServiceWorkerVersion = ServiceWorkerVersion;
/** @constructor */
SDK.ServiceWorkerRegistration = ServiceWorkerRegistration;
SDK.SDKModel.register(ServiceWorkerManager, SDK.Target.Capability.ServiceWorker, true);