refactor(service-worker): remove duplicate `Context` type (in favor of `ExtendableEvent`) (#42736)

This commit removes the duplicate `Context` interface and uses the
`ExtendableEvent` interface instead.

This is in preparation of switching from our custom typings to the
official TypeScript typings (`lib.webworker.d.ts`).

PR Close #42736
This commit is contained in:
George Kalpakas 2021-07-08 16:25:15 +03:00 committed by atscott
parent ad00d0830f
commit fe135e1198
6 changed files with 85 additions and 49 deletions

View File

@ -104,14 +104,3 @@ export class Adapter<T extends CacheStorage = CacheStorage> {
}); });
} }
} }
/**
* An event context in which an operation is taking place, which allows
* the delaying of Service Worker shutdown until certain triggers occur.
*/
export interface Context {
/**
* Delay shutdown of the Service Worker until the given promise resolves.
*/
waitUntil(fn: Promise<any>): void;
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Adapter, Context} from './adapter'; import {Adapter} from './adapter';
import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource} from './api'; import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource} from './api';
import {AssetGroup, LazyAssetGroup, PrefetchAssetGroup} from './assets'; import {AssetGroup, LazyAssetGroup, PrefetchAssetGroup} from './assets';
import {DataGroup} from './data'; import {DataGroup} from './data';
@ -135,7 +135,7 @@ export class AppVersion implements UpdateSource {
} }
} }
async handleFetch(req: Request, context: Context): Promise<Response|null> { async handleFetch(req: Request, event: ExtendableEvent): Promise<Response|null> {
// Check the request against each `AssetGroup` in sequence. If an `AssetGroup` can't handle the // Check the request against each `AssetGroup` in sequence. If an `AssetGroup` can't handle the
// request, // request,
// it will return `null`. Thus, the first non-null response is the SW's answer to the request. // it will return `null`. Thus, the first non-null response is the SW's answer to the request.
@ -152,7 +152,7 @@ export class AppVersion implements UpdateSource {
} }
// No response has been found yet. Maybe this group will have one. // No response has been found yet. Maybe this group will have one.
return group.handleFetch(req, context); return group.handleFetch(req, event);
}, Promise.resolve<Response|null>(null)); }, Promise.resolve<Response|null>(null));
// The result of the above is the asset response, if there is any, or null otherwise. Return the // The result of the above is the asset response, if there is any, or null otherwise. Return the
@ -170,7 +170,7 @@ export class AppVersion implements UpdateSource {
return resp; return resp;
} }
return group.handleFetch(req, context); return group.handleFetch(req, event);
}, Promise.resolve<Response|null>(null)); }, Promise.resolve<Response|null>(null));
// If the data caching group returned a response, go with it. // If the data caching group returned a response, go with it.
@ -195,7 +195,7 @@ export class AppVersion implements UpdateSource {
// This was a navigation request. Re-enter `handleFetch` with a request for // This was a navigation request. Re-enter `handleFetch` with a request for
// the URL. // the URL.
return this.handleFetch(this.adapter.newRequest(this.indexUrl), context); return this.handleFetch(this.adapter.newRequest(this.indexUrl), event);
} }
return null; return null;

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Adapter, Context} from './adapter'; import {Adapter} from './adapter';
import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource, UrlMetadata} from './api'; import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource, UrlMetadata} from './api';
import {Database, Table} from './database'; import {Database, Table} from './database';
import {CacheTable} from './db-cache'; import {CacheTable} from './db-cache';
@ -114,7 +114,7 @@ export abstract class AssetGroup {
/** /**
* Process a request for a given resource and return it, or return null if it's not available. * Process a request for a given resource and return it, or return null if it's not available.
*/ */
async handleFetch(req: Request, ctx: Context): Promise<Response|null> { async handleFetch(req: Request, _event: ExtendableEvent): Promise<Response|null> {
const url = this.adapter.normalizeUrl(req.url); const url = this.adapter.normalizeUrl(req.url);
// Either the request matches one of the known resource URLs, one of the patterns for // Either the request matches one of the known resource URLs, one of the patterns for
// dynamically matched URLs, or neither. Determine which is the case for this request in // dynamically matched URLs, or neither. Determine which is the case for this request in

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Adapter, Context} from './adapter'; import {Adapter} from './adapter';
import {Database, Table} from './database'; import {Database, Table} from './database';
import {CacheTable} from './db-cache'; import {CacheTable} from './db-cache';
import {DebugHandler} from './debug'; import {DebugHandler} from './debug';
@ -294,7 +294,7 @@ export class DataGroup {
* Process a fetch event and return a `Response` if the resource is covered by this group, * Process a fetch event and return a `Response` if the resource is covered by this group,
* or `null` otherwise. * or `null` otherwise.
*/ */
async handleFetch(req: Request, ctx: Context): Promise<Response|null> { async handleFetch(req: Request, event: ExtendableEvent): Promise<Response|null> {
// Do nothing // Do nothing
if (!this.patterns.some(pattern => pattern.test(req.url))) { if (!this.patterns.some(pattern => pattern.test(req.url))) {
return null; return null;
@ -314,9 +314,9 @@ export class DataGroup {
// Handle the request with whatever strategy was selected. // Handle the request with whatever strategy was selected.
switch (this.config.strategy) { switch (this.config.strategy) {
case 'freshness': case 'freshness':
return this.handleFetchWithFreshness(req, ctx, lru); return this.handleFetchWithFreshness(req, event, lru);
case 'performance': case 'performance':
return this.handleFetchWithPerformance(req, ctx, lru); return this.handleFetchWithPerformance(req, event, lru);
default: default:
throw new Error(`Unknown strategy: ${this.config.strategy}`); throw new Error(`Unknown strategy: ${this.config.strategy}`);
} }
@ -337,7 +337,7 @@ export class DataGroup {
} }
} }
private async handleFetchWithPerformance(req: Request, ctx: Context, lru: LruList): private async handleFetchWithPerformance(req: Request, event: ExtendableEvent, lru: LruList):
Promise<Response|null> { Promise<Response|null> {
let res: Response|null|undefined = null; let res: Response|null|undefined = null;
@ -348,7 +348,7 @@ export class DataGroup {
res = fromCache.res; res = fromCache.res;
// Check the age of the resource. // Check the age of the resource.
if (this.config.refreshAheadMs !== undefined && fromCache.age >= this.config.refreshAheadMs) { if (this.config.refreshAheadMs !== undefined && fromCache.age >= this.config.refreshAheadMs) {
ctx.waitUntil(this.safeCacheResponse(req, this.safeFetch(req), lru)); event.waitUntil(this.safeCacheResponse(req, this.safeFetch(req), lru));
} }
} }
@ -367,7 +367,7 @@ export class DataGroup {
res = this.adapter.newResponse(null, {status: 504, statusText: 'Gateway Timeout'}); res = this.adapter.newResponse(null, {status: 504, statusText: 'Gateway Timeout'});
// Cache the network response eventually. // Cache the network response eventually.
ctx.waitUntil(this.safeCacheResponse(req, networkFetch, lru)); event.waitUntil(this.safeCacheResponse(req, networkFetch, lru));
} else { } else {
// The request completed in time, so cache it inline with the response flow. // The request completed in time, so cache it inline with the response flow.
await this.safeCacheResponse(req, res, lru); await this.safeCacheResponse(req, res, lru);
@ -376,7 +376,7 @@ export class DataGroup {
return res; return res;
} }
private async handleFetchWithFreshness(req: Request, ctx: Context, lru: LruList): private async handleFetchWithFreshness(req: Request, event: ExtendableEvent, lru: LruList):
Promise<Response|null> { Promise<Response|null> {
// Start with a network fetch. // Start with a network fetch.
const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req); const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req);
@ -392,7 +392,7 @@ export class DataGroup {
// If the network fetch times out or errors, fall back on the cache. // If the network fetch times out or errors, fall back on the cache.
if (res === undefined) { if (res === undefined) {
ctx.waitUntil(this.safeCacheResponse(req, networkFetch, lru, true)); event.waitUntil(this.safeCacheResponse(req, networkFetch, lru, true));
// Ignore the age, the network response will be cached anyway due to the // Ignore the age, the network response will be cached anyway due to the
// behavior of freshness. // behavior of freshness.

View File

@ -12,7 +12,7 @@ import {IdleScheduler} from '../src/idle';
import {MockCache} from '../testing/cache'; import {MockCache} from '../testing/cache';
import {MockRequest} from '../testing/fetch'; import {MockRequest} from '../testing/fetch';
import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTable, tmpManifestSingleAssetGroup} from '../testing/mock'; import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTable, tmpManifestSingleAssetGroup} from '../testing/mock';
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope'; import {MockExtendableEvent, SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
(function() { (function() {
// Skip environments that don't support the minimum APIs needed to run the SW tests. // Skip environments that don't support the minimum APIs needed to run the SW tests.
@ -33,6 +33,8 @@ const scope = new SwTestHarnessBuilder().withServerState(server).build();
const db = new CacheDatabase(scope); const db = new CacheDatabase(scope);
const testEvent = new MockExtendableEvent('test');
describe('prefetch assets', () => { describe('prefetch assets', () => {
let group: PrefetchAssetGroup; let group: PrefetchAssetGroup;
@ -50,8 +52,8 @@ describe('prefetch assets', () => {
it('fully caches the two files', async () => { it('fully caches the two files', async () => {
await group.initializeFully(); await group.initializeFully();
scope.updateServerState(); scope.updateServerState();
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope); const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), testEvent);
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope); const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), testEvent);
expect(await res1!.text()).toEqual('this is foo'); expect(await res1!.text()).toEqual('this is foo');
expect(await res2!.text()).toEqual('this is bar'); expect(await res2!.text()).toEqual('this is bar');
}); });
@ -63,14 +65,14 @@ describe('prefetch assets', () => {
freshScope, freshScope, idle, manifest.assetGroups![0], tmpHashTable(manifest), freshScope, freshScope, idle, manifest.assetGroups![0], tmpHashTable(manifest),
new CacheDatabase(freshScope), 'test'); new CacheDatabase(freshScope), 'test');
await group.initializeFully(); await group.initializeFully();
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope); const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), testEvent);
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope); const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), testEvent);
expect(await res1!.text()).toEqual('this is foo'); expect(await res1!.text()).toEqual('this is foo');
expect(await res2!.text()).toEqual('this is bar'); expect(await res2!.text()).toEqual('this is bar');
}); });
it('caches properly if resources are requested before initialization', async () => { it('caches properly if resources are requested before initialization', async () => {
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope); const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), testEvent);
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope); const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), testEvent);
expect(await res1!.text()).toEqual('this is foo'); expect(await res1!.text()).toEqual('this is foo');
expect(await res2!.text()).toEqual('this is bar'); expect(await res2!.text()).toEqual('this is bar');
scope.updateServerState(); scope.updateServerState();
@ -90,7 +92,7 @@ describe('prefetch assets', () => {
it('CacheQueryOptions are passed through', async () => { it('CacheQueryOptions are passed through', async () => {
await group.initializeFully(); await group.initializeFully();
const matchSpy = spyOn(MockCache.prototype, 'match').and.callThrough(); const matchSpy = spyOn(MockCache.prototype, 'match').and.callThrough();
await group.handleFetch(scope.newRequest('/foo.txt'), scope); await group.handleFetch(scope.newRequest('/foo.txt'), testEvent);
expect(matchSpy).toHaveBeenCalledWith(new MockRequest('/foo.txt'), {ignoreVary: true}); expect(matchSpy).toHaveBeenCalledWith(new MockRequest('/foo.txt'), {ignoreVary: true});
}); });
}); });

View File

@ -8,7 +8,7 @@
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {Adapter, Context} from '../src/adapter'; import {Adapter} from '../src/adapter';
import {AssetGroupConfig, Manifest} from '../src/manifest'; import {AssetGroupConfig, Manifest} from '../src/manifest';
import {sha1} from '../src/sha1'; import {sha1} from '../src/sha1';
@ -105,8 +105,7 @@ export class MockClients implements Clients {
async claim(): Promise<any> {} async claim(): Promise<any> {}
} }
export class SwTestHarness extends Adapter<MockCacheStorage> implements Context, export class SwTestHarness extends Adapter<MockCacheStorage> implements ServiceWorkerGlobalScope {
ServiceWorkerGlobalScope {
readonly clients = new MockClients(); readonly clients = new MockClients();
private eventHandlers = new Map<string, Function>(); private eventHandlers = new Map<string, Function>();
private skippedWaiting = false; private skippedWaiting = false;
@ -239,8 +238,6 @@ export class SwTestHarness extends Adapter<MockCacheStorage> implements Context,
this.skippedWaiting = true; this.skippedWaiting = true;
} }
waitUntil(promise: Promise<void>): void {}
handleFetch(req: Request, clientId = ''): [Promise<Response|undefined>, Promise<void>] { handleFetch(req: Request, clientId = ''): [Promise<Response|undefined>, Promise<void>] {
if (!this.eventHandlers.has('fetch')) { if (!this.eventHandlers.has('fetch')) {
throw new Error('No fetch handler registered'); throw new Error('No fetch handler registered');
@ -376,7 +373,49 @@ export class ConfigBuilder {
} }
} }
class OneTimeContext implements Context { class MockEvent implements Event {
readonly AT_TARGET = -1;
readonly BUBBLING_PHASE = -1;
readonly CAPTURING_PHASE = -1;
readonly NONE = -1;
readonly bubbles = false;
cancelBubble = false;
readonly cancelable = false;
readonly composed = false;
readonly currentTarget = null;
readonly defaultPrevented = false;
readonly eventPhase = -1;
readonly isTrusted = false;
returnValue = false;
readonly srcElement = null;
readonly target = null;
readonly timeStamp = Date.now();
constructor(readonly type: string) {}
composedPath(): EventTarget[] {
this.notImplemented();
}
initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void {
this.notImplemented();
}
preventDefault(): void {
this.notImplemented();
}
stopImmediatePropagation(): void {
this.notImplemented();
}
stopPropagation(): void {
this.notImplemented();
}
private notImplemented(): never {
throw new Error('Method not implemented in `MockEvent`.');
}
}
export class MockExtendableEvent extends MockEvent implements ExtendableEvent {
private queue: Promise<void>[] = []; private queue: Promise<void>[] = [];
waitUntil(promise: Promise<void>): void { waitUntil(promise: Promise<void>): void {
@ -392,14 +431,12 @@ class OneTimeContext implements Context {
} }
} }
class MockExtendableEvent extends OneTimeContext {}
class MockFetchEvent extends MockExtendableEvent { class MockFetchEvent extends MockExtendableEvent {
response: Promise<Response|undefined> = Promise.resolve(undefined); response: Promise<Response|undefined> = Promise.resolve(undefined);
constructor( constructor(
readonly request: Request, readonly clientId: string, readonly resultingClientId: string) { readonly request: Request, readonly clientId: string, readonly resultingClientId: string) {
super(); super('fetch');
} }
respondWith(promise: Promise<Response>): Promise<Response> { respondWith(promise: Promise<Response>): Promise<Response> {
@ -410,13 +447,13 @@ class MockFetchEvent extends MockExtendableEvent {
class MockMessageEvent extends MockExtendableEvent { class MockMessageEvent extends MockExtendableEvent {
constructor(readonly data: Object, readonly source: MockClient|null) { constructor(readonly data: Object, readonly source: MockClient|null) {
super(); super('message');
} }
} }
class MockPushEvent extends MockExtendableEvent { class MockPushEvent extends MockExtendableEvent {
constructor(private _data: Object) { constructor(private _data: Object) {
super(); super('push');
} }
data = { data = {
json: () => this._data, json: () => this._data,
@ -425,11 +462,19 @@ class MockPushEvent extends MockExtendableEvent {
class MockNotificationEvent extends MockExtendableEvent { class MockNotificationEvent extends MockExtendableEvent {
constructor(private _notification: any, readonly action?: string) { constructor(private _notification: any, readonly action?: string) {
super(); super('notification');
} }
readonly notification = {...this._notification, close: () => undefined}; readonly notification = {...this._notification, close: () => undefined};
} }
class MockInstallEvent extends MockExtendableEvent {} class MockInstallEvent extends MockExtendableEvent {
constructor() {
super('install');
}
}
class MockActivateEvent extends MockExtendableEvent {} class MockActivateEvent extends MockExtendableEvent {
constructor() {
super('activate');
}
}