@serwist/window
Version:
Simplifies communications with Serwist packages running in the service worker
305 lines (298 loc) • 11.9 kB
JavaScript
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 };