Files
openhuman/app/src/services/socketService.ts
T
Steven EnamakelandGitHub 7ad3c73ac3 Refactor UI auth/bootstrap and remove legacy UI runtime layers (#63)
* Enhance E2E build process and backend URL handling

- Updated `e2e-build.sh` to set CI environment variable correctly for compatibility with various CI runners.
- Refactored `backendUrl.ts` to support dynamic backend URL resolution from Vite environment variables, improving flexibility in different deployment contexts.

* Implement web channel functionality and enhance chat features

- Introduced new web channel commands for sending and canceling chat messages, improving user interaction with the chat system.
- Added support for server-sent events (SSE) to handle real-time updates for chat interactions, enhancing responsiveness.
- Refactored existing code to improve readability and maintainability, including updates to the core HTTP router and JSON-RPC handling.
- Implemented comprehensive end-to-end tests for the web channel flow, ensuring robust functionality and user experience.
- Enhanced the overall structure of the web channel module, including event publishing and subscription mechanisms for better scalability.

* Refactor socket handling to unify communication across environments

- Removed Tauri-specific socket handling from various components, streamlining the codebase to use a single Socket.IO client for both web and desktop environments.
- Updated the `useIntelligenceSocket` hook to eliminate Tauri checks and directly utilize the socket service for message sending and chat initialization.
- Refactored metadata synchronization in Gmail and Notion services to use the unified socket service, enhancing consistency in socket communication.
- Improved error handling and logging across socket interactions to provide clearer feedback during connection issues.
- Enhanced the Conversations component to manage socket connection status, ensuring a more robust user experience during chat interactions.

* Refactor socket service and enhance core URL resolution

- Replaced the deprecated `getBackendUrl` function with `resolveCoreSocketBaseUrl` to improve backend URL resolution for socket connections, accommodating both Tauri and non-Tauri environments.
- Introduced `coreSocketBaseFromRpcUrl` to streamline the processing of RPC URLs.
- Removed the `tauriSocket.ts` file, consolidating socket handling into a unified service for better maintainability and clarity.
- Updated the socket connection logging to provide more informative messages during connection attempts.

* Add engineioxide and socketioxide packages to Cargo.lock and update Cargo.toml

- Introduced new packages: `engineioxide` and `socketioxide`, both at version 0.15.2, enhancing the project's capabilities.
- Updated `Cargo.toml` to include `socketioxide` with the "extensions" feature, ensuring proper integration with the existing codebase.
- Added dependencies for both packages, improving modularity and functionality across the application.

* Refactor Conversations component and streamline chat handling

- Removed unused imports and functions related to Notion context and socket handling, simplifying the Conversations component.
- Enhanced error handling during chat interactions, ensuring clearer feedback for users.
- Updated chatSend function parameters to eliminate unnecessary context, improving the clarity of the API call.
- Consolidated logic for managing chat message history and context, enhancing maintainability and readability of the code.

* Enhance core RPC client with improved logging and error handling

- Introduced debug logging for socket RPC requests and responses, providing better visibility into the communication process.
- Enhanced error handling in the core RPC client to log detailed error messages, improving debugging capabilities.
- Updated socket service to log inbound events, ensuring comprehensive tracking of socket interactions.
- Removed deprecated MCP handlers from the socket service, streamlining the codebase and improving maintainability.

* Refactor apiClient and socketService for improved maintainability

- Changed the declaration of `_getToken` from `let` to `var` in `apiClient.ts` to enhance variable scoping.
- Removed unused imports in `socketService.ts`, streamlining the code and improving readability.

* Refactor deep link handling and enhance E2E testing capabilities

- Updated the deep link configuration in `lib.rs` to use a more generalized `#[cfg(desktop)]` directive, improving compatibility across desktop platforms.
- Enhanced the `triggerDeepLink` function in `deep-link-helpers.ts` to include macOS-specific app activation and launching logic, ensuring better integration with the macOS environment.
- Introduced a new `buildBypassJwt` function to generate bypass JWT tokens for E2E testing, improving the flexibility of authentication flows in tests.
- Updated E2E tests in `conversations-web-channel-flow.spec.ts` to utilize the new `triggerAuthDeepLinkBypass` function, enhancing the testing of authentication scenarios.

* Enhance deep link handling and E2E test configuration

- Updated `triggerAuthDeepLink` in `deep-link-helpers.ts` to support environment-based bypass tokens, improving flexibility in authentication flows during E2E tests.
- Modified the `serve_on_ephemeral` function call in `json_rpc_e2e.rs` to disable the core HTTP router's default behavior, enhancing test isolation and control over the testing environment.

* Refactor deep link handling and enhance E2E test utilities

- Updated `authFlow.e2e.test.tsx` to utilize `window.__simulateDeepLink` for simulating deep links, improving test accuracy and alignment with real-world scenarios.
- Modified `lib.rs` to restrict deep link registration to Windows and Linux platforms, clarifying platform-specific behavior.
- Enhanced `deep-link-helpers.ts` with a new function to simulate deep links in the WebView, providing a fallback mechanism for E2E tests.
- Updated `conversations-web-channel-flow.spec.ts` to reflect changes in deep link triggering, improving logging and test clarity.

* Update dependencies in Cargo.lock and Cargo.toml for improved project stability

- Removed several unused dependencies from `Cargo.lock` and `Cargo.toml`, including `aes-gcm`, `argon2`, and `async-imap`, streamlining the project and reducing potential security vulnerabilities.
- Updated existing dependencies to their latest versions, ensuring compatibility and leveraging improvements in performance and security.
- Simplified the dependency structure by consolidating features and removing unnecessary comments, enhancing clarity and maintainability of the configuration files.

* Refactor App component and remove unused providers

- Removed `AIProvider` and `SkillProvider` from the `App` component, streamlining the component structure and improving readability.
- Updated the `UserProvider` to focus solely on bootstrapping the authentication token, enhancing its clarity and purpose.
- Adjusted the layout within the `App` component to maintain functionality without the removed providers, ensuring a smooth user experience.

* Remove deprecated AI components and types

- Deleted the `index.ts`, `loader.ts`, `types.ts`, and related test files from the AI module, streamlining the codebase by removing unused and outdated components.
- Eliminated the `default-constitution.md` and associated constitution files, which were no longer relevant to the current architecture.
- This cleanup enhances maintainability and reduces complexity within the AI system.

* Refactor user data fetching logic and remove legacy tools cache watcher

- Enhanced the `useUser` hook to prevent infinite retry loops by implementing a token check with a reference to track the last auto-fetch token.
- Removed the legacy file watcher for `TOOLS.md`, transitioning to a core RPC and socket event-based refresh mechanism for tool/config updates.
- Updated test state structure to include `isAuthBootstrapComplete` and `channelConnections`, improving test coverage and state management.

* Refactor channel module structure and add new providers

- Consolidated channel modules under a new `providers` directory, improving organization and clarity.
- Introduced new channel providers for DingTalk, Discord, Email, iMessage, IRC, and Lark, expanding the messaging capabilities of the application.
- Updated the `mod.rs` file to reflect the new structure and ensure proper module exports, enhancing maintainability and ease of use.

* refactor(ui): remove legacy providers/lib logic and stabilize auth bootstrap

* chore: apply pre-push auto-fixes
2026-03-29 20:22:38 -07:00

306 lines
8.8 KiB
TypeScript

import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
import debug from 'debug';
import { io, Socket } from 'socket.io-client';
import { SocketIOMCPTransportImpl } from '../lib/mcp';
import { syncToolsToBackend } from '../lib/skills';
import { store } from '../store';
import { upsertChannelConnection } from '../store/channelConnectionsSlice';
import { resetForUser, setSocketIdForUser, setStatusForUser } from '../store/socketSlice';
import type { ChannelAuthMode, ChannelConnectionStatus, ChannelType } from '../types/channels';
import { CORE_RPC_URL, IS_DEV } from '../utils/config';
import { createSafeLogData, sanitizeError } from '../utils/sanitize';
// Socket service logger using debug package
// Enable logging by setting DEBUG=socket* in environment or localStorage
const socketLog = debug('socket');
const socketWarn = debug('socket:warn');
const socketError = debug('socket:error');
// Enable socket logging in development by default
if (IS_DEV) {
debug.enable('socket*');
}
function coreSocketBaseFromRpcUrl(rpcUrl: string): string {
const trimmed = rpcUrl.trim().replace(/\/+$/, '');
return trimmed.endsWith('/rpc') ? trimmed.slice(0, -4) : trimmed;
}
async function resolveCoreSocketBaseUrl(): Promise<string> {
if (!coreIsTauri()) {
return coreSocketBaseFromRpcUrl(CORE_RPC_URL);
}
try {
const rpcUrl = await invoke<string>('core_rpc_url');
return coreSocketBaseFromRpcUrl(String(rpcUrl || CORE_RPC_URL));
} catch {
return coreSocketBaseFromRpcUrl(CORE_RPC_URL);
}
}
interface JwtPayload {
tgUserId?: string;
userId?: string;
sub?: string;
}
interface ChannelConnectionUpdatedEvent {
channel: ChannelType;
authMode: ChannelAuthMode;
status: ChannelConnectionStatus;
lastError?: string;
capabilities?: string[];
}
function isChannelConnectionUpdatePayload(value: unknown): value is ChannelConnectionUpdatedEvent {
if (!value || typeof value !== 'object') return false;
const obj = value as Record<string, unknown>;
const channel = obj.channel;
const authMode = obj.authMode;
const status = obj.status;
return (
(channel === 'telegram' || channel === 'discord') &&
(authMode === 'managed_dm' ||
authMode === 'oauth' ||
authMode === 'bot_token' ||
authMode === 'api_key') &&
(status === 'connected' ||
status === 'connecting' ||
status === 'disconnected' ||
status === 'error')
);
}
function getSocketUserId(): string {
const token = store.getState().auth.token;
if (!token) return '__pending__';
try {
const parts = token.split('.');
if (parts.length !== 3) return '__pending__';
const payloadBase64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const payloadJson = atob(payloadBase64);
const payload = JSON.parse(payloadJson) as JwtPayload;
const id = payload.tgUserId || payload.userId || payload.sub;
return id || '__pending__';
} catch {
return '__pending__';
}
}
class SocketService {
private socket: Socket | null = null;
private token: string | null = null;
private mcpTransport: SocketIOMCPTransportImpl | null = null;
/**
* Connect to the socket server with authentication.
*/
connect(token: string): void {
void this.connectAsync(token);
}
private async connectAsync(token: string): Promise<void> {
if (!token) return;
// Don't connect if already connected with the same token
if (this.socket?.connected && this.token === token) return;
// Disconnect existing connection if token changed or socket exists
if (this.socket) {
if (this.token !== token) {
this.disconnect();
} else if (this.socket.connected) {
return;
} else if (!this.socket.disconnected) {
// Socket is connecting, wait for it
return;
}
}
this.token = token;
const uid = getSocketUserId();
store.dispatch(setStatusForUser({ userId: uid, status: 'connecting' }));
const backendUrl = await resolveCoreSocketBaseUrl();
socketLog('Connecting to core socket', { userId: uid, backendUrl });
// Ensure we're not connecting to the wrong URL
if (backendUrl.includes('localhost:1420') || backendUrl.includes(':1420')) {
return;
}
const socketOptions = {
auth: { token },
path: '/socket.io/',
transports: ['websocket', 'polling'] as ('websocket' | 'polling')[],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
forceNew: true,
timeout: 2000,
upgrade: true,
query: {},
};
this.socket = io(backendUrl, socketOptions);
this.socket.onAny((event, ...args) => {
const firstArg = args.length > 0 ? args[0] : undefined;
socketLog(
'Inbound event',
createSafeLogData({ event, argsCount: args.length, hasData: args.length > 0 }, firstArg)
);
});
// Initialize MCP transport for client→server MCP requests
this.mcpTransport = new SocketIOMCPTransportImpl(this.socket);
// Connection event handlers
this.socket.on('connect', () => {
const socketId = this.socket?.id || null;
const uid = getSocketUserId();
socketLog('Connected', { socketId, userId: uid });
store.dispatch(setStatusForUser({ userId: uid, status: 'connected' }));
store.dispatch(setSocketIdForUser({ userId: uid, socketId }));
syncToolsToBackend();
});
this.socket.on('ready', () => {
const uid = getSocketUserId();
socketLog('Server ready - authentication successful', { userId: uid });
});
this.socket.on('error', (error: unknown) => {
const uid = getSocketUserId();
socketError('Server error', { userId: uid, error: sanitizeError(error) });
});
this.socket.on('disconnect', (reason: string) => {
const uid = getSocketUserId();
socketLog('Disconnected', { userId: uid, reason });
store.dispatch(setStatusForUser({ userId: uid, status: 'disconnected' }));
store.dispatch(setSocketIdForUser({ userId: uid, socketId: null }));
});
this.socket.on('connect_error', (error: Error) => {
const uid = getSocketUserId();
socketError('Connection error', { userId: uid, error: sanitizeError(error) });
store.dispatch(setStatusForUser({ userId: uid, status: 'disconnected' }));
});
this.socket.on('channel:connection-updated', data => {
if (!isChannelConnectionUpdatePayload(data)) return;
store.dispatch(
upsertChannelConnection({
channel: data.channel,
authMode: data.authMode,
patch: {
status: data.status,
lastError: data.lastError,
capabilities: data.capabilities ?? [],
},
})
);
});
this.socket.connect();
}
/**
* Disconnect from the socket server
*/
disconnect(): void {
if (this.socket) {
const uid = getSocketUserId();
socketLog('Disconnecting', { userId: uid });
this.socket.disconnect();
this.socket = null;
this.token = null;
this.mcpTransport = null;
store.dispatch(resetForUser({ userId: uid }));
}
}
/**
* Get the current socket instance
*/
getSocket(): Socket | null {
return this.socket;
}
/**
* Get the MCP transport for making client→server MCP requests
*/
getMCPTransport(): SocketIOMCPTransportImpl | null {
return this.mcpTransport;
}
/**
* Check if socket is connected
*/
isConnected(): boolean {
return this.socket?.connected || false;
}
/**
* Emit an event to the server
*/
emit(event: string, data?: unknown): void {
if (this.socket?.connected) {
socketLog('Emitting event', createSafeLogData({ event }, data));
this.socket.emit(event, data);
} else {
socketWarn('Cannot emit event - socket not connected', { event });
}
}
/**
* Listen to an event from the server
*/
on(event: string, callback: (...args: unknown[]) => void): void {
if (this.socket) {
const wrappedCallback = (...args: unknown[]) => {
socketLog('Received event', { event, argsCount: args.length, hasData: args.length > 0 });
callback(...args);
};
this.socket.on(event, wrappedCallback);
}
}
/**
* Remove an event listener
*/
off(event: string, callback?: (...args: unknown[]) => void): void {
if (this.socket) {
if (callback) {
this.socket.off(event, callback);
} else {
this.socket.off(event);
}
}
}
/**
* Listen to an event once
*/
once(event: string, callback: (...args: unknown[]) => void): void {
if (this.socket) {
const wrappedCallback = (...args: unknown[]) => {
socketLog('Received event (once)', {
event,
argsCount: args.length,
hasData: args.length > 0,
});
callback(...args);
};
this.socket.once(event, wrappedCallback);
}
}
}
export const socketService = new SocketService();