🆙 Automatic resume / reconnect when conection was lost

This commit is contained in:
duckietm
2026-03-23 13:24:32 +01:00
parent 531ea1c33d
commit 4701c0b153
3 changed files with 8 additions and 163 deletions
+1 -18
View File
@@ -11,19 +11,13 @@ export class SocketConnection implements IConnection
private _codec: ICodec = new EvaWireFormat(); private _codec: ICodec = new EvaWireFormat();
private _dataBuffer: ArrayBuffer = null; private _dataBuffer: ArrayBuffer = null;
private _isReady: boolean = false; private _isReady: boolean = false;
private _pendingClientMessages: IMessageComposer<unknown[]>[] = []; private _pendingClientMessages: IMessageComposer<unknown[]>[] = [];
private _pendingServerMessages: IMessageDataWrapper[] = []; private _pendingServerMessages: IMessageDataWrapper[] = [];
private _isAuthenticated: boolean = false; private _isAuthenticated: boolean = false;
// Store callbacks for cleanup
private _onOpenCallback: (event: Event) => void = null; private _onOpenCallback: (event: Event) => void = null;
private _onCloseCallback: (event: Event) => void = null; private _onCloseCallback: (event: Event) => void = null;
private _onErrorCallback: (event: Event) => void = null; private _onErrorCallback: (event: Event) => void = null;
private _onMessageCallback: (event: MessageEvent) => void = null; private _onMessageCallback: (event: MessageEvent) => void = null;
// Reconnection state
private _socketUrl: string = null; private _socketUrl: string = null;
private _reconnectAttempt: number = 0; private _reconnectAttempt: number = 0;
private _reconnectTimer: ReturnType<typeof setTimeout> = null; private _reconnectTimer: ReturnType<typeof setTimeout> = null;
@@ -51,8 +45,6 @@ export class SocketConnection implements IConnection
this._socket = new WebSocket(socketUrl); this._socket = new WebSocket(socketUrl);
this._socket.binaryType = 'arraybuffer'; this._socket.binaryType = 'arraybuffer';
// Store callbacks for cleanup
this._onOpenCallback = () => this.onSocketOpened(); this._onOpenCallback = () => this.onSocketOpened();
this._onCloseCallback = (event: Event) => this.onSocketClosed(event as CloseEvent); this._onCloseCallback = (event: Event) => this.onSocketClosed(event as CloseEvent);
this._onErrorCallback = () => this.onSocketError(); this._onErrorCallback = () => this.onSocketError();
@@ -72,8 +64,6 @@ export class SocketConnection implements IConnection
{ {
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[SocketConnection] Reconnected successfully after ' + this._reconnectAttempt + ' attempt(s)');
this._reconnectAttempt = 0; this._reconnectAttempt = 0;
this._isReconnecting = false; this._isReconnecting = false;
@@ -99,8 +89,6 @@ export class SocketConnection implements IConnection
if(code === 1000 || code === 1001) if(code === 1000 || code === 1001)
{ {
NitroLogger.log('[SocketConnection] Server closed cleanly (code ' + code + ') - not reconnecting');
this._isAuthenticated = false; this._isAuthenticated = false;
this._isReady = false; this._isReady = false;
@@ -122,7 +110,6 @@ export class SocketConnection implements IConnection
{ {
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[SocketConnection] Reconnect attempt ' + this._reconnectAttempt + ' failed');
return; return;
} }
@@ -136,8 +123,6 @@ export class SocketConnection implements IConnection
{ {
if(this._reconnectAttempt >= SocketConnection.MAX_RECONNECT_ATTEMPTS) if(this._reconnectAttempt >= SocketConnection.MAX_RECONNECT_ATTEMPTS)
{ {
NitroLogger.log('[SocketConnection] Max reconnect attempts reached (' + SocketConnection.MAX_RECONNECT_ATTEMPTS + ')');
this._isReconnecting = false; this._isReconnecting = false;
this._wasAuthenticated = false; this._wasAuthenticated = false;
@@ -160,8 +145,6 @@ export class SocketConnection implements IConnection
SocketConnection.MAX_RECONNECT_DELAY_MS SocketConnection.MAX_RECONNECT_DELAY_MS
); );
NitroLogger.log('[SocketConnection] Reconnecting in ' + Math.round(delay) + 'ms (attempt ' + this._reconnectAttempt + '/' + SocketConnection.MAX_RECONNECT_ATTEMPTS + ')');
GetEventDispatcher().dispatchEvent(new ReconnectEvent( GetEventDispatcher().dispatchEvent(new ReconnectEvent(
NitroEventType.SOCKET_RECONNECTING, NitroEventType.SOCKET_RECONNECTING,
this._reconnectAttempt, this._reconnectAttempt,
@@ -189,7 +172,7 @@ export class SocketConnection implements IConnection
if(this._socket.readyState === WebSocket.OPEN || this._socket.readyState === WebSocket.CONNECTING) if(this._socket.readyState === WebSocket.OPEN || this._socket.readyState === WebSocket.CONNECTING)
{ {
try { this._socket.close(); } catch(e) { /* ignore */ } try { this._socket.close(); } catch(e) {}
} }
this._socket = null; this._socket = null;
-2
View File
@@ -52,11 +52,9 @@ export class EventDispatcher implements IEventDispatcher
{ {
if(!event) return false; if(!event) return false;
// Debug: log SOCKET_ events to trace reconnection flow
if(event.type && event.type.startsWith('SOCKET_')) if(event.type && event.type.startsWith('SOCKET_'))
{ {
const listenerCount = this._listeners.get(event.type)?.length ?? 0; const listenerCount = this._listeners.get(event.type)?.length ?? 0;
console.log('[EventDispatcher] Dispatching ' + event.type + ' (listeners: ' + listenerCount + ')');
} }
NitroLogger.events('Dispatched Event', event.type); NitroLogger.events('Dispatched Event', event.type);
+7 -143
View File
@@ -19,7 +19,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
private _sessionStarting: boolean = false; private _sessionStarting: boolean = false;
private _viewerSession: IRoomSession = null; private _viewerSession: IRoomSession = null;
// Reconnection state tracking
private _lastRoomId: number = -1; private _lastRoomId: number = -1;
private _lastRoomPassword: string = null; private _lastRoomPassword: string = null;
private _isReconnecting: boolean = false; private _isReconnecting: boolean = false;
@@ -34,9 +33,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this.createHandlers(); this.createHandlers();
this.processPendingSession(); this.processPendingSession();
this.setupReconnectListener(); this.setupReconnectListener();
// Check if there's a persisted room from a network disconnect (Vite page reload).
// sessionStorage survives same-tab reloads but is cleared on browser close.
this.checkPersistedRoom(); this.checkPersistedRoom();
} }
@@ -52,19 +48,12 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(isNaN(roomId) || roomId <= 0) return; if(isNaN(roomId) || roomId <= 0) return;
NitroLogger.log('[RoomSessionManager] Found persisted room ' + roomId + ' - setting guard for page-reload restore');
// Pre-load memory state so attemptRoomReEntry and tryRestoreSession can use it
this._lastRoomId = roomId; this._lastRoomId = roomId;
this._lastRoomPassword = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null; this._lastRoomPassword = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
// Enable guard to block DesktopViewEvent until we enter the stored room
this._isReconnecting = true; this._isReconnecting = true;
} }
catch(e) catch(e){}
{
// sessionStorage not available
}
} }
private createHandlers(): void private createHandlers(): void
@@ -90,56 +79,31 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
private setupReconnectListener(): void private setupReconnectListener(): void
{ {
console.log('[RoomSessionManager] setupReconnectListener() - registering event listeners');
// Mark reconnecting state early so DesktopViewEvent / home room redirects
// don't clear the tracked room before we can re-enter it
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTING, () => GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTING, () =>
{ {
// Cancel any pending room ID clear from removeSession().
// The server sends DesktopViewEvent before closing the socket, which
// schedules a delayed clear. We need to preserve the room ID for re-entry.
this.cancelRoomIdClear(); this.cancelRoomIdClear();
// Re-persist room to sessionStorage (it was cleared in removeSession)
if(this._lastRoomId > 0) if(this._lastRoomId > 0)
{ {
this.persistRoom(this._lastRoomId, this._lastRoomPassword); this.persistRoom(this._lastRoomId, this._lastRoomPassword);
} }
console.log('[RoomSessionManager] SOCKET_RECONNECTING fired! lastRoomId=' + this._lastRoomId);
this._isReconnecting = true; this._isReconnecting = true;
}); });
// SOCKET_RECONNECTED: the WebSocket is open but NOT yet authenticated.
// We set up a fallback timer here in case the server doesn't send
// AuthenticatedEvent (e.g. SSO ticket consumed).
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTED, () => GetEventDispatcher().addEventListener(NitroEventType.SOCKET_RECONNECTED, () =>
{ {
console.log('[RoomSessionManager] SOCKET_RECONNECTED fired! lastRoomId=' + this._lastRoomId);
// Fallback: if REAUTHENTICATED doesn't fire within 5 seconds,
// try to re-enter the room anyway (the connection might still work)
this.clearGuardTimer(); this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() => this._reconnectGuardTimer = setTimeout(() =>
{ {
this._reconnectGuardTimer = null; this._reconnectGuardTimer = null;
if(!this._isReconnecting) return; if(!this._isReconnecting) return;
NitroLogger.log('[RoomSessionManager] REAUTHENTICATED timeout - attempting fallback room re-entry for room ' + this._lastRoomId);
this.attemptRoomReEntry(); this.attemptRoomReEntry();
}, 5000); }, 5000);
}); });
// REAUTHENTICATED: SSO handshake completed, connection is ready to send messages
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_REAUTHENTICATED, () => GetEventDispatcher().addEventListener(NitroEventType.SOCKET_REAUTHENTICATED, () =>
{ {
console.log('[RoomSessionManager] SOCKET_REAUTHENTICATED fired! lastRoomId=' + this._lastRoomId);
// Snapshot the saved position BEFORE re-entering (re-entry overwrites sessionStorage)
this.snapshotSavedPosition(); this.snapshotSavedPosition();
this.clearGuardTimer(); this.clearGuardTimer();
this.attemptRoomReEntry(); this.attemptRoomReEntry();
}); });
@@ -155,11 +119,8 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this.clearPersistedPosition(); this.clearPersistedPosition();
}); });
// When the socket is permanently closed (server shutdown, max retries),
// clear the persisted room so the next page load uses normal navigation
GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, () => GetEventDispatcher().addEventListener(NitroEventType.SOCKET_CLOSED, () =>
{ {
NitroLogger.log('[RoomSessionManager] SOCKET_CLOSED - clearing persisted room');
this.clearGuardTimer(); this.clearGuardTimer();
this._isReconnecting = false; this._isReconnecting = false;
this._lastRoomId = -1; this._lastRoomId = -1;
@@ -209,30 +170,17 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(roomId <= 0) if(roomId <= 0)
{ {
NitroLogger.log('[RoomSessionManager] No room to re-enter (lastRoomId=' + roomId + '), dropping guard');
this._isReconnecting = false; this._isReconnecting = false;
return; return;
} }
// Check if we already have a session for this room (seamless reconnection).
// The server-side SessionResumeManager kept the habbo alive in the room
// during the grace period. The client's room view is still rendered behind
// the reconnection overlay. Instead of tearing it down and rebuilding,
// just drop the guard so the room view "unfreezes" in place.
const existingSession = this.getSession(roomId); const existingSession = this.getSession(roomId);
if(existingSession) if(existingSession)
{ {
NitroLogger.log('[RoomSessionManager] Existing session found for room ' + roomId + ' — sending room enter request');
// Re-send room enter request to the server with saved spawn coordinates.
// The server will place the habbo directly at the saved position
// instead of the door tile, providing a seamless reconnection experience.
GetCommunication().connection.send(new RoomEnterComposer(roomId, password, this._savedPosX, this._savedPosY)); GetCommunication().connection.send(new RoomEnterComposer(roomId, password, this._savedPosX, this._savedPosY));
// Keep the guard up briefly to absorb any stray server-side redirects
// (DesktopViewEvent, etc.) from the login packet sequence, then drop it.
this.clearGuardTimer(); this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() => this._reconnectGuardTimer = setTimeout(() =>
{ {
@@ -240,7 +188,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[RoomSessionManager] Session resume guard timeout - dropping guard');
this._isReconnecting = false; this._isReconnecting = false;
} }
}, 5000); }, 5000);
@@ -248,21 +195,9 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
return; return;
} }
NitroLogger.log('[RoomSessionManager] Re-entering room ' + roomId);
// No existing session — full room entry (e.g. page reload restore)
this._sessions.clear(); this._sessions.clear();
this._viewerSession = null; this._viewerSession = null;
// Send the room enter request with saved spawn coordinates. The server
// will place the habbo at the saved position instead of the door tile.
this.createSession(roomId, password, this._savedPosX, this._savedPosY); this.createSession(roomId, password, this._savedPosX, this._savedPosY);
// Keep the guard up for a generous window to absorb any DesktopViewEvent
// or other server-side redirects that arrive after authentication.
// The guard drops when:
// 1. RS_CONNECTED/RS_READY fires (positive room entry confirmation), OR
// 2. This safety timeout expires (10 seconds)
this.clearGuardTimer(); this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() => this._reconnectGuardTimer = setTimeout(() =>
{ {
@@ -270,19 +205,11 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[RoomSessionManager] Guard timeout (10s) - dropping guard');
this._isReconnecting = false; this._isReconnecting = false;
} }
}, 10000); }, 10000);
} }
/**
* Called on page load (from MainView). Checks sessionStorage for a
* persisted room ID from a network disconnect and enters it instead
* of following the normal home room / hotel view flow.
*
* Returns true if a room restore was initiated.
*/
public tryRestoreSession(): boolean public tryRestoreSession(): boolean
{ {
try try
@@ -297,7 +224,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
const password = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null; const password = sessionStorage.getItem(STORAGE_KEY_ROOM_PASSWORD) || null;
// Read saved position for page-reload restore
let spawnX = -1; let spawnX = -1;
let spawnY = -1; let spawnY = -1;
@@ -314,17 +240,10 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(isNaN(spawnX) || isNaN(spawnY)) { spawnX = -1; spawnY = -1; } if(isNaN(spawnX) || isNaN(spawnY)) { spawnX = -1; spawnY = -1; }
} }
} }
catch(e) { /* ignore */ } catch(e) {}
NitroLogger.log('[RoomSessionManager] Restoring session for room ' + roomId + ' from sessionStorage (spawn: ' + spawnX + ', ' + spawnY + ')');
// Set the guard so DesktopViewEvent from the server's login sequence
// doesn't kick us to hotel view before we enter the room
this._isReconnecting = true; this._isReconnecting = true;
this.createSession(roomId, password, spawnX, spawnY); this.createSession(roomId, password, spawnX, spawnY);
// Drop the guard when room entry succeeds or after timeout
this.clearGuardTimer(); this.clearGuardTimer();
this._reconnectGuardTimer = setTimeout(() => this._reconnectGuardTimer = setTimeout(() =>
{ {
@@ -332,7 +251,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[RoomSessionManager] Restore guard timeout (10s) - dropping guard');
this._isReconnecting = false; this._isReconnecting = false;
} }
}, 10000); }, 10000);
@@ -367,10 +285,7 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this.clearPersistedRoom(); this.clearPersistedRoom();
} }
} }
catch(e) catch(e) {}
{
// sessionStorage not available (private browsing, etc.) - fail silently
}
} }
private clearPersistedRoom(): void private clearPersistedRoom(): void
@@ -379,14 +294,8 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
{ {
sessionStorage.removeItem(STORAGE_KEY_ROOM_ID); sessionStorage.removeItem(STORAGE_KEY_ROOM_ID);
sessionStorage.removeItem(STORAGE_KEY_ROOM_PASSWORD); sessionStorage.removeItem(STORAGE_KEY_ROOM_PASSWORD);
// Note: position keys (POS_X, POS_Y) are NOT cleared here.
// They persist across the disconnect→reconnect cycle and are
// sent to the server as spawn coordinates during re-entry.
}
catch(e)
{
// ignore
} }
catch(e) {}
} }
private clearPersistedPosition(): void private clearPersistedPosition(): void
@@ -396,10 +305,7 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
sessionStorage.removeItem(STORAGE_KEY_POS_X); sessionStorage.removeItem(STORAGE_KEY_POS_X);
sessionStorage.removeItem(STORAGE_KEY_POS_Y); sessionStorage.removeItem(STORAGE_KEY_POS_Y);
} }
catch(e) catch(e) {}
{
// ignore
}
} }
private snapshotSavedPosition(): void private snapshotSavedPosition(): void
@@ -414,7 +320,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this._savedPosX = parseInt(posX, 10); this._savedPosX = parseInt(posX, 10);
this._savedPosY = parseInt(posY, 10); this._savedPosY = parseInt(posY, 10);
NitroLogger.log('[RoomSessionManager] Snapshot saved position (' + this._savedPosX + ', ' + this._savedPosY + ')');
} }
catch(e) catch(e)
{ {
@@ -476,8 +381,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.CREATED, roomSession)); GetEventDispatcher().dispatchEvent(new RoomSessionEvent(RoomSessionEvent.CREATED, roomSession));
this._viewerSession = roomSession; this._viewerSession = roomSession;
// Track room for reconnection (memory + sessionStorage)
this._lastRoomId = roomSession.roomId; this._lastRoomId = roomSession.roomId;
this._lastRoomPassword = roomSession.password; this._lastRoomPassword = roomSession.password;
this.persistRoom(roomSession.roomId, roomSession.password); this.persistRoom(roomSession.roomId, roomSession.password);
@@ -513,13 +416,8 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(!session) return; if(!session) return;
// During reconnection, block BOTH the session map deletion AND the ENDED event.
// This preserves the session so attemptRoomReEntry can find it, and prevents
// the UI from flashing hotel view during the reconnection flow.
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[RoomSessionManager] removeSession fully blocked by reconnect guard (room=' + id + ', openLandingView=' + openLandingView + ')');
return; return;
} }
@@ -527,11 +425,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(openLandingView) if(openLandingView)
{ {
// Don't clear _lastRoomId immediately. During server shutdown the server
// sends DesktopViewEvent (which triggers removeSession) BEFORE closing the
// socket. If we clear the room ID now, the SOCKET_RECONNECTING handler
// won't know which room to re-enter. Instead, delay the clear so that
// SOCKET_RECONNECTING can cancel it and preserve the room info.
this.clearPersistedRoom(); this.clearPersistedRoom();
this.scheduleRoomIdClear(); this.scheduleRoomIdClear();
} }
@@ -545,43 +438,19 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(!session) if(!session)
{ {
NitroLogger.log('[RoomSessionManager] sessionUpdate(' + type + ') - no session found for id ' + id);
return; return;
} }
switch(type) switch(type)
{ {
case RoomSessionHandler.RS_CONNECTED: case RoomSessionHandler.RS_CONNECTED:
NitroLogger.log('[RoomSessionManager] RS_CONNECTED for room ' + id);
// Positive signal: we successfully entered the room.
// Do NOT drop the guard yet — the server's login sequence may still
// send a DesktopViewEvent that arrives after this. Keep the guard up
// and let RS_READY (or the existing timeout) handle the final drop.
if(this._isReconnecting)
{
NitroLogger.log('[RoomSessionManager] Room entry confirmed - guard stays up until RS_READY');
}
return; return;
case RoomSessionHandler.RS_READY: case RoomSessionHandler.RS_READY:
NitroLogger.log('[RoomSessionManager] RS_READY for room ' + id);
// Room is fully loaded. Keep the guard up for a short grace period
// to absorb any late DesktopViewEvent from the server's login sequence,
// then drop it. This prevents a race where the login sequence's
// DesktopViewEvent arrives after the room entry confirmation.
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[RoomSessionManager] Room ready confirmed - dropping guard in 3s'); if(this._savedPosX >= 0 && this._savedPosY >= 0)
// If we have saved spawn coordinates, send a walk command so the
// avatar moves to their previous position. This handles the EMU-restart
// case where the server has no ghost session and spawns at the door.
if(this._savedPosX >= 0 && this._savedPosY >= 0)
{ {
NitroLogger.log('[RoomSessionManager] Walking to saved position (' + this._savedPosX + ', ' + this._savedPosY + ')');
GetCommunication().connection.send(new RoomUnitWalkComposer(this._savedPosX, this._savedPosY)); GetCommunication().connection.send(new RoomUnitWalkComposer(this._savedPosX, this._savedPosY));
this._savedPosX = -1; this._savedPosX = -1;
this._savedPosY = -1; this._savedPosY = -1;
@@ -594,18 +463,14 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
if(this._isReconnecting) if(this._isReconnecting)
{ {
NitroLogger.log('[RoomSessionManager] Post-ready grace period elapsed - dropping guard'); this._isReconnecting = false;
this._isReconnecting = false;
} }
}, 3000); }, 3000);
} }
return; return;
case RoomSessionHandler.RS_DISCONNECTED: case RoomSessionHandler.RS_DISCONNECTED:
NitroLogger.log('[RoomSessionManager] RS_DISCONNECTED for room ' + id + ' (isReconnecting=' + this._isReconnecting + ')');
// During reconnection, don't process server-side disconnects
// (DesktopViewEvent / home room redirect) - we'll re-enter the room
if(this._isReconnecting) return; if(this._isReconnecting) return;
this.removeSession(id); this.removeSession(id);
@@ -625,7 +490,6 @@ export class RoomSessionManager implements IRoomSessionManager, IRoomHandlerList
this._sessions.set(this.getRoomId(toRoomId), existing); this._sessions.set(this.getRoomId(toRoomId), existing);
// Update tracked room
this._lastRoomId = toRoomId; this._lastRoomId = toRoomId;
this.persistRoom(toRoomId, existing.password); this.persistRoom(toRoomId, existing.password);