@angular/service-worker
Version:
Angular - service worker tooling!
435 lines (427 loc) • 13.7 kB
JavaScript
/**
* @license Angular v21.0.7
* (c) 2010-2025 Google LLC. https://angular.dev/
* License: MIT
*/
import * as i0 from '@angular/core';
import { ɵRuntimeError as _RuntimeError, ApplicationRef, Injectable, InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgZone, ɵformatRuntimeError as _formatRuntimeError, Injector, NgModule } from '@angular/core';
import { Observable, Subject, NEVER } from 'rxjs';
import { switchMap, take, filter, map } from 'rxjs/operators';
const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not supported by this browser';
class NgswCommChannel {
serviceWorker;
worker;
registration;
events;
constructor(serviceWorker, injector) {
this.serviceWorker = serviceWorker;
if (!serviceWorker) {
this.worker = this.events = this.registration = new Observable(subscriber => subscriber.error(new _RuntimeError(5601, (typeof ngDevMode === 'undefined' || ngDevMode) && ERR_SW_NOT_SUPPORTED)));
} else {
let currentWorker = null;
const workerSubject = new Subject();
this.worker = new Observable(subscriber => {
if (currentWorker !== null) {
subscriber.next(currentWorker);
}
return workerSubject.subscribe(v => subscriber.next(v));
});
const updateController = () => {
const {
controller
} = serviceWorker;
if (controller === null) {
return;
}
currentWorker = controller;
workerSubject.next(currentWorker);
};
serviceWorker.addEventListener('controllerchange', updateController);
updateController();
this.registration = this.worker.pipe(switchMap(() => serviceWorker.getRegistration().then(registration => {
if (!registration) {
throw new _RuntimeError(5601, (typeof ngDevMode === 'undefined' || ngDevMode) && ERR_SW_NOT_SUPPORTED);
}
return registration;
})));
const _events = new Subject();
this.events = _events.asObservable();
const messageListener = event => {
const {
data
} = event;
if (data?.type) {
_events.next(data);
}
};
serviceWorker.addEventListener('message', messageListener);
const appRef = injector?.get(ApplicationRef, null, {
optional: true
});
appRef?.onDestroy(() => {
serviceWorker.removeEventListener('controllerchange', updateController);
serviceWorker.removeEventListener('message', messageListener);
});
}
}
postMessage(action, payload) {
return new Promise(resolve => {
this.worker.pipe(take(1)).subscribe(sw => {
sw.postMessage({
action,
...payload
});
resolve();
});
});
}
postMessageWithOperation(type, payload, operationNonce) {
const waitForOperationCompleted = this.waitForOperationCompleted(operationNonce);
const postMessage = this.postMessage(type, payload);
return Promise.all([postMessage, waitForOperationCompleted]).then(([, result]) => result);
}
generateNonce() {
return Math.round(Math.random() * 10000000);
}
eventsOfType(type) {
let filterFn;
if (typeof type === 'string') {
filterFn = event => event.type === type;
} else {
filterFn = event => type.includes(event.type);
}
return this.events.pipe(filter(filterFn));
}
nextEventOfType(type) {
return this.eventsOfType(type).pipe(take(1));
}
waitForOperationCompleted(nonce) {
return new Promise((resolve, reject) => {
this.eventsOfType('OPERATION_COMPLETED').pipe(filter(event => event.nonce === nonce), take(1), map(event => {
if (event.result !== undefined) {
return event.result;
}
throw new Error(event.error);
})).subscribe({
next: resolve,
error: reject
});
});
}
get isEnabled() {
return !!this.serviceWorker;
}
}
class SwPush {
sw;
messages;
notificationClicks;
notificationCloses;
pushSubscriptionChanges;
subscription;
get isEnabled() {
return this.sw.isEnabled;
}
pushManager = null;
subscriptionChanges = new Subject();
constructor(sw) {
this.sw = sw;
if (!sw.isEnabled) {
this.messages = NEVER;
this.notificationClicks = NEVER;
this.notificationCloses = NEVER;
this.pushSubscriptionChanges = NEVER;
this.subscription = NEVER;
return;
}
this.messages = this.sw.eventsOfType('PUSH').pipe(map(message => message.data));
this.notificationClicks = this.sw.eventsOfType('NOTIFICATION_CLICK').pipe(map(message => message.data));
this.notificationCloses = this.sw.eventsOfType('NOTIFICATION_CLOSE').pipe(map(message => message.data));
this.pushSubscriptionChanges = this.sw.eventsOfType('PUSH_SUBSCRIPTION_CHANGE').pipe(map(message => message.data));
this.pushManager = this.sw.registration.pipe(map(registration => registration.pushManager));
const workerDrivenSubscriptions = this.pushManager.pipe(switchMap(pm => pm.getSubscription()));
this.subscription = new Observable(subscriber => {
const workerDrivenSubscription = workerDrivenSubscriptions.subscribe(subscriber);
const subscriptionChanges = this.subscriptionChanges.subscribe(subscriber);
return () => {
workerDrivenSubscription.unsubscribe();
subscriptionChanges.unsubscribe();
};
});
}
requestSubscription(options) {
if (!this.sw.isEnabled || this.pushManager === null) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const pushOptions = {
userVisibleOnly: true
};
let key = this.decodeBase64(options.serverPublicKey.replace(/_/g, '/').replace(/-/g, '+'));
let applicationServerKey = new Uint8Array(new ArrayBuffer(key.length));
for (let i = 0; i < key.length; i++) {
applicationServerKey[i] = key.charCodeAt(i);
}
pushOptions.applicationServerKey = applicationServerKey;
return new Promise((resolve, reject) => {
this.pushManager.pipe(switchMap(pm => pm.subscribe(pushOptions)), take(1)).subscribe({
next: sub => {
this.subscriptionChanges.next(sub);
resolve(sub);
},
error: reject
});
});
}
unsubscribe() {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const doUnsubscribe = sub => {
if (sub === null) {
throw new _RuntimeError(5602, (typeof ngDevMode === 'undefined' || ngDevMode) && 'Not subscribed to push notifications.');
}
return sub.unsubscribe().then(success => {
if (!success) {
throw new _RuntimeError(5603, (typeof ngDevMode === 'undefined' || ngDevMode) && 'Unsubscribe failed!');
}
this.subscriptionChanges.next(null);
});
};
return new Promise((resolve, reject) => {
this.subscription.pipe(take(1), switchMap(doUnsubscribe)).subscribe({
next: resolve,
error: reject
});
});
}
decodeBase64(input) {
return atob(input);
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: SwPush,
deps: [{
token: NgswCommChannel
}],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: SwPush
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: SwPush,
decorators: [{
type: Injectable
}],
ctorParameters: () => [{
type: NgswCommChannel
}]
});
class SwUpdate {
sw;
versionUpdates;
unrecoverable;
get isEnabled() {
return this.sw.isEnabled;
}
ongoingCheckForUpdate = null;
constructor(sw) {
this.sw = sw;
if (!sw.isEnabled) {
this.versionUpdates = NEVER;
this.unrecoverable = NEVER;
return;
}
this.versionUpdates = this.sw.eventsOfType(['VERSION_DETECTED', 'VERSION_INSTALLATION_FAILED', 'VERSION_READY', 'NO_NEW_VERSION_DETECTED']);
this.unrecoverable = this.sw.eventsOfType('UNRECOVERABLE_STATE');
}
checkForUpdate() {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
if (this.ongoingCheckForUpdate) {
return this.ongoingCheckForUpdate;
}
const nonce = this.sw.generateNonce();
this.ongoingCheckForUpdate = this.sw.postMessageWithOperation('CHECK_FOR_UPDATES', {
nonce
}, nonce).finally(() => {
this.ongoingCheckForUpdate = null;
});
return this.ongoingCheckForUpdate;
}
activateUpdate() {
if (!this.sw.isEnabled) {
return Promise.reject(new _RuntimeError(5601, (typeof ngDevMode === 'undefined' || ngDevMode) && ERR_SW_NOT_SUPPORTED));
}
const nonce = this.sw.generateNonce();
return this.sw.postMessageWithOperation('ACTIVATE_UPDATE', {
nonce
}, nonce);
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: SwUpdate,
deps: [{
token: NgswCommChannel
}],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: SwUpdate
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: SwUpdate,
decorators: [{
type: Injectable
}],
ctorParameters: () => [{
type: NgswCommChannel
}]
});
const SCRIPT = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGSW_REGISTER_SCRIPT' : '');
function ngswAppInitializer() {
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
return;
}
const options = inject(SwRegistrationOptions);
if (!('serviceWorker' in navigator && options.enabled !== false)) {
return;
}
const script = inject(SCRIPT);
const ngZone = inject(NgZone);
const appRef = inject(ApplicationRef);
ngZone.runOutsideAngular(() => {
const sw = navigator.serviceWorker;
const onControllerChange = () => sw.controller?.postMessage({
action: 'INITIALIZE'
});
sw.addEventListener('controllerchange', onControllerChange);
appRef.onDestroy(() => {
sw.removeEventListener('controllerchange', onControllerChange);
});
});
ngZone.runOutsideAngular(() => {
let readyToRegister;
const {
registrationStrategy
} = options;
if (typeof registrationStrategy === 'function') {
readyToRegister = new Promise(resolve => registrationStrategy().subscribe(() => resolve()));
} else {
const [strategy, ...args] = (registrationStrategy || 'registerWhenStable:30000').split(':');
switch (strategy) {
case 'registerImmediately':
readyToRegister = Promise.resolve();
break;
case 'registerWithDelay':
readyToRegister = delayWithTimeout(+args[0] || 0);
break;
case 'registerWhenStable':
readyToRegister = Promise.race([appRef.whenStable(), delayWithTimeout(+args[0])]);
break;
default:
throw new _RuntimeError(5600, (typeof ngDevMode === 'undefined' || ngDevMode) && `Unknown ServiceWorker registration strategy: ${options.registrationStrategy}`);
}
}
readyToRegister.then(() => {
if (appRef.destroyed) {
return;
}
navigator.serviceWorker.register(script, {
scope: options.scope,
updateViaCache: options.updateViaCache,
type: options.type
}).catch(err => console.error(_formatRuntimeError(5604, (typeof ngDevMode === 'undefined' || ngDevMode) && 'Service worker registration failed with: ' + err)));
});
});
}
function delayWithTimeout(timeout) {
return new Promise(resolve => setTimeout(resolve, timeout));
}
function ngswCommChannelFactory() {
const opts = inject(SwRegistrationOptions);
const injector = inject(Injector);
const isBrowser = !(typeof ngServerMode !== 'undefined' && ngServerMode);
return new NgswCommChannel(isBrowser && opts.enabled !== false ? navigator.serviceWorker : undefined, injector);
}
class SwRegistrationOptions {
enabled;
updateViaCache;
type;
scope;
registrationStrategy;
}
function provideServiceWorker(script, options = {}) {
return makeEnvironmentProviders([SwPush, SwUpdate, {
provide: SCRIPT,
useValue: script
}, {
provide: SwRegistrationOptions,
useValue: options
}, {
provide: NgswCommChannel,
useFactory: ngswCommChannelFactory
}, provideAppInitializer(ngswAppInitializer)]);
}
class ServiceWorkerModule {
static register(script, options = {}) {
return {
ngModule: ServiceWorkerModule,
providers: [provideServiceWorker(script, options)]
};
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: ServiceWorkerModule,
deps: [],
target: i0.ɵɵFactoryTarget.NgModule
});
static ɵmod = i0.ɵɵngDeclareNgModule({
minVersion: "14.0.0",
version: "21.0.7",
ngImport: i0,
type: ServiceWorkerModule
});
static ɵinj = i0.ɵɵngDeclareInjector({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: ServiceWorkerModule,
providers: [SwPush, SwUpdate]
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.7",
ngImport: i0,
type: ServiceWorkerModule,
decorators: [{
type: NgModule,
args: [{
providers: [SwPush, SwUpdate]
}]
}]
});
export { ServiceWorkerModule, SwPush, SwRegistrationOptions, SwUpdate, provideServiceWorker };
//# sourceMappingURL=service-worker.mjs.map