summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/website_event_track/controllers/webmanifest.py7
-rw-r--r--addons/website_event_track/static/src/js/service_worker.js62
-rw-r--r--addons/website_event_track/static/src/js/website_event_pwa_widget.js103
3 files changed, 112 insertions, 60 deletions
diff --git a/addons/website_event_track/controllers/webmanifest.py b/addons/website_event_track/controllers/webmanifest.py
index 9f4967ed70d..627c2581577 100644
--- a/addons/website_event_track/controllers/webmanifest.py
+++ b/addons/website_event_track/controllers/webmanifest.py
@@ -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')),
diff --git a/addons/website_event_track/static/src/js/service_worker.js b/addons/website_event_track/static/src/js/service_worker.js
index aec317435ef..41ce7e54297 100644
--- a/addons/website_event_track/static/src/js/service_worker.js
+++ b/addons/website_event_track/static/src/js/service_worker.js
@@ -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;
/**
*
@@ -46,6 +51,18 @@ 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}
*/
const fetchToCacheOfflinePage = () => caches.open(cacheName).then((cache) => cache.add(offlineRoute));
@@ -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) => {
diff --git a/addons/website_event_track/static/src/js/website_event_pwa_widget.js b/addons/website_event_track/static/src/js/website_event_pwa_widget.js
index 1eb93a9cc22..09a3f635417 100644
--- a/addons/website_event_track/static/src/js/website_event_pwa_widget.js
+++ b/addons/website_event_track/static/src/js/website_event_pwa_widget.js
@@ -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");
@@ -39,19 +54,17 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
*
* @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);
},
@@ -68,31 +80,20 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
//--------------------------------------------------------------------------
/**
- * 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 () {
@@ -173,18 +177,6 @@ odoo.define("website_event_track.website_event_pwa_widget", function (require) {
* @private
* @param ev {Event}
*/
- _onBeforeInstallPrompt: function (ev) {
- if (!config.device.isMobile) {
- return;
- }
- ev.preventDefault();
- this.deferredPrompt = ev;
- this._showInstallBanner();
- },
- /**
- * @private
- * @param ev {Event}
- */
_onPromptCloseBar: function (ev) {
ev.stopPropagation();
this._hideInstallBanner();
@@ -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");
+ });
},
});