/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Actor } = require("resource://devtools/shared/protocol.js"); const { serviceWorkerRegistrationSpec, } = require("resource://devtools/shared/specs/worker/service-worker-registration.js"); const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const { PushSubscriptionActor, } = require("resource://devtools/server/actors/worker/push-subscription.js"); const { ServiceWorkerActor, } = require("resource://devtools/server/actors/worker/service-worker.js"); XPCOMUtils.defineLazyServiceGetter( this, "swm", "@mozilla.org/serviceworkers/manager;1", "nsIServiceWorkerManager" ); XPCOMUtils.defineLazyServiceGetter( this, "PushService", "@mozilla.org/push/Service;1", "nsIPushService" ); class ServiceWorkerRegistrationActor extends Actor { /** * Create the ServiceWorkerRegistrationActor * @param DevToolsServerConnection conn * The server connection. * @param ServiceWorkerRegistrationInfo registration * The registration's information. */ constructor(conn, registration) { super(conn, serviceWorkerRegistrationSpec); this._registration = registration; this._pushSubscriptionActor = null; // A flag to know if preventShutdown has been called and we should // try to allow the shutdown of the SW when the actor is destroyed this._preventedShutdown = false; this._registration.addListener(this); this._createServiceWorkerActors(); Services.obs.addObserver(this, PushService.subscriptionModifiedTopic); } onChange() { this._destroyServiceWorkerActors(); this._createServiceWorkerActors(); this.emit("registration-changed"); } form() { const registration = this._registration; const evaluatingWorker = this._evaluatingWorker.form(); const installingWorker = this._installingWorker.form(); const waitingWorker = this._waitingWorker.form(); const activeWorker = this._activeWorker.form(); const newestWorker = activeWorker || waitingWorker || installingWorker || evaluatingWorker; return { actor: this.actorID, scope: registration.scope, url: registration.scriptSpec, evaluatingWorker, installingWorker, waitingWorker, activeWorker, fetch: newestWorker?.fetch, // Check if we have an active worker active: !!activeWorker, lastUpdateTime: registration.lastUpdateTime, traits: {}, }; } destroy() { super.destroy(); // Ensure resuming the service worker in case the connection drops if (this._registration.activeWorker && this._preventedShutdown) { this.allowShutdown(); } Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic); this._registration.removeListener(this); this._registration = null; if (this._pushSubscriptionActor) { this._pushSubscriptionActor.destroy(); } this._pushSubscriptionActor = null; this._destroyServiceWorkerActors(); this._evaluatingWorker = null; this._installingWorker = null; this._waitingWorker = null; this._activeWorker = null; } /** * Standard observer interface to listen to push messages and changes. */ observe(subject, topic, data) { const scope = this._registration.scope; if (data !== scope) { // This event doesn't concern us, pretend nothing happened. return; } switch (topic) { case PushService.subscriptionModifiedTopic: if (this._pushSubscriptionActor) { this._pushSubscriptionActor.destroy(); this._pushSubscriptionActor = null; } this.emit("push-subscription-modified"); break; } } start() { const { activeWorker } = this._registration; // TODO: don't return "started" if there's no active worker. if (activeWorker) { // This starts up the Service Worker if it's not already running. // Note that the Service Workers exist in content processes but are // managed from the parent process. This is why we call `attachDebugger` // here (in the parent process) instead of in a process script. activeWorker.attachDebugger(); activeWorker.detachDebugger(); } return { type: "started" }; } unregister() { const { principal, scope } = this._registration; const unregisterCallback = { unregisterSucceeded() {}, unregisterFailed() { console.error("Failed to unregister the service worker for " + scope); }, QueryInterface: ChromeUtils.generateQI([ "nsIServiceWorkerUnregisterCallback", ]), }; swm.propagateUnregister(principal, unregisterCallback, scope); return { type: "unregistered" }; } push() { const { principal, scope } = this._registration; const originAttributes = ChromeUtils.originAttributesToSuffix( principal.originAttributes ); swm.sendPushEvent(originAttributes, scope); } /** * Prevent the current active worker to shutdown after the idle timeout. */ preventShutdown() { if (!this._registration.activeWorker) { throw new Error( "ServiceWorkerRegistrationActor.preventShutdown could not find " + "activeWorker in parent-intercept mode" ); } // attachDebugger has to be called from the parent process in parent-intercept mode. this._registration.activeWorker.attachDebugger(); this._preventedShutdown = true; } /** * Allow the current active worker to shut down again. */ allowShutdown() { if (!this._registration.activeWorker) { throw new Error( "ServiceWorkerRegistrationActor.allowShutdown could not find " + "activeWorker in parent-intercept mode" ); } this._registration.activeWorker.detachDebugger(); this._preventedShutdown = false; } getPushSubscription() { const registration = this._registration; let pushSubscriptionActor = this._pushSubscriptionActor; if (pushSubscriptionActor) { return Promise.resolve(pushSubscriptionActor); } return new Promise((resolve, reject) => { PushService.getSubscription( registration.scope, registration.principal, (result, subscription) => { if (!subscription) { resolve(null); return; } pushSubscriptionActor = new PushSubscriptionActor( this.conn, subscription ); this._pushSubscriptionActor = pushSubscriptionActor; resolve(pushSubscriptionActor); } ); }); } _destroyServiceWorkerActors() { this._evaluatingWorker.destroy(); this._installingWorker.destroy(); this._waitingWorker.destroy(); this._activeWorker.destroy(); } _createServiceWorkerActors() { const { evaluatingWorker, installingWorker, waitingWorker, activeWorker } = this._registration; this._evaluatingWorker = new ServiceWorkerActor( this.conn, evaluatingWorker ); this._installingWorker = new ServiceWorkerActor( this.conn, installingWorker ); this._waitingWorker = new ServiceWorkerActor(this.conn, waitingWorker); this._activeWorker = new ServiceWorkerActor(this.conn, activeWorker); // Add the ServiceWorker actors as children of this ServiceWorkerRegistration actor, // assigning them valid actorIDs. this.manage(this._evaluatingWorker); this.manage(this._installingWorker); this.manage(this._waitingWorker); this.manage(this._activeWorker); } } exports.ServiceWorkerRegistrationActor = ServiceWorkerRegistrationActor;