Commit 2b401afc authored by Romain Courteaud's avatar Romain Courteaud

erp5_web_renderjs_ui: remove all clientIds handling

Waiting for all clients to be closed before activating the new service worker is not usable, as it will lead to user keeping the old code.
Drop the logic for now.

Fetch the list of precache url dynamically in order to decouple the worker code from the list of files.

Calculate dynamically the list of files from a python script, depending on the web site configuration.

Update the service worker as soon as the web site modification date changes.
This (I hope) will simplify ERP5 upgrade handling.
parent 9a69ba5e
/*jslint indent: 2*/ /*jslint indent: 2*/
/*global self, caches, fetch, Promise, URL, location, Response, console*/ /*global self, caches, fetch, Promise, URL, location*/
(function (self, caches, fetch, Promise, URL, location, Response) { (function (self, caches, fetch, Promise, URL, location) {
"use strict"; "use strict";
var prefix = location.toString() + '_', var prefix = location.toString() + '_',
// CLIENT_CACHE_MAPPING_NAME must not start with `prefix` CACHE_NAME = prefix + '${modification_date}',
// else it may be used as a normal content cache. required_url_list = [];
CLIENT_CACHE_MAPPING_NAME = '__erp5js_' + location.toString(),
CACHE_NAME = prefix + '_0014',
CACHE_MAP = {},
// Files required to make this app work offline
REQUIRED_FILES = [
],
required_url_list = [],
i;
for (i = 0; i < REQUIRED_FILES.length; i += 1) {
required_url_list.push(
new URL(REQUIRED_FILES[i], location.toString()).toString()
);
}
self.addEventListener('install', function (event) { self.addEventListener('install', function (event) {
// Perform install step: loading each required file into cache // Perform install step: loading each required file into cache
event.waitUntil( event.waitUntil(
...@@ -32,43 +19,32 @@ ...@@ -32,43 +19,32 @@
caches.has(CACHE_NAME) caches.has(CACHE_NAME)
.then(function (result) { .then(function (result) {
if (!result) { if (!result) {
caches.open(CACHE_NAME) return fetch('WebSection_getPrecacheManifest')
.then(function (cache) { .then(function (response) {
// Add all offline dependencies to the cache return Promise.all([
return Promise.all( response.json(),
required_url_list caches.open(CACHE_NAME)
.map(function (url) { ]);
/* Return a promise that's fulfilled
when each url is cached.
*/
// Use cache.add because safari does not support cache.addAll.
// console.log("Install " + CACHE_NAME + " = " + url);
return cache.add(url);
})
);
}) })
.then(function () { .then(function (result_list) {
return caches.keys(); var required_file_dict = result_list[0],
}) cache = result_list[1],
.then(function (keys) { key,
keys = keys.filter(function (key) {return key.startsWith(prefix); }); promise_list = [],
if (keys.length === 1) { url;
// When user accesses ERP5JS web site first time, service worker is
// installed but it is not activated yet, service worker is activated for (key in required_file_dict) {
// when the page is refreshed or when a new tab opens the site again. if (required_file_dict.hasOwnProperty(key)) {
// If user does not refresh the page and continue to use the site, url = new URL(key, location.toString()).toString();
// user can't use cache, so everything becomes slow. We must avoid this required_url_list.push(url);
// situation. // Use cache.add because safari does not support cache.addAll.
// So, we want to activate the new service worker immediately if it was // console.log("Install " + CACHE_NAME + " = " + url);
// the first one. (We must not activate the new service worker by promise_list.push(cache.add(url));
// skipWaiting if there is already an active service worker because it }
// causes code inconsistency by loading code from a different version of
// cache.
// If there is only one cache, it means that this is the first service worker,
// thus we can do skipWaiting. And self.registration is unreliable on
// Firefox, we can't use self.registration.active
return self.skipWaiting();
} }
// Add all offline dependencies to the cache
return Promise.all(promise_list);
}) })
.catch(function (error) { .catch(function (error) {
// Since we do not allow to override existing cache, if cache installation // Since we do not allow to override existing cache, if cache installation
...@@ -80,40 +56,23 @@ ...@@ -80,40 +56,23 @@
}); });
} }
}) })
.then(function () {
// When user accesses ERP5JS web site first time, service worker is
// installed but it is not activated yet, service worker is activated
// when the page is refreshed or when a new tab opens the site again.
// If user does not refresh the page and continue to use the site,
// user can't use cache, so everything becomes slow. We must avoid this
// situation.
// So, we want to activate the new service worker immediately if it was
// the first one.
return self.skipWaiting();
})
); );
}); });
self.addEventListener('fetch', function (event) { self.addEventListener('fetch', function (event) {
/* When a new service worker is installed, it adds a new Cache var url = new URL(event.request.url);
to Cache Storage. When a new client started using this
service worker, the new client uses the latest Cache at
that time by comparing with Cache keys. And once the client
is associated with a Cache key, it keeps using the same Cache
key, it must not use different Caches. Since service worker
is stateless, to maintain the mapping of client and Cache key,
we use Cache Storage as a persistent data store. The key of
this special Cache is CLIENT_CACHE_MAPPING_NAME.
*/
var url = new URL(event.request.url),
client_id = event.clientId.toString(),
// CACHE_MAP is a temprary data store.
// This should be kept until service worker stops.
cache_key,
erp5js_cache;
url.hash = ''; url.hash = '';
if (client_id) {
// client_id is null when it is the first request, in other words
// if request is navigate mode. Since major web browsers already
// implement client_id, if client_is is null, let's use the latest cache
// and don't get cache_key from CACHE_MAP and erp5js_cache.
cache_key = CACHE_MAP[client_id];
}
// console.log("Client Id = " + client_id);
/*
if (cache_key) {
console.log("cache_key from CACHE_MAP " + cache_key);
}
*/
if ((event.request.method !== 'GET') || if ((event.request.method !== 'GET') ||
(required_url_list.indexOf(url.toString()) === -1)) { (required_url_list.indexOf(url.toString()) === -1)) {
// Try not to use the untrustable fetch function // Try not to use the untrustable fetch function
...@@ -121,64 +80,11 @@ ...@@ -121,64 +80,11 @@
return; return;
} }
return event.respondWith( return event.respondWith(
Promise.resolve() caches.open(CACHE_NAME)
.then(function () {
if (!cache_key) {
// CLIENT_CACHE_MAPPING_NAME stores cache_key of each client.
return caches.open(CLIENT_CACHE_MAPPING_NAME)
.then(function (cache) {
// Service worker forget everything when it stops. So, when it started
// again, CACHE_MAP is empty, get the associated cache_key from the
// special Cache named CLIENT_CACHE_MAPPING_NAME.
erp5js_cache = cache;
return erp5js_cache.match(client_id)
.then(function (response) {
if (response) {
// We use Cache Storage as a persistent database.
cache_key = response.statusText;
CACHE_MAP[client_id] = cache_key;
// console.log("cache_key from Cache Storage " + cache_key);
}
});
});
}
})
.then(function () {
if (!cache_key) {
// If associated cache_key is not found, it means this client is a new one.
// Let's find the latest Cache.
return caches.keys()
.then(function (keys) {
keys = keys.filter(function (key) {return key.startsWith(prefix); });
// console.log("KEYS = " + keys);
if (keys.length) {
cache_key = keys.sort().reverse()[0];
if (client_id) {
CACHE_MAP[client_id] = cache_key;
}
} else {
cache_key = CACHE_NAME;
if (client_id) {
CACHE_MAP[client_id] = CACHE_NAME;
}
}
// Save the associated cache_key in a persistent database because service
// worker forget everything when it stops.
if (client_id) {
erp5js_cache.put(client_id, new Response(null, {"statusText": cache_key}));
}
});
}
})
.then(function () {
// Finally we have the associated cache_key. Let's find a cached response.
return caches.open(cache_key);
})
.then(function (cache) { .then(function (cache) {
// Don't give request object itself. Firefox's Cache Storage // Don't give request object itself. Firefox's Cache Storage
// does not work properly when VARY contains Accept-Language. // does not work properly when VARY contains Accept-Language.
// Give URL string instead, then cache.match works on both Firefox and Chrome. // Give URL string instead, then cache.match works on both Firefox and Chrome.
// console.log("MATCH " + cache_key + " " + url);
return cache.match(event.request.url); return cache.match(event.request.url);
}) })
.then(function (response) { .then(function (response) {
...@@ -188,7 +94,6 @@ ...@@ -188,7 +94,6 @@
} }
// Not in cache - return the result from the live server // Not in cache - return the result from the live server
// `fetch` is essentially a "fallback" // `fetch` is essentially a "fallback"
// console.log("MISS " + cache_key + " " + url);
return fetch(event.request); return fetch(event.request);
}) })
); );
...@@ -205,16 +110,15 @@ ...@@ -205,16 +110,15 @@
*/ */
.keys() .keys()
.then(function (keys) { .then(function (keys) {
keys = keys
.filter(function (key) {
// Filter by keys that don't start with the latest version prefix.
return key.startsWith(prefix);
})
.sort();
keys = keys.slice(0, keys.findIndex(function (element) {return element === CACHE_NAME; }));
// We return a promise that settles when all outdated caches are deleted. // We return a promise that settles when all outdated caches are deleted.
return Promise.all( return Promise.all(
keys keys
.filter(function (key) {
// Filter by keys that don't start with the latest version prefix.
// return !key.startsWith(version);
return ((key !== CACHE_NAME) &&
key.startsWith(prefix));
})
.map(function (key) { .map(function (key) {
/* Return a promise that's fulfilled /* Return a promise that's fulfilled
when each outdated cache is deleted. when each outdated cache is deleted.
...@@ -224,12 +128,9 @@ ...@@ -224,12 +128,9 @@
); );
}) })
.then(function () { .then(function () {
// If new service worker becomes active, it means that all clients self.clients.claim();
// (tabs, windows, etc) were already closed. Thus we can remove the
// client cache mapping.
caches.delete(CLIENT_CACHE_MAPPING_NAME);
}) })
); );
}); });
}(self, caches, fetch, Promise, URL, location, Response)); }(self, caches, fetch, Promise, URL, location));
\ No newline at end of file \ No newline at end of file
...@@ -95,6 +95,10 @@ ...@@ -95,6 +95,10 @@
<none/> <none/>
</value> </value>
</item> </item>
<item>
<key> <string>text_content_substitution_mapping_method_id</string> </key>
<value> <string>WebPage_getRenderJSSubstitutionMappingDict</string> </value>
</item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
<value> <string>ERP5 ServiceWorker</string> </value> <value> <string>ERP5 ServiceWorker</string> </value>
...@@ -234,7 +238,7 @@ ...@@ -234,7 +238,7 @@
</item> </item>
<item> <item>
<key> <string>serial</string> </key> <key> <string>serial</string> </key>
<value> <string>981.43215.21388.62139</string> </value> <value> <string>981.45037.17470.33382</string> </value>
</item> </item>
<item> <item>
<key> <string>state</string> </key> <key> <string>state</string> </key>
...@@ -252,7 +256,7 @@ ...@@ -252,7 +256,7 @@
</tuple> </tuple>
<state> <state>
<tuple> <tuple>
<float>1580982701.01</float> <float>1581092014.2</float>
<string>UTC</string> <string>UTC</string>
</tuple> </tuple>
</state> </state>
......
...@@ -11,9 +11,10 @@ response.setHeader("Access-Control-Allow-Origin", "*") ...@@ -11,9 +11,10 @@ response.setHeader("Access-Control-Allow-Origin", "*")
web_page = context web_page = context
web_section = context.getWebSectionValue() web_section = context.getWebSectionValue()
modification_date_string = web_page.Base_getWebSiteDrivenModificationDate().rfc822()
# Must-Revalidate caching policy uses Base_getWebSiteDrivenModificationDate # Must-Revalidate caching policy uses Base_getWebSiteDrivenModificationDate
if REQUEST.getHeader('If-Modified-Since', '') == web_page.Base_getWebSiteDrivenModificationDate().rfc822(): if REQUEST.getHeader('If-Modified-Since', '') == modification_date_string:
response.setStatus(304) response.setStatus(304)
return "" return ""
...@@ -23,6 +24,9 @@ web_content = web_page.getTextContent() ...@@ -23,6 +24,9 @@ web_content = web_page.getTextContent()
# set headers depending on type of script # set headers depending on type of script
if (portal_type == "Web Script"): if (portal_type == "Web Script"):
response.setHeader('Content-Type', 'application/javascript; charset=utf-8') response.setHeader('Content-Type', 'application/javascript; charset=utf-8')
web_content = web_page.TextDocument_substituteTextContent(web_content, mapping_dict={
'modification_date': modification_date_string
})
elif (portal_type == "Web Style"): elif (portal_type == "Web Style"):
response.setHeader('Content-Type', 'text/css; charset=utf-8') response.setHeader('Content-Type', 'text/css; charset=utf-8')
......
import json
if REQUEST is None:
REQUEST = context.REQUEST
if response is None:
response = REQUEST.RESPONSE
web_section = context
# Add all ERP5JS gadget
url_list = [
'favicon.ico',
'font-awesome/font-awesome-webfont.eot',
'font-awesome/font-awesome-webfont.woff',
'font-awesome/font-awesome-webfont.woff2',
'font-awesome/font-awesome-webfont.ttf',
'font-awesome/font-awesome-webfont.svg',
'gadget_erp5_worklist_empty.svg?format=svg',
'erp5_launcher_nojqm.js',
'gadget_erp5_nojqm.css',
'gadget_erp5_configure_editor.html',
'gadget_erp5_configure_editor.js',
'gadget_erp5_editor_panel.html',
'gadget_erp5_editor_panel.js',
'gadget_erp5_field_checkbox.html',
'gadget_erp5_field_checkbox.js',
'gadget_erp5_field_datetime.html',
'gadget_erp5_field_datetime.js',
'gadget_erp5_field_editor.html',
'gadget_erp5_field_editor.js',
'gadget_erp5_field_email.html',
'gadget_erp5_field_email.js',
'gadget_erp5_field_file.html',
'gadget_erp5_field_file.js',
'gadget_erp5_field_float.html',
'gadget_erp5_field_float.js',
'gadget_erp5_field_formbox.html',
'gadget_erp5_field_formbox.js',
'gadget_erp5_field_gadget.html',
'gadget_erp5_field_gadget.js',
'gadget_erp5_field_image.html',
'gadget_erp5_field_image.js',
'gadget_erp5_field_integer.html',
'gadget_erp5_field_integer.js',
'gadget_erp5_field_list.html',
'gadget_erp5_field_list.js',
'gadget_erp5_field_lines.html',
'gadget_erp5_field_lines.js',
'gadget_erp5_field_listbox.html',
'gadget_erp5_field_listbox.js',
'gadget_erp5_field_matrixbox.html',
'gadget_erp5_field_matrixbox.js',
'gadget_erp5_field_multicheckbox.html',
'gadget_erp5_field_multicheckbox.js',
'gadget_erp5_field_multilist.html',
'gadget_erp5_field_multilist.js',
'gadget_erp5_field_multirelationstring.html',
'gadget_erp5_field_multirelationstring.js',
'gadget_erp5_field_radio.html',
'gadget_erp5_field_radio.js',
'gadget_erp5_field_readonly.html',
'gadget_erp5_field_readonly.js',
'gadget_erp5_field_relationstring.html',
'gadget_erp5_field_relationstring.js',
'gadget_erp5_field_string.html',
'gadget_erp5_field_string.js',
'gadget_erp5_field_password.html',
'gadget_erp5_field_password.js',
'gadget_erp5_field_textarea.html',
'gadget_erp5_field_textarea.js',
'gadget_erp5_form.html',
'gadget_erp5_form.js',
'gadget_erp5_header.html',
'gadget_erp5_header.js',
'gadget_erp5_jio.html',
'gadget_erp5_jio.js',
'gadget_erp5_label_field.html',
'gadget_erp5_label_field.js',
'gadget_erp5_notification.html',
'gadget_erp5_notification.js',
'gadget_erp5_page_action.html',
'gadget_erp5_page_action.js',
'gadget_erp5_page_export.html',
'gadget_erp5_page_export.js',
'gadget_erp5_page_form.html',
'gadget_erp5_page_form.js',
'gadget_erp5_page_front.html',
'gadget_erp5_page_front.js',
'gadget_erp5_page_history.html',
'gadget_erp5_page_history.js',
'gadget_erp5_page_jump.html',
'gadget_erp5_page_jump.js',
'gadget_erp5_page_language.html',
'gadget_erp5_page_language.js',
'gadget_erp5_page_logout.html',
'gadget_erp5_page_logout.js',
'gadget_erp5_page_preference.html',
'gadget_erp5_page_preference.js',
'gadget_erp5_page_relation_search.html',
'gadget_erp5_page_relation_search.js',
'gadget_erp5_page_search.html',
'gadget_erp5_page_search.js',
'gadget_erp5_page_tab.html',
'gadget_erp5_page_tab.js',
'gadget_erp5_page_worklist.html',
'gadget_erp5_page_worklist.js',
'gadget_erp5_panel.html',
'gadget_erp5_panel.js',
'gadget_erp5_panel.png?format=png',
'gadget_erp5_pt_embedded_form_render.html',
'gadget_erp5_pt_embedded_form_render.js',
'gadget_erp5_pt_form_dialog.html',
'gadget_erp5_pt_form_dialog.js',
'gadget_erp5_pt_form_list.html',
'gadget_erp5_pt_form_list.js',
'gadget_erp5_pt_form_view.html',
'gadget_erp5_pt_form_view.js',
'gadget_erp5_pt_form_view_editable.html',
'gadget_erp5_pt_form_view_editable.js',
'gadget_erp5_pt_report_view.html',
'gadget_erp5_pt_report_view.js',
'gadget_erp5_router.html',
'gadget_erp5_router.js',
'gadget_erp5_relation_input.html',
'gadget_erp5_relation_input.js',
'gadget_erp5_search_editor.html',
'gadget_erp5_search_editor.js',
'gadget_erp5_searchfield.html',
'gadget_erp5_searchfield.js',
'gadget_erp5_sort_editor.html',
'gadget_erp5_sort_editor.js',
'gadget_global.js',
'gadget_html5_element.html',
'gadget_html5_element.js',
'gadget_html5_input.html',
'gadget_html5_input.js',
'gadget_html5_textarea.html',
'gadget_html5_textarea.js',
'gadget_html5_select.html',
'gadget_html5_select.js',
'gadget_erp5_global.js',
'gadget_jio.html',
'gadget_jio.js',
'gadget_translation.html',
'gadget_translation.js',
'gadget_translation_data.js',
'gadget_editor.html',
'gadget_editor.js',
'gadget_button_maximize.html',
'gadget_button_maximize.js',
'handlebars.js',
'jiodev.js',
'renderjs.js',
'rsvp.js',
]
# Add all root gadgets
default_url = './'
available_language_set = web_section.getLayoutProperty("available_language_set", default=['en'])
default_language = web_section.getLayoutProperty("default_available_language", default='en')
for language in available_language_set:
if language == default_language:
url_list.append(default_url)
else:
url_list.append('%s/' % language)
# Add all custom gadgets
url_list.extend([
web_section.getLayoutProperty("configuration_wallpaper_url", default=default_url),
web_section.getLayoutProperty("configuration_panel_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_router_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_header_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_jio_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_translation_gadget_url", default=default_url),
web_section.getLayoutProperty("configuration_stylesheet_url", default=default_url),
])
result = json.dumps(dict.fromkeys(url_list, None), indent=2)
response.setHeader('Content-Type', 'application/json')
return result
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_Cacheable__manager_id</string> </key>
<value> <string>must_revalidate_http_cache</string> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None, response=None</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Anonymous</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>WebSection_getPrecacheManifest</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment