diff --git a/shared/constants/init/index.desktop.tsx b/shared/constants/init/index.desktop.tsx index 08d952d63514..cc2b8dae260c 100644 --- a/shared/constants/init/index.desktop.tsx +++ b/shared/constants/init/index.desktop.tsx @@ -252,7 +252,19 @@ const onInstallCachedDokan = async () => { } } +const _platformUnsubs: Array<() => void> = __DEV__ + ? ((globalThis as any).__hmr_platformUnsubs ??= []) + : [] + +let _oneTimeInitDone: boolean = __DEV__ + ? ((globalThis as any).__hmr_oneTimeInitDone ?? false) + : false + export const initPlatformListener = () => { + // HMR cleanup: unsubscribe old store subscriptions before re-subscribing + for (const unsub of _platformUnsubs) unsub() + _platformUnsubs.length = 0 + useConfigState.setState(s => { s.dispatch.defer.dumpLogsNative = dumpLogs s.dispatch.defer.showMainNative = wrapErrors(() => showMainWindow?.()) @@ -266,12 +278,12 @@ export const initPlatformListener = () => { }) }) - useConfigState.subscribe((s, old) => { + _platformUnsubs.push(useConfigState.subscribe((s, old) => { if (s.appFocused === old.appFocused) return useFSState.getState().dispatch.onChangedFocus(s.appFocused) - }) + })) - useConfigState.subscribe((s, old) => { + _platformUnsubs.push(useConfigState.subscribe((s, old) => { if (s.loggedIn !== old.loggedIn) { s.dispatch.osNetworkStatusChanged(navigator.onLine, 'notavailable', true) } @@ -310,32 +322,60 @@ export const initPlatformListener = () => { } ignorePromise(f()) } - }) + })) - const handleWindowFocusEvents = () => { - const handle = (appFocused: boolean) => { - if (skipAppFocusActions) { - console.log('Skipping app focus actions!') - } else { - useConfigState.getState().dispatch.changedFocus(appFocused) + // One-time setup: window event listeners and input monitor (skip on HMR to avoid duplicates) + if (!_oneTimeInitDone) { + _oneTimeInitDone = true + if (__DEV__) (globalThis as any).__hmr_oneTimeInitDone = true + + const handleWindowFocusEvents = () => { + const handle = (appFocused: boolean) => { + if (skipAppFocusActions) { + console.log('Skipping app focus actions!') + } else { + useConfigState.getState().dispatch.changedFocus(appFocused) + } } + window.addEventListener('focus', () => handle(true)) + window.addEventListener('blur', () => handle(false)) } - window.addEventListener('focus', () => handle(true)) - window.addEventListener('blur', () => handle(false)) - } - handleWindowFocusEvents() + handleWindowFocusEvents() + + const setupReachabilityWatcher = () => { + window.addEventListener('online', () => + useConfigState.getState().dispatch.osNetworkStatusChanged(true, 'notavailable') + ) + window.addEventListener('offline', () => + useConfigState.getState().dispatch.osNetworkStatusChanged(false, 'notavailable') + ) + } + setupReachabilityWatcher() - const setupReachabilityWatcher = () => { - window.addEventListener('online', () => - useConfigState.getState().dispatch.osNetworkStatusChanged(true, 'notavailable') - ) - window.addEventListener('offline', () => - useConfigState.getState().dispatch.osNetworkStatusChanged(false, 'notavailable') - ) + if (isLinux) { + useConfigState.getState().dispatch.initUseNativeFrame() + } + useConfigState.getState().dispatch.initNotifySound() + useConfigState.getState().dispatch.initForceSmallNav() + useConfigState.getState().dispatch.initOpenAtLogin() + useConfigState.getState().dispatch.initAppUpdateLoop() + + const initializeInputMonitor = () => { + const inputMonitor = new InputMonitor() + inputMonitor.notifyActive = (userActive: boolean) => { + if (skipAppFocusActions) { + console.log('Skipping app focus actions!') + } else { + useConfigState.getState().dispatch.setActive(userActive) + // let node thread save file + activeChanged?.(Date.now(), userActive) + } + } + } + initializeInputMonitor() } - setupReachabilityWatcher() - useDaemonState.subscribe((s, old) => { + _platformUnsubs.push(useDaemonState.subscribe((s, old) => { if (s.handshakeVersion !== old.handshakeVersion) { if (!isWindows) return @@ -368,15 +408,7 @@ export const initPlatformListener = () => { tab: undefined, }) } - }) - - if (isLinux) { - useConfigState.getState().dispatch.initUseNativeFrame() - } - useConfigState.getState().dispatch.initNotifySound() - useConfigState.getState().dispatch.initForceSmallNav() - useConfigState.getState().dispatch.initOpenAtLogin() - useConfigState.getState().dispatch.initAppUpdateLoop() + })) useProfileState.setState(s => { s.dispatch.editAvatar = () => { @@ -386,20 +418,6 @@ export const initPlatformListener = () => { } }) - const initializeInputMonitor = () => { - const inputMonitor = new InputMonitor() - inputMonitor.notifyActive = (userActive: boolean) => { - if (skipAppFocusActions) { - console.log('Skipping app focus actions!') - } else { - useConfigState.getState().dispatch.setActive(userActive) - // let node thread save file - activeChanged?.(Date.now(), userActive) - } - } - } - initializeInputMonitor() - useDaemonState.setState(s => { s.dispatch.onRestartHandshakeNative = () => { const {handshakeFailedReason} = useDaemonState.getState() diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 830d47dd9019..8865e381c0a4 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -53,9 +53,15 @@ import {useRouterState} from '@/stores/router2' import * as Util from '@/constants/router2' import {setConvoDefer} from '@/stores/convostate' -let _emitStartupOnLoadDaemonConnectedOnce = false -let _devicesLoaded = false -let _gitLoaded = false +let _emitStartupOnLoadDaemonConnectedOnce: boolean = __DEV__ + ? ((globalThis as any).__hmr_startupOnce ?? false) + : false +let _devicesLoaded: boolean = __DEV__ ? ((globalThis as any).__hmr_devicesLoaded ?? false) : false +let _gitLoaded: boolean = __DEV__ ? ((globalThis as any).__hmr_gitLoaded ?? false) : false + +const _sharedUnsubs: Array<() => void> = __DEV__ + ? ((globalThis as any).__hmr_sharedUnsubs ??= []) + : [] export const onEngineConnected = () => { { @@ -397,6 +403,10 @@ export const initSettingsCallbacks = () => { } export const initSharedSubscriptions = () => { + // HMR cleanup: unsubscribe old store subscriptions before re-subscribing + for (const unsub of _sharedUnsubs) unsub() + _sharedUnsubs.length = 0 + setConvoDefer({ chatBlockButtonsMapHas: teamID => storeRegistry.getState('chat').blockButtonsMap.has(teamID), @@ -423,7 +433,7 @@ export const initSharedSubscriptions = () => { usersGetBio: username => storeRegistry.getState('users').dispatch.getBio(username), }) - useConfigState.subscribe((s, old) => { + _sharedUnsubs.push(useConfigState.subscribe((s, old) => { if (s.loadOnStartPhase !== old.loadOnStartPhase) { if (s.loadOnStartPhase === 'startupOrReloginButNotInARush') { const getFollowerInfo = () => { @@ -546,9 +556,9 @@ export const initSharedSubscriptions = () => { const cs = storeRegistry.getConvoState(getSelectedConversation()) cs.dispatch.markThreadAsRead() } - }) + })) - useDaemonState.subscribe((s, old) => { + _sharedUnsubs.push(useDaemonState.subscribe((s, old) => { if (s.handshakeVersion !== old.handshakeVersion) { useDarkModeState.getState().dispatch.loadDarkPrefs() storeRegistry.getState('chat').dispatch.loadStaticConfig() @@ -587,13 +597,14 @@ export const initSharedSubscriptions = () => { if (s.handshakeState === 'done') { if (!_emitStartupOnLoadDaemonConnectedOnce) { _emitStartupOnLoadDaemonConnectedOnce = true + if (__DEV__) (globalThis as any).__hmr_startupOnce = true useConfigState.getState().dispatch.loadOnStart('connectedToDaemonForFirstTime') } } } - }) + })) - useProvisionState.subscribe((s, old) => { + _sharedUnsubs.push(useProvisionState.subscribe((s, old) => { if (s.startProvisionTrigger !== old.startProvisionTrigger) { useConfigState.getState().dispatch.setLoginError() useConfigState.getState().dispatch.resetRevokedSelf() @@ -605,9 +616,9 @@ export const initSharedSubscriptions = () => { } ignorePromise(f()) } - }) + })) - useRouterState.subscribe((s, old) => { + _sharedUnsubs.push(useRouterState.subscribe((s, old) => { const next = s.navState as Util.NavState const prev = old.navState as Util.NavState if (prev === next) return @@ -684,7 +695,7 @@ export const initSharedSubscriptions = () => { } storeRegistry.getState('chat').dispatch.onRouteChanged(prev, next) - }) + })) initAutoResetCallbacks() initChat2Callbacks() @@ -721,6 +732,7 @@ export const _onEngineIncoming = (action: EngineGen.Actions) => { const hasValue = (newDevices?.length ?? 0) + (revokedDevices?.length ?? 0) > 0 if (_devicesLoaded || hasValue) { _devicesLoaded = true + if (__DEV__) (globalThis as any).__hmr_devicesLoaded = true const {useDevicesState} = require('@/stores/devices') as typeof UseDevicesStateType useDevicesState.getState().dispatch.onEngineIncomingImpl(action) } @@ -728,6 +740,7 @@ export const _onEngineIncoming = (action: EngineGen.Actions) => { const badges = new Set(badgeState.newGitRepoGlobalUniqueIDs) if (_gitLoaded || badges.size) { _gitLoaded = true + if (__DEV__) (globalThis as any).__hmr_gitLoaded = true const {useGitState} = require('@/stores/git') as typeof UseGitStateType useGitState.getState().dispatch.onEngineIncomingImpl(action) } diff --git a/shared/desktop/renderer/main2.desktop.tsx b/shared/desktop/renderer/main2.desktop.tsx index e2d29cd73822..c42d21a86e53 100644 --- a/shared/desktop/renderer/main2.desktop.tsx +++ b/shared/desktop/renderer/main2.desktop.tsx @@ -347,8 +347,9 @@ const setupHMR = () => { const load = () => { if (global.DEBUGLoaded) { - // only load once - console.log('Bail on load() on HMR') + // HMR detected — reinit subscriptions on new store instances + console.log('HMR: reinitializing store subscriptions') + initPlatformListener() return } global.DEBUGLoaded = true diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 372a1e9cb9f1..7c114a5ff3e5 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -431,10 +431,13 @@ const stubDefer: ConvoState['dispatch']['defer'] = { }, } -let convoDeferImpl: ConvoState['dispatch']['defer'] | undefined +let convoDeferImpl: ConvoState['dispatch']['defer'] | undefined = __DEV__ + ? (globalThis as any).__hmr_convoDeferImpl + : undefined export const setConvoDefer = (impl: ConvoState['dispatch']['defer']) => { convoDeferImpl = impl + if (__DEV__) (globalThis as any).__hmr_convoDeferImpl = impl for (const store of chatStores.values()) { const s = store.getState() store.setState({ @@ -3313,7 +3316,9 @@ const createSlice = (): Z.ImmerStateCreator => (set, get) => { } type MadeStore = UseBoundStore> -export const chatStores = new Map() +export const chatStores: Map = __DEV__ + ? ((globalThis as any).__hmr_chatStores ??= new Map()) + : new Map() export const clearChatStores = () => { chatStores.clear() diff --git a/shared/stores/team-building.tsx b/shared/stores/team-building.tsx index fb68799d16c0..cab98fa5d93d 100644 --- a/shared/stores/team-building.tsx +++ b/shared/stores/team-building.tsx @@ -497,7 +497,9 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { } type MadeStore = UseBoundStore> -export const TBstores = new Map() +export const TBstores: Map = __DEV__ + ? ((globalThis as any).__hmr_TBstores ??= new Map()) + : new Map() registerDebugClear(() => { TBstores.clear()