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 { Request, Response } from 'express';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { SignerImprovedService } from '../services/signer';
|
import { SignerService } from '../services/signer';
|
||||||
import { SessionManager } from '../utils/session-manager';
|
import { SessionManager } from '../utils/session-manager';
|
||||||
import { authTokens } from '../utils/auth-tokens';
|
import { authTokens } from '../utils/auth-tokens';
|
||||||
import { ProcessInfo, ProcessData, ProcessRoles, EOfficeStatus } from '../types';
|
import { ProcessInfo, ProcessData, ProcessRoles, EOfficeStatus } from '../types';
|
||||||
@ -32,7 +32,7 @@ export class ProcessController {
|
|||||||
const { pairingId } = req.query;
|
const { pairingId } = req.query;
|
||||||
|
|
||||||
// Execute signer operations with retry logic
|
// Execute signer operations with retry logic
|
||||||
const processResult = await SignerImprovedService.executeWithRetry(
|
const processResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
return await signerClient.getUserProcessByIdnot(userAuth.idNotUser.idNot);
|
return await signerClient.getUserProcessByIdnot(userAuth.idNotUser.idNot);
|
||||||
},
|
},
|
||||||
@ -91,7 +91,7 @@ export class ProcessController {
|
|||||||
privateFields.splice(privateFields.indexOf('idNot'), 1);
|
privateFields.splice(privateFields.indexOf('idNot'), 1);
|
||||||
|
|
||||||
// Get pairing ID with retry
|
// Get pairing ID with retry
|
||||||
const pairingResult = await SignerImprovedService.executeWithRetry(
|
const pairingResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
return await signerClient.getPairingId();
|
return await signerClient.getPairingId();
|
||||||
},
|
},
|
||||||
@ -125,7 +125,7 @@ export class ProcessController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create process with retry
|
// Create process with retry
|
||||||
const createResult = await SignerImprovedService.executeWithRetry(
|
const createResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
return await signerClient.createProcess(processData, privateFields, roles);
|
return await signerClient.createProcess(processData, privateFields, roles);
|
||||||
},
|
},
|
||||||
@ -155,7 +155,7 @@ export class ProcessController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if process is committed and handle role updates
|
// Check if process is committed and handle role updates
|
||||||
const processManagementResult = await SignerImprovedService.executeWithRetry(
|
const processManagementResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
const allProcesses = await signerClient.getOwnedProcesses();
|
const allProcesses = await signerClient.getOwnedProcesses();
|
||||||
|
|
||||||
@ -202,6 +202,12 @@ export class ProcessController {
|
|||||||
const stateId = updatedProcessReturn.updatedProcess.diffs[0].state_id;
|
const stateId = updatedProcessReturn.updatedProcess.diffs[0].state_id;
|
||||||
await signerClient.notifyUpdate(processId, stateId);
|
await signerClient.notifyUpdate(processId, stateId);
|
||||||
await signerClient.validateState(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
|
// Get office process with retry
|
||||||
const processResult = await SignerImprovedService.executeWithRetry(
|
const processResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
return await signerClient.getOfficeProcessByIdnot(userAuth.idNotUser.office.idNot);
|
return await signerClient.getOfficeProcessByIdnot(userAuth.idNotUser.office.idNot);
|
||||||
},
|
},
|
||||||
@ -264,7 +270,7 @@ export class ProcessController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get validator ID with retry
|
// Get validator ID with retry
|
||||||
const pairingResult = await SignerImprovedService.executeWithRetry(
|
const pairingResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
return await signerClient.getPairingId();
|
return await signerClient.getPairingId();
|
||||||
},
|
},
|
||||||
@ -311,6 +317,11 @@ export class ProcessController {
|
|||||||
// No need for public fields?
|
// No need for public fields?
|
||||||
|
|
||||||
const roles: ProcessRoles = {
|
const roles: ProcessRoles = {
|
||||||
|
collaborator: {
|
||||||
|
members: [],
|
||||||
|
validation_rules: [],
|
||||||
|
storages: []
|
||||||
|
},
|
||||||
owner: {
|
owner: {
|
||||||
members: [validatorId],
|
members: [validatorId],
|
||||||
validation_rules: [{
|
validation_rules: [{
|
||||||
@ -328,7 +339,7 @@ export class ProcessController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create office process with retry
|
// Create office process with retry
|
||||||
const createResult = await SignerImprovedService.executeWithRetry(
|
const createResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
return await signerClient.createProcess(processData, privateFields, roles);
|
return await signerClient.createProcess(processData, privateFields, roles);
|
||||||
},
|
},
|
||||||
@ -341,30 +352,105 @@ export class ProcessController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.info('Created new office process', { process: createResult.data });
|
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 = {
|
process = {
|
||||||
processId: createResult.data!.processCreated.processId || '',
|
processId: createResult.data!.processCreated.processId,
|
||||||
processData: createResult.data!.processData
|
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', {
|
Logger.info('Office process request completed successfully', {
|
||||||
requestId,
|
requestId,
|
||||||
processId: process?.processId
|
processId: processManagementResult.data?.processId
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: process
|
data: processManagementResult.data
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -401,7 +487,7 @@ export class ProcessController {
|
|||||||
|
|
||||||
Logger.info('Phone number lookup initiated', { requestId, email });
|
Logger.info('Phone number lookup initiated', { requestId, email });
|
||||||
|
|
||||||
const phoneResult = await SignerImprovedService.executeWithRetry(
|
const phoneResult = await SignerService.executeWithRetry(
|
||||||
async (signerClient) => {
|
async (signerClient) => {
|
||||||
return await signerClient.getPhoneNumberForEmail(email);
|
return await signerClient.getPhoneNumberForEmail(email);
|
||||||
},
|
},
|
||||||
@ -430,7 +516,7 @@ export class ProcessController {
|
|||||||
|
|
||||||
// Health check endpoint for signer service
|
// Health check endpoint for signer service
|
||||||
static getSignerHealth = asyncHandler(async (req: Request, res: Response): Promise<void> => {
|
static getSignerHealth = asyncHandler(async (req: Request, res: Response): Promise<void> => {
|
||||||
const healthStatus = SignerImprovedService.getHealthStatus();
|
const healthStatus = SignerService.getHealthStatus();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -447,7 +533,7 @@ export class ProcessController {
|
|||||||
|
|
||||||
Logger.info('Force signer reconnection requested', { requestId });
|
Logger.info('Force signer reconnection requested', { requestId });
|
||||||
|
|
||||||
const reconnectResult = await SignerImprovedService.forceReconnect();
|
const reconnectResult = await SignerService.forceReconnect();
|
||||||
|
|
||||||
if (!reconnectResult.success) {
|
if (!reconnectResult.success) {
|
||||||
throw new ExternalServiceError('Signer', reconnectResult.error?.message || 'Failed to reconnect', requestId);
|
throw new ExternalServiceError('Signer', reconnectResult.error?.message || 'Failed to reconnect', requestId);
|
||||||
@ -458,7 +544,7 @@ export class ProcessController {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Signer reconnection initiated',
|
message: 'Signer reconnection initiated',
|
||||||
data: SignerImprovedService.getHealthStatus()
|
data: SignerService.getHealthStatus()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import express from 'express';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
import { routes } from './routes';
|
import { routes } from './routes';
|
||||||
import { SignerImprovedService } from './services/signer-improved';
|
import { SignerService } from './services/signer';
|
||||||
import { SessionManager } from './utils/session-manager';
|
import { SessionManager } from './utils/session-manager';
|
||||||
import { EmailService } from './services/email';
|
import { EmailService } from './services/email';
|
||||||
import { authTokens } from './utils/auth-tokens';
|
import { authTokens } from './utils/auth-tokens';
|
||||||
@ -41,7 +41,7 @@ app.use(errorHandler);
|
|||||||
// Initialize signer service with enhanced reconnection logic
|
// Initialize signer service with enhanced reconnection logic
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await SignerImprovedService.initialize();
|
const result = await SignerService.initialize();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
Logger.info('Signer service initialized');
|
Logger.info('Signer service initialized');
|
||||||
} else {
|
} else {
|
||||||
@ -57,7 +57,7 @@ app.use(errorHandler);
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
// Set up signer connection monitoring
|
// Set up signer connection monitoring
|
||||||
SignerImprovedService.onConnectionChange((connected) => {
|
SignerService.onConnectionChange((connected) => {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
Logger.info('Signer connected');
|
Logger.info('Signer connected');
|
||||||
} else {
|
} else {
|
||||||
@ -110,13 +110,13 @@ async function startServer(): Promise<void> {
|
|||||||
// Graceful shutdown handling
|
// Graceful shutdown handling
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
Logger.info('SIGTERM received, shutting down gracefully');
|
Logger.info('SIGTERM received, shutting down gracefully');
|
||||||
SignerImprovedService.cleanup();
|
SignerService.cleanup();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
Logger.info('SIGINT received, shutting down gracefully');
|
Logger.info('SIGINT received, shutting down gracefully');
|
||||||
SignerImprovedService.cleanup();
|
SignerService.cleanup();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ process.on('uncaughtException', (error) => {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
SignerImprovedService.cleanup();
|
SignerService.cleanup();
|
||||||
process.exit(1);
|
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 { 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 {
|
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);
|
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();
|
* Get the signer client instance with connection validation
|
||||||
await client.connect();
|
*/
|
||||||
|
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