Remove PWA plugin (#39250)
This commit is contained in:
parent
c5432e3a09
commit
0172d819f7
@ -10,7 +10,7 @@ import { me, reduceMotion } from 'mastodon/initial_state';
|
||||
import ready from 'mastodon/ready';
|
||||
import { store } from 'mastodon/store';
|
||||
|
||||
import { isProduction, isDevelopment } from './utils/environment';
|
||||
import { isDevelopment, isProduction } from './utils/environment';
|
||||
|
||||
function main() {
|
||||
perf.start('main()');
|
||||
@ -41,29 +41,30 @@ function main() {
|
||||
);
|
||||
store.dispatch(setupBrowserNotifications());
|
||||
|
||||
if (isProduction() && me && 'serviceWorker' in navigator) {
|
||||
const { Workbox } = await import('workbox-window');
|
||||
const wb = new Workbox(
|
||||
isDevelopment() ? '/packs-dev/dev-sw.js?dev-sw' : '/sw.js',
|
||||
{ type: 'module', scope: '/' },
|
||||
);
|
||||
let registration;
|
||||
|
||||
try {
|
||||
registration = await wb.register();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (
|
||||
me &&
|
||||
'serviceWorker' in navigator &&
|
||||
(isDevelopment() || isProduction()) // Disallow testing environment
|
||||
) {
|
||||
let swPath = '/sw.js';
|
||||
if (isDevelopment()) {
|
||||
const { default: swDevUrl } =
|
||||
await import('@/mastodon/service_worker/sw?url');
|
||||
swPath = swDevUrl;
|
||||
}
|
||||
|
||||
if (
|
||||
registration &&
|
||||
'Notification' in window &&
|
||||
Notification.permission === 'granted'
|
||||
) {
|
||||
const registerPushNotifications =
|
||||
await import('mastodon/actions/push_notifications');
|
||||
await navigator.serviceWorker.register(swPath, {
|
||||
scope: '/',
|
||||
type: 'module',
|
||||
});
|
||||
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
if (isProduction()) {
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
const registerPushNotifications =
|
||||
await import('mastodon/actions/push_notifications');
|
||||
|
||||
store.dispatch(registerPushNotifications.register());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
390
app/javascript/mastodon/service_worker/caching.test.ts
Normal file
390
app/javascript/mastodon/service_worker/caching.test.ts
Normal file
@ -0,0 +1,390 @@
|
||||
import { DAY } from '../utils/time';
|
||||
|
||||
import { expireCachedItems, handleFetch } from './caching';
|
||||
|
||||
const now = 1_700_000_000_000;
|
||||
|
||||
describe('expireCachedItems', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('deletes expired entries and requests without a cached response', async () => {
|
||||
const cache = new MockCache();
|
||||
const missingRequest = new Request('https://example.com/missing');
|
||||
|
||||
cache.set('/fresh', now - DAY);
|
||||
cache.set('/expired', now - DAY * 31);
|
||||
cache.store.set(missingRequest.url, { request: missingRequest });
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(cache),
|
||||
});
|
||||
|
||||
await expireCachedItems({ name: 'images', ttl: DAY * 30, max: 5 });
|
||||
|
||||
expect(Array.from(cache.store.keys())).toEqual([
|
||||
'https://example.com/fresh',
|
||||
]);
|
||||
});
|
||||
|
||||
test('trims the oldest valid entries when the cache exceeds max size', async () => {
|
||||
const cache = new MockCache();
|
||||
|
||||
cache.set('/oldest', now - DAY * 3);
|
||||
cache.set('/older', now - DAY * 2);
|
||||
cache.set('/newest', now - DAY);
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(cache),
|
||||
});
|
||||
|
||||
await expireCachedItems({ name: 'images', ttl: DAY * 30, max: 2 });
|
||||
|
||||
expect(Array.from(cache.store.keys())).toEqual([
|
||||
'https://example.com/older',
|
||||
'https://example.com/newest',
|
||||
]);
|
||||
});
|
||||
|
||||
test('keeps entries without a timestamp header over timestamped entries', async () => {
|
||||
const cache = new MockCache();
|
||||
const untimestampedRequest = new Request('https://example.com/no-header');
|
||||
|
||||
cache.store.set(untimestampedRequest.url, {
|
||||
request: untimestampedRequest,
|
||||
response: createResponse(),
|
||||
});
|
||||
cache.set('/older', now - DAY * 2);
|
||||
cache.set('/newer', now - DAY);
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(cache),
|
||||
});
|
||||
|
||||
await expireCachedItems({ name: 'images', ttl: DAY * 30, max: 2 });
|
||||
|
||||
expect(Array.from(cache.store.keys())).toEqual([
|
||||
'https://example.com/no-header',
|
||||
'https://example.com/newer',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleFetch', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('serves cached images without hitting the network while the TTL is valid', async () => {
|
||||
const imageCache = new MockCache();
|
||||
const request = createRequest('/test.png', 'image');
|
||||
const cachedResponse = createResponse(now - DAY);
|
||||
|
||||
imageCache.store.set(request.url, { request, response: cachedResponse });
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(imageCache),
|
||||
});
|
||||
|
||||
const fetch = vi.fn();
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
|
||||
const { event, respondWith } = createFetchEvent(request);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
await expect(respondWith()).resolves.toBe(cachedResponse);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('fetches stale cached images from the network and stores a refreshed response', async () => {
|
||||
const imageCache = new MockCache();
|
||||
const request = createRequest('/stale.png', 'image');
|
||||
const networkResponse = new Response('fresh', {
|
||||
headers: { 'content-type': 'image/png' },
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
});
|
||||
const putSpy = vi.spyOn(imageCache, 'put');
|
||||
|
||||
imageCache.store.set(request.url, {
|
||||
request,
|
||||
response: createResponse(now - DAY * 8),
|
||||
});
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(imageCache),
|
||||
});
|
||||
|
||||
const fetch = vi.fn().mockResolvedValue(networkResponse);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
|
||||
const { event, respondWith } = createFetchEvent(request);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
await expect(respondWith()).resolves.toBe(networkResponse);
|
||||
expect(fetch).toHaveBeenCalledWith(request);
|
||||
expect(putSpy).toHaveBeenCalledWith(request, expect.any(Response));
|
||||
expect(
|
||||
imageCache.store.get(request.url)?.response?.headers.get('x-timestamp'),
|
||||
).toBe(now.toString());
|
||||
});
|
||||
|
||||
test('does not cache opaque image responses with status zero', async () => {
|
||||
const imageCache = new MockCache();
|
||||
const request = createRequest('/opaque.png', 'image');
|
||||
const opaqueResponse = Response.error();
|
||||
const putSpy = vi.spyOn(imageCache, 'put');
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(imageCache),
|
||||
});
|
||||
|
||||
const fetch = vi.fn().mockResolvedValue(opaqueResponse);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
|
||||
const { event, respondWith } = createFetchEvent(request);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
await expect(respondWith()).resolves.toBe(opaqueResponse);
|
||||
expect(fetch).toHaveBeenCalledWith(request);
|
||||
expect(putSpy).not.toHaveBeenCalled();
|
||||
expect(imageCache.store.size).toBe(0);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['/intl/en.js', '', 'mastodon-locales'],
|
||||
['/fonts/mastodon.woff2', 'font', 'mastodon-fonts'],
|
||||
])(
|
||||
'routes %s requests through %s',
|
||||
async (pathname, destination, cacheName) => {
|
||||
const cache = new MockCache();
|
||||
const request = createRequest(pathname, destination);
|
||||
const networkResponse = new Response('asset', { status: 200 });
|
||||
const open = vi.fn().mockImplementation((name: string) => {
|
||||
expect(name).toBe(cacheName);
|
||||
return Promise.resolve(cache);
|
||||
});
|
||||
|
||||
vi.stubGlobal('caches', { open });
|
||||
|
||||
const fetch = vi.fn().mockResolvedValue(networkResponse);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
|
||||
const { event, respondWith } = createFetchEvent(request);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
await expect(respondWith()).resolves.toBe(networkResponse);
|
||||
expect(fetch).toHaveBeenCalledWith(request);
|
||||
expect(open).toHaveBeenCalledWith(cacheName);
|
||||
},
|
||||
);
|
||||
|
||||
test('clears the root cache after a successful logout request', async () => {
|
||||
const webCache = new MockCache();
|
||||
const deleteSpy = vi.spyOn(webCache, 'delete');
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockImplementation((name: string) => {
|
||||
expect(name).toBe('mastodon-web');
|
||||
return Promise.resolve(webCache);
|
||||
}),
|
||||
});
|
||||
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValue(new Response(null, { status: 200 }));
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
|
||||
const { event, respondWith } = createFetchEvent(
|
||||
createRequest('/auth/sign_out'),
|
||||
);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
await expect(respondWith()).resolves.toBeInstanceOf(Response);
|
||||
expect(deleteSpy).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
test('does not clear the root cache after a failed logout request', async () => {
|
||||
const webCache = new MockCache();
|
||||
const deleteSpy = vi.spyOn(webCache, 'delete');
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(webCache),
|
||||
});
|
||||
|
||||
const fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
new Response(null, { status: 500, statusText: 'Error' }),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
|
||||
const { event, respondWith } = createFetchEvent(
|
||||
createRequest('/auth/sign_out'),
|
||||
);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
await expect(respondWith()).resolves.toBeInstanceOf(Response);
|
||||
expect(deleteSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('clears the root cache for opaqueredirect logout responses', async () => {
|
||||
const webCache = new MockCache();
|
||||
const deleteSpy = vi.spyOn(webCache, 'delete');
|
||||
const opaqueRedirectResponse = {
|
||||
ok: false,
|
||||
type: 'opaqueredirect',
|
||||
} as Response;
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
open: vi.fn().mockResolvedValue(webCache),
|
||||
});
|
||||
|
||||
const fetch = vi.fn().mockResolvedValue(opaqueRedirectResponse);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
|
||||
const { event, respondWith } = createFetchEvent(
|
||||
createRequest('/auth/sign_out'),
|
||||
);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
await expect(respondWith()).resolves.toBe(opaqueRedirectResponse);
|
||||
expect(deleteSpy).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
test('ignores requests that are not handled by the service worker cache', () => {
|
||||
const { event, respondWith, respondWithMock } = createFetchEvent(
|
||||
createRequest('/api/v1/timelines/home'),
|
||||
);
|
||||
|
||||
handleFetch(event);
|
||||
|
||||
expect(respondWithMock).not.toHaveBeenCalled();
|
||||
expect(respondWith()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
interface CacheEntry {
|
||||
request: Request;
|
||||
response?: Response;
|
||||
}
|
||||
|
||||
class MockCache implements Cache {
|
||||
public readonly store = new Map<string, CacheEntry>();
|
||||
|
||||
private normalizeRequest(request: RequestInfo | URL) {
|
||||
if (request instanceof Request) {
|
||||
return request.url;
|
||||
}
|
||||
|
||||
return request.toString();
|
||||
}
|
||||
|
||||
add(): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
addAll(): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
delete(request: RequestInfo | URL): Promise<boolean> {
|
||||
return Promise.resolve(this.store.delete(this.normalizeRequest(request)));
|
||||
}
|
||||
|
||||
keys(): Promise<readonly Request[]> {
|
||||
return Promise.resolve(
|
||||
Array.from(this.store.values(), ({ request }) => request),
|
||||
);
|
||||
}
|
||||
|
||||
match(request: RequestInfo | URL): Promise<Response | undefined> {
|
||||
return Promise.resolve(
|
||||
this.store.get(this.normalizeRequest(request))?.response,
|
||||
);
|
||||
}
|
||||
|
||||
matchAll(): Promise<readonly Response[]> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
put(request: RequestInfo | URL, response: Response): Promise<void> {
|
||||
const normalizedRequest =
|
||||
request instanceof Request ? request : new Request(request);
|
||||
|
||||
this.store.set(this.normalizeRequest(normalizedRequest), {
|
||||
request: normalizedRequest,
|
||||
response,
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
set(pathname: string, timestamp?: number) {
|
||||
const request = new Request(`https://example.com${pathname}`);
|
||||
|
||||
this.store.set(request.url, {
|
||||
request,
|
||||
response: createResponse(timestamp),
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
function createResponse(timestamp?: number) {
|
||||
const headers = new Headers();
|
||||
|
||||
if (timestamp !== undefined) {
|
||||
headers.set('x-timestamp', timestamp.toString());
|
||||
}
|
||||
|
||||
return new Response('body', { headers });
|
||||
}
|
||||
|
||||
function createRequest(pathname: string, destination = '') {
|
||||
const request = new Request(`https://example.com${pathname}`);
|
||||
|
||||
Object.defineProperty(request, 'destination', {
|
||||
value: destination,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
function createFetchEvent(request: Request) {
|
||||
let responsePromise: Promise<Response> | undefined;
|
||||
const respondWith = vi.fn((response: Response | Promise<Response>) => {
|
||||
responsePromise = Promise.resolve(response);
|
||||
});
|
||||
|
||||
return {
|
||||
event: {
|
||||
request,
|
||||
respondWith,
|
||||
} as unknown as FetchEvent,
|
||||
respondWith: () => responsePromise,
|
||||
respondWithMock: respondWith,
|
||||
};
|
||||
}
|
||||
143
app/javascript/mastodon/service_worker/caching.ts
Normal file
143
app/javascript/mastodon/service_worker/caching.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/// <reference lib="WebWorker" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { DAY } from '../utils/time';
|
||||
|
||||
const CACHE_NAME_PREFIX = 'mastodon-';
|
||||
const CACHE_HEADER_TTL = 'x-timestamp';
|
||||
|
||||
export async function cacheRoot() {
|
||||
const cache = await openWebCache();
|
||||
const response = await fetch('/', {
|
||||
credentials: 'include',
|
||||
redirect: 'manual',
|
||||
});
|
||||
await cache.put('/', response);
|
||||
}
|
||||
|
||||
export function handleFetch(event: FetchEvent) {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
if (url.pathname === '/auth/sign_out') {
|
||||
event.respondWith(handleLogout(event));
|
||||
} else if (/intl\/.*\.js$/.test(url.pathname)) {
|
||||
event.respondWith(cacheFirst({ event, name: 'locales' }));
|
||||
} else if (event.request.destination === 'font') {
|
||||
event.respondWith(cacheFirst({ event, name: 'fonts' }));
|
||||
} else if (event.request.destination === 'image') {
|
||||
event.respondWith(cacheFirst({ event, name: 'images', ttl: DAY * 7 }));
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheFirst({
|
||||
event,
|
||||
name,
|
||||
ttl = DAY * 30,
|
||||
max = 5,
|
||||
}: {
|
||||
event: FetchEvent;
|
||||
name: string;
|
||||
ttl?: number;
|
||||
max?: number;
|
||||
}) {
|
||||
const cache = await caches.open(`${CACHE_NAME_PREFIX}${name}`);
|
||||
const request = event.request;
|
||||
const cachedResponse = await cache.match(request);
|
||||
|
||||
// Start expiring cache items while the process continues.
|
||||
void expireCachedItems({ name, ttl, max });
|
||||
|
||||
if (cachedResponse) {
|
||||
// If we have a cached response, check the TTL header.
|
||||
const ttlHeader = Number.parseInt(
|
||||
cachedResponse.headers.get(CACHE_HEADER_TTL) ?? '0',
|
||||
);
|
||||
|
||||
if (!ttlHeader || ttlHeader + ttl > Date.now()) {
|
||||
return cachedResponse;
|
||||
}
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// For opaque responses, the status will be zero so we can't clone them.
|
||||
if (networkResponse.status !== 0) {
|
||||
// Clone request with a custom header to store timestamp.
|
||||
const cloneHeaders = new Headers(networkResponse.headers);
|
||||
cloneHeaders.set(CACHE_HEADER_TTL, Date.now().toString());
|
||||
|
||||
const cloneResponse = new Response(networkResponse.clone().body, {
|
||||
headers: cloneHeaders,
|
||||
status: networkResponse.status,
|
||||
statusText: networkResponse.statusText,
|
||||
});
|
||||
|
||||
await cache.put(request, cloneResponse);
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
}
|
||||
|
||||
export async function expireCachedItems({
|
||||
name,
|
||||
ttl = DAY * 30,
|
||||
max = 5,
|
||||
}: {
|
||||
name: string;
|
||||
ttl?: number;
|
||||
max?: number;
|
||||
}) {
|
||||
const cache = await caches.open(`${CACHE_NAME_PREFIX}${name}`);
|
||||
|
||||
const keys = await cache.keys();
|
||||
const now = Date.now();
|
||||
const validKeys: { key: Request; timestamp: number }[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const cachedResponse = await cache.match(key);
|
||||
|
||||
if (!cachedResponse) {
|
||||
await cache.delete(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const timestamp = Number.parseInt(
|
||||
cachedResponse.headers.get(CACHE_HEADER_TTL) ?? '0',
|
||||
);
|
||||
|
||||
if (!timestamp || timestamp + ttl > now) {
|
||||
validKeys.push({ key, timestamp: timestamp || Number.POSITIVE_INFINITY });
|
||||
continue;
|
||||
}
|
||||
|
||||
await cache.delete(key);
|
||||
}
|
||||
|
||||
if (validKeys.length <= max) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedValidKeys = validKeys.toSorted(
|
||||
({ timestamp: a }, { timestamp: b }) => a - b,
|
||||
);
|
||||
await Promise.all(
|
||||
sortedValidKeys
|
||||
.slice(0, sortedValidKeys.length - max)
|
||||
.map(({ key }) => cache.delete(key)),
|
||||
);
|
||||
}
|
||||
|
||||
function openWebCache() {
|
||||
return caches.open(`${CACHE_NAME_PREFIX}web`);
|
||||
}
|
||||
|
||||
async function handleLogout(event: FetchEvent) {
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (response.ok || response.type === 'opaqueredirect') {
|
||||
const cache = await openWebCache();
|
||||
await cache.delete('/');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
import { registerRoute } from 'workbox-routing';
|
||||
import { CacheFirst } from 'workbox-strategies';
|
||||
|
||||
import { handleNotificationClick, handlePush } from './web_push_notifications';
|
||||
|
||||
const CACHE_NAME_PREFIX = 'mastodon-';
|
||||
|
||||
function openWebCache() {
|
||||
return caches.open(`${CACHE_NAME_PREFIX}web`);
|
||||
}
|
||||
|
||||
function fetchRoot() {
|
||||
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
||||
}
|
||||
|
||||
|
||||
registerRoute(
|
||||
/intl\/.*\.js$/,
|
||||
new CacheFirst({
|
||||
cacheName: `${CACHE_NAME_PREFIX}locales`,
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
|
||||
maxEntries: 5,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ request }) => request.destination === 'font',
|
||||
new CacheFirst({
|
||||
cacheName: `${CACHE_NAME_PREFIX}fonts`,
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
|
||||
maxEntries: 5,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
registerRoute(
|
||||
({ request }) => request.destination === 'image',
|
||||
new CacheFirst({
|
||||
cacheName: `m${CACHE_NAME_PREFIX}media`,
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
|
||||
maxEntries: 256,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// Cause a new version of a registered Service Worker to replace an existing one
|
||||
// that is already installed, and replace the currently active worker on open pages.
|
||||
self.addEventListener('install', function(event) {
|
||||
event.waitUntil(Promise.all([openWebCache(), fetchRoot()]).then(([cache, root]) => cache.put('/', root)));
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function(event) {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', function(event) {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
if (url.pathname === '/auth/sign_out') {
|
||||
const asyncResponse = fetch(event.request);
|
||||
const asyncCache = openWebCache();
|
||||
|
||||
event.respondWith(asyncResponse.then(response => {
|
||||
if (response.ok || response.type === 'opaqueredirect') {
|
||||
return Promise.all([
|
||||
asyncCache.then(cache => cache.delete('/')),
|
||||
indexedDB.deleteDatabase('mastodon'),
|
||||
]).then(() => response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('push', handlePush);
|
||||
self.addEventListener('notificationclick', handleNotificationClick);
|
||||
22
app/javascript/mastodon/service_worker/sw.ts
Normal file
22
app/javascript/mastodon/service_worker/sw.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/// <reference lib="WebWorker" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { cacheRoot, handleFetch } from './caching';
|
||||
import { handleNotificationClick, handlePush } from './web_push_notifications';
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
|
||||
// Cause a new version of a registered Service Worker to replace an existing one
|
||||
// that is already installed, and replace the currently active worker on open pages.
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(cacheRoot());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', handleFetch);
|
||||
|
||||
self.addEventListener('push', handlePush);
|
||||
self.addEventListener('notificationclick', handleNotificationClick);
|
||||
@ -115,19 +115,15 @@
|
||||
"stacktrace-js": "^2.0.2",
|
||||
"stringz": "^2.1.0",
|
||||
"substring-trie": "^1.0.2",
|
||||
"terser": "^5.48.0",
|
||||
"tesseract.js": "^7.0.0",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"twitter-text": "3.1.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-manifest-sri": "^0.2.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-svgr": "^5.0.0",
|
||||
"wicg-inert": "^3.1.2",
|
||||
"workbox-expiration": "^7.3.0",
|
||||
"workbox-routing": "^7.3.0",
|
||||
"workbox-strategies": "^7.3.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
"wicg-inert": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
UserConfig,
|
||||
} from 'vite';
|
||||
import manifestSRI from 'vite-plugin-manifest-sri';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest';
|
||||
@ -155,6 +154,14 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||
}
|
||||
return '[name]-[hash].js';
|
||||
},
|
||||
entryFileNames({ name }) {
|
||||
// If this is the service worker, don't add the hash to the name.
|
||||
if (name === 'sw') {
|
||||
return '[name].js';
|
||||
}
|
||||
// Otherwise, use the same value as chunkFileNames.
|
||||
return '[name]-[hash].js';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -188,29 +195,6 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||
manifestSRI({
|
||||
manifestPaths: ['.vite/manifest.json'],
|
||||
}),
|
||||
VitePWA({
|
||||
srcDir: path.resolve(jsRoot, 'mastodon/service_worker'),
|
||||
// We need to use injectManifest because we use our own service worker
|
||||
strategies: 'injectManifest',
|
||||
manifest: false,
|
||||
injectRegister: false,
|
||||
injectManifest: {
|
||||
// Do not inject a manifest, we don't use precache
|
||||
injectionPoint: undefined,
|
||||
buildPlugins: {
|
||||
vite: [
|
||||
// Provide a virtual import with only the locales used in the ServiceWorker
|
||||
MastodonServiceWorkerLocales(),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Force the output location, because we have a symlink in `public/sw.js`
|
||||
outDir: path.resolve(__dirname, 'public/packs'),
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
type: 'module',
|
||||
},
|
||||
}),
|
||||
svgr(),
|
||||
// Old library types need to be converted
|
||||
optimizeLodashImports() as PluginOption,
|
||||
@ -224,7 +208,9 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||
};
|
||||
|
||||
async function findEntrypoints() {
|
||||
const entrypoints: Record<string, string> = {};
|
||||
const entrypoints: Record<string, string> = {
|
||||
sw: path.resolve(jsRoot, 'mastodon/service_worker/sw.ts'),
|
||||
};
|
||||
|
||||
// First, JS entrypoints
|
||||
const jsEntrypointsDir = path.resolve(jsRoot, 'entrypoints');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user