@angular/service-worker
Version:
Angular - service worker tooling!
500 lines (489 loc) • 19.9 kB
JavaScript
/**
* @license Angular v14.0.4
* (c) 2010-2022 Google LLC. https://angular.io/
* License: MIT
*/
import { isPlatformBrowser } from '@angular/common';
import * as i0 from '@angular/core';
import { Injectable, InjectionToken, NgZone, ApplicationRef, PLATFORM_ID, APP_INITIALIZER, Injector, NgModule } from '@angular/core';
import { defer, throwError, fromEvent, of, concat, Subject, NEVER, merge } from 'rxjs';
import { map, filter, switchMap, publish, take, tap, delay } from 'rxjs/operators';
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not supported by this browser';
function errorObservable(message) {
return defer(() => throwError(new Error(message)));
}
/**
* @publicApi
*/
class NgswCommChannel {
constructor(serviceWorker) {
this.serviceWorker = serviceWorker;
if (!serviceWorker) {
this.worker = this.events = this.registration = errorObservable(ERR_SW_NOT_SUPPORTED);
}
else {
const controllerChangeEvents = fromEvent(serviceWorker, 'controllerchange');
const controllerChanges = controllerChangeEvents.pipe(map(() => serviceWorker.controller));
const currentController = defer(() => of(serviceWorker.controller));
const controllerWithChanges = concat(currentController, controllerChanges);
this.worker = controllerWithChanges.pipe(filter((c) => !!c));
this.registration = (this.worker.pipe(switchMap(() => serviceWorker.getRegistration())));
const rawEvents = fromEvent(serviceWorker, 'message');
const rawEventPayload = rawEvents.pipe(map(event => event.data));
const eventsUnconnected = rawEventPayload.pipe(filter(event => event && event.type));
const events = eventsUnconnected.pipe(publish());
events.connect();
this.events = events;
}
}
postMessage(action, payload) {
return this.worker
.pipe(take(1), tap((sw) => {
sw.postMessage({
action,
...payload,
});
}))
.toPromise()
.then(() => undefined);
}
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 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);
}))
.toPromise();
}
get isEnabled() {
return !!this.serviceWorker;
}
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Subscribe and listen to
* [Web Push
* Notifications](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices) through
* Angular Service Worker.
*
* @usageNotes
*
* You can inject a `SwPush` instance into any component or service
* as a dependency.
*
* <code-example path="service-worker/push/module.ts" region="inject-sw-push"
* header="app.component.ts"></code-example>
*
* To subscribe, call `SwPush.requestSubscription()`, which asks the user for permission.
* The call returns a `Promise` with a new
* [`PushSubscription`](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription)
* instance.
*
* <code-example path="service-worker/push/module.ts" region="subscribe-to-push"
* header="app.component.ts"></code-example>
*
* A request is rejected if the user denies permission, or if the browser
* blocks or does not support the Push API or ServiceWorkers.
* Check `SwPush.isEnabled` to confirm status.
*
* Invoke Push Notifications by pushing a message with the following payload.
*
* ```ts
* {
* "notification": {
* "actions": NotificationAction[],
* "badge": USVString,
* "body": DOMString,
* "data": any,
* "dir": "auto"|"ltr"|"rtl",
* "icon": USVString,
* "image": USVString,
* "lang": DOMString,
* "renotify": boolean,
* "requireInteraction": boolean,
* "silent": boolean,
* "tag": DOMString,
* "timestamp": DOMTimeStamp,
* "title": DOMString,
* "vibrate": number[]
* }
* }
* ```
*
* Only `title` is required. See `Notification`
* [instance
* properties](https://developer.mozilla.org/en-US/docs/Web/API/Notification#Instance_properties).
*
* While the subscription is active, Service Worker listens for
* [PushEvent](https://developer.mozilla.org/en-US/docs/Web/API/PushEvent)
* occurrences and creates
* [Notification](https://developer.mozilla.org/en-US/docs/Web/API/Notification)
* instances in response.
*
* Unsubscribe using `SwPush.unsubscribe()`.
*
* An application can subscribe to `SwPush.notificationClicks` observable to be notified when a user
* clicks on a notification. For example:
*
* <code-example path="service-worker/push/module.ts" region="subscribe-to-notification-clicks"
* header="app.component.ts"></code-example>
*
* You can read more on handling notification clicks in the [Service worker notifications
* guide](guide/service-worker-notifications).
*
* @see [Push Notifications](https://developers.google.com/web/fundamentals/codelabs/push-notifications/)
* @see [Angular Push Notifications](https://blog.angular-university.io/angular-push-notifications/)
* @see [MDN: Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
* @see [MDN: Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API)
* @see [MDN: Web Push API Notifications best practices](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices)
*
* @publicApi
*/
class SwPush {
constructor(sw) {
this.sw = sw;
this.subscriptionChanges = new Subject();
if (!sw.isEnabled) {
this.messages = NEVER;
this.notificationClicks = 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.pushManager = this.sw.registration.pipe(map(registration => registration.pushManager));
const workerDrivenSubscriptions = this.pushManager.pipe(switchMap(pm => pm.getSubscription()));
this.subscription = merge(workerDrivenSubscriptions, this.subscriptionChanges);
}
/**
* True if the Service Worker is enabled (supported by the browser and enabled via
* `ServiceWorkerModule`).
*/
get isEnabled() {
return this.sw.isEnabled;
}
/**
* Subscribes to Web Push Notifications,
* after requesting and receiving user permission.
*
* @param options An object containing the `serverPublicKey` string.
* @returns A Promise that resolves to the new subscription object.
*/
requestSubscription(options) {
if (!this.sw.isEnabled) {
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 this.pushManager.pipe(switchMap(pm => pm.subscribe(pushOptions)), take(1))
.toPromise()
.then(sub => {
this.subscriptionChanges.next(sub);
return sub;
});
}
/**
* Unsubscribes from Service Worker push notifications.
*
* @returns A Promise that is resolved when the operation succeeds, or is rejected if there is no
* active subscription or the unsubscribe operation fails.
*/
unsubscribe() {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const doUnsubscribe = (sub) => {
if (sub === null) {
throw new Error('Not subscribed to push notifications.');
}
return sub.unsubscribe().then(success => {
if (!success) {
throw new Error('Unsubscribe failed!');
}
this.subscriptionChanges.next(null);
});
};
return this.subscription.pipe(take(1), switchMap(doUnsubscribe)).toPromise();
}
decodeBase64(input) {
return atob(input);
}
}
SwPush.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SwPush, deps: [{ token: NgswCommChannel }], target: i0.ɵɵFactoryTarget.Injectable });
SwPush.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SwPush });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SwPush, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: NgswCommChannel }]; } });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Subscribe to update notifications from the Service Worker, trigger update
* checks, and forcibly activate updates.
*
* @see {@link guide/service-worker-communications Service worker communication guide}
*
* @publicApi
*/
class SwUpdate {
constructor(sw) {
this.sw = sw;
if (!sw.isEnabled) {
this.versionUpdates = NEVER;
this.available = NEVER;
this.activated = NEVER;
this.unrecoverable = NEVER;
return;
}
this.versionUpdates = this.sw.eventsOfType([
'VERSION_DETECTED',
'VERSION_INSTALLATION_FAILED',
'VERSION_READY',
'NO_NEW_VERSION_DETECTED',
]);
this.available = this.versionUpdates.pipe(filter((evt) => evt.type === 'VERSION_READY'), map(evt => ({
type: 'UPDATE_AVAILABLE',
current: evt.currentVersion,
available: evt.latestVersion,
})));
this.activated = this.sw.eventsOfType('UPDATE_ACTIVATED');
this.unrecoverable = this.sw.eventsOfType('UNRECOVERABLE_STATE');
}
/**
* True if the Service Worker is enabled (supported by the browser and enabled via
* `ServiceWorkerModule`).
*/
get isEnabled() {
return this.sw.isEnabled;
}
/**
* Checks for an update and waits until the new version is downloaded from the server and ready
* for activation.
*
* @returns a promise that
* - resolves to `true` if a new version was found and is ready to be activated.
* - resolves to `false` if no new version was found
* - rejects if any error occurs
*/
checkForUpdate() {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const nonce = this.sw.generateNonce();
return this.sw.postMessageWithOperation('CHECK_FOR_UPDATES', { nonce }, nonce);
}
/**
* Updates the current client (i.e. browser tab) to the latest version that is ready for
* activation.
*
* @returns a promise that
* - resolves to `true` if an update was activated successfully
* - resolves to `false` if no update was available (for example, the client was already on the
* latest version).
* - rejects if any error occurs
*/
activateUpdate() {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
}
const nonce = this.sw.generateNonce();
return this.sw.postMessageWithOperation('ACTIVATE_UPDATE', { nonce }, nonce);
}
}
SwUpdate.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SwUpdate, deps: [{ token: NgswCommChannel }], target: i0.ɵɵFactoryTarget.Injectable });
SwUpdate.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SwUpdate });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SwUpdate, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: NgswCommChannel }]; } });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Token that can be used to provide options for `ServiceWorkerModule` outside of
* `ServiceWorkerModule.register()`.
*
* You can use this token to define a provider that generates the registration options at runtime,
* for example via a function call:
*
* {@example service-worker/registration-options/module.ts region="registration-options"
* header="app.module.ts"}
*
* @publicApi
*/
class SwRegistrationOptions {
}
const SCRIPT = new InjectionToken('NGSW_REGISTER_SCRIPT');
function ngswAppInitializer(injector, script, options, platformId) {
const initializer = () => {
if (!(isPlatformBrowser(platformId) && ('serviceWorker' in navigator) &&
options.enabled !== false)) {
return;
}
// Wait for service worker controller changes, and fire an INITIALIZE action when a new SW
// becomes active. This allows the SW to initialize itself even if there is no application
// traffic.
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (navigator.serviceWorker.controller !== null) {
navigator.serviceWorker.controller.postMessage({ action: 'INITIALIZE' });
}
});
let readyToRegister$;
if (typeof options.registrationStrategy === 'function') {
readyToRegister$ = options.registrationStrategy();
}
else {
const [strategy, ...args] = (options.registrationStrategy || 'registerWhenStable:30000').split(':');
switch (strategy) {
case 'registerImmediately':
readyToRegister$ = of(null);
break;
case 'registerWithDelay':
readyToRegister$ = delayWithTimeout(+args[0] || 0);
break;
case 'registerWhenStable':
readyToRegister$ = !args[0] ? whenStable(injector) :
merge(whenStable(injector), delayWithTimeout(+args[0]));
break;
default:
// Unknown strategy.
throw new Error(`Unknown ServiceWorker registration strategy: ${options.registrationStrategy}`);
}
}
// Don't return anything to avoid blocking the application until the SW is registered.
// Also, run outside the Angular zone to avoid preventing the app from stabilizing (especially
// given that some registration strategies wait for the app to stabilize).
// Catch and log the error if SW registration fails to avoid uncaught rejection warning.
const ngZone = injector.get(NgZone);
ngZone.runOutsideAngular(() => readyToRegister$.pipe(take(1)).subscribe(() => navigator.serviceWorker.register(script, { scope: options.scope })
.catch(err => console.error('Service worker registration failed with:', err))));
};
return initializer;
}
function delayWithTimeout(timeout) {
return of(null).pipe(delay(timeout));
}
function whenStable(injector) {
const appRef = injector.get(ApplicationRef);
return appRef.isStable.pipe(filter(stable => stable));
}
function ngswCommChannelFactory(opts, platformId) {
return new NgswCommChannel(isPlatformBrowser(platformId) && opts.enabled !== false ? navigator.serviceWorker :
undefined);
}
/**
* @publicApi
*/
class ServiceWorkerModule {
/**
* Register the given Angular Service Worker script.
*
* If `enabled` is set to `false` in the given options, the module will behave as if service
* workers are not supported by the browser, and the service worker will not be registered.
*/
static register(script, opts = {}) {
return {
ngModule: ServiceWorkerModule,
providers: [
{ provide: SCRIPT, useValue: script },
{ provide: SwRegistrationOptions, useValue: opts },
{
provide: NgswCommChannel,
useFactory: ngswCommChannelFactory,
deps: [SwRegistrationOptions, PLATFORM_ID]
},
{
provide: APP_INITIALIZER,
useFactory: ngswAppInitializer,
deps: [Injector, SCRIPT, SwRegistrationOptions, PLATFORM_ID],
multi: true,
},
],
};
}
}
ServiceWorkerModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: ServiceWorkerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
ServiceWorkerModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.0.4", ngImport: i0, type: ServiceWorkerModule });
ServiceWorkerModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: ServiceWorkerModule, providers: [SwPush, SwUpdate] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: ServiceWorkerModule, decorators: [{
type: NgModule,
args: [{
providers: [SwPush, SwUpdate],
}]
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// This file only reexports content of the `src` folder. Keep it that way.
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Generated bundle index. Do not edit.
*/
export { ServiceWorkerModule, SwPush, SwRegistrationOptions, SwUpdate };
//# sourceMappingURL=service-worker.mjs.map