From e90c8c122fbd0f7cf6608db63da2dcd9ec27e0d0 Mon Sep 17 00:00:00 2001 From: David Dreschner Date: Fri, 16 Oct 2026 20:09:49 +0200 Subject: [PATCH] fix(auto-logout): Send heartbeats while editing instead of while idle With `session_keepalive` disabled, the heartbeat was only sent when the stored last activity was older then half the session lifetime. While the user kept editing, the activity was refreshed every 10 seconds, so no heartbeat was sent and the session expired mid-edit. Once idle, a heartbeat was sent on every check and kept the session alive. Send at most one heartbeat per interval, and only for interactions Collabora reported since the last one. Unregister the handler when the document is closed, so closed documents stop sending heartbeats, and keep reporting the activity to core when the browser storage is not available. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: David Dreschner --- src/mixins/autoLogout.js | 41 ++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/mixins/autoLogout.js b/src/mixins/autoLogout.js index adb42fe434..e01ad4b8c1 100644 --- a/src/mixins/autoLogout.js +++ b/src/mixins/autoLogout.js @@ -24,6 +24,7 @@ export default { return { autoLogoutInterval: null, autoLogoutRegistered: false, + lastHeartbeat: Date.now(), } }, @@ -41,13 +42,22 @@ export default { if (this.autoLogoutInterval) { clearInterval(this.autoLogoutInterval) } + if (this.autoLogoutRegistered) { + this.postMessage.unregisterPostMessageHandler(this.handleGetUserStateResponse) + } }, methods: { extendAutoLogout() { - const oldValue = localStorage.getItem('lastActive') const newValue = Date.now().toString() - localStorage.setItem('lastActive', newValue) + let oldValue = null + try { + oldValue = localStorage.getItem('lastActive') + localStorage.setItem('lastActive', newValue) + } catch (error) { + // Core reads the activity from the event below, so only other tabs miss it + console.debug('[richdocuments] Could not store the last activity', error) + } const event = new StorageEvent('storage', { key: 'lastActive', oldValue, @@ -69,18 +79,25 @@ export default { return } - const elapsed = args.Elapsed - const lastActive = Number(localStorage.getItem('lastActive')) - const timeSinceLastActive = Date.now() - lastActive - const recheckAfter = (getInterval() * 1000) - - if (!config.session_keepalive && timeSinceLastActive > recheckAfter) { - axios.get(generateUrl('/apps/richdocuments/heartbeat')) + // Seconds since the last user interaction in the document + if (args.Elapsed < 30) { + console.debug('[richdocuments] Extending auto logout timeout, office idle since ' + args.Elapsed) + this.extendAutoLogout() } - if (elapsed < 30) { - console.debug('[richdocuments] Extending auto logout timeout, office idle since ' + elapsed, timeSinceLastActive, recheckAfter, timeSinceLastActive > recheckAfter) - this.extendAutoLogout() + // Collabora does not extend the user session on interaction + const lastInteraction = Date.now() - args.Elapsed * 1000 + if (!config.session_keepalive && lastInteraction > this.lastHeartbeat && Date.now() - this.lastHeartbeat > getInterval() * 1000) { + const previousHeartbeat = this.lastHeartbeat + const heartbeat = Date.now() + this.lastHeartbeat = heartbeat + axios.get(generateUrl('/apps/richdocuments/heartbeat')).catch((error) => { + console.debug('[richdocuments] Heartbeat failed, retrying with the next check', error) + // A request that hangs past the next interval must not undo the newer attempt + if (this.lastHeartbeat === heartbeat) { + this.lastHeartbeat = previousHeartbeat + } + }) } }, },