discourse/app/assets/javascripts/service-worker.js.erb
Blake Erickson 7a7c53894d
FIX: Excessive video data downloading from service worker (#24924)
This bug appears to only be on Chrome due to the service worker fetching
the video content on page load instead of on play. For some reason
though the service worker would fetch around 4x more than the size of
the video resulting in excessive data being downloaded especially for
larger videos.

meta https://meta.discourse.org/t/287817
internal /t/111387/52
2023-12-15 13:29:50 -07:00

296 lines
9.0 KiB
Plaintext

'use strict';
<%
base = if GlobalSetting.use_s3? && GlobalSetting.s3_cdn_url
GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url
elsif GlobalSetting.cdn_url
GlobalSetting.cdn_url + Discourse.base_path
else
Discourse.base_path
end
workbox_base = "#{base}/assets/#{EmberCli.workbox_dir_name}"
%>
importScripts("<%= "#{workbox_base}/workbox-sw.js" %>");
workbox.setConfig({
modulePathPrefix: "<%= workbox_base %>",
debug: false
});
var authUrls = ["auth", "session/sso_login", "session/sso", "srv/status"].map(path => `<%= Discourse.base_path %>/${path}`);
var chatRegex = /\/chat\/channel\/(\d+)\//;
var inlineReplyIcon = "<%= UrlHelper.absolute("/images/push-notifications/inline_reply.png") %>";
const oldCacheNames = [
"discourse-1", "external-1"
]
oldCacheNames.forEach(cacheName => caches.delete(cacheName))
var cacheVersion = "2";
var discourseCacheName = "discourse-" + cacheVersion;
var externalCacheName = "external-" + cacheVersion;
const expirationOptions = {
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
maxEntries: 250,
purgeOnQuotaError: true, // safe to automatically delete if exceeding the available storage
matchOptions: {
ignoreVary: true // Many discourse responses include `Vary` header. This option is required to ensure they are cleaned up correctly.
}
}
// Chrome 97 shipped with broken samesite cookie handling when proxying requests through service workers
// https://bugs.chromium.org/p/chromium/issues/detail?id=1286367
var chromeVersionMatch = navigator.userAgent.match(/Chrome\/97.0.(\d+)/);
var isBrokenChrome97 = chromeVersionMatch && parseInt(chromeVersionMatch[1]) <= 4692;
var isApple = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// Cache all GET requests, so Discourse can be used while offline
workbox.routing.registerRoute(
function(args) {
return args.url.origin === location.origin && !authUrls.some(u => args.url.pathname.startsWith(u)) && !isBrokenChrome97 && !isApple;
}, // Match all except auth routes
new workbox.strategies.NetworkFirst({ // This will only use the cache when a network request fails
cacheName: discourseCacheName,
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [200] // opaque responses will return status code '0'
}), // for s3 secure uploads signed urls
new workbox.expiration.ExpirationPlugin(expirationOptions),
],
})
);
var cdnUrls = [];
<% if GlobalSetting.try(:cdn_cors_enabled) %>
cdnUrls = ["<%= "#{GlobalSetting.s3_cdn_url}" %>", "<%= "#{GlobalSetting.cdn_url}" %>"].filter(Boolean);
if (cdnUrls.length > 0) {
var cdnCacheName = "cdn-" + cacheVersion;
var cdnUrl = "<%= "#{GlobalSetting.cdn_url}" %>";
var appendQueryStringPlugin = {
requestWillFetch: function (args) {
var request = args.request;
if (request.url.startsWith(cdnUrl)) {
var url = new URL(request.url);
// Using this temporary query param to force browsers to redownload images from server.
url.searchParams.append('refresh', 'true');
return new Request(url.href, request);
}
return request;
}
};
workbox.routing.registerRoute(
function(args) {
var matching = cdnUrls.filter(
function(url) {
return args.url.href.startsWith(url);
}
);
return matching.length > 0;
}, // Match all cdn resources
new workbox.strategies.NetworkFirst({ // This will only use the cache when a network request fails
cacheName: cdnCacheName,
fetchOptions: {
mode: 'cors',
credentials: 'omit'
},
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [200] // opaque responses will return status code '0'
}),
new workbox.expiration.ExpirationPlugin(expirationOptions),
appendQueryStringPlugin
],
})
);
}
<% end %>
workbox.routing.registerRoute(
function(args) {
if (args.url.origin === location.origin) {
return false;
}
// Exclude videos from service worker
if (args.event.request.headers.has('range')) {
return false;
}
var matching = cdnUrls.filter(
function(url) {
return args.url.href.startsWith(url);
}
);
return matching.length === 0;
}, // Match all other external resources
new workbox.strategies.NetworkFirst({ // This will only use the cache when a network request fails
cacheName: externalCacheName,
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [200] // opaque responses will return status code '0'
}),
new workbox.expiration.ExpirationPlugin(expirationOptions),
],
})
);
var idleThresholdTime = 1000 * 10; // 10 seconds
var lastAction = -1;
function isIdle() {
return lastAction + idleThresholdTime < Date.now();
}
function showNotification(title, body, icon, badge, tag, baseUrl, url) {
var notificationOptions = {
body: body,
icon: icon,
badge: badge,
data: { url: url, baseUrl: baseUrl },
tag: tag
}
if (chatRegex.test(url)) {
notificationOptions['actions'] = [{
action: "reply",
title: "Reply",
placeholder: "reply",
type: "text",
icon: inlineReplyIcon
}];
}
return self.registration.showNotification(title, notificationOptions);
}
self.addEventListener('push', function(event) {
var payload = event.data.json();
if(!isIdle() && payload.hide_when_active) {
return false;
}
event.waitUntil(
self.registration.getNotifications({ tag: payload.tag }).then(function(notifications) {
if (notifications && notifications.length > 0) {
notifications.forEach(function(notification) {
notification.close();
});
}
return showNotification(payload.title, payload.body, payload.icon, payload.badge, payload.tag, payload.base_url, payload.url);
})
);
});
self.addEventListener('notificationclick', function(event) {
// Android doesn't close the notification when you click on it
// See: http://crbug.com/463146
event.notification.close();
var url = event.notification.data.url;
var baseUrl = event.notification.data.baseUrl;
if (event.action === "reply") {
let csrf;
fetch("/session/csrf", {
credentials: "include",
headers: {
Accept: "application/json",
},
})
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not OK");
}
return response.json();
})
.then((data) => {
csrf = data.csrf;
let chatTest = url.match(chatRegex);
if (chatTest.length > 0) {
let chatChannel = chatTest[1];
fetch(`${baseUrl}/chat/${chatChannel}.json`, {
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-CSRF-Token": csrf,
},
body: `message=${event.reply}`,
method: "POST",
mode: "cors",
});
}
});
} else {
// This looks to see if the current window is already open and
// focuses if it is
event.waitUntil(
clients.matchAll({ type: "window" }).then(function (clientList) {
var reusedClientWindow = clientList.some(function (client) {
if (client.url === baseUrl + url && "focus" in client) {
client.focus();
return true;
}
if ("postMessage" in client && "focus" in client) {
client.focus();
client.postMessage({ url: url });
return true;
}
return false;
});
if (!reusedClientWindow && clients.openWindow)
return clients.openWindow(baseUrl + url);
})
);
}
});
self.addEventListener('message', function(event) {
if('lastAction' in event.data){
lastAction = event.data.lastAction;
}
});
self.addEventListener('pushsubscriptionchange', function(event) {
event.waitUntil(
Promise.all(
fetch('<%= Discourse.base_url %>/push_notifications/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({
"subscription[endpoint]": event.newSubscription.endpoint,
"subscription[keys][auth]": event.newSubscription.toJSON().keys.auth,
"subscription[keys][p256dh]": event.newSubscription.toJSON().keys.p256dh,
"send_confirmation": false
})
}),
fetch('<%= Discourse.base_url %>/push_notifications/unsubscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({
"subscription[endpoint]": event.oldSubscription.endpoint,
"subscription[keys][auth]": event.oldSubscription.toJSON().keys.auth,
"subscription[keys][p256dh]": event.oldSubscription.toJSON().keys.p256dh
})
})
)
);
});
<% DiscoursePluginRegistry.service_workers.each do |js| %>
<%=raw "#{File.read(js)}" %>
<% end %>