refactor: share realtime socket auth helpers

This commit is contained in:
Saravana Achu Mac 2026-04-04 12:11:43 -07:00
parent e1bb6e790e
commit 8f7d5358aa
4 changed files with 19 additions and 12 deletions

View File

@ -30,7 +30,7 @@ It assumes:
- [x] Backend migrated into `backend/` and passing typecheck, build, test, and backend verification gates - [x] Backend migrated into `backend/` and passing typecheck, build, test, and backend verification gates
- [x] Web migrated into `web/` with shared runtime, shared kill-switch gate, shared telemetry bootstrap, and normalized backend URL resolution - [x] Web migrated into `web/` with shared runtime, shared kill-switch gate, shared telemetry bootstrap, and normalized backend URL resolution
- [x] Mobile migrated into `mobile/` with product identity, shared runtime bootstrap, launch-time kill-switch gate, transitional Supabase auth, live backend polling plus websocket-backed updates, startup/error telemetry capture, secure session storage with invalidation handling, and explicit degraded/offline status surfacing - [x] Mobile migrated into `mobile/` with product identity, shared runtime bootstrap, launch-time kill-switch gate, transitional Supabase auth, live backend polling plus websocket-backed updates, startup/error telemetry capture, secure session storage with invalidation handling, and explicit degraded/offline status surfacing
- [-] DRY cleanup completed for runtime/config/bootstrap concerns and shared Supabase bootstrap, but not yet for all auth/session internals - [-] DRY cleanup completed for runtime/config/bootstrap concerns, shared Supabase bootstrap, and shared websocket auth helpers, but not yet for all auth/session internals
- [!] Full common-platform auth replacement remains a follow-up for web and mobile; current implementation uses transitional Supabase-backed auth to stay compatible with the backend's current JWT boundary - [!] Full common-platform auth replacement remains a follow-up for web and mobile; current implementation uses transitional Supabase-backed auth to stay compatible with the backend's current JWT boundary
## 3. Guiding Rules ## 3. Guiding Rules
@ -261,7 +261,7 @@ Move the web dashboard onto the new repo and onto shared platform bootstrap patt
- [x] Move runtime config to common conventions - [x] Move runtime config to common conventions
- [x] Define product config - [x] Define product config
- [x] Define API client and websocket client - [x] Define API client and websocket client
- [ ] Standardize websocket token propagation - [x] Standardize websocket token propagation
- [x] Integrate maintenance and kill-switch UX states - [x] Integrate maintenance and kill-switch UX states
- [x] Define shell-level maintenance and kill-switch behavior - [x] Define shell-level maintenance and kill-switch behavior
- [ ] Classify each current web tab as ship, defer, or redesign - [ ] Classify each current web tab as ship, defer, or redesign

View File

@ -4,6 +4,7 @@ import { io, type Socket } from 'socket.io-client';
import { mobileRuntime } from '@/lib/runtime'; import { mobileRuntime } from '@/lib/runtime';
import { mobileTelemetry, trackMobileError } from '@/lib/telemetry'; import { mobileTelemetry, trackMobileError } from '@/lib/telemetry';
import { useMobileAuth } from '@/providers/MobileAuthProvider'; import { useMobileAuth } from '@/providers/MobileAuthProvider';
import { buildTradingSocketOptions, isUnauthorizedSocketError } from '../../shared/realtime.js';
type HealthSnapshot = { type HealthSnapshot = {
tradingControl?: { tradingControl?: {
@ -223,10 +224,7 @@ export function TradingDataProvider({ children }: { children: ReactNode }) {
})); }));
}; };
socket = io(tradingSocketUrl, { socket = io(tradingSocketUrl, buildTradingSocketOptions(accessToken));
transports: ['polling', 'websocket'],
auth: { token: accessToken },
});
socket.on('connect', () => { socket.on('connect', () => {
setConnected(true); setConnected(true);
@ -242,7 +240,7 @@ export function TradingDataProvider({ children }: { children: ReactNode }) {
socket.on('connect_error', (socketError) => { socket.on('connect_error', (socketError) => {
setError(socketError.message); setError(socketError.message);
trackMobileError('realtime', 'socket_connect_failed', socketError); trackMobileError('realtime', 'socket_connect_failed', socketError);
if (socketError.message.toLowerCase().includes('unauthorized')) { if (isUnauthorizedSocketError(socketError.message)) {
void invalidateSession(socketError.message); void invalidateSession(socketError.message);
} }
}); });

12
shared/realtime.ts Normal file
View File

@ -0,0 +1,12 @@
export function buildTradingSocketOptions(token: string, socketPath?: string) {
return {
transports: ['polling', 'websocket'] as ('polling' | 'websocket')[],
auth: { token },
...(socketPath ? { path: socketPath } : {}),
};
}
export function isUnauthorizedSocketError(message: string) {
const normalizedMessage = message.toLowerCase();
return normalizedMessage.includes('unauthorized') || normalizedMessage.includes('invalid token');
}

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
import { buildTradingSocketOptions } from '../../../shared/realtime.js';
import { supabase } from '../lib/supabaseClient'; import { supabase } from '../lib/supabaseClient';
export interface TradingControlSnapshot { export interface TradingControlSnapshot {
@ -284,11 +285,7 @@ export const useWebSocket = (url: string) => {
return; return;
} }
const socketOptions = { const socketOptions = buildTradingSocketOptions(token, import.meta.env.VITE_SOCKET_PATH);
transports: ['polling', 'websocket'] as ('polling' | 'websocket')[],
auth: { token },
...(import.meta.env.VITE_SOCKET_PATH ? { path: import.meta.env.VITE_SOCKET_PATH } : {})
};
newSocket = io(url, socketOptions); newSocket = io(url, socketOptions);