|
| 1 | +/* |
| 2 | + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
| 3 | + * SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | + */ |
| 5 | + |
| 6 | +import { createSharedComposable } from '@vueuse/core' |
| 7 | +import { onBeforeUnmount, watch } from 'vue' |
| 8 | +import { useIsAway } from './composables/useIsAway.ts' |
| 9 | +import { useUserStatusStore } from './userStatus.store.ts' |
| 10 | + |
| 11 | +// General notes: |
| 12 | +// - Server has INVALIDATE_STATUS_THRESHOLD with 15 minutes, preventing immediate status update on heartbeat request |
| 13 | +// See: https://github.com/nextcloud/server/blob/v31.0.5/apps/user_status/lib/Service/StatusService.php |
| 14 | +// - However, "online" status has higher priority than "away" |
| 15 | +// - Thus: |
| 16 | +// - Changing "Away -> Online" is immediate |
| 17 | +// - Changing "Online -> Away" has a 15 minutes threshold |
| 18 | +// - See: https://github.com/nextcloud/server/blob/v31.0.5/apps/user_status/lib/Service/StatusService.php#L41-L48 |
| 19 | +// and: https://github.com/nextcloud/server/blob/master/apps/user_status/lib/Listener/UserLiveStatusListener.php#L75-L87 |
| 20 | +// - This might change in future to have symmetric behavior on heartbeat |
| 21 | + |
| 22 | +/** How often to send the heartbeat. Must be less than 15 min. */ |
| 23 | +const HEARTBEAT_INTERVAL = 5 * 60 * 1000 // 5 minutes |
| 24 | + |
| 25 | +/** How long user is considered active before going away */ |
| 26 | +const AWAY_THRESHOLD = 2 * 60 * 1000 // 2 minutes |
| 27 | + |
| 28 | +/** |
| 29 | + * Background heartbeat with user status update |
| 30 | + */ |
| 31 | +export const useHeartbeat = createSharedComposable(() => { |
| 32 | + const userStatusStore = useUserStatusStore() |
| 33 | + const isAway = useIsAway(AWAY_THRESHOLD) |
| 34 | + |
| 35 | + let heartbeatTimeout: number | undefined |
| 36 | + |
| 37 | + /** |
| 38 | + * Send a heartbeat |
| 39 | + */ |
| 40 | + async function heartbeat() { |
| 41 | + try { |
| 42 | + await userStatusStore.updateUserStatusWithHeartbeat(isAway.value) |
| 43 | + } catch (error) { |
| 44 | + console.error('Error on heartbeat:', error) |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + /** |
| 49 | + * Start heartbeat interval |
| 50 | + */ |
| 51 | + async function restartHeartbeat() { |
| 52 | + if (heartbeatTimeout) { |
| 53 | + clearTimeout(heartbeatTimeout) |
| 54 | + } |
| 55 | + await heartbeat() |
| 56 | + // TODO: fix when main and renderer process have separate tsconfig |
| 57 | + heartbeatTimeout = setTimeout(heartbeat, HEARTBEAT_INTERVAL) as unknown as number |
| 58 | + } |
| 59 | + |
| 60 | + // Restart heartbeat to immediately notify server on state change |
| 61 | + watch(isAway, () => { |
| 62 | + // Note: both app and system level activity state changes to inactive with a threshold. |
| 63 | + // Only lock/unlock state can be changed many times in a short period, but it is unlikely |
| 64 | + // Thus it unlikely overloads with heartbeat, no need to debounce |
| 65 | + restartHeartbeat() |
| 66 | + }, { immediate: true }) |
| 67 | + |
| 68 | + onBeforeUnmount(() => { |
| 69 | + clearTimeout(heartbeatTimeout) |
| 70 | + }) |
| 71 | +}) |
0 commit comments