feat(service-worker): introduce the @angular/service-worker package (#19274)

This service worker is a conceptual derivative of the existing @angular/service-worker maintained at github.com/angular/mobile-toolkit, but has been rewritten to support use across a much wider variety of applications.

Entrypoints include:

@angular/service-worker: a library for use within Angular client apps to communicate with the service worker.
@angular/service-worker/gen: a library for generating ngsw.json files from glob-based SW config files.
@angular/service-worker/ngsw-worker.js: the bundled service worker script itself.
@angular/service-worker/ngsw-cli.js: a CLI tool for generating ngsw.json files from glob-based SW config files.
This commit is contained in:
Alex Rickabaugh
2017-09-28 16:18:12 -07:00
committed by Victor Berchet
parent 7c1d3e0f5a
commit d442b6855f
63 changed files with 6722 additions and 8 deletions

View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. 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 Inc. 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
*/
export {ServiceWorkerModule} from './module';
export {SwPush} from './push';
export {SwUpdate} from './update';

View File

@ -0,0 +1,180 @@
/**
* @license
* Copyright Google Inc. 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
*/
import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {Observable} from 'rxjs/Observable';
import {ConnectableObservable} from 'rxjs/observable/ConnectableObservable';
import {concat as obs_concat} from 'rxjs/observable/concat';
import {defer as obs_defer} from 'rxjs/observable/defer';
import {fromEvent as obs_fromEvent} from 'rxjs/observable/fromEvent';
import {of as obs_of} from 'rxjs/observable/of';
import {_throw as obs_throw} from 'rxjs/observable/throw';
import {_do as op_do} from 'rxjs/operator/do';
import {filter as op_filter} from 'rxjs/operator/filter';
import {map as op_map} from 'rxjs/operator/map';
import {publish as op_publish} from 'rxjs/operator/publish';
import {startWith as op_startWith} from 'rxjs/operator/startWith';
import {switchMap as op_switchMap} from 'rxjs/operator/switchMap';
import {take as op_take} from 'rxjs/operator/take';
import {toPromise as op_toPromise} from 'rxjs/operator/toPromise';
const ERR_SW_NOT_SUPPORTED = 'Service workers are not supported by this browser';
export interface Version {
hash: string;
appData?: Object;
}
/**
* @experimental
*/
export interface UpdateAvailableEvent {
type: 'UPDATE_AVAILABLE';
current: Version;
available: Version;
}
/**
* @experimental
*/
export interface UpdateActivatedEvent {
type: 'UPDATE_ACTIVATED';
previous?: Version;
current: Version;
}
export type IncomingEvent = UpdateAvailableEvent | UpdateActivatedEvent;
interface TypedEvent {
type: string;
}
interface StatusEvent {
type: 'STATUS';
nonce: number;
status: boolean;
error?: string;
}
function errorObservable(message: string): Observable<any> {
return obs_defer(() => obs_throw(new Error(message)));
}
/**
* @experimental
*/
export class NgswCommChannel {
/**
* @internal
*/
readonly worker: Observable<ServiceWorker>;
/**
* @internal
*/
readonly registration: Observable<ServiceWorkerRegistration>;
/**
* @internal
*/
readonly events: Observable<IncomingEvent>;
constructor(serviceWorker: ServiceWorkerContainer|undefined) {
if (!serviceWorker) {
this.worker = this.events = errorObservable(ERR_SW_NOT_SUPPORTED);
} else {
const controllerChangeEvents =
<Observable<any>>(obs_fromEvent(serviceWorker, 'controllerchange'));
const controllerChanges = <Observable<ServiceWorker|null>>(
op_map.call(controllerChangeEvents, () => serviceWorker.controller));
const currentController =
<Observable<ServiceWorker|null>>(obs_defer(() => obs_of(serviceWorker.controller)));
const controllerWithChanges =
<Observable<ServiceWorker|null>>(obs_concat(currentController, controllerChanges));
this.worker = <Observable<ServiceWorker>>(
op_filter.call(controllerWithChanges, (c: ServiceWorker) => !!c));
this.registration = <Observable<ServiceWorkerRegistration>>(
op_switchMap.call(this.worker, () => serviceWorker.getRegistration()));
const rawEvents = <Observable<MessageEvent>>(op_switchMap.call(
this.registration, (reg: ServiceWorkerRegistration) => obs_fromEvent(reg, 'message')));
const rawEventPayload =
<Observable<Object>>(op_map.call(rawEvents, (event: MessageEvent) => event.data));
const eventsUnconnected = <Observable<IncomingEvent>>(
op_filter.call(rawEventPayload, (event: Object) => !!event && !!(event as any)['type']));
const events = <ConnectableObservable<IncomingEvent>>(op_publish.call(eventsUnconnected));
this.events = events;
events.connect();
}
}
/**
* @internal
*/
postMessage(action: string, payload: Object): Promise<void> {
const worker = op_take.call(this.worker, 1);
const sideEffect = op_do.call(worker, (sw: ServiceWorker) => {
sw.postMessage({
action, ...payload,
});
});
return <Promise<void>>(op_toPromise.call(sideEffect).then(() => undefined));
}
/**
* @internal
*/
postMessageWithStatus(type: string, payload: Object, nonce: number): Promise<void> {
const waitForStatus = this.waitForStatus(nonce);
const postMessage = this.postMessage(type, payload);
return Promise.all([waitForStatus, postMessage]).then(() => undefined);
}
/**
* @internal
*/
generateNonce(): number { return Math.round(Math.random() * 10000000); }
/**
* @internal
*/
eventsOfType<T>(type: string): Observable<T> {
return <Observable<T>>(
op_filter.call(this.events, (event: T & TypedEvent) => { return event.type === type; }));
}
/**
* @internal
*/
nextEventOfType<T>(type: string): Observable<T> {
return <Observable<T>>(op_take.call(this.eventsOfType(type), 1));
}
/**
* @internal
*/
waitForStatus(nonce: number): Promise<void> {
const statusEventsWithNonce = <Observable<StatusEvent>>(
op_filter.call(this.eventsOfType('STATUS'), (event: StatusEvent) => event.nonce === nonce));
const singleStatusEvent = <Observable<StatusEvent>>(op_take.call(statusEventsWithNonce, 1));
const mapErrorAndValue =
<Observable<void>>(op_map.call(singleStatusEvent, (event: StatusEvent) => {
if (event.status) {
return undefined;
}
throw new Error(event.error !);
}));
return op_toPromise.call(mapErrorAndValue);
}
}

View File

@ -0,0 +1,66 @@
/**
* @license
* Copyright Google Inc. 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
*/
import {APP_INITIALIZER, ApplicationRef, Inject, InjectionToken, Injector, ModuleWithProviders, NgModule} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {filter as op_filter} from 'rxjs/operator/filter';
import {take as op_take} from 'rxjs/operator/take';
import {toPromise as op_toPromise} from 'rxjs/operator/toPromise';
import {NgswCommChannel} from './low_level';
import {SwPush} from './push';
import {SwUpdate} from './update';
export const SCRIPT = new InjectionToken<string>('NGSW_REGISTER_SCRIPT');
export const OPTS = new InjectionToken<Object>('NGSW_REGISTER_OPTIONS');
export function ngswAppInitializer(
injector: Injector, script: string, options: RegistrationOptions): Function {
const initializer = () => {
const app = injector.get<ApplicationRef>(ApplicationRef);
if (!('serviceWorker' in navigator)) {
return;
}
const onStable =
op_filter.call(app.isStable, (stable: boolean) => !!stable) as Observable<boolean>;
const isStable = op_take.call(onStable, 1) as Observable<boolean>;
const whenStable = op_toPromise.call(isStable) as Promise<boolean>;
return whenStable.then(() => navigator.serviceWorker.register(script, options))
.then(() => undefined) as Promise<void>;
};
return initializer;
}
export function ngswCommChannelFactory(): NgswCommChannel {
return new NgswCommChannel(navigator.serviceWorker);
}
/**
* @experimental
*/
@NgModule({
providers: [SwPush, SwUpdate],
})
export class ServiceWorkerModule {
static register(script: string, opts: RegistrationOptions = {}): ModuleWithProviders {
return {
ngModule: ServiceWorkerModule,
providers: [
{provide: SCRIPT, useValue: script},
{provide: OPTS, useValue: opts},
{provide: NgswCommChannel, useFactory: ngswCommChannelFactory},
{
provide: APP_INITIALIZER,
useFactory: ngswAppInitializer,
deps: [Injector, SCRIPT, OPTS],
multi: true,
},
],
};
}
}

View File

@ -0,0 +1,82 @@
/**
* @license
* Copyright Google Inc. 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
*/
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {merge as obs_merge} from 'rxjs/observable/merge';
import {map as op_map} from 'rxjs/operator/map';
import {switchMap as op_switchMap} from 'rxjs/operator/switchMap';
import {take as op_take} from 'rxjs/operator/take';
import {toPromise as op_toPromise} from 'rxjs/operator/toPromise';
import {NgswCommChannel} from './low_level';
/**
* Subscribe and listen to push notifications from the Service Worker.
*
* @experimental
*/
export class SwPush {
readonly messages: Observable<object>;
readonly subscription: Observable<PushSubscription|null>;
private pushManager: Observable<PushManager>;
private subscriptionChanges: Subject<PushSubscription|null> =
new Subject<PushSubscription|null>();
constructor(private sw: NgswCommChannel) {
this.messages =
op_map.call(this.sw.eventsOfType('PUSH'), (message: {data: object}) => message.data);
this.pushManager = <Observable<PushManager>>(op_map.call(
this.sw.registration,
(registration: ServiceWorkerRegistration) => { return registration.pushManager; }));
const workerDrivenSubscriptions = <Observable<PushSubscription|null>>(op_switchMap.call(
this.pushManager, (pm: PushManager) => pm.getSubscription().then(sub => { return sub; })));
this.subscription = obs_merge.call(workerDrivenSubscriptions, this.subscriptionChanges);
}
requestSubscription(options: {serverPublicKey: string}): Promise<PushSubscription> {
const pushOptions: PushSubscriptionOptionsInit = {userVisibleOnly: true};
let key = atob(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;
const subscribe = <Observable<PushSubscription>>(
op_switchMap.call(this.pushManager, (pm: PushManager) => pm.subscribe(pushOptions)));
const subscribeOnce = op_take.call(subscribe, 1);
return (op_toPromise.call(subscribeOnce) as Promise<PushSubscription>).then(sub => {
this.subscriptionChanges.next(sub);
return sub;
});
}
unsubscribe(): Promise<void> {
const unsubscribe = op_switchMap.call(this.subscription, (sub: PushSubscription | null) => {
if (sub !== null) {
return sub.unsubscribe().then(success => {
if (success) {
this.subscriptionChanges.next(null);
return undefined;
} else {
throw new Error('Unsubscribe failed!');
}
});
} else {
throw new Error('Not subscribed to push notifications.');
}
});
const unsubscribeOnce = op_take.call(unsubscribe, 1);
return op_toPromise.call(unsubscribeOnce) as Promise<void>;
}
}

View File

@ -0,0 +1,42 @@
/**
* @license
* Copyright Google Inc. 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
*/
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {defer as obs_defer} from 'rxjs/observable/defer';
import {map as op_map} from 'rxjs/operator/map';
import {NgswCommChannel, UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
/**
* Subscribe to update notifications from the Service Worker, trigger update
* checks, and forcibly activate updates.
*
* @experimental
*/
@Injectable()
export class SwUpdate {
readonly available: Observable<UpdateAvailableEvent>;
readonly activated: Observable<UpdateActivatedEvent>;
constructor(private sw: NgswCommChannel) {
this.available = this.sw.eventsOfType('UPDATE_AVAILABLE');
this.activated = this.sw.eventsOfType('UPDATE_ACTIVATED');
}
checkForUpdate(): Promise<void> {
const statusNonce = this.sw.generateNonce();
return this.sw.postMessageWithStatus('CHECK_FOR_UPDATES', {statusNonce}, statusNonce);
}
activateUpdate(): Promise<void> {
const statusNonce = this.sw.generateNonce();
return this.sw.postMessageWithStatus('ACTIVATE_UPDATE', {statusNonce}, statusNonce);
}
}