service-worker-registration.js (8033B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { Actor } = require("resource://devtools/shared/protocol.js"); 8 const { 9 serviceWorkerRegistrationSpec, 10 } = require("resource://devtools/shared/specs/worker/service-worker-registration.js"); 11 12 const { XPCOMUtils } = ChromeUtils.importESModule( 13 "resource://gre/modules/XPCOMUtils.sys.mjs", 14 { global: "contextual" } 15 ); 16 const { 17 PushSubscriptionActor, 18 } = require("resource://devtools/server/actors/worker/push-subscription.js"); 19 const { 20 ServiceWorkerActor, 21 } = require("resource://devtools/server/actors/worker/service-worker.js"); 22 23 XPCOMUtils.defineLazyServiceGetter( 24 this, 25 "swm", 26 "@mozilla.org/serviceworkers/manager;1", 27 Ci.nsIServiceWorkerManager 28 ); 29 30 XPCOMUtils.defineLazyServiceGetter( 31 this, 32 "PushService", 33 "@mozilla.org/push/Service;1", 34 Ci.nsIPushService 35 ); 36 37 class ServiceWorkerRegistrationActor extends Actor { 38 /** 39 * Create the ServiceWorkerRegistrationActor 40 * 41 * @param DevToolsServerConnection conn 42 * The server connection. 43 * @param ServiceWorkerRegistrationInfo registration 44 * The registration's information. 45 */ 46 constructor(conn, registration) { 47 super(conn, serviceWorkerRegistrationSpec); 48 this._registration = registration; 49 this._pushSubscriptionActor = null; 50 51 // A flag to know if preventShutdown has been called and we should 52 // try to allow the shutdown of the SW when the actor is destroyed 53 this._preventedShutdown = false; 54 55 this._registration.addListener(this); 56 57 this._createServiceWorkerActors(); 58 59 Services.obs.addObserver(this, PushService.subscriptionModifiedTopic); 60 } 61 62 onChange() { 63 this._destroyServiceWorkerActors(); 64 this._createServiceWorkerActors(); 65 this.emit("registration-changed"); 66 } 67 68 form() { 69 const registration = this._registration; 70 const evaluatingWorker = this._evaluatingWorker.form(); 71 const installingWorker = this._installingWorker.form(); 72 const waitingWorker = this._waitingWorker.form(); 73 const activeWorker = this._activeWorker.form(); 74 75 const newestWorker = 76 activeWorker || waitingWorker || installingWorker || evaluatingWorker; 77 78 return { 79 actor: this.actorID, 80 scope: registration.scope, 81 url: registration.scriptSpec, 82 evaluatingWorker, 83 installingWorker, 84 waitingWorker, 85 activeWorker, 86 fetch: newestWorker?.fetch, 87 // Check if we have an active worker 88 active: !!activeWorker, 89 lastUpdateTime: registration.lastUpdateTime, 90 traits: {}, 91 }; 92 } 93 94 destroy() { 95 super.destroy(); 96 97 // Ensure resuming the service worker in case the connection drops 98 if (this._registration.activeWorker && this._preventedShutdown) { 99 this.allowShutdown(); 100 } 101 102 Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic); 103 this._registration.removeListener(this); 104 this._registration = null; 105 if (this._pushSubscriptionActor) { 106 this._pushSubscriptionActor.destroy(); 107 } 108 this._pushSubscriptionActor = null; 109 110 this._destroyServiceWorkerActors(); 111 112 this._evaluatingWorker = null; 113 this._installingWorker = null; 114 this._waitingWorker = null; 115 this._activeWorker = null; 116 } 117 118 /** 119 * Standard observer interface to listen to push messages and changes. 120 */ 121 observe(subject, topic, data) { 122 const scope = this._registration.scope; 123 if (data !== scope) { 124 // This event doesn't concern us, pretend nothing happened. 125 return; 126 } 127 switch (topic) { 128 case PushService.subscriptionModifiedTopic: 129 if (this._pushSubscriptionActor) { 130 this._pushSubscriptionActor.destroy(); 131 this._pushSubscriptionActor = null; 132 } 133 this.emit("push-subscription-modified"); 134 break; 135 } 136 } 137 138 start() { 139 const { activeWorker } = this._registration; 140 141 // TODO: don't return "started" if there's no active worker. 142 if (activeWorker) { 143 // This starts up the Service Worker if it's not already running. 144 // Note that the Service Workers exist in content processes but are 145 // managed from the parent process. This is why we call `attachDebugger` 146 // here (in the parent process) instead of in a process script. 147 activeWorker.attachDebugger(); 148 activeWorker.detachDebugger(); 149 } 150 151 return { type: "started" }; 152 } 153 154 unregister() { 155 const { principal, scope } = this._registration; 156 const unregisterCallback = { 157 unregisterSucceeded() {}, 158 unregisterFailed() { 159 console.error("Failed to unregister the service worker for " + scope); 160 }, 161 QueryInterface: ChromeUtils.generateQI([ 162 "nsIServiceWorkerUnregisterCallback", 163 ]), 164 }; 165 swm.propagateUnregister(principal, unregisterCallback, scope); 166 167 return { type: "unregistered" }; 168 } 169 170 push() { 171 const { principal, scope } = this._registration; 172 const originAttributes = ChromeUtils.originAttributesToSuffix( 173 principal.originAttributes 174 ); 175 swm.sendPushEvent(originAttributes, scope); 176 } 177 178 /** 179 * Prevent the current active worker to shutdown after the idle timeout. 180 */ 181 preventShutdown() { 182 if (!this._registration.activeWorker) { 183 throw new Error( 184 "ServiceWorkerRegistrationActor.preventShutdown could not find " + 185 "activeWorker in parent-intercept mode" 186 ); 187 } 188 189 // attachDebugger has to be called from the parent process in parent-intercept mode. 190 this._registration.activeWorker.attachDebugger(); 191 this._preventedShutdown = true; 192 } 193 194 /** 195 * Allow the current active worker to shut down again. 196 */ 197 allowShutdown() { 198 if (!this._registration.activeWorker) { 199 throw new Error( 200 "ServiceWorkerRegistrationActor.allowShutdown could not find " + 201 "activeWorker in parent-intercept mode" 202 ); 203 } 204 205 this._registration.activeWorker.detachDebugger(); 206 this._preventedShutdown = false; 207 } 208 209 getPushSubscription() { 210 const registration = this._registration; 211 let pushSubscriptionActor = this._pushSubscriptionActor; 212 if (pushSubscriptionActor) { 213 return Promise.resolve(pushSubscriptionActor); 214 } 215 return new Promise(resolve => { 216 PushService.getSubscription( 217 registration.scope, 218 registration.principal, 219 (result, subscription) => { 220 if (!subscription) { 221 resolve(null); 222 return; 223 } 224 pushSubscriptionActor = new PushSubscriptionActor( 225 this.conn, 226 subscription 227 ); 228 this._pushSubscriptionActor = pushSubscriptionActor; 229 resolve(pushSubscriptionActor); 230 } 231 ); 232 }); 233 } 234 235 _destroyServiceWorkerActors() { 236 this._evaluatingWorker.destroy(); 237 this._installingWorker.destroy(); 238 this._waitingWorker.destroy(); 239 this._activeWorker.destroy(); 240 } 241 242 _createServiceWorkerActors() { 243 const { evaluatingWorker, installingWorker, waitingWorker, activeWorker } = 244 this._registration; 245 const origin = this._registration.principal.origin; 246 247 this._evaluatingWorker = new ServiceWorkerActor( 248 this.conn, 249 evaluatingWorker, 250 origin 251 ); 252 this._installingWorker = new ServiceWorkerActor( 253 this.conn, 254 installingWorker, 255 origin 256 ); 257 this._waitingWorker = new ServiceWorkerActor( 258 this.conn, 259 waitingWorker, 260 origin 261 ); 262 this._activeWorker = new ServiceWorkerActor( 263 this.conn, 264 activeWorker, 265 origin 266 ); 267 268 // Add the ServiceWorker actors as children of this ServiceWorkerRegistration actor, 269 // assigning them valid actorIDs. 270 this.manage(this._evaluatingWorker); 271 this.manage(this._installingWorker); 272 this.manage(this._waitingWorker); 273 this.manage(this._activeWorker); 274 } 275 } 276 277 exports.ServiceWorkerRegistrationActor = ServiceWorkerRegistrationActor;