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 * found in the LICENSE file at https://angular.io/license
*/ */
import {NormalizedUrl} from './api';
/** /**
* Adapts the service worker to its runtime environment. * Adapts the service worker to its runtime environment.
* *
@ -75,16 +78,10 @@ export class Adapter {
* @param url The raw request URL. * @param url The raw request URL.
* @return A normalized representation of the 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. // Check the URL's origin against the ServiceWorker's.
const parsed = this.parseUrl(url, this.scopeUrl); const parsed = this.parseUrl(url, this.scopeUrl);
if (parsed.origin === this.origin) { return (parsed.origin === this.origin ? parsed.path : url) as NormalizedUrl;
// 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;
}
} }
/** /**

View File

@ -12,6 +12,18 @@ export enum UpdateCacheStatus {
CACHED, 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. * 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 * If an old version of the resource doesn't exist, or exists but does
* not match the hash given, this returns null. * 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. * 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 * `Response`, but the cache metadata needed to re-cache the resource in
* a newer `AppVersion`. * 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. * 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 * This allows for the discovery of resources which are not listed in the
* manifest but which were picked up because they matched URL patterns. * 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. * Check whether a particular resource exists in the most recent cache.

View File

@ -7,7 +7,7 @@
*/ */
import {Adapter, Context} from './adapter'; 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 {AssetGroup, LazyAssetGroup, PrefetchAssetGroup} from './assets';
import {DataGroup} from './data'; import {DataGroup} from './data';
import {Database} from './database'; import {Database} from './database';
@ -31,10 +31,9 @@ const BACKWARDS_COMPATIBILITY_NAVIGATION_URLS = [
*/ */
export class AppVersion implements UpdateSource { export class AppVersion implements UpdateSource {
/** /**
* A Map of absolute URL paths (/foo.txt) to the known hash of their * A Map of absolute URL paths (`/foo.txt`) to the known hash of their contents (if available).
* 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. * 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. * 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, // Verify that this version has the requested resource cached. If not,
// there's no point in trying. // there's no point in trying.
if (!this.hashTable.has(url)) { 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. * 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 // Limit the search to asset groups, and only scan the cache, don't
// load resources from the network. // load resources from the network.
return this.assetGroups.reduce(async (potentialResponse, group) => { return this.assetGroups.reduce(async (potentialResponse, group) => {
@ -256,10 +255,10 @@ export class AppVersion implements UpdateSource {
/** /**
* List all unhashed resources from all asset groups. * List all unhashed resources from all asset groups.
*/ */
previouslyCachedResources(): Promise<string[]> { previouslyCachedResources(): Promise<NormalizedUrl[]> {
return this.assetGroups.reduce(async (resources, group) => { return this.assetGroups.reduce(
return (await resources).concat(await group.unhashedResources()); async (resources, group) => (await resources).concat(await group.unhashedResources()),
}, Promise.resolve<string[]>([])); Promise.resolve<NormalizedUrl[]>([]));
} }
async recentCacheStatus(url: string): Promise<UpdateCacheStatus> { async recentCacheStatus(url: string): Promise<UpdateCacheStatus> {

View File

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

View File

@ -7,7 +7,7 @@
*/ */
import {Adapter} from './adapter'; 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 {AppVersion} from './app-version';
import {Database} from './database'; import {Database} from './database';
import {DebugHandler} from './debug'; 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, * Determine if a specific version of the given resource is cached anywhere within the SW,
* and fetch it if so. * and fetch it if so.
*/ */
lookupResourceWithHash(url: string, hash: string): Promise<Response|null> { lookupResourceWithHash(url: NormalizedUrl, hash: string): Promise<Response|null> {
return Array return Array
// Scan through the set of all cached versions, valid or otherwise. It's safe to do such // 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 // 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)); }, Promise.resolve<Response|null>(null));
} }
async lookupResourceWithoutHash(url: string): Promise<CacheState|null> { async lookupResourceWithoutHash(url: NormalizedUrl): Promise<CacheState|null> {
await this.initialized; await this.initialized;
const version = this.versions.get(this.latestHash!); const version = this.versions.get(this.latestHash!);
return version ? version.lookupResourceWithoutHash(url) : null; return version ? version.lookupResourceWithoutHash(url) : null;
} }
async previouslyCachedResources(): Promise<string[]> { async previouslyCachedResources(): Promise<NormalizedUrl[]> {
await this.initialized; await this.initialized;
const version = this.versions.get(this.latestHash!); const version = this.versions.get(this.latestHash!);
return version ? version.previouslyCachedResources() : []; return version ? version.previouslyCachedResources() : [];

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license * 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. * 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). * (This is usually the ServiceWorker's origin or registration scope).
* @return A normalized representation of the URL. * @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, path, search} = parseUrl(url, relativeTo);
const {origin: relativeToOrigin} = parseUrl(relativeTo); const {origin: relativeToOrigin} = parseUrl(relativeTo);
return (origin === relativeToOrigin) ? path + search : url; return ((origin === relativeToOrigin) ? path + search : url) as NormalizedUrl;
} }
/** /**