mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-23 07:02:44 -05:00
fix(mobile): stabilize offline sensing fallback
This commit is contained in:
1 parent
7036282ccf
commit
a11d9a4aa0
2 files changed
+90
-9
No files matched your search
@@ -111,6 +111,73 @@ describe('WsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('connection fallback', () => {
|
||||
it('uses simulation when an active server connection closes normally', () => {
|
||||
const OrigWebSocket = globalThis.WebSocket;
|
||||
let socket: any;
|
||||
|
||||
class MockWebSocket {
|
||||
static OPEN = 1;
|
||||
static CONNECTING = 0;
|
||||
readyState = 0;
|
||||
onopen: (() => void) | null = null;
|
||||
onclose: ((event: { code: number }) => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
onmessage: (() => void) | null = null;
|
||||
close() {}
|
||||
constructor() {
|
||||
socket = this;
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.WebSocket = MockWebSocket as any;
|
||||
try {
|
||||
const ws = createWsService();
|
||||
ws.connect('http://localhost:3000');
|
||||
socket.onclose?.({ code: 1000 });
|
||||
|
||||
expect(ws.getStatus()).toBe('simulated');
|
||||
ws.disconnect();
|
||||
} finally {
|
||||
globalThis.WebSocket = OrigWebSocket;
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves retry progress instead of restarting the first attempt', () => {
|
||||
const OrigWebSocket = globalThis.WebSocket;
|
||||
const sockets: any[] = [];
|
||||
|
||||
class MockWebSocket {
|
||||
static OPEN = 1;
|
||||
static CONNECTING = 0;
|
||||
readyState = 0;
|
||||
onopen: (() => void) | null = null;
|
||||
onclose: ((event: { code: number }) => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
onmessage: (() => void) | null = null;
|
||||
close() {}
|
||||
constructor() {
|
||||
sockets.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.WebSocket = MockWebSocket as any;
|
||||
try {
|
||||
const ws = createWsService();
|
||||
ws.connect('http://localhost:3000');
|
||||
sockets[0].onclose?.({ code: 1006 });
|
||||
jest.runOnlyPendingTimers();
|
||||
sockets[1].onclose?.({ code: 1006 });
|
||||
|
||||
expect(ws.getStatus()).toBe('simulated');
|
||||
expect(sockets).toHaveLength(2);
|
||||
ws.disconnect();
|
||||
} finally {
|
||||
globalThis.WebSocket = OrigWebSocket;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe and unsubscribe', () => {
|
||||
it('adds a listener and returns an unsubscribe function', () => {
|
||||
const ws = createWsService();
|
||||
|
||||
@@ -17,9 +17,16 @@ class WsService {
|
||||
private status: ConnectionStatus = 'disconnected';
|
||||
|
||||
connect(url: string): void {
|
||||
const wasActive = this.active;
|
||||
const targetChanged = url !== this.targetUrl;
|
||||
this.targetUrl = url;
|
||||
this.active = true;
|
||||
this.reconnectAttempt = 0;
|
||||
|
||||
// A scheduled reconnect must preserve its attempt count. Resetting it on
|
||||
// every call traps an unavailable installation in an infinite first retry.
|
||||
if (!wasActive || targetChanged) {
|
||||
this.reconnectAttempt = 0;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
this.handleStatusChange('simulated');
|
||||
@@ -31,10 +38,18 @@ class WsService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleStatusChange('connecting');
|
||||
this.openConnection();
|
||||
}
|
||||
|
||||
private openConnection(): void {
|
||||
// When simulation is already providing frames, keep that honest status in
|
||||
// the UI while the real endpoint is probed in the background.
|
||||
if (!this.simulationTimer) {
|
||||
this.handleStatusChange('connecting');
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoint = this.buildWsUrl(url);
|
||||
const endpoint = this.buildWsUrl(this.targetUrl);
|
||||
const socket = new WebSocket(endpoint);
|
||||
this.ws = socket;
|
||||
|
||||
@@ -64,10 +79,8 @@ class WsService {
|
||||
this.handleStatusChange('disconnected');
|
||||
return;
|
||||
}
|
||||
if (evt.code === 1000) {
|
||||
this.handleStatusChange('disconnected');
|
||||
return;
|
||||
}
|
||||
// A server is allowed to close with 1000. It is only an intentional
|
||||
// client disconnect when `active` is false (handled above).
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
} catch {
|
||||
@@ -125,12 +138,13 @@ class WsService {
|
||||
|
||||
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
|
||||
this.reconnectAttempt += 1;
|
||||
this.startSimulation();
|
||||
this.handleStatusChange('simulated');
|
||||
this.clearReconnectTimer();
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.connect(this.targetUrl);
|
||||
this.openConnection();
|
||||
}, delay);
|
||||
this.startSimulation();
|
||||
}
|
||||
|
||||
private startSimulation(): void {
|
||||
|
||||
Reference in new issue
Block a user