[FW][FIX] website_event_track: PWA install & ServiceWorker caching
This PR aims at preventing the cluttering of the ServiceWorker's cache by filtering out non-necessary assets and preventing "opaque" responses from being cached. Also, it registers earlier the listener for the PWA install event, allowing to properly show the installation's banner in most of the case. Finally, it fixes the optional URL's language parsing by allowing custom language naming. closes odoo/odoo#59001 Forward-port-of: odoo/odoo#58994 Signed-off-by: Adrien Dieudonné (adr) <adr@odoo.com> Signed-off-by: Pierre Paridans <pparidans@users.noreply.github.com>
This commit is contained in:
@@ -48,8 +48,13 @@ class TrackManifest(http.Controller):
|
||||
""" Returns a ServiceWorker javascript file scoped for website_event
|
||||
"""
|
||||
sw_file = get_module_resource('website_event_track', 'static/src/js/service_worker.js')
|
||||
with open(sw_file, 'rb') as fp:
|
||||
with open(sw_file, 'r') as fp:
|
||||
body = fp.read()
|
||||
js_cdn_url = 'undefined'
|
||||
if request.website.cdn_activated:
|
||||
cdn_url = request.website.cdn_url.replace('"','%22').replace('\x5c','%5C')
|
||||
js_cdn_url = '"%s"' % cdn_url
|
||||
body = body.replace('__ODOO_CDN_URL__', js_cdn_url)
|
||||
response = request.make_response(body, [
|
||||
('Content-Type', 'text/javascript'),
|
||||
('Service-Worker-Allowed', url_for('/event')),
|
||||
|
||||
@@ -3,6 +3,9 @@ importScripts("/website_event_track/static/lib/idb-keyval/idb-keyval.js");
|
||||
const PREFIX = "odoo-event";
|
||||
const SYNCABLE_ROUTES = ["/event/track/toggle_reminder"];
|
||||
const CACHABLE_ROUTES = ["/web/webclient/version_info"];
|
||||
const MAX_CACHE_SIZE = 512 * 1024 * 1024; // 500 MB
|
||||
const MAX_CACHE_QUOTA = 0.5;
|
||||
const CDN_URL = __ODOO_CDN_URL__; // {string|undefined} the cdn_url configured for the website if activated
|
||||
|
||||
const { Store, set, get, del } = idbKeyval;
|
||||
const pendingRequestsQueueName = `${PREFIX}-pending-requests`;
|
||||
@@ -10,6 +13,8 @@ const cacheName = `${PREFIX}-cache`;
|
||||
const syncStore = new Store(`${PREFIX}-sync-db`, `${PREFIX}-sync-store`);
|
||||
const cacheStore = new Store(`${PREFIX}-cache-db`, `${PREFIX}-cache-store`);
|
||||
const offlineRoute = `${self.registration.scope}/offline`;
|
||||
const scopeURL = new URL(self.registration.scope);
|
||||
const cdnURL = CDN_URL ? (CDN_URL.startsWith("http") ? new URL(CDN_URL) : new URL(`http:${CDN_URL}`)) : undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -44,6 +49,18 @@ const isSyncableURL = canHandleRoutes(SYNCABLE_ROUTES);
|
||||
*/
|
||||
const isCachableURL = canHandleRoutes(CACHABLE_ROUTES);
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {boolean} true if navigator has a quota we can read and we reached it
|
||||
*/
|
||||
const isCacheFull = async () => {
|
||||
if (!("storage" in navigator && "estimate" in navigator.storage)) {
|
||||
return false;
|
||||
}
|
||||
const { usage, quota } = await navigator.storage.estimate();
|
||||
return usage / quota > MAX_CACHE_QUOTA || usage > MAX_CACHE_SIZE;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {Promise}
|
||||
@@ -133,6 +150,33 @@ const buildEmptyResponse = () => new Response(JSON.stringify({ jsonrpc: "2.0", i
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const cacheRequest = async (request, response) => {
|
||||
// only attempts to cache local or cdn delivered urls
|
||||
const url = new URL(request.url);
|
||||
if (url.hostname !== scopeURL.hostname && (!cdnURL || url.hostname !== cdnURL.hostname)) {
|
||||
console.error(`ignoring cache for ${request.url} => ${url.hostname}, local: ${scopeURL.hostname}, cdn: ${cdnURL ? cdnURL.hostname : cdnURL}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// don't even attempt to cache:
|
||||
// - error pages (why cache that?)
|
||||
// - non-"basic" response types, which include tracker 1-time opaque requests
|
||||
// that are consuming cache space for no reason (namely due to padding MBs accounted for
|
||||
// each opaque request)
|
||||
if (!response || !response.ok || response.type !== "basic") {
|
||||
console.error(`ignoring cache for ${request.url} => ${response.type}, mode: ${request.mode}, cache: ${request.cache}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// never blow up cache quota, as it will break things, and the space
|
||||
// is shared with cookies and localStorage
|
||||
if (await isCacheFull()) {
|
||||
// TODO: clear some part of the cache to free older/less-relevant content
|
||||
console.log("Cache full, not caching!");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`grant cache for ${request.url} => ${response.type}, mode: ${request.mode}, cache: ${request.cache},
|
||||
isGet: ${isGET(request)}, isCachable: ${isCachableURL(request.url)}`);
|
||||
if (isGET(request)) {
|
||||
const cache = await caches.open(cacheName);
|
||||
await cache.put(request, response.clone());
|
||||
@@ -191,7 +235,7 @@ const matchCache = async (request) => {
|
||||
* @param {FetchEvent} param0
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const processFetchEvent = async ({ request }) => {
|
||||
const processFetchRequest = async ({ request }) => {
|
||||
const requestCopy = request.clone();
|
||||
let response;
|
||||
try {
|
||||
@@ -254,11 +298,17 @@ const processPendingRequests = async () => {
|
||||
*/
|
||||
const prefetchUrls = async (urls = []) => {
|
||||
const cache = await caches.open(cacheName);
|
||||
let urlsToCache = new Set(urls);
|
||||
for (let url of urlsToCache) {
|
||||
(await cache.match(url)) ? urlsToCache.delete(url) : undefined;
|
||||
const uniqUrls = new Set(urls);
|
||||
for (let url of uniqUrls) {
|
||||
if (await cache.match(url)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await processFetchRequest({ request: new Request(url) });
|
||||
} catch (error) {
|
||||
console.error(`fail to prefetch ${url} : ${error}`);
|
||||
}
|
||||
}
|
||||
return cache.addAll([...urlsToCache]);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -293,7 +343,7 @@ const processMessage = (data) => {
|
||||
};
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
event.respondWith(processFetchEvent(event));
|
||||
event.respondWith(processFetchRequest(event));
|
||||
});
|
||||
|
||||
self.addEventListener("sync", (event) => {
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
odoo.define("website_event_track.website_event_pwa_widget", function (require) {
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
* The "deferredPrompt" Promise will resolve only if the "beforeinstallprompt" event
|
||||
* has been triggered. It allows to register this listener as soon as possible
|
||||
* to avoid missed-events (as the browser can trigger it very early in the page lifecycle).
|
||||
*/
|
||||
var deferredPrompt = new Promise(function (resolve, reject) {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return reject();
|
||||
}
|
||||
window.addEventListener("beforeinstallprompt", function (ev) {
|
||||
ev.preventDefault();
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
|
||||
var config = require("web.config");
|
||||
var publicWidget = require("web.public.widget");
|
||||
var utils = require("web.utils");
|
||||
@@ -35,23 +50,21 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
|
||||
prompt_close_bar: "_onPromptCloseBar",
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.beforeInstallPromptHandler = this._onBeforeInstallPrompt.bind(this);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var superProm = this._super.apply(this, arguments);
|
||||
window.addEventListener("beforeinstallprompt", this.beforeInstallPromptHandler);
|
||||
return superProm.then(this._registerServiceWorker.bind(this)).then(this._prefetch.bind(this));
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments)
|
||||
.then(this._registerServiceWorker.bind(this))
|
||||
.then(function () {
|
||||
// Don't wait for the prompt's Promise as it may never resolve.
|
||||
deferredPrompt.then(self._showInstallBanner.bind(self)).catch(function () {
|
||||
console.log("ServiceWorker not supported");
|
||||
});
|
||||
})
|
||||
.then(this._prefetch.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -59,7 +72,6 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
window.removeEventListener("beforeinstallprompt", this.beforeInstallPromptHandler);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -67,32 +79,21 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the website's language from the URL
|
||||
*
|
||||
* Note: this method performs a matching against both the simple (eg. "en") and complete (eg. "en_US")
|
||||
* language formatting in the url. This is required as the cookie always returns the complete
|
||||
* form but the URL can use both, depending on the language (eg. "en_US" => "en" but "en_UK" => "en_UK").
|
||||
* @private
|
||||
* @return {String}
|
||||
*/
|
||||
_getLangPrefix: function () {
|
||||
var lang = utils.get_cookie("frontend_lang");
|
||||
var path = window.location.pathname;
|
||||
var matches = path.match(new RegExp("^\/?(" + lang + "|" + lang.slice(0, 2) + ")\/"));
|
||||
if (lang && matches && matches[1]) {
|
||||
return "/" + matches[1];
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the PWA's scope
|
||||
*
|
||||
* Note: this method performs a matching to handle URLs with the language prefix.
|
||||
* Typically this prefix is in the form of "en" or "en_US" but it can also be
|
||||
* any string using the customization options in the Website's settings.
|
||||
* @private
|
||||
* @returns {String}
|
||||
*/
|
||||
_getScope: function () {
|
||||
return this._getLangPrefix() + "/event";
|
||||
var matches = window.location.pathname.match(/^(\/(?:event|[^/]+\/event))\/?/);
|
||||
if (matches && matches[1]) {
|
||||
return matches[1];
|
||||
}
|
||||
return "/event";
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -156,6 +157,9 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
|
||||
* @private
|
||||
*/
|
||||
_showInstallBanner: function () {
|
||||
if (!config.device.isMobile) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
this.installBanner = new PWAInstallBanner(this);
|
||||
this.installBanner.appendTo(this.$el).then(function () {
|
||||
@@ -169,18 +173,6 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param ev {Event}
|
||||
*/
|
||||
_onBeforeInstallPrompt: function (ev) {
|
||||
if (!config.device.isMobile) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this.deferredPrompt = ev;
|
||||
this._showInstallBanner();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param ev {Event}
|
||||
@@ -195,15 +187,20 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
|
||||
*/
|
||||
_onPromptInstall: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.deferredPrompt.prompt();
|
||||
this._hideInstallBanner();
|
||||
this.deferredPrompt.userChoice.then(function (choiceResult) {
|
||||
if (choiceResult.outcome === "accepted") {
|
||||
console.log("User accepted the install prompt");
|
||||
} else {
|
||||
console.log("User dismissed the install prompt");
|
||||
}
|
||||
});
|
||||
deferredPrompt.then(function (prompt) {
|
||||
prompt.prompt();
|
||||
prompt.userChoice.then(function (choiceResult) {
|
||||
if (choiceResult.outcome === "accepted") {
|
||||
console.log("User accepted the install prompt");
|
||||
} else {
|
||||
console.log("User dismissed the install prompt");
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
console.log("ServiceWorker not supported");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user