From a61ba164247086d007fea9af6672fd1ec0e46b8c Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 1 Oct 2025 10:08:28 +0700 Subject: [PATCH] Minor updates --- README.md | 155 +++++++++++++++++++-------------------------- examples/README.md | 36 ++++++++++- 2 files changed, 98 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 3cb42f2bab..acd0f46751 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ async function main() { // 3. Create and start an agent const agent = new AgentImpl('agent-1' as any, { 'my-service': async (options) => { - const uuid = options.uuid ?? ('container-' + Date.now()) as ContainerUuid + const uuid = options.uuid ?? (('container-' + Date.now()) as ContainerUuid) const container = new MyServiceContainer(uuid) return { uuid, @@ -377,10 +377,10 @@ class ChatRoomContainer implements Container { case 'sendMessage': const message = `${data.username}: ${data.text}` this.messages.push(message) - + // Broadcast to all connected clients await this.broadcast({ type: 'newMessage', message, timestamp: Date.now() }) - + return { success: true, messageId: this.messages.length - 1 } case 'getHistory': @@ -405,7 +405,7 @@ class ChatRoomContainer implements Container { connect(clientId: ClientUuid, broadcast: (data: any) => Promise): void { console.log(`Client ${clientId} connected to ${this.roomName}`) this.clients.set(clientId, broadcast) - + // Send welcome message to new client broadcast({ type: 'welcome', message: `Welcome to ${this.roomName}!` }).catch(console.error) } @@ -416,15 +416,15 @@ class ChatRoomContainer implements Container { } private async broadcast(event: any): Promise { - const promises = Array.from(this.clients.values()).map(handler => - handler(event).catch(err => console.error('Broadcast error:', err)) + const promises = Array.from(this.clients.values()).map((handler) => + handler(event).catch((err) => console.error('Broadcast error:', err)) ) await Promise.all(promises) } } // Usage -const chatRef = await client.get('chat-room' as any, { +const chatRef = await client.get('chat-room' as any, { uuid: 'room-general' as any, labels: ['public', 'general'] }) @@ -436,9 +436,9 @@ connection.on = async (event) => { } // Send a message -await connection.request('sendMessage', { - username: 'Alice', - text: 'Hello everyone!' +await connection.request('sendMessage', { + username: 'Alice', + text: 'Hello everyone!' }) // Later, disconnect @@ -458,16 +458,13 @@ import type { Container, ContainerUuid, ClientUuid, ContainerKind } from '@hceng class LeaderServiceContainer implements Container { private isActive = false - constructor( - readonly uuid: ContainerUuid, - readonly instanceName: string - ) {} + constructor(readonly uuid: ContainerUuid, readonly instanceName: string) {} async request(operation: string, data?: any): Promise { switch (operation) { case 'status': - return { - uuid: this.uuid, + return { + uuid: this.uuid, instance: this.instanceName, active: this.isActive, timestamp: Date.now() @@ -501,12 +498,7 @@ class LeaderServiceContainer implements Container { disconnect(clientId: ClientUuid): void {} } -async function createHAAgent( - agentId: string, - instanceName: string, - sharedUuid: ContainerUuid, - port: number -) { +async function createHAAgent(agentId: string, instanceName: string, sharedUuid: ContainerUuid, port: number) { const tickManager = new TickManagerImpl(1) const agent = new AgentImpl(agentId as any, {}) @@ -547,7 +539,8 @@ async function runHAExample() { // Monitor failover events client.onUpdate(async (event) => { for (const container of event.containers) { - if (container.event === 2) { // NetworkEventKind.removed + if (container.event === 2) { + // NetworkEventKind.removed console.log(`Container removed: ${container.container.uuid}`) console.log('Failover should occur automatically...') } @@ -562,13 +555,13 @@ async function runHAExample() { for (let i = 0; i < 3; i++) { const result = await leaderRef.request('processTask', { taskId: i, data: 'test' }) console.log('Task result:', result) - await new Promise(resolve => setTimeout(resolve, 500)) + await new Promise((resolve) => setTimeout(resolve, 500)) } // Simulate primary failure console.log('\n=== Simulating Primary Failure ===') await primary.agent.terminate(leaderUuid) - await new Promise(resolve => setTimeout(resolve, 2000)) // Wait for failover + await new Promise((resolve) => setTimeout(resolve, 2000)) // Wait for failover // Secondary should take over const newLeaderRef = await client.get('leader-service' as any, { uuid: leaderUuid }) @@ -599,10 +592,7 @@ class TenantWorkspaceContainer implements Container { private users = new Set() private documents = new Map() - constructor( - readonly uuid: ContainerUuid, - readonly tenantId: string - ) { + constructor(readonly uuid: ContainerUuid, readonly tenantId: string) { console.log(`Workspace created for tenant: ${tenantId}`) } @@ -623,8 +613,8 @@ class TenantWorkspaceContainer implements Container { return { success: true, document: doc } case 'listDocuments': - return { - success: true, + return { + success: true, documents: Array.from(this.documents.entries()).map(([id, doc]) => ({ id, ...doc })) } @@ -657,7 +647,7 @@ class TenantWorkspaceContainer implements Container { const agent = new AgentImpl('workspace-agent' as any, { 'tenant-workspace': async (options: GetOptions) => { const tenantId = options.labels?.[0] || 'default' - const uuid = options.uuid ?? `workspace-${tenantId}-${Date.now()}` as ContainerUuid + const uuid = options.uuid ?? (`workspace-${tenantId}-${Date.now()}` as ContainerUuid) const container = new TenantWorkspaceContainer(uuid, tenantId) return { uuid, @@ -678,9 +668,9 @@ async function getTenantWorkspace(client: any, tenantId: string) { // Example usage const tenant1Workspace = await getTenantWorkspace(client, 'tenant-acme') await tenant1Workspace.request('addUser', { userId: 'user-1' }) -await tenant1Workspace.request('createDocument', { - title: 'Q1 Report', - content: 'Financial data...' +await tenant1Workspace.request('createDocument', { + title: 'Q1 Report', + content: 'Financial data...' }) const tenant2Workspace = await getTenantWorkspace(client, 'tenant-globex') @@ -715,27 +705,27 @@ const customClient = createNetworkClient('localhost:3737', 30) async function developmentWorkflow() { await devClient.waitConnection(10000) // 10 second connection timeout - + // Container will stay alive for 1 hour even without activity // Perfect for debugging and stepping through code const containerRef = await devClient.get('debug-service' as any, {}) - + // ... debug your code without worrying about timeouts - + await containerRef.close() await devClient.close() } async function productionWorkflow() { await prodClient.waitConnection(5000) // 5 second connection timeout - + // Container will be cleaned up after 3 seconds of inactivity // Ensures resources are freed quickly in production const containerRef = await prodClient.get('prod-service' as any, {}) - + // Do work... const result = await containerRef.request('process', { data: 'important' }) - + await containerRef.close() await prodClient.close() } @@ -744,7 +734,7 @@ async function productionWorkflow() { function createClientForEnvironment(networkHost: string) { const isDevelopment = process.env.NODE_ENV === 'development' const timeout = isDevelopment ? 3600 : 3 // 1 hour vs 3 seconds - + return createNetworkClient(networkHost, timeout) } @@ -801,14 +791,14 @@ import type { NetworkEvent, NetworkEventKind } from '@hcengineering/network-core // Monitor all network events const unsubscribe = client.onUpdate(async (event: NetworkEvent) => { console.log('=== Network Event ===') - + // Agent events for (const agentEvent of event.agents) { const eventType = ['added', 'updated', 'removed'][agentEvent.event] console.log(`Agent ${agentEvent.id}: ${eventType}`) console.log(' Supports kinds:', agentEvent.kinds) } - + // Container events for (const containerEvent of event.containers) { const eventType = ['added', 'updated', 'removed'][containerEvent.event] @@ -816,8 +806,9 @@ const unsubscribe = client.onUpdate(async (event: NetworkEvent) => { console.log(' Kind:', containerEvent.container.kind) console.log(' Agent:', containerEvent.container.agentId) console.log(' Labels:', containerEvent.container.labels) - - if (containerEvent.event === 2) { // NetworkEventKind.removed + + if (containerEvent.event === 2) { + // NetworkEventKind.removed console.log(' Container was removed - may trigger failover') } } @@ -837,21 +828,16 @@ unsubscribe() This example shows proper error handling patterns: ```typescript -async function robustContainerAccess( - client: any, - kind: string, - options: any, - maxRetries = 3 -) { +async function robustContainerAccess(client: any, kind: string, options: any, maxRetries = 3) { let lastError: Error | undefined - + for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`Attempt ${attempt}/${maxRetries}...`) - + // Try to get container const containerRef = await client.get(kind, options) - + try { // Try to send request const result = await containerRef.request('process', { attempt }) @@ -860,37 +846,33 @@ async function robustContainerAccess( } catch (requestError: any) { console.error('Request failed:', requestError.message) lastError = requestError - + // Release the failed container await containerRef.close().catch(() => {}) - + // Wait before retry with exponential backoff if (attempt < maxRetries) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000) console.log(`Waiting ${delay}ms before retry...`) - await new Promise(resolve => setTimeout(resolve, delay)) + await new Promise((resolve) => setTimeout(resolve, delay)) } } } catch (getError: any) { console.error('Failed to get container:', getError.message) lastError = getError - + if (attempt < maxRetries) { - await new Promise(resolve => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 1000)) } } } - + throw new Error(`Failed after ${maxRetries} attempts: ${lastError?.message}`) } // Usage try { - const result = await robustContainerAccess( - client, - 'unreliable-service' as any, - { labels: ['production'] } - ) + const result = await robustContainerAccess(client, 'unreliable-service' as any, { labels: ['production'] }) console.log('Final result:', result) } catch (error: any) { console.error('All attempts failed:', error.message) @@ -912,10 +894,7 @@ class ProductionContainer implements Container { private connections = new Map Promise>() private shutdownRequested = false - constructor( - readonly uuid: ContainerUuid, - private readonly config: any - ) { + constructor(readonly uuid: ContainerUuid, private readonly config: any) { console.log(`[${uuid}] Container started`) } @@ -943,16 +922,16 @@ class ProductionContainer implements Container { async terminate(): Promise { if (this.shutdownRequested) return - + this.shutdownRequested = true console.log(`[${this.uuid}] Terminating...`) - + // Notify all connected clients await this.broadcastShutdown() - + // Cleanup resources this.connections.clear() - + console.log(`[${this.uuid}] Terminated`) } @@ -967,9 +946,10 @@ class ProductionContainer implements Container { } private async broadcastShutdown(): Promise { - const promises = Array.from(this.connections.values()).map(handler => - handler({ type: 'shutdown', message: 'Container is terminating' }) - .catch(err => console.error('Failed to notify client:', err)) + const promises = Array.from(this.connections.values()).map((handler) => + handler({ type: 'shutdown', message: 'Container is terminating' }).catch((err) => + console.error('Failed to notify client:', err) + ) ) await Promise.all(promises) } @@ -979,11 +959,11 @@ async function startProductionSystem() { // 1. Start network server const tickManager = new TickManagerImpl(1000) tickManager.start() - + const network = new NetworkImpl(tickManager) const server = new NetworkServer( - network, - tickManager, + network, + tickManager, '*', // Bind to all interfaces 3737 ) @@ -994,7 +974,7 @@ async function startProductionSystem() { for (let i = 1; i <= 3; i++) { const agent = new AgentImpl(`agent-${i}` as any, { 'production-service': async (options) => { - const uuid = options.uuid ?? `svc-${Date.now()}-${i}` as ContainerUuid + const uuid = options.uuid ?? (`svc-${Date.now()}-${i}` as ContainerUuid) const container = new ProductionContainer(uuid, { agentId: i }) return { uuid, @@ -1004,12 +984,7 @@ async function startProductionSystem() { } }) - const agentServer = new NetworkAgentServer( - tickManager, - 'localhost', - '*', - 3738 + i - ) + const agentServer = new NetworkAgentServer(tickManager, 'localhost', '*', 3738 + i) await agentServer.start(agent) agents.push({ agent, server: agentServer }) console.log(`✓ Agent ${i} started on port ${3738 + i}`) @@ -1029,8 +1004,8 @@ async function startProductionSystem() { // 5. Setup monitoring client.onUpdate(async (event) => { // Log events for monitoring/alerting - event.agents.forEach(a => console.log(`Agent event: ${a.id}`)) - event.containers.forEach(c => console.log(`Container event: ${c.container.uuid}`)) + event.agents.forEach((a) => console.log(`Agent event: ${a.id}`)) + event.containers.forEach((c) => console.log(`Container event: ${c.container.uuid}`)) }) console.log('✓ Production system ready') @@ -1233,5 +1208,3 @@ This project is licensed under the Eclipse Public License 2.0 - see the [LICENSE --- **Note**: This is a foundational networking library for the Huly ecosystem. For application-level documentation, please refer to the main Huly platform repository. - - diff --git a/examples/README.md b/examples/README.md index a2835cde42..3733d9d58a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,15 +7,17 @@ This directory contains comprehensive examples demonstrating various aspects of Before running any example, make sure you have: 1. Built the project: + ```bash node common/scripts/install-run-rush.js build ``` 2. Either start a standalone network server: + ```bash cd pods/network-pod && rushx dev ``` - + Or let the example start its own network server (most examples do this). 3. Run an example: @@ -26,9 +28,11 @@ Before running any example, make sure you have: ## Examples ### 01 - Basic Container with Request/Response + **File**: `01-basic-container-request-response.ts` Learn the fundamentals of creating containers that handle various operations. This example shows: + - Container implementation basics - Request/response pattern - State management within containers @@ -39,9 +43,11 @@ Learn the fundamentals of creating containers that handle various operations. Th --- ### 02 - Event Broadcasting + **File**: `02-event-broadcasting.ts` Demonstrates real-time event broadcasting to multiple connected clients. Features: + - Multiple clients connecting to the same container - Broadcasting events to all connected clients - Chat room implementation @@ -52,9 +58,11 @@ Demonstrates real-time event broadcasting to multiple connected clients. Feature --- ### 03 - Multi-Tenant Container Management + **File**: `03-multi-tenant.ts` Shows how to build multi-tenant applications with isolated workspaces. Includes: + - Per-tenant container isolation - Label-based container selection - Tenant-specific data management @@ -65,9 +73,11 @@ Shows how to build multi-tenant applications with isolated workspaces. Includes: --- ### 04 - Complete Production Setup + **File**: `04-complete-production-setup.ts` A comprehensive production-ready example with: + - Multiple redundant agents for high availability - Health monitoring and metrics collection - Event monitoring for observability @@ -80,9 +90,11 @@ A comprehensive production-ready example with: --- ### 05 - Error Handling and Retry Logic + **File**: `05-error-handling-retry.ts` Advanced error handling patterns including: + - Exponential backoff retry logic - Timeout handling - Graceful degradation with fallbacks @@ -94,9 +106,11 @@ Advanced error handling patterns including: --- ### Custom Timeout Example + **File**: `custom-timeout-example.ts` Shows how to configure different timeouts for various environments: + - Development vs production timeouts - Connection timeout configuration - Keep-alive timeout settings @@ -106,9 +120,11 @@ Shows how to configure different timeouts for various environments: --- ### High Availability (HA) Stateless Container Example + **File**: `ha-stateless-container-example.ts` Demonstrates automatic failover with stateless containers: + - Multiple agents competing for the same container UUID - Automatic leader election - Failover when primary fails @@ -119,17 +135,23 @@ Demonstrates automatic failover with stateless containers: ## Example Categories ### For Beginners + Start with these examples to learn the basics: + 1. `01-basic-container-request-response.ts` 2. `custom-timeout-example.ts` ### For Application Developers + Build real applications with these patterns: + 1. `02-event-broadcasting.ts` - Real-time features 2. `03-multi-tenant.ts` - SaaS applications ### For DevOps/Production + Production-ready patterns: + 1. `04-complete-production-setup.ts` - Full production setup 2. `05-error-handling-retry.ts` - Robust error handling 3. `ha-stateless-container-example.ts` - High availability @@ -137,18 +159,23 @@ Production-ready patterns: ## Running Examples ### Run a single example: + ```bash npx ts-node examples/01-basic-container-request-response.ts ``` ### Run with custom network host: + Most examples connect to `localhost:3737` by default. To use a different host: + ```typescript const client = createNetworkClient('your-host:3737') ``` ### Debug mode: + Run with longer timeouts for debugging: + ```bash NODE_ENV=development npx ts-node examples/01-basic-container-request-response.ts ``` @@ -174,7 +201,7 @@ class MyContainer implements Container { async ping(): Promise {} async terminate(): Promise {} - + connect(clientId: ClientUuid, broadcast: (data: any) => Promise): void {} disconnect(clientId: ClientUuid): void {} } @@ -220,21 +247,25 @@ await server.start(agent) ## Troubleshooting ### Connection refused + - Ensure the network server is running on the specified port - Check firewall settings - Verify the correct host and port are being used ### Container not found + - Ensure the agent is registered with the network - Check that the container kind matches what the agent supports - Verify the agent is still alive (check agent health) ### Timeout errors + - Increase timeout values for debugging: `createNetworkClient('host:port', 3600)` - Check network connectivity - Verify services are responding to ping/health checks ### Type errors in examples + The examples use dynamic container kinds (strings) which may show TypeScript errors. This is expected and doesn't affect functionality. In production, define proper types: ```typescript @@ -260,6 +291,7 @@ To contribute a new example: 6. Submit a pull request Example template structure: + - Header comment with description and usage - Imports - Container implementation (if applicable)