@ -26,7 +26,7 @@ export class NodeFilesystem implements Filesystem {
|
||||
return entries.filter((entry: any) => entry.stats.isDirectory())
|
||||
.map((entry: any) => path.posix.join(_path, entry.entry))
|
||||
.reduce(
|
||||
async(list: Promise<string[]>, subdir: string) =>
|
||||
async (list: Promise<string[]>, subdir: string) =>
|
||||
(await list).concat(await this.list(subdir)),
|
||||
Promise.resolve(files));
|
||||
}
|
||||
@ -47,5 +47,7 @@ export class NodeFilesystem implements Filesystem {
|
||||
fs.writeFileSync(file, contents);
|
||||
}
|
||||
|
||||
private canonical(_path: string): string { return path.posix.join(this.base, _path); }
|
||||
private canonical(_path: string): string {
|
||||
return path.posix.join(this.base, _path);
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const configParsed = JSON.parse(fs.readFileSync(config).toString());
|
||||
const filesystem = new NodeFilesystem(distDir);
|
||||
const gen = new Generator(filesystem, baseHref);
|
||||
|
||||
(async() => {
|
||||
(async () => {
|
||||
const control = await gen.process(configParsed);
|
||||
await filesystem.write('/ngsw.json', JSON.stringify(control, null, 2));
|
||||
})();
|
@ -133,7 +133,7 @@ function arrayBufferToWords32(buffer: ArrayBuffer, endian: Endian): number[] {
|
||||
return words32;
|
||||
}
|
||||
|
||||
function byteAt(str: string | Uint8Array, index: number): number {
|
||||
function byteAt(str: string|Uint8Array, index: number): number {
|
||||
if (typeof str === 'string') {
|
||||
return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
|
||||
} else {
|
||||
@ -141,7 +141,7 @@ function byteAt(str: string | Uint8Array, index: number): number {
|
||||
}
|
||||
}
|
||||
|
||||
function wordAt(str: string | Uint8Array, index: number, endian: Endian): number {
|
||||
function wordAt(str: string|Uint8Array, index: number, endian: Endian): number {
|
||||
let word = 0;
|
||||
if (endian === Endian.Big) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
|
@ -34,17 +34,18 @@ export class Generator {
|
||||
configVersion: 1,
|
||||
timestamp: Date.now(),
|
||||
appData: config.appData,
|
||||
index: joinUrls(this.baseHref, config.index), assetGroups,
|
||||
index: joinUrls(this.baseHref, config.index),
|
||||
assetGroups,
|
||||
dataGroups: this.processDataGroups(config),
|
||||
hashTable: withOrderedKeys(unorderedHashTable),
|
||||
navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
|
||||
};
|
||||
}
|
||||
|
||||
private async processAssetGroups(config: Config, hashTable: {[file: string]: string | undefined}):
|
||||
private async processAssetGroups(config: Config, hashTable: {[file: string]: string|undefined}):
|
||||
Promise<Object[]> {
|
||||
const seenMap = new Set<string>();
|
||||
return Promise.all((config.assetGroups || []).map(async(group) => {
|
||||
return Promise.all((config.assetGroups || []).map(async (group) => {
|
||||
if ((group.resources as any).versionedFiles) {
|
||||
throw new Error(
|
||||
`Asset-group '${group.name}' in 'ngsw-config.json' uses the 'versionedFiles' option, ` +
|
||||
@ -58,7 +59,7 @@ export class Generator {
|
||||
matchedFiles.forEach(file => seenMap.add(file));
|
||||
|
||||
// Add the hashes.
|
||||
await matchedFiles.reduce(async(previous, file) => {
|
||||
await matchedFiles.reduce(async (previous, file) => {
|
||||
await previous;
|
||||
const hash = await this.fs.hash(file);
|
||||
hashTable[joinUrls(this.baseHref, file)] = hash;
|
||||
@ -143,8 +144,8 @@ function joinUrls(a: string, b: string): string {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
function withOrderedKeys<T extends{[key: string]: any}>(unorderedObj: T): T {
|
||||
const orderedObj = {} as{[key: string]: any};
|
||||
function withOrderedKeys<T extends {[key: string]: any}>(unorderedObj: T): T {
|
||||
const orderedObj = {} as {[key: string]: any};
|
||||
Object.keys(unorderedObj).sort().forEach(key => orderedObj[key] = unorderedObj[key]);
|
||||
return orderedObj as T;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export function globToRegex(glob: string, literalQuestionMark = false): string {
|
||||
const segments = glob.split('/').reverse();
|
||||
let regex: string = '';
|
||||
while (segments.length > 0) {
|
||||
const segment = segments.pop() !;
|
||||
const segment = segments.pop()!;
|
||||
if (segment === '**') {
|
||||
if (segments.length > 0) {
|
||||
regex += WILD_OPEN;
|
||||
|
@ -51,6 +51,8 @@ export interface DataGroup {
|
||||
urls: Glob[];
|
||||
version?: number;
|
||||
cacheConfig: {
|
||||
maxSize: number; maxAge: Duration; timeout?: Duration; strategy?: 'freshness' | 'performance';
|
||||
maxSize: number; maxAge: Duration;
|
||||
timeout?: Duration;
|
||||
strategy?: 'freshness' | 'performance';
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import {MockFilesystem} from '../testing/mock';
|
||||
describe('Generator', () => {
|
||||
beforeEach(() => spyOn(Date, 'now').and.returnValue(1234567890123));
|
||||
|
||||
it('generates a correct config', async() => {
|
||||
it('generates a correct config', async () => {
|
||||
const fs = new MockFilesystem({
|
||||
'/index.html': 'This is a test',
|
||||
'/main.css': 'This is a CSS file',
|
||||
@ -125,7 +125,7 @@ describe('Generator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses default `navigationUrls` if not provided', async() => {
|
||||
it('uses default `navigationUrls` if not provided', async () => {
|
||||
const fs = new MockFilesystem({
|
||||
'/index.html': 'This is a test',
|
||||
});
|
||||
@ -151,7 +151,7 @@ describe('Generator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the obsolete `versionedFiles` is used', async() => {
|
||||
it('throws if the obsolete `versionedFiles` is used', async () => {
|
||||
const fs = new MockFilesystem({
|
||||
'/index.html': 'This is a test',
|
||||
'/main.js': 'This is a JS file',
|
||||
|
@ -12,17 +12,23 @@ import {Filesystem} from '../src/filesystem';
|
||||
export class MockFilesystem implements Filesystem {
|
||||
private files = new Map<string, string>();
|
||||
|
||||
constructor(files: {[name: string]: string | undefined}) {
|
||||
Object.keys(files).forEach(path => this.files.set(path, files[path] !));
|
||||
constructor(files: {[name: string]: string|undefined}) {
|
||||
Object.keys(files).forEach(path => this.files.set(path, files[path]!));
|
||||
}
|
||||
|
||||
async list(dir: string): Promise<string[]> {
|
||||
return Array.from(this.files.keys()).filter(path => path.startsWith(dir));
|
||||
}
|
||||
|
||||
async read(path: string): Promise<string> { return this.files.get(path) !; }
|
||||
async read(path: string): Promise<string> {
|
||||
return this.files.get(path)!;
|
||||
}
|
||||
|
||||
async hash(path: string): Promise<string> { return sha1(this.files.get(path) !); }
|
||||
async hash(path: string): Promise<string> {
|
||||
return sha1(this.files.get(path)!);
|
||||
}
|
||||
|
||||
async write(path: string, contents: string): Promise<void> { this.files.set(path, contents); }
|
||||
async write(path: string, contents: string): Promise<void> {
|
||||
this.files.set(path, contents);
|
||||
}
|
||||
}
|
@ -8,10 +8,13 @@
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
self.addEventListener('install', event => { self.skipWaiting(); });
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
self.registration.unregister().then(
|
||||
() => { console.log('NGSW Safety Worker - unregistered old service worker'); });
|
||||
self.registration.unregister().then(() => {
|
||||
console.log('NGSW Safety Worker - unregistered old service worker');
|
||||
});
|
||||
});
|
||||
|
@ -7,12 +7,12 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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 {UpdateActivatedEvent, UpdateAvailableEvent} from './low_level';
|
||||
export {ServiceWorkerModule, SwRegistrationOptions} from './module';
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ConnectableObservable, Observable, concat, defer, fromEvent, of , throwError} from 'rxjs';
|
||||
import {concat, ConnectableObservable, defer, fromEvent, Observable, of, throwError} from 'rxjs';
|
||||
import {filter, map, publish, switchMap, take, tap} from 'rxjs/operators';
|
||||
|
||||
export const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not supported by this browser';
|
||||
@ -41,9 +41,11 @@ export interface PushEvent {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export type IncomingEvent = UpdateAvailableEvent | UpdateActivatedEvent;
|
||||
export type IncomingEvent = UpdateAvailableEvent|UpdateActivatedEvent;
|
||||
|
||||
export interface TypedEvent { type: string; }
|
||||
export interface TypedEvent {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface StatusEvent {
|
||||
type: 'STATUS';
|
||||
@ -73,7 +75,7 @@ export class NgswCommChannel {
|
||||
} else {
|
||||
const controllerChangeEvents = fromEvent(serviceWorker, 'controllerchange');
|
||||
const controllerChanges = controllerChangeEvents.pipe(map(() => serviceWorker.controller));
|
||||
const currentController = defer(() => of (serviceWorker.controller));
|
||||
const currentController = defer(() => of(serviceWorker.controller));
|
||||
const controllerWithChanges = concat(currentController, controllerChanges);
|
||||
|
||||
this.worker = controllerWithChanges.pipe(filter((c): c is ServiceWorker => !!c));
|
||||
@ -95,7 +97,8 @@ export class NgswCommChannel {
|
||||
return this.worker
|
||||
.pipe(take(1), tap((sw: ServiceWorker) => {
|
||||
sw.postMessage({
|
||||
action, ...payload,
|
||||
action,
|
||||
...payload,
|
||||
});
|
||||
}))
|
||||
.toPromise()
|
||||
@ -108,7 +111,9 @@ export class NgswCommChannel {
|
||||
return Promise.all([waitForStatus, postMessage]).then(() => undefined);
|
||||
}
|
||||
|
||||
generateNonce(): number { return Math.round(Math.random() * 10000000); }
|
||||
generateNonce(): number {
|
||||
return Math.round(Math.random() * 10000000);
|
||||
}
|
||||
|
||||
eventsOfType<T extends TypedEvent>(type: T['type']): Observable<T> {
|
||||
const filterFn = (event: TypedEvent): event is T => event.type === type;
|
||||
@ -125,10 +130,12 @@ export class NgswCommChannel {
|
||||
if (event.status) {
|
||||
return undefined;
|
||||
}
|
||||
throw new Error(event.error !);
|
||||
throw new Error(event.error!);
|
||||
}))
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
get isEnabled(): boolean { return !!this.serviceWorker; }
|
||||
get isEnabled(): boolean {
|
||||
return !!this.serviceWorker;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
import {APP_INITIALIZER, ApplicationRef, InjectionToken, Injector, ModuleWithProviders, NgModule, PLATFORM_ID} from '@angular/core';
|
||||
import {Observable, of } from 'rxjs';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {delay, filter, take} from 'rxjs/operators';
|
||||
|
||||
import {NgswCommChannel} from './low_level';
|
||||
@ -99,10 +99,10 @@ export function ngswAppInitializer(
|
||||
const [strategy, ...args] = (options.registrationStrategy || 'registerWhenStable').split(':');
|
||||
switch (strategy) {
|
||||
case 'registerImmediately':
|
||||
readyToRegister$ = of (null);
|
||||
readyToRegister$ = of(null);
|
||||
break;
|
||||
case 'registerWithDelay':
|
||||
readyToRegister$ = of (null).pipe(delay(+args[0] || 0));
|
||||
readyToRegister$ = of(null).pipe(delay(+args[0] || 0));
|
||||
break;
|
||||
case 'registerWhenStable':
|
||||
const appRef = injector.get<ApplicationRef>(ApplicationRef);
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NEVER, Observable, Subject, merge} from 'rxjs';
|
||||
import {merge, NEVER, Observable, Subject} from 'rxjs';
|
||||
import {map, switchMap, take} from 'rxjs/operators';
|
||||
|
||||
import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level';
|
||||
@ -15,22 +15,25 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level';
|
||||
|
||||
/**
|
||||
* Subscribe and listen to
|
||||
* [Web Push Notifications](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices)
|
||||
* through Angular Service Worker.
|
||||
* [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>
|
||||
* <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>
|
||||
* <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.
|
||||
@ -61,7 +64,8 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level';
|
||||
* ```
|
||||
*
|
||||
* Only `title` is required. See `Notification`
|
||||
* [instance properties](https://developer.mozilla.org/en-US/docs/Web/API/Notification#Instance_properties).
|
||||
* [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)
|
||||
@ -74,7 +78,8 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level';
|
||||
* 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>
|
||||
* <code-example path="service-worker/push/module.ts" region="subscribe-to-notification-clicks"
|
||||
* header="app.component.ts"></code-example>
|
||||
*
|
||||
* @see [Push Notifications](https://developers.google.com/web/fundamentals/codelabs/push-notifications/)
|
||||
* @see [Angular Push Notifications](https://blog.angular-university.io/angular-push-notifications/)
|
||||
@ -102,11 +107,12 @@ export class SwPush {
|
||||
*
|
||||
* [Mozilla Notification]: https://developer.mozilla.org/en-US/docs/Web/API/Notification
|
||||
*/
|
||||
readonly notificationClicks: Observable < {
|
||||
action: string;
|
||||
notification: NotificationOptions&{ title: string }
|
||||
}
|
||||
> ;
|
||||
readonly notificationClicks: Observable<{
|
||||
action: string; notification: NotificationOptions &
|
||||
{
|
||||
title: string
|
||||
}
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Emits the currently active
|
||||
@ -119,10 +125,12 @@ export class SwPush {
|
||||
* True if the Service Worker is enabled (supported by the browser and enabled via
|
||||
* `ServiceWorkerModule`).
|
||||
*/
|
||||
get isEnabled(): boolean { return this.sw.isEnabled; }
|
||||
get isEnabled(): boolean {
|
||||
return this.sw.isEnabled;
|
||||
}
|
||||
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private pushManager !: Observable<PushManager>;
|
||||
private pushManager!: Observable<PushManager>;
|
||||
private subscriptionChanges = new Subject<PushSubscription|null>();
|
||||
|
||||
constructor(private sw: NgswCommChannel) {
|
||||
@ -182,7 +190,7 @@ export class SwPush {
|
||||
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
|
||||
}
|
||||
|
||||
const doUnsubscribe = (sub: PushSubscription | null) => {
|
||||
const doUnsubscribe = (sub: PushSubscription|null) => {
|
||||
if (sub === null) {
|
||||
throw new Error('Not subscribed to push notifications.');
|
||||
}
|
||||
@ -199,5 +207,7 @@ export class SwPush {
|
||||
return this.subscription.pipe(take(1), switchMap(doUnsubscribe)).toPromise();
|
||||
}
|
||||
|
||||
private decodeBase64(input: string): string { return atob(input); }
|
||||
private decodeBase64(input: string): string {
|
||||
return atob(input);
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,9 @@ export class SwUpdate {
|
||||
* True if the Service Worker is enabled (supported by the browser and enabled via
|
||||
* `ServiceWorkerModule`).
|
||||
*/
|
||||
get isEnabled(): boolean { return this.sw.isEnabled; }
|
||||
get isEnabled(): boolean {
|
||||
return this.sw.isEnabled;
|
||||
}
|
||||
|
||||
constructor(private sw: NgswCommChannel) {
|
||||
if (!sw.isEnabled) {
|
||||
|
@ -20,129 +20,136 @@ import {Observable} from 'rxjs';
|
||||
import {take} from 'rxjs/operators';
|
||||
|
||||
(function() {
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dist = new MockFileSystemBuilder().addFile('/only.txt', 'this is only').build();
|
||||
const dist = new MockFileSystemBuilder().addFile('/only.txt', 'this is only').build();
|
||||
|
||||
const distUpdate = new MockFileSystemBuilder().addFile('/only.txt', 'this is only v2').build();
|
||||
const distUpdate = new MockFileSystemBuilder().addFile('/only.txt', 'this is only v2').build();
|
||||
|
||||
function obsToSinglePromise<T>(obs: Observable<T>): Promise<T> {
|
||||
return obs.pipe(take(1)).toPromise();
|
||||
}
|
||||
function obsToSinglePromise<T>(obs: Observable<T>): Promise<T> {
|
||||
return obs.pipe(take(1)).toPromise();
|
||||
}
|
||||
|
||||
const manifest: Manifest = {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
appData: {version: '1'},
|
||||
index: '/only.txt',
|
||||
assetGroups: [{
|
||||
name: 'assets',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: ['/only.txt'],
|
||||
patterns: [],
|
||||
}],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(dist),
|
||||
};
|
||||
const manifest: Manifest = {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
appData: {version: '1'},
|
||||
index: '/only.txt',
|
||||
assetGroups: [{
|
||||
name: 'assets',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: ['/only.txt'],
|
||||
patterns: [],
|
||||
}],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(dist),
|
||||
};
|
||||
|
||||
const manifestUpdate: Manifest = {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
appData: {version: '2'},
|
||||
index: '/only.txt',
|
||||
assetGroups: [{
|
||||
name: 'assets',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: ['/only.txt'],
|
||||
patterns: [],
|
||||
}],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(distUpdate),
|
||||
};
|
||||
const manifestUpdate: Manifest = {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
appData: {version: '2'},
|
||||
index: '/only.txt',
|
||||
assetGroups: [{
|
||||
name: 'assets',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: ['/only.txt'],
|
||||
patterns: [],
|
||||
}],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(distUpdate),
|
||||
};
|
||||
|
||||
const server = new MockServerStateBuilder().withStaticFiles(dist).withManifest(manifest).build();
|
||||
const server = new MockServerStateBuilder().withStaticFiles(dist).withManifest(manifest).build();
|
||||
|
||||
const serverUpdate =
|
||||
new MockServerStateBuilder().withStaticFiles(distUpdate).withManifest(manifestUpdate).build();
|
||||
const serverUpdate =
|
||||
new MockServerStateBuilder().withStaticFiles(distUpdate).withManifest(manifestUpdate).build();
|
||||
|
||||
|
||||
describe('ngsw + companion lib', () => {
|
||||
let mock: MockServiceWorkerContainer;
|
||||
let comm: NgswCommChannel;
|
||||
let reg: MockServiceWorkerRegistration;
|
||||
let scope: SwTestHarness;
|
||||
let driver: Driver;
|
||||
describe('ngsw + companion lib', () => {
|
||||
let mock: MockServiceWorkerContainer;
|
||||
let comm: NgswCommChannel;
|
||||
let reg: MockServiceWorkerRegistration;
|
||||
let scope: SwTestHarness;
|
||||
let driver: Driver;
|
||||
|
||||
beforeEach(async() => {
|
||||
// Fire up the client.
|
||||
mock = new MockServiceWorkerContainer();
|
||||
comm = new NgswCommChannel(mock as any);
|
||||
scope = new SwTestHarnessBuilder().withServerState(server).build();
|
||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||
beforeEach(async () => {
|
||||
// Fire up the client.
|
||||
mock = new MockServiceWorkerContainer();
|
||||
comm = new NgswCommChannel(mock as any);
|
||||
scope = new SwTestHarnessBuilder().withServerState(server).build();
|
||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||
|
||||
scope.clients.add('default');
|
||||
scope.clients.getMock('default') !.queue.subscribe(msg => { mock.sendMessage(msg); });
|
||||
|
||||
mock.messages.subscribe(msg => { scope.handleMessage(msg, 'default'); });
|
||||
mock.notificationClicks.subscribe((msg: Object) => { scope.handleMessage(msg, 'default'); });
|
||||
|
||||
mock.setupSw();
|
||||
reg = mock.mockRegistration !;
|
||||
|
||||
await Promise.all(scope.handleFetch(new MockRequest('/only.txt'), 'default'));
|
||||
await driver.initialized;
|
||||
scope.clients.add('default');
|
||||
scope.clients.getMock('default')!.queue.subscribe(msg => {
|
||||
mock.sendMessage(msg);
|
||||
});
|
||||
|
||||
it('communicates back and forth via update check', async() => {
|
||||
const update = new SwUpdate(comm);
|
||||
await update.checkForUpdate();
|
||||
mock.messages.subscribe(msg => {
|
||||
scope.handleMessage(msg, 'default');
|
||||
});
|
||||
mock.notificationClicks.subscribe((msg: Object) => {
|
||||
scope.handleMessage(msg, 'default');
|
||||
});
|
||||
|
||||
it('detects an actual update', async() => {
|
||||
const update = new SwUpdate(comm);
|
||||
scope.updateServerState(serverUpdate);
|
||||
mock.setupSw();
|
||||
reg = mock.mockRegistration!;
|
||||
|
||||
const gotUpdateNotice =
|
||||
(async() => { const notice = await obsToSinglePromise(update.available); })();
|
||||
await Promise.all(scope.handleFetch(new MockRequest('/only.txt'), 'default'));
|
||||
await driver.initialized;
|
||||
});
|
||||
|
||||
await update.checkForUpdate();
|
||||
await gotUpdateNotice;
|
||||
});
|
||||
it('communicates back and forth via update check', async () => {
|
||||
const update = new SwUpdate(comm);
|
||||
await update.checkForUpdate();
|
||||
});
|
||||
|
||||
it('receives push message notifications', async() => {
|
||||
const push = new SwPush(comm);
|
||||
scope.updateServerState(serverUpdate);
|
||||
it('detects an actual update', async () => {
|
||||
const update = new SwUpdate(comm);
|
||||
scope.updateServerState(serverUpdate);
|
||||
|
||||
const gotPushNotice = (async() => {
|
||||
const message = await obsToSinglePromise(push.messages);
|
||||
expect(message).toEqual({
|
||||
test: 'success',
|
||||
});
|
||||
})();
|
||||
const gotUpdateNotice = (async () => {
|
||||
const notice = await obsToSinglePromise(update.available);
|
||||
})();
|
||||
|
||||
await scope.handlePush({
|
||||
await update.checkForUpdate();
|
||||
await gotUpdateNotice;
|
||||
});
|
||||
|
||||
it('receives push message notifications', async () => {
|
||||
const push = new SwPush(comm);
|
||||
scope.updateServerState(serverUpdate);
|
||||
|
||||
const gotPushNotice = (async () => {
|
||||
const message = await obsToSinglePromise(push.messages);
|
||||
expect(message).toEqual({
|
||||
test: 'success',
|
||||
});
|
||||
await gotPushNotice;
|
||||
});
|
||||
|
||||
it('receives push message click events', async() => {
|
||||
const push = new SwPush(comm);
|
||||
scope.updateServerState(serverUpdate);
|
||||
|
||||
const gotNotificationClick = (async() => {
|
||||
const event: any = await obsToSinglePromise(push.notificationClicks);
|
||||
expect(event.action).toEqual('clicked');
|
||||
expect(event.notification.title).toEqual('This is a test');
|
||||
})();
|
||||
|
||||
await scope.handleClick({title: 'This is a test'}, 'clicked');
|
||||
await gotNotificationClick;
|
||||
})();
|
||||
|
||||
await scope.handlePush({
|
||||
test: 'success',
|
||||
});
|
||||
await gotPushNotice;
|
||||
});
|
||||
|
||||
it('receives push message click events', async () => {
|
||||
const push = new SwPush(comm);
|
||||
scope.updateServerState(serverUpdate);
|
||||
|
||||
const gotNotificationClick = (async () => {
|
||||
const event: any = await obsToSinglePromise(push.notificationClicks);
|
||||
expect(event.action).toEqual('clicked');
|
||||
expect(event.notification.title).toEqual('This is a test');
|
||||
})();
|
||||
|
||||
await scope.handleClick({title: 'This is a test'}, 'clicked');
|
||||
await gotNotificationClick;
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -16,7 +16,9 @@ export const patchDecodeBase64 = (proto: {decodeBase64: typeof atob}) => {
|
||||
const newDecodeBase64 = (input: string) => Buffer.from(input, 'base64').toString('binary');
|
||||
|
||||
proto.decodeBase64 = newDecodeBase64;
|
||||
unpatch = () => { proto.decodeBase64 = oldDecodeBase64; };
|
||||
unpatch = () => {
|
||||
proto.decodeBase64 = oldDecodeBase64;
|
||||
};
|
||||
}
|
||||
|
||||
return unpatch;
|
||||
@ -46,7 +48,9 @@ export class MockServiceWorkerContainer {
|
||||
}
|
||||
}
|
||||
|
||||
async register(url: string): Promise<void> { return; }
|
||||
async register(url: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async getRegistration(): Promise<ServiceWorkerRegistration> {
|
||||
return this.mockRegistration as any;
|
||||
@ -68,7 +72,9 @@ export class MockServiceWorkerContainer {
|
||||
export class MockServiceWorker {
|
||||
constructor(private mock: MockServiceWorkerContainer, readonly scriptURL: string) {}
|
||||
|
||||
postMessage(value: Object) { this.mock.messages.next(value); }
|
||||
postMessage(value: Object) {
|
||||
this.mock.messages.next(value);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockServiceWorkerRegistration {
|
||||
@ -78,14 +84,18 @@ export class MockServiceWorkerRegistration {
|
||||
export class MockPushManager {
|
||||
private subscription: PushSubscription|null = null;
|
||||
|
||||
getSubscription(): Promise<PushSubscription|null> { return Promise.resolve(this.subscription); }
|
||||
getSubscription(): Promise<PushSubscription|null> {
|
||||
return Promise.resolve(this.subscription);
|
||||
}
|
||||
|
||||
subscribe(options?: PushSubscriptionOptionsInit): Promise<PushSubscription> {
|
||||
this.subscription = new MockPushSubscription() as any;
|
||||
return Promise.resolve(this.subscription !);
|
||||
return Promise.resolve(this.subscription!);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockPushSubscription {
|
||||
unsubscribe(): Promise<boolean> { return Promise.resolve(true); }
|
||||
unsubscribe(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
@ -32,22 +32,30 @@ export class Adapter {
|
||||
/**
|
||||
* Wrapper around the `Response` constructor.
|
||||
*/
|
||||
newResponse(body: any, init?: ResponseInit) { return new Response(body, init); }
|
||||
newResponse(body: any, init?: ResponseInit) {
|
||||
return new Response(body, init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the `Headers` constructor.
|
||||
*/
|
||||
newHeaders(headers: {[name: string]: string}): Headers { return new Headers(headers); }
|
||||
newHeaders(headers: {[name: string]: string}): Headers {
|
||||
return new Headers(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a given object is an instance of `Client`.
|
||||
*/
|
||||
isClient(source: any): source is Client { return (source instanceof Client); }
|
||||
isClient(source: any): source is Client {
|
||||
return (source instanceof Client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current UNIX time in milliseconds.
|
||||
*/
|
||||
get time(): number { return Date.now(); }
|
||||
get time(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the pathname of a URL.
|
||||
@ -63,7 +71,9 @@ export class Adapter {
|
||||
* Wait for a given amount of time before completing a Promise.
|
||||
*/
|
||||
timeout(ms: number): Promise<void> {
|
||||
return new Promise<void>(resolve => { setTimeout(() => resolve(), ms); });
|
||||
return new Promise<void>(resolve => {
|
||||
setTimeout(() => resolve(), ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,9 @@ export interface CacheState {
|
||||
metadata?: UrlMetadata;
|
||||
}
|
||||
|
||||
export interface DebugLogger { log(value: string|Error, context?: string): void; }
|
||||
export interface DebugLogger {
|
||||
log(value: string|Error, context?: string): void;
|
||||
}
|
||||
|
||||
export interface DebugState {
|
||||
state: string;
|
||||
|
@ -57,7 +57,9 @@ export class AppVersion implements UpdateSource {
|
||||
*/
|
||||
private _okay = true;
|
||||
|
||||
get okay(): boolean { return this._okay; }
|
||||
get okay(): boolean {
|
||||
return this._okay;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private scope: ServiceWorkerGlobalScope, private adapter: Adapter, private database: Database,
|
||||
@ -117,7 +119,7 @@ export class AppVersion implements UpdateSource {
|
||||
// Fully initialize each asset group, in series. Starts with an empty Promise,
|
||||
// and waits for the previous groups to have been initialized before initializing
|
||||
// the next one in turn.
|
||||
await this.assetGroups.reduce<Promise<void>>(async(previous, group) => {
|
||||
await this.assetGroups.reduce<Promise<void>>(async (previous, group) => {
|
||||
// Wait for the previous groups to complete initialization. If there is a
|
||||
// failure, this will throw, and each subsequent group will throw, until the
|
||||
// whole sequence fails.
|
||||
@ -140,7 +142,7 @@ export class AppVersion implements UpdateSource {
|
||||
// the group list, keeping track of a possible response. If there is one, it gets passed
|
||||
// through, and if
|
||||
// not the next group is consulted to produce a candidate response.
|
||||
const asset = await this.assetGroups.reduce(async(potentialResponse, group) => {
|
||||
const asset = await this.assetGroups.reduce(async (potentialResponse, group) => {
|
||||
// Wait on the previous potential response. If it's not null, it should just be passed
|
||||
// through.
|
||||
const resp = await potentialResponse;
|
||||
@ -161,7 +163,7 @@ export class AppVersion implements UpdateSource {
|
||||
|
||||
// Perform the same reduction operation as above, but this time processing
|
||||
// the data caching groups.
|
||||
const data = await this.dataGroups.reduce(async(potentialResponse, group) => {
|
||||
const data = await this.dataGroups.reduce(async (potentialResponse, group) => {
|
||||
const resp = await potentialResponse;
|
||||
if (resp !== null) {
|
||||
return resp;
|
||||
@ -233,7 +235,7 @@ export class AppVersion implements UpdateSource {
|
||||
lookupResourceWithoutHash(url: string): Promise<CacheState|null> {
|
||||
// Limit the search to asset groups, and only scan the cache, don't
|
||||
// load resources from the network.
|
||||
return this.assetGroups.reduce(async(potentialResponse, group) => {
|
||||
return this.assetGroups.reduce(async (potentialResponse, group) => {
|
||||
const resp = await potentialResponse;
|
||||
if (resp !== null) {
|
||||
return resp;
|
||||
@ -249,13 +251,13 @@ export class AppVersion implements UpdateSource {
|
||||
* List all unhashed resources from all asset groups.
|
||||
*/
|
||||
previouslyCachedResources(): Promise<string[]> {
|
||||
return this.assetGroups.reduce(async(resources, group) => {
|
||||
return this.assetGroups.reduce(async (resources, group) => {
|
||||
return (await resources).concat(await group.unhashedResources());
|
||||
}, Promise.resolve<string[]>([]));
|
||||
}
|
||||
|
||||
async recentCacheStatus(url: string): Promise<UpdateCacheStatus> {
|
||||
return this.assetGroups.reduce(async(current, group) => {
|
||||
return this.assetGroups.reduce(async (current, group) => {
|
||||
const status = await current;
|
||||
if (status === UpdateCacheStatus.CACHED) {
|
||||
return status;
|
||||
@ -279,7 +281,9 @@ export class AppVersion implements UpdateSource {
|
||||
/**
|
||||
* Get the opaque application data which was provided with the manifest.
|
||||
*/
|
||||
get appData(): Object|null { return this.manifest.appData || null; }
|
||||
get appData(): Object|null {
|
||||
return this.manifest.appData || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a request accepts `text/html` (based on the `Accept` header).
|
||||
|
@ -9,7 +9,7 @@
|
||||
import {Adapter, Context} from './adapter';
|
||||
import {CacheState, UpdateCacheStatus, UpdateSource, UrlMetadata} from './api';
|
||||
import {Database, Table} from './database';
|
||||
import {SwCriticalError, errorToString} from './error';
|
||||
import {errorToString, SwCriticalError} from './error';
|
||||
import {IdleScheduler} from './idle';
|
||||
import {AssetGroupConfig} from './manifest';
|
||||
import {sha1Binary} from './sha1';
|
||||
@ -133,8 +133,9 @@ export abstract class AssetGroup {
|
||||
// to make sure it's still usable.
|
||||
if (await this.needToRevalidate(req, cachedResponse)) {
|
||||
this.idle.schedule(
|
||||
`revalidate(${this.prefix}, ${this.config.name}): ${req.url}`,
|
||||
async() => { await this.fetchAndCacheOnce(req); });
|
||||
`revalidate(${this.prefix}, ${this.config.name}): ${req.url}`, async () => {
|
||||
await this.fetchAndCacheOnce(req);
|
||||
});
|
||||
}
|
||||
|
||||
// In either case (revalidation or not), the cached response must be good.
|
||||
@ -178,7 +179,7 @@ export abstract class AssetGroup {
|
||||
// 3) The request has no applicable caching headers, and must be revalidated.
|
||||
if (res.headers.has('Cache-Control')) {
|
||||
// Figure out if there is a max-age directive in the Cache-Control header.
|
||||
const cacheControl = res.headers.get('Cache-Control') !;
|
||||
const cacheControl = res.headers.get('Cache-Control')!;
|
||||
const cacheDirectives =
|
||||
cacheControl
|
||||
// Directives are comma-separated within the Cache-Control header value.
|
||||
@ -229,7 +230,7 @@ export abstract class AssetGroup {
|
||||
}
|
||||
} else if (res.headers.has('Expires')) {
|
||||
// Determine if the expiration time has passed.
|
||||
const expiresStr = res.headers.get('Expires') !;
|
||||
const expiresStr = res.headers.get('Expires')!;
|
||||
try {
|
||||
// The request needs to be revalidated if the current time is later than the expiration
|
||||
// time, if it parses correctly.
|
||||
@ -292,7 +293,7 @@ export abstract class AssetGroup {
|
||||
if (this.inFlightRequests.has(req.url)) {
|
||||
// There is a caching operation already in progress for this request. Wait for it to
|
||||
// complete, and hopefully it will have yielded a useful response.
|
||||
return this.inFlightRequests.get(req.url) !;
|
||||
return this.inFlightRequests.get(req.url)!;
|
||||
}
|
||||
|
||||
// No other caching operation is being attempted for this resource, so it will be owned here.
|
||||
@ -312,8 +313,8 @@ export abstract class AssetGroup {
|
||||
// It's very important that only successful responses are cached. Unsuccessful responses
|
||||
// should never be cached as this can completely break applications.
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Response not Ok (fetchAndCacheOnce): request for ${req.url} returned response ${res.status} ${res.statusText}`);
|
||||
throw new Error(`Response not Ok (fetchAndCacheOnce): request for ${
|
||||
req.url} returned response ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -337,8 +338,8 @@ export abstract class AssetGroup {
|
||||
// but the SW is still running and serving another tab. In that case, trying to write to the
|
||||
// caches throws an `Entry was not found` error.
|
||||
// If this happens the SW can no longer work correctly. This situation is unrecoverable.
|
||||
throw new SwCriticalError(
|
||||
`Failed to update the caches for request to '${req.url}' (fetchAndCacheOnce): ${errorToString(err)}`);
|
||||
throw new SwCriticalError(`Failed to update the caches for request to '${
|
||||
req.url}' (fetchAndCacheOnce): ${errorToString(err)}`);
|
||||
}
|
||||
} finally {
|
||||
// Finally, it can be removed from `inFlightRequests`. This might result in a double-remove
|
||||
@ -356,7 +357,8 @@ export abstract class AssetGroup {
|
||||
// If the redirect limit is exhausted, fail with an error.
|
||||
if (redirectLimit === 0) {
|
||||
throw new SwCriticalError(
|
||||
`Response hit redirect limit (fetchFromNetwork): request redirected too many times, next is ${res.url}`);
|
||||
`Response hit redirect limit (fetchFromNetwork): request redirected too many times, next is ${
|
||||
res.url}`);
|
||||
}
|
||||
|
||||
// Unwrap the redirect directly.
|
||||
@ -377,7 +379,7 @@ export abstract class AssetGroup {
|
||||
if (this.hashes.has(url)) {
|
||||
// It turns out this resource does have a hash. Look it up. Unless the fetched version
|
||||
// matches this hash, it's invalid and the whole manifest may need to be thrown out.
|
||||
const canonicalHash = this.hashes.get(url) !;
|
||||
const canonicalHash = this.hashes.get(url)!;
|
||||
|
||||
// Ideally, the resource would be requested with cache-busting to guarantee the SW gets
|
||||
// the freshest version. However, doing this would eliminate any chance of the response
|
||||
@ -420,7 +422,9 @@ export abstract class AssetGroup {
|
||||
// If the response was unsuccessful, there's nothing more that can be done.
|
||||
if (!cacheBustedResult.ok) {
|
||||
throw new SwCriticalError(
|
||||
`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${req.url} returned response ${cacheBustedResult.status} ${cacheBustedResult.statusText}`);
|
||||
`Response not Ok (cacheBustedFetchFromNetwork): cache busted request for ${
|
||||
req.url} returned response ${cacheBustedResult.status} ${
|
||||
cacheBustedResult.statusText}`);
|
||||
}
|
||||
|
||||
// Hash the contents.
|
||||
@ -429,8 +433,8 @@ export abstract class AssetGroup {
|
||||
// If the cache-busted version doesn't match, then the manifest is not an accurate
|
||||
// representation of the server's current set of files, and the SW should give up.
|
||||
if (canonicalHash !== cacheBustedHash) {
|
||||
throw new SwCriticalError(
|
||||
`Hash mismatch (cacheBustedFetchFromNetwork): ${req.url}: expected ${canonicalHash}, got ${cacheBustedHash} (after cache busting)`);
|
||||
throw new SwCriticalError(`Hash mismatch (cacheBustedFetchFromNetwork): ${
|
||||
req.url}: expected ${canonicalHash}, got ${cacheBustedHash} (after cache busting)`);
|
||||
}
|
||||
|
||||
// If it does match, then use the cache-busted result.
|
||||
@ -455,7 +459,7 @@ export abstract class AssetGroup {
|
||||
const meta = await this.metadata;
|
||||
// Check if this resource is hashed and already exists in the cache of a prior version.
|
||||
if (this.hashes.has(url)) {
|
||||
const hash = this.hashes.get(url) !;
|
||||
const hash = this.hashes.get(url)!;
|
||||
|
||||
// Check the caches of prior versions, using the hash to ensure the correct version of
|
||||
// the resource is loaded.
|
||||
@ -465,7 +469,7 @@ export abstract class AssetGroup {
|
||||
if (res !== null) {
|
||||
// Copy to this cache.
|
||||
await cache.put(req, res);
|
||||
await meta.write(req.url, { ts: this.adapter.time, used: false } as UrlMetadata);
|
||||
await meta.write(req.url, {ts: this.adapter.time, used: false} as UrlMetadata);
|
||||
|
||||
// No need to do anything further with this resource, it's now cached properly.
|
||||
return true;
|
||||
@ -506,7 +510,7 @@ export class PrefetchAssetGroup extends AssetGroup {
|
||||
// Cache all known resources serially. As this reduce proceeds, each Promise waits
|
||||
// on the last before starting the fetch/cache operation for the next request. Any
|
||||
// errors cause fall-through to the final Promise which rejects.
|
||||
await this.config.urls.reduce(async(previous: Promise<void>, url: string) => {
|
||||
await this.config.urls.reduce(async (previous: Promise<void>, url: string) => {
|
||||
// Wait on all previous operations to complete.
|
||||
await previous;
|
||||
|
||||
@ -537,14 +541,14 @@ export class PrefetchAssetGroup extends AssetGroup {
|
||||
|
||||
// Select all of the previously cached resources. These are cached unhashed resources
|
||||
// from previous versions of the app, in any asset group.
|
||||
await(await updateFrom.previouslyCachedResources())
|
||||
await (await updateFrom.previouslyCachedResources())
|
||||
// First, narrow down the set of resources to those which are handled by this group.
|
||||
// Either it's a known URL, or it matches a given pattern.
|
||||
.filter(
|
||||
url => this.config.urls.some(cacheUrl => cacheUrl === url) ||
|
||||
this.patterns.some(pattern => pattern.test(url)))
|
||||
// Finally, process each resource in turn.
|
||||
.reduce(async(previous, url) => {
|
||||
.reduce(async (previous, url) => {
|
||||
await previous;
|
||||
const req = this.adapter.newRequest(url);
|
||||
|
||||
@ -565,7 +569,7 @@ export class PrefetchAssetGroup extends AssetGroup {
|
||||
// Write it into the cache. It may already be expired, but it can still serve
|
||||
// traffic until it's updated (stale-while-revalidate approach).
|
||||
await cache.put(req, res.response);
|
||||
await metaTable.write(url, { ...res.metadata, used: false } as UrlMetadata);
|
||||
await metaTable.write(url, {...res.metadata, used: false} as UrlMetadata);
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}
|
||||
@ -583,7 +587,7 @@ export class LazyAssetGroup extends AssetGroup {
|
||||
const cache = await this.cache;
|
||||
|
||||
// Loop through the listed resources, caching any which are available.
|
||||
await this.config.urls.reduce(async(previous: Promise<void>, url: string) => {
|
||||
await this.config.urls.reduce(async (previous: Promise<void>, url: string) => {
|
||||
// Wait on all previous operations to complete.
|
||||
await previous;
|
||||
|
||||
|
@ -59,7 +59,7 @@ interface LruState {
|
||||
/**
|
||||
* Map of URLs to data for each URL (including next/prev pointers).
|
||||
*/
|
||||
map: {[url: string]: LruNode | undefined};
|
||||
map: {[url: string]: LruNode|undefined};
|
||||
|
||||
/**
|
||||
* Count of the number of nodes in the chain.
|
||||
@ -88,7 +88,9 @@ class LruList {
|
||||
/**
|
||||
* The current count of URLs in the list.
|
||||
*/
|
||||
get size(): number { return this.state.count; }
|
||||
get size(): number {
|
||||
return this.state.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the tail.
|
||||
@ -125,7 +127,7 @@ class LruList {
|
||||
}
|
||||
|
||||
// There is at least one other node. Make the next node the new head.
|
||||
const next = this.state.map[node.next !] !;
|
||||
const next = this.state.map[node.next!]!;
|
||||
next.previous = null;
|
||||
this.state.head = next.url;
|
||||
node.next = null;
|
||||
@ -136,7 +138,7 @@ class LruList {
|
||||
|
||||
// The node is not the head, so it has a previous. It may or may not be the tail.
|
||||
// If it is not, then it has a next. First, grab the previous node.
|
||||
const previous = this.state.map[node.previous !] !;
|
||||
const previous = this.state.map[node.previous!]!;
|
||||
|
||||
// Fix the forward pointer to skip over node and go directly to node.next.
|
||||
previous.next = node.next;
|
||||
@ -146,10 +148,10 @@ class LruList {
|
||||
// updated to point to the previous node (removing the tail).
|
||||
if (node.next !== null) {
|
||||
// There is a next node, fix its back pointer to skip this node.
|
||||
this.state.map[node.next] !.previous = node.previous !;
|
||||
this.state.map[node.next]!.previous = node.previous!;
|
||||
} else {
|
||||
// There is no next node - the accessed node must be the tail. Move the tail pointer.
|
||||
this.state.tail = node.previous !;
|
||||
this.state.tail = node.previous!;
|
||||
}
|
||||
|
||||
node.next = null;
|
||||
@ -189,7 +191,7 @@ class LruList {
|
||||
// First, check if there's an existing head node. If there is, it has previous: null.
|
||||
// Its previous pointer should be set to the node we're inserting.
|
||||
if (this.state.head !== null) {
|
||||
this.state.map[this.state.head] !.previous = url;
|
||||
this.state.map[this.state.head]!.previous = url;
|
||||
}
|
||||
|
||||
// The next pointer of the node being inserted gets set to the old head, before the head
|
||||
@ -276,7 +278,7 @@ export class DataGroup {
|
||||
}
|
||||
const table = await this.lruTable;
|
||||
try {
|
||||
return table.write('lru', this._lru !.state);
|
||||
return table.write('lru', this._lru!.state);
|
||||
} catch (err) {
|
||||
// Writing lru cache table failed. This could be a result of a full storage.
|
||||
// Continue serving clients as usual.
|
||||
@ -413,7 +415,7 @@ export class DataGroup {
|
||||
// Otherwise, just fetch from the network directly.
|
||||
if (this.config.timeoutMs !== undefined) {
|
||||
const networkFetch = this.scope.fetch(req);
|
||||
const safeNetworkFetch = (async() => {
|
||||
const safeNetworkFetch = (async () => {
|
||||
try {
|
||||
return await networkFetch;
|
||||
} catch {
|
||||
@ -423,7 +425,7 @@ export class DataGroup {
|
||||
});
|
||||
}
|
||||
})();
|
||||
const networkFetchUndefinedError = (async() => {
|
||||
const networkFetchUndefinedError = (async () => {
|
||||
try {
|
||||
return await networkFetch;
|
||||
} catch {
|
||||
@ -454,7 +456,8 @@ export class DataGroup {
|
||||
// Since this data is cached lazily and temporarily, continue serving clients as usual.
|
||||
this.debugHandler.log(
|
||||
err,
|
||||
`DataGroup(${this.config.name}@${this.config.version}).safeCacheResponse(${req.url}, status: ${res.status})`);
|
||||
`DataGroup(${this.config.name}@${this.config.version}).safeCacheResponse(${
|
||||
req.url}, status: ${res.status})`);
|
||||
|
||||
// TODO: Better detect/handle full storage; e.g. using
|
||||
// [navigator.storage](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorStorage/storage).
|
||||
@ -529,7 +532,7 @@ export class DataGroup {
|
||||
|
||||
// Store the response in the cache (cloning because the browser will consume
|
||||
// the body during the caching operation).
|
||||
await(await this.cache).put(req, res.clone());
|
||||
await (await this.cache).put(req, res.clone());
|
||||
|
||||
// Store the age of the cache.
|
||||
const ageTable = await this.ageTable;
|
||||
|
@ -37,7 +37,7 @@ export class CacheDatabase implements Database {
|
||||
.then(cache => new CacheTable(name, cache, this.adapter));
|
||||
this.tables.set(name, table);
|
||||
}
|
||||
return this.tables.get(name) !;
|
||||
return this.tables.get(name)!;
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,9 +47,13 @@ export class CacheDatabase implements Database {
|
||||
export class CacheTable implements Table {
|
||||
constructor(readonly table: string, private cache: Cache, private adapter: Adapter) {}
|
||||
|
||||
private request(key: string): Request { return this.adapter.newRequest('/' + key); }
|
||||
private request(key: string): Request {
|
||||
return this.adapter.newRequest('/' + key);
|
||||
}
|
||||
|
||||
'delete'(key: string): Promise<boolean> { return this.cache.delete(this.request(key)); }
|
||||
'delete'(key: string): Promise<boolean> {
|
||||
return this.cache.delete(this.request(key));
|
||||
}
|
||||
|
||||
keys(): Promise<string[]> {
|
||||
return this.cache.keys().then(requests => requests.map(req => req.url.substr(1)));
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Adapter} from './adapter';
|
||||
import {DebugLogger, Debuggable} from './api';
|
||||
import {Debuggable, DebugLogger} from './api';
|
||||
|
||||
const DEBUG_LOG_BUFFER_SIZE = 100;
|
||||
|
||||
@ -102,7 +102,9 @@ ${msgIdle}`,
|
||||
this.debugLogA.push({value, time: this.adapter.time, context});
|
||||
}
|
||||
|
||||
private errorToString(err: Error): string { return `${err.name}(${err.message}, ${err.stack})`; }
|
||||
private errorToString(err: Error): string {
|
||||
return `${err.name}(${err.message}, ${err.stack})`;
|
||||
}
|
||||
|
||||
private formatDebugLog(log: DebugMessage[]): string {
|
||||
return log.map(entry => `[${this.since(entry.time)}] ${entry.value} ${entry.context}`)
|
||||
|
@ -7,14 +7,14 @@
|
||||
*/
|
||||
|
||||
import {Adapter} from './adapter';
|
||||
import {CacheState, DebugIdleState, DebugState, DebugVersion, Debuggable, UpdateCacheStatus, UpdateSource} from './api';
|
||||
import {CacheState, Debuggable, DebugIdleState, DebugState, DebugVersion, UpdateCacheStatus, UpdateSource} from './api';
|
||||
import {AppVersion} from './app-version';
|
||||
import {Database} from './database';
|
||||
import {DebugHandler} from './debug';
|
||||
import {errorToString} from './error';
|
||||
import {IdleScheduler} from './idle';
|
||||
import {Manifest, ManifestHash, hashManifest} from './manifest';
|
||||
import {MsgAny, isMsgActivateUpdate, isMsgCheckForUpdates} from './msg';
|
||||
import {hashManifest, Manifest, ManifestHash} from './manifest';
|
||||
import {isMsgActivateUpdate, isMsgCheckForUpdates, MsgAny} from './msg';
|
||||
|
||||
type ClientId = string;
|
||||
|
||||
@ -117,20 +117,20 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// almost as straightforward as restarting the SW. Because of this, it's always
|
||||
// safe to skip waiting until application tabs are closed, and activate the new
|
||||
// SW version immediately.
|
||||
event !.waitUntil(this.scope.skipWaiting());
|
||||
event!.waitUntil(this.scope.skipWaiting());
|
||||
});
|
||||
|
||||
// The activate event is triggered when this version of the service worker is
|
||||
// first activated.
|
||||
this.scope.addEventListener('activate', (event) => {
|
||||
event !.waitUntil((async() => {
|
||||
event!.waitUntil((async () => {
|
||||
// As above, it's safe to take over from existing clients immediately, since the new SW
|
||||
// version will continue to serve the old application.
|
||||
await this.scope.clients.claim();
|
||||
|
||||
// Once all clients have been taken over, we can delete caches used by old versions of
|
||||
// `@angular/service-worker`, which are no longer needed. This can happen in the background.
|
||||
this.idle.schedule('activate: cleanup-old-sw-caches', async() => {
|
||||
this.idle.schedule('activate: cleanup-old-sw-caches', async () => {
|
||||
try {
|
||||
await this.cleanupOldSwCaches();
|
||||
} catch (err) {
|
||||
@ -156,10 +156,10 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
});
|
||||
|
||||
// Handle the fetch, message, and push events.
|
||||
this.scope.addEventListener('fetch', (event) => this.onFetch(event !));
|
||||
this.scope.addEventListener('message', (event) => this.onMessage(event !));
|
||||
this.scope.addEventListener('push', (event) => this.onPush(event !));
|
||||
this.scope.addEventListener('notificationclick', (event) => this.onClick(event !));
|
||||
this.scope.addEventListener('fetch', (event) => this.onFetch(event!));
|
||||
this.scope.addEventListener('message', (event) => this.onMessage(event!));
|
||||
this.scope.addEventListener('push', (event) => this.onPush(event!));
|
||||
this.scope.addEventListener('notificationclick', (event) => this.onClick(event!));
|
||||
|
||||
// The debugger generates debug pages in response to debugging requests.
|
||||
this.debugger = new DebugHandler(this, this.adapter);
|
||||
@ -250,7 +250,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
return;
|
||||
}
|
||||
|
||||
event.waitUntil((async() => {
|
||||
event.waitUntil((async () => {
|
||||
// Initialization is the only event which is sent directly from the SW to itself, and thus
|
||||
// `event.source` is not a `Client`. Handle it here, before the check for `Client` sources.
|
||||
if (data.action === 'INITIALIZE') {
|
||||
@ -312,7 +312,9 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
|
||||
private async handleMessage(msg: MsgAny&{action: string}, from: Client): Promise<void> {
|
||||
if (isMsgCheckForUpdates(msg)) {
|
||||
const action = (async() => { await this.checkForUpdate(); })();
|
||||
const action = (async () => {
|
||||
await this.checkForUpdate();
|
||||
})();
|
||||
await this.reportStatus(from, action, msg.statusNonce);
|
||||
} else if (isMsgActivateUpdate(msg)) {
|
||||
await this.reportStatus(from, this.updateClient(from), msg.statusNonce);
|
||||
@ -327,11 +329,11 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
if (!data.notification || !data.notification.title) {
|
||||
return;
|
||||
}
|
||||
const desc = data.notification as{[key: string]: string | undefined};
|
||||
let options: {[key: string]: string | undefined} = {};
|
||||
const desc = data.notification as {[key: string]: string | undefined};
|
||||
let options: {[key: string]: string|undefined} = {};
|
||||
NOTIFICATION_OPTION_NAMES.filter(name => desc.hasOwnProperty(name))
|
||||
.forEach(name => options[name] = desc[name]);
|
||||
await this.scope.registration.showNotification(desc['title'] !, options);
|
||||
await this.scope.registration.showNotification(desc['title']!, options);
|
||||
}
|
||||
|
||||
private async handleClick(notification: Notification, action?: string): Promise<void> {
|
||||
@ -378,20 +380,20 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// Look up the application data associated with the existing version. If there
|
||||
// isn't any, fall back on using the hash.
|
||||
if (existing !== undefined) {
|
||||
const existingVersion = this.versions.get(existing) !;
|
||||
const existingVersion = this.versions.get(existing)!;
|
||||
previous = this.mergeHashWithAppData(existingVersion.manifest, existing);
|
||||
}
|
||||
|
||||
// Set the current version used by the client, and sync the mapping to disk.
|
||||
this.clientVersionMap.set(client.id, this.latestHash !);
|
||||
this.clientVersionMap.set(client.id, this.latestHash!);
|
||||
await this.sync();
|
||||
|
||||
// Notify the client about this activation.
|
||||
const current = this.versions.get(this.latestHash !) !;
|
||||
const current = this.versions.get(this.latestHash!)!;
|
||||
const notice = {
|
||||
type: 'UPDATE_ACTIVATED',
|
||||
previous,
|
||||
current: this.mergeHashWithAppData(current.manifest, this.latestHash !),
|
||||
current: this.mergeHashWithAppData(current.manifest, this.latestHash!),
|
||||
};
|
||||
|
||||
client.postMessage(notice);
|
||||
@ -410,7 +412,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// On navigation requests, check for new updates.
|
||||
if (event.request.mode === 'navigate' && !this.scheduledNavUpdateCheck) {
|
||||
this.scheduledNavUpdateCheck = true;
|
||||
this.idle.schedule('check-updates-on-navigation', async() => {
|
||||
this.idle.schedule('check-updates-on-navigation', async () => {
|
||||
this.scheduledNavUpdateCheck = false;
|
||||
await this.checkForUpdate();
|
||||
});
|
||||
@ -490,7 +492,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
|
||||
// Successfully loaded from saved state. This implies a manifest exists, so
|
||||
// the update check needs to happen in the background.
|
||||
this.idle.schedule('init post-load (update, cleanup)', async() => {
|
||||
this.idle.schedule('init post-load (update, cleanup)', async () => {
|
||||
await this.checkForUpdate();
|
||||
try {
|
||||
await this.cleanupCaches();
|
||||
@ -530,8 +532,9 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// created for it.
|
||||
if (!this.versions.has(hash)) {
|
||||
this.versions.set(
|
||||
hash, new AppVersion(
|
||||
this.scope, this.adapter, this.db, this.idle, this.debugger, manifest, hash));
|
||||
hash,
|
||||
new AppVersion(
|
||||
this.scope, this.adapter, this.db, this.idle, this.debugger, manifest, hash));
|
||||
}
|
||||
});
|
||||
|
||||
@ -567,12 +570,12 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// full initialization.
|
||||
// If any of these initializations fail, versionFailed() will be called either
|
||||
// synchronously or asynchronously to handle the failure and re-map clients.
|
||||
await Promise.all(Object.keys(manifests).map(async(hash: ManifestHash) => {
|
||||
await Promise.all(Object.keys(manifests).map(async (hash: ManifestHash) => {
|
||||
try {
|
||||
// Attempt to schedule or initialize this version. If this operation is
|
||||
// successful, then initialization either succeeded or was scheduled. If
|
||||
// it fails, then full initialization was attempted and failed.
|
||||
await this.scheduleInitialization(this.versions.get(hash) !);
|
||||
await this.scheduleInitialization(this.versions.get(hash)!);
|
||||
} catch (err) {
|
||||
this.debugger.log(err, `initialize: schedule init of ${hash}`);
|
||||
return false;
|
||||
@ -587,7 +590,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
throw new Error(
|
||||
`Invariant violated (${debugName}): want AppVersion for ${hash} but not loaded`);
|
||||
}
|
||||
return this.versions.get(hash) !;
|
||||
return this.versions.get(hash)!;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -601,7 +604,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// Check if there is an assigned client id.
|
||||
if (this.clientVersionMap.has(clientId)) {
|
||||
// There is an assignment for this client already.
|
||||
const hash = this.clientVersionMap.get(clientId) !;
|
||||
const hash = this.clientVersionMap.get(clientId)!;
|
||||
let appVersion = this.lookupVersionByHash(hash, 'assignVersion');
|
||||
|
||||
// Ordinarily, this client would be served from its assigned version. But, if this
|
||||
@ -705,9 +708,9 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
}
|
||||
|
||||
private async deleteAllCaches(): Promise<void> {
|
||||
await(await this.scope.caches.keys())
|
||||
await (await this.scope.caches.keys())
|
||||
.filter(key => key.startsWith(`${this.adapter.cacheNamePrefix}:`))
|
||||
.reduce(async(previous, key) => {
|
||||
.reduce(async (previous, key) => {
|
||||
await Promise.all([
|
||||
previous,
|
||||
this.scope.caches.delete(key),
|
||||
@ -721,7 +724,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
* awaited, as under some conditions the AppVersion might be initialized immediately.
|
||||
*/
|
||||
private async scheduleInitialization(appVersion: AppVersion): Promise<void> {
|
||||
const initialize = async() => {
|
||||
const initialize = async () => {
|
||||
try {
|
||||
await appVersion.initializeFully();
|
||||
} catch (err) {
|
||||
@ -768,7 +771,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// The latest version is viable, but this older version isn't. The only
|
||||
// possible remedy is to stop serving the older version and go to the network.
|
||||
// Put the affected clients on the latest version.
|
||||
affectedClients.forEach(clientId => this.clientVersionMap.set(clientId, this.latestHash !));
|
||||
affectedClients.forEach(clientId => this.clientVersionMap.set(clientId, this.latestHash!));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -788,8 +791,8 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
if (manifest.configVersion !== SUPPORTED_CONFIG_VERSION) {
|
||||
await this.deleteAllCaches();
|
||||
await this.scope.registration.unregister();
|
||||
throw new Error(
|
||||
`Invalid config version: expected ${SUPPORTED_CONFIG_VERSION}, got ${manifest.configVersion}.`);
|
||||
throw new Error(`Invalid config version: expected ${SUPPORTED_CONFIG_VERSION}, got ${
|
||||
manifest.configVersion}.`);
|
||||
}
|
||||
|
||||
// Cause the new version to become fully initialized. If this fails, then the
|
||||
@ -853,16 +856,20 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
|
||||
// Construct a serializable map of hashes to manifests.
|
||||
const manifests: ManifestMap = {};
|
||||
this.versions.forEach((version, hash) => { manifests[hash] = version.manifest; });
|
||||
this.versions.forEach((version, hash) => {
|
||||
manifests[hash] = version.manifest;
|
||||
});
|
||||
|
||||
// Construct a serializable map of client ids to version hashes.
|
||||
const assignments: ClientAssignments = {};
|
||||
this.clientVersionMap.forEach((hash, clientId) => { assignments[clientId] = hash; });
|
||||
this.clientVersionMap.forEach((hash, clientId) => {
|
||||
assignments[clientId] = hash;
|
||||
});
|
||||
|
||||
// Record the latest entry. Since this is a sync which is necessarily happening after
|
||||
// initialization, latestHash should always be valid.
|
||||
const latest: LatestEntry = {
|
||||
latest: this.latestHash !,
|
||||
latest: this.latestHash!,
|
||||
};
|
||||
|
||||
// Synchronize all of these.
|
||||
@ -900,7 +907,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
.filter(version => !usedVersions.has(version) && version !== this.latestHash);
|
||||
|
||||
// Remove all the versions which are no longer used.
|
||||
await obsoleteVersions.reduce(async(previous, version) => {
|
||||
await obsoleteVersions.reduce(async (previous, version) => {
|
||||
// Wait for the other cleanup operations to complete.
|
||||
await previous;
|
||||
|
||||
@ -908,7 +915,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// shouldn't happen, but handle it just in case).
|
||||
try {
|
||||
// Get ahold of the AppVersion for this particular hash.
|
||||
const instance = this.versions.get(version) !;
|
||||
const instance = this.versions.get(version)!;
|
||||
|
||||
// Delete it from the canonical map.
|
||||
this.versions.delete(version);
|
||||
@ -951,7 +958,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// reduction, if a response has already been identified, then pass it through, as no
|
||||
// future operation could change the response. If no response has been found yet, keep
|
||||
// checking versions until one is or until all versions have been exhausted.
|
||||
.reduce(async(prev, version) => {
|
||||
.reduce(async (prev, version) => {
|
||||
// First, check the previous result. If a non-null result has been found already, just
|
||||
// return it.
|
||||
if (await prev !== null) {
|
||||
@ -965,18 +972,18 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
|
||||
async lookupResourceWithoutHash(url: string): Promise<CacheState|null> {
|
||||
await this.initialized;
|
||||
const version = this.versions.get(this.latestHash !);
|
||||
const version = this.versions.get(this.latestHash!);
|
||||
return version ? version.lookupResourceWithoutHash(url) : null;
|
||||
}
|
||||
|
||||
async previouslyCachedResources(): Promise<string[]> {
|
||||
await this.initialized;
|
||||
const version = this.versions.get(this.latestHash !);
|
||||
const version = this.versions.get(this.latestHash!);
|
||||
return version ? version.previouslyCachedResources() : [];
|
||||
}
|
||||
|
||||
async recentCacheStatus(url: string): Promise<UpdateCacheStatus> {
|
||||
const version = this.versions.get(this.latestHash !);
|
||||
const version = this.versions.get(this.latestHash!);
|
||||
return version ? version.recentCacheStatus(url) : UpdateCacheStatus.NOT_CACHED;
|
||||
}
|
||||
|
||||
@ -992,7 +999,7 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
|
||||
const clients = await this.scope.clients.matchAll();
|
||||
|
||||
await clients.reduce(async(previous, client) => {
|
||||
await clients.reduce(async (previous, client) => {
|
||||
await previous;
|
||||
|
||||
// Firstly, determine which version this client is on.
|
||||
@ -1007,23 +1014,24 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.versions.get(version) !;
|
||||
const current = this.versions.get(version)!;
|
||||
|
||||
// Send a notice.
|
||||
const notice = {
|
||||
type: 'UPDATE_AVAILABLE',
|
||||
current: this.mergeHashWithAppData(current.manifest, version),
|
||||
available: this.mergeHashWithAppData(next.manifest, this.latestHash !),
|
||||
available: this.mergeHashWithAppData(next.manifest, this.latestHash!),
|
||||
};
|
||||
|
||||
client.postMessage(notice);
|
||||
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
async broadcast(msg: Object): Promise<void> {
|
||||
const clients = await this.scope.clients.matchAll();
|
||||
clients.forEach(client => { client.postMessage(msg); });
|
||||
clients.forEach(client => {
|
||||
client.postMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
async debugState(): Promise<DebugState> {
|
||||
@ -1038,13 +1046,14 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
async debugVersions(): Promise<DebugVersion[]> {
|
||||
// Build list of versions.
|
||||
return Array.from(this.versions.keys()).map(hash => {
|
||||
const version = this.versions.get(hash) !;
|
||||
const version = this.versions.get(hash)!;
|
||||
const clients = Array.from(this.clientVersionMap.entries())
|
||||
.filter(([clientId, version]) => version === hash)
|
||||
.map(([clientId, version]) => clientId);
|
||||
return {
|
||||
hash,
|
||||
manifest: version.manifest, clients,
|
||||
manifest: version.manifest,
|
||||
clients,
|
||||
status: '',
|
||||
};
|
||||
});
|
||||
|
@ -6,7 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export class SwCriticalError extends Error { readonly isCritical: boolean = true; }
|
||||
export class SwCriticalError extends Error {
|
||||
readonly isCritical: boolean = true;
|
||||
}
|
||||
|
||||
export function errorToString(error: any): string {
|
||||
if (error instanceof Error) {
|
||||
|
@ -60,7 +60,7 @@ export class IdleScheduler {
|
||||
const queue = this.queue;
|
||||
this.queue = [];
|
||||
|
||||
await queue.reduce(async(previous, task) => {
|
||||
await queue.reduce(async (previous, task) => {
|
||||
await previous;
|
||||
try {
|
||||
await task.run();
|
||||
@ -80,11 +80,17 @@ export class IdleScheduler {
|
||||
schedule(desc: string, run: () => Promise<void>): void {
|
||||
this.queue.push({desc, run});
|
||||
if (this.emptyResolve === null) {
|
||||
this.empty = new Promise(resolve => { this.emptyResolve = resolve; });
|
||||
this.empty = new Promise(resolve => {
|
||||
this.emptyResolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get size(): number { return this.queue.length; }
|
||||
get size(): number {
|
||||
return this.queue.length;
|
||||
}
|
||||
|
||||
get taskDescriptions(): string[] { return this.queue.map(task => task.desc); }
|
||||
get taskDescriptions(): string[] {
|
||||
return this.queue.map(task => task.desc);
|
||||
}
|
||||
}
|
@ -6,7 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export interface MsgAny { action: string; }
|
||||
export interface MsgAny {
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface MsgCheckForUpdates {
|
||||
action: 'CHECK_FOR_UPDATES';
|
||||
|
@ -53,9 +53,9 @@ interface WindowClient {
|
||||
navigate(url: string): Promise<WindowClient>;
|
||||
}
|
||||
|
||||
type ClientFrameType = 'auxiliary' | 'top-level' | 'nested' | 'none';
|
||||
type ClientMatchTypes = 'window' | 'worker' | 'sharedworker' | 'all';
|
||||
type WindowClientState = 'hidden' | 'visible' | 'prerender' | 'unloaded';
|
||||
type ClientFrameType = 'auxiliary'|'top-level'|'nested'|'none';
|
||||
type ClientMatchTypes = 'window'|'worker'|'sharedworker'|'all';
|
||||
type WindowClientState = 'hidden'|'visible'|'prerender'|'unloaded';
|
||||
|
||||
// Fetch API
|
||||
|
||||
|
@ -133,7 +133,7 @@ function arrayBufferToWords32(buffer: ArrayBuffer, endian: Endian): number[] {
|
||||
return words32;
|
||||
}
|
||||
|
||||
function byteAt(str: string | Uint8Array, index: number): number {
|
||||
function byteAt(str: string|Uint8Array, index: number): number {
|
||||
if (typeof str === 'string') {
|
||||
return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
|
||||
} else {
|
||||
@ -141,7 +141,7 @@ function byteAt(str: string | Uint8Array, index: number): number {
|
||||
}
|
||||
}
|
||||
|
||||
function wordAt(str: string | Uint8Array, index: number, endian: Endian): number {
|
||||
function wordAt(str: string|Uint8Array, index: number, endian: Endian): number {
|
||||
let word = 0;
|
||||
if (endian === Endian.Big) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
|
@ -14,283 +14,283 @@ import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTableForFs} from '
|
||||
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
|
||||
|
||||
(function() {
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dist = new MockFileSystemBuilder()
|
||||
.addFile('/foo.txt', 'this is foo')
|
||||
.addFile('/bar.txt', 'this is bar')
|
||||
.addFile('/api/test', 'version 1')
|
||||
.addFile('/api/a', 'version A')
|
||||
.addFile('/api/b', 'version B')
|
||||
.addFile('/api/c', 'version C')
|
||||
.addFile('/api/d', 'version D')
|
||||
.addFile('/api/e', 'version E')
|
||||
.addFile('/fresh/data', 'this is fresh data')
|
||||
.addFile('/refresh/data', 'this is some data')
|
||||
.build();
|
||||
const dist = new MockFileSystemBuilder()
|
||||
.addFile('/foo.txt', 'this is foo')
|
||||
.addFile('/bar.txt', 'this is bar')
|
||||
.addFile('/api/test', 'version 1')
|
||||
.addFile('/api/a', 'version A')
|
||||
.addFile('/api/b', 'version B')
|
||||
.addFile('/api/c', 'version C')
|
||||
.addFile('/api/d', 'version D')
|
||||
.addFile('/api/e', 'version E')
|
||||
.addFile('/fresh/data', 'this is fresh data')
|
||||
.addFile('/refresh/data', 'this is some data')
|
||||
.build();
|
||||
|
||||
|
||||
const distUpdate = new MockFileSystemBuilder()
|
||||
.addFile('/foo.txt', 'this is foo v2')
|
||||
.addFile('/bar.txt', 'this is bar')
|
||||
.addFile('/api/test', 'version 2')
|
||||
.addFile('/fresh/data', 'this is fresher data')
|
||||
.addFile('/refresh/data', 'this is refreshed data')
|
||||
.build();
|
||||
const distUpdate = new MockFileSystemBuilder()
|
||||
.addFile('/foo.txt', 'this is foo v2')
|
||||
.addFile('/bar.txt', 'this is bar')
|
||||
.addFile('/api/test', 'version 2')
|
||||
.addFile('/fresh/data', 'this is fresher data')
|
||||
.addFile('/refresh/data', 'this is refreshed data')
|
||||
.build();
|
||||
|
||||
const manifest: Manifest = {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
index: '/index.html',
|
||||
assetGroups: [
|
||||
{
|
||||
name: 'assets',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: [
|
||||
'/foo.txt',
|
||||
'/bar.txt',
|
||||
],
|
||||
patterns: [],
|
||||
},
|
||||
],
|
||||
dataGroups: [
|
||||
{
|
||||
name: 'testPerf',
|
||||
maxSize: 3,
|
||||
strategy: 'performance',
|
||||
patterns: ['^/api/.*$'],
|
||||
timeoutMs: 1000,
|
||||
maxAge: 5000,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: 'testRefresh',
|
||||
maxSize: 3,
|
||||
strategy: 'performance',
|
||||
patterns: ['^/refresh/.*$'],
|
||||
timeoutMs: 1000,
|
||||
refreshAheadMs: 1000,
|
||||
maxAge: 5000,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: 'testFresh',
|
||||
maxSize: 3,
|
||||
strategy: 'freshness',
|
||||
patterns: ['^/fresh/.*$'],
|
||||
timeoutMs: 1000,
|
||||
maxAge: 5000,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(dist),
|
||||
};
|
||||
const manifest: Manifest = {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
index: '/index.html',
|
||||
assetGroups: [
|
||||
{
|
||||
name: 'assets',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: [
|
||||
'/foo.txt',
|
||||
'/bar.txt',
|
||||
],
|
||||
patterns: [],
|
||||
},
|
||||
],
|
||||
dataGroups: [
|
||||
{
|
||||
name: 'testPerf',
|
||||
maxSize: 3,
|
||||
strategy: 'performance',
|
||||
patterns: ['^/api/.*$'],
|
||||
timeoutMs: 1000,
|
||||
maxAge: 5000,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: 'testRefresh',
|
||||
maxSize: 3,
|
||||
strategy: 'performance',
|
||||
patterns: ['^/refresh/.*$'],
|
||||
timeoutMs: 1000,
|
||||
refreshAheadMs: 1000,
|
||||
maxAge: 5000,
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: 'testFresh',
|
||||
maxSize: 3,
|
||||
strategy: 'freshness',
|
||||
patterns: ['^/fresh/.*$'],
|
||||
timeoutMs: 1000,
|
||||
maxAge: 5000,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(dist),
|
||||
};
|
||||
|
||||
const seqIncreasedManifest: Manifest = {
|
||||
...manifest,
|
||||
dataGroups: [
|
||||
{
|
||||
...manifest.dataGroups ![0],
|
||||
version: 2,
|
||||
},
|
||||
manifest.dataGroups ![1],
|
||||
manifest.dataGroups ![2],
|
||||
],
|
||||
};
|
||||
const seqIncreasedManifest: Manifest = {
|
||||
...manifest,
|
||||
dataGroups: [
|
||||
{
|
||||
...manifest.dataGroups![0],
|
||||
version: 2,
|
||||
},
|
||||
manifest.dataGroups![1],
|
||||
manifest.dataGroups![2],
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
const server = new MockServerStateBuilder().withStaticFiles(dist).withManifest(manifest).build();
|
||||
const server = new MockServerStateBuilder().withStaticFiles(dist).withManifest(manifest).build();
|
||||
|
||||
const serverUpdate =
|
||||
new MockServerStateBuilder().withStaticFiles(distUpdate).withManifest(manifest).build();
|
||||
const serverUpdate =
|
||||
new MockServerStateBuilder().withStaticFiles(distUpdate).withManifest(manifest).build();
|
||||
|
||||
const serverSeqUpdate = new MockServerStateBuilder()
|
||||
.withStaticFiles(distUpdate)
|
||||
.withManifest(seqIncreasedManifest)
|
||||
.build();
|
||||
const serverSeqUpdate = new MockServerStateBuilder()
|
||||
.withStaticFiles(distUpdate)
|
||||
.withManifest(seqIncreasedManifest)
|
||||
.build();
|
||||
|
||||
|
||||
describe('data cache', () => {
|
||||
let scope: SwTestHarness;
|
||||
let driver: Driver;
|
||||
beforeEach(async() => {
|
||||
scope = new SwTestHarnessBuilder().withServerState(server).build();
|
||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||
describe('data cache', () => {
|
||||
let scope: SwTestHarness;
|
||||
let driver: Driver;
|
||||
beforeEach(async () => {
|
||||
scope = new SwTestHarnessBuilder().withServerState(server).build();
|
||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||
|
||||
// Initialize.
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
await driver.initialized;
|
||||
// Initialize.
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
await driver.initialized;
|
||||
server.clearRequests();
|
||||
serverUpdate.clearRequests();
|
||||
serverSeqUpdate.clearRequests();
|
||||
});
|
||||
afterEach(() => {
|
||||
server.reset();
|
||||
serverUpdate.reset();
|
||||
serverSeqUpdate.reset();
|
||||
});
|
||||
|
||||
describe('in performance mode', () => {
|
||||
it('names the caches correctly', async () => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
const keys = await scope.caches.keys();
|
||||
expect(keys.every(key => key.startsWith('ngsw:/:'))).toEqual(true);
|
||||
});
|
||||
|
||||
it('caches a basic request', async () => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
server.assertSawRequestFor('/api/test');
|
||||
scope.advance(1000);
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('does not cache opaque responses', async () => {
|
||||
expect(await makeNoCorsRequest(scope, '/api/test')).toBe('');
|
||||
server.assertSawRequestFor('/api/test');
|
||||
|
||||
expect(await makeNoCorsRequest(scope, '/api/test')).toBe('');
|
||||
server.assertSawRequestFor('/api/test');
|
||||
});
|
||||
|
||||
it('refreshes after awhile', async () => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
server.clearRequests();
|
||||
serverUpdate.clearRequests();
|
||||
serverSeqUpdate.clearRequests();
|
||||
});
|
||||
afterEach(() => {
|
||||
server.reset();
|
||||
serverUpdate.reset();
|
||||
serverSeqUpdate.reset();
|
||||
scope.advance(10000);
|
||||
scope.updateServerState(serverUpdate);
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 2');
|
||||
});
|
||||
|
||||
describe('in performance mode', () => {
|
||||
it('names the caches correctly', async() => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
const keys = await scope.caches.keys();
|
||||
expect(keys.every(key => key.startsWith('ngsw:/:'))).toEqual(true);
|
||||
});
|
||||
|
||||
it('caches a basic request', async() => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
server.assertSawRequestFor('/api/test');
|
||||
scope.advance(1000);
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('does not cache opaque responses', async() => {
|
||||
expect(await makeNoCorsRequest(scope, '/api/test')).toBe('');
|
||||
server.assertSawRequestFor('/api/test');
|
||||
|
||||
expect(await makeNoCorsRequest(scope, '/api/test')).toBe('');
|
||||
server.assertSawRequestFor('/api/test');
|
||||
});
|
||||
|
||||
it('refreshes after awhile', async() => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
server.clearRequests();
|
||||
scope.advance(10000);
|
||||
scope.updateServerState(serverUpdate);
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 2');
|
||||
});
|
||||
|
||||
it('expires the least recently used entry', async() => {
|
||||
expect(await makeRequest(scope, '/api/a')).toEqual('version A');
|
||||
expect(await makeRequest(scope, '/api/b')).toEqual('version B');
|
||||
expect(await makeRequest(scope, '/api/c')).toEqual('version C');
|
||||
expect(await makeRequest(scope, '/api/d')).toEqual('version D');
|
||||
expect(await makeRequest(scope, '/api/e')).toEqual('version E');
|
||||
server.clearRequests();
|
||||
expect(await makeRequest(scope, '/api/c')).toEqual('version C');
|
||||
expect(await makeRequest(scope, '/api/d')).toEqual('version D');
|
||||
expect(await makeRequest(scope, '/api/e')).toEqual('version E');
|
||||
server.assertNoOtherRequests();
|
||||
expect(await makeRequest(scope, '/api/a')).toEqual('version A');
|
||||
expect(await makeRequest(scope, '/api/b')).toEqual('version B');
|
||||
server.assertSawRequestFor('/api/a');
|
||||
server.assertSawRequestFor('/api/b');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('does not carry over cache with new version', async() => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
scope.updateServerState(serverSeqUpdate);
|
||||
expect(await driver.checkForUpdate()).toEqual(true);
|
||||
await driver.updateClient(await scope.clients.get('default'));
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 2');
|
||||
});
|
||||
it('expires the least recently used entry', async () => {
|
||||
expect(await makeRequest(scope, '/api/a')).toEqual('version A');
|
||||
expect(await makeRequest(scope, '/api/b')).toEqual('version B');
|
||||
expect(await makeRequest(scope, '/api/c')).toEqual('version C');
|
||||
expect(await makeRequest(scope, '/api/d')).toEqual('version D');
|
||||
expect(await makeRequest(scope, '/api/e')).toEqual('version E');
|
||||
server.clearRequests();
|
||||
expect(await makeRequest(scope, '/api/c')).toEqual('version C');
|
||||
expect(await makeRequest(scope, '/api/d')).toEqual('version D');
|
||||
expect(await makeRequest(scope, '/api/e')).toEqual('version E');
|
||||
server.assertNoOtherRequests();
|
||||
expect(await makeRequest(scope, '/api/a')).toEqual('version A');
|
||||
expect(await makeRequest(scope, '/api/b')).toEqual('version B');
|
||||
server.assertSawRequestFor('/api/a');
|
||||
server.assertSawRequestFor('/api/b');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
describe('in freshness mode', () => {
|
||||
it('goes to the server first', async() => {
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.clearRequests();
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.assertNoOtherRequests();
|
||||
scope.updateServerState(serverUpdate);
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresher data');
|
||||
serverUpdate.assertSawRequestFor('/fresh/data');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('caches opaque responses', async() => {
|
||||
expect(await makeNoCorsRequest(scope, '/fresh/data')).toBe('');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
|
||||
server.online = false;
|
||||
|
||||
expect(await makeRequest(scope, '/fresh/data')).toBe('');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('falls back on the cache when server times out', async() => {
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.clearRequests();
|
||||
scope.updateServerState(serverUpdate);
|
||||
serverUpdate.pause();
|
||||
const [res, done] = makePendingRequest(scope, '/fresh/data');
|
||||
|
||||
await serverUpdate.nextRequest;
|
||||
|
||||
// Since the network request doesn't return within the timeout of 1,000ms,
|
||||
// this should return cached data.
|
||||
scope.advance(2000);
|
||||
|
||||
expect(await res).toEqual('this is fresh data');
|
||||
|
||||
// Unpausing allows the worker to continue with caching.
|
||||
serverUpdate.unpause();
|
||||
await done;
|
||||
|
||||
serverUpdate.pause();
|
||||
const [res2, done2] = makePendingRequest(scope, '/fresh/data');
|
||||
await serverUpdate.nextRequest;
|
||||
scope.advance(2000);
|
||||
expect(await res2).toEqual('this is fresher data');
|
||||
});
|
||||
|
||||
it('refreshes ahead', async() => {
|
||||
server.assertNoOtherRequests();
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is some data');
|
||||
server.assertSawRequestFor('/refresh/data');
|
||||
server.clearRequests();
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is some data');
|
||||
server.assertNoOtherRequests();
|
||||
scope.updateServerState(serverUpdate);
|
||||
scope.advance(1500);
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is some data');
|
||||
serverUpdate.assertSawRequestFor('/refresh/data');
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is refreshed data');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('caches opaque responses on refresh', async() => {
|
||||
// Make the initial request and populate the cache.
|
||||
expect(await makeRequest(scope, '/fresh/data')).toBe('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.clearRequests();
|
||||
|
||||
// Update the server state and pause the server, so the next request times out.
|
||||
scope.updateServerState(serverUpdate);
|
||||
serverUpdate.pause();
|
||||
const [res, done] =
|
||||
makePendingRequest(scope, new MockRequest('/fresh/data', {mode: 'no-cors'}));
|
||||
|
||||
// The network request times out after 1,000ms and the cached response is returned.
|
||||
await serverUpdate.nextRequest;
|
||||
scope.advance(2000);
|
||||
expect(await res).toBe('this is fresh data');
|
||||
|
||||
// Unpause the server to allow the network request to complete and be cached.
|
||||
serverUpdate.unpause();
|
||||
await done;
|
||||
|
||||
// Pause the server to force the cached (opaque) response to be returned.
|
||||
serverUpdate.pause();
|
||||
const [res2] = makePendingRequest(scope, '/fresh/data');
|
||||
await serverUpdate.nextRequest;
|
||||
scope.advance(2000);
|
||||
|
||||
expect(await res2).toBe('');
|
||||
});
|
||||
it('does not carry over cache with new version', async () => {
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
|
||||
scope.updateServerState(serverSeqUpdate);
|
||||
expect(await driver.checkForUpdate()).toEqual(true);
|
||||
await driver.updateClient(await scope.clients.get('default'));
|
||||
expect(await makeRequest(scope, '/api/test')).toEqual('version 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('in freshness mode', () => {
|
||||
it('goes to the server first', async () => {
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.clearRequests();
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.assertNoOtherRequests();
|
||||
scope.updateServerState(serverUpdate);
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresher data');
|
||||
serverUpdate.assertSawRequestFor('/fresh/data');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('caches opaque responses', async () => {
|
||||
expect(await makeNoCorsRequest(scope, '/fresh/data')).toBe('');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
|
||||
server.online = false;
|
||||
|
||||
expect(await makeRequest(scope, '/fresh/data')).toBe('');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('falls back on the cache when server times out', async () => {
|
||||
expect(await makeRequest(scope, '/fresh/data')).toEqual('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.clearRequests();
|
||||
scope.updateServerState(serverUpdate);
|
||||
serverUpdate.pause();
|
||||
const [res, done] = makePendingRequest(scope, '/fresh/data');
|
||||
|
||||
await serverUpdate.nextRequest;
|
||||
|
||||
// Since the network request doesn't return within the timeout of 1,000ms,
|
||||
// this should return cached data.
|
||||
scope.advance(2000);
|
||||
|
||||
expect(await res).toEqual('this is fresh data');
|
||||
|
||||
// Unpausing allows the worker to continue with caching.
|
||||
serverUpdate.unpause();
|
||||
await done;
|
||||
|
||||
serverUpdate.pause();
|
||||
const [res2, done2] = makePendingRequest(scope, '/fresh/data');
|
||||
await serverUpdate.nextRequest;
|
||||
scope.advance(2000);
|
||||
expect(await res2).toEqual('this is fresher data');
|
||||
});
|
||||
|
||||
it('refreshes ahead', async () => {
|
||||
server.assertNoOtherRequests();
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is some data');
|
||||
server.assertSawRequestFor('/refresh/data');
|
||||
server.clearRequests();
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is some data');
|
||||
server.assertNoOtherRequests();
|
||||
scope.updateServerState(serverUpdate);
|
||||
scope.advance(1500);
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is some data');
|
||||
serverUpdate.assertSawRequestFor('/refresh/data');
|
||||
expect(await makeRequest(scope, '/refresh/data')).toEqual('this is refreshed data');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
it('caches opaque responses on refresh', async () => {
|
||||
// Make the initial request and populate the cache.
|
||||
expect(await makeRequest(scope, '/fresh/data')).toBe('this is fresh data');
|
||||
server.assertSawRequestFor('/fresh/data');
|
||||
server.clearRequests();
|
||||
|
||||
// Update the server state and pause the server, so the next request times out.
|
||||
scope.updateServerState(serverUpdate);
|
||||
serverUpdate.pause();
|
||||
const [res, done] =
|
||||
makePendingRequest(scope, new MockRequest('/fresh/data', {mode: 'no-cors'}));
|
||||
|
||||
// The network request times out after 1,000ms and the cached response is returned.
|
||||
await serverUpdate.nextRequest;
|
||||
scope.advance(2000);
|
||||
expect(await res).toBe('this is fresh data');
|
||||
|
||||
// Unpause the server to allow the network request to complete and be cached.
|
||||
serverUpdate.unpause();
|
||||
await done;
|
||||
|
||||
// Pause the server to force the cached (opaque) response to be returned.
|
||||
serverUpdate.pause();
|
||||
const [res2] = makePendingRequest(scope, '/fresh/data');
|
||||
await serverUpdate.nextRequest;
|
||||
scope.advance(2000);
|
||||
|
||||
expect(await res2).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function makeRequest(scope: SwTestHarness, url: string, clientId?: string): Promise<string|null> {
|
||||
@ -305,9 +305,8 @@ function makeNoCorsRequest(
|
||||
return done.then(() => resTextPromise);
|
||||
}
|
||||
|
||||
function makePendingRequest(
|
||||
scope: SwTestHarness, urlOrReq: string | MockRequest,
|
||||
clientId?: string): [Promise<string|null>, Promise<void>] {
|
||||
function makePendingRequest(scope: SwTestHarness, urlOrReq: string|MockRequest, clientId?: string):
|
||||
[Promise<string|null>, Promise<void>] {
|
||||
const req = (typeof urlOrReq === 'string') ? new MockRequest(urlOrReq) : urlOrReq;
|
||||
const [resPromise, done] = scope.handleFetch(req, clientId || 'default');
|
||||
return [
|
||||
|
@ -10,128 +10,132 @@ import {IdleScheduler} from '../src/idle';
|
||||
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
|
||||
|
||||
(function() {
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
describe('IdleScheduler', () => {
|
||||
let scope: SwTestHarness;
|
||||
let idle: IdleScheduler;
|
||||
describe('IdleScheduler', () => {
|
||||
let scope: SwTestHarness;
|
||||
let idle: IdleScheduler;
|
||||
|
||||
beforeEach(() => {
|
||||
scope = new SwTestHarnessBuilder().build();
|
||||
idle = new IdleScheduler(scope, 1000, {
|
||||
log: (v, context) => console.error(v, context),
|
||||
});
|
||||
});
|
||||
|
||||
// Validate that a single idle task executes when trigger()
|
||||
// is called and the idle timeout passes.
|
||||
it('executes scheduled work on time', async() => {
|
||||
// Set up a single idle task to set the completed flag to true when it runs.
|
||||
let completed: boolean = false;
|
||||
idle.schedule('work', async() => { completed = true; });
|
||||
|
||||
// Simply scheduling the task should not cause it to execute.
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Trigger the idle mechanism. This returns a Promise that should resolve
|
||||
// once the idle timeout has passed.
|
||||
const trigger = idle.trigger();
|
||||
|
||||
// Advance the clock beyond the idle timeout, causing the idle tasks to run.
|
||||
scope.advance(1100);
|
||||
|
||||
// It should now be possible to wait for the trigger, and for the idle queue
|
||||
// to be empty.
|
||||
await trigger;
|
||||
await idle.empty;
|
||||
|
||||
// The task should now have run.
|
||||
expect(completed).toEqual(true);
|
||||
});
|
||||
|
||||
it('waits for multiple tasks to complete serially', async() => {
|
||||
// Schedule several tasks that will increase a counter according to its
|
||||
// current value. If these tasks execute in parallel, the writes to the counter
|
||||
// will race, and the test will fail.
|
||||
let counter: number = 2;
|
||||
idle.schedule('double counter', async() => {
|
||||
let local = counter;
|
||||
await Promise.resolve();
|
||||
local *= 2;
|
||||
await Promise.resolve();
|
||||
counter = local * 2;
|
||||
});
|
||||
idle.schedule('triple counter', async() => {
|
||||
// If this expect fails, it comes out of the 'await trigger' below.
|
||||
expect(counter).toEqual(8);
|
||||
|
||||
// Multiply the counter by 3 twice.
|
||||
let local = counter;
|
||||
await Promise.resolve();
|
||||
local *= 3;
|
||||
await Promise.resolve();
|
||||
counter = local * 3;
|
||||
});
|
||||
|
||||
// Trigger the idle mechanism once.
|
||||
const trigger = idle.trigger();
|
||||
|
||||
// Advance the clock beyond the idle timeout, causing the idle tasks to run, and
|
||||
// wait for them to complete.
|
||||
scope.advance(1100);
|
||||
await trigger;
|
||||
await idle.empty;
|
||||
|
||||
// Assert that both tasks executed in the correct serial sequence by validating
|
||||
// that the counter reached the correct value.
|
||||
expect(counter).toEqual(2 * 2 * 2 * 3 * 3);
|
||||
});
|
||||
|
||||
// Validate that a single idle task does not execute until trigger() has been called
|
||||
// and sufficient time passes without it being called again.
|
||||
it('does not execute work until timeout passes with no triggers', async() => {
|
||||
// Set up a single idle task to set the completed flag to true when it runs.
|
||||
let completed: boolean = false;
|
||||
idle.schedule('work', async() => { completed = true; });
|
||||
|
||||
// Trigger the queue once. This trigger will start a timer for the idle timeout,
|
||||
// but another trigger() will be called before that timeout passes.
|
||||
const firstTrigger = idle.trigger();
|
||||
|
||||
// Advance the clock a little, but not enough to actually cause tasks to execute.
|
||||
scope.advance(500);
|
||||
|
||||
// Assert that the task has not yet run.
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Next, trigger the queue again.
|
||||
const secondTrigger = idle.trigger();
|
||||
|
||||
// Advance the clock beyond the timeout for the first trigger, but not the second.
|
||||
// This should cause the first trigger to resolve, but without running the task.
|
||||
scope.advance(600);
|
||||
await firstTrigger;
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Schedule a third trigger. This is the one that will eventually resolve the task.
|
||||
const thirdTrigger = idle.trigger();
|
||||
|
||||
// Again, advance beyond the second trigger and verify it didn't resolve the task.
|
||||
scope.advance(500);
|
||||
await secondTrigger;
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Finally, advance beyond the third trigger, which should cause the task to be
|
||||
// executed finally.
|
||||
scope.advance(600);
|
||||
await thirdTrigger;
|
||||
await idle.empty;
|
||||
|
||||
// The task should have executed.
|
||||
expect(completed).toEqual(true);
|
||||
beforeEach(() => {
|
||||
scope = new SwTestHarnessBuilder().build();
|
||||
idle = new IdleScheduler(scope, 1000, {
|
||||
log: (v, context) => console.error(v, context),
|
||||
});
|
||||
});
|
||||
|
||||
// Validate that a single idle task executes when trigger()
|
||||
// is called and the idle timeout passes.
|
||||
it('executes scheduled work on time', async () => {
|
||||
// Set up a single idle task to set the completed flag to true when it runs.
|
||||
let completed: boolean = false;
|
||||
idle.schedule('work', async () => {
|
||||
completed = true;
|
||||
});
|
||||
|
||||
// Simply scheduling the task should not cause it to execute.
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Trigger the idle mechanism. This returns a Promise that should resolve
|
||||
// once the idle timeout has passed.
|
||||
const trigger = idle.trigger();
|
||||
|
||||
// Advance the clock beyond the idle timeout, causing the idle tasks to run.
|
||||
scope.advance(1100);
|
||||
|
||||
// It should now be possible to wait for the trigger, and for the idle queue
|
||||
// to be empty.
|
||||
await trigger;
|
||||
await idle.empty;
|
||||
|
||||
// The task should now have run.
|
||||
expect(completed).toEqual(true);
|
||||
});
|
||||
|
||||
it('waits for multiple tasks to complete serially', async () => {
|
||||
// Schedule several tasks that will increase a counter according to its
|
||||
// current value. If these tasks execute in parallel, the writes to the counter
|
||||
// will race, and the test will fail.
|
||||
let counter: number = 2;
|
||||
idle.schedule('double counter', async () => {
|
||||
let local = counter;
|
||||
await Promise.resolve();
|
||||
local *= 2;
|
||||
await Promise.resolve();
|
||||
counter = local * 2;
|
||||
});
|
||||
idle.schedule('triple counter', async () => {
|
||||
// If this expect fails, it comes out of the 'await trigger' below.
|
||||
expect(counter).toEqual(8);
|
||||
|
||||
// Multiply the counter by 3 twice.
|
||||
let local = counter;
|
||||
await Promise.resolve();
|
||||
local *= 3;
|
||||
await Promise.resolve();
|
||||
counter = local * 3;
|
||||
});
|
||||
|
||||
// Trigger the idle mechanism once.
|
||||
const trigger = idle.trigger();
|
||||
|
||||
// Advance the clock beyond the idle timeout, causing the idle tasks to run, and
|
||||
// wait for them to complete.
|
||||
scope.advance(1100);
|
||||
await trigger;
|
||||
await idle.empty;
|
||||
|
||||
// Assert that both tasks executed in the correct serial sequence by validating
|
||||
// that the counter reached the correct value.
|
||||
expect(counter).toEqual(2 * 2 * 2 * 3 * 3);
|
||||
});
|
||||
|
||||
// Validate that a single idle task does not execute until trigger() has been called
|
||||
// and sufficient time passes without it being called again.
|
||||
it('does not execute work until timeout passes with no triggers', async () => {
|
||||
// Set up a single idle task to set the completed flag to true when it runs.
|
||||
let completed: boolean = false;
|
||||
idle.schedule('work', async () => {
|
||||
completed = true;
|
||||
});
|
||||
|
||||
// Trigger the queue once. This trigger will start a timer for the idle timeout,
|
||||
// but another trigger() will be called before that timeout passes.
|
||||
const firstTrigger = idle.trigger();
|
||||
|
||||
// Advance the clock a little, but not enough to actually cause tasks to execute.
|
||||
scope.advance(500);
|
||||
|
||||
// Assert that the task has not yet run.
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Next, trigger the queue again.
|
||||
const secondTrigger = idle.trigger();
|
||||
|
||||
// Advance the clock beyond the timeout for the first trigger, but not the second.
|
||||
// This should cause the first trigger to resolve, but without running the task.
|
||||
scope.advance(600);
|
||||
await firstTrigger;
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Schedule a third trigger. This is the one that will eventually resolve the task.
|
||||
const thirdTrigger = idle.trigger();
|
||||
|
||||
// Again, advance beyond the second trigger and verify it didn't resolve the task.
|
||||
scope.advance(500);
|
||||
await secondTrigger;
|
||||
expect(completed).toEqual(false);
|
||||
|
||||
// Finally, advance beyond the third trigger, which should cause the task to be
|
||||
// executed finally.
|
||||
scope.advance(600);
|
||||
await thirdTrigger;
|
||||
await idle.empty;
|
||||
|
||||
// The task should have executed.
|
||||
expect(completed).toEqual(true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
@ -13,77 +13,78 @@ import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTable, tmpManifest
|
||||
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
|
||||
|
||||
(function() {
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
if (!SwTestHarness.envIsSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dist = new MockFileSystemBuilder()
|
||||
.addFile('/foo.txt', 'this is foo')
|
||||
.addFile('/bar.txt', 'this is bar')
|
||||
.build();
|
||||
const dist = new MockFileSystemBuilder()
|
||||
.addFile('/foo.txt', 'this is foo')
|
||||
.addFile('/bar.txt', 'this is bar')
|
||||
.build();
|
||||
|
||||
const manifest = tmpManifestSingleAssetGroup(dist);
|
||||
const manifest = tmpManifestSingleAssetGroup(dist);
|
||||
|
||||
const server = new MockServerStateBuilder().withStaticFiles(dist).withManifest(manifest).build();
|
||||
const server = new MockServerStateBuilder().withStaticFiles(dist).withManifest(manifest).build();
|
||||
|
||||
const scope = new SwTestHarnessBuilder().withServerState(server).build();
|
||||
const scope = new SwTestHarnessBuilder().withServerState(server).build();
|
||||
|
||||
const db = new CacheDatabase(scope, scope);
|
||||
const db = new CacheDatabase(scope, scope);
|
||||
|
||||
|
||||
describe('prefetch assets', () => {
|
||||
let group: PrefetchAssetGroup;
|
||||
let idle: IdleScheduler;
|
||||
beforeEach(() => {
|
||||
idle = new IdleScheduler(null !, 3000, {
|
||||
log: (v, ctx = '') => console.error(v, ctx),
|
||||
});
|
||||
group = new PrefetchAssetGroup(
|
||||
scope, scope, idle, manifest.assetGroups ![0], tmpHashTable(manifest), db, 'test');
|
||||
});
|
||||
it('initializes without crashing', async() => { await group.initializeFully(); });
|
||||
it('fully caches the two files', async() => {
|
||||
await group.initializeFully();
|
||||
scope.updateServerState();
|
||||
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope);
|
||||
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope);
|
||||
expect(await res1 !.text()).toEqual('this is foo');
|
||||
expect(await res2 !.text()).toEqual('this is bar');
|
||||
});
|
||||
it('persists the cache across restarts', async() => {
|
||||
await group.initializeFully();
|
||||
const freshScope =
|
||||
new SwTestHarnessBuilder().withCacheState(scope.caches.dehydrate()).build();
|
||||
group = new PrefetchAssetGroup(
|
||||
freshScope, freshScope, idle, manifest.assetGroups ![0], tmpHashTable(manifest),
|
||||
new CacheDatabase(freshScope, freshScope), 'test');
|
||||
await group.initializeFully();
|
||||
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope);
|
||||
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope);
|
||||
expect(await res1 !.text()).toEqual('this is foo');
|
||||
expect(await res2 !.text()).toEqual('this is bar');
|
||||
});
|
||||
it('caches properly if resources are requested before initialization', async() => {
|
||||
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope);
|
||||
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope);
|
||||
expect(await res1 !.text()).toEqual('this is foo');
|
||||
expect(await res2 !.text()).toEqual('this is bar');
|
||||
scope.updateServerState();
|
||||
await group.initializeFully();
|
||||
});
|
||||
it('throws if the server-side content does not match the manifest hash', async() => {
|
||||
const badHashFs = dist.extend().addFile('/foo.txt', 'corrupted file').build();
|
||||
const badServer =
|
||||
new MockServerStateBuilder().withManifest(manifest).withStaticFiles(badHashFs).build();
|
||||
const badScope = new SwTestHarnessBuilder().withServerState(badServer).build();
|
||||
group = new PrefetchAssetGroup(
|
||||
badScope, badScope, idle, manifest.assetGroups ![0], tmpHashTable(manifest),
|
||||
new CacheDatabase(badScope, badScope), 'test');
|
||||
const err = await errorFrom(group.initializeFully());
|
||||
expect(err.message).toContain('Hash mismatch');
|
||||
describe('prefetch assets', () => {
|
||||
let group: PrefetchAssetGroup;
|
||||
let idle: IdleScheduler;
|
||||
beforeEach(() => {
|
||||
idle = new IdleScheduler(null!, 3000, {
|
||||
log: (v, ctx = '') => console.error(v, ctx),
|
||||
});
|
||||
group = new PrefetchAssetGroup(
|
||||
scope, scope, idle, manifest.assetGroups![0], tmpHashTable(manifest), db, 'test');
|
||||
});
|
||||
it('initializes without crashing', async () => {
|
||||
await group.initializeFully();
|
||||
});
|
||||
it('fully caches the two files', async () => {
|
||||
await group.initializeFully();
|
||||
scope.updateServerState();
|
||||
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope);
|
||||
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope);
|
||||
expect(await res1!.text()).toEqual('this is foo');
|
||||
expect(await res2!.text()).toEqual('this is bar');
|
||||
});
|
||||
it('persists the cache across restarts', async () => {
|
||||
await group.initializeFully();
|
||||
const freshScope = new SwTestHarnessBuilder().withCacheState(scope.caches.dehydrate()).build();
|
||||
group = new PrefetchAssetGroup(
|
||||
freshScope, freshScope, idle, manifest.assetGroups![0], tmpHashTable(manifest),
|
||||
new CacheDatabase(freshScope, freshScope), 'test');
|
||||
await group.initializeFully();
|
||||
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope);
|
||||
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope);
|
||||
expect(await res1!.text()).toEqual('this is foo');
|
||||
expect(await res2!.text()).toEqual('this is bar');
|
||||
});
|
||||
it('caches properly if resources are requested before initialization', async () => {
|
||||
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope);
|
||||
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope);
|
||||
expect(await res1!.text()).toEqual('this is foo');
|
||||
expect(await res2!.text()).toEqual('this is bar');
|
||||
scope.updateServerState();
|
||||
await group.initializeFully();
|
||||
});
|
||||
it('throws if the server-side content does not match the manifest hash', async () => {
|
||||
const badHashFs = dist.extend().addFile('/foo.txt', 'corrupted file').build();
|
||||
const badServer =
|
||||
new MockServerStateBuilder().withManifest(manifest).withStaticFiles(badHashFs).build();
|
||||
const badScope = new SwTestHarnessBuilder().withServerState(badServer).build();
|
||||
group = new PrefetchAssetGroup(
|
||||
badScope, badScope, idle, manifest.assetGroups![0], tmpHashTable(manifest),
|
||||
new CacheDatabase(badScope, badScope), 'test');
|
||||
const err = await errorFrom(group.initializeFully());
|
||||
expect(err.message).toContain('Hash mismatch');
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
function errorFrom(promise: Promise<any>): Promise<any> {
|
||||
|
@ -28,14 +28,19 @@ export class MockCacheStorage implements CacheStorage {
|
||||
constructor(private origin: string, hydrateFrom?: string) {
|
||||
if (hydrateFrom !== undefined) {
|
||||
const hydrated = JSON.parse(hydrateFrom) as DehydratedCacheStorage;
|
||||
Object.keys(hydrated).forEach(
|
||||
name => { this.caches.set(name, new MockCache(this.origin, hydrated[name])); });
|
||||
Object.keys(hydrated).forEach(name => {
|
||||
this.caches.set(name, new MockCache(this.origin, hydrated[name]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async has(name: string): Promise<boolean> { return this.caches.has(name); }
|
||||
async has(name: string): Promise<boolean> {
|
||||
return this.caches.has(name);
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> { return Array.from(this.caches.keys()); }
|
||||
async keys(): Promise<string[]> {
|
||||
return Array.from(this.caches.keys());
|
||||
}
|
||||
|
||||
async open(name: string): Promise<Cache> {
|
||||
if (!this.caches.has(name)) {
|
||||
@ -67,7 +72,7 @@ export class MockCacheStorage implements CacheStorage {
|
||||
dehydrate(): string {
|
||||
const dehydrated: DehydratedCacheStorage = {};
|
||||
Array.from(this.caches.keys()).forEach(name => {
|
||||
const cache = this.caches.get(name) !;
|
||||
const cache = this.caches.get(name)!;
|
||||
dehydrated[name] = cache.dehydrate();
|
||||
});
|
||||
return JSON.stringify(dehydrated);
|
||||
@ -82,16 +87,21 @@ export class MockCache {
|
||||
Object.keys(hydrated).forEach(url => {
|
||||
const resp = hydrated[url];
|
||||
this.cache.set(
|
||||
url, new MockResponse(
|
||||
resp.body,
|
||||
{status: resp.status, statusText: resp.statusText, headers: resp.headers}));
|
||||
url,
|
||||
new MockResponse(
|
||||
resp.body,
|
||||
{status: resp.status, statusText: resp.statusText, headers: resp.headers}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async add(request: RequestInfo): Promise<void> { throw 'Not implemented'; }
|
||||
async add(request: RequestInfo): Promise<void> {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
async addAll(requests: RequestInfo[]): Promise<void> { throw 'Not implemented'; }
|
||||
async addAll(requests: RequestInfo[]): Promise<void> {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
async 'delete'(request: RequestInfo): Promise<boolean> {
|
||||
const url = (typeof request === 'string' ? request : request.url);
|
||||
@ -119,7 +129,7 @@ export class MockCache {
|
||||
if (res !== undefined) {
|
||||
res = res.clone();
|
||||
}
|
||||
return res !;
|
||||
return res!;
|
||||
}
|
||||
|
||||
async matchAll(request?: Request|string, options?: CacheQueryOptions): Promise<Response[]> {
|
||||
@ -128,7 +138,7 @@ export class MockCache {
|
||||
}
|
||||
const url = (typeof request === 'string' ? request : request.url);
|
||||
if (this.cache.has(url)) {
|
||||
return [this.cache.get(url) !];
|
||||
return [this.cache.get(url)!];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@ -156,8 +166,9 @@ export class MockCache {
|
||||
headers: {},
|
||||
} as DehydratedResponse;
|
||||
|
||||
resp.headers.forEach(
|
||||
(value: string, name: string) => { dehydratedResp.headers[name] = value; });
|
||||
resp.headers.forEach((value: string, name: string) => {
|
||||
dehydratedResp.headers[name] = value;
|
||||
});
|
||||
|
||||
dehydrated[url] = dehydratedResp;
|
||||
});
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
export class MockBody implements Body {
|
||||
readonly body !: ReadableStream;
|
||||
readonly body!: ReadableStream;
|
||||
bodyUsed: boolean = false;
|
||||
|
||||
constructor(public _body: string|null) {}
|
||||
@ -24,13 +24,21 @@ export class MockBody implements Body {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async blob(): Promise<Blob> { throw 'Not implemented'; }
|
||||
async blob(): Promise<Blob> {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
async json(): Promise<any> { return JSON.parse(this.getBody()); }
|
||||
async json(): Promise<any> {
|
||||
return JSON.parse(this.getBody());
|
||||
}
|
||||
|
||||
async text(): Promise<string> { return this.getBody(); }
|
||||
async text(): Promise<string> {
|
||||
return this.getBody();
|
||||
}
|
||||
|
||||
async formData(): Promise<FormData> { throw 'Not implemented'; }
|
||||
async formData(): Promise<FormData> {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
private getBody(): string {
|
||||
if (this.bodyUsed === true) {
|
||||
@ -47,31 +55,51 @@ export class MockBody implements Body {
|
||||
export class MockHeaders implements Headers {
|
||||
map = new Map<string, string>();
|
||||
|
||||
[Symbol.iterator]() { return this.map[Symbol.iterator](); }
|
||||
[Symbol.iterator]() {
|
||||
return this.map[Symbol.iterator]();
|
||||
}
|
||||
|
||||
append(name: string, value: string): void { this.map.set(name.toLowerCase(), value); }
|
||||
append(name: string, value: string): void {
|
||||
this.map.set(name.toLowerCase(), value);
|
||||
}
|
||||
|
||||
delete (name: string): void { this.map.delete(name.toLowerCase()); }
|
||||
delete(name: string): void {
|
||||
this.map.delete(name.toLowerCase());
|
||||
}
|
||||
|
||||
entries() { return this.map.entries(); }
|
||||
entries() {
|
||||
return this.map.entries();
|
||||
}
|
||||
|
||||
forEach(callback: Function): void { this.map.forEach(callback as any); }
|
||||
forEach(callback: Function): void {
|
||||
this.map.forEach(callback as any);
|
||||
}
|
||||
|
||||
get(name: string): string|null { return this.map.get(name.toLowerCase()) || null; }
|
||||
get(name: string): string|null {
|
||||
return this.map.get(name.toLowerCase()) || null;
|
||||
}
|
||||
|
||||
has(name: string): boolean { return this.map.has(name.toLowerCase()); }
|
||||
has(name: string): boolean {
|
||||
return this.map.has(name.toLowerCase());
|
||||
}
|
||||
|
||||
keys() { return this.map.keys(); }
|
||||
keys() {
|
||||
return this.map.keys();
|
||||
}
|
||||
|
||||
set(name: string, value: string): void { this.map.set(name.toLowerCase(), value); }
|
||||
set(name: string, value: string): void {
|
||||
this.map.set(name.toLowerCase(), value);
|
||||
}
|
||||
|
||||
values() { return this.map.values(); }
|
||||
values() {
|
||||
return this.map.values();
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRequest extends MockBody implements Request {
|
||||
readonly isHistoryNavigation: boolean = false;
|
||||
readonly isReloadNavigation: boolean = false;
|
||||
readonly body !: ReadableStream;
|
||||
readonly body!: ReadableStream;
|
||||
readonly cache: RequestCache = 'default';
|
||||
readonly credentials: RequestCredentials = 'omit';
|
||||
readonly destination: RequestDestination = 'document';
|
||||
@ -88,17 +116,19 @@ export class MockRequest extends MockBody implements Request {
|
||||
url: string;
|
||||
|
||||
constructor(input: string|Request, init: RequestInit = {}) {
|
||||
super(init !== undefined ? (init.body as(string | null)) || null : null);
|
||||
super(init !== undefined ? (init.body as (string | null)) || null : null);
|
||||
if (typeof input !== 'string') {
|
||||
throw 'Not implemented';
|
||||
}
|
||||
this.url = input;
|
||||
const headers = init.headers as{[key: string]: string};
|
||||
const headers = init.headers as {[key: string]: string};
|
||||
if (headers !== undefined) {
|
||||
if (headers instanceof MockHeaders) {
|
||||
this.headers = headers;
|
||||
} else {
|
||||
Object.keys(headers).forEach(header => { this.headers.set(header, headers[header]); });
|
||||
Object.keys(headers).forEach(header => {
|
||||
this.headers.set(header, headers[header]);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (init.cache !== undefined) {
|
||||
@ -128,7 +158,9 @@ export class MockRequest extends MockBody implements Request {
|
||||
export class MockResponse extends MockBody implements Response {
|
||||
readonly trailer: Promise<Headers> = Promise.resolve(new MockHeaders());
|
||||
readonly headers: Headers = new MockHeaders();
|
||||
get ok(): boolean { return this.status >= 200 && this.status < 300; }
|
||||
get ok(): boolean {
|
||||
return this.status >= 200 && this.status < 300;
|
||||
}
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly type: ResponseType = 'basic';
|
||||
@ -141,12 +173,14 @@ export class MockResponse extends MockBody implements Response {
|
||||
super(typeof body === 'string' ? body : null);
|
||||
this.status = (init.status !== undefined) ? init.status : 200;
|
||||
this.statusText = init.statusText || 'OK';
|
||||
const headers = init.headers as{[key: string]: string};
|
||||
const headers = init.headers as {[key: string]: string};
|
||||
if (headers !== undefined) {
|
||||
if (headers instanceof MockHeaders) {
|
||||
this.headers = headers;
|
||||
} else {
|
||||
Object.keys(headers).forEach(header => { this.headers.set(header, headers[header]); });
|
||||
Object.keys(headers).forEach(header => {
|
||||
this.headers.set(header, headers[header]);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (init.type !== undefined) {
|
||||
|
@ -20,7 +20,9 @@ export class MockFile {
|
||||
readonly path: string, readonly contents: string, readonly headers = {},
|
||||
readonly hashThisFile: boolean) {}
|
||||
|
||||
get hash(): string { return sha1(this.contents); }
|
||||
get hash(): string {
|
||||
return sha1(this.contents);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockFileSystemBuilder {
|
||||
@ -36,18 +38,22 @@ export class MockFileSystemBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): MockFileSystem { return new MockFileSystem(this.resources); }
|
||||
build(): MockFileSystem {
|
||||
return new MockFileSystem(this.resources);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockFileSystem {
|
||||
constructor(private resources: Map<string, MockFile>) {}
|
||||
|
||||
lookup(path: string): MockFile|undefined { return this.resources.get(path); }
|
||||
lookup(path: string): MockFile|undefined {
|
||||
return this.resources.get(path);
|
||||
}
|
||||
|
||||
extend(): MockFileSystemBuilder {
|
||||
const builder = new MockFileSystemBuilder();
|
||||
Array.from(this.resources.keys()).forEach(path => {
|
||||
const res = this.resources.get(path) !;
|
||||
const res = this.resources.get(path)!;
|
||||
if (res.hashThisFile) {
|
||||
builder.addFile(path, res.contents, res.headers);
|
||||
} else {
|
||||
@ -57,7 +63,9 @@ export class MockFileSystem {
|
||||
return builder;
|
||||
}
|
||||
|
||||
list(): string[] { return Array.from(this.resources.keys()); }
|
||||
list(): string[] {
|
||||
return Array.from(this.resources.keys());
|
||||
}
|
||||
}
|
||||
|
||||
export class MockServerStateBuilder {
|
||||
@ -66,7 +74,7 @@ export class MockServerStateBuilder {
|
||||
|
||||
withStaticFiles(fs: MockFileSystem): MockServerStateBuilder {
|
||||
fs.list().forEach(path => {
|
||||
const file = fs.lookup(path) !;
|
||||
const file = fs.lookup(path)!;
|
||||
this.resources.set(path, new MockResponse(file.contents, {headers: file.headers}));
|
||||
});
|
||||
return this;
|
||||
@ -102,17 +110,21 @@ export class MockServerState {
|
||||
private gate: Promise<void> = Promise.resolve();
|
||||
private resolve: Function|null = null;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private resolveNextRequest !: Function;
|
||||
private resolveNextRequest!: Function;
|
||||
online = true;
|
||||
nextRequest: Promise<Request>;
|
||||
|
||||
constructor(private resources: Map<string, Response>, private errors: Set<string>) {
|
||||
this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; });
|
||||
this.nextRequest = new Promise(resolve => {
|
||||
this.resolveNextRequest = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
this.resolveNextRequest(req);
|
||||
this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; });
|
||||
this.nextRequest = new Promise(resolve => {
|
||||
this.resolveNextRequest = resolve;
|
||||
});
|
||||
|
||||
await this.gate;
|
||||
|
||||
@ -127,7 +139,7 @@ export class MockServerState {
|
||||
}
|
||||
const url = req.url.split('?')[0];
|
||||
if (this.resources.has(url)) {
|
||||
return this.resources.get(url) !.clone();
|
||||
return this.resources.get(url)!.clone();
|
||||
}
|
||||
if (this.errors.has(url)) {
|
||||
throw new Error('Intentional failure!');
|
||||
@ -136,7 +148,9 @@ export class MockServerState {
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.gate = new Promise(resolve => { this.resolve = resolve; });
|
||||
this.gate = new Promise(resolve => {
|
||||
this.resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
unpause(): void {
|
||||
@ -170,18 +184,24 @@ export class MockServerState {
|
||||
|
||||
assertNoOtherRequests(): void {
|
||||
if (!this.noOtherRequests()) {
|
||||
throw new Error(
|
||||
`Expected no other requests, got requests for ${this.requests.map(req => req.url.split('?')[0]).join(', ')}`);
|
||||
throw new Error(`Expected no other requests, got requests for ${
|
||||
this.requests.map(req => req.url.split('?')[0]).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
noOtherRequests(): boolean { return this.requests.length === 0; }
|
||||
noOtherRequests(): boolean {
|
||||
return this.requests.length === 0;
|
||||
}
|
||||
|
||||
clearRequests(): void { this.requests = []; }
|
||||
clearRequests(): void {
|
||||
this.requests = [];
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.clearRequests();
|
||||
this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; });
|
||||
this.nextRequest = new Promise(resolve => {
|
||||
this.resolveNextRequest = resolve;
|
||||
});
|
||||
this.gate = Promise.resolve();
|
||||
this.resolve = null;
|
||||
this.online = true;
|
||||
@ -191,7 +211,9 @@ export class MockServerState {
|
||||
export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
|
||||
const files = fs.list();
|
||||
const hashTable: {[url: string]: string} = {};
|
||||
files.forEach(path => { hashTable[path] = fs.lookup(path) !.hash; });
|
||||
files.forEach(path => {
|
||||
hashTable[path] = fs.lookup(path)!.hash;
|
||||
});
|
||||
return {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
@ -205,7 +227,8 @@ export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
|
||||
patterns: [],
|
||||
},
|
||||
],
|
||||
navigationUrls: [], hashTable,
|
||||
navigationUrls: [],
|
||||
hashTable,
|
||||
};
|
||||
}
|
||||
|
||||
@ -213,7 +236,7 @@ export function tmpHashTableForFs(
|
||||
fs: MockFileSystem, breakHashes: {[url: string]: boolean} = {}): {[url: string]: string} {
|
||||
const table: {[url: string]: string} = {};
|
||||
fs.list().forEach(path => {
|
||||
const file = fs.lookup(path) !;
|
||||
const file = fs.lookup(path)!;
|
||||
if (file.hashThisFile) {
|
||||
table[path] = file.hash;
|
||||
if (breakHashes[path]) {
|
||||
|
@ -47,7 +47,9 @@ export class SwTestHarnessBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): SwTestHarness { return new SwTestHarness(this.server, this.caches, this.origin); }
|
||||
build(): SwTestHarness {
|
||||
return new SwTestHarness(this.server, this.caches, this.origin);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockClients implements Clients {
|
||||
@ -60,11 +62,17 @@ export class MockClients implements Clients {
|
||||
this.clients.set(clientId, new MockClient(clientId));
|
||||
}
|
||||
|
||||
remove(clientId: string): void { this.clients.delete(clientId); }
|
||||
remove(clientId: string): void {
|
||||
this.clients.delete(clientId);
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Client> { return this.clients.get(id) !as any as Client; }
|
||||
async get(id: string): Promise<Client> {
|
||||
return this.clients.get(id)! as any as Client;
|
||||
}
|
||||
|
||||
getMock(id: string): MockClient|undefined { return this.clients.get(id); }
|
||||
getMock(id: string): MockClient|undefined {
|
||||
return this.clients.get(id);
|
||||
}
|
||||
|
||||
async matchAll(): Promise<Client[]> {
|
||||
return Array.from(this.clients.values()) as any[] as Client[];
|
||||
@ -82,17 +90,23 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
private selfMessageQueue: any[] = [];
|
||||
autoAdvanceTime = false;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
unregistered !: boolean;
|
||||
unregistered!: boolean;
|
||||
readonly notifications: {title: string, options: Object}[] = [];
|
||||
readonly registration: ServiceWorkerRegistration = {
|
||||
active: {
|
||||
postMessage: (msg: any) => { this.selfMessageQueue.push(msg); },
|
||||
postMessage: (msg: any) => {
|
||||
this.selfMessageQueue.push(msg);
|
||||
},
|
||||
},
|
||||
scope: this.origin,
|
||||
showNotification: (title: string, options: Object) => {
|
||||
this.notifications.push({title, options});
|
||||
},
|
||||
unregister: () => { this.unregistered = true; },
|
||||
showNotification:
|
||||
(title: string, options: Object) => {
|
||||
this.notifications.push({title, options});
|
||||
},
|
||||
unregister:
|
||||
() => {
|
||||
this.unregistered = true;
|
||||
},
|
||||
} as any;
|
||||
|
||||
static envIsSupported(): boolean {
|
||||
@ -131,7 +145,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
while (this.selfMessageQueue.length > 0) {
|
||||
const queue = this.selfMessageQueue;
|
||||
this.selfMessageQueue = [];
|
||||
await queue.reduce(async(previous, msg) => {
|
||||
await queue.reduce(async (previous, msg) => {
|
||||
await previous;
|
||||
await this.handleMessage(msg, null);
|
||||
}, Promise.resolve());
|
||||
@ -145,18 +159,20 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
let skippedWaiting: boolean = false;
|
||||
if (this.eventHandlers.has('install')) {
|
||||
const installEvent = new MockInstallEvent();
|
||||
this.eventHandlers.get('install') !(installEvent);
|
||||
this.eventHandlers.get('install')!(installEvent);
|
||||
await installEvent.ready;
|
||||
skippedWaiting = this.skippedWaiting;
|
||||
}
|
||||
if (this.eventHandlers.has('activate')) {
|
||||
const activateEvent = new MockActivateEvent();
|
||||
this.eventHandlers.get('activate') !(activateEvent);
|
||||
this.eventHandlers.get('activate')!(activateEvent);
|
||||
await activateEvent.ready;
|
||||
}
|
||||
return skippedWaiting;
|
||||
}
|
||||
updateServerState(server?: MockServerState): void { this.server = server || EMPTY_SERVER_STATE; }
|
||||
updateServerState(server?: MockServerState): void {
|
||||
this.server = server || EMPTY_SERVER_STATE;
|
||||
}
|
||||
|
||||
fetch(req: string|Request): Promise<Response> {
|
||||
if (typeof req === 'string') {
|
||||
@ -177,11 +193,17 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
this.eventHandlers.set(event, handler);
|
||||
}
|
||||
|
||||
removeEventListener(event: string, handler?: Function): void { this.eventHandlers.delete(event); }
|
||||
removeEventListener(event: string, handler?: Function): void {
|
||||
this.eventHandlers.delete(event);
|
||||
}
|
||||
|
||||
newRequest(url: string, init: Object = {}): Request { return new MockRequest(url, init); }
|
||||
newRequest(url: string, init: Object = {}): Request {
|
||||
return new MockRequest(url, init);
|
||||
}
|
||||
|
||||
newResponse(body: string, init: Object = {}): Response { return new MockResponse(body, init); }
|
||||
newResponse(body: string, init: Object = {}): Response {
|
||||
return new MockResponse(body, init);
|
||||
}
|
||||
|
||||
newHeaders(headers: {[name: string]: string}): Headers {
|
||||
return Object.keys(headers).reduce((mock, name) => {
|
||||
@ -202,7 +224,9 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
};
|
||||
}
|
||||
|
||||
async skipWaiting(): Promise<void> { this.skippedWaiting = true; }
|
||||
async skipWaiting(): Promise<void> {
|
||||
this.skippedWaiting = true;
|
||||
}
|
||||
|
||||
waitUntil(promise: Promise<void>): void {}
|
||||
|
||||
@ -212,7 +236,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
throw new Error('No fetch handler registered');
|
||||
}
|
||||
const event = new MockFetchEvent(req, clientId);
|
||||
this.eventHandlers.get('fetch') !.call(this, event);
|
||||
this.eventHandlers.get('fetch')!.call(this, event);
|
||||
|
||||
if (clientId) {
|
||||
this.clients.add(clientId);
|
||||
@ -232,7 +256,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
this.clients.add(clientId);
|
||||
event = new MockMessageEvent(data, this.clients.getMock(clientId) || null);
|
||||
}
|
||||
this.eventHandlers.get('message') !.call(this, event);
|
||||
this.eventHandlers.get('message')!.call(this, event);
|
||||
return event.ready;
|
||||
}
|
||||
|
||||
@ -241,7 +265,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
throw new Error('No push handler registered');
|
||||
}
|
||||
const event = new MockPushEvent(data);
|
||||
this.eventHandlers.get('push') !.call(this, event);
|
||||
this.eventHandlers.get('push')!.call(this, event);
|
||||
return event.ready;
|
||||
}
|
||||
|
||||
@ -250,7 +274,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
throw new Error('No notificationclick handler registered');
|
||||
}
|
||||
const event = new MockNotificationEvent(notification, action);
|
||||
this.eventHandlers.get('notificationclick') !.call(this, event);
|
||||
this.eventHandlers.get('notificationclick')!.call(this, event);
|
||||
return event.ready;
|
||||
}
|
||||
|
||||
@ -281,7 +305,9 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
||||
});
|
||||
}
|
||||
|
||||
isClient(obj: any): obj is Client { return obj instanceof MockClient; }
|
||||
isClient(obj: any): obj is Client {
|
||||
return obj instanceof MockClient;
|
||||
}
|
||||
}
|
||||
|
||||
interface StaticFile {
|
||||
@ -304,9 +330,13 @@ export class AssetGroupBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
finish(): ConfigBuilder { return this.up; }
|
||||
finish(): ConfigBuilder {
|
||||
return this.up;
|
||||
}
|
||||
|
||||
toManifestGroup(): AssetGroupConfig { return null !; }
|
||||
toManifestGroup(): AssetGroupConfig {
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigBuilder {
|
||||
@ -324,8 +354,10 @@ export class ConfigBuilder {
|
||||
return {
|
||||
configVersion: 1,
|
||||
timestamp: 1234567890123,
|
||||
index: '/index.html', assetGroups,
|
||||
navigationUrls: [], hashTable,
|
||||
index: '/index.html',
|
||||
assetGroups,
|
||||
navigationUrls: [],
|
||||
hashTable,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -333,10 +365,12 @@ export class ConfigBuilder {
|
||||
class OneTimeContext implements Context {
|
||||
private queue: Promise<void>[] = [];
|
||||
|
||||
waitUntil(promise: Promise<void>): void { this.queue.push(promise); }
|
||||
waitUntil(promise: Promise<void>): void {
|
||||
this.queue.push(promise);
|
||||
}
|
||||
|
||||
get ready(): Promise<void> {
|
||||
return (async() => {
|
||||
return (async () => {
|
||||
while (this.queue.length > 0) {
|
||||
await this.queue.shift();
|
||||
}
|
||||
@ -349,7 +383,9 @@ class MockExtendableEvent extends OneTimeContext {}
|
||||
class MockFetchEvent extends MockExtendableEvent {
|
||||
response: Promise<Response|undefined> = Promise.resolve(undefined);
|
||||
|
||||
constructor(readonly request: Request, readonly clientId: string|null) { super(); }
|
||||
constructor(readonly request: Request, readonly clientId: string|null) {
|
||||
super();
|
||||
}
|
||||
|
||||
respondWith(promise: Promise<Response>): Promise<Response> {
|
||||
this.response = promise;
|
||||
@ -358,17 +394,23 @@ class MockFetchEvent extends MockExtendableEvent {
|
||||
}
|
||||
|
||||
class MockMessageEvent extends MockExtendableEvent {
|
||||
constructor(readonly data: Object, readonly source: MockClient|null) { super(); }
|
||||
constructor(readonly data: Object, readonly source: MockClient|null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class MockPushEvent extends MockExtendableEvent {
|
||||
constructor(private _data: Object) { super(); }
|
||||
constructor(private _data: Object) {
|
||||
super();
|
||||
}
|
||||
data = {
|
||||
json: () => this._data,
|
||||
};
|
||||
}
|
||||
class MockNotificationEvent extends MockExtendableEvent {
|
||||
constructor(private _notification: any, readonly action?: string) { super(); }
|
||||
constructor(private _notification: any, readonly action?: string) {
|
||||
super();
|
||||
}
|
||||
readonly notification = {...this._notification, close: () => undefined};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user