mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-17 19:19:47 +00:00
Enhanced offline (#249)
* Notification of updates & reloading * Using version in service worker & allowing version to appear elsewhere * Stupid file * Ditching changelog for now. Using package json. * Ugh.
This commit is contained in:
@@ -12,6 +12,15 @@ import '../custom-els/LoadingSpinner';
|
||||
// This is imported for TypeScript only. It isn't used.
|
||||
import Compress from '../compress';
|
||||
|
||||
const compressPromise = import(
|
||||
/* webpackChunkName: "main-app" */
|
||||
'../compress',
|
||||
);
|
||||
const offlinerPromise = import(
|
||||
/* webpackChunkName: "offliner" */
|
||||
'../../lib/offliner',
|
||||
);
|
||||
|
||||
export interface SourceImage {
|
||||
file: File | Fileish;
|
||||
data: ImageData;
|
||||
@@ -36,16 +45,13 @@ export default class App extends Component<Props, State> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
import(
|
||||
/* webpackChunkName: "main-app" */
|
||||
'../compress',
|
||||
).then((module) => {
|
||||
compressPromise.then((module) => {
|
||||
this.setState({ Compress: module.default });
|
||||
}).catch(() => {
|
||||
this.showSnack('Failed to load app');
|
||||
});
|
||||
|
||||
navigator.serviceWorker.register('../../sw');
|
||||
offlinerPromise.then(({ offliner }) => offliner(this.showSnack));
|
||||
|
||||
// In development, persist application state across hot reloads:
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { get, set } from 'idb-keyval';
|
||||
|
||||
import { bind, Fileish } from '../../lib/initial-util';
|
||||
import { blobToImg, drawableToImageData, blobToText } from '../../lib/util';
|
||||
@@ -156,12 +155,6 @@ async function processSvg(blob: Blob): Promise<HTMLImageElement> {
|
||||
return blobToImg(new Blob([newSource], { type: 'image/svg+xml' }));
|
||||
}
|
||||
|
||||
async function getMostActiveServiceWorker() {
|
||||
const reg = await navigator.serviceWorker.getRegistration();
|
||||
if (!reg) return null;
|
||||
return reg.active || reg.waiting || reg.installing;
|
||||
}
|
||||
|
||||
// These are only used in the mobile view
|
||||
const resultTitles = ['Top', 'Bottom'];
|
||||
// These are only used in the desktop view
|
||||
@@ -203,16 +196,7 @@ export default class Compress extends Component<Props, State> {
|
||||
this.widthQuery.addListener(this.onMobileWidthChange);
|
||||
this.updateFile(props.file);
|
||||
|
||||
// If this is the first time the user has interacted with the app, tell the service worker to
|
||||
// cache all the codecs.
|
||||
get<boolean | undefined>('user-interacted')
|
||||
.then(async (userInteracted: boolean | undefined) => {
|
||||
if (userInteracted) return;
|
||||
set('user-interacted', true);
|
||||
const serviceWorker = await getMostActiveServiceWorker();
|
||||
if (!serviceWorker) return; // Service worker not installing yet.
|
||||
serviceWorker.postMessage('cache-all');
|
||||
});
|
||||
import('../../lib/offliner').then(({ mainAppLoaded }) => mainAppLoaded());
|
||||
}
|
||||
|
||||
@bind
|
||||
|
||||
@@ -8,12 +8,9 @@ export interface SnackOptions {
|
||||
function createSnack(message: string, options: SnackOptions): [Element, Promise<string>] {
|
||||
const {
|
||||
timeout = 0,
|
||||
actions = [],
|
||||
actions = ['dismiss'],
|
||||
} = options;
|
||||
|
||||
// Provide a default 'dismiss' action
|
||||
if (!timeout && actions.length === 0) actions.push('dismiss');
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = style.snackbar;
|
||||
el.setAttribute('aria-live', 'assertive');
|
||||
|
||||
91
src/lib/offliner.ts
Normal file
91
src/lib/offliner.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { get, set } from 'idb-keyval';
|
||||
|
||||
// Just for TypeScript
|
||||
import SnackBarElement from './SnackBar';
|
||||
|
||||
/** 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Set up the service worker and monitor changes */
|
||||
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
navigator.serviceWorker.register('../sw');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
2
src/missing-types.d.ts
vendored
2
src/missing-types.d.ts
vendored
@@ -32,3 +32,5 @@ declare module 'url-loader!*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare var VERSION: string;
|
||||
|
||||
@@ -8,8 +8,7 @@ declare var self: ServiceWorkerGlobalScope;
|
||||
// This is populated by webpack.
|
||||
declare var BUILD_ASSETS: string[];
|
||||
|
||||
const version = '1.0.0';
|
||||
const versionedCache = 'static-' + version;
|
||||
const versionedCache = 'static-' + VERSION;
|
||||
const dynamicCache = 'dynamic';
|
||||
const expectedCaches = [versionedCache, dynamicCache];
|
||||
|
||||
@@ -28,6 +27,8 @@ self.addEventListener('install', (event) => {
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
self.clients.claim();
|
||||
|
||||
event.waitUntil(async function () {
|
||||
// Remove old caches.
|
||||
const promises = (await caches.keys()).map((cacheName) => {
|
||||
@@ -57,7 +58,12 @@ self.addEventListener('fetch', (event) => {
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data === 'cache-all') {
|
||||
event.waitUntil(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
|
||||
switch (event.data) {
|
||||
case 'cache-all':
|
||||
event.waitUntil(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
|
||||
break;
|
||||
case 'skip-waiting':
|
||||
self.skipWaiting();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,6 +60,8 @@ export async function cacheBasics(cacheName: string, buildAssets: string[]) {
|
||||
'first-interaction.',
|
||||
// Main app JS & CSS:
|
||||
'main-app.',
|
||||
// Service worker handler:
|
||||
'offliner.',
|
||||
// Little icons for the demo images on the homescreen:
|
||||
'icon-demo-',
|
||||
// Site logo:
|
||||
|
||||
Reference in New Issue
Block a user