@aws-amplify/core
Version:
Core category of aws-amplify
264 lines (262 loc) • 12.7 kB
JavaScript
;
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServiceWorkerClass = void 0;
const Logger_1 = require("../Logger");
const utils_1 = require("../utils");
const errors_1 = require("../errors");
const pinpoint_1 = require("../providers/pinpoint");
const singleton_1 = require("../singleton");
const errorHelpers_1 = require("./errorHelpers");
/**
* Provides a means to registering a service worker in the browser
* and communicating with it via postMessage events.
* https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/
*
* postMessage events are currently not supported in all browsers. See:
* https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
*
* At the minmum this class will register the service worker and listen
* and attempt to dispatch messages on state change and record analytics
* events based on the service worker lifecycle.
*/
class ServiceWorkerClass {
constructor() {
// The AWS Amplify logger
this._logger = new Logger_1.ConsoleLogger('ServiceWorker');
}
/**
* Get the currently active service worker
*/
get serviceWorker() {
(0, errorHelpers_1.assert)(this._serviceWorker !== undefined, errorHelpers_1.ServiceWorkerErrorCode.UndefinedInstance);
return this._serviceWorker;
}
/**
* Register the service-worker.js file in the browser
* Make sure the service-worker.js is part of the build
* for example with Angular, modify the angular-cli.json file
* and add to "assets" array "service-worker.js"
*
* Note: when `options.onStateChange` is omitted, this method implicitly
* records service worker lifecycle (`statechange`) events to Amazon
* Pinpoint. That built-in auto-recording is deprecated and will be removed
* in a future major version — only the implicit Pinpoint recording is
* deprecated, not `register()` itself. Provide `options.onStateChange` to
* observe lifecycle state changes and emit vendor-neutral telemetry instead.
* @param {string} filePath Service worker file. Defaults to "/service-worker.js"
* @param {string} scope The service worker scope. Defaults to "/"
* - API Doc: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register
* @param {ServiceWorkerOptions} [options] Optional registration options. When
* `onStateChange` is provided it is invoked on every service worker state
* change and replaces the built-in Pinpoint auto-recording. It is also
* invoked once with the worker's current state at registration time, so an
* already-active worker (which dispatches no `statechange` event) is still
* observed. This initial emit applies only to `onStateChange`; the built-in
* Pinpoint path is unaffected.
* @returns {Promise}
* - resolve(ServiceWorkerRegistration)
* - reject(Error)
**/
register(filePath = '/service-worker.js', scope = '/', options) {
this._onStateChange = options?.onStateChange;
this._logger.debug(`registering ${filePath}`);
this._logger.debug(`registering service worker with scope ${scope}`);
return new Promise((resolve, reject) => {
if (navigator && 'serviceWorker' in navigator) {
navigator.serviceWorker
.register(filePath, {
scope,
})
.then(registration => {
if (registration.installing) {
this._serviceWorker = registration.installing;
}
else if (registration.waiting) {
this._serviceWorker = registration.waiting;
}
else if (registration.active) {
this._serviceWorker = registration.active;
}
this._registration = registration;
this._setupListeners();
this._logger.debug(`Service Worker Registration Success: ${registration}`);
resolve(registration);
})
.catch(error => {
this._logger.debug(`Service Worker Registration Failed ${error}`);
reject(new errors_1.AmplifyError({
name: errorHelpers_1.ServiceWorkerErrorCode.Unavailable,
message: 'Service Worker not available',
underlyingError: error,
}));
});
}
else {
reject(new errors_1.AmplifyError({
name: errorHelpers_1.ServiceWorkerErrorCode.Unavailable,
message: 'Service Worker not available',
}));
}
});
}
/**
* Enable web push notifications. If not subscribed, a new subscription will
* be created and registered.
* Test Push Server: https://web-push-codelab.glitch.me/
* Push Server Libraries: https://github.com/web-push-libs/
* API Doc: https://developers.google.com/web/fundamentals/codelabs/push-notifications/
* @param publicKey
* @returns {Promise}
* - resolve(PushSubscription)
* - reject(Error)
*/
enablePush(publicKey) {
(0, errorHelpers_1.assert)(this._registration !== undefined, errorHelpers_1.ServiceWorkerErrorCode.UndefinedRegistration);
this._publicKey = publicKey;
return new Promise((resolve, reject) => {
if ((0, utils_1.isBrowser)()) {
(0, errorHelpers_1.assert)(this._registration !== undefined, errorHelpers_1.ServiceWorkerErrorCode.UndefinedRegistration);
this._registration.pushManager.getSubscription().then(subscription => {
if (subscription) {
this._subscription = subscription;
this._logger.debug(`User is subscribed to push: ${JSON.stringify(subscription)}`);
resolve(subscription);
}
else {
this._logger.debug(`User is NOT subscribed to push`);
return this._registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this._urlB64ToUint8Array(publicKey),
})
.then(pushManagerSubscription => {
this._subscription = pushManagerSubscription;
this._logger.debug(`User subscribed: ${JSON.stringify(pushManagerSubscription)}`);
resolve(pushManagerSubscription);
})
.catch(error => {
this._logger.error(error);
});
}
});
}
else {
reject(new errors_1.AmplifyError({
name: errorHelpers_1.ServiceWorkerErrorCode.Unavailable,
message: 'Service Worker not available',
}));
}
});
}
/**
* Convert a base64 encoded string to a Uint8 array for the push server key
* @param base64String
*/
_urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
/**
* Send a message to the service worker. The service worker needs
* to implement `self.addEventListener('message') to handle the
* message. This ***currently*** does not work in Safari or IE.
* @param {object | string} message An arbitrary JSON object or string message to send to the service worker
* - see: https://developer.mozilla.org/en-US/docs/Web/API/Transferable
* @returns {Promise}
**/
send(message) {
if (this._serviceWorker) {
this._serviceWorker.postMessage(typeof message === 'object' ? JSON.stringify(message) : message);
}
}
/**
* Listen for service worker state change and message events
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/state
*
* Each call to `register()` attaches its own `statechange` listener. The
* `onStateChange` handler is captured in a local at listener-creation time,
* so re-registering with a different handler only affects its own listener
* and never re-targets a previously attached one.
**/
_setupListeners() {
const onStateChange = this._onStateChange;
this.serviceWorker.addEventListener('statechange', async () => {
const currentState = this.serviceWorker.state;
this._logger.debug(`ServiceWorker statechange: ${currentState}`);
// Notify a consumer-provided handler, isolating any error it throws so
// it cannot surface as an unhandled rejection from the listener.
await this._notifyStateChange(onStateChange, currentState);
// When no handler is provided, fall back to the built-in (deprecated)
// Pinpoint auto-recording. A supplied handler overrides it, preventing
// double-recording of the same state change.
if (!onStateChange) {
const { appId, region, bufferSize, flushInterval, flushSize, resendLimit, } = singleton_1.Amplify.getConfig().Analytics?.Pinpoint ?? {};
const { credentials } = await (0, singleton_1.fetchAuthSession)();
if (appId && region && credentials) {
// Pinpoint is configured, record an event
(0, pinpoint_1.record)({
appId,
region,
category: 'Core',
credentials,
bufferSize,
flushInterval,
flushSize,
resendLimit,
event: {
name: 'ServiceWorker',
attributes: {
state: currentState,
},
},
});
}
}
});
// A worker that is already in a state when this listener is attached
// (e.g. `activated` on a repeat visit, where `register()` resolves via
// the `registration.active` branch) emits no `statechange` event, so the
// consumer hook would otherwise never observe the current state. Notify
// the hook once with the current state to close that gap. The built-in
// Pinpoint path is intentionally NOT invoked here: doing so would change
// existing (no-hook) behavior, which must remain identical to today.
const initialState = this.serviceWorker.state;
if (onStateChange && initialState) {
this._logger.debug(`ServiceWorker initial state: ${initialState}`);
this._notifyStateChange(onStateChange, initialState).catch(() => {
// _notifyStateChange already logs handler errors; this catch only
// satisfies the no-floating-promises contract for the fire-and-forget
// initial emit.
});
}
this.serviceWorker.addEventListener('message', event => {
this._logger.debug(`ServiceWorker message event: ${event}`);
});
}
/**
* Invoke the consumer `onStateChange` handler with the given state, isolating
* any error it throws (or rejects with, for an async handler) so it cannot
* surface as an unhandled rejection. Awaiting the handler means a rejected
* promise from an async handler is caught here too; `await undefined`
* resolves immediately for sync or absent handlers.
*/
async _notifyStateChange(onStateChange, state) {
try {
await onStateChange?.(state);
}
catch (e) {
this._logger.error('onStateChange handler threw', e);
}
}
}
exports.ServiceWorkerClass = ServiceWorkerClass;
//# sourceMappingURL=ServiceWorker.js.map