UNPKG

@serwist/window

Version:

Simplifies communications with Serwist packages running in the service worker

305 lines (298 loc) 11.9 kB
import { Deferred, logger } from 'serwist/internal'; import { isCurrentPageOutOfScope } from './index.internal.js'; const messageSW = (sw, data)=>{ return new Promise((resolve)=>{ const messageChannel = new MessageChannel(); messageChannel.port1.onmessage = (event)=>{ resolve(event.data); }; sw.postMessage(data, [ messageChannel.port2 ]); }); }; class SerwistEvent { type; target; sw; originalEvent; isExternal; constructor(type, props){ this.type = type; Object.assign(this, props); } } class SerwistEventTarget { _eventListenerRegistry = new Map(); addEventListener(type, listener) { const foo = this._getEventListenersByType(type); foo.add(listener); } removeEventListener(type, listener) { this._getEventListenersByType(type).delete(listener); } dispatchEvent(event) { event.target = this; const listeners = this._getEventListenersByType(event.type); for (const listener of listeners){ listener(event); } } _getEventListenersByType(type) { if (!this._eventListenerRegistry.has(type)) { this._eventListenerRegistry.set(type, new Set()); } return this._eventListenerRegistry.get(type); } } function urlsMatch(url1, url2) { const { href } = location; return new URL(url1, href).href === new URL(url2, href).href; } const WAITING_TIMEOUT_DURATION = 200; const REGISTRATION_TIMEOUT_DURATION = 60000; const SKIP_WAITING_MESSAGE = { type: "SKIP_WAITING" }; class Serwist extends SerwistEventTarget { _scriptURL; _registerOptions = {}; _updateFoundCount = 0; _swDeferred = new Deferred(); _activeDeferred = new Deferred(); _controllingDeferred = new Deferred(); _registrationTime = 0; _isUpdate; _compatibleControllingSW; _registration; _sw; _ownSWs = new Set(); _externalSW; _waitingTimeout; constructor(scriptURL, registerOptions = {}){ super(); this._scriptURL = scriptURL; this._registerOptions = registerOptions; navigator.serviceWorker.addEventListener("message", this._onMessage); } async register({ immediate = false } = {}) { if (process.env.NODE_ENV !== "production") { if (this._registrationTime) { logger.error("Cannot re-register a Serwist instance after it has been registered. Create a new instance instead."); return; } } if (!immediate && document.readyState !== "complete") { await new Promise((res)=>window.addEventListener("load", res)); } this._isUpdate = Boolean(navigator.serviceWorker.controller); this._compatibleControllingSW = this._getControllingSWIfCompatible(); this._registration = await this._registerScript(); if (this._compatibleControllingSW) { this._sw = this._compatibleControllingSW; this._activeDeferred.resolve(this._compatibleControllingSW); this._controllingDeferred.resolve(this._compatibleControllingSW); this._compatibleControllingSW.addEventListener("statechange", this._onStateChange, { once: true }); } const waitingSW = this._registration.waiting; if (waitingSW && urlsMatch(waitingSW.scriptURL, this._scriptURL.toString())) { this._sw = waitingSW; void Promise.resolve().then(()=>{ this.dispatchEvent(new SerwistEvent("waiting", { sw: waitingSW, wasWaitingBeforeRegister: true })); if (process.env.NODE_ENV !== "production") { logger.warn("A service worker was already waiting to activate before this script was registered..."); } }); } if (this._sw) { this._swDeferred.resolve(this._sw); this._ownSWs.add(this._sw); } if (process.env.NODE_ENV !== "production") { logger.log("Successfully registered service worker.", this._scriptURL.toString()); if (navigator.serviceWorker.controller) { if (this._compatibleControllingSW) { logger.debug("A service worker with the same script URL is already controlling this page."); } else { logger.debug("A service worker with a different script URL is currently controlling the page. The browser is now fetching the new script now..."); } } if (isCurrentPageOutOfScope(this._registerOptions.scope || this._scriptURL.toString())) { logger.warn("The current page is not in scope for the registered service worker. Was this a mistake?"); } } this._registration.addEventListener("updatefound", this._onUpdateFound); navigator.serviceWorker.addEventListener("controllerchange", this._onControllerChange); return this._registration; } async update() { if (!this._registration) { if (process.env.NODE_ENV !== "production") { logger.error("Cannot update a Serwist instance without being registered. Register the Serwist instance first."); } return; } await this._registration.update(); } get active() { return this._activeDeferred.promise; } get controlling() { return this._controllingDeferred.promise; } getSW() { return this._sw !== undefined ? Promise.resolve(this._sw) : this._swDeferred.promise; } async messageSW(data) { const sw = await this.getSW(); return messageSW(sw, data); } messageSkipWaiting() { if (this._registration?.waiting) { void messageSW(this._registration.waiting, SKIP_WAITING_MESSAGE); } } _getControllingSWIfCompatible() { const controller = navigator.serviceWorker.controller; if (controller && urlsMatch(controller.scriptURL, this._scriptURL.toString())) { return controller; } return undefined; } async _registerScript() { try { const reg = await navigator.serviceWorker.register(this._scriptURL, this._registerOptions); this._registrationTime = performance.now(); return reg; } catch (error) { if (process.env.NODE_ENV !== "production") { logger.error(error); } throw error; } } _onUpdateFound = (originalEvent)=>{ const registration = this._registration; const installingSW = registration.installing; const updateLikelyTriggeredExternally = this._updateFoundCount > 0 || !urlsMatch(installingSW.scriptURL, this._scriptURL.toString()) || performance.now() > this._registrationTime + REGISTRATION_TIMEOUT_DURATION; if (updateLikelyTriggeredExternally) { this._externalSW = installingSW; registration.removeEventListener("updatefound", this._onUpdateFound); } else { this._sw = installingSW; this._ownSWs.add(installingSW); this._swDeferred.resolve(installingSW); if (process.env.NODE_ENV !== "production") { if (this._isUpdate) { logger.log("Updated service worker found. Installing now..."); } else { logger.log("Service worker is installing..."); } } } this.dispatchEvent(new SerwistEvent("installing", { sw: installingSW, originalEvent, isExternal: updateLikelyTriggeredExternally, isUpdate: this._isUpdate })); ++this._updateFoundCount; installingSW.addEventListener("statechange", this._onStateChange); }; _onStateChange = (originalEvent)=>{ const registration = this._registration; const sw = originalEvent.target; const { state } = sw; const isExternal = sw === this._externalSW; const eventProps = { sw, isExternal, originalEvent }; if (!isExternal && this._isUpdate) { eventProps.isUpdate = true; } this.dispatchEvent(new SerwistEvent(state, eventProps)); if (state === "installed") { this._waitingTimeout = self.setTimeout(()=>{ if (state === "installed" && registration.waiting === sw) { this.dispatchEvent(new SerwistEvent("waiting", eventProps)); if (process.env.NODE_ENV !== "production") { if (isExternal) { logger.warn("An external service worker has installed but is " + "waiting for this client to close before activating..."); } else { logger.warn("The service worker has installed but is waiting " + "for existing clients to close before activating..."); } } } }, WAITING_TIMEOUT_DURATION); } else if (state === "activating") { clearTimeout(this._waitingTimeout); if (!isExternal) { this._activeDeferred.resolve(sw); } } if (process.env.NODE_ENV !== "production") { switch(state){ case "installed": if (isExternal) { logger.warn("An external service worker has installed. " + "You may want to suggest users reload this page."); } else { logger.log("Registered service worker installed."); } break; case "activated": if (isExternal) { logger.warn("An external service worker has activated."); } else { logger.log("Registered service worker activated."); if (sw !== navigator.serviceWorker.controller) { logger.warn("The registered service worker is active but " + "not yet controlling the page. Reload or run " + "`clients.claim()` in the service worker."); } } break; case "redundant": if (sw === this._compatibleControllingSW) { logger.log("Previously controlling service worker now redundant!"); } else if (!isExternal) { logger.log("Registered service worker now redundant!"); } break; } } }; _onControllerChange = (originalEvent)=>{ const sw = this._sw; const isExternal = sw !== navigator.serviceWorker.controller; this.dispatchEvent(new SerwistEvent("controlling", { isExternal, originalEvent, sw, isUpdate: this._isUpdate })); if (!isExternal) { if (process.env.NODE_ENV !== "production") { logger.log("Registered service worker now controlling this page."); } this._controllingDeferred.resolve(sw); } }; _onMessage = async (originalEvent)=>{ const { data, ports, source } = originalEvent; await this.getSW(); if (this._ownSWs.has(source)) { this.dispatchEvent(new SerwistEvent("message", { data, originalEvent, ports, sw: source })); } }; } export { Serwist, SerwistEvent, messageSW };