feat(service-worker): support multiple apps on different subpaths of a domain (#27080)

Previously, it was not possible to have multiple apps (using
`@angular/service-worker`) on different subpaths of the same domain,
because each SW would overwrite the caches of the others (even though
their scope was different).

This commit fixes it by ensuring that the cache names created by the SW
are different for each scope.

Fixes #21388

PR Close #27080
This commit is contained in:
Sheik Althaf 2019-03-20 23:29:15 +02:00 committed by Matias Niemelä
parent 37a154e4e6
commit e721c08c7f
7 changed files with 34 additions and 15 deletions

View File

@ -12,5 +12,5 @@ import {Driver} from './src/driver';
const scope = self as any as ServiceWorkerGlobalScope;
const adapter = new Adapter();
const adapter = new Adapter(scope);
const driver = new Driver(scope, adapter, new CacheDatabase(scope, adapter));

View File

@ -13,6 +13,15 @@
* from the global scope.
*/
export class Adapter {
readonly cacheNamePrefix: 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 = new URL(scope.registration.scope).pathname;
this.cacheNamePrefix = 'ngsw:' + baseHref;
}
/**
* Wrapper around the `Request` constructor.
*/

View File

@ -72,7 +72,7 @@ export class AppVersion implements UpdateSource {
this.assetGroups = (manifest.assetGroups || []).map(config => {
// Every asset group has a cache that's prefixed by the manifest hash and the name of the
// group.
const prefix = `ngsw:${this.manifestHash}:assets`;
const prefix = `${adapter.cacheNamePrefix}:${this.manifestHash}:assets`;
// Check the caching mode, which determines when resources will be fetched/updated.
switch (config.installMode) {
case 'prefetch':
@ -89,7 +89,7 @@ export class AppVersion implements UpdateSource {
.map(
config => new DataGroup(
this.scope, this.adapter, config, this.database,
`ngsw:${config.version}:data`));
`${adapter.cacheNamePrefix}:${config.version}:data`));
// This keeps backwards compatibility with app versions without navigation urls.
// Fix: https://github.com/angular/angular/issues/27209

View File

@ -23,16 +23,17 @@ export class CacheDatabase implements Database {
if (this.tables.has(name)) {
this.tables.delete(name);
}
return this.scope.caches.delete(`ngsw:db:${name}`);
return this.scope.caches.delete(`${this.adapter.cacheNamePrefix}:db:${name}`);
}
list(): Promise<string[]> {
return this.scope.caches.keys().then(keys => keys.filter(key => key.startsWith('ngsw:db:')));
return this.scope.caches.keys().then(
keys => keys.filter(key => key.startsWith(`${this.adapter.cacheNamePrefix}:db:`)));
}
open(name: string): Promise<Table> {
if (!this.tables.has(name)) {
const table = this.scope.caches.open(`ngsw:db:${name}`)
const table = this.scope.caches.open(`${this.adapter.cacheNamePrefix}:db:${name}`)
.then(cache => new CacheTable(name, cache, this.adapter));
this.tables.set(name, table);
}

View File

@ -705,7 +705,7 @@ export class Driver implements Debuggable, UpdateSource {
private async deleteAllCaches(): Promise<void> {
await(await this.scope.caches.keys())
.filter(key => key.startsWith('ngsw:'))
.filter(key => key.startsWith(`${this.adapter.cacheNamePrefix}:`))
.reduce(async(previous, key) => {
await Promise.all([
previous,
@ -924,9 +924,7 @@ export class Driver implements Debuggable, UpdateSource {
*/
async cleanupOldSwCaches(): Promise<void> {
const cacheNames = await this.scope.caches.keys();
const oldSwCacheNames =
cacheNames.filter(name => /^ngsw:(?:active|staged|manifest:.+)$/.test(name));
const oldSwCacheNames = cacheNames.filter(name => /^ngsw:(?!\/)/.test(name));
await Promise.all(oldSwCacheNames.map(name => this.scope.caches.delete(name)));
}

View File

@ -587,7 +587,7 @@ import {async_beforeEach, async_fit, async_it} from './async';
serverUpdate.assertNoOtherRequests();
let keys = await scope.caches.keys();
let hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:${manifestHash}:`));
let hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`));
expect(hasOriginalCaches).toEqual(true);
scope.clients.remove('default');
@ -600,7 +600,7 @@ import {async_beforeEach, async_fit, async_it} from './async';
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2');
keys = await scope.caches.keys();
hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:${manifestHash}:`));
hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`));
expect(hasOriginalCaches).toEqual(false);
});
@ -938,13 +938,21 @@ import {async_beforeEach, async_fit, async_it} from './async';
describe('cleanupOldSwCaches()', () => {
async_it('should delete the correct caches', async() => {
const oldSwCacheNames = ['ngsw:active', 'ngsw:staged', 'ngsw:manifest:a1b2c3:super:duper'];
const oldSwCacheNames = [
// Example cache names from the beta versions of `@angular/service-worker`.
'ngsw:active',
'ngsw:staged',
'ngsw:manifest:a1b2c3:super:duper',
// Example cache names from the beta versions of `@angular/service-worker`.
'ngsw:a1b2c3:assets:foo',
'ngsw:db:a1b2c3:assets:bar',
];
const otherCacheNames = [
'ngsuu:active',
'not:ngsw:active',
'ngsw:staged:not',
'NgSw:StAgEd',
'ngsw:manifest',
'ngsw:/:active',
'ngsw:/foo/:staged',
];
const allCacheNames = oldSwCacheNames.concat(otherCacheNames);

View File

@ -74,6 +74,7 @@ export class MockClients implements Clients {
}
export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context {
readonly cacheNamePrefix: string;
readonly clients = new MockClients();
private eventHandlers = new Map<string, Function>();
private skippedWaiting = true;
@ -115,6 +116,8 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
constructor(private server: MockServerState, readonly caches: MockCacheStorage) {
this.time = Date.now();
const baseHref = new URL(this.registration.scope).pathname;
this.cacheNamePrefix = 'ngsw:' + baseHref;
}
async resolveSelfMessages(): Promise<void> {