Compare commits

...

1 Commits

Author SHA1 Message Date
nayan9617 cd40768f1c fix: reset relay sockets on visibility change to prevent auth hangs on sleep/wake
When browser tab is hidden (sleep), tear down all relay socket connections
completely. This forces fresh socket creation and clean auth handshake on wake,
preventing stuck AuthStatus.PendingResponse states.

- Add documentVisibility store tracking document.visibilityState
- Call Pool.get().remove(url) for relay sockets during hidden teardown
- Integrate socket reset in syncUserData, syncSpaces, syncDMs
- Add explanatory comments for visibility guards and race conditions

Fixes: App stuck in "Authenticating" after Mac sleep/wake cycle
Tested with: wss://news.utxo.one/
2026-04-07 00:15:39 +05:30
7 changed files with 181 additions and 55 deletions
+1
View File
@@ -0,0 +1 @@
[{"eventName":"NEXT_CLI_SESSION_STOPPED","payload":{"nextVersion":"16.2.2","nodeVersion":"v24.7.0","cliCommand":"dev","durationMilliseconds":6291,"turboFlag":true,"pagesDir":false,"appDir":true,"isRspack":false}}]
+1
View File
@@ -0,0 +1 @@
{"encryption.key":"mT8zxPCClGbcV2DiRln03XdmvHCQ+orzt0Q2KKaiDd4=","encryption.expire_at":1776709211601}
Binary file not shown.
View File
+3
View File
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
+18
View File
@@ -156,6 +156,24 @@ import {readFeed} from "@lib/feeds"
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
// Keep sync logic informed when the tab sleeps or wakes so subscriptions can be torn down cleanly.
export const documentVisibility = readable<DocumentVisibilityState>(
typeof document !== "undefined" ? document.visibilityState : "visible",
set => {
if (typeof document === "undefined") return
const onVisibilityChange = () => {
set(document.visibilityState)
}
document.addEventListener("visibilitychange", onVisibilityChange)
return () => {
document.removeEventListener("visibilitychange", onVisibilityChange)
}
},
)
export const ROOM = "h"
export const PROTECTED = ["-"]
+158 -55
View File
@@ -6,6 +6,7 @@ import {PollResponse} from "nostr-tools/kinds"
import {
getListTags,
getRelayTagValues,
type List,
WRAP,
ROOM_META,
ROOM_DELETE,
@@ -22,7 +23,7 @@ import {
getTagValue,
} from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util"
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
import {request, requestOne, Difference, DifferenceEvent, Pool} from "@welshman/net"
import {
pubkey,
loadRelay,
@@ -45,6 +46,7 @@ import {
MESSAGE_KINDS,
CONTENT_KINDS,
INDEXER_RELAYS,
documentVisibility,
loadSettings,
loadGroupList,
userSpaceUrls,
@@ -68,6 +70,10 @@ type SyncOpts = {
onEvent?: (event: TrustedEvent) => void
}
const resetRelaySocket = (url: string) => {
Pool.get().remove(url)
}
const pullOneWithFallback = async (
url: string,
filter: Filter,
@@ -86,6 +92,12 @@ const pullOneWithFallback = async (
const shouldFallback =
!hasNegentropy(url) ||
(await new Promise(resolve => {
if (signal.aborted) {
resolve(false)
return
}
// If visibility flips while the negentropy diff is opening, skip the fallback path and let teardown win.
const diff = new Difference({relay: url, filter, events: cachedEvents, signal})
diff.on(DifferenceEvent.Error, () => {
@@ -111,9 +123,7 @@ export const pullWithFallback = async ({url, signal, filters, onEvent}: SyncOpts
if (signal.aborted) return
for (const filter of filters) {
pullOneWithFallback(url, filter, signal, onEvent)
}
await Promise.all(filters.map(filter => pullOneWithFallback(url, filter, signal, onEvent)))
}
const listen = ({url, signal, filters, onEvent}: SyncOpts) => {
@@ -197,7 +207,7 @@ const syncUserRoomMembership = (url: string, h: string) => {
const syncUserData = () => {
const unsubscribersByKey = new Map<string, Unsubscriber>()
const unsubscribeGroupList = userGroupList.subscribe($userGroupList => {
const syncGroupList = ($userGroupList: List | undefined) => {
if ($userGroupList) {
const keys = new Set<string>()
@@ -226,37 +236,76 @@ const syncUserData = () => {
}
}
}
})
}
const unsubscribeRelayList = userRelayList.subscribe($userRelayList => {
if ($userRelayList) {
loadBlossomServerList($userRelayList.event.pubkey)
loadBlockedRelayList($userRelayList.event.pubkey)
loadFollowList($userRelayList.event.pubkey)
loadGroupList($userRelayList.event.pubkey)
loadMuteList($userRelayList.event.pubkey)
loadProfile($userRelayList.event.pubkey)
loadSettings($userRelayList.event.pubkey)
loadFeedsForPubkey($userRelayList.event.pubkey)
}
})
const syncRelayList = ($userRelayList: List | undefined) => {
const pubkey = $userRelayList?.event?.pubkey
const unsubscribeFollows = userFollowList.subscribe(async $userFollowList => {
if (!pubkey) return
loadBlossomServerList(pubkey)
loadBlockedRelayList(pubkey)
loadFollowList(pubkey)
loadGroupList(pubkey)
loadMuteList(pubkey)
loadProfile(pubkey)
loadSettings(pubkey)
loadFeedsForPubkey(pubkey)
}
const syncFollowList = () => {
for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
// This isn't urgent, avoid clogging other stuff up
await sleep(1000)
void (async () => {
// This isn't urgent, avoid clogging other stuff up
await sleep(1000)
await Promise.all(
pubkeys.flatMap(pk => [
loadRelayList(pk),
loadGroupList(pk),
loadProfile(pk),
loadFollowList(pk),
loadMuteList(pk),
]),
)
await Promise.all(
pubkeys.flatMap(pk => [
loadRelayList(pk),
loadGroupList(pk),
loadProfile(pk),
loadFollowList(pk),
loadMuteList(pk),
]),
)
})()
}
})
}
const unsubscribeGroupList = derived([userGroupList, documentVisibility], identity).subscribe(
([$userGroupList, $visibility]) => {
if ($visibility === "hidden") {
const urls = new Set(Array.from(unsubscribersByKey.keys()).map(key => key.split("'")[0]))
unsubscribersByKey.forEach(call)
unsubscribersByKey.clear()
for (const url of urls) {
resetRelaySocket(url)
}
return
}
syncGroupList($userGroupList)
},
)
const unsubscribeRelayList = derived([userRelayList, documentVisibility], identity).subscribe(
([$userRelayList, $visibility]) => {
if ($visibility === "hidden") return
syncRelayList($userRelayList)
},
)
const unsubscribeFollows = derived([userFollowList, documentVisibility], identity).subscribe(
([$userFollowList, $visibility]) => {
if ($visibility === "hidden") return
syncFollowList()
},
)
return () => {
unsubscribersByKey.forEach(call)
@@ -321,11 +370,26 @@ const syncSpace = (url: string, rooms: string[]) => {
}
const syncSpaces = () => {
const store = derived([userGroupList, page], identity)
const store = derived([userGroupList, page, documentVisibility], identity)
const unsubscribersByUrl = new Map<string, Unsubscriber>()
const roomsByUrl = new Map<string, string>()
const unsubscribe = store.subscribe(([$userGroupList, $page]) => {
const unsubscribe = store.subscribe(([$userGroupList, $page, $visibility]) => {
if ($visibility === "hidden") {
// Hidden tabs should drop every live space subscription so we restart from a clean slate on wake.
for (const url of unsubscribersByUrl.keys()) {
resetRelaySocket(url)
}
for (const unsubscribe of unsubscribersByUrl.values()) {
unsubscribe()
}
unsubscribersByUrl.clear()
roomsByUrl.clear()
return
}
const urls = new Set(getSpaceUrlsFromGroupList($userGroupList))
if ($page.params.relay) {
@@ -338,6 +402,7 @@ const syncSpaces = () => {
unsubscribersByUrl.delete(url)
roomsByUrl.delete(url)
unsubscribe()
resetRelaySocket(url)
}
}
@@ -357,8 +422,9 @@ const syncSpaces = () => {
})
return () => {
for (const unsubscriber of unsubscribersByUrl.values()) {
for (const [url, unsubscriber] of unsubscribersByUrl.entries()) {
unsubscriber()
resetRelaySocket(url)
}
unsubscribe()
@@ -383,11 +449,51 @@ const syncDMs = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>()
let currentPubkey: string | undefined
let currentShouldUnwrap = false
// Late relay-list promises can resolve after a hide/show cycle, so keep the last visible state here.
let currentVisibility: DocumentVisibilityState = "visible"
const unsubscribeAll = () => {
const unsubscribeAll = (resetSockets = false) => {
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
unsubscribersByUrl.delete(url)
unsubscribe()
if (resetSockets) {
resetRelaySocket(url)
}
}
}
const syncPubkey = ($pubkey: string | undefined, $shouldUnwrap: boolean) => {
if ($pubkey !== currentPubkey) {
unsubscribeAll()
}
if ($pubkey && $shouldUnwrap) {
loadRelayList($pubkey)
.then(() => loadMessagingRelayList($pubkey))
.then($l => {
if (
$l &&
currentVisibility === "visible" &&
currentPubkey === $pubkey &&
currentShouldUnwrap === $shouldUnwrap
) {
subscribeAll($pubkey, getRelayTagValues(getListTags($l)))
}
})
}
currentPubkey = $pubkey
currentShouldUnwrap = $shouldUnwrap
}
const syncList = ($userMessagingRelayList: List | undefined) => {
const $pubkey = pubkey.get()
const $shouldUnwrap = shouldUnwrap.get()
if ($pubkey && $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
}
}
@@ -408,33 +514,30 @@ const syncDMs = () => {
}
}
// When pubkey changes, re-sync
const unsubscribePubkey = derived([pubkey, shouldUnwrap], identity).subscribe(
([$pubkey, $shouldUnwrap]) => {
if ($pubkey !== currentPubkey) {
unsubscribeAll()
// When pubkey or visibility changes, re-sync
const unsubscribePubkey = derived([pubkey, shouldUnwrap, documentVisibility], identity).subscribe(
([$pubkey, $shouldUnwrap, $visibility]) => {
currentVisibility = $visibility
if ($visibility === "hidden") {
unsubscribeAll(true)
return
}
// If we have a pubkey, refresh our user's relay list then sync our subscriptions
if ($pubkey && $shouldUnwrap) {
loadRelayList($pubkey)
.then(() => loadMessagingRelayList($pubkey))
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
}
currentPubkey = $pubkey
syncPubkey($pubkey, $shouldUnwrap)
},
)
// When user messaging relays change, update synchronization
const unsubscribeList = userMessagingRelayList.subscribe($userMessagingRelayList => {
const $pubkey = pubkey.get()
const $shouldUnwrap = shouldUnwrap.get()
const unsubscribeList = derived([userMessagingRelayList, documentVisibility], identity).subscribe(
([$userMessagingRelayList, $visibility]) => {
currentVisibility = $visibility
if ($pubkey && $shouldUnwrap) {
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
}
})
if ($visibility === "hidden") return
syncList($userMessagingRelayList)
},
)
return () => {
unsubscribeAll()