2017-09-28 16:18:12 -07:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2017-11-12 12:39:27 +13:00
|
|
|
import {isPlatformBrowser} from '@angular/common';
|
|
|
|
import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
|
2017-09-28 16:18:12 -07:00
|
|
|
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';
|
|
|
|
|
2017-11-30 08:22:41 -08:00
|
|
|
export const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not supported by this browser';
|
2017-09-28 16:18:12 -07:00
|
|
|
|
|
|
|
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>;
|
|
|
|
|
2017-11-12 12:39:27 +13:00
|
|
|
constructor(
|
|
|
|
private serviceWorker: ServiceWorkerContainer|undefined,
|
|
|
|
@Inject(PLATFORM_ID) platformId: string) {
|
|
|
|
if (!serviceWorker || !isPlatformBrowser(platformId)) {
|
|
|
|
this.serviceWorker = undefined;
|
2017-11-30 08:22:41 -08:00
|
|
|
this.worker = this.events = this.registration = errorObservable(ERR_SW_NOT_SUPPORTED);
|
2017-09-28 16:18:12 -07:00
|
|
|
} 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()));
|
|
|
|
|
2017-10-26 10:29:36 -07:00
|
|
|
const rawEvents = obs_fromEvent(serviceWorker, 'message');
|
2017-09-28 16:18:12 -07:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2017-11-30 08:22:41 -08:00
|
|
|
|
|
|
|
get isEnabled(): boolean { return !!this.serviceWorker; }
|
2017-09-28 16:18:12 -07:00
|
|
|
}
|