From e721c08c7f8136147d428fbf29cea2354fcc700c Mon Sep 17 00:00:00 2001 From: Sheik Althaf Date: Wed, 20 Mar 2019 23:29:15 +0200 Subject: [PATCH] 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 --- packages/service-worker/worker/main.ts | 2 +- packages/service-worker/worker/src/adapter.ts | 9 +++++++++ .../service-worker/worker/src/app-version.ts | 4 ++-- packages/service-worker/worker/src/db-cache.ts | 7 ++++--- packages/service-worker/worker/src/driver.ts | 6 ++---- .../service-worker/worker/test/happy_spec.ts | 18 +++++++++++++----- .../service-worker/worker/testing/scope.ts | 3 +++ 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/service-worker/worker/main.ts b/packages/service-worker/worker/main.ts index 11f86ccb5e..dab1ec368e 100644 --- a/packages/service-worker/worker/main.ts +++ b/packages/service-worker/worker/main.ts @@ -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)); diff --git a/packages/service-worker/worker/src/adapter.ts b/packages/service-worker/worker/src/adapter.ts index 2e2847d62e..5955cdd56b 100644 --- a/packages/service-worker/worker/src/adapter.ts +++ b/packages/service-worker/worker/src/adapter.ts @@ -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. */ diff --git a/packages/service-worker/worker/src/app-version.ts b/packages/service-worker/worker/src/app-version.ts index dafdcabfe0..5db2d6d3a4 100644 --- a/packages/service-worker/worker/src/app-version.ts +++ b/packages/service-worker/worker/src/app-version.ts @@ -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 diff --git a/packages/service-worker/worker/src/db-cache.ts b/packages/service-worker/worker/src/db-cache.ts index 581aadf2fb..2a4b112821 100644 --- a/packages/service-worker/worker/src/db-cache.ts +++ b/packages/service-worker/worker/src/db-cache.ts @@ -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 { - 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 { 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); } diff --git a/packages/service-worker/worker/src/driver.ts b/packages/service-worker/worker/src/driver.ts index 186f21ed80..d7b69ba57f 100644 --- a/packages/service-worker/worker/src/driver.ts +++ b/packages/service-worker/worker/src/driver.ts @@ -705,7 +705,7 @@ export class Driver implements Debuggable, UpdateSource { private async deleteAllCaches(): Promise { 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 { 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))); } diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index 59ce6d6d1f..95bf43eec3 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -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); diff --git a/packages/service-worker/worker/testing/scope.ts b/packages/service-worker/worker/testing/scope.ts index f2c0d1f66e..1ce31cde4b 100644 --- a/packages/service-worker/worker/testing/scope.ts +++ b/packages/service-worker/worker/testing/scope.ts @@ -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(); 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 {