mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
add identity to socket auth hopefully will fix a lot of probem
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// Hook: useUserIdentitySync
|
||||
// Purpose: Keeps local identity state synchronized with server session/auth updates. Scope: Handles identity hydration, change propagation, and persistence touch points.
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../context/SessionContext.jsx';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import { getBrowserFingerprintId } from '../lib/browserFingerprint.js';
|
||||
@@ -9,6 +9,7 @@ import { useSettingsNamespace } from '../settings/index.js';
|
||||
export default function useUserIdentitySync({ identitySurface = 'passive' } = {}) {
|
||||
const socket = useSocket();
|
||||
const connected = useSessionSelector((state) => state.connected);
|
||||
const serverCookieUserId = useSessionSelector((state) => state.session?.identity?.cookieUserId || '');
|
||||
const { identifySession } = useSessionActions();
|
||||
const { value: identity, status: identityStatus, save: saveIdentity } = useSettingsNamespace('identity', {
|
||||
cookieUserId: '',
|
||||
@@ -19,9 +20,6 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
{ enabled: false },
|
||||
);
|
||||
|
||||
const lastAckSocketRef = useRef(null);
|
||||
const fingerprintRef = useRef('');
|
||||
|
||||
const ready =
|
||||
identityStatus === 'ready' && profileStatus === 'ready' && overseerPreferenceStatus === 'ready';
|
||||
const cookieUserId = (identity?.cookieUserId || '').trim();
|
||||
@@ -32,23 +30,15 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
const sendIdentify = useCallback(async () => {
|
||||
if (!ready || !connected || !socket?.id) return;
|
||||
try {
|
||||
if (!fingerprintRef.current) {
|
||||
/*
|
||||
The portable cookie key is still the cross-device identity signal.
|
||||
Thumbmark adds a same-device signal that survives cookie clearing, so
|
||||
both are sent together whenever the heartbeat identifies this socket.
|
||||
*/
|
||||
fingerprintRef.current = await getBrowserFingerprintId();
|
||||
}
|
||||
/*
|
||||
Every route shares the same persisted identity key, but only the main
|
||||
driver page should trigger duplicate-tab prevention. Sending the surface
|
||||
with the heartbeat lets the server make that decision before spectator
|
||||
pages finish their role switch.
|
||||
Handshake auth establishes identity before connection handlers run.
|
||||
This event remains the live-update path for settings that change while
|
||||
the current transport stays connected, so existing callers and server
|
||||
behavior do not need a second update contract.
|
||||
*/
|
||||
const resp = await identifySession({
|
||||
cookieUserId,
|
||||
fingerprintId: fingerprintRef.current,
|
||||
fingerprintId: await getBrowserFingerprintId(),
|
||||
nickname,
|
||||
overseerEnabled,
|
||||
identitySurface: normalizedIdentitySurface,
|
||||
@@ -57,12 +47,11 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
if (nextKey && nextKey !== cookieUserId) {
|
||||
saveIdentity((current) => ({ ...(current || {}), cookieUserId: nextKey }));
|
||||
}
|
||||
lastAckSocketRef.current = socket.id;
|
||||
} catch {
|
||||
/*
|
||||
The permanent heartbeat below is the retry mechanism. A failed or
|
||||
half-open request must not create separate timer state that can stop
|
||||
future identity sends or disappear during a socket transition.
|
||||
A failed live update must not replace Socket.IO's connection lifecycle
|
||||
with custom retry state. The next reconnect reads the latest persisted
|
||||
settings through handshake auth and re-establishes the complete identity.
|
||||
*/
|
||||
}
|
||||
}, [
|
||||
@@ -78,46 +67,25 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready || !connected || !socket?.id) return;
|
||||
if (lastAckSocketRef.current === socket.id) return;
|
||||
sendIdentify();
|
||||
}, [connected, ready, sendIdentify, socket?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
This runs once when the route's settings become ready and again only when
|
||||
an identity value changes. Reconnects receive the same data from the auth
|
||||
callback, so there is intentionally no timer, visibility retry, or online
|
||||
retry here.
|
||||
*/
|
||||
if (!ready || !connected || !socket?.id) return;
|
||||
sendIdentify();
|
||||
}, [ready, connected, socket?.id, cookieUserId, nickname, overseerEnabled, normalizedIdentitySurface, sendIdentify]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => {
|
||||
if (!socket?.connected) return;
|
||||
sendIdentify();
|
||||
};
|
||||
const handleVisibility = () => {
|
||||
if (typeof document === 'undefined' || document.visibilityState !== 'visible') return;
|
||||
if (!socket?.connected) return;
|
||||
sendIdentify();
|
||||
};
|
||||
window.addEventListener('online', handleOnline);
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
};
|
||||
}, [sendIdentify, socket?.connected]);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Keep this interval installed whenever persisted identity settings are
|
||||
ready, including while Socket.IO is reconnecting. Each tick checks the
|
||||
current connection before sending, so reconnects resume heartbeats without
|
||||
depending on an acknowledgement or another effect recreating the timer.
|
||||
A first-time browser has no portable key to send in its handshake, so the
|
||||
canonical identity service creates one. Session sync is authoritative for
|
||||
that generated value; persisting it here makes every later handshake carry
|
||||
the same user key without depending on a session:identify acknowledgement.
|
||||
*/
|
||||
if (!ready) return undefined;
|
||||
const timer = setInterval(() => {
|
||||
if (!socket?.connected) return;
|
||||
sendIdentify();
|
||||
}, 2000);
|
||||
return () => clearInterval(timer);
|
||||
}, [ready, sendIdentify, socket]);
|
||||
const nextKey = String(serverCookieUserId || '').trim();
|
||||
if (!nextKey || nextKey === cookieUserId) return;
|
||||
saveIdentity((current) => ({ ...(current || {}), cookieUserId: nextKey }));
|
||||
}, [cookieUserId, saveIdentity, serverCookieUserId]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Socket Library Helper
|
||||
// Purpose: Builds and exports the browser Socket.IO client with shared defaults. Scope: Centralizes connection URL/options so all modules use consistent socket behavior.
|
||||
import { io } from 'socket.io-client';
|
||||
import { getBrowserFingerprintId } from './browserFingerprint.js';
|
||||
import { loadSettings } from '../settings/persistence.js';
|
||||
|
||||
const configured = import.meta.env.VITE_ROVERD_URL?.trim();
|
||||
@@ -9,8 +10,48 @@ console.info('[socket] connecting to', resolvedUrl);
|
||||
const settings = loadSettings();
|
||||
const transportPref = settings?.page?.connectionTransport || 'websocket';
|
||||
const transports = transportPref === 'polling' ? ['polling'] : ['websocket', 'polling'];
|
||||
|
||||
function getIdentitySurface() {
|
||||
/*
|
||||
Duplicate-driver protection is connection-specific, so the route must be
|
||||
known during the handshake rather than waiting for a React route component
|
||||
to mount. The main driver and dedicated PTZ controller are the two active
|
||||
control surfaces; every other route is passive identity-wise.
|
||||
*/
|
||||
return window.location.pathname === '/' || window.location.pathname === '/ptz'
|
||||
? 'driver'
|
||||
: 'passive';
|
||||
}
|
||||
|
||||
async function buildSocketIdentity() {
|
||||
/*
|
||||
Socket.IO invokes this auth callback again for every reconnection. Reading
|
||||
persistence here, instead of reusing the module-level settings snapshot,
|
||||
ensures that nickname, preferences, imported settings, and a server-issued
|
||||
identity key are current whenever a new socket is accepted.
|
||||
*/
|
||||
const currentSettings = loadSettings();
|
||||
const fingerprintId = await getBrowserFingerprintId();
|
||||
return {
|
||||
cookieUserId: String(currentSettings?.identity?.cookieUserId || '').trim(),
|
||||
fingerprintId,
|
||||
nickname: String(currentSettings?.profile?.nickname || '').trim(),
|
||||
overseerEnabled: Boolean(currentSettings?.overseerPreference?.enabled),
|
||||
identitySurface: getIdentitySurface(),
|
||||
};
|
||||
}
|
||||
|
||||
export const socket = io(resolvedUrl, {
|
||||
transports,
|
||||
timeout: 15000,
|
||||
/*
|
||||
The callback form lets the existing asynchronous fingerprint finish before
|
||||
Socket.IO sends its namespace CONNECT packet. Server connection middleware
|
||||
can therefore attach the canonical identity before any feature service sees
|
||||
the socket, eliminating the previous connected-but-unidentified window.
|
||||
*/
|
||||
auth: (callback) => {
|
||||
buildSocketIdentity().then(callback);
|
||||
},
|
||||
});
|
||||
socket.on('connect_error', (err) => console.error('connect_error', err.code, err.message, err.data));
|
||||
|
||||
Reference in New Issue
Block a user