refactor(service-worker): use nominal type for normalized URLs (#37922)

Some ServiceWorker operations and methods require normalized URLs.
Previously, the generic `string` type was used.

This commit introduces a new `NormalizedUrl` type, a special kind of
`string`, to make this requirement explicit and use the type system to
enforce it.

PR Close #37922
This commit is contained in:
George Kalpakas 2020-07-06 16:55:39 +03:00 committed by atscott
parent d19ef6534f
commit fb735d625b
6 changed files with 41 additions and 30 deletions

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NormalizedUrl} from './api';
/**
* Adapts the service worker to its runtime environment.
*
@ -75,16 +78,10 @@ export class Adapter {
* @param url The raw request URL.
* @return A normalized representation of the URL.
*/
normalizeUrl(url: string): string {
normalizeUrl(url: string): NormalizedUrl {
// Check the URL's origin against the ServiceWorker's.
const parsed = this.parseUrl(url, this.scopeUrl);
if (parsed.origin === this.origin) {
// The URL is relative to the SW's origin: Return the path only.
return parsed.path;
} else {
// The URL is not relative to the SW's origin: Return the full URL.
return url;
}
return (parsed.origin === this.origin ? parsed.path : url) as NormalizedUrl;
}
/**

View File

@ -12,6 +12,18 @@ export enum UpdateCacheStatus {
CACHED,
}
/**
* A `string` representing a URL that has been normalized relative to an origin (usually that of the
* ServiceWorker).
*
* If the URL is relative to the origin, then it is representated by the path part only. Otherwise,
* the full URL is used.
*
* NOTE: A `string` is not assignable to a `NormalizedUrl`, but a `NormalizedUrl` is assignable to a
* `string`.
*/
export type NormalizedUrl = string&{_brand: 'normalizedUrl'};
/**
* A source for old versions of URL contents and other resources.
*
@ -27,7 +39,7 @@ export interface UpdateSource {
* If an old version of the resource doesn't exist, or exists but does
* not match the hash given, this returns null.
*/
lookupResourceWithHash(url: string, hash: string): Promise<Response|null>;
lookupResourceWithHash(url: NormalizedUrl, hash: string): Promise<Response|null>;
/**
* Lookup an older version of a resource for which the hash is not known.
@ -37,7 +49,7 @@ export interface UpdateSource {
* `Response`, but the cache metadata needed to re-cache the resource in
* a newer `AppVersion`.
*/
lookupResourceWithoutHash(url: string): Promise<CacheState|null>;
lookupResourceWithoutHash(url: NormalizedUrl): Promise<CacheState|null>;
/**
* List the URLs of all of the resources which were previously cached.
@ -45,7 +57,7 @@ export interface UpdateSource {
* This allows for the discovery of resources which are not listed in the
* manifest but which were picked up because they matched URL patterns.
*/
previouslyCachedResources(): Promise<string[]>;
previouslyCachedResources(): Promise<NormalizedUrl[]>;
/**
* Check whether a particular resource exists in the most recent cache.

View File

@ -7,7 +7,7 @@
*/
import {Adapter, Context} from './adapter';
import {CacheState, UpdateCacheStatus, UpdateSource} from './api';
import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource} from './api';
import {AssetGroup, LazyAssetGroup, PrefetchAssetGroup} from './assets';
import {DataGroup} from './data';
import {Database} from './database';
@ -31,10 +31,9 @@ const BACKWARDS_COMPATIBILITY_NAVIGATION_URLS = [
*/
export class AppVersion implements UpdateSource {
/**
* A Map of absolute URL paths (/foo.txt) to the known hash of their
* contents (if available).
* A Map of absolute URL paths (`/foo.txt`) to the known hash of their contents (if available).
*/
private hashTable = new Map<string, string>();
private hashTable = new Map<NormalizedUrl, string>();
/**
* All of the asset groups active in this version of the app.
@ -218,7 +217,7 @@ export class AppVersion implements UpdateSource {
/**
* Check this version for a given resource with a particular hash.
*/
async lookupResourceWithHash(url: string, hash: string): Promise<Response|null> {
async lookupResourceWithHash(url: NormalizedUrl, hash: string): Promise<Response|null> {
// Verify that this version has the requested resource cached. If not,
// there's no point in trying.
if (!this.hashTable.has(url)) {
@ -238,7 +237,7 @@ export class AppVersion implements UpdateSource {
/**
* Check this version for a given resource regardless of its hash.
*/
lookupResourceWithoutHash(url: string): Promise<CacheState|null> {
lookupResourceWithoutHash(url: NormalizedUrl): 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) => {
@ -256,10 +255,10 @@ export class AppVersion implements UpdateSource {
/**
* List all unhashed resources from all asset groups.
*/
previouslyCachedResources(): Promise<string[]> {
return this.assetGroups.reduce(async (resources, group) => {
return (await resources).concat(await group.unhashedResources());
}, Promise.resolve<string[]>([]));
previouslyCachedResources(): Promise<NormalizedUrl[]> {
return this.assetGroups.reduce(
async (resources, group) => (await resources).concat(await group.unhashedResources()),
Promise.resolve<NormalizedUrl[]>([]));
}
async recentCacheStatus(url: string): Promise<UpdateCacheStatus> {

View File

@ -7,7 +7,7 @@
*/
import {Adapter, Context} from './adapter';
import {CacheState, UpdateCacheStatus, UpdateSource, UrlMetadata} from './api';
import {CacheState, NormalizedUrl, UpdateCacheStatus, UpdateSource, UrlMetadata} from './api';
import {Database, Table} from './database';
import {errorToString, SwCriticalError} from './error';
import {IdleScheduler} from './idle';
@ -29,7 +29,7 @@ export abstract class AssetGroup {
/**
* Normalized resource URLs.
*/
protected urls: string[] = [];
protected urls: NormalizedUrl[] = [];
/**
* Regular expression patterns.
@ -266,7 +266,7 @@ export abstract class AssetGroup {
/**
* Lookup all resources currently stored in the cache which have no associated hash.
*/
async unhashedResources(): Promise<string[]> {
async unhashedResources(): Promise<NormalizedUrl[]> {
const cache = await this.cache;
// Start with the set of all cached requests.
return (await cache.keys())

View File

@ -7,7 +7,7 @@
*/
import {Adapter} from './adapter';
import {CacheState, Debuggable, DebugIdleState, DebugState, DebugVersion, UpdateCacheStatus, UpdateSource} from './api';
import {CacheState, Debuggable, DebugIdleState, DebugState, DebugVersion, NormalizedUrl, UpdateCacheStatus, UpdateSource} from './api';
import {AppVersion} from './app-version';
import {Database} from './database';
import {DebugHandler} from './debug';
@ -959,7 +959,7 @@ export class Driver implements Debuggable, UpdateSource {
* Determine if a specific version of the given resource is cached anywhere within the SW,
* and fetch it if so.
*/
lookupResourceWithHash(url: string, hash: string): Promise<Response|null> {
lookupResourceWithHash(url: NormalizedUrl, hash: string): Promise<Response|null> {
return Array
// Scan through the set of all cached versions, valid or otherwise. It's safe to do such
// lookups even for invalid versions as the cached version of a resource will have the
@ -981,13 +981,13 @@ export class Driver implements Debuggable, UpdateSource {
}, Promise.resolve<Response|null>(null));
}
async lookupResourceWithoutHash(url: string): Promise<CacheState|null> {
async lookupResourceWithoutHash(url: NormalizedUrl): Promise<CacheState|null> {
await this.initialized;
const version = this.versions.get(this.latestHash!);
return version ? version.lookupResourceWithoutHash(url) : null;
}
async previouslyCachedResources(): Promise<string[]> {
async previouslyCachedResources(): Promise<NormalizedUrl[]> {
await this.initialized;
const version = this.versions.get(this.latestHash!);
return version ? version.previouslyCachedResources() : [];

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NormalizedUrl} from '../src/api';
/**
* Get a normalized representation of a URL relative to a provided base URL.
*
@ -19,11 +22,11 @@
* (This is usually the ServiceWorker's origin or registration scope).
* @return A normalized representation of the URL.
*/
export function normalizeUrl(url: string, relativeTo: string): string {
export function normalizeUrl(url: string, relativeTo: string): NormalizedUrl {
const {origin, path, search} = parseUrl(url, relativeTo);
const {origin: relativeToOrigin} = parseUrl(relativeTo);
return (origin === relativeToOrigin) ? path + search : url;
return ((origin === relativeToOrigin) ? path + search : url) as NormalizedUrl;
}
/**