mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 17:49:52 +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;
|
||||
}
|
||||
|
||||
declare module 'data-url:*' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
declare var ga: {
|
||||
(...args: any[]): void;
|
||||
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": {
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
"del": "^5.1.0",
|
||||
"file-drop-element": "^1.0.1",
|
||||
"husky": "^4.3.0",
|
||||
"idb-keyval": "^3.2.0",
|
||||
"lint-staged": "^10.4.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"postcss": "^7.0.34",
|
||||
"postcss-modules": "^3.2.2",
|
||||
"postcss-nested": "^4.2.3",
|
||||
|
||||
@@ -29,6 +29,8 @@ import runScript from './lib/run-script';
|
||||
import emitFiles from './lib/emit-files-plugin';
|
||||
import imageWorkerPlugin from './lib/image-worker-plugin';
|
||||
import initialCssPlugin from './lib/initial-css-plugin';
|
||||
import serviceWorkerPlugin from './lib/sw-plugin';
|
||||
import dataURLPlugin from './lib/data-url-plugin';
|
||||
|
||||
function resolveFileUrl({ fileName }) {
|
||||
return JSON.stringify(fileName.replace(/^static\//, '/'));
|
||||
@@ -41,6 +43,19 @@ function resolveImportMeta(property, { chunkId }) {
|
||||
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 }) {
|
||||
const omtLoaderPromise = fsp.readFile(
|
||||
path.join(__dirname, 'lib', 'omt.ejs'),
|
||||
@@ -60,13 +75,13 @@ export default async function ({ watch }) {
|
||||
'src/client',
|
||||
'src/shared',
|
||||
'src/features',
|
||||
'src/sw',
|
||||
'codecs',
|
||||
]),
|
||||
urlPlugin(),
|
||||
dataURLPlugin(),
|
||||
cssPlugin(resolveFileUrl),
|
||||
];
|
||||
const dir = '.tmp/build';
|
||||
const staticPath = 'static/c/[name]-[hash][extname]';
|
||||
|
||||
return {
|
||||
input: 'src/static-build/index.tsx',
|
||||
@@ -86,6 +101,7 @@ export default async function ({ watch }) {
|
||||
plugins: [
|
||||
{ resolveFileUrl, resolveImportMeta },
|
||||
OMT({ loader: await omtLoaderPromise }),
|
||||
serviceWorkerPlugin({ output: 'static/sw.js' }),
|
||||
...commonPlugins(),
|
||||
commonjs(),
|
||||
resolve(),
|
||||
@@ -96,8 +112,8 @@ export default async function ({ watch }) {
|
||||
{
|
||||
dir,
|
||||
format: 'amd',
|
||||
chunkFileNames: staticPath.replace('[extname]', '.js'),
|
||||
entryFileNames: staticPath.replace('[extname]', '.js'),
|
||||
chunkFileNames: jsFileName,
|
||||
entryFileNames: jsFileName,
|
||||
},
|
||||
resolveFileUrl,
|
||||
),
|
||||
|
||||
@@ -15,7 +15,9 @@ import 'shared/initial-app/custom-els/loading-spinner';
|
||||
const ROUTE_EDITOR = '/editor';
|
||||
|
||||
//const compressPromise = import('../compress');
|
||||
//const swBridgePromise = import('../../lib/sw-bridge');
|
||||
const swBridgePromise = import('client/lazy-app/sw-bridge');
|
||||
|
||||
console.log(swBridgePromise);
|
||||
|
||||
function 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 'service-worker:*' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
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": [
|
||||
{ "path": "./src/client" },
|
||||
{ "path": "./src/static-build" },
|
||||
{ "path": "./src/shared" }
|
||||
{ "path": "./src/shared" },
|
||||
{ "path": "./src/sw" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user