scripts/loremaster.mjs:107-114:
authManager.onStateChange(async (state, user) => {
if (state === AuthState.LOGGED_IN && !game.loremaster?.socketClient?.isConnected) {
console.log(`${MODULE_NAME} | Auth successful, initializing connection...`);
ui.notifications.info(`${MODULE_NAME}: Connected as ${user?.displayName}. Initializing...`);
await initializeLoremaster();
}
});
The guard is !isConnected, but a SocketClient that is mid-handshake has isConnected === false. If initializeLoremaster() is already running (started by the ready hook for an already-authenticated user) and the auth state then transitions to LOGGED_IN for any reason — token re-validation, refresh, manual login flow — this listener fires a second initializeLoremaster() while the first is still inside socketClient.connect().
Result: two parallel connect attempts, two pending sockets on game.loremaster, race on which one wins.
initializeLoremaster does try to clean up at the top:
// scripts/loremaster.mjs:189-192
if (game.loremaster?.socketClient) {
game.loremaster.socketClient.disconnect();
}
…but the cleanup happens against whichever client is currently on game.loremaster, which may be the one the first invocation is mid-handshake on. Disconnecting it mid-handshake can leave the first invocation's await socketClient.connect() in an undefined state.
Suggested fix: track init state explicitly (isInitializing flag) and skip the listener path while it's true; or serialize all initializeLoremaster calls through a single Promise chain.
scripts/loremaster.mjs:107-114:The guard is
!isConnected, but aSocketClientthat is mid-handshake hasisConnected === false. IfinitializeLoremaster()is already running (started by thereadyhook for an already-authenticated user) and the auth state then transitions toLOGGED_INfor any reason — token re-validation, refresh, manual login flow — this listener fires a secondinitializeLoremaster()while the first is still insidesocketClient.connect().Result: two parallel connect attempts, two pending sockets on
game.loremaster, race on which one wins.initializeLoremasterdoes try to clean up at the top:…but the cleanup happens against whichever client is currently on
game.loremaster, which may be the one the first invocation is mid-handshake on. Disconnecting it mid-handshake can leave the first invocation'sawait socketClient.connect()in an undefined state.Suggested fix: track init state explicitly (
isInitializingflag) and skip the listener path while it's true; or serialize allinitializeLoremastercalls through a single Promise chain.