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 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));
|
const driver = new Driver(scope, adapter, new CacheDatabase(scope, adapter));
|
||||||
|
@ -14,12 +14,18 @@
|
|||||||
*/
|
*/
|
||||||
export class Adapter {
|
export class Adapter {
|
||||||
readonly cacheNamePrefix: string;
|
readonly cacheNamePrefix: string;
|
||||||
|
private readonly origin: string;
|
||||||
|
|
||||||
constructor(scope: ServiceWorkerGlobalScope) {
|
constructor(protected readonly scopeUrl: string) {
|
||||||
// Suffixing `ngsw` with the baseHref to avoid clash of cache names
|
const parsedScopeUrl = this.parseUrl(this.scopeUrl);
|
||||||
// for SWs with different scopes on the same domain.
|
|
||||||
const baseHref = this.parseUrl(scope.registration.scope).path;
|
// Determine the origin from the registration scope. This is used to differentiate between
|
||||||
this.cacheNamePrefix = 'ngsw:' + baseHref;
|
// 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} {
|
parseUrl(url: string, relativeTo?: string): {origin: string, path: string, search: string} {
|
||||||
// Workaround a Safari bug, see
|
// Workaround a Safari bug, see
|
||||||
|
@ -47,9 +47,6 @@ export abstract class AssetGroup {
|
|||||||
*/
|
*/
|
||||||
protected metadata: Promise<Table>;
|
protected metadata: Promise<Table>;
|
||||||
|
|
||||||
|
|
||||||
private origin: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected scope: ServiceWorkerGlobalScope, protected adapter: Adapter,
|
protected scope: ServiceWorkerGlobalScope, protected adapter: Adapter,
|
||||||
protected idle: IdleScheduler, protected config: AssetGroupConfig,
|
protected idle: IdleScheduler, protected config: AssetGroupConfig,
|
||||||
@ -67,10 +64,6 @@ export abstract class AssetGroup {
|
|||||||
// the timestamp of when it was added to the cache.
|
// the timestamp of when it was added to the cache.
|
||||||
this.metadata =
|
this.metadata =
|
||||||
this.db.open(`${this.prefix}:${this.config.name}:meta`, this.config.cacheQueryOptions);
|
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> {
|
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.
|
* 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, 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
|
// 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
|
||||||
// order to decide how to handle it.
|
// 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
|
* 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
|
* 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.
|
* Load a particular asset from the network, accounting for hash validation.
|
||||||
*/
|
*/
|
||||||
protected async cacheBustedFetchFromNetwork(req: Request): Promise<Response> {
|
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
|
// 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.
|
// 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):
|
protected async maybeUpdate(updateFrom: UpdateSource, req: Request, cache: Cache):
|
||||||
Promise<boolean> {
|
Promise<boolean> {
|
||||||
const url = this.getConfigUrl(req.url);
|
const url = this.adapter.normalizeUrl(req.url);
|
||||||
const meta = await this.metadata;
|
const meta = await this.metadata;
|
||||||
// Check if this resource is hashed and already exists in the cache of a prior version.
|
// Check if this resource is hashed and already exists in the cache of a prior version.
|
||||||
if (this.hashes.has(url)) {
|
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.
|
// 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.
|
// Either it's a known URL, or it matches a given pattern.
|
||||||
.filter(
|
.filter(
|
||||||
url => this.config.urls.some(cacheUrl => cacheUrl === url) ||
|
url => this.config.urls.indexOf(url) !== -1 ||
|
||||||
this.patterns.some(pattern => pattern.test(url)))
|
this.patterns.some(pattern => pattern.test(url)))
|
||||||
// Finally, process each resource in turn.
|
// Finally, process each resource in turn.
|
||||||
.reduce(async (previous, url) => {
|
.reduce(async (previous, url) => {
|
||||||
|
@ -81,8 +81,7 @@ export class MockClients implements Clients {
|
|||||||
async claim(): Promise<any> {}
|
async claim(): Promise<any> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context {
|
export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope, Context {
|
||||||
readonly cacheNamePrefix: string;
|
|
||||||
readonly clients = new MockClients();
|
readonly clients = new MockClients();
|
||||||
private eventHandlers = new Map<string, Function>();
|
private eventHandlers = new Map<string, Function>();
|
||||||
private skippedWaiting = true;
|
private skippedWaiting = true;
|
||||||
@ -98,7 +97,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
this.selfMessageQueue.push(msg);
|
this.selfMessageQueue.push(msg);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scope: this.origin,
|
scope: this.scopeUrl,
|
||||||
showNotification:
|
showNotification:
|
||||||
(title: string, options: Object) => {
|
(title: string, options: Object) => {
|
||||||
this.notifications.push({title, options});
|
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');
|
return url && (typeof url.parse === 'function') && (typeof url.resolve === 'function');
|
||||||
}
|
}
|
||||||
|
|
||||||
time: number;
|
get time() {
|
||||||
|
return this.mockTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mockTime = Date.now();
|
||||||
|
|
||||||
private timers: {
|
private timers: {
|
||||||
at: number,
|
at: number,
|
||||||
@ -135,10 +138,8 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private server: MockServerState, readonly caches: MockCacheStorage, private origin: string) {
|
private server: MockServerState, readonly caches: MockCacheStorage, scopeUrl: string) {
|
||||||
const baseHref = this.parseUrl(origin).path;
|
super(scopeUrl);
|
||||||
this.cacheNamePrefix = 'ngsw:' + baseHref;
|
|
||||||
this.time = Date.now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveSelfMessages(): Promise<void> {
|
async resolveSelfMessages(): Promise<void> {
|
||||||
@ -170,6 +171,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
}
|
}
|
||||||
return skippedWaiting;
|
return skippedWaiting;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateServerState(server?: MockServerState): void {
|
updateServerState(server?: MockServerState): void {
|
||||||
this.server = server || EMPTY_SERVER_STATE;
|
this.server = server || EMPTY_SERVER_STATE;
|
||||||
}
|
}
|
||||||
@ -281,7 +283,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
timeout(ms: number): Promise<void> {
|
timeout(ms: number): Promise<void> {
|
||||||
const promise = new Promise<void>(resolve => {
|
const promise = new Promise<void>(resolve => {
|
||||||
this.timers.push({
|
this.timers.push({
|
||||||
at: this.time + ms,
|
at: this.mockTime + ms,
|
||||||
duration: ms,
|
duration: ms,
|
||||||
fn: resolve,
|
fn: resolve,
|
||||||
fired: false,
|
fired: false,
|
||||||
@ -296,9 +298,9 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
advance(by: number): void {
|
advance(by: number): void {
|
||||||
this.time += by;
|
this.mockTime += by;
|
||||||
this.timers.filter(timer => !timer.fired)
|
this.timers.filter(timer => !timer.fired)
|
||||||
.filter(timer => timer.at <= this.time)
|
.filter(timer => timer.at <= this.mockTime)
|
||||||
.forEach(timer => {
|
.forEach(timer => {
|
||||||
timer.fired = true;
|
timer.fired = true;
|
||||||
timer.fn();
|
timer.fn();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user