init
This commit is contained in:
commit
f8adee1aad
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
205
README.md
Normal file
205
README.md
Normal file
@ -0,0 +1,205 @@
|
||||
# SDK Signer Client
|
||||
|
||||
A TypeScript client library for connecting to the SDK Signer WebSocket API. This package provides a clean, type-safe interface for communicating with the SDK Signer server.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install sdk-signer-client
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { SDKSignerClient, MessageType } from 'sdk-signer-client';
|
||||
|
||||
// Create client instance
|
||||
const client = new SDKSignerClient({
|
||||
url: 'ws://localhost:9090',
|
||||
apiKey: 'your-api-key'
|
||||
});
|
||||
|
||||
// Connect to server
|
||||
await client.connect();
|
||||
|
||||
// Wait for server to send LISTENING message
|
||||
const response = await client.waitForListening();
|
||||
console.log('Server response:', response);
|
||||
|
||||
// Disconnect when done
|
||||
client.disconnect();
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```typescript
|
||||
import { ClientConfig } from 'sdk-signer-client';
|
||||
|
||||
const config: ClientConfig = {
|
||||
url: 'ws://localhost:9090', // WebSocket server URL
|
||||
apiKey: 'your-api-key', // API key for authentication
|
||||
timeout: 5000, // Connection timeout (ms)
|
||||
reconnectInterval: 3000, // Reconnection delay (ms)
|
||||
maxReconnectAttempts: 5 // Max reconnection attempts
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor
|
||||
|
||||
```typescript
|
||||
new SDKSignerClient(config: ClientConfig)
|
||||
```
|
||||
|
||||
Creates a new client instance with the specified configuration.
|
||||
|
||||
### Connection Methods
|
||||
|
||||
#### `connect(options?: ConnectionOptions): Promise<void>`
|
||||
|
||||
Establishes a WebSocket connection to the server.
|
||||
|
||||
```typescript
|
||||
await client.connect({
|
||||
timeout: 10000,
|
||||
headers: { 'Custom-Header': 'value' }
|
||||
});
|
||||
```
|
||||
|
||||
#### `disconnect(): void`
|
||||
|
||||
Closes the WebSocket connection and stops reconnection attempts.
|
||||
|
||||
#### `isConnectedToServer(): boolean`
|
||||
|
||||
Returns `true` if the client is connected to the server.
|
||||
|
||||
### Message Methods
|
||||
|
||||
#### `send(message: ClientMessage): void`
|
||||
|
||||
Sends a message to the server.
|
||||
|
||||
```typescript
|
||||
client.send({
|
||||
type: MessageType.LISTENING,
|
||||
messageId: 'unique-id'
|
||||
});
|
||||
```
|
||||
|
||||
#### `sendAndWait(message: ClientMessage, expectedType: MessageType, timeout?: number): Promise<ServerResponse>`
|
||||
|
||||
Sends a message and waits for a specific response type.
|
||||
|
||||
```typescript
|
||||
const response = await client.sendAndWait(
|
||||
{ type: MessageType.NOTIFY_UPDATE, processId: '123', stateId: '456' },
|
||||
MessageType.UPDATE_NOTIFIED,
|
||||
10000
|
||||
);
|
||||
```
|
||||
|
||||
### Convenience Methods
|
||||
|
||||
#### `listen(): Promise<ServerResponse>`
|
||||
|
||||
Sends a listening message to establish connection.
|
||||
|
||||
#### `notifyUpdate(processId: string, stateId: string): Promise<ServerResponse>`
|
||||
|
||||
Notifies an update for a process.
|
||||
|
||||
#### `validateState(processId: string, stateId: string): Promise<ServerResponse>`
|
||||
|
||||
Validates a state.
|
||||
|
||||
#### `updateProcess(processId: string, stateId: string, data: any): Promise<ServerResponse>`
|
||||
|
||||
Updates a process with additional data.
|
||||
|
||||
### Event Handling
|
||||
|
||||
```typescript
|
||||
// Connection events
|
||||
client.on('open', () => console.log('Connected!'));
|
||||
client.on('close', () => console.log('Disconnected!'));
|
||||
client.on('error', (error) => console.error('Error:', error));
|
||||
client.on('reconnect', () => console.log('Reconnected!'));
|
||||
|
||||
// Message events
|
||||
client.on('message', (response) => console.log('Received:', response));
|
||||
|
||||
// Remove event handlers
|
||||
client.off('open');
|
||||
```
|
||||
|
||||
## Message Types
|
||||
|
||||
The client supports all message types defined by the server:
|
||||
|
||||
- `LISTENING` - Establish connection
|
||||
- `NOTIFY_UPDATE` - Notify process update
|
||||
- `VALIDATE_STATE` - Validate process state
|
||||
- `UPDATE_PROCESS` - Update process data
|
||||
- `ERROR` - Error responses
|
||||
- And many more...
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await client.connect();
|
||||
const response = await client.notifyUpdate('process-123', 'state-456');
|
||||
console.log('Success:', response);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
|
||||
// Or use event handlers
|
||||
client.on('error', (error) => {
|
||||
console.error('Connection error:', error);
|
||||
});
|
||||
```
|
||||
|
||||
## Reconnection
|
||||
|
||||
The client automatically attempts to reconnect when the connection is lost:
|
||||
|
||||
- Exponential backoff with configurable intervals
|
||||
- Configurable maximum reconnection attempts
|
||||
- Automatic cleanup on manual disconnect
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Build the package
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development mode
|
||||
npm run dev
|
||||
|
||||
# Clean build artifacts
|
||||
npm run clean
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
This package is written in TypeScript and includes full type definitions. All interfaces and types are exported for use in your own TypeScript projects.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
73
examples/basic-usage.ts
Normal file
73
examples/basic-usage.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { SDKSignerClient, MessageType } from '../src/index';
|
||||
|
||||
async function basicExample() {
|
||||
// Use environment variables for configuration, with fallbacks
|
||||
const serverUrl = process.env.SERVER_URL || 'ws://localhost:9090';
|
||||
const apiKey = process.env.API_KEY || 'your-api-key-change-this';
|
||||
|
||||
console.log(`🔧 Testing against server: ${serverUrl}`);
|
||||
console.log(`🔑 Using API key: ${apiKey.substring(0, 10)}...`);
|
||||
console.log('');
|
||||
|
||||
// Create client instance
|
||||
const client = new SDKSignerClient({
|
||||
url: serverUrl,
|
||||
apiKey: apiKey
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
client.on('open', () => {
|
||||
console.log('✅ Connected to SDK Signer server');
|
||||
});
|
||||
|
||||
client.on('close', () => {
|
||||
console.log('🔌 Disconnected from server');
|
||||
});
|
||||
|
||||
client.on('error', (error: Error) => {
|
||||
console.error('❌ Error:', error.message);
|
||||
});
|
||||
|
||||
client.on('message', (response: any) => {
|
||||
console.log('📨 Received:', response);
|
||||
});
|
||||
|
||||
try {
|
||||
// Connect to server
|
||||
console.log('🔗 Connecting to server...');
|
||||
await client.connect();
|
||||
|
||||
// Wait for server to send LISTENING message
|
||||
console.log('👂 Waiting for server LISTENING message...');
|
||||
const listeningResponse = await client.waitForListening();
|
||||
console.log('✅ Server listening:', listeningResponse);
|
||||
|
||||
// Example: Notify an update
|
||||
console.log('📢 Notifying update...');
|
||||
const updateResponse = await client.notifyUpdate('process-123', 'state-456');
|
||||
console.log('✅ Update response:', updateResponse);
|
||||
|
||||
// Example: Validate a state
|
||||
console.log('✅ Validating state...');
|
||||
const validateResponse = await client.validateState('process-123', 'state-456');
|
||||
console.log('✅ Validation response:', validateResponse);
|
||||
|
||||
// Wait a bit before disconnecting
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in example:', error);
|
||||
process.exit(1); // Exit with error code for CI/CD
|
||||
} finally {
|
||||
// Disconnect
|
||||
console.log('🔌 Disconnecting...');
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
if (require.main === module) {
|
||||
basicExample().catch(console.error);
|
||||
}
|
||||
|
||||
export { basicExample };
|
15
jest.config.js
Normal file
15
jest.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
};
|
3741
package-lock.json
generated
Normal file
3741
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Executable file
41
package.json
Executable file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "sdk-signer-client",
|
||||
"version": "1.0.0",
|
||||
"description": "Client library for SDK Signer WebSocket API",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:integration": "ts-node examples/basic-usage.ts",
|
||||
"clean": "rm -rf dist",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"websocket",
|
||||
"client",
|
||||
"sdk",
|
||||
"signer",
|
||||
"api"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/ws": "^8.5.10",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"README.md"
|
||||
]
|
||||
}
|
105
src/__tests__/client.test.ts
Normal file
105
src/__tests__/client.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { SDKSignerClient, MessageType, ClientConfig } from '../index';
|
||||
|
||||
describe('SDKSignerClient', () => {
|
||||
let client: SDKSignerClient;
|
||||
const mockConfig: ClientConfig = {
|
||||
url: 'ws://localhost:9090',
|
||||
apiKey: 'test-api-key'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = new SDKSignerClient(mockConfig);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create client with default config', () => {
|
||||
const client = new SDKSignerClient({ url: 'ws://test', apiKey: 'key' });
|
||||
expect(client).toBeInstanceOf(SDKSignerClient);
|
||||
});
|
||||
|
||||
it('should merge custom config with defaults', () => {
|
||||
const customConfig: ClientConfig = {
|
||||
url: 'ws://test',
|
||||
apiKey: 'key',
|
||||
timeout: 10000,
|
||||
reconnectInterval: 5000,
|
||||
maxReconnectAttempts: 3
|
||||
};
|
||||
|
||||
const client = new SDKSignerClient(customConfig);
|
||||
expect(client).toBeInstanceOf(SDKSignerClient);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connection state', () => {
|
||||
it('should start disconnected', () => {
|
||||
expect(client.isConnectedToServer()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message generation', () => {
|
||||
it('should generate unique message IDs', () => {
|
||||
const client = new SDKSignerClient(mockConfig);
|
||||
|
||||
// Access private method for testing
|
||||
const generateId = (client as any).generateMessageId.bind(client);
|
||||
const id1 = generateId();
|
||||
const id2 = generateId();
|
||||
|
||||
expect(id1).not.toBe(id2);
|
||||
expect(id1).toMatch(/^msg_\d+_[a-z0-9]+$/);
|
||||
expect(id2).toMatch(/^msg_\d+_[a-z0-9]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('event handling', () => {
|
||||
it('should register and call event handlers', () => {
|
||||
const mockHandler = jest.fn();
|
||||
|
||||
client.on('open', mockHandler);
|
||||
expect(client).toBeDefined();
|
||||
|
||||
// Note: We can't easily test WebSocket events without a real server
|
||||
// This test just ensures the method exists and doesn't throw
|
||||
});
|
||||
|
||||
it('should remove event handlers', () => {
|
||||
const mockHandler = jest.fn();
|
||||
|
||||
client.on('open', mockHandler);
|
||||
client.off('open');
|
||||
|
||||
// Should not throw
|
||||
expect(client).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('message creation', () => {
|
||||
it('should create listening message', () => {
|
||||
const message = {
|
||||
type: MessageType.LISTENING,
|
||||
messageId: 'test-id'
|
||||
};
|
||||
|
||||
expect(message.type).toBe(MessageType.LISTENING);
|
||||
expect(message.messageId).toBe('test-id');
|
||||
});
|
||||
|
||||
it('should create notify update message', () => {
|
||||
const message = {
|
||||
type: MessageType.NOTIFY_UPDATE,
|
||||
processId: 'process-123',
|
||||
stateId: 'state-456',
|
||||
messageId: 'test-id'
|
||||
};
|
||||
|
||||
expect(message.type).toBe(MessageType.NOTIFY_UPDATE);
|
||||
expect(message.processId).toBe('process-123');
|
||||
expect(message.stateId).toBe('state-456');
|
||||
});
|
||||
});
|
||||
});
|
331
src/client.ts
Normal file
331
src/client.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import WebSocket from 'ws';
|
||||
import {
|
||||
ClientConfig,
|
||||
ClientMessage,
|
||||
ServerResponse,
|
||||
ClientEvents,
|
||||
MessageType,
|
||||
ConnectionOptions,
|
||||
MessageHandler,
|
||||
ConnectionHandler,
|
||||
ErrorHandler
|
||||
} from './types';
|
||||
|
||||
export class SDKSignerClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private config: ClientConfig;
|
||||
private events: ClientEvents = {};
|
||||
private reconnectAttempts = 0;
|
||||
private reconnectTimer: NodeJS.Timeout | null = null;
|
||||
private messageHandlers = new Map<string, MessageHandler>();
|
||||
private isConnecting = false;
|
||||
private isConnected = false;
|
||||
|
||||
constructor(config: ClientConfig) {
|
||||
this.config = {
|
||||
timeout: 5000,
|
||||
reconnectInterval: 3000,
|
||||
maxReconnectAttempts: 5,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the SDK Signer server
|
||||
*/
|
||||
async connect(options: ConnectionOptions = {}): Promise<void> {
|
||||
if (this.isConnecting || this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = options.timeout || this.config.timeout || 5000;
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.isConnecting = false;
|
||||
reject(new Error('Connection timeout'));
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.config.url, {
|
||||
headers: options.headers
|
||||
});
|
||||
|
||||
this.ws.on('open', () => {
|
||||
clearTimeout(timeoutId);
|
||||
this.isConnecting = false;
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
console.log('✅ Connected to SDK Signer server');
|
||||
|
||||
if (this.events.open) {
|
||||
this.events.open();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.ws.on('message', (data: WebSocket.Data) => {
|
||||
try {
|
||||
const response: ServerResponse = JSON.parse(data.toString());
|
||||
this.handleMessage(response);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse message:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('close', (code: number, reason: Buffer) => {
|
||||
this.isConnected = false;
|
||||
console.log(`🔌 Connection closed: ${code} - ${reason.toString()}`);
|
||||
|
||||
if (this.events.close) {
|
||||
this.events.close();
|
||||
}
|
||||
|
||||
// Attempt to reconnect if not manually closed
|
||||
if (code !== 1000) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('error', (error: Error) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.isConnecting = false;
|
||||
console.error('❌ WebSocket error:', error);
|
||||
|
||||
if (this.events.error) {
|
||||
this.events.error(error);
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
this.isConnecting = false;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the server
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
this.ws.close(1000, 'Client disconnect');
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
this.isConnected = false;
|
||||
this.reconnectAttempts = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the server
|
||||
*/
|
||||
send(message: ClientMessage): void {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket is not connected');
|
||||
}
|
||||
|
||||
// Add API key if not present
|
||||
if (!message.apiKey && this.config.apiKey) {
|
||||
message.apiKey = this.config.apiKey;
|
||||
}
|
||||
|
||||
// Generate message ID if not present
|
||||
if (!message.messageId) {
|
||||
message.messageId = this.generateMessageId();
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and wait for a specific response
|
||||
*/
|
||||
async sendAndWait(
|
||||
message: ClientMessage,
|
||||
expectedType: MessageType,
|
||||
timeout: number = 10000
|
||||
): Promise<ServerResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.messageHandlers.delete(message.messageId!);
|
||||
reject(new Error(`Timeout waiting for ${expectedType} response to message ${message.type} (${message.messageId})`));
|
||||
}, timeout);
|
||||
|
||||
const messageId = message.messageId || this.generateMessageId();
|
||||
message.messageId = messageId;
|
||||
|
||||
const handler = (response: ServerResponse) => {
|
||||
if (response.messageId === messageId) {
|
||||
clearTimeout(timeoutId);
|
||||
this.messageHandlers.delete(messageId);
|
||||
|
||||
if (response.type === MessageType.ERROR) {
|
||||
const errorMessage = response.error ?
|
||||
(typeof response.error === 'string' ? response.error : JSON.stringify(response.error)) :
|
||||
'Unknown server error';
|
||||
reject(new Error(`Server error for ${message.type}: ${errorMessage}`));
|
||||
} else if (response.type === expectedType) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(`Unexpected response type: ${response.type}, expected: ${expectedType}`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.messageHandlers.set(messageId, handler);
|
||||
this.send(message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set event handlers
|
||||
*/
|
||||
on(event: keyof ClientEvents, handler: ConnectionHandler | ErrorHandler | MessageHandler): void {
|
||||
this.events[event] = handler as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event handler
|
||||
*/
|
||||
off(event: keyof ClientEvents): void {
|
||||
delete this.events[event];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if client is connected
|
||||
*/
|
||||
isConnectedToServer(): boolean {
|
||||
return this.isConnected && this.ws?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for server to send LISTENING message (connection establishment)
|
||||
*/
|
||||
async waitForListening(): Promise<ServerResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timeout waiting for server LISTENING message'));
|
||||
}, this.config.timeout || 10000);
|
||||
|
||||
const handler = (response: ServerResponse) => {
|
||||
if (response.type === MessageType.LISTENING) {
|
||||
clearTimeout(timeout);
|
||||
this.off('message');
|
||||
resolve(response);
|
||||
}
|
||||
};
|
||||
|
||||
this.on('message', handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify an update for a process
|
||||
*/
|
||||
async notifyUpdate(processId: string, stateId: string): Promise<ServerResponse> {
|
||||
const message: ClientMessage = {
|
||||
type: MessageType.NOTIFY_UPDATE,
|
||||
processId,
|
||||
stateId,
|
||||
messageId: this.generateMessageId()
|
||||
};
|
||||
|
||||
return this.sendAndWait(message, MessageType.UPDATE_NOTIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a state
|
||||
*/
|
||||
async validateState(processId: string, stateId: string): Promise<ServerResponse> {
|
||||
const message: ClientMessage = {
|
||||
type: MessageType.VALIDATE_STATE,
|
||||
processId,
|
||||
stateId,
|
||||
messageId: this.generateMessageId()
|
||||
};
|
||||
|
||||
return this.sendAndWait(message, MessageType.STATE_VALIDATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a process
|
||||
*/
|
||||
async updateProcess(processId: string, stateId: string, data: any): Promise<ServerResponse> {
|
||||
const message: ClientMessage = {
|
||||
type: MessageType.UPDATE_PROCESS,
|
||||
processId,
|
||||
stateId,
|
||||
...data,
|
||||
messageId: this.generateMessageId()
|
||||
};
|
||||
|
||||
return this.sendAndWait(message, MessageType.PROCESS_UPDATED);
|
||||
}
|
||||
|
||||
private handleMessage(response: ServerResponse): void {
|
||||
// Call general message handler
|
||||
if (this.events.message) {
|
||||
this.events.message(response);
|
||||
}
|
||||
|
||||
// Call specific message handler if exists
|
||||
if (response.messageId) {
|
||||
const handler = this.messageHandlers.get(response.messageId);
|
||||
if (handler) {
|
||||
handler(response);
|
||||
this.messageHandlers.delete(response.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle error responses
|
||||
if (response.type === MessageType.ERROR) {
|
||||
const errorMessage = response.error ?
|
||||
(typeof response.error === 'string' ? response.error : JSON.stringify(response.error)) :
|
||||
'Unknown server error';
|
||||
|
||||
console.error('Server error:', errorMessage);
|
||||
console.error('Full error response:', JSON.stringify(response, null, 2));
|
||||
|
||||
if (this.events.error) {
|
||||
this.events.error(new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectAttempts >= (this.config.maxReconnectAttempts || 5)) {
|
||||
console.error('Max reconnection attempts reached');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
const delay = (this.config.reconnectInterval || 3000) * this.reconnectAttempts;
|
||||
|
||||
console.log(`🔄 Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
||||
|
||||
this.reconnectTimer = setTimeout(async () => {
|
||||
try {
|
||||
await this.connect();
|
||||
if (this.events.reconnect) {
|
||||
this.events.reconnect();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Reconnection failed:', error);
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private generateMessageId(): string {
|
||||
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
15
src/index.ts
Normal file
15
src/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Main entry point for SDK Signer Client
|
||||
export { SDKSignerClient } from './client';
|
||||
export * from './types';
|
||||
|
||||
// Re-export commonly used types for convenience
|
||||
export {
|
||||
MessageType,
|
||||
ClientConfig,
|
||||
ClientMessage,
|
||||
ServerResponse,
|
||||
ClientEvents,
|
||||
ProcessData,
|
||||
UpdateProcessData,
|
||||
ValidationResult
|
||||
} from './types';
|
97
src/types.ts
Normal file
97
src/types.ts
Normal file
@ -0,0 +1,97 @@
|
||||
// Client-side type definitions for SDK Signer API
|
||||
|
||||
export enum MessageType {
|
||||
// Establish connection and keep alive
|
||||
LISTENING = 'LISTENING',
|
||||
REQUEST_LINK = 'REQUEST_LINK',
|
||||
LINK_ACCEPTED = 'LINK_ACCEPTED',
|
||||
ERROR = 'ERROR',
|
||||
VALIDATE_TOKEN = 'VALIDATE_TOKEN',
|
||||
RENEW_TOKEN = 'RENEW_TOKEN',
|
||||
// Get various information
|
||||
GET_PAIRING_ID = 'GET_PAIRING_ID',
|
||||
GET_PROCESSES = 'GET_PROCESSES',
|
||||
GET_MY_PROCESSES = 'GET_MY_PROCESSES',
|
||||
PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED',
|
||||
RETRIEVE_DATA = 'RETRIEVE_DATA',
|
||||
DATA_RETRIEVED = 'DATA_RETRIEVED',
|
||||
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
|
||||
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
|
||||
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
|
||||
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
|
||||
// Processes
|
||||
CREATE_PROCESS = 'CREATE_PROCESS',
|
||||
PROCESS_CREATED = 'PROCESS_CREATED',
|
||||
UPDATE_PROCESS = 'UPDATE_PROCESS',
|
||||
PROCESS_UPDATED = 'PROCESS_UPDATED',
|
||||
NOTIFY_UPDATE = 'NOTIFY_UPDATE',
|
||||
UPDATE_NOTIFIED = 'UPDATE_NOTIFIED',
|
||||
VALIDATE_STATE = 'VALIDATE_STATE',
|
||||
STATE_VALIDATED = 'STATE_VALIDATED',
|
||||
// Hash and merkle proof
|
||||
HASH_VALUE = 'HASH_VALUE',
|
||||
VALUE_HASHED = 'VALUE_HASHED',
|
||||
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
|
||||
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED',
|
||||
VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF',
|
||||
MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED',
|
||||
// Account management
|
||||
ADD_DEVICE = 'ADD_DEVICE',
|
||||
DEVICE_ADDED = 'DEVICE_ADDED',
|
||||
}
|
||||
|
||||
export interface ClientMessage {
|
||||
type: MessageType;
|
||||
messageId?: string;
|
||||
apiKey?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ServerResponse {
|
||||
type: MessageType;
|
||||
messageId?: string;
|
||||
error?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ClientConfig {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
timeout?: number;
|
||||
reconnectInterval?: number;
|
||||
maxReconnectAttempts?: number;
|
||||
}
|
||||
|
||||
export interface ConnectionOptions {
|
||||
timeout?: number;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type MessageHandler = (response: ServerResponse) => void;
|
||||
export type ConnectionHandler = () => void;
|
||||
export type ErrorHandler = (error: Error) => void;
|
||||
|
||||
export interface ClientEvents {
|
||||
open?: ConnectionHandler;
|
||||
close?: ConnectionHandler;
|
||||
error?: ErrorHandler;
|
||||
message?: MessageHandler;
|
||||
reconnect?: ConnectionHandler;
|
||||
}
|
||||
|
||||
export interface ProcessData {
|
||||
processId: string;
|
||||
stateId: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface UpdateProcessData extends ProcessData {
|
||||
// Add specific fields for process updates
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: any;
|
||||
}
|
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"examples/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user