refactor(service-worker): move asset URL normalization to `Adapter` (#37922)
This is in preparation of enabling the ServiceWorker to handle relative paths in `ngsw.json` (as discussed in #25055), which will require normalizing URLs in other parts of the ServiceWorker. PR Close #37922
This commit is contained in:
parent
2156beed0c
commit
d380e93b82
|
@ -12,5 +12,5 @@ import {Driver} from './src/driver';
|
|||
|
||||
const scope = self as any as ServiceWorkerGlobalScope;
|
||||
|
||||
const adapter = new Adapter(scope);
|
||||
const adapter = new Adapter(scope.registration.scope);
|
||||
const driver = new Driver(scope, adapter, new CacheDatabase(scope, adapter));
|
||||
|
|
|
@ -14,12 +14,18 @@
|
|||
*/
|
||||
export class Adapter {
|
||||
readonly cacheNamePrefix: string;
|
||||
private readonly origin: string;
|
||||
|
||||
constructor(scope: ServiceWorkerGlobalScope) {
|
||||
// Suffixing `ngsw` with the baseHref to avoid clash of cache names
|
||||
// for SWs with different scopes on the same domain.
|
||||
const baseHref = this.parseUrl(scope.registration.scope).path;
|
||||
this.cacheNamePrefix = 'ngsw:' + baseHref;
|
||||
constructor(protected readonly scopeUrl: string) {
|
||||
const parsedScopeUrl = this.parseUrl(this.scopeUrl);
|
||||
|
||||
// Determine the origin from the registration scope. This is used to differentiate between
|
||||
// relative and absolute URLs.
|
||||
this.origin = parsedScopeUrl.origin;
|
||||
|
||||
// Suffixing `ngsw` with the baseHref to avoid clash of cache names for SWs with different
|
||||
// scopes on the same domain.
|
||||
this.cacheNamePrefix = 'ngsw:' + parsedScopeUrl.path;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,7 +64,31 @@ export class Adapter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extract the pathname of a URL.
|
||||
* Get a normalized representation of a URL such as those found in the ServiceWorker's `ngsw.json`
|
||||
* configuration.
|
||||
*
|
||||
* More specifically:
|
||||
* 1. Resolve the URL relative to the ServiceWorker's scope.
|
||||
* 2. If the URL is relative to the ServiceWorker's own origin, then only return the path part.
|
||||
* Otherwise, return the full URL.
|
||||
*
|
||||
* @param url The raw request URL.
|
||||
* @return A normalized representation of the URL.
|
||||
*/
|
||||
normalizeUrl(url: string): string {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a URL into its different parts, such as `origin`, `path` and `search`.
|
||||
*/
|
||||
parseUrl(url: string, relativeTo?: string): {origin: string, path: string, search: string} {
|
||||
// Workaround a Safari bug, see
|
||||
|
|
|
@ -47,9 +47,6 @@ export abstract class AssetGroup {
|
|||
*/
|
||||
protected metadata: Promise<Table>;
|
||||
|
||||
|
||||
private origin: string;
|
||||
|
||||
constructor(
|
||||
protected scope: ServiceWorkerGlobalScope, protected adapter: Adapter,
|
||||
protected idle: IdleScheduler, protected config: AssetGroupConfig,
|
||||
|
@ -67,10 +64,6 @@ export abstract class AssetGroup {
|
|||
// the timestamp of when it was added to the cache.
|
||||
this.metadata =
|
||||
this.db.open(`${this.prefix}:${this.config.name}:meta`, this.config.cacheQueryOptions);
|
||||
|
||||
// Determine the origin from the registration scope. This is used to differentiate between
|
||||
// relative and absolute URLs.
|
||||
this.origin = this.adapter.parseUrl(this.scope.registration.scope).origin;
|
||||
}
|
||||
|
||||
async cacheStatus(url: string): Promise<UpdateCacheStatus> {
|
||||
|
@ -108,7 +101,7 @@ export abstract class AssetGroup {
|
|||
* 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> {
|
||||
const url = this.getConfigUrl(req.url);
|
||||
const url = this.adapter.normalizeUrl(req.url);
|
||||
// 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
|
||||
// order to decide how to handle it.
|
||||
|
@ -156,18 +149,6 @@ export abstract class AssetGroup {
|
|||
}
|
||||
}
|
||||
|
||||
private getConfigUrl(url: string): string {
|
||||
// If the URL is relative to the SW's own origin, then only consider the path relative to
|
||||
// the domain root. Determine this by checking the URL's origin against the SW's.
|
||||
const parsed = this.adapter.parseUrl(url, this.scope.registration.scope);
|
||||
if (parsed.origin === this.origin) {
|
||||
// The URL is relative to the SW's origin domain.
|
||||
return parsed.path;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some resources are cached without a hash, meaning that their expiration is controlled
|
||||
* by HTTP caching headers. Check whether the given request/response pair is still valid
|
||||
|
@ -373,7 +354,7 @@ export abstract class AssetGroup {
|
|||
* Load a particular asset from the network, accounting for hash validation.
|
||||
*/
|
||||
protected async cacheBustedFetchFromNetwork(req: Request): Promise<Response> {
|
||||
const url = this.getConfigUrl(req.url);
|
||||
const url = this.adapter.normalizeUrl(req.url);
|
||||
|
||||
// If a hash is available for this resource, then compare the fetched version with the
|
||||
// canonical hash. Otherwise, the network version will have to be trusted.
|
||||
|
@ -456,7 +437,7 @@ export abstract class AssetGroup {
|
|||
*/
|
||||
protected async maybeUpdate(updateFrom: UpdateSource, req: Request, cache: Cache):
|
||||
Promise<boolean> {
|
||||
const url = this.getConfigUrl(req.url);
|
||||
const url = this.adapter.normalizeUrl(req.url);
|
||||
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)) {
|
||||
|
@ -546,7 +527,7 @@ export class PrefetchAssetGroup extends AssetGroup {
|
|||
// 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) ||
|
||||
url => this.config.urls.indexOf(url) !== -1 ||
|
||||
this.patterns.some(pattern => pattern.test(url)))
|
||||
// Finally, process each resource in turn.
|
||||
.reduce(async (previous, url) => {
|
||||
|
|
|
@ -81,8 +81,7 @@ export class MockClients implements Clients {
|
|||
async claim(): Promise<any> {}
|
||||
}
|
||||
|
||||
export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context {
|
||||
readonly cacheNamePrefix: string;
|
||||
export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope, Context {
|
||||
readonly clients = new MockClients();
|
||||
private eventHandlers = new Map<string, Function>();
|
||||
private skippedWaiting = true;
|
||||
|
@ -98,7 +97,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||
this.selfMessageQueue.push(msg);
|
||||
},
|
||||
},
|
||||
scope: this.origin,
|
||||
scope: this.scopeUrl,
|
||||
showNotification:
|
||||
(title: string, options: Object) => {
|
||||
this.notifications.push({title, options});
|
||||
|
@ -125,7 +124,11 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||
return url && (typeof url.parse === 'function') && (typeof url.resolve === 'function');
|
||||
}
|
||||
|
||||
time: number;
|
||||
get time() {
|
||||
return this.mockTime;
|
||||
}
|
||||
|
||||
private mockTime = Date.now();
|
||||
|
||||
private timers: {
|
||||
at: number,
|
||||
|
@ -135,10 +138,8 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||
}[] = [];
|
||||
|
||||
constructor(
|
||||
private server: MockServerState, readonly caches: MockCacheStorage, private origin: string) {
|
||||
const baseHref = this.parseUrl(origin).path;
|
||||
this.cacheNamePrefix = 'ngsw:' + baseHref;
|
||||
this.time = Date.now();
|
||||
private server: MockServerState, readonly caches: MockCacheStorage, scopeUrl: string) {
|
||||
super(scopeUrl);
|
||||
}
|
||||
|
||||
async resolveSelfMessages(): Promise<void> {
|
||||
|
@ -170,6 +171,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||
}
|
||||
return skippedWaiting;
|
||||
}
|
||||
|
||||
updateServerState(server?: MockServerState): void {
|
||||
this.server = server || EMPTY_SERVER_STATE;
|
||||
}
|
||||
|
@ -281,7 +283,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||
timeout(ms: number): Promise<void> {
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.timers.push({
|
||||
at: this.time + ms,
|
||||
at: this.mockTime + ms,
|
||||
duration: ms,
|
||||
fn: resolve,
|
||||
fired: false,
|
||||
|
@ -296,9 +298,9 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||
}
|
||||
|
||||
advance(by: number): void {
|
||||
this.time += by;
|
||||
this.mockTime += by;
|
||||
this.timers.filter(timer => !timer.fired)
|
||||
.filter(timer => timer.at <= this.time)
|
||||
.filter(timer => timer.at <= this.mockTime)
|
||||
.forEach(timer => {
|
||||
timer.fired = true;
|
||||
timer.fn();
|
||||
|
|
Loading…
Reference in New Issue