From 301cb8620b6faec44b4b8469b552328124d1aa24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= <3857362+corrideat@users.noreply.github.com> Date: Fri, 31 Jan 2025 23:28:23 +0200 Subject: [PATCH] Improve GI_VERSION update logic (#2568) * Improve GI_VERSION update logic Closes #2562. * Feedback --- frontend/controller/backend.js | 35 +++++++- frontend/controller/service-worker.js | 79 ++++++++++--------- .../controller/serviceworkers/sw-primary.js | 36 +++++++-- 3 files changed, 101 insertions(+), 49 deletions(-) diff --git a/frontend/controller/backend.js b/frontend/controller/backend.js index cf4e1d1e0f..0c4a476efb 100644 --- a/frontend/controller/backend.js +++ b/frontend/controller/backend.js @@ -3,6 +3,7 @@ import type { JSONObject } from '~/shared/types.js' import sbp from '@sbp/sbp' +import { HOURS_MILLIS } from '~/frontend/model/contracts/shared/time.js' import { NOTIFICATION_TYPE } from '~/shared/pubsub.js' import { handleFetchResult } from './utils/misc.js' import { PUBSUB_INSTANCE } from './instance-keys.js' @@ -14,14 +15,40 @@ const languageFileMap = new Map([ ]) sbp('okTurtles.events/on', NOTIFICATION_TYPE.VERSION_INFO, (versionInfo) => { + if (versionInfo.GI_VERSION === process.env.GI_VERSION) { + // No refresh necessary, we're already at the latest version + sessionStorage.removeItem(NOTIFICATION_TYPE.VERSION_INFO) + return + } + console.info('New Group Income version available:', versionInfo) - // Prevent the client from trying to reconnect when the page starts unloading. - sbp('okTurtles.data/get', PUBSUB_INSTANCE).destroy() // TODO: allow the user to manually reload the page later. + try { + // Store the current VERSION_INFO in session storage to prevent infinite + // reload loops + const existingSerialized = sessionStorage.getItem(NOTIFICATION_TYPE.VERSION_INFO) + if (existingSerialized) { + const existingVersionInfo = JSON.parse(existingSerialized) + if ( + Array.isArray(existingVersionInfo) && + !(Date.now() - existingVersionInfo[0] >= 2.5 * HOURS_MILLIS) && + versionInfo.GI_VERSION === existingVersionInfo[1].GI_VERSION + ) { + console.warn('[NOTIFICATION_TYPE.VERSION_INFO] A different Group Income version is available, but reloading has failed to address it', { existingVersionInfo, versionInfo }) + return + } + } + + sessionStorage.setItem(NOTIFICATION_TYPE.VERSION_INFO, JSON.stringify([Date.now(), versionInfo])) + } catch (e) { + console.error('[NOTIFICATION_TYPE.VERSION_INFO] Error in handler', e) + } + // Prevent the client from trying to reconnect when the page starts unloading. + sbp('okTurtles.data/get', PUBSUB_INSTANCE)?.destroy() window.location.reload() }) -sbp('sbp/selectors/register', { +export default (sbp('sbp/selectors/register', { /** * Fetches a JSON object containing translation strings for a given language. * @@ -39,4 +66,4 @@ sbp('sbp/selectors/register', { .then(handleFetchResult('json')) } } -}) +}): string[]) diff --git a/frontend/controller/service-worker.js b/frontend/controller/service-worker.js index afef5b8d65..d5477249f1 100644 --- a/frontend/controller/service-worker.js +++ b/frontend/controller/service-worker.js @@ -51,6 +51,44 @@ sbp('sbp/selectors/register', { await swRegistration.update() setInterval(() => sbp('service-worker/update'), HOURS_MILLIS) + navigator.serviceWorker.addEventListener('message', event => { + const data = event.data + const silentEmit = sbp('sbp/selectors/fn', 'okTurtles.events/emit') + + if (typeof data === 'object' && data.type) { + switch (data.type) { + case 'pong': + break + case 'event': { + sbp('okTurtles.events/emit', event.data.subtype, ...deserializer(event.data.data)) + break + } + case 'navigate': { + if (data.groupID) { + sbp('state/vuex/commit', 'setCurrentGroupId', { contractID: data.groupID }) + } + sbp('controller/router').push({ path: data.path }).catch(console.warn) + break + } + // `sbp` invocations from the SW to the app. Used by the + // `notificationclick` handler for notifications that have an + // `sbpInvocation` instead of a `linkTo` property. + case 'sbp': { + sbp(...deserializer(event.data.data)) + break + } + case CAPTURED_LOGS: { + // Emit silently to avoid flooding logs with event emitted entries + silentEmit(CAPTURED_LOGS, ...deserializer(event.data.data)) + break + } + default: + console.error('[sw] Received unknown message type from the service worker:', data) + break + } + } + }) + // Send a 'ready' message to the SW and wait back for a response // This way we ensure that Chelonia has been set up await new Promise((resolve, reject) => { @@ -71,7 +109,8 @@ sbp('sbp/selectors/register', { navigator.serviceWorker.ready.then((worker) => { worker.active.postMessage({ type: 'ready', - port: messageChannel.port2 + port: messageChannel.port2, + GI_VERSION: process.env.GI_VERSION }, [messageChannel.port2]) }).catch((e) => { reject(e) @@ -110,44 +149,6 @@ sbp('sbp/selectors/register', { // there are open tabs, which makes it faster and smoother to interact // with contracts than if the service worker had to be restarted. setInterval(() => navigator.serviceWorker.controller?.postMessage({ type: 'ping' }), 5000) - - navigator.serviceWorker.addEventListener('message', event => { - const data = event.data - const silentEmit = sbp('sbp/selectors/fn', 'okTurtles.events/emit') - - if (typeof data === 'object' && data.type) { - switch (data.type) { - case 'pong': - break - case 'event': { - sbp('okTurtles.events/emit', event.data.subtype, ...deserializer(event.data.data)) - break - } - case 'navigate': { - if (data.groupID) { - sbp('state/vuex/commit', 'setCurrentGroupId', { contractID: data.groupID }) - } - sbp('controller/router').push({ path: data.path }).catch(console.warn) - break - } - // `sbp` invocations from the SW to the app. Used by the - // `notificationclick` handler for notifications that have an - // `sbpInvocation` instead of a `linkTo` property. - case 'sbp': { - sbp(...deserializer(event.data.data)) - break - } - case CAPTURED_LOGS: { - // Emit silently to avoid flooding logs with event emitted entries - silentEmit(CAPTURED_LOGS, ...deserializer(event.data.data)) - break - } - default: - console.error('[sw] Received unknown message type from the service worker:', data) - break - } - } - }) } catch (e) { console.error('error setting up service worker:', e) throw e diff --git a/frontend/controller/serviceworkers/sw-primary.js b/frontend/controller/serviceworkers/sw-primary.js index 2882b80eb7..928e632f9a 100644 --- a/frontend/controller/serviceworkers/sw-primary.js +++ b/frontend/controller/serviceworkers/sw-primary.js @@ -19,7 +19,8 @@ import { KV_KEYS } from '~/frontend/utils/constants.js' import { CHELONIA_STATE_MODIFIED, LOGIN, LOGIN_ERROR, LOGOUT } from '~/frontend/utils/events.js' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' import { Secret } from '~/shared/domains/chelonia/Secret.js' -import { CHELONIA_RESET, CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, EVENT_HANDLED } from '~/shared/domains/chelonia/events.js' +import { CHELONIA_RESET, CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, CONTRACT_REGISTERED, EVENT_HANDLED } from '~/shared/domains/chelonia/events.js' +import { NOTIFICATION_TYPE } from '~/shared/pubsub.js' import { deserializer, serializer } from '~/shared/serdes/index.js' import { ACCEPTED_GROUP, CAPTURED_LOGS, CHATROOM_USER_STOP_TYPING, @@ -103,11 +104,12 @@ sbp('okTurtles.events/on', CHELONIA_RESET, setupRootState) // These are all of the events that will be forwarded to all open tabs and windows ;[ - CHELONIA_RESET, CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, EVENT_HANDLED, LOGIN, - LOGIN_ERROR, LOGOUT, ACCEPTED_GROUP, CHATROOM_USER_STOP_TYPING, - CHATROOM_USER_TYPING, DELETED_CHATROOM, LEFT_CHATROOM, LEFT_GROUP, - JOINED_CHATROOM, JOINED_GROUP, KV_EVENT, MESSAGE_RECEIVE, MESSAGE_SEND, - NAMESPACE_REGISTRATION, NEW_LAST_LOGGED_IN, + CHELONIA_RESET, CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, CONTRACT_REGISTERED, + EVENT_HANDLED, LOGIN, LOGIN_ERROR, LOGOUT, ACCEPTED_GROUP, + CHATROOM_USER_STOP_TYPING, CHATROOM_USER_TYPING, DELETED_CHATROOM, + LEFT_CHATROOM, LEFT_GROUP, JOINED_CHATROOM, JOINED_GROUP, KV_EVENT, + NOTIFICATION_TYPE.VERSION_INFO, + MESSAGE_RECEIVE, MESSAGE_SEND, NAMESPACE_REGISTRATION, NEW_LAST_LOGGED_IN, NEW_PREFERENCES, NEW_UNREAD_MESSAGES, NOTIFICATION_EMITTED, NOTIFICATION_REMOVED, NOTIFICATION_STATUS_LOADED, OFFLINE, ONLINE, PROPOSAL_ARCHIVED, SERIOUS_ERROR, SWITCH_GROUP @@ -265,6 +267,11 @@ sbp('okTurtles.events/on', CHELONIA_RESET, () => { sbp('gi.periodicNotifications/init') }) +let currentVersionInfo +sbp('okTurtles.events/on', NOTIFICATION_TYPE.VERSION_INFO, (versionInfo) => { + currentVersionInfo = versionInfo +}) + sbp('okTurtles.data/set', 'API_URL', self.location.origin) setupRootState() const setupPromise = setupChelonia() @@ -359,6 +366,23 @@ self.addEventListener('message', function (event) { }).finally(() => { port.close() }) + + // If the window is outdated (different GI_VERSION), trigger an event + // of type 'NOTIFICATION_TYPE.VERSION_INFO'. + // This handles new SW clients that have an outdated + // `process.env.GI_VERSION` (for example, by having loaded a cached + // version of `main.js`). + if ( + currentVersionInfo && + event.source && + event.data.GI_VERSION !== currentVersionInfo.GI_VERSION + ) { + event.source.postMessage({ + type: 'event', + subtype: NOTIFICATION_TYPE.VERSION_INFO, + data: [currentVersionInfo] + }) + } break } default: