@vite-pwa/workbox-window
Version:
Simplifies communications with Workbox packages running in the service worker
584 lines (574 loc) • 19 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
Workbox: () => Workbox,
WorkboxEvent: () => WorkboxEvent,
messageSW: () => messageSW
});
module.exports = __toCommonJS(src_exports);
// src/messageSW.ts
function messageSW(sw, data) {
return new Promise((resolve) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
resolve(event.data);
};
sw.postMessage(data, [messageChannel.port2]);
});
}
// src/utils/dontWaitFor.ts
function dontWaitFor(promise) {
void promise.then(() => {
});
}
// src/utils/logger.ts
var logger = process.env.NODE_ENV === "production" ? null : (() => {
if (!("__WB_DISABLE_DEV_LOGS" in globalThis))
self.__WB_DISABLE_DEV_LOGS = false;
let inGroup = false;
const methodToColorMap = {
debug: "#7f8c8d",
// Gray
log: "#2ecc71",
// Green
warn: "#f39c12",
// Yellow
error: "#c0392b",
// Red
groupCollapsed: "#3498db",
// Blue
groupEnd: null
// No colored prefix on groupEnd
};
const print = function(method, args) {
if (self.__WB_DISABLE_DEV_LOGS)
return;
if (method === "groupCollapsed") {
if (typeof navigator !== "undefined" && /^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
console[method](...args);
return;
}
}
const styles = [
`background: ${methodToColorMap[method]}`,
"border-radius: 0.5em",
"color: white",
"font-weight: bold",
"padding: 2px 0.5em"
];
const logPrefix = inGroup ? [] : ["%c@vite-pwa/workbox-window", styles.join(";")];
console[method](...logPrefix, ...args);
if (method === "groupCollapsed")
inGroup = true;
if (method === "groupEnd")
inGroup = false;
};
const api = {};
const loggerMethods = Object.keys(methodToColorMap);
for (const key of loggerMethods) {
const method = key;
api[method] = (...args) => {
print(method, args);
};
}
return api;
})();
// src/utils/Deferred.ts
var Deferred = class {
promise;
resolve;
reject;
/**
* Creates a promise and exposes its resolve and reject functions as methods.
*/
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
};
// src/utils/WorkboxEventTarget.ts
var WorkboxEventTarget = class {
_eventListenerRegistry = /* @__PURE__ */ new Map();
/**
* @param {string} type
* @param {Function} listener
* @private
*/
addEventListener(type, listener) {
const foo = this._getEventListenersByType(type);
foo.add(listener);
}
/**
* @param {string} type
* @param {Function} listener
* @private
*/
removeEventListener(type, listener) {
this._getEventListenersByType(type).delete(listener);
}
/**
* @param {object} event
* @private
*/
dispatchEvent(event) {
event.target = this;
const listeners = this._getEventListenersByType(event.type);
for (const listener of listeners)
listener(event);
}
/**
* Returns a Set of listeners associated with the passed event type.
* If no handlers have been registered, an empty Set is returned.
*
* @param {string} type The event type.
* @return {Set<ListenerCallback>} An array of handler functions.
* @private
*/
_getEventListenersByType(type) {
if (!this._eventListenerRegistry.has(type))
this._eventListenerRegistry.set(type, /* @__PURE__ */ new Set());
return this._eventListenerRegistry.get(type);
}
};
// src/utils/urlsMatch.ts
function urlsMatch(url1, url2) {
const { href } = location;
return new URL(url1, href).href === new URL(url2, href).href;
}
// src/utils/WorkboxEvent.ts
var WorkboxEvent = class {
constructor(type, props) {
this.type = type;
Object.assign(this, props);
}
target;
sw;
originalEvent;
isExternal;
};
// src/Workbox.ts
var WAITING_TIMEOUT_DURATION = 200;
var REGISTRATION_TIMEOUT_DURATION = 6e4;
var SKIP_WAITING_MESSAGE = { type: "SKIP_WAITING" };
var Workbox = class extends WorkboxEventTarget {
_scriptURL;
_registerOptions = {};
_updateFoundCount = 0;
// Deferreds we can resolve later.
_swDeferred = new Deferred();
_activeDeferred = new Deferred();
_controllingDeferred = new Deferred();
_registrationTime = 0;
_isUpdate;
_compatibleControllingSW;
_registration;
_sw;
_ownSWs = /* @__PURE__ */ new Set();
_externalSW;
_waitingTimeout;
/**
* Creates a new Workbox instance with a script URL and service worker
* options. The script URL and options are the same as those used when
* calling [navigator.serviceWorker.register(scriptURL, options)](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register).
*
* @param {string|TrustedScriptURL} scriptURL The service worker script
* associated with this instance. Using a
* [`TrustedScriptURL`](https://web.dev/trusted-types/) is supported.
* @param {object} [registerOptions] The service worker options associated
* with this instance.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
constructor(scriptURL, registerOptions = {}) {
super();
this._scriptURL = scriptURL;
this._registerOptions = registerOptions;
navigator.serviceWorker.addEventListener("message", this._onMessage);
}
/**
* Registers a service worker for this instances script URL and service
* worker options. By default this method delays registration until after
* the window has loaded.
*
* @param {object} [options]
* @param {Function} [options.immediate] Setting this to true will
* register the service worker immediately, even if the window has
* not loaded (not recommended).
*/
async register({ immediate = false } = {}) {
if (process.env.NODE_ENV !== "production") {
if (this._registrationTime) {
logger.error(
"Cannot re-register a Workbox instance after it has been registered. Create a new instance instead."
);
return;
}
}
if (!immediate && document.readyState !== "complete")
await new Promise((resolve) => window.addEventListener("load", resolve));
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;
dontWaitFor(
Promise.resolve().then(() => {
this.dispatchEvent(
new WorkboxEvent("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..."
);
}
}
const currentPageIsOutOfScope = () => {
const scopeURL = new URL(
this._registerOptions.scope || this._scriptURL.toString(),
document.baseURI
);
const scopeURLBasePath = new URL("./", scopeURL.href).pathname;
return !location.pathname.startsWith(scopeURLBasePath);
};
if (currentPageIsOutOfScope()) {
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;
}
/**
* Checks for updates of the registered service worker.
*/
async update() {
if (!this._registration) {
if (process.env.NODE_ENV !== "production") {
logger.error(
"Cannot update a Workbox instance without being registered. Register the Workbox instance first."
);
}
return;
}
await this._registration.update();
}
/**
* Resolves to the service worker registered by this instance as soon as it
* is active. If a service worker was already controlling at registration
* time then it will resolve to that if the script URLs (and optionally
* script versions) match, otherwise it will wait until an update is found
* and activates.
*
* @return {Promise<ServiceWorker>}
*/
get active() {
return this._activeDeferred.promise;
}
/**
* Resolves to the service worker registered by this instance as soon as it
* is controlling the page. If a service worker was already controlling at
* registration time then it will resolve to that if the script URLs (and
* optionally script versions) match, otherwise it will wait until an update
* is found and starts controlling the page.
* Note: the first time a service worker is installed it will active but
* not start controlling the page unless `clients.claim()` is called in the
* service worker.
*
* @return {Promise<ServiceWorker>}
*/
get controlling() {
return this._controllingDeferred.promise;
}
/**
* Resolves with a reference to a service worker that matches the script URL
* of this instance, as soon as it's available.
*
* If, at registration time, there's already an active or waiting service
* worker with a matching script URL, it will be used (with the waiting
* service worker taking precedence over the active service worker if both
* match, since the waiting service worker would have been registered more
* recently).
* If there's no matching active or waiting service worker at registration
* time then the promise will not resolve until an update is found and starts
* installing, at which point the installing service worker is used.
*
* @return {Promise<ServiceWorker>}
*/
getSW() {
return this._sw !== void 0 ? Promise.resolve(this._sw) : this._swDeferred.promise;
}
/**
* Sends the passed data object to the service worker registered by this
* instance (via {@link workbox-window.Workbox#getSW}) and resolves
* with a response (if any).
*
* A response can be set in a message handler in the service worker by
* calling `event.ports[0].postMessage(...)`, which will resolve the promise
* returned by `messageSW()`. If no response is set, the promise will never
* resolve.
*
* @param {object} data An object to send to the service worker
* @return {Promise<object>}
*/
// We might be able to change the 'data' type to Record<string, unknown> in the future.
async messageSW(data) {
const sw = await this.getSW();
return messageSW(sw, data);
}
/**
* Sends a `{type: 'SKIP_WAITING'}` message to the service worker that's
* currently in the `waiting` state associated with the current registration.
*
* If there is no current registration or no service worker is `waiting`,
* calling this will have no effect.
*/
messageSkipWaiting() {
if (this._registration && this._registration.waiting)
void messageSW(this._registration.waiting, SKIP_WAITING_MESSAGE);
}
/**
* Checks for a service worker already controlling the page and returns
* it if its script URL matches.
*
* @private
* @return {ServiceWorker|undefined}
*/
_getControllingSWIfCompatible() {
const controller = navigator.serviceWorker.controller;
if (controller && urlsMatch(controller.scriptURL, this._scriptURL.toString()))
return controller;
else
return void 0;
}
/**
* Registers a service worker for this instances script URL and register
* options and tracks the time registration was complete.
*
* @private
*/
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;
}
}
/**
* @private
*/
_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 (navigator.serviceWorker.controller)
logger.log("Updated service worker found. Installing now...");
else
logger.log("Service worker is installing...");
}
}
this.dispatchEvent(
new WorkboxEvent("installing", {
sw: installingSW,
originalEvent,
isExternal: updateLikelyTriggeredExternally,
isUpdate: this._isUpdate
})
);
++this._updateFoundCount;
installingSW.addEventListener("statechange", this._onStateChange);
};
/**
* @private
* @param {Event} originalEvent
*/
_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 WorkboxEvent(state, eventProps)
);
if (state === "installed") {
this._waitingTimeout = self.setTimeout(() => {
if (state === "installed" && registration.waiting === sw) {
this.dispatchEvent(new WorkboxEvent("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;
}
}
};
/**
* @private
* @param {Event} originalEvent
*/
_onControllerChange = (originalEvent) => {
const sw = this._sw;
const isExternal = sw !== navigator.serviceWorker.controller;
this.dispatchEvent(
new WorkboxEvent("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);
}
};
/**
* @private
* @param {Event} originalEvent
*/
_onMessage = async (originalEvent) => {
const { data, ports, source } = originalEvent;
await this.getSW();
if (this._ownSWs.has(source)) {
this.dispatchEvent(
new WorkboxEvent("message", {
// Can't change type 'any' of data.
data,
originalEvent,
ports,
sw: source
})
);
}
};
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Workbox,
WorkboxEvent,
messageSW
});