diff --git a/README.md b/README.md index 289d43fccb..6b5850cff1 100644 --- a/README.md +++ b/README.md @@ -1151,21 +1151,222 @@ process.on('SIGINT', async () => { ## 📚 API Reference -### Network Interface +### NetworkClient Interface -The core `Network` interface provides: +The `NetworkClient` interface is the main entry point for interacting with the Huly Virtual Network: -- `register(record, agent)`: Register an agent with the network -- `unregister(agentId)`: Unregister an agent from the network -- `ping(agentId | clientId)`: Mark an agent or client as alive -- `get(client, uuid, request)`: Get or create a container -- `list(kind)`: List containers of a specific kind -- `release(client, uuid)`: Release a container reference -- `request(target, operation, data)`: Send request to container -- `agents()`: Get all registered agents -- `kinds()`: Get all supported container kinds +- `register(agent)`: Register a NetworkAgent to be processed by the network +- `agents()`: Get all registered agents with their information +- `kinds()`: Get a full unique set of supported container kinds +- `get(kind, options)`: Get or start a container of the specified kind +- `list(kind?)`: List all containers, optionally filtered by kind +- `request(target, operation, data?)`: Send a request to a container using proxy connection +- `onUpdate(listener)`: Register a listener for network events (returns unsubscribe function) +- `waitConnection(timeout?)`: Wait for network connection (0 = wait indefinitely) +- `close()`: Close the client connection -### Agent Interface +### ClientWithAgents Interface + +The `ClientWithAgents` interface extends `NetworkClient` and is returned by `createNetworkClient()`. It adds agent hosting capabilities: + +- **All NetworkClient methods** (listed above) +- `serveAgent(endpointUrl, factory, statelessContainers?)`: Create and register an agent with container factories + +#### serveAgent Method + +The `serveAgent` method allows a client to act as both a client and an agent host: + +```typescript +serveAgent( + endpointUrl: string, + factory: Record, + statelessContainers?: StatelessContainersFactory +): Promise +``` + +**Parameters:** + +- `endpointUrl`: The endpoint URL for the agent (e.g., `'localhost:3738'`) +- `factory`: A record mapping container kinds to factory functions for creating containers on-demand +- `statelessContainers`: Optional factory for creating stateless containers that support high availability + +**Basic Usage Example:** + +```typescript +import { createNetworkClient, containerOnAgentEndpointRef } from '@hcengineering/network-client' +import type { Container, ContainerUuid, ClientUuid } from '@hcengineering/network-core' + +// 1. Create a container implementation +class MyServiceContainer implements Container { + constructor(readonly uuid: ContainerUuid) {} + + async request(operation: string, data?: any): Promise { + return { success: true, operation, data } + } + + async ping(): Promise {} + async terminate(): Promise {} + connect(clientId: ClientUuid, broadcast: (data: any) => Promise): void {} + disconnect(clientId: ClientUuid): void {} +} + +// 2. Connect and serve an agent +const client = createNetworkClient('localhost:3737') +await client.waitConnection(5000) + +await client.serveAgent('localhost:3738', { + 'my-service': async (options, agentEndpoint) => { + const uuid = options.uuid ?? (`container-${Date.now()}` as ContainerUuid) + const container = new MyServiceContainer(uuid) + return { + uuid, + container, + endpoint: containerOnAgentEndpointRef(agentEndpoint!, uuid) + } + } +}) + +// 3. Now this client can also request containers +const containerRef = await client.get('my-service' as any, {}) +const result = await containerRef.request('test', { value: 42 }) +await containerRef.close() +``` + +**High Availability Example with Stateless Containers:** + +```typescript +import { createNetworkClient, containerOnAgentEndpointRef } from '@hcengineering/network-client' +import type { Container, ContainerUuid, ContainerKind } from '@hcengineering/network-core' + +class LeaderServiceContainer implements Container { + constructor(readonly uuid: ContainerUuid, readonly instanceName: string) {} + + async request(operation: string, data?: any): Promise { + return { instance: this.instanceName, operation, data } + } + + async ping(): Promise {} + async terminate(): Promise {} + connect(clientId: any, broadcast: (data: any) => Promise): void {} + disconnect(clientId: any): void {} +} + +// Shared UUID for HA failover +const leaderUuid = 'service-leader-001' as ContainerUuid + +// Primary agent +const primaryClient = createNetworkClient('localhost:3737') +await primaryClient.waitConnection(5000) + +await primaryClient.serveAgent( + 'localhost:3801', + {}, // No on-demand factories + (agentEndpoint) => { + // Stateless container for automatic failover + const container = new LeaderServiceContainer(leaderUuid, 'Primary') + return [ + { + uuid: leaderUuid, + kind: 'leader-service' as ContainerKind, + endpoint: containerOnAgentEndpointRef(agentEndpoint, leaderUuid), + container + } + ] + } +) + +// Secondary agent (backup) +const secondaryClient = createNetworkClient('localhost:3737') +await secondaryClient.waitConnection(5000) + +await secondaryClient.serveAgent('localhost:3802', {}, (agentEndpoint) => { + const container = new LeaderServiceContainer(leaderUuid, 'Secondary') + return [ + { + uuid: leaderUuid, + kind: 'leader-service' as ContainerKind, + endpoint: containerOnAgentEndpointRef(agentEndpoint, leaderUuid), + container + } + ] +}) + +// Primary wins, Secondary automatically takes over if Primary fails +const leaderRef = await primaryClient.get('leader-service' as any, { uuid: leaderUuid }) +const result = await leaderRef.request('status') +console.log('Active leader:', result.instance) // "Primary" +``` + +**Multi-Tenant Example:** + +```typescript +class TenantContainer implements Container { + constructor(readonly uuid: ContainerUuid, readonly tenantId: string) {} + + async request(operation: string, data?: any): Promise { + return { tenantId: this.tenantId, operation, data } + } + + async ping(): Promise {} + async terminate(): Promise {} + connect(clientId: any, broadcast: (data: any) => Promise): void {} + disconnect(clientId: any): void {} +} + +const client = createNetworkClient('localhost:3737') +await client.waitConnection(5000) + +await client.serveAgent('localhost:3738', { + 'tenant-workspace': async (options, agentEndpoint) => { + const tenantId = options.labels?.[0] || 'default' + const uuid = options.uuid ?? (`workspace-${tenantId}` as ContainerUuid) + const container = new TenantContainer(uuid, tenantId) + return { + uuid, + container, + endpoint: containerOnAgentEndpointRef(agentEndpoint!, uuid) + } + } +}) + +// Request tenant-specific containers +const tenant1 = await client.get('tenant-workspace' as any, { labels: ['tenant-1'] }) +const tenant2 = await client.get('tenant-workspace' as any, { labels: ['tenant-2'] }) + +// Each tenant has isolated containers +await tenant1.request('getData') +await tenant2.request('getData') +``` + +**See Also:** + +- [Quick Start Example](#-getting-started) - Basic `serveAgent` usage +- [HA Stateless Containers Example](examples/ha-stateless-container-example.ts) - Full HA setup with `serveAgent` +- [Production Setup Example](examples/04-complete-production-setup.ts) - Complete production configuration + +### ContainerReference Interface + +The `ContainerReference` is returned by `client.get()` and represents a reference to a container: + +- `uuid`: The unique identifier of the container +- `endpoint`: The container's endpoint reference +- `request(operation, data?)`: Send a request to the container +- `connect()`: Establish a direct connection to the container (returns `ContainerConnection`) +- `cast(interfaceName?)`: Create a typed proxy for the container +- `close()`: Release the container reference +- `onEndpointUpdate?()`: Optional callback when container endpoint changes + +### ContainerConnection Interface + +The `ContainerConnection` is returned by `containerRef.connect()` for direct bidirectional communication: + +- `containerId`: The UUID of the connected container +- `request(operation, data?)`: Send a request to the container +- `cast(interfaceName?)`: Create a typed proxy for the connection +- `on?(data)`: Optional handler for receiving notifications from the container +- `close()`: Close the connection + +### NetworkAgent Interface The `NetworkAgent` interface defines: @@ -1187,16 +1388,6 @@ The `Container` interface includes: - `ping()`: Health check response - Optional: `onTerminated()`: Cleanup callback -### TickManager Interface - -The `TickManager` handles time-based operations: - -- `now()`: Get current timestamp -- `register(handler, interval)`: Register periodic handler (interval in seconds) -- `start()`: Start the tick manager -- `stop()`: Stop the tick manager -- `waitTick(ticks)`: Wait for specific number of ticks - ## 🏗️ Building Applications ### Container Types diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index f5593181a6..36a1e3a401 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -106,8 +106,8 @@ importers: version: 8.3.2 devDependencies: '@hcengineering/platform-rig': - specifier: ^0.7.4 - version: 0.7.4(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) + specifier: ^0.7.8 + version: 0.7.8(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) '@types/jest': specifier: ^29.5.5 version: 29.5.14 @@ -984,19 +984,6 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@hcengineering/platform-rig@0.7.4': - resolution: {integrity: sha512-NiUaWYDfXN5UEzReOfRjiGnRNYxU1N/QL0VMCLGjKUcogrMMSdMfVZDt1IdTXrgAllMyim57lXMaeX8vR38ZoQ==} - hasBin: true - peerDependencies: - '@typescript-eslint/eslint-plugin': ^6.11.0 - '@typescript-eslint/parser': ^6.11.0 - eslint: ^8.54.0 - eslint-config-standard-with-typescript: ^40.0.0 - eslint-plugin-import: ^2.26.0 - eslint-plugin-n: ^15.4.0 - eslint-plugin-promise: ^6.1.1 - prettier: ^3.0.0 - '@hcengineering/platform-rig@0.7.8': resolution: {integrity: sha512-w19uz2e+pCsn6nuDGvpw5Gl33foVMYo6FaHRL1icVDAxaA9RMRE3SNYuroxOv4XzjNu/mhFo9XU6QuJB2ta1Xg==} hasBin: true @@ -3337,19 +3324,6 @@ snapshots: '@eslint/js@8.57.1': {} - '@hcengineering/platform-rig@0.7.4(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2)': - dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.2) - esbuild: 0.25.9 - esbuild-plugin-copy: 2.1.1(esbuild@0.25.9) - eslint: 8.57.1 - eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.2) - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) - prettier: 3.6.2 - '@hcengineering/platform-rig@0.7.8(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-config-standard-with-typescript@40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.2))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2)': dependencies: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)