build: reformat repo to new clang@1.4.0 (#36613)

PR Close #36613
This commit is contained in:
Joey Perrott
2020-04-13 16:40:21 -07:00
committed by atscott
parent 5e80e7e216
commit 698b0288be
1160 changed files with 31667 additions and 24000 deletions

View File

@ -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);
}
}

View File

@ -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));
})();

View File

@ -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++) {

View File

@ -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;
}

View File

@ -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;

View File

@ -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';
};
}

View File

@ -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',

View 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);
}
}

View File

@ -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');
});
});

View File

@ -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';

View File

@ -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;
}
}

View File

@ -8,7 +8,7 @@
import {isPlatformBrowser} from '@angular/common';
import {APP_INITIALIZER, ApplicationRef, InjectionToken, Injector, ModuleWithProviders, NgModule, NgZone, PLATFORM_ID} from '@angular/core';
import {Observable, merge, of } from 'rxjs';
import {merge, Observable, of} from 'rxjs';
import {delay, filter, take} from 'rxjs/operators';
import {NgswCommChannel} from './low_level';
@ -105,7 +105,7 @@ export function ngswAppInitializer(
switch (strategy) {
case 'registerImmediately':
readyToRegister$ = of (null);
readyToRegister$ = of(null);
break;
case 'registerWithDelay':
readyToRegister$ = delayWithTimeout(+args[0] || 0);
@ -136,7 +136,7 @@ export function ngswAppInitializer(
}
function delayWithTimeout(timeout: number): Observable<unknown> {
return of (null).pipe(delay(timeout));
return of(null).pipe(delay(timeout));
}
function whenStable(injector: Injector): Observable<unknown> {

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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;
});
});
})();

View File

@ -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);
}
}

View File

@ -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);
});
}
}

View File

@ -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;

View File

@ -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).

View File

@ -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;

View File

@ -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;

View File

@ -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)));

View File

@ -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}`)

View File

@ -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: '',
};
});

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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';

View File

@ -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

View File

@ -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++) {

View File

@ -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 [

View File

@ -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);
});
});
})();

View File

@ -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> {

View File

@ -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;
});

View File

@ -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) {

View File

@ -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]) {

View File

@ -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};
}