docbrown/app/views/service_worker/index.js.erb
rhymes d8bc399a56
Restore static error pages (#11882)
* Revert "Use static error files in serviceworker (#11806)"

This reverts commit 2c96c14021.

* Revert "Use custom, dynamic, error pages (#11744)"

This reverts commit 2ab0719501.

* Remove GitHub report prompt for good, see #11718

* Bump service worker cache version
2020-12-14 12:06:41 -05:00

228 lines
9 KiB
Text

// Serviceworkers file. This code gets installed in users browsers and runs code before the request is made.
<% unless Rails.env.test? || ENV["SKIP_SERVICEWORKERS"] == "true" %>
const staticCacheName = 'static-1.4';
const expectedCaches = [
staticCacheName
];
self.addEventListener('install', event => {
self.skipWaiting();
if (/iPhone|CriOS|iPad/i.test(navigator.userAgent)) {
// iOS seems to have issues.
return;
}
// Populate initial serviceworker cache.
event.waitUntil(
caches.open(staticCacheName)
.then(cache => cache.addAll([
"/shell_top", // head, top bar, inline styles
"/shell_bottom", // footer
"/async_info/shell_version", // For comparing changes in the shell. Should be incremented with style changes.
"/404.html", // Not found page
"/500.html", // Error page
"/offline.html" //Offline page
]))
);
});
// remove caches that aren't in expectedCaches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => {
if (!expectedCaches.includes(key)) return caches.delete(key);
})
))
);
});
// Create a composed streamed webpage with shell and core content
function createPageStream(request) {
const stream = new ReadableStream({
start(controller) {
Promise.all([caches.match('/shell_top'), caches.match('/shell_bottom')])
.then((cachedShellMatches) => {
const cachedShellTop = cachedShellMatches[0];
const cachedShellBottom = cachedShellMatches[1];
if (!cachedShellTop || !cachedShellBottom) { // return if shell isn't cached.
return
}
// the body url is the request url plus 'include'
const url = new URL(request.url);
url.searchParams.set('i', 'i'); // Adds ?i=i or &i=i, which is our indicator for "internal" partial page
const startFetch = Promise.resolve(cachedShellTop);
const endFetch = Promise.resolve(cachedShellBottom);
const middleFetch = fetch(url).then(response => {
if (!response.ok && response.status === 404) {
return caches.match('/404.html');
}
if (!response.ok && response.status != 404) {
return caches.match('/500.html');
}
return response;
}).catch(err => caches.match('/offline.html'));
function pushStream(stream) {
const reader = stream.getReader();
return reader.read().then(function process(result) {
if (result.done) return;
controller.enqueue(result.value);
return reader.read().then(process);
});
}
startFetch
.then(response => pushStream(response.body))
.then(() => middleFetch)
.then(response => pushStream(response.body))
.then(() => endFetch)
.then(response => pushStream(response.body))
.then(() => controller.close());
})
}
});
return new Response(stream, {
headers: {'Content-Type': 'text/html; charset=utf-8'}
});
}
function includesUnsupportedPath(url) {
return [
'/%F0%9F%92%B8', // 💸 (hiring)
'/abtests', // Skip for field_test dashboard
'/admin', // Don't fetch for admin dashboard.
'/ahoy/', // Skip for ahoy message redirects
'/api', // redirects
'/api/', // Don't run on API endpoints.
'/checkin', // Don't run on checkin reroutes.
'/embed/', // Don't fetch for embeded content.
'/enter', // Don't run on registration.
'/feed', // Skip the RSS feed
'/i/', // Ignore locally stored image path
'/images/', // Ignore nginx proxy path
'/internal', // redirects
'/locale/', // Don't run on explicit locale endpoints.
'/new',
'/oauth/', // Skip oauth apps
'/onboarding', // Don't run on onboarding.
'/podcasts', // redirects
'/rails/mailers', // Skip for mailers previews in development mode
'/robots.txt', // Skip robots for web crawlers
'/rss', // Skip the RSS feed alternative path
'/search/chat_channels', // Don't run on search endpoints
'/search/feed_content',
'/search/listings',
'/search/reactions',
'/search/tags',
'/search/users',
'/shell_', // Don't fetch for shell.
'/shop', // redirects
'/sidekiq', // Skip for Sidekiq dashboard
'/sitemap-', // Don't run on registration.
'/social_previews', // Skip for social previews
'/survey', // redirects
'/uploads/', // Ignore locally stored image path
'/users/auth', // Don't run on authentication.
'/welcome', // Don't run on welcome reroutes.
'/workshops', // redirects
].some(path => url.pathname.includes(path))
}
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (/iPhone|CriOS|iPad/i.test(navigator.userAgent)) {
// iOS seems to have issues.
return;
}
<% if Rails.env.production? %>
// We should generally not run this in development
// Because the assets will not have cache-busting fingerprints.
// Fetch and/or JS and CSS assets for better persistence than memory cache.
if (url.pathname.startsWith("/assets/") || url.pathname.startsWith("/packs/")) {
event.respondWith(
caches.open(staticCacheName).then(function (cache) {
return cache.match(event.request).then(function (response) {
return (
response ||
fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
})
);
});
})
);
}
<% end %>
if (url.origin === location.origin) {
if (event.clientId === "" && // Not fetched via AJAX after page load.
event.request.method == "GET" && // Don't fetch on POST, DELETE, etc.
!event.request.referrer.includes('/signout_confirm') && // If this is the referrer, we instead want to flush.
!url.href.includes('i=i') && // Parameter representing "internal" navigation.
!url.href.includes('.css') && // Don't run on CSS.
!url.href.includes('.js') && // Don't run on JS.
!url.href.includes('?preview=') && // Skip for preview pages.
!url.href.includes('?signin') && // Don't run on sign in.
!includesUnsupportedPath(url) &&
caches.match('/shell_top') && // Ensure shell_top is in the cache.
caches.match('/shell_bottom') // Ensure shell_bottom is in the cache.
) {
event.respondWith(createPageStream(event.request)); // Respond with the stream
// Ping version endpoint to see if we should fetch new shell.
if (!caches.match('/async_info/shell_version')) { // Check if we have a cached shell version
caches.open(staticCacheName)
.then(cache => cache.addAll([
"/async_info/shell_version",
]));
return;
}
fetch('/async_info/shell_version').then(response => response.json()).then(json => {
caches.match('/async_info/shell_version')
.then(cachedResponse => (cachedResponse === undefined) ? {} : cachedResponse.json())
.then(cacheJson => {
if (cacheJson['version'] != json['version']) {
caches.open(staticCacheName)
.then(cache => cache.addAll([
"/shell_top",
"/shell_bottom",
"/async_info/shell_version"
]));
}
})
})
return;
}
// Fetch new shell upon events that signify change in session.
if (event.clientId === "" &&
(event.request.referrer.includes('/signout_confirm') || url.href.includes('?signin') || url.href.includes('/onboarding'))) {
caches.open(staticCacheName)
.then(cache => cache.addAll([
"/shell_top",
"/shell_bottom",
]));
}
// Periodically delete assets when they pile up
if (Math.random() > 0.97) {
caches.open(staticCacheName).then(function(cache) {
cache.keys().then(function(keys) {
if (keys.length > 100) {
keys.forEach(function(r) {
if (r.url.includes("/assets/") || r.url.includes("/packs/")) {
cache.delete(r);
};
});
}
});
});
}
}
});
<% end %>