diff --git a/index.html b/index.html index 74e44c99..b7b55d75 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,7 @@ ` + - `` + - ``); - removeOrEnableScript(doc("script#service-worker"), offline); + const mainScripts = [ + `` + ]; + if (!modernOnly) { + mainScripts.push( + ``, + `` + ); + } + doc("script#main").replaceWith(mainScripts.join("")); + doc("script#service-worker").attr("type", "text/javascript"); const versionScript = doc("script#version"); versionScript.attr("type", "text/javascript"); let vSource = versionScript.contents().text(); vSource = vSource.replace(`"%%VERSION%%"`, `"${version}"`); + vSource = vSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`); versionScript.text(vSource); - - if (offline) { - doc("html").attr("manifest", assets.resolve("manifest.appcache")); - doc("head").append(``); - } + doc("head").append(``); await assets.writeUnhashed("index.html", doc.html()); } @@ -205,7 +211,22 @@ async function buildJsLegacy(inputFiles) { return code; } -async function buildOffline(version, assets) { +const SERVICEWORKER_NONCACHED_ASSETS = [ + "hydrogen-legacy.js", + "olm_legacy.js", + "sw.js", +]; + +function isPreCached(asset) { + return asset.endsWith(".svg") || + asset.endsWith(".png") || + asset.endsWith(".css") || + asset.endsWith(".wasm") || + // most environments don't need the worker + asset.endsWith(".js") && asset !== "worker.js"; +} + +async function buildManifest(assets) { const webManifest = JSON.parse(await fs.readFile(path.join(projectDir, "assets/manifest.json"), "utf8")); // copy manifest icons for (const icon of webManifest.icons) { @@ -213,25 +234,33 @@ async function buildOffline(version, assets) { const iconTargetPath = path.basename(icon.src); icon.src = await assets.write(iconTargetPath, iconData); } - // write appcache manifest - const appCacheLines = [ - `CACHE MANIFEST`, - `# v${version}`, - `NETWORK`, - `"*"`, - `CACHE`, - ]; - appCacheLines.push(...assets.allWithout(["hydrogen.js"])); - const swOfflineFiles = assets.allWithout(["hydrogen-legacy.js", "olm_legacy.js"]); - const appCacheManifest = appCacheLines.join("\n") + "\n"; - await assets.writeUnhashed("manifest.appcache", appCacheManifest); + await assets.write("manifest.json", JSON.stringify(webManifest)); +} + +async function buildServiceWorker(globalHash, assets) { + const unhashedPreCachedAssets = ["index.html"]; + const hashedPreCachedAssets = []; + const hashedCachedOnRequestAssets = []; + + for (const [unresolved, resolved] of assets) { + if (SERVICEWORKER_NONCACHED_ASSETS.includes(unresolved)) { + continue; + } else if (unresolved === resolved) { + unhashedPreCachedAssets.push(resolved); + } else if (isPreCached(unresolved)) { + hashedPreCachedAssets.push(resolved); + } else { + hashedCachedOnRequestAssets.push(resolved); + } + } // write service worker let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8"); - swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`); - swSource = swSource.replace(`"%%OFFLINE_FILES%%"`, JSON.stringify(swOfflineFiles)); + swSource = swSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`); + swSource = swSource.replace(`"%%UNHASHED_PRECACHED_ASSETS%%"`, JSON.stringify(unhashedPreCachedAssets)); + swSource = swSource.replace(`"%%HASHED_PRECACHED_ASSETS%%"`, JSON.stringify(hashedPreCachedAssets)); + swSource = swSource.replace(`"%%HASHED_CACHED_ON_REQUEST_ASSETS%%"`, JSON.stringify(hashedCachedOnRequestAssets)); // service worker should not have a hashed name as it is polled by the browser for updates await assets.writeUnhashed("sw.js", swSource); - await assets.write("manifest.json", JSON.stringify(webManifest)); } async function buildCssBundles(buildFn, themes, assets) { @@ -281,14 +310,6 @@ async function buildCssLegacy(entryPath, urlMapper = null) { return result.css; } -function removeOrEnableScript(scriptNode, enable) { - if (enable) { - scriptNode.attr("type", "text/javascript"); - } else { - scriptNode.remove(); - } -} - async function removeDirIfExists(targetDir) { try { await fs.rmdir(targetDir, {recursive: true}); @@ -397,16 +418,25 @@ class AssetMap { } } - all() { - return Array.from(this._assets.values()); + [Symbol.iterator]() { + return this._assets.entries(); } - allWithout(excluded) { - excluded = excluded.map(p => this.resolve(p)); - return this.all().filter(p => excluded.indexOf(p) === -1); + isUnhashed(relPath) { + const resolvedPath = this._assets.get(relPath); + if (!resolvedPath) { + throw new Error("Unknown asset: " + relPath); + } + return relPath === resolvedPath; + } + + get size() { + return this._assets.size; + } + + has(relPath) { + return this._assets.has(relPath); } } - - -build().catch(err => console.error(err)); +build(program).catch(err => console.error(err)); diff --git a/src/service-worker.template.js b/src/service-worker.template.js index 485afba8..cad5843a 100644 --- a/src/service-worker.template.js +++ b/src/service-worker.template.js @@ -14,38 +14,90 @@ See the License for the specific language governing permissions and limitations under the License. */ -const VERSION = "%%VERSION%%"; -const OFFLINE_FILES = "%%OFFLINE_FILES%%"; -// TODO: cache these files when requested -// The difficulty is that these are relative filenames, and we don't have access to document.baseURI -// Clients.match({type: "window"}).url and assume they are all the same? they really should be ... safari doesn't support this though -const CACHE_FILES = "%%CACHE_FILES%%"; -const cacheName = `hydrogen-${VERSION}`; +const GLOBAL_HASH = "%%GLOBAL_HASH%%"; +const UNHASHED_PRECACHED_ASSETS = "%%UNHASHED_PRECACHED_ASSETS%%"; +const HASHED_PRECACHED_ASSETS = "%%HASHED_PRECACHED_ASSETS%%"; +const HASHED_CACHED_ON_REQUEST_ASSETS = "%%HASHED_CACHED_ON_REQUEST_ASSETS%%"; +const unhashedCacheName = `hydrogen-assets-${GLOBAL_HASH}`; +const hashedCacheName = `hydrogen-assets`; self.addEventListener('install', function(e) { - e.waitUntil( - caches.open(cacheName).then(function(cache) { - return cache.addAll(OFFLINE_FILES); - }) - ); + e.waitUntil((async () => { + const unhashedCache = await caches.open(unhashedCacheName); + await unhashedCache.addAll(UNHASHED_PRECACHED_ASSETS); + const hashedCache = await caches.open(hashedCacheName); + await Promise.all(HASHED_PRECACHED_ASSETS.map(async asset => { + if (!await hashedCache.match(asset)) { + await hashedCache.add(asset); + } + })); + })()); }); -self.addEventListener('activate', (event) => { - event.waitUntil( - caches.keys().then((keyList) => { - return Promise.all(keyList.map((key) => { - if (key !== cacheName) { - return caches.delete(key); +async function purgeOldCaches() { + // remove any caches we don't know about + const keyList = await caches.keys(); + for (const key of keyList) { + if (key !== unhashedCacheName && key !== hashedCacheName) { + await caches.delete(key); } - })); - }) - ); + } + // remove the cache for any old hashed resource + const hashedCache = await caches.open(hashedCacheName); + const keys = await hashedCache.keys(); + const hashedAssetURLs = + HASHED_PRECACHED_ASSETS + .concat(HASHED_CACHED_ON_REQUEST_ASSETS) + .map(a => new URL(a, self.registration.scope).href); + + for (const request of keys) { + if (!hashedAssetURLs.some(url => url === request.url)) { + hashedCache.delete(request); + } + } +} + +self.addEventListener('activate', (event) => { + event.waitUntil(purgeOldCaches()); }); self.addEventListener('fetch', (event) => { - event.respondWith( - caches.open(cacheName) - .then(cache => cache.match(event.request)) - .then((response) => response || fetch(event.request)) - ); + event.respondWith(handleRequest(event.request)); }); + +async function handleRequest(request) { + const baseURL = self.registration.scope; + if (request.url === baseURL) { + request = new Request(new URL("index.html", baseURL)); + } + let response = await readCache(request); + if (!response) { + response = await fetch(request); + await updateCache(request, response); + } + return response; +} + +async function updateCache(request, response) { + const baseURL = self.registration.scope; + if(!request.url.startsWith(baseURL)) { + return; + } + let assetName = request.url.substr(baseURL.length); + if (HASHED_CACHED_ON_REQUEST_ASSETS.includes(assetName)) { + const cache = await caches.open(hashedCacheName); + await cache.put(request, response.clone()); + } +} + +async function readCache(request) { + const unhashedCache = await caches.open(unhashedCacheName); + let response = await unhashedCache.match(request); + if (response) { + return response; + } + const hashedCache = await caches.open(hashedCacheName); + response = await hashedCache.match(request); + return response; +} + diff --git a/src/ui/web/login/common.js b/src/ui/web/login/common.js index f1e6496f..b84e5e6a 100644 --- a/src/ui/web/login/common.js +++ b/src/ui/web/login/common.js @@ -18,7 +18,7 @@ export function hydrogenGithubLink(t) { if (window.HYDROGEN_VERSION) { return t.a({target: "_blank", href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${window.HYDROGEN_VERSION}`}, - `Hydrogen v${window.HYDROGEN_VERSION} on Github`); + `Hydrogen v${window.HYDROGEN_VERSION} (${window.HYDROGEN_GLOBAL_HASH}) on Github`); } else { return t.a({target: "_blank", href: "https://github.com/vector-im/hydrogen-web"}, "Hydrogen on Github");