Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unregister non-matching serviceworkers #15834

Merged
merged 10 commits into from
May 12, 2021
3 changes: 2 additions & 1 deletion models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ var (
"debug",
"error",
"explore",
"favicon.ico",
"ghost",
"help",
"install",
Expand All @@ -807,10 +808,10 @@ var (
"repo",
"robots.txt",
"search",
"serviceworker.js",
"stars",
"template",
"user",
"favicon.ico",
}

reservedUserPatterns = []string{"*.keys", "*.gpg"}
Expand Down
48 changes: 26 additions & 22 deletions web_src/js/features/serviceworker.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
const {UseServiceWorker, AppSubUrl, AppVer} = window.config;
import {joinPaths} from '../utils.js';

const {UseServiceWorker, AppSubUrl, AssetUrlPrefix, AppVer} = window.config;
const cachePrefix = 'static-cache-v'; // actual version is set in the service worker script
const workerAssetPath = joinPaths(AssetUrlPrefix, 'serviceworker.js');

async function unregister() {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map((registration) => {
return registration.active && registration.unregister();
}));
async function unregisterAll() {
for (const registration of await navigator.serviceWorker.getRegistrations()) {
if (registration.active) await registration.unregister();
}
}

async function unregisterOtherWorkers() {
for (const registration of await navigator.serviceWorker.getRegistrations()) {
const scriptURL = registration.active?.scriptURL || '';
if (!scriptURL.endsWith(workerAssetPath)) await registration.unregister();
}
}

async function invalidateCache() {
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys.map((key) => {
return key.startsWith(cachePrefix) && caches.delete(key);
}));
for (const key of await caches.keys()) {
if (key.startsWith(cachePrefix)) caches.delete(key);
}
}

async function checkCacheValidity() {
Expand All @@ -30,24 +38,20 @@ export default async function initServiceWorker() {
if (!('serviceWorker' in navigator)) return;

if (UseServiceWorker) {
// unregister all service workers where scriptURL does not match the current one
await unregisterOtherWorkers();
try {
// normally we'd serve the service worker as a static asset from AssetUrlPrefix but
// the spec strictly requires it to be same-origin so it has to be AppSubUrl to work
await Promise.all([
checkCacheValidity(),
navigator.serviceWorker.register(`${AppSubUrl}/assets/serviceworker.js`),
]);
await checkCacheValidity();
await navigator.serviceWorker.register(joinPaths(AppSubUrl, workerAssetPath));
} catch (err) {
console.error(err);
await Promise.all([
invalidateCache(),
unregister(),
]);
await invalidateCache();
await unregisterAll();
}
} else {
await Promise.all([
invalidateCache(),
unregister(),
]);
await invalidateCache();
await unregisterAll();
}
}
8 changes: 2 additions & 6 deletions web_src/js/publicpath.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// This sets up the URL prefix used in webpack's chunk loading.
// This file must be imported before any lazy-loading is being attempted.
import {joinPaths} from './utils.js';
const {AssetUrlPrefix} = window.config;

if (AssetUrlPrefix) {
__webpack_public_path__ = AssetUrlPrefix.endsWith('/') ? AssetUrlPrefix : `${AssetUrlPrefix}/`;
} else {
const url = new URL(document.currentScript.src);
__webpack_public_path__ = url.pathname.replace(/\/[^/]*?\/[^/]*?$/, '/');
}
__webpack_public_path__ = joinPaths(AssetUrlPrefix, '/');
10 changes: 10 additions & 0 deletions web_src/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ export function extname(path = '') {
return ext || '';
}

// join a list of path segments with slashes, ensuring no double slashes
export function joinPaths(...parts) {
let str = '';
for (const part of parts) {
if (!part) continue;
str = !str ? part : `${str.replace(/\/$/, '')}/${part.replace(/^\//, '')}`;
}
return str;
}

// test whether a variable is an object
export function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
Expand Down
41 changes: 40 additions & 1 deletion web_src/js/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
basename, extname, isObject, uniq, stripTags,
basename, extname, isObject, uniq, stripTags, joinPaths,
} from './utils.js';

test('basename', () => {
Expand All @@ -15,6 +15,45 @@ test('extname', () => {
expect(extname('file.js')).toEqual('.js');
});

test('joinPaths', () => {
expect(joinPaths('', '')).toEqual('');
expect(joinPaths('', 'b')).toEqual('b');
expect(joinPaths('', '/b')).toEqual('/b');
expect(joinPaths('', '/b/')).toEqual('/b/');
expect(joinPaths('a', '')).toEqual('a');
expect(joinPaths('/a', '')).toEqual('/a');
expect(joinPaths('/a/', '')).toEqual('/a/');
expect(joinPaths('a', 'b')).toEqual('a/b');
expect(joinPaths('a', '/b')).toEqual('a/b');
expect(joinPaths('/a', '/b')).toEqual('/a/b');
expect(joinPaths('/a', '/b')).toEqual('/a/b');
expect(joinPaths('/a/', '/b')).toEqual('/a/b');
expect(joinPaths('/a', '/b/')).toEqual('/a/b/');
expect(joinPaths('/a/', '/b/')).toEqual('/a/b/');

expect(joinPaths('', '', '')).toEqual('');
expect(joinPaths('', 'b', '')).toEqual('b');
expect(joinPaths('', 'b', 'c')).toEqual('b/c');
expect(joinPaths('', '', 'c')).toEqual('c');
expect(joinPaths('', '/b', '/c')).toEqual('/b/c');
expect(joinPaths('/a', '', '/c')).toEqual('/a/c');
expect(joinPaths('/a', '/b', '')).toEqual('/a/b');

expect(joinPaths('', '/')).toEqual('/');
expect(joinPaths('a', '/')).toEqual('a/');
expect(joinPaths('', '/', '/')).toEqual('/');
expect(joinPaths('/', '/')).toEqual('/');
expect(joinPaths('/', '')).toEqual('/');
expect(joinPaths('/', 'b')).toEqual('/b');
expect(joinPaths('/', 'b/')).toEqual('/b/');
expect(joinPaths('/', '', '/')).toEqual('/');
expect(joinPaths('/', 'b', '/')).toEqual('/b/');
expect(joinPaths('/', 'b/', '/')).toEqual('/b/');
expect(joinPaths('a', '/', '/')).toEqual('a/');
expect(joinPaths('/', '/', 'c')).toEqual('/c');
expect(joinPaths('/', '/', 'c/')).toEqual('/c/');
});

test('isObject', () => {
expect(isObject({})).toBeTrue();
expect(isObject([])).toBeFalse();
Expand Down