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