mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-15 01:59:57 +00:00
Service worker building (but not quite right yet)
This commit is contained in:
41
lib/data-url-plugin.js
Normal file
41
lib/data-url-plugin.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
|
import { lookup as lookupMime } from 'mime-types';
|
||||||
|
|
||||||
|
const prefix = 'data-url:';
|
||||||
|
|
||||||
|
export default function dataURLPlugin() {
|
||||||
|
return {
|
||||||
|
name: 'data-url-plugin',
|
||||||
|
async resolveId(id, importer) {
|
||||||
|
if (!id.startsWith(prefix)) return;
|
||||||
|
return (
|
||||||
|
prefix + (await this.resolve(id.slice(prefix.length), importer)).id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async load(id) {
|
||||||
|
if (!id.startsWith(prefix)) return;
|
||||||
|
const realId = id.slice(prefix.length);
|
||||||
|
this.addWatchFile(realId);
|
||||||
|
|
||||||
|
const source = await fs.readFile(realId);
|
||||||
|
const type = lookupMime(realId) || 'text/plain';
|
||||||
|
|
||||||
|
return `export default 'data:${type};base64,${source.toString(
|
||||||
|
'base64',
|
||||||
|
)}';`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
67
lib/sw-plugin.js
Normal file
67
lib/sw-plugin.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
import { posix } from 'path';
|
||||||
|
|
||||||
|
const importPrefix = 'service-worker:';
|
||||||
|
|
||||||
|
export default function serviceWorkerPlugin({
|
||||||
|
output = 'sw.js',
|
||||||
|
filterAssets = () => true,
|
||||||
|
} = {}) {
|
||||||
|
return {
|
||||||
|
name: 'service-worker',
|
||||||
|
async resolveId(id, importer) {
|
||||||
|
if (!id.startsWith(importPrefix)) return;
|
||||||
|
|
||||||
|
const plainId = id.slice(importPrefix.length);
|
||||||
|
const result = await this.resolve(plainId, importer);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
return importPrefix + result.id;
|
||||||
|
},
|
||||||
|
load(id) {
|
||||||
|
if (!id.startsWith(importPrefix)) return;
|
||||||
|
const fileId = this.emitFile({
|
||||||
|
type: 'chunk',
|
||||||
|
id: id.slice(importPrefix.length),
|
||||||
|
fileName: output,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `export default import.meta.ROLLUP_FILE_URL_${fileId};`;
|
||||||
|
},
|
||||||
|
generateBundle(options, bundle) {
|
||||||
|
const swChunk = bundle[output];
|
||||||
|
const toCacheInSW = Object.values(bundle).filter(
|
||||||
|
(item) => item !== swChunk && filterAssets(item),
|
||||||
|
);
|
||||||
|
const urls = toCacheInSW.map(
|
||||||
|
(item) =>
|
||||||
|
posix
|
||||||
|
.relative(posix.dirname(output), item.fileName)
|
||||||
|
.replace(/((?<=^|\/)index)?\.html?$/, '') || '.',
|
||||||
|
);
|
||||||
|
|
||||||
|
const versionHash = createHash('sha1');
|
||||||
|
for (const item of toCacheInSW) {
|
||||||
|
versionHash.update(item.code || item.source);
|
||||||
|
}
|
||||||
|
const version = versionHash.digest('hex');
|
||||||
|
|
||||||
|
swChunk.code =
|
||||||
|
`const ASSETS = ${JSON.stringify(urls)};\n` +
|
||||||
|
`const VERSION = ${JSON.stringify(version)};\n` +
|
||||||
|
swChunk.code;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
5
missing-types.d.ts
vendored
5
missing-types.d.ts
vendored
@@ -27,6 +27,11 @@ declare module 'css:*' {
|
|||||||
export default source;
|
export default source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'data-url:*' {
|
||||||
|
const url: string;
|
||||||
|
export default url;
|
||||||
|
}
|
||||||
|
|
||||||
declare var ga: {
|
declare var ga: {
|
||||||
(...args: any[]): void;
|
(...args: any[]): void;
|
||||||
q: any[];
|
q: any[];
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -2041,6 +2041,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"idb-keyval": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ignore": {
|
"ignore": {
|
||||||
"version": "5.1.8",
|
"version": "5.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
"del": "^5.1.0",
|
"del": "^5.1.0",
|
||||||
"file-drop-element": "^1.0.1",
|
"file-drop-element": "^1.0.1",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.0",
|
||||||
|
"idb-keyval": "^3.2.0",
|
||||||
"lint-staged": "^10.4.0",
|
"lint-staged": "^10.4.0",
|
||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"mime-types": "^2.1.27",
|
||||||
"postcss": "^7.0.34",
|
"postcss": "^7.0.34",
|
||||||
"postcss-modules": "^3.2.2",
|
"postcss-modules": "^3.2.2",
|
||||||
"postcss-nested": "^4.2.3",
|
"postcss-nested": "^4.2.3",
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import runScript from './lib/run-script';
|
|||||||
import emitFiles from './lib/emit-files-plugin';
|
import emitFiles from './lib/emit-files-plugin';
|
||||||
import imageWorkerPlugin from './lib/image-worker-plugin';
|
import imageWorkerPlugin from './lib/image-worker-plugin';
|
||||||
import initialCssPlugin from './lib/initial-css-plugin';
|
import initialCssPlugin from './lib/initial-css-plugin';
|
||||||
|
import serviceWorkerPlugin from './lib/sw-plugin';
|
||||||
|
import dataURLPlugin from './lib/data-url-plugin';
|
||||||
|
|
||||||
function resolveFileUrl({ fileName }) {
|
function resolveFileUrl({ fileName }) {
|
||||||
return JSON.stringify(fileName.replace(/^static\//, '/'));
|
return JSON.stringify(fileName.replace(/^static\//, '/'));
|
||||||
@@ -41,6 +43,19 @@ function resolveImportMeta(property, { chunkId }) {
|
|||||||
return `new URL(${resolveFileUrl({ fileName: chunkId })}, location).href`;
|
return `new URL(${resolveFileUrl({ fileName: chunkId })}, location).href`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dir = '.tmp/build';
|
||||||
|
const staticPath = 'static/c/[name]-[hash][extname]';
|
||||||
|
const jsPath = staticPath.replace('[extname]', '.js');
|
||||||
|
|
||||||
|
function jsFileName(chunkInfo) {
|
||||||
|
if (!chunkInfo.facadeModuleId) return jsPath;
|
||||||
|
const parsedPath = path.parse(chunkInfo.facadeModuleId);
|
||||||
|
if (parsedPath.name !== 'index') return jsPath;
|
||||||
|
// Come up with a better name than 'index'
|
||||||
|
const name = parsedPath.dir.split('/').slice(-1);
|
||||||
|
return jsPath.replace('[name]', name);
|
||||||
|
}
|
||||||
|
|
||||||
export default async function ({ watch }) {
|
export default async function ({ watch }) {
|
||||||
const omtLoaderPromise = fsp.readFile(
|
const omtLoaderPromise = fsp.readFile(
|
||||||
path.join(__dirname, 'lib', 'omt.ejs'),
|
path.join(__dirname, 'lib', 'omt.ejs'),
|
||||||
@@ -60,13 +75,13 @@ export default async function ({ watch }) {
|
|||||||
'src/client',
|
'src/client',
|
||||||
'src/shared',
|
'src/shared',
|
||||||
'src/features',
|
'src/features',
|
||||||
|
'src/sw',
|
||||||
'codecs',
|
'codecs',
|
||||||
]),
|
]),
|
||||||
urlPlugin(),
|
urlPlugin(),
|
||||||
|
dataURLPlugin(),
|
||||||
cssPlugin(resolveFileUrl),
|
cssPlugin(resolveFileUrl),
|
||||||
];
|
];
|
||||||
const dir = '.tmp/build';
|
|
||||||
const staticPath = 'static/c/[name]-[hash][extname]';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
input: 'src/static-build/index.tsx',
|
input: 'src/static-build/index.tsx',
|
||||||
@@ -86,6 +101,7 @@ export default async function ({ watch }) {
|
|||||||
plugins: [
|
plugins: [
|
||||||
{ resolveFileUrl, resolveImportMeta },
|
{ resolveFileUrl, resolveImportMeta },
|
||||||
OMT({ loader: await omtLoaderPromise }),
|
OMT({ loader: await omtLoaderPromise }),
|
||||||
|
serviceWorkerPlugin({ output: 'static/sw.js' }),
|
||||||
...commonPlugins(),
|
...commonPlugins(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
resolve(),
|
resolve(),
|
||||||
@@ -96,8 +112,8 @@ export default async function ({ watch }) {
|
|||||||
{
|
{
|
||||||
dir,
|
dir,
|
||||||
format: 'amd',
|
format: 'amd',
|
||||||
chunkFileNames: staticPath.replace('[extname]', '.js'),
|
chunkFileNames: jsFileName,
|
||||||
entryFileNames: staticPath.replace('[extname]', '.js'),
|
entryFileNames: jsFileName,
|
||||||
},
|
},
|
||||||
resolveFileUrl,
|
resolveFileUrl,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import 'shared/initial-app/custom-els/loading-spinner';
|
|||||||
const ROUTE_EDITOR = '/editor';
|
const ROUTE_EDITOR = '/editor';
|
||||||
|
|
||||||
//const compressPromise = import('../compress');
|
//const compressPromise = import('../compress');
|
||||||
//const swBridgePromise = import('../../lib/sw-bridge');
|
const swBridgePromise = import('client/lazy-app/sw-bridge');
|
||||||
|
|
||||||
|
console.log(swBridgePromise);
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
|
|||||||
111
src/client/lazy-app/sw-bridge/index.ts
Normal file
111
src/client/lazy-app/sw-bridge/index.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
|
||||||
|
|
||||||
|
import { get, set } from 'idb-keyval';
|
||||||
|
|
||||||
|
import swUrl from 'service-worker:sw';
|
||||||
|
|
||||||
|
/** Tell the service worker to skip waiting */
|
||||||
|
async function skipWaiting() {
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (!reg || !reg.waiting) return;
|
||||||
|
reg.waiting.postMessage('skip-waiting');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the service worker that's 'active' or closest to 'active' */
|
||||||
|
async function getMostActiveServiceWorker() {
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (!reg) return null;
|
||||||
|
return reg.active || reg.waiting || reg.installing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait for an installing worker */
|
||||||
|
async function installingWorker(
|
||||||
|
reg: ServiceWorkerRegistration,
|
||||||
|
): Promise<ServiceWorker> {
|
||||||
|
if (reg.installing) return reg.installing;
|
||||||
|
return new Promise<ServiceWorker>((resolve) => {
|
||||||
|
reg.addEventListener('updatefound', () => resolve(reg.installing!), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait a service worker to become waiting */
|
||||||
|
async function updateReady(reg: ServiceWorkerRegistration): Promise<void> {
|
||||||
|
if (reg.waiting) return;
|
||||||
|
const installing = await installingWorker(reg);
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
installing.addEventListener('statechange', () => {
|
||||||
|
if (installing.state === 'installed') resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait for a shared image */
|
||||||
|
export function getSharedImage(): Promise<File> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const onmessage = (event: MessageEvent) => {
|
||||||
|
if (event.data.action !== 'load-image') return;
|
||||||
|
resolve(event.data.file);
|
||||||
|
navigator.serviceWorker.removeEventListener('message', onmessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.serviceWorker.addEventListener('message', onmessage);
|
||||||
|
|
||||||
|
// This message is picked up by the service worker - it's how it knows we're ready to receive
|
||||||
|
// the file.
|
||||||
|
navigator.serviceWorker.controller!.postMessage('share-ready');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set up the service worker and monitor changes */
|
||||||
|
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
|
||||||
|
if (__PRODUCTION__) navigator.serviceWorker.register(swUrl);
|
||||||
|
|
||||||
|
const hasController = !!navigator.serviceWorker.controller;
|
||||||
|
|
||||||
|
// Look for changes in the controller
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', async () => {
|
||||||
|
// Is it the first install?
|
||||||
|
if (!hasController) {
|
||||||
|
showSnack('Ready to work offline', { timeout: 5000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise reload (the user will have agreed to this).
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we don't have a controller, we don't need to check for updates – we've just loaded from the
|
||||||
|
// network.
|
||||||
|
if (!hasController) return;
|
||||||
|
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
// Service worker not registered yet.
|
||||||
|
if (!reg) return;
|
||||||
|
// Look for updates
|
||||||
|
await updateReady(reg);
|
||||||
|
|
||||||
|
// Ask the user if they want to update.
|
||||||
|
const result = await showSnack('Update available', {
|
||||||
|
actions: ['reload', 'dismiss'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tell the waiting worker to activate, this will change the controller and cause a reload (see
|
||||||
|
// 'controllerchange')
|
||||||
|
if (result === 'reload') skipWaiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell the service worker the main app has loaded. If it's the first time the service worker has
|
||||||
|
* heard about this, cache the heavier assets like codecs.
|
||||||
|
*/
|
||||||
|
export async function mainAppLoaded() {
|
||||||
|
// If the user has already interacted, no need to tell the service worker anything.
|
||||||
|
const userInteracted = await get<boolean | undefined>('user-interacted');
|
||||||
|
if (userInteracted) return;
|
||||||
|
set('user-interacted', true);
|
||||||
|
const serviceWorker = await getMostActiveServiceWorker();
|
||||||
|
if (!serviceWorker) return; // Service worker not installing yet.
|
||||||
|
serviceWorker.postMessage('cache-all');
|
||||||
|
}
|
||||||
5
src/client/missing-types.d.ts
vendored
5
src/client/missing-types.d.ts
vendored
@@ -19,4 +19,9 @@ interface Navigator {
|
|||||||
|
|
||||||
declare module 'add-css:*' {}
|
declare module 'add-css:*' {}
|
||||||
|
|
||||||
|
declare module 'service-worker:*' {
|
||||||
|
const url: string;
|
||||||
|
export default url;
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'preact/debug' {}
|
declare module 'preact/debug' {}
|
||||||
|
|||||||
91
src/sw/index.ts
Normal file
91
src/sw/index.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
cacheOrNetworkAndCache,
|
||||||
|
cleanupCache,
|
||||||
|
cacheOrNetwork,
|
||||||
|
cacheBasics,
|
||||||
|
cacheAdditionalProcessors,
|
||||||
|
serveShareTarget,
|
||||||
|
} from './util';
|
||||||
|
import { get } from 'idb-keyval';
|
||||||
|
|
||||||
|
// Give TypeScript the correct global.
|
||||||
|
declare var self: ServiceWorkerGlobalScope;
|
||||||
|
// This is populated by webpack.
|
||||||
|
declare var BUILD_ASSETS: string[];
|
||||||
|
|
||||||
|
const versionedCache = 'static-' + VERSION;
|
||||||
|
const dynamicCache = 'dynamic';
|
||||||
|
const expectedCaches = [versionedCache, dynamicCache];
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async function () {
|
||||||
|
const promises = [];
|
||||||
|
promises.push(cacheBasics(versionedCache, BUILD_ASSETS));
|
||||||
|
|
||||||
|
// If the user has already interacted with the app, update the codecs too.
|
||||||
|
if (await get('user-interacted')) {
|
||||||
|
promises.push(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
self.clients.claim();
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
(async function () {
|
||||||
|
// Remove old caches.
|
||||||
|
const promises = (await caches.keys()).map((cacheName) => {
|
||||||
|
if (!expectedCaches.includes(cacheName))
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all<any>(promises);
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// Don't care about other-origin URLs
|
||||||
|
if (url.origin !== location.origin) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
url.pathname === '/' &&
|
||||||
|
url.searchParams.has('share-target') &&
|
||||||
|
event.request.method === 'POST'
|
||||||
|
) {
|
||||||
|
serveShareTarget(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only care about GET from here on in.
|
||||||
|
if (event.request.method !== 'GET') return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
url.pathname.startsWith('/demo-') ||
|
||||||
|
url.pathname.startsWith('/wc-polyfill')
|
||||||
|
) {
|
||||||
|
cacheOrNetworkAndCache(event, dynamicCache);
|
||||||
|
cleanupCache(event, dynamicCache, BUILD_ASSETS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheOrNetwork(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
switch (event.data) {
|
||||||
|
case 'cache-all':
|
||||||
|
event.waitUntil(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
|
||||||
|
break;
|
||||||
|
case 'skip-waiting':
|
||||||
|
self.skipWaiting();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
16
src/sw/missing-types.d.ts
vendored
Normal file
16
src/sw/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/// <reference path="../../missing-types.d.ts" />
|
||||||
|
|
||||||
|
declare const VERSION: string;
|
||||||
|
declare const ASSETS: string[];
|
||||||
BIN
src/sw/tiny.avif
Normal file
BIN
src/sw/tiny.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 303 B |
|
Before Width: | Height: | Size: 38 B After Width: | Height: | Size: 38 B |
6
src/sw/tsconfig.json
Normal file
6
src/sw/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../generic-tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["webworker", "esnext"]
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/sw/util.ts
Normal file
179
src/sw/util.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import webpDataUrl from 'data-url:./tiny.webp';
|
||||||
|
import avifDataUrl from 'data-url:./tiny.avif';
|
||||||
|
|
||||||
|
// Give TypeScript the correct global.
|
||||||
|
declare var self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
export function cacheOrNetwork(event: FetchEvent): void {
|
||||||
|
event.respondWith(
|
||||||
|
(async function () {
|
||||||
|
const cachedResponse = await caches.match(event.request, {
|
||||||
|
ignoreSearch: true,
|
||||||
|
});
|
||||||
|
return cachedResponse || fetch(event.request);
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cacheOrNetworkAndCache(
|
||||||
|
event: FetchEvent,
|
||||||
|
cacheName: string,
|
||||||
|
): void {
|
||||||
|
event.respondWith(
|
||||||
|
(async function () {
|
||||||
|
const { request } = event;
|
||||||
|
// Return from cache if possible.
|
||||||
|
const cachedResponse = await caches.match(request);
|
||||||
|
if (cachedResponse) return cachedResponse;
|
||||||
|
|
||||||
|
// Else go to the network.
|
||||||
|
const response = await fetch(request);
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
(async function () {
|
||||||
|
// Cache what we fetched.
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
await cache.put(request, responseToCache);
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the network response.
|
||||||
|
return response;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveShareTarget(event: FetchEvent): void {
|
||||||
|
const dataPromise = event.request.formData();
|
||||||
|
|
||||||
|
// Redirect so the user can refresh the page without resending data.
|
||||||
|
// @ts-ignore It doesn't like me giving a response to respondWith, although it's allowed.
|
||||||
|
event.respondWith(Response.redirect('/?share-target'));
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
(async function () {
|
||||||
|
// The page sends this message to tell the service worker it's ready to receive the file.
|
||||||
|
await nextMessage('share-ready');
|
||||||
|
const client = await self.clients.get(event.resultingClientId);
|
||||||
|
const data = await dataPromise;
|
||||||
|
const file = data.get('file');
|
||||||
|
client!.postMessage({ file, action: 'load-image' });
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanupCache(
|
||||||
|
event: FetchEvent,
|
||||||
|
cacheName: string,
|
||||||
|
keepAssets: string[],
|
||||||
|
) {
|
||||||
|
event.waitUntil(
|
||||||
|
(async function () {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
|
||||||
|
// Clean old entries from the dynamic cache.
|
||||||
|
const requests = await cache.keys();
|
||||||
|
const promises = requests.map((cachedRequest) => {
|
||||||
|
// Get pathname without leading /
|
||||||
|
const assetPath = new URL(cachedRequest.url).pathname.slice(1);
|
||||||
|
// If it isn't one of our keepAssets, we don't need it anymore.
|
||||||
|
if (!keepAssets.includes(assetPath)) return cache.delete(cachedRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all<any>(promises);
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssetsWithPrefix(assets: string[], prefixes: string[]) {
|
||||||
|
return assets.filter((asset) =>
|
||||||
|
prefixes.some((prefix) => asset.startsWith(prefix)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cacheBasics(cacheName: string, buildAssets: string[]) {
|
||||||
|
const toCache = ['/', '/assets/favicon.ico'];
|
||||||
|
|
||||||
|
const prefixesToCache = [
|
||||||
|
// Main app JS & CSS:
|
||||||
|
'main-app.',
|
||||||
|
// Service worker handler:
|
||||||
|
'offliner.',
|
||||||
|
// Little icons for the demo images on the homescreen:
|
||||||
|
'icon-demo-',
|
||||||
|
// Site logo:
|
||||||
|
'logo.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache);
|
||||||
|
|
||||||
|
toCache.push(...prefixMatches);
|
||||||
|
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
await cache.addAll(toCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cacheAdditionalProcessors(
|
||||||
|
cacheName: string,
|
||||||
|
buildAssets: string[],
|
||||||
|
) {
|
||||||
|
let toCache = [];
|
||||||
|
|
||||||
|
const prefixesToCache = [
|
||||||
|
// Worker which handles image processing:
|
||||||
|
'processor-worker.',
|
||||||
|
// processor-worker imports:
|
||||||
|
'process-',
|
||||||
|
];
|
||||||
|
|
||||||
|
const prefixMatches = getAssetsWithPrefix(buildAssets, prefixesToCache);
|
||||||
|
const wasm = buildAssets.filter((asset) => asset.endsWith('.wasm'));
|
||||||
|
|
||||||
|
toCache.push(...prefixMatches, ...wasm);
|
||||||
|
|
||||||
|
const [supportsWebP, supportsAvif] = await Promise.all(
|
||||||
|
[webpDataUrl, avifDataUrl].map(async (dataUrl) => {
|
||||||
|
if (!self.createImageBitmap) return false;
|
||||||
|
const response = await fetch(dataUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
return createImageBitmap(blob).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// No point caching decoders the browser already supports:
|
||||||
|
toCache = toCache.filter(
|
||||||
|
(asset) =>
|
||||||
|
(supportsWebP ? !/webp[\-_]dec/.test(asset) : true) &&
|
||||||
|
(supportsAvif ? !/avif[\-_]dec/.test(asset) : true),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
await cache.addAll(toCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMessageResolveMap = new Map<string, (() => void)[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait on a message with a particular event.data value.
|
||||||
|
*
|
||||||
|
* @param dataVal The event.data value.
|
||||||
|
*/
|
||||||
|
function nextMessage(dataVal: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!nextMessageResolveMap.has(dataVal)) {
|
||||||
|
nextMessageResolveMap.set(dataVal, []);
|
||||||
|
}
|
||||||
|
nextMessageResolveMap.get(dataVal)!.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
const resolvers = nextMessageResolveMap.get(event.data);
|
||||||
|
if (!resolvers) return;
|
||||||
|
nextMessageResolveMap.delete(event.data);
|
||||||
|
for (const resolve of resolvers) resolve();
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "./src/client" },
|
{ "path": "./src/client" },
|
||||||
{ "path": "./src/static-build" },
|
{ "path": "./src/static-build" },
|
||||||
{ "path": "./src/shared" }
|
{ "path": "./src/shared" },
|
||||||
|
{ "path": "./src/sw" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user