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
|
|
|
|
|
*/
|
|
|
|
|
|
2018-02-27 17:06:06 -05:00
|
|
|
import {ConnectableObservable, Observable, concat, defer, fromEvent, of , throwError} from 'rxjs';
|
|
|
|
|
import {filter, map, publish, switchMap, take, tap} from 'rxjs/operators';
|
2017-09-28 16:18:12 -07:00
|
|
|
|
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;
|
|
|
|
|
|
2018-02-27 17:06:06 -05:00
|
|
|
export interface TypedEvent { type: string; }
|
2017-09-28 16:18:12 -07:00
|
|
|
|
|
|
|
|
interface StatusEvent {
|
|
|
|
|
type: 'STATUS';
|
|
|
|
|
nonce: number;
|
|
|
|
|
status: boolean;
|
|
|
|
|
error?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function errorObservable(message: string): Observable<any> {
|
2018-02-27 17:06:06 -05:00
|
|
|
return defer(() => throwError(new Error(message)));
|
2017-09-28 16:18:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @experimental
|
|
|
|
|
*/
|
|
|
|
|
export class NgswCommChannel {
|
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
|
|
|
|
readonly worker: Observable<ServiceWorker>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
|
|
|
|
readonly registration: Observable<ServiceWorkerRegistration>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2018-02-27 17:06:06 -05:00
|
|
|
readonly events: Observable<TypedEvent>;
|
2017-09-28 16:18:12 -07:00
|
|
|
|
2018-05-10 00:11:43 +09:00
|
|
|
constructor(private serviceWorker: ServiceWorkerContainer|undefined) {
|
|
|
|
|
if (!serviceWorker) {
|
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 =
|
2018-02-27 17:06:06 -05:00
|
|
|
<Observable<any>>(fromEvent(serviceWorker, 'controllerchange'));
|
2017-09-28 16:18:12 -07:00
|
|
|
const controllerChanges = <Observable<ServiceWorker|null>>(
|
2018-02-27 17:06:06 -05:00
|
|
|
controllerChangeEvents.pipe(map(() => serviceWorker.controller)));
|
2017-09-28 16:18:12 -07:00
|
|
|
|
|
|
|
|
const currentController =
|
2018-02-27 17:06:06 -05:00
|
|
|
<Observable<ServiceWorker|null>>(defer(() => of (serviceWorker.controller)));
|
2017-09-28 16:18:12 -07:00
|
|
|
|
|
|
|
|
const controllerWithChanges =
|
2018-02-27 17:06:06 -05:00
|
|
|
<Observable<ServiceWorker|null>>(concat(currentController, controllerChanges));
|
2017-09-28 16:18:12 -07:00
|
|
|
this.worker = <Observable<ServiceWorker>>(
|
2018-02-27 17:06:06 -05:00
|
|
|
controllerWithChanges.pipe(filter((c: ServiceWorker) => !!c)));
|
2017-09-28 16:18:12 -07:00
|
|
|
|
|
|
|
|
this.registration = <Observable<ServiceWorkerRegistration>>(
|
2018-02-27 17:06:06 -05:00
|
|
|
this.worker.pipe(switchMap(() => serviceWorker.getRegistration())));
|
2017-09-28 16:18:12 -07:00
|
|
|
|
2018-02-27 17:06:06 -05:00
|
|
|
const rawEvents = fromEvent(serviceWorker, 'message');
|
2017-09-28 16:18:12 -07:00
|
|
|
|
2018-02-27 17:06:06 -05:00
|
|
|
const rawEventPayload = rawEvents.pipe(map((event: MessageEvent) => event.data));
|
|
|
|
|
const eventsUnconnected =
|
|
|
|
|
(rawEventPayload.pipe(filter((event: Object) => !!event && !!(event as any)['type'])));
|
|
|
|
|
const events = eventsUnconnected.pipe(publish()) as ConnectableObservable<IncomingEvent>;
|
2017-09-28 16:18:12 -07:00
|
|
|
this.events = events;
|
|
|
|
|
events.connect();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
|
|
|
|
postMessage(action: string, payload: Object): Promise<void> {
|
2018-02-27 17:06:06 -05:00
|
|
|
return this.worker
|
|
|
|
|
.pipe(take(1), tap((sw: ServiceWorker) => {
|
|
|
|
|
sw.postMessage({
|
|
|
|
|
action, ...payload,
|
|
|
|
|
});
|
|
|
|
|
}))
|
|
|
|
|
.toPromise()
|
|
|
|
|
.then(() => undefined);
|
2017-09-28 16:18:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
2018-02-27 17:06:06 -05:00
|
|
|
// TODO(i): the typings and casts in this method are wonky, we should revisit it and make the
|
|
|
|
|
// types flow correctly
|
|
|
|
|
eventsOfType<T extends TypedEvent>(type: string): Observable<T> {
|
|
|
|
|
return <Observable<T>>this.events.pipe(filter((event) => { return event.type === type; }));
|
2017-09-28 16:18:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2018-02-27 17:06:06 -05:00
|
|
|
// TODO(i): the typings and casts in this method are wonky, we should revisit it and make the
|
|
|
|
|
// types flow correctly
|
|
|
|
|
nextEventOfType<T extends TypedEvent>(type: string): Observable<T> {
|
|
|
|
|
return <Observable<T>>(this.eventsOfType(type).pipe(take(1)));
|
2017-09-28 16:18:12 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
|
|
|
|
waitForStatus(nonce: number): Promise<void> {
|
2018-02-27 17:06:06 -05:00
|
|
|
return this.eventsOfType<StatusEvent>('STATUS')
|
|
|
|
|
.pipe(
|
|
|
|
|
filter((event: StatusEvent) => event.nonce === nonce), take(1),
|
|
|
|
|
map((event: StatusEvent) => {
|
|
|
|
|
if (event.status) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
throw new Error(event.error !);
|
|
|
|
|
}))
|
|
|
|
|
.toPromise();
|
2017-09-28 16:18:12 -07:00
|
|
|
}
|
2017-11-30 08:22:41 -08:00
|
|
|
|
|
|
|
|
get isEnabled(): boolean { return !!this.serviceWorker; }
|
2018-02-27 17:06:06 -05:00
|
|
|
}
|