UNPKG

@angular/service-worker

Version:

Angular - service worker tooling!

500 lines (489 loc) 19.9 kB
/** * @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