Rename signer-improved to simply signer
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 51s
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 51s
This commit is contained in:
parent
b04679ba34
commit
7d47eec1f2
@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SignerImprovedService } from '../services/signer';
|
||||
import { SignerService } from '../services/signer';
|
||||
import { SessionManager } from '../utils/session-manager';
|
||||
import { authTokens } from '../utils/auth-tokens';
|
||||
import { ProcessInfo, ProcessData, ProcessRoles, EOfficeStatus } from '../types';
|
||||
@ -32,7 +32,7 @@ export class ProcessController {
|
||||
const { pairingId } = req.query;
|
||||
|
||||
// Execute signer operations with retry logic
|
||||
const processResult = await SignerImprovedService.executeWithRetry(
|
||||
const processResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
return await signerClient.getUserProcessByIdnot(userAuth.idNotUser.idNot);
|
||||
},
|
||||
@ -91,7 +91,7 @@ export class ProcessController {
|
||||
privateFields.splice(privateFields.indexOf('idNot'), 1);
|
||||
|
||||
// Get pairing ID with retry
|
||||
const pairingResult = await SignerImprovedService.executeWithRetry(
|
||||
const pairingResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
return await signerClient.getPairingId();
|
||||
},
|
||||
@ -125,7 +125,7 @@ export class ProcessController {
|
||||
};
|
||||
|
||||
// Create process with retry
|
||||
const createResult = await SignerImprovedService.executeWithRetry(
|
||||
const createResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
return await signerClient.createProcess(processData, privateFields, roles);
|
||||
},
|
||||
@ -155,7 +155,7 @@ export class ProcessController {
|
||||
}
|
||||
|
||||
// Check if process is committed and handle role updates
|
||||
const processManagementResult = await SignerImprovedService.executeWithRetry(
|
||||
const processManagementResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
const allProcesses = await signerClient.getOwnedProcesses();
|
||||
|
||||
@ -202,6 +202,12 @@ export class ProcessController {
|
||||
const stateId = updatedProcessReturn.updatedProcess.diffs[0].state_id;
|
||||
await signerClient.notifyUpdate(processId, stateId);
|
||||
await signerClient.validateState(processId, stateId);
|
||||
} else {
|
||||
Logger.info('PairingId already in owner role', {
|
||||
requestId,
|
||||
pairingId: req.query.pairingId,
|
||||
roles: roles['owner'].members
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,7 +249,7 @@ export class ProcessController {
|
||||
}
|
||||
|
||||
// Get office process with retry
|
||||
const processResult = await SignerImprovedService.executeWithRetry(
|
||||
const processResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
return await signerClient.getOfficeProcessByIdnot(userAuth.idNotUser.office.idNot);
|
||||
},
|
||||
@ -264,7 +270,7 @@ export class ProcessController {
|
||||
});
|
||||
|
||||
// Get validator ID with retry
|
||||
const pairingResult = await SignerImprovedService.executeWithRetry(
|
||||
const pairingResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
return await signerClient.getPairingId();
|
||||
},
|
||||
@ -311,6 +317,11 @@ export class ProcessController {
|
||||
// No need for public fields?
|
||||
|
||||
const roles: ProcessRoles = {
|
||||
collaborator: {
|
||||
members: [],
|
||||
validation_rules: [],
|
||||
storages: []
|
||||
},
|
||||
owner: {
|
||||
members: [validatorId],
|
||||
validation_rules: [{
|
||||
@ -328,7 +339,7 @@ export class ProcessController {
|
||||
};
|
||||
|
||||
// Create office process with retry
|
||||
const createResult = await SignerImprovedService.executeWithRetry(
|
||||
const createResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
return await signerClient.createProcess(processData, privateFields, roles);
|
||||
},
|
||||
@ -341,30 +352,105 @@ export class ProcessController {
|
||||
}
|
||||
|
||||
Logger.info('Created new office process', { process: createResult.data });
|
||||
|
||||
// Use the idnot number to identify all active members of the office
|
||||
const officeCollaborators = await IdNotController.getOfficeRattachements(userAuth.idNotUser.office.idNot);
|
||||
Logger.debug('Office collaborators', { officeCollaborators });
|
||||
|
||||
// Logger.info('Created new office process', {
|
||||
// requestId,
|
||||
// processId: createResult.data!.processId
|
||||
// });
|
||||
|
||||
process = {
|
||||
processId: createResult.data!.processCreated.processId || '',
|
||||
processId: createResult.data!.processCreated.processId,
|
||||
processData: createResult.data!.processData
|
||||
};
|
||||
}
|
||||
|
||||
Logger.info('Using office process', {
|
||||
requestId,
|
||||
processId: process.processId,
|
||||
processData: process.processData
|
||||
});
|
||||
|
||||
// Check if process is committed and handle role updates
|
||||
const processManagementResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
const allProcesses = await signerClient.getOwnedProcesses();
|
||||
|
||||
if (allProcesses && process) {
|
||||
const processStates = allProcesses.processes[process.processId].states;
|
||||
const isNotCommited = processStates.length === 2
|
||||
&& processStates[1].commited_in === processStates[0].commited_in;
|
||||
|
||||
if (isNotCommited) {
|
||||
Logger.info('Process not committed, committing it', {
|
||||
requestId,
|
||||
processId: process.processId
|
||||
});
|
||||
await signerClient.validateState(process.processId, processStates[0].state_id);
|
||||
}
|
||||
}
|
||||
|
||||
// // Use the idnot number to identify all active members of the office
|
||||
// const officeCollaborators = await IdNotController.getOfficeRattachements(userAuth.idNotUser.office.idNot);
|
||||
// Logger.debug('Office collaborators', { officeCollaborators });
|
||||
|
||||
// let roles: ProcessRoles;
|
||||
// if (isNotCommited) {
|
||||
// const firstState = processStates[0];
|
||||
// roles = firstState.roles;
|
||||
// } else {
|
||||
// const tip = processStates[processStates.length - 1].commited_in;
|
||||
// const lastState = processStates.findLast((state: any) => state.commited_in !== tip);
|
||||
// roles = lastState.roles;
|
||||
// }
|
||||
|
||||
// if (!roles) {
|
||||
// throw new Error('No roles found');
|
||||
// } else if (!roles['owner'] || !roles['collaborator']) {
|
||||
// throw new Error('No owner or collaborator role found');
|
||||
// }
|
||||
|
||||
// for (const collaborator of officeCollaborators) {
|
||||
// if (collaborator.idNot === userAuth.idNotUser.idNot) {
|
||||
// // We add ourselves regardless of the activity status
|
||||
// // We should have a collaborator process
|
||||
|
||||
// continue;
|
||||
// }
|
||||
// if (collaborator.activite === 'En exercice') {
|
||||
// // TODO we check if the collaborator has a process
|
||||
// // If not, we create a new process for them
|
||||
// // if yes, we add the collaborator process in role `collaborator`
|
||||
// // we also lookup the pairing ids in the collaborator process and put them all in the owner role
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!roles['owner'].members.includes(req.query.pairingId as string)) {
|
||||
// Logger.info('Adding new pairingId to owner role', {
|
||||
// requestId,
|
||||
// pairingId: req.query.pairingId,
|
||||
// processId: process.processId
|
||||
// });
|
||||
|
||||
// roles['owner'].members.push(req.query.pairingId as string);
|
||||
// const updatedProcessReturn = await signerClient.updateProcess(process.processId, {}, [], roles);
|
||||
// const processId = updatedProcessReturn.updatedProcess.process_id;
|
||||
// const stateId = updatedProcessReturn.updatedProcess.diffs[0].state_id;
|
||||
// await signerClient.notifyUpdate(processId, stateId);
|
||||
// await signerClient.validateState(processId, stateId);
|
||||
// }
|
||||
return process;
|
||||
},
|
||||
'processManagement',
|
||||
2
|
||||
);
|
||||
|
||||
console.log('processManagementResult', processManagementResult);
|
||||
if (!processManagementResult.success) {
|
||||
throw new ExternalServiceError('Signer', processManagementResult.error?.message || 'Failed to manage office process', requestId);
|
||||
}
|
||||
|
||||
Logger.info('Office process request completed successfully', {
|
||||
requestId,
|
||||
processId: process?.processId
|
||||
processId: processManagementResult.data?.processId
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: process
|
||||
data: processManagementResult.data
|
||||
});
|
||||
});
|
||||
|
||||
@ -401,7 +487,7 @@ export class ProcessController {
|
||||
|
||||
Logger.info('Phone number lookup initiated', { requestId, email });
|
||||
|
||||
const phoneResult = await SignerImprovedService.executeWithRetry(
|
||||
const phoneResult = await SignerService.executeWithRetry(
|
||||
async (signerClient) => {
|
||||
return await signerClient.getPhoneNumberForEmail(email);
|
||||
},
|
||||
@ -430,7 +516,7 @@ export class ProcessController {
|
||||
|
||||
// Health check endpoint for signer service
|
||||
static getSignerHealth = asyncHandler(async (req: Request, res: Response): Promise<void> => {
|
||||
const healthStatus = SignerImprovedService.getHealthStatus();
|
||||
const healthStatus = SignerService.getHealthStatus();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@ -447,7 +533,7 @@ export class ProcessController {
|
||||
|
||||
Logger.info('Force signer reconnection requested', { requestId });
|
||||
|
||||
const reconnectResult = await SignerImprovedService.forceReconnect();
|
||||
const reconnectResult = await SignerService.forceReconnect();
|
||||
|
||||
if (!reconnectResult.success) {
|
||||
throw new ExternalServiceError('Signer', reconnectResult.error?.message || 'Failed to reconnect', requestId);
|
||||
@ -458,7 +544,7 @@ export class ProcessController {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Signer reconnection initiated',
|
||||
data: SignerImprovedService.getHealthStatus()
|
||||
data: SignerService.getHealthStatus()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { config } from './config';
|
||||
import { routes } from './routes';
|
||||
import { SignerImprovedService } from './services/signer-improved';
|
||||
import { SignerService } from './services/signer';
|
||||
import { SessionManager } from './utils/session-manager';
|
||||
import { EmailService } from './services/email';
|
||||
import { authTokens } from './utils/auth-tokens';
|
||||
@ -41,7 +41,7 @@ app.use(errorHandler);
|
||||
// Initialize signer service with enhanced reconnection logic
|
||||
(async () => {
|
||||
try {
|
||||
const result = await SignerImprovedService.initialize();
|
||||
const result = await SignerService.initialize();
|
||||
if (result.success) {
|
||||
Logger.info('Signer service initialized');
|
||||
} else {
|
||||
@ -57,7 +57,7 @@ app.use(errorHandler);
|
||||
})();
|
||||
|
||||
// Set up signer connection monitoring
|
||||
SignerImprovedService.onConnectionChange((connected) => {
|
||||
SignerService.onConnectionChange((connected) => {
|
||||
if (connected) {
|
||||
Logger.info('Signer connected');
|
||||
} else {
|
||||
@ -110,13 +110,13 @@ async function startServer(): Promise<void> {
|
||||
// Graceful shutdown handling
|
||||
process.on('SIGTERM', () => {
|
||||
Logger.info('SIGTERM received, shutting down gracefully');
|
||||
SignerImprovedService.cleanup();
|
||||
SignerService.cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
Logger.info('SIGINT received, shutting down gracefully');
|
||||
SignerImprovedService.cleanup();
|
||||
SignerService.cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@ -126,7 +126,7 @@ process.on('uncaughtException', (error) => {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
SignerImprovedService.cleanup();
|
||||
SignerService.cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
@ -1,482 +0,0 @@
|
||||
import { SDKSignerClient, ClientConfig } from 'sdk-signer-client';
|
||||
import { signerConfig } from '../../config/signer';
|
||||
import { Logger } from '../../utils/logger';
|
||||
import { Result, ServiceResult } from '../../utils/result';
|
||||
|
||||
export enum SignerConnectionState {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
export interface SignerHealthCheck {
|
||||
state: SignerConnectionState;
|
||||
lastConnected?: number;
|
||||
lastError?: string;
|
||||
reconnectAttempts: number;
|
||||
nextReconnectAt?: number;
|
||||
}
|
||||
|
||||
export class SignerImprovedService {
|
||||
private static instance: SDKSignerClient | null = null;
|
||||
private static connectionState: SignerConnectionState = SignerConnectionState.DISCONNECTED;
|
||||
private static reconnectAttempts: number = 0;
|
||||
private static lastConnected: number | null = null;
|
||||
private static lastError: string | null = null;
|
||||
private static reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
private static maxReconnectAttempts: number = 10; // More attempts than the SDK default
|
||||
private static reconnectInterval: number = 5000; // 5 seconds
|
||||
private static backoffMultiplier: number = 1.5; // Exponential backoff
|
||||
private static maxReconnectInterval: number = 60000; // Max 60 seconds between attempts
|
||||
private static healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
private static connectionCallbacks: Array<(connected: boolean) => void> = [];
|
||||
|
||||
/**
|
||||
* Initialize the signer service with enhanced reconnection logic
|
||||
*/
|
||||
static async initialize(): Promise<ServiceResult<void>> {
|
||||
try {
|
||||
Logger.info('Initializing Signer service');
|
||||
|
||||
// Create client instance
|
||||
this.instance = new SDKSignerClient(signerConfig);
|
||||
|
||||
// Set up event listeners for connection monitoring
|
||||
this.setupEventListeners();
|
||||
|
||||
// Start periodic health checks
|
||||
this.startHealthChecks();
|
||||
|
||||
// Attempt initial connection
|
||||
const connectResult = await this.connect();
|
||||
|
||||
return connectResult;
|
||||
} catch (error) {
|
||||
Logger.error('Failed to initialize Signer service', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
return Result.fromError(error instanceof Error ? error : new Error('Initialization failed'), 'SIGNER_INIT_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signer client instance with connection validation
|
||||
*/
|
||||
static async getInstance(): Promise<ServiceResult<SDKSignerClient>> {
|
||||
if (!this.instance) {
|
||||
const initResult = await this.initialize();
|
||||
if (!initResult.success) {
|
||||
return Result.failure('SIGNER_NOT_INITIALIZED', 'Signer service not initialized', initResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're connected
|
||||
if (this.connectionState !== SignerConnectionState.CONNECTED) {
|
||||
// Try to reconnect if not already attempting
|
||||
if (this.connectionState !== SignerConnectionState.CONNECTING &&
|
||||
this.connectionState !== SignerConnectionState.RECONNECTING) {
|
||||
Logger.warn('Signer not connected, attempting reconnection');
|
||||
const reconnectResult = await this.connect();
|
||||
if (!reconnectResult.success) {
|
||||
return Result.failure('SIGNER_CONNECTION_FAILED', 'Failed to connect to signer', reconnectResult.error);
|
||||
}
|
||||
} else {
|
||||
return Result.failure('SIGNER_CONNECTING', 'Signer is currently connecting, please retry');
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(this.instance!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the signer service with retry logic
|
||||
*/
|
||||
private static async connect(): Promise<ServiceResult<void>> {
|
||||
if (!this.instance) {
|
||||
return Result.failure('SIGNER_NOT_INITIALIZED', 'Signer client not initialized');
|
||||
}
|
||||
|
||||
this.connectionState = this.reconnectAttempts === 0 ? SignerConnectionState.CONNECTING : SignerConnectionState.RECONNECTING;
|
||||
|
||||
try {
|
||||
// Only log connection attempts if we've been trying for a while
|
||||
if (this.reconnectAttempts > 2) {
|
||||
Logger.info('Signer connection attempt', {
|
||||
attempt: this.reconnectAttempts + 1,
|
||||
maxAttempts: this.maxReconnectAttempts
|
||||
});
|
||||
}
|
||||
|
||||
await this.instance.connect();
|
||||
|
||||
// Connection successful - state will be updated by the 'open' event handler
|
||||
return Result.success(undefined);
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown connection error';
|
||||
this.lastError = errorMessage;
|
||||
|
||||
// Only log detailed errors after several attempts or if we've reached the limit
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
this.connectionState = SignerConnectionState.FAILED;
|
||||
Logger.error('Signer connection failed after all attempts', {
|
||||
attempts: this.maxReconnectAttempts,
|
||||
lastError: errorMessage
|
||||
});
|
||||
return Result.failure('SIGNER_CONNECTION_FAILED', `Failed to connect after ${this.maxReconnectAttempts} attempts: ${errorMessage}`);
|
||||
} else if (this.reconnectAttempts > 3) {
|
||||
Logger.warn('Signer connection failing', {
|
||||
attempt: this.reconnectAttempts + 1,
|
||||
error: errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule reconnection with exponential backoff
|
||||
this.scheduleReconnection();
|
||||
|
||||
return Result.failure('SIGNER_CONNECTION_FAILED', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a reconnection attempt with exponential backoff
|
||||
*/
|
||||
private static scheduleReconnection(): void {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
|
||||
// Calculate delay with exponential backoff
|
||||
const baseDelay = this.reconnectInterval;
|
||||
const delay = Math.min(
|
||||
baseDelay * Math.pow(this.backoffMultiplier, this.reconnectAttempts),
|
||||
this.maxReconnectInterval
|
||||
);
|
||||
|
||||
this.reconnectAttempts++;
|
||||
|
||||
// Only log scheduling if this is taking a while
|
||||
if (this.reconnectAttempts > 2) {
|
||||
Logger.debug('Next reconnection in', { delayMs: delay });
|
||||
}
|
||||
|
||||
this.reconnectTimeout = setTimeout(async () => {
|
||||
await this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners for the signer client
|
||||
*/
|
||||
private static setupEventListeners(): void {
|
||||
if (!this.instance) return;
|
||||
|
||||
try {
|
||||
// Listen for WebSocket close events
|
||||
this.instance.on('close', () => {
|
||||
// Don't log here - let handleDisconnection() do the logging
|
||||
this.handleDisconnection();
|
||||
});
|
||||
|
||||
// Listen for WebSocket error events
|
||||
this.instance.on('error', (error: Error) => {
|
||||
// Only log if it's a new error
|
||||
if (this.lastError !== error.message) {
|
||||
Logger.error('Signer WebSocket error', { error: error.message });
|
||||
}
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
// Listen for successful reconnection events
|
||||
this.instance.on('reconnect', () => {
|
||||
Logger.info('Signer reconnected via SDK');
|
||||
this.handleSDKReconnection();
|
||||
});
|
||||
|
||||
// Listen for connection open events
|
||||
this.instance.on('open', () => {
|
||||
Logger.info('Signer connected');
|
||||
this.handleSDKConnection();
|
||||
});
|
||||
|
||||
Logger.debug('Signer event listeners configured');
|
||||
|
||||
} catch (error) {
|
||||
Logger.warn('Could not set up SDK event listeners', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle disconnection events from WebSocket
|
||||
*/
|
||||
private static handleDisconnection(): void {
|
||||
// Only handle if we were actually connected (avoid duplicate handling)
|
||||
if (this.connectionState === SignerConnectionState.CONNECTED) {
|
||||
Logger.warn('Signer disconnected - reconnecting...');
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
this.notifyConnectionCallbacks(false);
|
||||
|
||||
// Cancel any pending health checks since we know we're disconnected
|
||||
this.cancelScheduledOperations();
|
||||
|
||||
// Initiate immediate reconnection (but respect ongoing attempts)
|
||||
if (!this.reconnectTimeout) {
|
||||
this.scheduleReconnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle connection errors from WebSocket
|
||||
*/
|
||||
private static handleError(error: Error): void {
|
||||
this.lastError = error.message;
|
||||
|
||||
// Only log detailed error if we were connected (avoid startup error spam)
|
||||
if (this.connectionState === SignerConnectionState.CONNECTED) {
|
||||
Logger.error('Signer connection error', { error: error.message });
|
||||
this.handleDisconnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful SDK reconnection
|
||||
*/
|
||||
private static handleSDKReconnection(): void {
|
||||
// The SDK handled the reconnection, update our state
|
||||
this.connectionState = SignerConnectionState.CONNECTED;
|
||||
this.lastConnected = Date.now();
|
||||
this.lastError = null;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Clear any pending reconnection timeout since SDK handled it
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
// Notify callbacks (they will handle the logging)
|
||||
this.notifyConnectionCallbacks(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SDK connection open events
|
||||
*/
|
||||
private static handleSDKConnection(): void {
|
||||
this.connectionState = SignerConnectionState.CONNECTED;
|
||||
this.lastConnected = Date.now();
|
||||
this.lastError = null;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Clear any pending operations
|
||||
this.cancelScheduledOperations();
|
||||
|
||||
// Notify callbacks (they will handle the logging)
|
||||
this.notifyConnectionCallbacks(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel scheduled operations (reconnection, health checks during known disconnection)
|
||||
*/
|
||||
private static cancelScheduledOperations(): void {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic health checks
|
||||
*/
|
||||
private static startHealthChecks(): void {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
}
|
||||
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
await this.performHealthCheck();
|
||||
}, 30000); // Check every 30 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a health check on the signer connection
|
||||
*/
|
||||
private static async performHealthCheck(): Promise<void> {
|
||||
// Only perform health checks if we think we're connected
|
||||
if (!this.instance || this.connectionState !== SignerConnectionState.CONNECTED) {
|
||||
Logger.debug('Skipping health check - not in connected state', {
|
||||
state: this.connectionState,
|
||||
hasInstance: !!this.instance
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try a simple operation to verify connection is actually working
|
||||
await this.instance.getPairingId();
|
||||
|
||||
Logger.debug('Signer health check passed');
|
||||
|
||||
} catch (error) {
|
||||
Logger.warn('Signer health check failed - connection may be stale', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
|
||||
// Health check failed, but we haven't received a close/error event
|
||||
// This indicates a stale connection - treat as disconnection
|
||||
this.handleDisconnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a signer operation with automatic retry on connection failure
|
||||
*/
|
||||
static async executeWithRetry<T>(
|
||||
operation: (client: SDKSignerClient) => Promise<T>,
|
||||
operationName: string,
|
||||
maxRetries: number = 3
|
||||
): Promise<ServiceResult<T>> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const clientResult = await this.getInstance();
|
||||
if (!clientResult.success) {
|
||||
lastError = new Error(clientResult.error?.message || 'Failed to get signer instance');
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// Only log operation retries if it's the final attempt or taking too long
|
||||
if (attempt === maxRetries - 1) {
|
||||
Logger.warn(`${operationName} final retry attempt`, {
|
||||
error: lastError.message
|
||||
});
|
||||
}
|
||||
|
||||
// Wait before retry
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const result = await operation(clientResult.data!);
|
||||
|
||||
// Only log success if it took multiple attempts
|
||||
if (attempt > 1) {
|
||||
Logger.info(`${operationName} succeeded after ${attempt} attempts`);
|
||||
}
|
||||
|
||||
return Result.success(result);
|
||||
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error('Unknown error');
|
||||
|
||||
// Only log operation failures if it's taking multiple attempts
|
||||
if (attempt > 1) {
|
||||
Logger.warn(`${operationName} attempt ${attempt} failed`, {
|
||||
error: lastError.message
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a connection error, force reconnection
|
||||
if (lastError.message.includes('connection') || lastError.message.includes('websocket')) {
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// Wait before retry with exponential backoff
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.error(`${operationName} failed after ${maxRetries} attempts`, {
|
||||
error: lastError?.message || 'Unknown error'
|
||||
});
|
||||
|
||||
return Result.fromError(lastError || new Error('Operation failed'), 'SIGNER_OPERATION_FAILED');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection health status
|
||||
*/
|
||||
static getHealthStatus(): SignerHealthCheck {
|
||||
return {
|
||||
state: this.connectionState,
|
||||
lastConnected: this.lastConnected || undefined,
|
||||
lastError: this.lastError || undefined,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
nextReconnectAt: this.reconnectTimeout ? Date.now() + this.reconnectInterval : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Force reconnection (useful for manual recovery)
|
||||
*/
|
||||
static async forceReconnect(): Promise<ServiceResult<void>> {
|
||||
Logger.info('Force reconnection requested');
|
||||
|
||||
// Reset state
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
return await this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for connection state changes
|
||||
*/
|
||||
static onConnectionChange(callback: (connected: boolean) => void): () => void {
|
||||
this.connectionCallbacks.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.connectionCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.connectionCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all connection callbacks
|
||||
*/
|
||||
private static notifyConnectionCallbacks(connected: boolean): void {
|
||||
this.connectionCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(connected);
|
||||
} catch (error) {
|
||||
Logger.error('Error in connection callback', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
static cleanup(): void {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
|
||||
this.connectionCallbacks = [];
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
}
|
||||
}
|
@ -1,18 +1,482 @@
|
||||
import { SDKSignerClient } from 'sdk-signer-client';
|
||||
import { SDKSignerClient, ClientConfig } from 'sdk-signer-client';
|
||||
import { signerConfig } from '../../config/signer';
|
||||
import { Logger } from '../../utils/logger';
|
||||
import { Result, ServiceResult } from '../../utils/result';
|
||||
|
||||
export enum SignerConnectionState {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
export interface SignerHealthCheck {
|
||||
state: SignerConnectionState;
|
||||
lastConnected?: number;
|
||||
lastError?: string;
|
||||
reconnectAttempts: number;
|
||||
nextReconnectAt?: number;
|
||||
}
|
||||
|
||||
export class SignerService {
|
||||
private static instance: SDKSignerClient;
|
||||
private static instance: SDKSignerClient | null = null;
|
||||
private static connectionState: SignerConnectionState = SignerConnectionState.DISCONNECTED;
|
||||
private static reconnectAttempts: number = 0;
|
||||
private static lastConnected: number | null = null;
|
||||
private static lastError: string | null = null;
|
||||
private static reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
private static maxReconnectAttempts: number = 10; // More attempts than the SDK default
|
||||
private static reconnectInterval: number = 5000; // 5 seconds
|
||||
private static backoffMultiplier: number = 1.5; // Exponential backoff
|
||||
private static maxReconnectInterval: number = 60000; // Max 60 seconds between attempts
|
||||
private static healthCheckInterval: NodeJS.Timeout | null = null;
|
||||
private static connectionCallbacks: Array<(connected: boolean) => void> = [];
|
||||
|
||||
static getInstance(): SDKSignerClient {
|
||||
if (!this.instance) {
|
||||
/**
|
||||
* Initialize the signer service with enhanced reconnection logic
|
||||
*/
|
||||
static async initialize(): Promise<ServiceResult<void>> {
|
||||
try {
|
||||
Logger.info('Initializing Signer service');
|
||||
|
||||
// Create client instance
|
||||
this.instance = new SDKSignerClient(signerConfig);
|
||||
|
||||
// Set up event listeners for connection monitoring
|
||||
this.setupEventListeners();
|
||||
|
||||
// Start periodic health checks
|
||||
this.startHealthChecks();
|
||||
|
||||
// Attempt initial connection
|
||||
const connectResult = await this.connect();
|
||||
|
||||
return connectResult;
|
||||
} catch (error) {
|
||||
Logger.error('Failed to initialize Signer service', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
return Result.fromError(error instanceof Error ? error : new Error('Initialization failed'), 'SIGNER_INIT_ERROR');
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
static async connect(): Promise<void> {
|
||||
const client = this.getInstance();
|
||||
await client.connect();
|
||||
/**
|
||||
* Get the signer client instance with connection validation
|
||||
*/
|
||||
static async getInstance(): Promise<ServiceResult<SDKSignerClient>> {
|
||||
if (!this.instance) {
|
||||
const initResult = await this.initialize();
|
||||
if (!initResult.success) {
|
||||
return Result.failure('SIGNER_NOT_INITIALIZED', 'Signer service not initialized', initResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're connected
|
||||
if (this.connectionState !== SignerConnectionState.CONNECTED) {
|
||||
// Try to reconnect if not already attempting
|
||||
if (this.connectionState !== SignerConnectionState.CONNECTING &&
|
||||
this.connectionState !== SignerConnectionState.RECONNECTING) {
|
||||
Logger.warn('Signer not connected, attempting reconnection');
|
||||
const reconnectResult = await this.connect();
|
||||
if (!reconnectResult.success) {
|
||||
return Result.failure('SIGNER_CONNECTION_FAILED', 'Failed to connect to signer', reconnectResult.error);
|
||||
}
|
||||
} else {
|
||||
return Result.failure('SIGNER_CONNECTING', 'Signer is currently connecting, please retry');
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(this.instance!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the signer service with retry logic
|
||||
*/
|
||||
private static async connect(): Promise<ServiceResult<void>> {
|
||||
if (!this.instance) {
|
||||
return Result.failure('SIGNER_NOT_INITIALIZED', 'Signer client not initialized');
|
||||
}
|
||||
|
||||
this.connectionState = this.reconnectAttempts === 0 ? SignerConnectionState.CONNECTING : SignerConnectionState.RECONNECTING;
|
||||
|
||||
try {
|
||||
// Only log connection attempts if we've been trying for a while
|
||||
if (this.reconnectAttempts > 2) {
|
||||
Logger.info('Signer connection attempt', {
|
||||
attempt: this.reconnectAttempts + 1,
|
||||
maxAttempts: this.maxReconnectAttempts
|
||||
});
|
||||
}
|
||||
|
||||
await this.instance.connect();
|
||||
|
||||
// Connection successful - state will be updated by the 'open' event handler
|
||||
return Result.success(undefined);
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown connection error';
|
||||
this.lastError = errorMessage;
|
||||
|
||||
// Only log detailed errors after several attempts or if we've reached the limit
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
this.connectionState = SignerConnectionState.FAILED;
|
||||
Logger.error('Signer connection failed after all attempts', {
|
||||
attempts: this.maxReconnectAttempts,
|
||||
lastError: errorMessage
|
||||
});
|
||||
return Result.failure('SIGNER_CONNECTION_FAILED', `Failed to connect after ${this.maxReconnectAttempts} attempts: ${errorMessage}`);
|
||||
} else if (this.reconnectAttempts > 3) {
|
||||
Logger.warn('Signer connection failing', {
|
||||
attempt: this.reconnectAttempts + 1,
|
||||
error: errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule reconnection with exponential backoff
|
||||
this.scheduleReconnection();
|
||||
|
||||
return Result.failure('SIGNER_CONNECTION_FAILED', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a reconnection attempt with exponential backoff
|
||||
*/
|
||||
private static scheduleReconnection(): void {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
|
||||
// Calculate delay with exponential backoff
|
||||
const baseDelay = this.reconnectInterval;
|
||||
const delay = Math.min(
|
||||
baseDelay * Math.pow(this.backoffMultiplier, this.reconnectAttempts),
|
||||
this.maxReconnectInterval
|
||||
);
|
||||
|
||||
this.reconnectAttempts++;
|
||||
|
||||
// Only log scheduling if this is taking a while
|
||||
if (this.reconnectAttempts > 2) {
|
||||
Logger.debug('Next reconnection in', { delayMs: delay });
|
||||
}
|
||||
|
||||
this.reconnectTimeout = setTimeout(async () => {
|
||||
await this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners for the signer client
|
||||
*/
|
||||
private static setupEventListeners(): void {
|
||||
if (!this.instance) return;
|
||||
|
||||
try {
|
||||
// Listen for WebSocket close events
|
||||
this.instance.on('close', () => {
|
||||
// Don't log here - let handleDisconnection() do the logging
|
||||
this.handleDisconnection();
|
||||
});
|
||||
|
||||
// Listen for WebSocket error events
|
||||
this.instance.on('error', (error: Error) => {
|
||||
// Only log if it's a new error
|
||||
if (this.lastError !== error.message) {
|
||||
Logger.error('Signer WebSocket error', { error: error.message });
|
||||
}
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
// Listen for successful reconnection events
|
||||
this.instance.on('reconnect', () => {
|
||||
Logger.info('Signer reconnected via SDK');
|
||||
this.handleSDKReconnection();
|
||||
});
|
||||
|
||||
// Listen for connection open events
|
||||
this.instance.on('open', () => {
|
||||
Logger.info('Signer connected');
|
||||
this.handleSDKConnection();
|
||||
});
|
||||
|
||||
Logger.debug('Signer event listeners configured');
|
||||
|
||||
} catch (error) {
|
||||
Logger.warn('Could not set up SDK event listeners', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle disconnection events from WebSocket
|
||||
*/
|
||||
private static handleDisconnection(): void {
|
||||
// Only handle if we were actually connected (avoid duplicate handling)
|
||||
if (this.connectionState === SignerConnectionState.CONNECTED) {
|
||||
Logger.warn('Signer disconnected - reconnecting...');
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
this.notifyConnectionCallbacks(false);
|
||||
|
||||
// Cancel any pending health checks since we know we're disconnected
|
||||
this.cancelScheduledOperations();
|
||||
|
||||
// Initiate immediate reconnection (but respect ongoing attempts)
|
||||
if (!this.reconnectTimeout) {
|
||||
this.scheduleReconnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle connection errors from WebSocket
|
||||
*/
|
||||
private static handleError(error: Error): void {
|
||||
this.lastError = error.message;
|
||||
|
||||
// Only log detailed error if we were connected (avoid startup error spam)
|
||||
if (this.connectionState === SignerConnectionState.CONNECTED) {
|
||||
Logger.error('Signer connection error', { error: error.message });
|
||||
this.handleDisconnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful SDK reconnection
|
||||
*/
|
||||
private static handleSDKReconnection(): void {
|
||||
// The SDK handled the reconnection, update our state
|
||||
this.connectionState = SignerConnectionState.CONNECTED;
|
||||
this.lastConnected = Date.now();
|
||||
this.lastError = null;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Clear any pending reconnection timeout since SDK handled it
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
// Notify callbacks (they will handle the logging)
|
||||
this.notifyConnectionCallbacks(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SDK connection open events
|
||||
*/
|
||||
private static handleSDKConnection(): void {
|
||||
this.connectionState = SignerConnectionState.CONNECTED;
|
||||
this.lastConnected = Date.now();
|
||||
this.lastError = null;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Clear any pending operations
|
||||
this.cancelScheduledOperations();
|
||||
|
||||
// Notify callbacks (they will handle the logging)
|
||||
this.notifyConnectionCallbacks(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel scheduled operations (reconnection, health checks during known disconnection)
|
||||
*/
|
||||
private static cancelScheduledOperations(): void {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic health checks
|
||||
*/
|
||||
private static startHealthChecks(): void {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
}
|
||||
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
await this.performHealthCheck();
|
||||
}, 30000); // Check every 30 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a health check on the signer connection
|
||||
*/
|
||||
private static async performHealthCheck(): Promise<void> {
|
||||
// Only perform health checks if we think we're connected
|
||||
if (!this.instance || this.connectionState !== SignerConnectionState.CONNECTED) {
|
||||
Logger.debug('Skipping health check - not in connected state', {
|
||||
state: this.connectionState,
|
||||
hasInstance: !!this.instance
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try a simple operation to verify connection is actually working
|
||||
await this.instance.getPairingId();
|
||||
|
||||
Logger.debug('Signer health check passed');
|
||||
|
||||
} catch (error) {
|
||||
Logger.warn('Signer health check failed - connection may be stale', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
|
||||
// Health check failed, but we haven't received a close/error event
|
||||
// This indicates a stale connection - treat as disconnection
|
||||
this.handleDisconnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a signer operation with automatic retry on connection failure
|
||||
*/
|
||||
static async executeWithRetry<T>(
|
||||
operation: (client: SDKSignerClient) => Promise<T>,
|
||||
operationName: string,
|
||||
maxRetries: number = 3
|
||||
): Promise<ServiceResult<T>> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const clientResult = await this.getInstance();
|
||||
if (!clientResult.success) {
|
||||
lastError = new Error(clientResult.error?.message || 'Failed to get signer instance');
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// Only log operation retries if it's the final attempt or taking too long
|
||||
if (attempt === maxRetries - 1) {
|
||||
Logger.warn(`${operationName} final retry attempt`, {
|
||||
error: lastError.message
|
||||
});
|
||||
}
|
||||
|
||||
// Wait before retry
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const result = await operation(clientResult.data!);
|
||||
|
||||
// Only log success if it took multiple attempts
|
||||
if (attempt > 1) {
|
||||
Logger.info(`${operationName} succeeded after ${attempt} attempts`);
|
||||
}
|
||||
|
||||
return Result.success(result);
|
||||
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error('Unknown error');
|
||||
|
||||
// Only log operation failures if it's taking multiple attempts
|
||||
if (attempt > 1) {
|
||||
Logger.warn(`${operationName} attempt ${attempt} failed`, {
|
||||
error: lastError.message
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a connection error, force reconnection
|
||||
if (lastError.message.includes('connection') || lastError.message.includes('websocket')) {
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// Wait before retry with exponential backoff
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.error(`${operationName} failed after ${maxRetries} attempts`, {
|
||||
error: lastError?.message || 'Unknown error'
|
||||
});
|
||||
|
||||
return Result.fromError(lastError || new Error('Operation failed'), 'SIGNER_OPERATION_FAILED');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection health status
|
||||
*/
|
||||
static getHealthStatus(): SignerHealthCheck {
|
||||
return {
|
||||
state: this.connectionState,
|
||||
lastConnected: this.lastConnected || undefined,
|
||||
lastError: this.lastError || undefined,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
nextReconnectAt: this.reconnectTimeout ? Date.now() + this.reconnectInterval : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Force reconnection (useful for manual recovery)
|
||||
*/
|
||||
static async forceReconnect(): Promise<ServiceResult<void>> {
|
||||
Logger.info('Force reconnection requested');
|
||||
|
||||
// Reset state
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
return await this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for connection state changes
|
||||
*/
|
||||
static onConnectionChange(callback: (connected: boolean) => void): () => void {
|
||||
this.connectionCallbacks.push(callback);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.connectionCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.connectionCallbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all connection callbacks
|
||||
*/
|
||||
private static notifyConnectionCallbacks(connected: boolean): void {
|
||||
this.connectionCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(connected);
|
||||
} catch (error) {
|
||||
Logger.error('Error in connection callback', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
static cleanup(): void {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
|
||||
this.connectionCallbacks = [];
|
||||
this.connectionState = SignerConnectionState.DISCONNECTED;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user