From 3652896a4097c44b25d27702c2fd2ae9f55fd110 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Mon, 25 Aug 2025 01:09:11 +0700 Subject: [PATCH] Huly Network Signed-off-by: Andrey Sobolev --- .github/workflows/main.yml | 1 + README.md | 18 +- common/config/rush/pnpm-lock.yaml | 282 ++++-- communication | 2 +- network/README.md | 145 +++ network/core/.eslintrc.js | 7 + network/core/config/rig.json | 5 + network/core/docs/alive-checkins.md | 0 network/core/jest.config.js | 7 + network/core/package.json | 53 + .../core/src/__test__/alive-checkins.spec.ts | 44 + network/core/src/__test__/network.spec.ts | 72 ++ network/core/src/__test__/samples.ts | 27 + network/core/src/__test__/tickMgr.spec.ts | 28 + network/core/src/api/discovery.ts | 11 + network/core/src/api/net.ts | 192 ++++ network/core/src/api/timeouts.ts | 4 + network/core/src/api/types.ts | 9 + network/core/src/api/utils.ts | 14 + network/core/src/discovery/static.ts | 14 + network/core/src/index.ts | 8 + network/core/src/net/agent.ts | 114 +++ network/core/src/net/containers.ts | 24 + network/core/src/net/index.ts | 9 + network/core/src/net/network.ts | 432 ++++++++ network/core/src/node/node.ts | 291 ++++++ network/core/src/node/session.ts | 142 +++ network/core/src/utils.ts | 88 ++ network/core/tsconfig.json | 12 + network/docs/Schema.png | Bin 0 -> 214102 bytes network/docs/api-reference.md | 950 ++++++++++++++++++ network/docs/readme.md | 208 ++++ network/todo.md | 22 + network/zeromq/.eslintrc.js | 7 + network/zeromq/.npmignore | 4 + network/zeromq/config/rig.json | 5 + network/zeromq/jest.config.js | 7 + network/zeromq/package.json | 42 + network/zeromq/src/__test__/backrpc.spec.ts | 104 ++ network/zeromq/src/__test__/network.spec.ts | 189 ++++ network/zeromq/src/__test__/samples.ts | 27 + network/zeromq/src/__test__/zmq.spec.ts | 237 +++++ network/zeromq/src/agent.ts | 209 ++++ network/zeromq/src/backrpc.ts | 389 +++++++ network/zeromq/src/client.ts | 276 +++++ network/zeromq/src/endpoints.ts | 60 ++ network/zeromq/src/index.ts | 3 + network/zeromq/src/server.ts | 144 +++ network/zeromq/src/types.ts | 16 + network/zeromq/tsconfig.json | 12 + rush.json | 9 + 51 files changed, 4883 insertions(+), 92 deletions(-) create mode 100644 network/README.md create mode 100644 network/core/.eslintrc.js create mode 100644 network/core/config/rig.json create mode 100644 network/core/docs/alive-checkins.md create mode 100644 network/core/jest.config.js create mode 100644 network/core/package.json create mode 100644 network/core/src/__test__/alive-checkins.spec.ts create mode 100644 network/core/src/__test__/network.spec.ts create mode 100644 network/core/src/__test__/samples.ts create mode 100644 network/core/src/__test__/tickMgr.spec.ts create mode 100644 network/core/src/api/discovery.ts create mode 100644 network/core/src/api/net.ts create mode 100644 network/core/src/api/timeouts.ts create mode 100644 network/core/src/api/types.ts create mode 100644 network/core/src/api/utils.ts create mode 100644 network/core/src/discovery/static.ts create mode 100644 network/core/src/index.ts create mode 100644 network/core/src/net/agent.ts create mode 100644 network/core/src/net/containers.ts create mode 100644 network/core/src/net/index.ts create mode 100644 network/core/src/net/network.ts create mode 100644 network/core/src/node/node.ts create mode 100644 network/core/src/node/session.ts create mode 100644 network/core/src/utils.ts create mode 100644 network/core/tsconfig.json create mode 100644 network/docs/Schema.png create mode 100644 network/docs/api-reference.md create mode 100644 network/docs/readme.md create mode 100644 network/todo.md create mode 100644 network/zeromq/.eslintrc.js create mode 100644 network/zeromq/.npmignore create mode 100644 network/zeromq/config/rig.json create mode 100644 network/zeromq/jest.config.js create mode 100644 network/zeromq/package.json create mode 100644 network/zeromq/src/__test__/backrpc.spec.ts create mode 100644 network/zeromq/src/__test__/network.spec.ts create mode 100644 network/zeromq/src/__test__/samples.ts create mode 100644 network/zeromq/src/__test__/zmq.spec.ts create mode 100644 network/zeromq/src/agent.ts create mode 100644 network/zeromq/src/backrpc.ts create mode 100644 network/zeromq/src/client.ts create mode 100644 network/zeromq/src/endpoints.ts create mode 100644 network/zeromq/src/index.ts create mode 100644 network/zeromq/src/server.ts create mode 100644 network/zeromq/src/types.ts create mode 100644 network/zeromq/tsconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70ec96b9db1..7ea3c1bfbae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,6 +23,7 @@ on: env: CacheFolders: | communication + network common desktop desktop-package diff --git a/README.md b/README.md index 4dd9d922179..a7fe1b0a4d5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,17 @@ If you want to interact with Huly programmatically, check out our [API Client](. You can find API usage examples in the [Huly examples](https://github.com/hcengineering/huly-examples) repository. +## Huly Virtual Network + +The platform features a distributed network architecture that enables scalable, fault-tolerant communication between accounts, workspaces, and nodes. The [Huly Virtual Network](./network/README.md) provides: + +- **Distributed Load Balancing**: Intelligent routing across multiple nodes using consistent hashing +- **Multi-Tenant Architecture**: Secure workspace isolation with role-based access control +- **Fault Tolerance**: Automatic failover and recovery mechanisms +- **Real-time Communication**: Event-driven architecture with broadcast capabilities + +For detailed information about the network architecture, deployment, and API reference, see the [Network Documentation](./network/README.md). + ## Table of Contents - [Huly Platform](#huly-platform) @@ -35,6 +46,7 @@ You can find API usage examples in the [Huly examples](https://github.com/hcengi - [Self-Hosting](#self-hosting) - [Activity](#activity) - [API Client](#api-client) + - [Huly Virtual Network](#huly-virtual-network) - [Table of Contents](#table-of-contents) - [Pre-requisites](#pre-requisites) - [Verification](#verification) @@ -106,6 +118,7 @@ This project uses GitHub Packages for dependency management. To successfully dow Follow these steps: 1. Generate a GitHub Token: + - Log in to your GitHub account - Go to **Settings** > **Developer settings** > **Personal access tokens** (https://github.com/settings/personal-access-tokens) - Click **Generate new token** @@ -113,13 +126,13 @@ Follow these steps: - Generate the token and copy it 2. Authenticate with npm: + ```bash npm login --registry=https://npm.pkg.github.com ``` When prompted, enter your GitHub username, use the generated token as your password - ## Fast start ```bash @@ -280,6 +293,7 @@ This guide describes the nuances of building and running the application from so #### Disk Space Requirements Ensure you have sufficient disk space available: + - A fully deployed local application in clean Docker will consume slightly more than **35 GB** of WSL virtual disk space - The application folder after build (sources + artifacts) will occupy **4.5 GB** @@ -303,6 +317,7 @@ Make sure Docker is accessible from WSL: Windows Git often automatically replaces line endings. Since most build scripts are `.sh` files, ensure your Windows checkout doesn't break them. **Solution options:** + - Checkout from WSL instead of Windows - Configure Git on Windows to disable auto-replacement: ```bash @@ -343,6 +358,7 @@ After these preparations, the build instructions should work without issues. When starting the application (`rush docker:up`), some network ports in Windows might be occupied. You can fix port mapping in the `\dev\docker-compose.yaml` file. **Important:** Depending on which port you change, you'll need to: + 1. Find what's using that port 2. Update the new address in the corresponding service configuration diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 7ee56d3b07e..99981d55e7c 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -835,6 +835,12 @@ importers: '@rush-temp/mongo': specifier: file:./projects/mongo.tgz version: file:projects/mongo.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + '@rush-temp/network': + specifier: file:./projects/network.tgz + version: file:projects/network.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + '@rush-temp/network-zeromq': + specifier: file:./projects/network-zeromq.tgz + version: file:projects/network-zeromq.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/notification': specifier: file:./projects/notification.tgz version: file:projects/notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -2389,6 +2395,9 @@ importers: yjs: specifier: ^13.6.23 version: 13.6.23 + zeromq: + specifier: ^6.5.0 + version: 6.5.0 zod: specifier: ^3.22.4 version: 3.24.2 @@ -5444,6 +5453,14 @@ packages: resolution: {integrity: sha512-2NrKTsPik2KN7fUtLuhOmZFyaailUgcbTQKLiXEvmcNdiCBYdpopLuCl7wU9l1MffhKvekpCb05WoB4os4ZSdA==, tarball: file:projects/mongo.tgz} version: 0.0.0 + '@rush-temp/network-zeromq@file:projects/network-zeromq.tgz': + resolution: {integrity: sha512-hXbmbcq2y/BwdXvrOeP5hpZYRqssUggoqp4cRGU5rFZRLRbQLDDU9k3AkCVi385ijUkp0EpsaDhzyEwgY4pXTw==, tarball: file:projects/network-zeromq.tgz} + version: 0.0.0 + + '@rush-temp/network@file:projects/network.tgz': + resolution: {integrity: sha512-NZhhGCoxzJydJ7hjYD8QJFl/iQPt6Vj6hrRsTsKBBO2iS25HRxjxZ1CTTTkbSU/3ZzKBZ8qrlJZuc4LIsyezCA==, tarball: file:projects/network.tgz} + version: 0.0.0 + '@rush-temp/notification-assets@file:projects/notification-assets.tgz': resolution: {integrity: sha512-vQTl0ZJNng9Y+dSKnBRwSbf1c6zVJp2xTUCyH0pB4DiJcco0GiHxu0vS1eVRM6X1sgCQ9w7bxX8S6Wyvp9IcOw==, tarball: file:projects/notification-assets.tgz} version: 0.0.0 @@ -8383,6 +8400,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cmake-ts@1.0.2: + resolution: {integrity: sha512-5l++JHE7MxFuyV/OwJf3ek7ZZN1aGPFPM5oUz6AnK5inQAPe4TFXRMz5sA2qg2FRgByPWdqO+gSfIPo8GzoKNQ==} + hasBin: true + co-body@6.1.0: resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} @@ -9883,6 +9904,10 @@ packages: resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} engines: {node: '>= 0.4.0'} + fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -11901,6 +11926,10 @@ packages: node-addon-api@1.7.2: resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + node-addon-api@8.5.0: + resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} + engines: {node: ^18 || ^20 || >= 21} + node-api-version@0.2.0: resolution: {integrity: sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==} @@ -14668,6 +14697,10 @@ packages: zen-observable@0.8.15: resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + zeromq@6.5.0: + resolution: {integrity: sha512-vWOrt19lvcXTxu5tiHXfEGQuldSlU+qZn2TT+4EbRQzaciWGwNZ99QQTolQOmcwVgZLodv+1QfC6UZs2PX/6pQ==} + engines: {node: '>= 12'} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -14731,17 +14764,17 @@ snapshots: '@aws-sdk/types': 3.734.0 '@aws-sdk/util-locate-window': 3.568.0 '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 '@aws-sdk/types': 3.734.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-crypto/supports-web-crypto@5.2.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@aws-crypto/util@5.2.0': dependencies: @@ -14910,7 +14943,7 @@ snapshots: '@smithy/types': 4.1.0 '@smithy/util-middleware': 4.0.1 fast-xml-parser: 4.4.1 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/credential-provider-env@3.734.0': dependencies: @@ -14918,7 +14951,7 @@ snapshots: '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/credential-provider-http@3.734.0': dependencies: @@ -14931,7 +14964,7 @@ snapshots: '@smithy/smithy-client': 4.1.3 '@smithy/types': 4.1.0 '@smithy/util-stream': 4.0.2 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/credential-provider-ini@3.734.0': dependencies: @@ -14947,7 +14980,7 @@ snapshots: '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 transitivePeerDependencies: - aws-crt @@ -14964,7 +14997,7 @@ snapshots: '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 transitivePeerDependencies: - aws-crt @@ -14975,7 +15008,7 @@ snapshots: '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/credential-provider-sso@3.734.0': dependencies: @@ -14986,7 +15019,7 @@ snapshots: '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 transitivePeerDependencies: - aws-crt @@ -14997,7 +15030,7 @@ snapshots: '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 transitivePeerDependencies: - aws-crt @@ -15050,7 +15083,7 @@ snapshots: '@aws-sdk/types': 3.734.0 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/middleware-location-constraint@3.734.0': dependencies: @@ -15062,14 +15095,14 @@ snapshots: dependencies: '@aws-sdk/types': 3.734.0 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/middleware-recursion-detection@3.734.0': dependencies: '@aws-sdk/types': 3.734.0 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/middleware-sdk-s3@3.734.0': dependencies: @@ -15086,7 +15119,7 @@ snapshots: '@smithy/util-middleware': 4.0.1 '@smithy/util-stream': 4.0.2 '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/middleware-ssec@3.734.0': dependencies: @@ -15102,7 +15135,7 @@ snapshots: '@smithy/core': 3.1.2 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/nested-clients@3.734.0': dependencies: @@ -15154,7 +15187,7 @@ snapshots: '@smithy/types': 4.1.0 '@smithy/util-config-provider': 4.0.0 '@smithy/util-middleware': 4.0.1 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/s3-request-presigner@3.738.0': dependencies: @@ -15174,7 +15207,7 @@ snapshots: '@smithy/protocol-http': 5.0.1 '@smithy/signature-v4': 5.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/token-providers@3.734.0': dependencies: @@ -15190,7 +15223,7 @@ snapshots: '@aws-sdk/types@3.734.0': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/util-arn-parser@3.723.0': dependencies: @@ -15201,7 +15234,7 @@ snapshots: '@aws-sdk/types': 3.734.0 '@smithy/types': 4.1.0 '@smithy/util-endpoints': 3.0.1 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/util-format-url@3.734.0': dependencies: @@ -15212,14 +15245,14 @@ snapshots: '@aws-sdk/util-locate-window@3.568.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/util-user-agent-browser@3.734.0': dependencies: '@aws-sdk/types': 3.734.0 '@smithy/types': 4.1.0 bowser: 2.11.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/util-user-agent-node@3.734.0': dependencies: @@ -15227,7 +15260,7 @@ snapshots: '@aws-sdk/types': 3.734.0 '@smithy/node-config-provider': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@aws-sdk/xml-builder@3.734.0': dependencies: @@ -15548,7 +15581,7 @@ snapshots: node-gyp: 9.4.1 ora: 5.4.1 read-binary-file-arch: 1.0.6 - semver: 7.7.2 + semver: 7.6.3 tar: 6.2.0 yargs: 17.7.2 transitivePeerDependencies: @@ -15989,7 +16022,7 @@ snapshots: jest-util: 29.7.0 jest-validate: 29.7.0 jest-watcher: 29.7.0 - micromatch: 4.0.8 + micromatch: 4.0.5 pretty-format: 29.7.0 slash: 3.0.0 strip-ansi: 6.0.1 @@ -16100,7 +16133,7 @@ snapshots: jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 - micromatch: 4.0.8 + micromatch: 4.0.5 pirates: 4.0.6 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -16550,7 +16583,7 @@ snapshots: '@npmcli/fs@2.1.2': dependencies: '@gar/promisify': 1.1.3 - semver: 7.7.2 + semver: 7.6.3 '@npmcli/move-file@2.0.1': dependencies: @@ -17451,7 +17484,7 @@ snapshots: extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.4.0 - semver: 7.7.2 + semver: 7.6.3 tar-fs: 3.0.6 unbzip2-stream: 1.4.3 yargs: 17.7.2 @@ -24473,6 +24506,62 @@ snapshots: - supports-color - ts-node + '@rush-temp/network-zeromq@file:projects/network-zeromq.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': + dependencies: + '@types/jest': 29.5.12 + '@types/node': 22.15.29 + '@types/uuid': 8.3.4 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + prettier: 3.2.5 + ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) + typescript: 5.8.3 + uuid: 8.3.2 + zeromq: 6.5.0 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + + '@rush-temp/network@file:projects/network.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': + dependencies: + '@types/jest': 29.5.12 + '@types/node': 22.15.29 + '@types/uuid': 8.3.4 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + prettier: 3.2.5 + simplytyped: 3.3.0(typescript@5.8.3) + ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) + typescript: 5.8.3 + uuid: 8.3.2 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + '@rush-temp/notification-assets@file:projects/notification-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@types/jest': 29.5.12 @@ -31137,7 +31226,7 @@ snapshots: '@smithy/types': 4.1.0 '@smithy/util-config-provider': 4.0.0 '@smithy/util-middleware': 4.0.1 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/core@3.1.2': dependencies: @@ -31148,7 +31237,7 @@ snapshots: '@smithy/util-middleware': 4.0.1 '@smithy/util-stream': 4.0.2 '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/credential-provider-imds@4.0.1': dependencies: @@ -31156,7 +31245,7 @@ snapshots: '@smithy/property-provider': 4.0.1 '@smithy/types': 4.1.0 '@smithy/url-parser': 4.0.1 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/eventstream-codec@4.0.1': dependencies: @@ -31194,7 +31283,7 @@ snapshots: '@smithy/querystring-builder': 4.0.1 '@smithy/types': 4.1.0 '@smithy/util-base64': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/hash-blob-browser@4.0.1': dependencies: @@ -31208,7 +31297,7 @@ snapshots: '@smithy/types': 4.1.0 '@smithy/util-buffer-from': 4.0.0 '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/hash-stream-node@4.0.1': dependencies: @@ -31219,7 +31308,7 @@ snapshots: '@smithy/invalid-dependency@4.0.1': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/is-array-buffer@2.2.0': dependencies: @@ -31227,7 +31316,7 @@ snapshots: '@smithy/is-array-buffer@4.0.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/md5-js@4.0.1': dependencies: @@ -31239,7 +31328,7 @@ snapshots: dependencies: '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/middleware-endpoint@4.0.3': dependencies: @@ -31250,7 +31339,7 @@ snapshots: '@smithy/types': 4.1.0 '@smithy/url-parser': 4.0.1 '@smithy/util-middleware': 4.0.1 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/middleware-retry@4.0.4': dependencies: @@ -31261,25 +31350,25 @@ snapshots: '@smithy/types': 4.1.0 '@smithy/util-middleware': 4.0.1 '@smithy/util-retry': 4.0.1 - tslib: 2.8.1 + tslib: 2.7.0 uuid: 9.0.1 '@smithy/middleware-serde@4.0.2': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/middleware-stack@4.0.1': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/node-config-provider@4.0.1': dependencies: '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/node-http-handler@4.0.2': dependencies: @@ -31292,12 +31381,12 @@ snapshots: '@smithy/property-provider@4.0.1': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/protocol-http@5.0.1': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/querystring-builder@4.0.1': dependencies: @@ -31308,7 +31397,7 @@ snapshots: '@smithy/querystring-parser@4.0.1': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/service-error-classification@4.0.1': dependencies: @@ -31317,7 +31406,7 @@ snapshots: '@smithy/shared-ini-file-loader@4.0.1': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/signature-v4@5.0.1': dependencies: @@ -31328,7 +31417,7 @@ snapshots: '@smithy/util-middleware': 4.0.1 '@smithy/util-uri-escape': 4.0.0 '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/smithy-client@4.1.3': dependencies: @@ -31338,31 +31427,31 @@ snapshots: '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 '@smithy/util-stream': 4.0.2 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/types@4.1.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/url-parser@4.0.1': dependencies: '@smithy/querystring-parser': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-base64@4.0.0': dependencies: '@smithy/util-buffer-from': 4.0.0 '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-body-length-browser@4.0.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-body-length-node@4.0.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-buffer-from@2.2.0': dependencies: @@ -31372,11 +31461,11 @@ snapshots: '@smithy/util-buffer-from@4.0.0': dependencies: '@smithy/is-array-buffer': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-config-provider@4.0.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-defaults-mode-browser@4.0.4': dependencies: @@ -31384,7 +31473,7 @@ snapshots: '@smithy/smithy-client': 4.1.3 '@smithy/types': 4.1.0 bowser: 2.11.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-defaults-mode-node@4.0.4': dependencies: @@ -31394,28 +31483,28 @@ snapshots: '@smithy/property-provider': 4.0.1 '@smithy/smithy-client': 4.1.3 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-endpoints@3.0.1': dependencies: '@smithy/node-config-provider': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-hex-encoding@4.0.0': dependencies: - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-middleware@4.0.1': dependencies: '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-retry@4.0.1': dependencies: '@smithy/service-error-classification': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-stream@4.0.2': dependencies: @@ -31426,7 +31515,7 @@ snapshots: '@smithy/util-buffer-from': 4.0.0 '@smithy/util-hex-encoding': 4.0.0 '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-uri-escape@4.0.0': dependencies: @@ -31435,18 +31524,18 @@ snapshots: '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-utf8@4.0.0': dependencies: '@smithy/util-buffer-from': 4.0.0 - tslib: 2.8.1 + tslib: 2.7.0 '@smithy/util-waiter@4.0.2': dependencies: '@smithy/abort-controller': 4.0.1 '@smithy/types': 4.1.0 - tslib: 2.8.1 + tslib: 2.7.0 '@swc/core-darwin-arm64@1.13.5': optional: true @@ -32455,7 +32544,7 @@ snapshots: graphemer: 1.4.0 ignore: 5.3.1 natural-compare-lite: 1.4.0 - semver: 7.7.2 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -32552,7 +32641,7 @@ snapshots: debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.2 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -32584,7 +32673,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) eslint: 8.56.0 eslint-scope: 5.1.1 - semver: 7.7.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -32932,7 +33021,7 @@ snapshots: minimatch: 10.0.1 resedit: 1.7.1 sanitize-filename: 1.6.3 - semver: 7.7.2 + semver: 7.6.3 tar: 6.2.0 temp-file: 3.4.0 transitivePeerDependencies: @@ -33095,7 +33184,7 @@ snapshots: async-mutex@0.3.2: dependencies: - tslib: 2.8.1 + tslib: 2.7.0 async@3.2.5: {} @@ -33288,7 +33377,7 @@ snapshots: braces@3.0.2: dependencies: - fill-range: 7.1.1 + fill-range: 7.0.1 braces@3.0.3: dependencies: @@ -33394,7 +33483,7 @@ snapshots: builtins@5.0.1: dependencies: - semver: 7.7.2 + semver: 7.6.0 busboy@1.6.0: dependencies: @@ -33455,7 +33544,7 @@ snapshots: camel-case@4.1.2: dependencies: pascal-case: 3.1.2 - tslib: 2.8.1 + tslib: 2.6.2 camelcase-css@2.0.1: {} @@ -33620,6 +33709,8 @@ snapshots: clone@1.0.4: {} + cmake-ts@1.0.2: {} + co-body@6.1.0: dependencies: inflation: 2.1.0 @@ -33747,7 +33838,7 @@ snapshots: json-schema-typed: 7.0.3 onetime: 5.1.2 pkg-up: 3.1.0 - semver: 7.7.2 + semver: 7.6.3 confbox@0.1.8: {} @@ -35441,6 +35532,10 @@ snapshots: filesize@8.0.7: {} + fill-range@7.0.1: + dependencies: + to-regex-range: 5.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -35762,7 +35857,7 @@ snapshots: es6-error: 4.1.1 matcher: 3.0.0 roarr: 2.15.4 - semver: 7.7.2 + semver: 7.6.3 serialize-error: 7.0.1 optional: true @@ -36100,7 +36195,7 @@ snapshots: http-proxy: 1.18.1 is-glob: 4.0.3 is-plain-obj: 3.0.0 - micromatch: 4.0.8 + micromatch: 4.0.5 optionalDependencies: '@types/express': 4.17.21 transitivePeerDependencies: @@ -36461,7 +36556,7 @@ snapshots: '@babel/parser': 7.23.9 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -36577,7 +36672,7 @@ snapshots: jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.8 + micromatch: 4.0.5 parse-json: 5.2.0 pretty-format: 29.7.0 slash: 3.0.0 @@ -36652,7 +36747,7 @@ snapshots: jest-regex-util: 29.6.3 jest-util: 29.7.0 jest-worker: 29.7.0 - micromatch: 4.0.8 + micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 @@ -36676,7 +36771,7 @@ snapshots: '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.8 + micromatch: 4.0.5 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -36786,7 +36881,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -36966,7 +37061,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.2 + semver: 7.6.3 jsx-ast-utils@3.3.5: dependencies: @@ -37451,7 +37546,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.2 + semver: 7.6.3 make-error@1.3.6: {} @@ -37590,7 +37685,7 @@ snapshots: micromatch@4.0.5: dependencies: - braces: 3.0.3 + braces: 3.0.2 picomatch: 2.3.1 micromatch@4.0.8: @@ -37838,16 +37933,18 @@ snapshots: node-abi@3.55.0: dependencies: - semver: 7.7.2 + semver: 7.6.3 node-abort-controller@3.1.1: {} node-addon-api@1.7.2: optional: true + node-addon-api@8.5.0: {} + node-api-version@0.2.0: dependencies: - semver: 7.7.2 + semver: 7.6.3 node-cron@3.0.3: dependencies: @@ -37882,7 +37979,7 @@ snapshots: nopt: 6.0.0 npmlog: 6.0.2 rimraf: 3.0.2 - semver: 7.7.2 + semver: 7.6.3 tar: 6.2.0 which: 2.0.2 transitivePeerDependencies: @@ -38201,7 +38298,7 @@ snapshots: param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.8.1 + tslib: 2.6.2 parent-module@1.0.1: dependencies: @@ -39345,7 +39442,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.7.2 + semver: 7.6.3 simplytyped@3.3.0(typescript@5.8.3): dependencies: @@ -39736,7 +39833,7 @@ snapshots: methods: 1.1.2 mime: 2.6.0 qs: 6.11.2 - semver: 7.7.2 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -40098,7 +40195,7 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.6.0 typescript: 5.8.3 yargs-parser: 21.1.1 optionalDependencies: @@ -40982,6 +41079,11 @@ snapshots: zen-observable@0.8.15: {} + zeromq@6.5.0: + dependencies: + cmake-ts: 1.0.2 + node-addon-api: 8.5.0 + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/communication b/communication index a8d58f18bb6..9673971e925 160000 --- a/communication +++ b/communication @@ -1 +1 @@ -Subproject commit a8d58f18bb68e4cd4e3a97526af11eb800fb9fc3 +Subproject commit 9673971e9257bd167074b78921de746184604289 diff --git a/network/README.md b/network/README.md new file mode 100644 index 00000000000..7044f036ca0 --- /dev/null +++ b/network/README.md @@ -0,0 +1,145 @@ +# Huly Virtual Network + +A distributed, scalable virtual network architecture for the Huly that enables fault-tolerant performance communication. + +## 🚀 Overview + +The Huly Virtual Network is a sophisticated distributed system designed to handle enterprise-scale workloads with the following key capabilities: + +- **Distributed Load Balancing**: Intelligent routing to key components across multiple phytsical nodes. +- **Multi-Tenant Architecture**: Secure isolation of containers for user sessions/search engines/transaction processors. +- **Fault Tolerance**: Automatic failover and recovery mechanisms +- **Scaling**: Horizontal and vertical service scalling. +- **Real-time Communication**: Event-driven architecture with broadcast capabilities between containers. + +## Network Architecture Components + +### Agents & Containers + +Agent is a top of the rock for containers. Containers are work horses for any application build on top of Huly Network, they could be started, located and used. +Every container could be located by its {kind + uuid} or using a labeling system. After being found anyone could send a message to container and ask for some activity. + +Container's could be communicated two ways: + +1. Anyone could send a message to container using network. +2. A request/response connection could be established to container using network. + +Network provide a references to containers, if container is references it will be active until reference is exists. If there is no references to contaienr, it will be keept for some timeout and closed. + +Agents provide a list of container kinds they support to be started to Huly Network, manage them and provide live/monitoring and networking capabilities. +Communication to containers are managed by Huly Network. + +```mermaid +flowchart BT + subgraph Agent["Network Agent"] + subgraph _TX["Transactor"] + TX1["ws1"] + TX2["ws2"] + end + subgraph _VMM["Virtual Machine"] + AG1["Hulia Agent"] + AG2["James Agent"] + end + subgraph _Sessions["Session"] + S1["user1"] + S2["user2"] + end + subgraph _Query["Query Engine"] + Q1["Europe"] + Q3["ws3"] + end + end + subgraph Agent2["Network Agent2"] + subgraph _TX_2["Transactor"] + TX3["ws3"] + TX4["ws4"] + end + subgraph _Query_2["Query Engine"] + Q1_2["Europe"] + Q2_2["America"] + end + end + + subgraph NET["Huly Network"] + Containers["Containers"] + Agents["Agents"] + end + + + S1 -.-> TX1 & Q1 & Q2_2 & TX4 + Agent --> NET + Agent2 --> NET + AG1@{ shape: stored-data} + AG2@{ shape: stored-data} + S1@{ shape: h-cyl} + S2@{ shape: h-cyl} + style Agent stroke:#00C853 +``` + +## Building Huly on top of Huly Network + +Huly could be managed by following set of container kinds, `session`, `query`, `transactor`. + +- session -> a map/reduce/find executor for queries and transactions from client. +- query -> a DB query engine, execute `find` requests from session and pass them to DB, allow to search for all data per region. Should have access to tables of account -> workspace mapping for security. +- transactor -> modification archestrator for all edit operations, do them one by one. + +```mermaid +flowchart + Endpoint -.->| + connect + session/user1 + |HulyNetwork[Huly Network] + + Endpoint <-->|find,tx| parsonal-ws:user1 + + parsonal-ws:user1 -..->|get-workspace info| DatalakeDB + + parsonal-ws:user1 -..->|find| query:europe + + query:europe -..->|resp| parsonal-ws:user1 + + parsonal-ws:user1 -..->|response chunks| Endpoint + + parsonal-ws:user1 -..->|tx| transactor:ws1 + + transactor:ws1 -..->|event's| HulyPulse + + HulyPulse <--> Client + + Client <--> Endpoint + + query:europe -..->|"update"| QueryDB + transactor:ws1 -..->|update| DatalakeDB + + transactor:ws1 -..->|txes| Kafka[Output Queue] + + Kafka -..-> Indexer[Structure + + Fulltext Index] + + Indexer -..-> QueryDB + + Indexer -..->|indexed tx| HulyPulse + + Kafka -..-> AsyncTriggers + + AsyncTriggers -..->|find| query:europe + + AsyncTriggers -..->|derived txes| transactor:ws1 + + InputQueue -->|txes| transactor:ws1 + + Services[Services + Github/Telegram/Translate] -..-> InputQueue + + Kafka -..-> Services + + Services -..-> query:europe + + QueryDB@{shape: database} + InputQueue@{shape: database} + DatalakeDB@{shape: database} + Kafka@{shape: database} + parsonal-ws:user1@{ shape: h-cyl} + +``` diff --git a/network/core/.eslintrc.js b/network/core/.eslintrc.js new file mode 100644 index 00000000000..ce90fb9646f --- /dev/null +++ b/network/core/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/network/core/config/rig.json b/network/core/config/rig.json new file mode 100644 index 00000000000..78cc5a17334 --- /dev/null +++ b/network/core/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/network/core/docs/alive-checkins.md b/network/core/docs/alive-checkins.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/core/jest.config.js b/network/core/jest.config.js new file mode 100644 index 00000000000..2cfd408b679 --- /dev/null +++ b/network/core/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/network/core/package.json b/network/core/package.json new file mode 100644 index 00000000000..9190e5f2e50 --- /dev/null +++ b/network/core/package.json @@ -0,0 +1,53 @@ +{ + "name": "@hcengineering/network", + "version": "0.6.32", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Huly Platform Contributors", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --forceExit", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "simplytyped": "^3.3.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.8.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^22.15.29", + "@types/uuid": "^8.3.1" + }, + "dependencies": { + "@hcengineering/analytics": "^0.6.0", + "uuid": "^8.3.2" + }, + "repository": "https://github.com/hcengineering/platform", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/network/core/src/__test__/alive-checkins.spec.ts b/network/core/src/__test__/alive-checkins.spec.ts new file mode 100644 index 00000000000..8e9321bcd98 --- /dev/null +++ b/network/core/src/__test__/alive-checkins.spec.ts @@ -0,0 +1,44 @@ +import type { AgentUuid } from '../api/net' +import { timeouts } from '../api/timeouts' +import { AgentImpl, NetworkImpl } from '../net' +import { TickManagerImpl } from '../utils' + +class FakeTickManager extends TickManagerImpl { + time: number = 0 + + now (): number { + return this.time + } +} + +const agents = { + agent1: 'agent1' as AgentUuid, + agent2: 'agent2' as AgentUuid, + agent3: 'agent3' as AgentUuid +} + +describe('alive checkins tests', () => { + it('should mark and remove agent if not alive', async () => { + const tickManager = new FakeTickManager(1) // 1 tick per second + const network = new NetworkImpl(tickManager) + const agent1 = new AgentImpl(agents.agent1, {}) + + // Register agent + await agent1.register(network) + + // Get initial lastSeen time + const agentRecord = (await network.agents()).find(a => a.agentId === agents.agent1) + expect(agentRecord).toBeDefined() + + // Make looks like agent is not alive anymore + tickManager.time += 1000 + timeouts.aliveTimeout * 1000 + + // Trigger alive callback + await (network as any).checkAlive() + + // Check that lastSeen was updated (we can't easily test the exact timestamp, + // but we can verify the agent is still in the network) + const updatedAgentRecord = (await network.agents()).find(a => a.agentId === agents.agent1) + expect(updatedAgentRecord).toBeUndefined() + }) +}) diff --git a/network/core/src/__test__/network.spec.ts b/network/core/src/__test__/network.spec.ts new file mode 100644 index 00000000000..012e123b85c --- /dev/null +++ b/network/core/src/__test__/network.spec.ts @@ -0,0 +1,72 @@ +import type { AgentUuid, ClientUuid, ContainerEndpointRef, ContainerKind } from '../api/net' +import { AgentImpl, NetworkImpl, type Container } from '../net' +import { TickManagerImpl } from '../utils' + +// class DummyConnectionManager implements ConnectionManager { +// async connect (endpoint: ContainerEndpointRef): Promise { +// throw new Error('Method not implemented.') +// } +// } + +class DummyContainer implements Container { + lastVisit: number = 0 + onTerminated?: (() => void) | undefined + + async request (operation: string, data: any): Promise { + return undefined + } + + async terminate (): Promise {} + + async ping (): Promise { + this.lastVisit = Date.now() + } + + connect (clientId: ClientUuid, handler: (data: any) => Promise): void { + + } + + disconnect (clientId: ClientUuid): void { + + } +} + +const agents = { + agent1: 'agent1' as AgentUuid, + agent2: 'agent2' as AgentUuid, + agent3: 'agent3' as AgentUuid +} + +const kinds: Record = { + session: 'session' as ContainerKind +} + +describe('network tests', () => { + it('check register agent', async () => { + const tickManager = new TickManagerImpl(1) // 1 tick per second + const network = new NetworkImpl(tickManager) + + const agent1 = new AgentImpl(agents.agent1, {}) + await agent1.register(network) + + expect((network as any)._agents.size).toBe(1) + }) + + it('start container', async () => { + const tickManager = new TickManagerImpl(1) // 1 tick per second + const network = new NetworkImpl(tickManager) + + const agent1 = new AgentImpl( + agents.agent1, + { + [kinds.session]: () => + Promise.resolve([new DummyContainer(), '' as ContainerEndpointRef]) + } + ) + await agent1.register(network) + expect((network as any)._agents.size).toBe(1) + + const s1 = await network.get(agents.agent1 as any, 's1' as any, { kind: kinds.session }) + expect(s1).toBeDefined() + }) +}) diff --git a/network/core/src/__test__/samples.ts b/network/core/src/__test__/samples.ts new file mode 100644 index 00000000000..337d39ad8cb --- /dev/null +++ b/network/core/src/__test__/samples.ts @@ -0,0 +1,27 @@ +import type { AccountUuid, WorkspaceUuid } from '../api/types' +import { StaticWorkspaceDiscovery } from '../discovery/static' + +export const workspaces = { + ws1: 'ws1' as WorkspaceUuid, + ws2: 'ws2' as WorkspaceUuid, + ws3: 'ws3' as WorkspaceUuid, + ws4: 'ws4' as WorkspaceUuid, + ws5: 'ws5' as WorkspaceUuid, + ws6: 'ws6' as WorkspaceUuid, + ws7: 'ws7' as WorkspaceUuid, + ws8: 'ws8' as WorkspaceUuid, + ws9: 'ws9' as WorkspaceUuid, + ws10: 'ws10' as WorkspaceUuid +} + +export const users = { + user1: 'user1' as AccountUuid, + user2: 'user2' as AccountUuid +} + +export const wsDiscovery = new StaticWorkspaceDiscovery({ + [users.user1]: [workspaces.ws1, workspaces.ws2, workspaces.ws3], + [users.user2]: [workspaces.ws4, workspaces.ws5, workspaces.ws6], + [workspaces.ws1]: [workspaces.ws7, workspaces.ws8], + [workspaces.ws8]: [workspaces.ws9, workspaces.ws10] +}) diff --git a/network/core/src/__test__/tickMgr.spec.ts b/network/core/src/__test__/tickMgr.spec.ts new file mode 100644 index 00000000000..baf9c360a33 --- /dev/null +++ b/network/core/src/__test__/tickMgr.spec.ts @@ -0,0 +1,28 @@ +import { TickManagerImpl } from '../utils' + +describe('check tickManager', () => { + it('check ticks', async () => { + const mgr = new TickManagerImpl(20) + const h1 = mgr.nextHash() + + // await mgr.tick() + expect(mgr.isMe(h1, 1)).toBe(true) + expect(mgr.isMe(h1, 2)).toBe(true) + expect(mgr.isMe(h1, 3)).toBe(true) + + ;(mgr as any)._tick = 20 + expect(mgr.isMe(h1, 1)).toBe(true) + expect(mgr.isMe(h1, 2)).toBe(false) + expect(mgr.isMe(h1, 3)).toBe(false) + + ;(mgr as any)._tick = 40 + expect(mgr.isMe(h1, 1)).toBe(true) + expect(mgr.isMe(h1, 2)).toBe(true) + expect(mgr.isMe(h1, 3)).toBe(false) + + ;(mgr as any)._tick = 60 + expect(mgr.isMe(h1, 1)).toBe(true) + expect(mgr.isMe(h1, 2)).toBe(false) + expect(mgr.isMe(h1, 3)).toBe(true) + }) +}) \ No newline at end of file diff --git a/network/core/src/api/discovery.ts b/network/core/src/api/discovery.ts new file mode 100644 index 00000000000..b1e13de6d6c --- /dev/null +++ b/network/core/src/api/discovery.ts @@ -0,0 +1,11 @@ +import type { AccountUuid, WorkspaceUuid } from './types' + +export interface WorkspaceDiscovery { + byAccount: (account: AccountUuid) => Promise + + byWorkspace: (workspace: WorkspaceUuid) => Promise +} + +export interface AccountDiscovery { + byWorkspace: (workspace: WorkspaceUuid) => Promise +} diff --git a/network/core/src/api/net.ts b/network/core/src/api/net.ts new file mode 100644 index 00000000000..4963f956fff --- /dev/null +++ b/network/core/src/api/net.ts @@ -0,0 +1,192 @@ +import type { Container } from '../net' + +export type ContainerUuid = string & { _containerUuid: true } +export type ContainerKind = string & { _containerKind: true } +export type AgentUuid = string & { _networkAgentUuid: true } +export type ClientUuid = string & { _networkClientUuid: true } +export type ContainerEndpointRef = string & { _containerEndpointRef: true } +export type AgentEndpointRef = string & { _agentEndpointRef: true } + +export interface ContainerRecord { + agentId: AgentUuid + uuid: ContainerUuid + kind: ContainerKind + endpoint: ContainerEndpointRef + lastVisit: number // Last time when container was visited + + // Last request used + extra?: Record // Extra parameters for container start + + labels?: string[] +} + +export interface AgentRecord { + agentId: AgentUuid + + // If endpoint is not sepecified, container send will be passthrought via network connection. + // Individal containers still could have connections. + endpoint?: AgentEndpointRef + + // A change to containers + containers: ContainerRecord[] + kinds: ContainerKind[] +} + +export interface ContainerRequest { + kind: ContainerKind + extra?: Record // Extra parameters for container start + + labels?: string[] +} + +/** + * Interface to Huly network on server. + */ +export interface Network { + /* + * Register or reregister agent in network. + * On every network restart agent should reconnect to network. + */ + register: (record: AgentRecord, agent: NetworkAgent) => Promise + + // Mark an agent as alive (updates lastSeen timestamp) + ping: (agentId: AgentUuid | ClientUuid) => void + + agents: () => Promise + + // A full uniq set of supported container kinds. + kinds: () => Promise + + /* + * Get/Start of required container kind on agent + * Will start a required container on agent, if not already started. + */ + get: (client: ClientUuid, uuid: ContainerUuid, request: ContainerRequest) => Promise + + /** + * Release a container for a client, if container is not used anymore it will be shutdown with a shutdown delay. + */ + release: (client: ClientUuid, uuid: ContainerUuid) => Promise + + list: (kind: ContainerKind) => Promise + + // Send some data to container, using proxy connection. + request: (target: ContainerUuid, operation: string, data?: any) => Promise +} + +export interface NetworkWithClients { + addClient: (clientUuid: ClientUuid, onContainer?: (event: ContainerEvent) => Promise) => void + removeClient: (clientUuid: ClientUuid) => void +} + +export type ContainerUpdateListener = (event: ContainerEvent) => Promise + +/** + * Interface to Huly network. + * + * Identification is generated during instantions of client. + */ +export interface NetworkClient { + /* + * Register or a NetworkAgent API to be processed by network. + * On every network change restart agent register method will be called. + */ + register: (agent: NetworkAgent) => Promise + + agents: () => Promise + + // A full uniq set of supported container kinds. + kinds: () => Promise + + /* + * Get/Start of required container kind on agent + * Will start a required container on agent, if not already started. + */ + get: (uuid: ContainerUuid, request: ContainerRequest) => Promise + + list: (kind: ContainerKind) => Promise + + // Send some data to container, using proxy connection. + request: (target: ContainerUuid, operation: string, data?: any) => Promise + + // Register on container update listener + onContainerUpdate: (listener: ContainerUpdateListener) => void + + close: () => Promise +} + +export interface ConnectionManager { + connect: (endpoint: ContainerEndpointRef) => Promise +} + +/** + * A client reference to container, until closed, client will notify network about container is still required. + */ +export interface ContainerReference { + uuid: ContainerUuid + + endpoint: ContainerEndpointRef + + close: () => Promise + + connect: (timeout?: number) => Promise + + request: (operation: string, data?: any) => Promise +} + +export interface ContainerEvent { + added: ContainerRecord[] + deleted: ContainerRecord[] + updated: ContainerRecord[] +} + +/** + * Interface to Huly Agent on agent. + */ +export interface NetworkAgent { + // Agent uniq identigier, should be same on agent restarts. + uuid: AgentUuid + + // Agent connection endpoint. + endpoint?: AgentEndpointRef + + // A supported set of container kinds supported to be managed by the agent + kinds: ContainerKind[] + + // event handled from agent to network events. + onUpdate?: (event: ContainerEvent) => Promise + + // Send agent update info to network, if applicable. + onAgentUpdate?: () => Promise + + // Get/Start of required container kind on agent + get: (uuid: ContainerUuid, request: ContainerRequest) => Promise + + // A low level reference to container + getContainer: (uuid: ContainerUuid) => Promise + + // List of active containers + list: (kind?: ContainerKind) => Promise + + // Send some data to container + request: (target: ContainerUuid, operation: string, data?: any) => Promise + + // ask for immediate termination for container + terminate: (container: ContainerEndpointRef) => Promise +} + +// A request/reponse interface to container. +export interface ContainerConnection { + containerId: ContainerUuid + + // A simple request/response to container. + request: (operation: string, data?: any) => Promise + + // A chunk streaming of results + // stream: (data: any) => Iterable + + // Recieve not a requests but also any kind of notifications. + on?: (data: any) => Promise + + close: () => Promise +} diff --git a/network/core/src/api/timeouts.ts b/network/core/src/api/timeouts.ts new file mode 100644 index 00000000000..108f38589cb --- /dev/null +++ b/network/core/src/api/timeouts.ts @@ -0,0 +1,4 @@ +export const timeouts = { + aliveTimeout: 3, // seconds - timeout for detecting dead agents + pingInterval: 1 // seconds - how often to ping agents +} diff --git a/network/core/src/api/types.ts b/network/core/src/api/types.ts new file mode 100644 index 00000000000..a43d45fce2e --- /dev/null +++ b/network/core/src/api/types.ts @@ -0,0 +1,9 @@ +/** + * A unique identifier for a workspace. + */ +export type WorkspaceUuid = string & { __workspaceUuid: true } + +/** + * A unique identifier for an account. + */ +export type AccountUuid = string & { __accountUuid: true } diff --git a/network/core/src/api/utils.ts b/network/core/src/api/utils.ts new file mode 100644 index 00000000000..30dbf289b0e --- /dev/null +++ b/network/core/src/api/utils.ts @@ -0,0 +1,14 @@ +export type TickHandler = () => void | Promise + +export interface TickManager { + now: () => number + + // Interval in seconds + register: (handler: TickHandler, interval: number) => void + + // Start tick manager + start: () => void + + // Stop tick manager + stop: () => void +} diff --git a/network/core/src/discovery/static.ts b/network/core/src/discovery/static.ts new file mode 100644 index 00000000000..265dda52c5d --- /dev/null +++ b/network/core/src/discovery/static.ts @@ -0,0 +1,14 @@ +import type { WorkspaceDiscovery } from '../api/discovery' +import type { AccountUuid, WorkspaceUuid } from '../api/types' + +export class StaticWorkspaceDiscovery implements WorkspaceDiscovery { + constructor (private readonly workspaces: Record) {} + + async byAccount (account: AccountUuid): Promise { + return this.workspaces[account] ?? [] + } + + async byWorkspace (workspace: WorkspaceUuid): Promise { + return this.workspaces[workspace] ?? [] + } +} diff --git a/network/core/src/index.ts b/network/core/src/index.ts new file mode 100644 index 00000000000..ac33b5392e9 --- /dev/null +++ b/network/core/src/index.ts @@ -0,0 +1,8 @@ +export * from './api/discovery' +export * from './api/types' +export * from './api/utils' +export * from './api/net' +export * from './api/timeouts' +export * from './discovery/static' +export * from './utils' +export * from './net/index' diff --git a/network/core/src/net/agent.ts b/network/core/src/net/agent.ts new file mode 100644 index 00000000000..40e5a13bd97 --- /dev/null +++ b/network/core/src/net/agent.ts @@ -0,0 +1,114 @@ +import type { + AgentEndpointRef, + AgentUuid, + ContainerEndpointRef, + ContainerKind, + ContainerRecord, + ContainerRequest, + ContainerUuid, + Network, + NetworkAgent +} from '../api/net' +import type { Container, ContainerFactory } from './containers' + +interface ContainerRecordImpl { + container: Container + uuid: ContainerUuid + endpoint: ContainerEndpointRef + kind: ContainerKind + + lastVisit: number +} + +export class AgentImpl implements NetworkAgent { + // Own, managed containers + private readonly _byId = new Map>() + + private readonly _containers = new Map() + + endpoint?: AgentEndpointRef | undefined + + constructor ( + readonly uuid: AgentUuid, + private readonly factory: Record + ) { + } + + async register (network: Network): Promise { + const cleanContainers = await network.register({ + agentId: this.uuid, + containers: await this.list(), + kinds: this.kinds, + endpoint: this.endpoint + }, this) + for (const c of cleanContainers) { + await this.terminate(c) + } + } + + async list (kind?: ContainerKind): Promise { + return Array.from(this._containers.values()) + .filter((it) => !(it instanceof Promise) && (kind === undefined || it.kind === kind)) + .map((it) => ({ + agentId: this.uuid, + uuid: it.uuid, + endpoint: it.endpoint, + kind: it.kind, + lastVisit: it.lastVisit + })) + } + + get kinds (): ContainerKind[] { + return Object.keys(this.factory) as ContainerKind[] + } + + async getContainerImpl (uuid: ContainerUuid): Promise { + let current = this._byId.get(uuid) + if (current instanceof Promise) { + current = await current + this._byId.set(uuid, current) + } + return current + } + + async getContainer (uuid: ContainerUuid): Promise { + return (await this.getContainerImpl(uuid))?.container + } + + async get (uuid: ContainerUuid, request: ContainerRequest): Promise { + const current = await this.getContainerImpl(uuid) + if (current !== undefined) { + return current.endpoint + } + + let container: ContainerRecordImpl | Promise = this.factory[request.kind](uuid, request).then(r => ({ + container: r[0], + endpoint: r[1], + kind: request.kind, + lastVisit: Date.now(), + uuid + })) + this._byId.set(uuid, container) + container = await container + this._containers.set(container.endpoint, container) + this._byId.set(uuid, container) + + return container.endpoint + } + + async terminate (endpoint: ContainerEndpointRef): Promise { + const current = this._containers.get(endpoint) + if (current !== undefined) { + this._containers.delete(endpoint) + await current.container.terminate() + } + } + + async request (target: ContainerUuid, operation: string, data?: any): Promise { + const container = await this.getContainer(target) + if (container === undefined) { + throw new Error(`Container ${target} not found`) + } + return await container.request(operation, data) + } +} diff --git a/network/core/src/net/containers.ts b/network/core/src/net/containers.ts new file mode 100644 index 00000000000..993c6292747 --- /dev/null +++ b/network/core/src/net/containers.ts @@ -0,0 +1,24 @@ +import type { ClientUuid, ContainerEndpointRef, ContainerRecord, ContainerRequest, ContainerUuid } from '../api/net' + +export interface Container { + request: (operation: string, data?: any, clientId?: ClientUuid) => Promise + + // Called when the container is terminated + onTerminated?: () => void + + terminate: () => Promise + + ping: () => Promise + + connect: (clientId: ClientUuid, broadcast: (data: any) => Promise) => void + disconnect: (clientId: ClientUuid) => void +} + +export type ContainerFactory = (uuid: ContainerUuid, request: ContainerRequest) => Promise<[Container, ContainerEndpointRef]> + +export interface ContainerRecordImpl { + record: ContainerRecord + endpoint: ContainerEndpointRef | Promise + + clients: Set +} diff --git a/network/core/src/net/index.ts b/network/core/src/net/index.ts new file mode 100644 index 00000000000..fd00173a106 --- /dev/null +++ b/network/core/src/net/index.ts @@ -0,0 +1,9 @@ +import type { ContainerUuid } from '../api/net' + +export * from './containers' +export * from './network' +export * from './agent' + +export function composeCID (prefix: string, id: string): ContainerUuid { + return `${prefix}_${id}` as ContainerUuid +} diff --git a/network/core/src/net/network.ts b/network/core/src/net/network.ts new file mode 100644 index 00000000000..3d2f8741a3d --- /dev/null +++ b/network/core/src/net/network.ts @@ -0,0 +1,432 @@ +import type { + AgentEndpointRef, + AgentRecord, + AgentUuid, + ClientUuid, + ContainerEndpointRef, + ContainerEvent, + ContainerKind, + ContainerRecord, + ContainerRequest, + ContainerUuid, + Network, + NetworkAgent, + NetworkWithClients +} from '../api/net' +import { timeouts } from '../api/timeouts' +import type { TickManager } from '../api/utils' +import type { ContainerRecordImpl } from './containers' + +interface AgentRecordImpl { + api: NetworkAgent + containers: Map + endpoint?: AgentEndpointRef + kinds: ContainerKind[] + + lastSeen: number // Last time when agent was seen +} + +interface ClientRecordImpl { + lastSeen: number + containers: Set + onContainer?: (event: ContainerEvent) => Promise +} +/** + * Server network implementation. + */ +export class NetworkImpl implements Network, NetworkWithClients { + private idx: number = 0 + + private readonly _agents = new Map() + + private readonly _containers = new Map() + + private readonly _clients = new Map() + + private readonly _orphanedContainers = new Map() + + private eventQueue: ContainerEvent[] = [] + + constructor (private readonly tickManager: TickManager) { + // Register for periodic agent health checks + tickManager.register(() => { + void this.checkAlive().catch((err) => { + console.error('Error during network health check:', err) + }) + // Check for events on every tick + void this.sendEvents() + }, timeouts.aliveTimeout) + } + + async agents (): Promise { + return Array.from( + this._agents.values().map(({ api, containers }) => ({ + agentId: api.uuid, + endpoint: api.endpoint, + kinds: api.kinds, + containers: Object.values(containers).map(({ record }) => record) + })) + ) + } + + async kinds (): Promise { + return Array.from( + this._agents + .values() + .map((it) => it.kinds) + .flatMap((it) => it) + ) + } + + async list (kind: ContainerKind): Promise { + return Array.from(this._agents.values()) + .flatMap((it) => Array.from(it.containers.values())) + .filter((it) => it.record.kind === kind) + .map((it) => it.record) + } + + async request (target: ContainerUuid, operation: string, data?: any): Promise { + const agentId = this._containers.get(target) + if (agentId === undefined) { + throw new Error(`Container ${target} not found`) + } + const agent = this._agents.get(agentId) + if (agent === undefined) { + throw new Error(`Agent ${agentId} not found for container ${target}`) + } + const container = agent.containers.get(target) + if (container === undefined) { + throw new Error(`Container ${target} not registered on agent ${agentId}`) + } + return await agent.api.request(target, operation, data) + } + + async register (record: AgentRecord, agent: NetworkAgent): Promise { + const containers: ContainerRecord[] = record.containers + const newContainers = new Map( + containers.map((record) => [ + record.uuid, + { + record, + request: { kind: record.kind }, + endpoint: record.endpoint, + clients: new Set([]) + } + ]) + ) + + const containerEvent: ContainerEvent = { + added: [], + deleted: [], + updated: [] + } + + // Register agent record + const oldAgent = this._agents.get(record.agentId) + if (oldAgent !== undefined) { + // In case re-register or reconnect is happened. + // Check if some of container changed endpoints. + for (const rec of containers) { + const oldRec = oldAgent.containers.get(rec.uuid) + if (oldRec !== undefined) { + if (oldRec.record.endpoint !== rec.endpoint) { + oldRec.endpoint = rec.endpoint // Update endpoint + containerEvent.updated.push(rec) + } + } + } + // Handle remove of containers + for (const oldC of oldAgent.containers.values()) { + if (newContainers.get(oldC.record.uuid) === undefined) { + containerEvent.deleted.push(oldC.record) + this._containers.delete(oldC.record.uuid) // Remove from active container registry + } + } + } + + const containersToShutdown: ContainerEndpointRef[] = [] + + // Update active container registry. + for (const rec of containers) { + const oldAgentId = this._containers.get(rec.uuid) + if (oldAgentId === undefined) { + containerEvent.added.push(rec) + this._containers.set(rec.uuid, record.agentId) + } + if (oldAgentId !== record.agentId) { + containersToShutdown.push(rec.endpoint) + } + } + + // update agent record + + this._agents.set(record.agentId, { + api: agent, + containers: newContainers, + endpoint: record.endpoint, + kinds: record.kinds, + lastSeen: this.tickManager.now() + }) + + this.eventQueue.push(containerEvent) + + // Send notification to all agents about containers update. + return containersToShutdown + } + + async sendEvents (): Promise { + const events = [...this.eventQueue] + this.eventQueue = [] + if (events.length === 0) { + return + } + // Combine events + + const finalEvent: ContainerEvent = { + added: [], + deleted: [], + updated: [] + } + for (const event of events) { + finalEvent.added.push(...event.added) + finalEvent.deleted.push(...event.deleted) + finalEvent.updated.push(...event.updated) + } + + // Skip deleted events. + const deletedIds = finalEvent.deleted.map((c) => c.uuid) + finalEvent.added = finalEvent.added.filter((c) => !deletedIds.includes(c.uuid)) + finalEvent.updated = finalEvent.updated.filter((c) => !deletedIds.includes(c.uuid)) + + for (const [clientUuid, client] of Object.entries(this._clients)) { + if (client.onContainer !== undefined) { + try { + // We should not block on broadcast to clients. + void client.onContainer(finalEvent) + } catch (err: any) { + console.error(`Error in client ${clientUuid} onContainer callback:`, err) + } + } + } + } + + addClient (clientUuid: ClientUuid, onContainer?: (event: ContainerEvent) => Promise): void { + this._clients.set(clientUuid, { lastSeen: this.tickManager.now(), containers: new Set(), onContainer }) + } + + removeClient (clientUuid: ClientUuid): void { + this._clients.delete(clientUuid) + } + + async get (clientUuid: ClientUuid, uuid: ContainerUuid, request: ContainerRequest): Promise { + this.ping(clientUuid) + + let client = this._clients.get(clientUuid) + if (client === undefined) { + client = { lastSeen: this.tickManager.now(), containers: new Set() } + this._clients.set(clientUuid, client) + } + client.containers.add(uuid) + + const record = await this.getContainer(uuid, request, [clientUuid]) + if (record.endpoint instanceof Promise) { + return await record.endpoint + } + return record.endpoint + } + + async getContainer ( + uuid: ContainerUuid, + request: ContainerRequest, + clients: ClientUuid[] + ): Promise { + const existing = this._containers.get(uuid) + if (existing !== undefined) { + const agent = this._agents.get(existing) + const containerImpl = agent?.containers?.get(uuid) + if (containerImpl !== undefined) { + if (!(containerImpl.endpoint instanceof Promise)) { + this._orphanedContainers.delete(containerImpl.endpoint) + } + for (const cl of clients) { + containerImpl.clients.add(cl) + } + return containerImpl + } + } + // Select agent using round/robin and register it in agent + const agent = Array.from(this._agents.values())[++this.idx % this._agents.size] + + const record: ContainerRecordImpl = { + record: { + uuid, + agentId: agent.api.uuid, + kind: request.kind, + lastVisit: this.tickManager.now(), + endpoint: '' as ContainerEndpointRef, // Placeholder, will be updated later + labels: request.labels, + extra: request.extra + }, + clients: new Set(clients), + endpoint: agent.api.get(uuid, request) + } + agent.containers.set(uuid, record) + this._containers.set(uuid, agent.api.uuid) + + // Wait for endpoint to be established + try { + const endpointRef = await record.endpoint + record.endpoint = endpointRef + this.eventQueue.push({ + added: [record.record], + deleted: [], + updated: [] + }) + return record + } catch (err: any) { + this._containers.delete(uuid) // Remove from active container registry + throw new Error(`Failed to get endpoint for container ${uuid}: ${err.message}`) + } + } + + async release (client: ClientUuid, uuid: ContainerUuid): Promise { + const _client = this._clients.get(client) + _client?.containers.delete(uuid) + + const existing = this._containers.get(uuid) + if (existing !== undefined) { + const agent = this._agents.get(existing) + const containerImpl = agent?.containers?.get(uuid) + if (containerImpl !== undefined) { + containerImpl.clients.delete(client) + if (containerImpl.clients.size === 0 && !(containerImpl.endpoint instanceof Promise)) { + this._orphanedContainers.set(containerImpl.endpoint, containerImpl) + } + } + } + } + + async terminate (container: ContainerRecordImpl): Promise { + this._containers.delete(container.record.uuid) // Remove from active container registry + this.eventQueue.push({ + added: [], + deleted: [container.record], + updated: [] + }) + const agent = this._agents.get(container.record.agentId) + agent?.containers.delete(container.record.uuid) + + let endpoint = container.endpoint + if (endpoint instanceof Promise) { + endpoint = await endpoint + } + await agent?.api.terminate(endpoint) + } + + /** + * Mark an agent as alive (updates lastSeen timestamp) + */ + ping (id: AgentUuid | ClientUuid): void { + const agent = this._agents.get(id as AgentUuid) + if (agent != null) { + agent.lastSeen = this.tickManager.now() + } + + // Agent could be also a client. + const client = this._clients.get(id as ClientUuid) + if (client != null) { + client.lastSeen = this.tickManager.now() + } + } + + async handleTimeout (client: ClientUuid): Promise { + // Handle outdated clients + const clientRecord = this._clients.get(client) + if (clientRecord !== undefined) { + for (const uuid of clientRecord.containers) { + await this.release(client, uuid) + } + } + this._clients.delete(client) + } + + /** + * Perform periodic health check of all registered agents + */ + private async checkAlive (): Promise { + const now = this.tickManager.now() + const deadAgents: AgentUuid[] = [] + + // Check each agent's last seen time + for (const [agentId, agentRecord] of this._agents.entries()) { + const timeSinceLastSeen = now - agentRecord.lastSeen + + if (timeSinceLastSeen > timeouts.aliveTimeout * 1000) { + console.warn(`Agent ${agentId} has been inactive for ${Math.round(timeSinceLastSeen / 1000)}s, marking as dead`) + deadAgents.push(agentId) + } + } + + // Remove dead agents and their containers + for (const agentId of deadAgents) { + await this.processDeadAgent(agentId) + } + + // Handle termination of orphaned containers + for (const container of [...this._orphanedContainers.values()]) { + void this.terminate(container).catch((err) => { + console.error(`Failed to terminate orphaned container ${container.record.uuid}: ${err.message}`) + }) + } + } + + /** + * Remove a dead agent and clean up its containers + */ + private async processDeadAgent (agentId: AgentUuid): Promise { + const agent = this._agents.get(agentId) + if (agent == null) { + return + } + + console.log(`Removing dead agent ${agentId} and its ${agent.containers.size} containers`) + + // Collect containers to remove + const affectedContainers: ContainerRecordImpl[] = [] + for (const [containerId, containerRecord] of agent.containers.entries()) { + affectedContainers.push(containerRecord) + this._containers.delete(containerId) + } + + // Remove agent + this._agents.delete(agentId) + + const containerEvent: ContainerEvent = { + added: [], + deleted: [], + updated: [] + } + + // We need to add requests for all used containers + for (const container of affectedContainers) { + if (!this._orphanedContainers.delete(container.record.endpoint)) { + // Container is used, we need to re-create it + const containerImpl = await this.getContainer( + container.record.uuid, + { kind: container.record.kind, extra: container.record.extra, labels: container.record.labels }, + [...Array.from(container.clients)] + ) + let endpoint = containerImpl.endpoint + if (endpoint instanceof Promise) { + endpoint = await endpoint + } + containerEvent.updated.push({ ...container.record, endpoint }) + } else { + containerEvent.deleted.push(container.record) + } + } + if (affectedContainers.length > 0) { + this.eventQueue.push(containerEvent) + } + } +} diff --git a/network/core/src/node/node.ts b/network/core/src/node/node.ts new file mode 100644 index 00000000000..c2878e35b8b --- /dev/null +++ b/network/core/src/node/node.ts @@ -0,0 +1,291 @@ +// import type { ClientBroadcast } from '../api/client' +// import type { NodeData, NodeDiscovery, WorkspaceDiscovery } from '../api/discovery' +// import type { Node, NodeAskOptions, NodeFactory, NodeManager, Workspace, WorkspaceFactory } from '../api/node' +// import type { Request, RequestAkn, Response, ResponseValue } from '../api/request' +// import { timeouts } from '../api/timeouts' +// import type { AccountUuid, NodeUuid, WorkspaceUuid } from '../api/types' +// import type { TickManager } from '../api/utils' +// import { groupByArray } from '../utils' + +// class WorkspaceSession { +// workspace: Workspace | Promise +// tick: number +// lastUse: number +// state: 'ready' | 'suspended' + +// constructor (workspace: Workspace | Promise, tick: number, lastUse: number, state: 'ready' | 'suspended') { +// this.workspace = workspace +// this.tick = tick +// this.lastUse = lastUse +// this.state = state +// } + +// async getWorkspace (): Promise { +// if (this.workspace instanceof Promise) { +// this.workspace = await this.workspace +// } +// return this.workspace +// } + +// async suspend (): Promise { +// const ws = await this.getWorkspace() +// if (this.state === 'ready') { +// this.workspace = ws.suspend().then(() => ws) +// await this.workspace +// this.state = 'suspended' +// } +// } + +// async resume (): Promise { +// const ws = await this.getWorkspace() +// if (this.state === 'suspended') { +// this.workspace = ws.resume().then(() => ws) +// await this.workspace +// this.state = 'ready' +// } +// } +// } + +// export class NodeImpl implements Node { +// workspaces: Record = {} +// constructor ( +// readonly _id: NodeUuid, +// readonly workspaceFactory: WorkspaceFactory, +// readonly workspaceDiscovery: WorkspaceDiscovery, +// readonly discovery: NodeManager, +// readonly tickManager: TickManager, +// readonly onClose?: () => Promise +// ) { +// this.tickManager.register(async (tick, tps) => { +// this.handleWorkspaceClose(tick, tps) +// }) +// } + +// onClientBroadcast?: ClientBroadcast + +// handleWorkspaceClose (tick: number, tps: number): void { +// const now = this.tickManager.now() +// for (const [wsid, { workspace, tick: wstick, lastUse }] of Object.entries(this.workspaces)) { +// if (tick % tps === wstick && !(workspace instanceof Promise)) { +// if (now - lastUse > timeouts.closeWorkspaceTimeout) { +// // Not used for 5 minutes, close it +// // eslint-disable-next-line @typescript-eslint/no-dynamic-delete +// delete this.workspaces[wsid as WorkspaceUuid] +// void workspace.suspend().catch() +// } +// } +// } +// } + +// async workspace (workspaceId: WorkspaceUuid): Promise { +// let workspace = this.workspaces[workspaceId] +// if (workspace == null) { +// // Create and store the promise immediately to prevent race conditions +// const wrk = this.workspaceFactory(workspaceId) +// const tick = this.tickManager.nextHash() +// workspace = new WorkspaceSession(wrk, tick, this.tickManager.now(), 'ready') +// this.workspaces[workspaceId] = workspace +// } +// if (workspace.state === 'suspended') { +// await workspace.resume() +// } + +// return workspace +// } + +// async ask(req: Request, options?: NodeAskOptions): Promise { +// const result: RequestAkn = { +// workspaces: {} +// } +// const workspaces = options?.target ?? (await this.workspaceDiscovery.byAccount(req.account)) + +// const byNode = await this.groupWorkspaces(workspaces) + +// // For self workspaces we need to resolve child workspaces. +// await this.includeChildWorkspaces(byNode, options) + +// const promises: Array> = [] +// for (const [node, _workspaces] of byNode.entries()) { +// const workspaces = _workspaces.filter((ws) => req.workspaces[ws] == null) +// if (workspaces.length === 0) { +// continue +// } + +// if (node === this._id) { +// const localWorkspaces = this.getFilteredWorkspaces(workspaces, options) + +// for (const ws of localWorkspaces) { +// req.workspaces[ws] = this._id +// result.workspaces[ws] = this._id +// } + +// void this.askLocal(req, localWorkspaces).catch((err) => { +// console.error('failed to ask local workspaces', err) +// }) +// } else { +// const wrk = await this.discovery.node(node) +// promises.push(this.askTo(req, wrk, workspaces, result, options)) +// } +// } +// await Promise.all(promises) +// return result +// } + +// private getFilteredWorkspaces (workspaces: WorkspaceUuid[], options: NodeAskOptions | undefined): WorkspaceUuid[] { +// let localWorkspaces = workspaces +// if (options?.workspace !== undefined) { +// const wsSet = new Set(options?.workspace) +// localWorkspaces = workspaces.filter((it) => wsSet.has(it)) +// } +// return localWorkspaces +// } + +// private async groupWorkspaces (workspaces: WorkspaceUuid[]): Promise> { +// const byNode = new Map() +// for (const workspace of workspaces) { +// const node = await this.discovery.byWorkspace(workspace) +// byNode.set(node, (byNode.get(node) ?? []).concat(workspace)) +// } +// return byNode +// } + +// private async includeChildWorkspaces ( +// byNode: Map, +// options?: NodeAskOptions +// ): Promise { +// const selfWorkspace = byNode.get(this._id) ?? [] +// if (selfWorkspace.length > 0) { +// let optionsSet: Set | undefined +// if (options?.workspace !== undefined) { +// // We need to enhance options to include child workspaces +// optionsSet = new Set(options.workspace) +// } +// for (const ws of selfWorkspace) { +// const childWs = await this.workspaceDiscovery.byWorkspace(ws) +// if (options?.workspace !== undefined && optionsSet !== undefined && optionsSet.has(ws)) { +// options.workspace.push(...childWs) +// } +// for (const cws of childWs) { +// const node = await this.discovery.byWorkspace(cws) +// byNode.set(node, (byNode.get(node) ?? []).concat(cws)) +// } +// } +// } +// } + +// async askTo( +// req: Request, +// wrk: Node, +// workspaces: WorkspaceUuid[], +// result: RequestAkn, +// options?: NodeAskOptions +// ): Promise { +// const response = await wrk.ask(req, { ...options, target: workspaces }) +// const localWorkspaces = new Set( +// this.getFilteredWorkspaces(Object.keys(response.workspaces) as WorkspaceUuid[], options) +// ) +// if (localWorkspaces.size > 0) { +// for (const [ws, nodeId] of Object.entries(response.workspaces)) { +// if (!localWorkspaces.has(ws as WorkspaceUuid)) { +// continue +// } +// result.workspaces[ws as WorkspaceUuid] = nodeId +// req.workspaces[ws as WorkspaceUuid] = nodeId +// } +// } +// } + +// async askLocal(req: Request, workspaces: WorkspaceUuid[]): Promise { +// const targetNode = await this.discovery.byAccount(req.account) +// const target = targetNode === this._id ? this : await this.discovery.node(targetNode) +// for (const ws of workspaces) { +// const worker = await this.workspace(ws) +// const data = await (await worker.getWorkspace()).ask(req) +// await target.broadcast([ +// { +// _id: req._id, +// account: req.account, +// workspaceId: ws, +// nodeId: this._id, +// data +// } +// ]) +// } +// } + +// async modify(workspaceId: WorkspaceUuid, req: Request): Promise> { +// const wsNode = await this.discovery.byWorkspace(workspaceId) + +// if (wsNode === this._id) { +// const wrk = await this.workspace(workspaceId) +// return await (await wrk.getWorkspace()).modify(req) +// } +// const node = await this.discovery.node(wsNode) +// return await node.modify(workspaceId, req) +// } + +// async broadcast(req: Array>): Promise { +// const byAccount = groupByArray(req, (it) => it.account) +// for (const [account, values] of byAccount.entries()) { +// const nodeId = await this.discovery.byAccount(account) +// if (this._id === nodeId) { +// // Broadcast to local clients +// await this.onClientBroadcast?.(account, values) +// } else { +// // Broadcast to remote node +// const wrk = await this.discovery.node(nodeId) +// await wrk.broadcast(values) +// } +// } +// } + +// async ping (workspaces: WorkspaceUuid[], processChildren: boolean): Promise { +// const wsSet = new Set(workspaces) + +// if (processChildren) { +// const toProcess = Array.from(wsSet) + +// while (toProcess.length > 0) { +// const ws = toProcess.pop() +// if (ws === undefined) { +// break +// } +// const childWs = await this.workspaceDiscovery.byWorkspace(ws) +// for (const cws of childWs) { +// if (!wsSet.has(cws)) { +// wsSet.add(cws) +// toProcess.push(cws) +// } +// } +// } +// } + +// const byNode = await this.groupWorkspaces(Array.from(wsSet)) + +// for (const [node, workspaces] of byNode.entries()) { +// if (node === this._id) { +// // Ping local workspaces +// for (const ws of workspaces) { +// const wrk = await this.workspace(ws) +// wrk.lastUse = this.tickManager.now() +// } +// } else { +// const wrk = await this.discovery.node(node) +// await wrk.ping(workspaces, false) +// } +// } +// } + +// async close (): Promise { +// for (const { workspace } of Object.values(this.workspaces)) { +// if (workspace instanceof Promise) { +// await workspace.then(async (w) => { +// await w.close() +// }) +// } else { +// await workspace.close() +// } +// } +// await this.onClose?.() +// } +// } diff --git a/network/core/src/node/session.ts b/network/core/src/node/session.ts new file mode 100644 index 00000000000..85ec4b5b74c --- /dev/null +++ b/network/core/src/node/session.ts @@ -0,0 +1,142 @@ +// import { v4 as uuid } from 'uuid' +// import { type AskOptions, type Client, type SessionManager } from '../api/client' +// import type { NodeDiscovery, WorkspaceDiscovery } from '../api/discovery' +// import type { Node } from '../api/node' +// import type { Request, RequestAkn, RequestId, Response, ResponseValue } from '../api/request' +// import { timeouts } from '../api/timeouts' +// import type { AccountUuid, WorkspaceUuid } from '../api/types' +// import type { TickManager } from '../api/utils' +// import type { NodeImpl } from './node' + +// interface RequestData { +// request: Request +// time: number +// responses: Array> +// akn: RequestAkn | undefined +// promise: Promise> + +// resolve: (value: ResponseValue) => void +// reject: (err: Error) => void +// } +// class SessionImpl implements Client { +// requests = new Map>() + +// onClose?: () => void +// onBroadcast?: ((response: Response) => void) | undefined + +// lastOp: number = performance.now() + +// constructor ( +// readonly account: AccountUuid, +// readonly sessionId: string, +// readonly localNode: Node, +// readonly tick: number +// ) {} + +// async ask(req: T, options?: AskOptions): Promise> { +// this.lastOp = performance.now() + +// const requestId = uuid() as RequestId +// const request: Request = { +// _id: requestId, +// account: this.account, +// data: req, +// workspaces: {} +// } + +// let resolveRequest = (value: ResponseValue): void => {} +// let rejectRequest = (_: Error): void => {} + +// const rdata: RequestData = { +// request, +// time: Date.now(), +// akn: undefined, +// responses: [], +// resolve: () => {}, +// reject: () => {}, +// promise: new Promise>((resolve, reject) => { +// resolveRequest = resolve as RequestData['resolve'] +// rejectRequest = reject as RequestData['reject'] +// }) +// } +// this.requests.set(requestId, rdata) +// rdata.resolve = resolveRequest +// rdata.reject = rejectRequest + +// rdata.akn = await this.localNode.ask(request, { ...(options ?? {}), target: undefined }) + +// this.checkResponses(rdata, rdata.responses) + +// return await rdata.promise +// } + +// async modify(workspaceId: WorkspaceUuid, req: T): Promise> { +// this.lastOp = performance.now() + +// const requestId = uuid() as RequestId +// const request: Request = { +// _id: requestId, +// account: this.account, +// data: req, +// workspaces: {} +// } +// return await this.localNode.modify(workspaceId, request) +// } + +// checkResponses (rdata: RequestData, responses: Array>): void { +// for (const response of responses) { +// if (response._id == null) { +// continue +// } +// if (rdata.akn?.workspaces[response.workspaceId] !== undefined) { +// // eslint-disable-next-line @typescript-eslint/no-dynamic-delete +// delete rdata.akn.workspaces[response.workspaceId] +// } +// } +// if (rdata.akn !== undefined && Object.keys(rdata.akn.workspaces).length === 0) { +// rdata.responses.sort((a, b) => a.workspaceId.localeCompare(b.workspaceId)) + +// // Flatten all response values properly +// const allValues = rdata.responses.flatMap((r) => r.data.value) +// const totalCount = rdata.responses.reduce((sum, r) => sum + r.data.total, 0) + +// rdata.resolve({ value: allValues, total: totalCount }) +// this.requests.delete(rdata.request._id) +// } +// } + +// handleResponse(responses: Array>): void { +// for (const response of responses) { +// if (response._id == null) { +// // This is a broadcast response, call the callback if it exists. +// this.onBroadcast?.(response) +// continue +// } + +// const rdata = this.requests.get(response._id) +// if (rdata == null) { +// console.warn('Response for unknown request', response._id, response) +// continue +// } + +// rdata.responses.push(response) + +// this.checkResponses(rdata, [response]) +// } +// } + +// close (): void { +// this.onClose?.() +// } + +// async retryIfNeeded (time: number): Promise { +// for (const [, rdata] of this.requests.entries()) { +// if (time - rdata.time > timeouts.retryTimeout) { +// const wsretry = Array.from(Object.keys(rdata.akn?.workspaces ?? {})) as WorkspaceUuid[] +// if (wsretry.length > 0) { +// await this.localNode.ask(rdata.request, { target: wsretry }) +// } +// } +// } +// } +// } diff --git a/network/core/src/utils.ts b/network/core/src/utils.ts new file mode 100644 index 00000000000..2cb187829de --- /dev/null +++ b/network/core/src/utils.ts @@ -0,0 +1,88 @@ +import type { TickHandler, TickManager } from './api/utils' + +export function groupByArray (array: T[], keyProvider: (item: T) => K): Map { + const result = new Map() + + array.forEach((item) => { + const key = keyProvider(item) + + if (!result.has(key)) { + result.set(key, [item]) + } else { + result.get(key)?.push(item) + } + }) + + return result +} + +/** + * Handles a time unification and inform about ticks. + */ +export class TickManagerImpl implements TickManager { + handlers: [TickHandler, number, number][] = [] + + hashCounter: number = 0 + + _tick: number = 0 + + constructor (readonly tps: number) { + if (tps > 1000 || tps < 1) { + throw new Error('Ticks per second has an invalid value: must be >= 1 && <= 1000') + } + } + + now (): number { + // Use performance.now() when available, otherwise fall back to Date.now() + // performance is available in recent Node versions, but guard for portability. + return (globalThis as any).performance?.now?.() ?? Date.now() + } + + nextHash (): number { + // Use post-increment so first hash can be 0, and avoid negative modulo issues. + return this.hashCounter++ % this.tps + } + + register (handler: TickHandler, interval: number): void { + if (!Number.isFinite(interval) || interval < 1) { + throw new Error('Interval must be a finite number >= 1 (seconds)') + } + const hash = this.nextHash() + this.handlers.push([handler, hash, interval]) + } + + async tick (): Promise { + this._tick++ + for (const [h, hash, interval] of this.handlers) { + try { + if (this.isMe(hash, interval)) { + await h() + } + } catch (err: any) { + console.error(`Error in tick handler for tick ${this._tick}:`, err) + } + } + } + + isMe (tickId: number, seconds: number): boolean { + if (!Number.isFinite(seconds) || seconds < 1) return false + // triggers once every (tps * seconds) ticks at the offset `tickId % tps` + return this._tick % (this.tps * seconds) === tickId % this.tps + } + + stop = () => {} + + start (): void { + const to = setInterval( + () => { + this.tick().catch((err) => { + console.error('Error in tick manager:', err) + }) + }, + Math.round(1000 / this.tps) + ) + this.stop = () => { + clearInterval(to) + } + } +} diff --git a/network/core/tsconfig.json b/network/core/tsconfig.json new file mode 100644 index 00000000000..c6a877cf6c3 --- /dev/null +++ b/network/core/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/network/docs/Schema.png b/network/docs/Schema.png new file mode 100644 index 0000000000000000000000000000000000000000..bcdbb8d42a4c6184b630732faa6f9447db9dc3e7 GIT binary patch literal 214102 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 z zw=H-tsn_|U&Kc#8@F0?3MG|G=)tVb_7v8MZs2-xnLuD8$QOD;h- zz7)DO-Zjtf9JYt^y1e>_XI0KhxkDV-NO^McCWu9#Rh7uOk+!0*2|SCu|92*K>g|L;moe{ukKIpOK-|vTj*_l5+|SaG?wG#SJlGrpzr|>3Dp9cm?{HU zKZUB}2=5?aq(55)oW#KgSbWkYeJ6xqris#1p``3WjFhXrb(K1EqVLPHyQfmRX%hoH zfkXk~99XMU_SpAXu+sL&wh;Ahh)w9p $!V2h}XcqGD)kzY(?$qyXg0|AQk~)eMStWF;8dRgr2R_ znAePrpbbs$-*`nvZhPss3R$5g`GN3I#An5y%}+nCne0{Li|9u3W#1_TZ@!ds|M~C` zC~|QkLR&o#GDT3`_c7ap&nM5ivN~d8ok*>N52{W;Y(tV(e*K}`TJ=JwMe)=tpc!aY zJl-B!T#u1kb3kMU2Pm%arkGh(P5XjpW=-1wyLC=;!I*T|=yTvg7Te=GHn?NuD{Sh8 z$@xI3iA^zshrk`8byp(yxCXh&Qpv7o;|Uuaq|1AQpKg~ zcX*cIv#;#<40Ep!_LUq}CQ3{ukBTkJ8w$3WLXfu9!e9Pr*qe{;I4s;<3%jn_&Ubai z0tdPs;@%}zB^BP6D*0Tws17WMru2FS>mQ`dL-wZ0`+K&MijbS%e118T7l9XMKclNA z zBpYAbx;-jXk71u^LvM2Y%v|P4$Lh){LyE>+CP;u5(wS09C)k?VXEZ#Fg%pdrV2BTm zZus`AvGKv!>BlZv2Cxr|=F|p=Hd&q+@oeHVe*2Ays!h-6h_pm^hEx7=O(!5tkUtB$ zPL*C+YvdoEUYE9`Q$#m`$OtH!;8pnb(OM%)f≦WYiL(tbE`JtHC?VwV$XyMM!24 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

`IUwc)&aIf^ckHOo{& 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

i%gZP5|Ntj8RZGq_gSymN2d;(cxFu^-{z; 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: Request, options?: NodeAskOptions) => Promise + + /** + * Send a modification request to a specific workspace + * @param workspaceId - Target workspace identifier + * @param req - The modification request + * @returns Promise resolving to the response value + */ + modify: (workspaceId: WorkspaceUuid, req: Request) => Promise> + + /** + * Health check for workspaces + * @param workspaces - Array of workspace identifiers to ping + * @param processChildren - Whether to include child workspaces + */ + ping: (workspaces: WorkspaceUuid[], processChildren: boolean) => Promise + + /** + * Inform clients about some request/Response + * @param req - Array of responses to broadcast + */ + broadcast: (req: Array>) => Promise + + /** + * Gracefully close the node and cleanup resources + */ + close: () => Promise +} +``` + +### Workspace Interface + +Interface for individual workspace instances within nodes. + +```typescript +interface Workspace { + /** + * Unique identifier for the workspace + */ + _id: WorkspaceUuid + + /** + * Execute a query on the workspace + * @param req - The query request + * @returns Promise resolving to the query result + */ + ask: (req: Request) => Promise> + + /** + * Execute a modification on the workspace + * @param req - The modification request + * @returns Promise resolving to the modification result + */ + modify: (req: Request) => Promise> + + /** + * Suspend any system resources, be ready for a resume before any new requests. + */ + suspend: () => Promise + + /** + * A restore state and be able to respond for user actions. + */ + resume: () => Promise + + /** + * Permanently close the workspace and cleanup all resources + */ + close: () => Promise +} +``` + +## 🏗️ Node Management + +### NodeManager Interface + +Central manager for node discovery and access. + +```typescript +interface NodeManager extends NodeDiscovery { + /** + * Get a node instance by its identifier + * @param node - Node identifier + * @returns Promise resolving to the node instance + */ + node: (node: NodeUuid) => Promise +} +``` + +### NodeFactory Type + +Factory function for creating node instances. + +```typescript +type NodeFactory = (node: NodeUuid) => Promise +``` + +### NodeAskOptions + +Extended options for node query operations. + +```typescript +interface NodeAskOptions extends AskOptions { + /** + * Specific workspaces to target for the request + * If not specified, all accessible workspaces are queried + */ + target?: WorkspaceUuid[] +} +``` + +## 🏢 Workspace Operations + +### WorkspaceFactory Type + +Factory function for creating workspace instances. + +```typescript +type WorkspaceFactory = (workspaceId: WorkspaceUuid) => Promise +``` + +### Workspace Lifecycle + +Workspaces follow a specific lifecycle pattern: + +```mermaid +stateDiagram-v2 + [*] --> Created: WorkspaceFactory() + Created --> Active: First Request + Active --> Suspended: suspend() + Suspended --> Active: resume() + Active --> Closed: close() + Suspended --> Closed: close() + Closed --> [*] +``` + +### Example Workspace Implementation + +```typescript +class WorkspaceImpl implements Workspace { + constructor(public readonly _id: WorkspaceUuid, private pipeline: Pipeline, private config: WorkspaceConfig) {} + + async ask(req: Request): Promise> { + try { + // Validate request + await this.validateRequest(req) + + // Execute query through pipeline + const result = await this.pipeline.ask(req) + + // Transform and return result + return this.transformResponse(result) + } catch (error) { + throw new NetworkError('Query failed', { cause: error }) + } + } + + async modify(req: Request): Promise> { + try { + // Start transaction + const transaction = await this.pipeline.startTransaction() + + // Execute modification + const result = await transaction.modify(req) + + // Commit transaction + await transaction.commit() + + return this.transformResponse(result) + } catch (error) { + // Rollback on error + await transaction?.rollback() + throw new NetworkError('Modification failed', { cause: error }) + } + } + + async suspend(): Promise { + await this.pipeline.suspend() + this.config.suspended = true + } + + async resume(): Promise { + await this.pipeline.resume() + this.config.suspended = false + } + + async close(): Promise { + await this.pipeline.close() + } +} +``` + +## 🔍 Discovery Services + +### NodeDiscovery Interface + +Service for discovering and managing node topology. + +```typescript +interface NodeDiscovery { + /** + * Get the node responsible for a specific workspace + * @param workspace - Workspace identifier + * @returns Node identifier + */ + byWorkspace: (workspace: WorkspaceUuid) => Promise + + /** + * Get the node responsible for a specific account + * @param account - Account identifier + * @returns Node identifier + */ + byAccount: (account: AccountUuid) => Promise + + /** + * Get all available nodes + * @returns Iterable of node identifiers + */ + list: () => Iterable + + /** + * Get statistics/metadata for a specific node + * @param node - Node identifier + * @returns Node metadata + */ + stats: (node: NodeUuid) => Promise +} + +/** + * Node metadata type + */ +type NodeData = Record +``` + +### WorkspaceDiscovery Interface + +Service for discovering workspace locations and relationships. + +````typescript +interface WorkspaceDiscovery { + /** + * Get all workspaces accessible by an account + * @param account - Account identifier + * @returns Array of workspace identifiers + */ + byAccount: (account: AccountUuid) => Promise + + /** + * Get child workspaces of a parent workspace + * @param workspace - Parent workspace identifier + * @returns Array of child workspace identifiers + */ + byWorkspace: (workspace: WorkspaceUuid) => Promise +} + +### AccountDiscovery Interface + +Service for discovering accounts associated with workspaces. + +```typescript +interface AccountDiscovery { + /** + * Get all accounts that have access to a specific workspace + * @param workspace - Workspace identifier + * @returns Array of account identifiers + */ + byWorkspace: (workspace: WorkspaceUuid) => Promise +} +```` + +## 🚀 Transport Layer + +### ClientTransport Interface + +Interface for client-side transport communication. + +```typescript +interface ClientTransport { + /** + * Send a request to a specific client + * @param clientId - Account identifier + * @param reqId - Request identifier + * @param body - Request body + * @returns Promise resolving to response + */ + request: (clientId: AccountUuid, reqId: RequestId, body: any) => Promise + + /** + * Subscribe to messages for an account + * @param account - Account identifier + */ + subscribe: (account: AccountUuid) => void + + /** + * Unsubscribe from messages for an account + * @param account - Account identifier + */ + unsubscribe: (account: AccountUuid) => void + + /** + * Close the transport connection + */ + close: () => Promise +} +``` + +### ServerTransport Interface + +Interface for server-side transport communication. + +```typescript +interface ServerTransport { + /** + * Node identifier for this transport + */ + nodeId: NodeUuid + + /** + * Send a request to a target node + * @param target - Target node identifier + * @param body - Request body + * @returns Promise resolving to response + */ + request: (target: NodeUuid, body: any) => Promise + + /** + * Send a message to a target node + * @param target - Target node identifier + * @param reqId - Request identifier (optional) + * @param body - Message body + */ + send: (target: NodeUuid, reqId: RequestId | undefined, body: any) => Promise + + /** + * Close the transport connection + */ + close: () => Promise +} +``` + +### Static Discovery Implementation + +```typescript +class StaticNodeDiscovery implements NodeDiscovery { + private nodes: Map + private accountHashRing: ConsistentHashRing + + constructor(nodes: Array<[NodeUuid, NodeMetadata]>) { + this.nodes = new Map(nodes) + this.accountHashRing = new ConsistentHashRing(Array.from(this.nodes.keys())) + } + + async getAccountNode(account: AccountUuid): Promise { + return this.accountHashRing.getNode(account) + } + + async getNodes(): Promise { + return Array.from(this.nodes.keys()) + } + + async registerNode(node: NodeUuid, metadata: NodeMetadata): Promise { + this.nodes.set(node, metadata) + this.accountHashRing.addNode(node) + } + + async unregisterNode(node: NodeUuid): Promise { + this.nodes.delete(node) + this.accountHashRing.removeNode(node) + } +} +``` + +## 👥 Session Management + +### SessionManager Interface + +Central coordinator for client sessions and workspace access. + +```typescript +interface SessionManager { + /** + * Register a new client session + * @param account - Account identifier + * @param sessionId - Session identifier + * @returns Client interface for the session + */ + register: (account: AccountUuid, sessionId: string) => Promise + + /** + * Unregister and close a client session + * @param sessionId - Session identifier + */ + unregister: (sessionId: string) => Promise + + /** + * Close the session manager and all active sessions + */ + close: () => void +} +``` + +### Client Interface + +Interface for client sessions to interact with the network. + +```typescript +interface Client { + /** + * Account associated with this client + */ + account: AccountUuid + + /** + * Unique session identifier + */ + sessionId: string + + /** + * Send a query request + * @param req - The request data + * @param options - Optional request configuration + * @returns Promise resolving to the response + */ + ask: (req: T, options?: AskOptions) => Promise> + + /** + * Send a modification request to a specific workspace + * @param workspaceId - Target workspace identifier + * @param req - The modification request data + * @returns Promise resolving to the response + */ + modify: (workspaceId: WorkspaceUuid, req: T) => Promise> + + /** + * Callback for handling broadcast messages + */ + onBroadcast?: (response: Response) => void + + /** + * Callback for handling session close + */ + onClose?: () => void +} +``` + +## 📨 Request/Response Types + +### Core Types + +```typescript +/** + * Unique identifier types + */ +type WorkspaceUuid = string & { __workspaceUuid: true } +type AccountUuid = string & { __accountUuid: true } +type NodeUuid = string & { __nodeUuid: true } + +/** + * Request identifier type + */ +type RequestId = string & { __requestId: true } + +/** + * Request structure + */ +interface Request { + _id: RequestId + account: AccountUuid + + // Workspace filter + workspace?: WorkspaceUuid | WorkspaceUuid[] + + workspaces: Record // A list of already processed workspaces. + data: T +} + +/** + * Response structure + */ +interface Response { + _id: RequestId | undefined + account: AccountUuid + + nodeId: NodeUuid + workspaceId: WorkspaceUuid + data: ResponseValue +} + +/** + * Response value wrapper + */ +interface ResponseValue { + value: T[] + total: number +} + +/** + * Request acknowledgment + */ +interface RequestAkn { + // A list of nodes we need to retrieve data from, or retry to ask again if required. + workspaces: Record +} +``` + +### Request Options + +```typescript +interface AskOptions { + /** + * Specific workspaces to target for the request + */ + workspace?: WorkspaceUuid[] +} +``` + +### Node Metadata + +```typescript +interface NodeMetadata { + /** + * Geographic region where the node is located + */ + region: string + + /** + * Processing capacity of the node + */ + capacity: number + + /** + * Network endpoints for the node + */ + endpoints?: { + internal: string + external: string + } + + /** + * Node status information + */ + status?: { + healthy: boolean + lastSeen: number + version: string + } +} +``` + +## ❌ Error Handling + +### NetworkError Class + +```typescript +class NetworkError extends Error { + constructor(message: string, public code?: string, public details?: any) { + super(message) + this.name = 'NetworkError' + } +} +``` + +### Error Types + +```typescript +enum NetworkErrorCode { + // Connection errors + CONNECTION_FAILED = 'CONNECTION_FAILED', + CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT', + CONNECTION_REFUSED = 'CONNECTION_REFUSED', + + // Authentication errors + AUTHENTICATION_FAILED = 'AUTHENTICATION_FAILED', + AUTHORIZATION_FAILED = 'AUTHORIZATION_FAILED', + SESSION_EXPIRED = 'SESSION_EXPIRED', + + // Request errors + INVALID_REQUEST = 'INVALID_REQUEST', + REQUEST_TIMEOUT = 'REQUEST_TIMEOUT', + RATE_LIMITED = 'RATE_LIMITED', + + // Workspace errors + WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', + WORKSPACE_UNAVAILABLE = 'WORKSPACE_UNAVAILABLE', + WORKSPACE_SUSPENDED = 'WORKSPACE_SUSPENDED', + + // Node errors + NODE_NOT_FOUND = 'NODE_NOT_FOUND', + NODE_UNAVAILABLE = 'NODE_UNAVAILABLE', + NODE_OVERLOADED = 'NODE_OVERLOADED', + + // System errors + INTERNAL_ERROR = 'INTERNAL_ERROR', + SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', + MAINTENANCE_MODE = 'MAINTENANCE_MODE' +} +``` + +### Error Handling Patterns + +```typescript +// Retry with exponential backoff +async function retryWithBackoff( + operation: () => Promise, + maxRetries: number = 3, + baseDelay: number = 1000 +): Promise { + let lastError: Error + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await operation() + } catch (error) { + lastError = error + + if (attempt === maxRetries) { + throw new NetworkError('Max retries exceeded', 'MAX_RETRIES', { + attempts: attempt + 1, + lastError + }) + } + + // Exponential backoff with jitter + const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000 + await new Promise((resolve) => setTimeout(resolve, delay)) + } + } + + throw lastError +} + +// Circuit breaker pattern +class CircuitBreaker { + private failures: number = 0 + private lastFailTime: number = 0 + private state: 'closed' | 'open' | 'half-open' = 'closed' + + constructor(private failureThreshold: number = 5, private timeout: number = 60000) {} + + async execute(operation: () => Promise): Promise { + if (this.state === 'open') { + if (Date.now() - this.lastFailTime > this.timeout) { + this.state = 'half-open' + } else { + throw new NetworkError('Circuit breaker is open', 'CIRCUIT_OPEN') + } + } + + try { + const result = await operation() + this.onSuccess() + return result + } catch (error) { + this.onFailure() + throw error + } + } + + private onSuccess(): void { + this.failures = 0 + this.state = 'closed' + } + + private onFailure(): void { + this.failures++ + this.lastFailTime = Date.now() + + if (this.failures >= this.failureThreshold) { + this.state = 'open' + } + } +} +``` + +## 💡 Usage Examples + +### Basic Client Usage + +```typescript +import { SessionManagerImpl, StaticNodeDiscovery, StaticWorkspaceDiscovery } from '@hcengineering/network' + +// Setup discovery services +const nodeDiscovery = new StaticNodeDiscovery([ + ['node1', { region: 'us-east', capacity: 100 }], + ['node2', { region: 'us-west', capacity: 150 }] +]) + +const workspaceDiscovery = new StaticWorkspaceDiscovery({ + user1: ['workspace1', 'workspace2'], + user2: ['workspace3'] +}) + +// Create session manager +const sessionManager = new SessionManagerImpl(nodeFactory, operationHandler, workspaceDiscovery, nodeDiscovery) + +// Register client and perform operations +async function example() { + // Register a new client session + const client = await sessionManager.register('user1' as AccountUuid, 'session1') + + // Set up broadcast handler + client.onBroadcast = (response) => { + console.log('Received broadcast:', response) + } + + // Perform a query + const queryResult = await client.ask( + { + method: 'findDocuments', + collection: 'tasks', + filter: { status: 'active' } + }, + { + timeout: 5000, + useCache: true + } + ) + + // Perform a modification + const modifyResult = await client.modify('workspace1' as WorkspaceUuid, { + method: 'updateDocument', + collection: 'tasks', + id: 'task123', + updates: { status: 'completed' } + }) + + console.log('Query result:', queryResult) + console.log('Modify result:', modifyResult) +} +``` + +### Advanced Node Implementation + +```typescript +class AdvancedNode implements Node { + private workspaces: Map = new Map() + private circuitBreaker = new CircuitBreaker() + + constructor(public readonly _id: NodeUuid, private workspaceFactory: WorkspaceFactory, private config: NodeConfig) {} + + async ask(req: Request, options?: NodeAskOptions): Promise { + const requestId = generateId() + + try { + // Process request with circuit breaker + await this.circuitBreaker.execute(async () => { + const workspaces = options?.target || (await this.getAvailableWorkspaces()) + + // Distribute query across target workspaces + const promises = workspaces.map(async (workspaceId) => { + const workspace = await this.getOrCreateWorkspace(workspaceId) + return workspace.ask(req) + }) + + // Wait for all responses with timeout + const results = await Promise.allSettled(promises) + + // Aggregate results + const aggregatedResult = this.aggregateResults(results) + + // Store result for later retrieval + await this.storeResult(requestId, aggregatedResult) + }) + + return { + id: requestId, + acknowledged: true, + timestamp: Date.now() + } + } catch (error) { + throw new NetworkError('Ask operation failed', 'ASK_FAILED', { + requestId, + error: error.message + }) + } + } + + async modify(workspaceId: WorkspaceUuid, req: Request): Promise> { + try { + const workspace = await this.getOrCreateWorkspace(workspaceId) + return await workspace.modify(req) + } catch (error) { + throw new NetworkError('Modify operation failed', 'MODIFY_FAILED', { + workspaceId, + error: error.message + }) + } + } + + async ping(workspaces: WorkspaceUuid[], processChildren: boolean): Promise { + const promises = workspaces.map(async (workspaceId) => { + try { + const workspace = this.workspaces.get(workspaceId) + if (workspace) { + // Perform health check + await this.checkWorkspaceHealth(workspace) + + if (processChildren) { + const children = await this.getChildWorkspaces(workspaceId) + await this.ping(children, false) + } + } + } catch (error) { + console.warn(`Ping failed for workspace ${workspaceId}:`, error) + } + }) + + await Promise.allSettled(promises) + } + + async broadcast(responses: Array>): Promise { + // Implement broadcast logic based on your transport layer + // This could use WebSockets, message queues, etc. + for (const response of responses) { + await this.sendToClients(response) + } + } + + async close(): Promise { + // Close all workspaces + const closePromises = Array.from(this.workspaces.values()).map((ws) => ws.close()) + await Promise.allSettled(closePromises) + + this.workspaces.clear() + } + + private async getOrCreateWorkspace(workspaceId: WorkspaceUuid): Promise { + let workspace = this.workspaces.get(workspaceId) + + if (!workspace) { + workspace = await this.workspaceFactory(workspaceId) + this.workspaces.set(workspaceId, workspace) + } + + return workspace + } + + private aggregateResults(results: PromiseSettledResult[]): any { + const successful = results + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value) + + // Implement your aggregation logic here + return successful.reduce((acc, result) => { + // Merge results based on your data structure + return { ...acc, ...result } + }, {}) + } +} +``` + +### Custom Discovery Service + +```typescript +class DatabaseNodeDiscovery implements NodeDiscovery { + constructor(private database: Database) {} + + async getAccountNode(account: AccountUuid): Promise { + const result = await this.database.query('SELECT node_id FROM account_node_mapping WHERE account_id = ?', [account]) + + if (result.length === 0) { + // Assign node using consistent hashing + const availableNodes = await this.getNodes() + const nodeId = this.hashToNode(account, availableNodes) + + // Store mapping in database + await this.database.execute('INSERT INTO account_node_mapping (account_id, node_id) VALUES (?, ?)', [ + account, + nodeId + ]) + + return nodeId + } + + return result[0].node_id as NodeUuid + } + + async getNodes(): Promise { + const result = await this.database.query('SELECT node_id FROM nodes WHERE status = "active"') + + return result.map((row) => row.node_id as NodeUuid) + } + + async registerNode(node: NodeUuid, metadata: NodeMetadata): Promise { + await this.database.execute( + 'INSERT OR REPLACE INTO nodes (node_id, metadata, status, last_seen) VALUES (?, ?, "active", ?)', + [node, JSON.stringify(metadata), Date.now()] + ) + } + + async unregisterNode(node: NodeUuid): Promise { + await this.database.execute('UPDATE nodes SET status = "inactive" WHERE node_id = ?', [node]) + } + + private hashToNode(account: AccountUuid, nodes: NodeUuid[]): NodeUuid { + // Simple hash-based selection + const hash = this.simpleHash(account) + const index = hash % nodes.length + return nodes[index] + } + + private simpleHash(str: string): number { + let hash = 0 + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // Convert to 32-bit integer + } + return Math.abs(hash) + } +} +``` + +--- + +For more detailed examples and advanced usage patterns, refer to the [Huly Examples Repository](https://github.com/hcengineering/huly-examples). diff --git a/network/docs/readme.md b/network/docs/readme.md new file mode 100644 index 00000000000..181b7d1a5b8 --- /dev/null +++ b/network/docs/readme.md @@ -0,0 +1,208 @@ +# Huly Virtual Network + +A distributed network architecture for the Huly platform that enables scalable, fault-tolerant communication between accounts, workspaces, and nodes. + +![Schema](./Schema.png) + +## Overview + +The Huly virtual network implements a distributed system with the following key components: + +- **Nodes**: Computational units that handle requests and manage workspaces +- **Workspaces**: Isolated environments that contain application data and logic +- **Accounts**: User identities that can access multiple workspaces +- **Sessions**: Client connections to the network + +## Architecture Components + +### Account → Workspace Mapping + +The `AccountDB` is responsible for mapping `AccountUuid` to `WorkspaceUuid[]` representing all workspaces accessible by a given account. + +**Key Features:** + +- Multi-tenant workspace access +- Role-based permissions (Owner, Member, Guest) +- Workspace discovery and enumeration + +### Account → Node Mapping + +Each account is mapped to a specific node using a consistent hashing algorithm: + +```text +AccountUuid → hash → DHT → NodeId +``` + +**Implementation:** + +```typescript +hash(AccountUuid) % nodes.length +``` + +**Benefits:** + +- Load balancing across nodes +- Consistent routing for user operations +- Fault tolerance through re-hashing + +### Workspace → Workspace Mapping + +Workspaces can aggregate content from sub-workspaces, enabling hierarchical organization and unified access patterns. + +**Features:** + +- Parent-child workspace relationships +- Cascading operations across workspace hierarchies +- Unified query interface for related workspaces + +### Workspace Lifecycle Management + +Workspaces have complex startup/shutdown cycles managed by the network: + +- **Lazy Loading**: Workspaces are activated on-demand +- **Resource Management**: Automatic cleanup of unused workspaces +- **Health Monitoring**: Continuous workspace health checks + +## Core Operations + +### Query Operations (Map/Reduce) + +Distributed query processing across multiple workspaces: + +```text +1. Request with RequestId +2. AccountUuid → PersonalId → NodeId (routing) +3. Post request to Personal NodeId + 3.1 Personal Node: Resolve workspace → NodeIds mapping + 3.2 Personal Node: Distribute query to required nodes + 4.1 Target Nodes: Check workspace status, activate if needed + 4.2 Target Nodes: Execute query on workspace + 4.3 Target Nodes: Process child workspaces if applicable + 4.4 Target Nodes: Subscribe to workspace changes + 4.5 Target Nodes: Perform map/reduce on results + 4.6 Target Node: Pass result to personal Node. + 3.3 Personal Node: Pass result to client. + 3.4. Collect and aggregate responses + 3.5. Handle retries for failed workspaces + 3.6. Cancel requests when needed + 3.7. Return final response to client +``` + +### Modify Operations + +Transactional modifications across the distributed system: + +```text +1. Request with RequestId +2. AccountUuid + PersonalId → NodeId (routing) +3. Post to Personal NodeId + 3.1 Personal Node: WorkspaceId → NodeId resolution + 4.1 Target Node: Execute operation on workspace + 4.2 Target Node: Return response to personal node + 3.2 Personal Node: Forward response to client +``` + +### Broadcast Operations + +Efficient message distribution to multiple clients: + +**Account Broadcast:** + +```text +1. AccountUuid → PersonalId → NodeId (targeting) +2. Post message to client's personal node +3. Node broadcasts to all connected clients +``` + +**Workspace Broadcast:** + +```text +1. WorkspaceId → AccountUuid[] → NodeId[] (fan-out) +2. Broadcast to all relevant nodes +3. Each node broadcasts to its connected clients +``` + +## Implementation Details + +### Core Interfaces + +**Node Interface:** + +```typescript +interface Node { + _id: NodeUuid + ask: (req: Request, options?: AskOptions) => Promise + modify: (workspaceId: WorkspaceUuid, req: Request) => Promise> + ping: (accounts: AccountUuid[]) => Promise + broadcast: (req: Array>) => Promise + close: () => Promise +} +``` + +**Workspace Interface:** + +```typescript +interface Workspace { + _id: WorkspaceUuid + lastUse: number + ask: (req: Request) => Promise> + modify: (req: Request) => Promise> + ping: () => void + close: () => Promise +} +``` + +### Discovery Services + +**Node Discovery:** + +- Hash-based node selection +- Health monitoring and failover +- Dynamic node registration/deregistration + +**Workspace Discovery:** + +- Account-to-workspace mapping +- Workspace hierarchy resolution +- Real-time workspace availability + +### Session Management + +**Client Session:** + +```typescript +interface Client { + account: AccountUuid + sessionId: string + ask: (req: T, options?: AskOptions) => Promise> + modify: (workspaceId: WorkspaceUuid, req: T) => Promise> + onBroadcast?: (response: Response) => void + onClose?: () => void +} +``` + +## Usage Examples + +```typescript +// Initialize node discovery +const nodeDiscovery = new StaticNodeDiscovery([ + ['node1', { region: 'us-east', capacity: 100 }], + ['node2', { region: 'us-west', capacity: 150 }] +]); + +// Create workspace discovery +const workspaceDiscovery = new StaticWorkspaceDiscovery({ + 'user1': ['workspace1', 'workspace2'], + 'user2': ['workspace3'] +}); + +// Initialize session manager +const sessionManager = new SessionManagerImpl(nodeFactory, operationHandler, workspaceDiscovery, nodeDiscovery); + +// Register client +const client = await sessionManager.register('user1', 'session1'); + +// Perform operations +const result = await client.ask('query-data', { workspace: ['workspace1'] }); +await client.modify('workspace1', { action: 'update', data: {...} }); +``` diff --git a/network/todo.md b/network/todo.md new file mode 100644 index 00000000000..4e6a84f3f2d --- /dev/null +++ b/network/todo.md @@ -0,0 +1,22 @@ +# Problems not solve + +- [x] Basic functional implementaion and zeromq transport based implementation. + + - [x] basic tests + +- Add opentelementry for monitoring/logging + +- Add docker + real network benchmark test + +- Retry logic - need more carefully track request/response retry in case of node/workspace mises. + +- Rate limit logic not implemented, unclear how to manage it now. + +- Memory overhelming issues, if ask request too many data per node or final reduce node. + + 1. Possible solution implement streaming of responses. + 2. Add limits per workspace request? + +- Work on more real life examples. Integrate into platform. + +- Not sure if warmup/ping is really needed. diff --git a/network/zeromq/.eslintrc.js b/network/zeromq/.eslintrc.js new file mode 100644 index 00000000000..ce90fb9646f --- /dev/null +++ b/network/zeromq/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/network/zeromq/.npmignore b/network/zeromq/.npmignore new file mode 100644 index 00000000000..e3ec093c383 --- /dev/null +++ b/network/zeromq/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/network/zeromq/config/rig.json b/network/zeromq/config/rig.json new file mode 100644 index 00000000000..78cc5a17334 --- /dev/null +++ b/network/zeromq/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/network/zeromq/jest.config.js b/network/zeromq/jest.config.js new file mode 100644 index 00000000000..2cfd408b679 --- /dev/null +++ b/network/zeromq/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/network/zeromq/package.json b/network/zeromq/package.json new file mode 100644 index 00000000000..fb8cc290012 --- /dev/null +++ b/network/zeromq/package.json @@ -0,0 +1,42 @@ +{ + "name": "@hcengineering/network-zeromq", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --forceExit", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.8.3", + "@types/node": "^22.15.29", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/uuid": "^8.3.1" + }, + "dependencies": { + "@hcengineering/network": "^0.6.0", + "zeromq": "^6.5.0", + "uuid": "^8.3.2" + } +} diff --git a/network/zeromq/src/__test__/backrpc.spec.ts b/network/zeromq/src/__test__/backrpc.spec.ts new file mode 100644 index 00000000000..f7ae104d98c --- /dev/null +++ b/network/zeromq/src/__test__/backrpc.spec.ts @@ -0,0 +1,104 @@ +import { TickManagerImpl } from '@hcengineering/network' +import { BackRPCClient, BackRPCServer, type ClientId } from '../backrpc' + +describe('backrpc', () => { + it('test request/response', async () => { + const tickMgr = new TickManagerImpl(10) + const server = new BackRPCServer({ + requestHandler: async (client, method, params, send) => { + switch (method) { + case 'hello': + await send('World') + break + case 'do': + await send(await server.request(client, 'callback', '')) + break + default: + await send('unknown') + } + }, + helloHandler: async (clientId) => { + console.log(`Client ${clientId} connected`) + } + }, tickMgr) + + const client = new BackRPCClient( + 'client1' as ClientId, + { + requestHandler: async (method, param, send) => { + if (method === 'callback') { + await send('callback-value') + } + await send('') + }, + onRegister: async () => { + console.log('Client registered') + } + }, 'localhost', await server.getPort(), tickMgr) + + const response = await client.request('hello', 'world') + expect(response).toBe('World') + + const response2 = await client.request('do', '') + expect(response2).toBe('callback-value') + + client.close() + server.close() + }) + + it('test check-register', async () => { + const tickMgr = new TickManagerImpl(10) + let server = new BackRPCServer({ + requestHandler: async (client, method, params, send) => { + }, + helloHandler: async (clientId) => { + console.log(`Client ${clientId} connected`) + } + }, tickMgr, '*', 8701) + + let registered = 0 + + let doResolve: () => void + const p: Promise = new Promise((resolve) => { + doResolve = () => { resolve() } + }) + + let doResolve2: () => void + const p2: Promise = new Promise((resolve) => { + doResolve2 = () => { resolve() } + }) + const client = new BackRPCClient( + 'client1' as ClientId, + { + requestHandler: async (method, param, send) => { + }, + onRegister: async () => { + registered++ + doResolve() + if (registered === 2) { + doResolve2() + } + } + }, 'localhost', 8701, tickMgr) + + await p + + expect(registered).toBe(1) + + server.close() + + server = new BackRPCServer({ + requestHandler: async (client, method, params, send) => { + }, + helloHandler: async (clientId) => { + console.log(`Client ${clientId} connected`) + } + }, tickMgr, '*', 8701) + + await p2 + expect(registered).toBe(2) + + client.close() + server.close() + }) +}) diff --git a/network/zeromq/src/__test__/network.spec.ts b/network/zeromq/src/__test__/network.spec.ts new file mode 100644 index 00000000000..28c36013a3c --- /dev/null +++ b/network/zeromq/src/__test__/network.spec.ts @@ -0,0 +1,189 @@ +import { + AgentImpl, + composeCID, + NetworkImpl, + TickManagerImpl, + type AgentEndpointRef, + type AgentUuid, + type ClientUuid, + type Container, + type ContainerConnection, + type ContainerEndpointRef, + type ContainerKind, + type ContainerUuid, + type NetworkClient, + type TickManager +} from '@hcengineering/network' +import { NetworkAgentServer } from '../agent' +import { BackRPCServer } from '../backrpc' +import { NetworkClientImpl } from '../client' +import { containerDirectRef, containerOnAgentEndpointRef, EndpointKind, parseEndpointRef } from '../endpoints' +import { NetworkServer } from '../server' + +const agents = { + agent1: 'agent1' as AgentUuid, + agent2: 'agent2' as AgentUuid +} + +const kinds = { + session: 'session' as ContainerKind, + workspace: 'workspace' as ContainerKind +} + +class DummySessionContainer implements Container { + async request (operation: string, data?: any, clientId?: ClientUuid): Promise { + if (operation === 'test') { + for (const [k, bk] of this.eventHandlers.entries()) { + if (k === clientId) { + await bk('event') + } + } + return 'test-ok' + } + throw new Error('Unknown operation') + } + + // Called when the container is terminated + onTerminated?: () => void + + async terminate (): Promise {} + + async ping (): Promise {} + + connect (clientId: ClientUuid, handler: (data: any) => Promise): void { + this.eventHandlers.set(clientId, handler) + } + + disconnect (clientId: ClientUuid): void { + this.eventHandlers.delete(clientId) + } + + private readonly eventHandlers = new Map Promise>() +} + +class DummyWorkspaceContainer implements Container { + server!: BackRPCServer + + constructor ( + readonly uuid: ContainerUuid, + readonly agentId: AgentUuid, + readonly networkClient: NetworkClient + ) {} + + async start (tickMgr: TickManager): Promise { + this.server = new BackRPCServer( + { + requestHandler: async (client, method, params, send) => { + // Handle incoming requests + if (method === 'test') { + await send('test-ok') + } + throw new Error('Unknown method') + } + }, + tickMgr, + 'localhost', + 0 + ) + const port = await this.server.getPort() + return containerDirectRef('localhost', port, this.uuid, this.agentId) + } + + async request (operation: string, data?: any): Promise { + return '' + } + + // Called when the container is terminated + onTerminated?: () => void + + async terminate (): Promise { + this.server.close() + } + + async ping (): Promise {} + + connect (clientId: ClientUuid, handler: (data: any) => Promise): void { + this.eventHandlers.set(clientId, handler) + } + + disconnect (clientId: ClientUuid): void { + this.eventHandlers.delete(clientId) + } + + private readonly eventHandlers = new Map Promise>() +} + +function createAgent1 (tickMgr: TickManagerImpl, networkClient: NetworkClient): AgentImpl { + const agent: AgentImpl = new AgentImpl(agents.agent1, { + [kinds.session]: async (uuid) => { + return [new DummySessionContainer(), containerOnAgentEndpointRef(agent.endpoint as AgentEndpointRef, uuid)] + }, + [kinds.workspace]: async (uuid) => { + const container = new DummyWorkspaceContainer(uuid, agent.uuid, networkClient) + const endpoint = await container.start(tickMgr) + return [container, endpoint] + } + }) + return agent +} + +jest.setTimeout(500000) + +describe('check network server is working fine', () => { + it('check client connect to network', async () => { + const tickMgr = new TickManagerImpl(10) // 10 ticks per second + const net = new NetworkImpl(tickMgr) + + // we need some values to be available + const network = new NetworkServer(net, tickMgr, '*', 0) + + const networkClient: NetworkClient = new NetworkClientImpl('localhost', await network.rpcServer.getPort(), tickMgr) + + const agent = createAgent1(tickMgr, networkClient) + // Random port on * + const agentServer = new NetworkAgentServer(tickMgr, 'localhost', '*', 0) + await agentServer.start(agent) + + await networkClient.register(agent) + + const _kinds = await networkClient.kinds() + expect(_kinds).toEqual(['session', 'workspace']) + + const _agents = await networkClient.agents() + expect(_agents.length).toEqual(1) + expect(_agents[0].agentId).toEqual(agents.agent1) + expect(_agents[0].containers.length).toEqual(0) + + // Start a new container and check if messaging works + const containerRef = await networkClient.get(composeCID('session', 'user1'), { kind: kinds.session }) + + const data = parseEndpointRef(containerRef.endpoint) + expect(data.kind).toEqual(EndpointKind.routed) + + const containers = await networkClient.list(kinds.session) + expect(containers.length).toEqual(1) + + const containerConnection: ContainerConnection = await containerRef.connect() + expect(containerConnection).toBeDefined() + + // Verify requests and events are working fine. + const events: any[] = [] + const p = new Promise(resolve => { + containerConnection.on = async (data) => { + events.push(data) + resolve() + } + }) + + const resp1 = await containerConnection.request('test') + expect(resp1).toEqual('test-ok') + + await p + expect(events.length).toEqual(1) + expect(events[0]).toEqual('event') + + await agentServer.close() + await networkClient.close() + await network.close() + }) +}) diff --git a/network/zeromq/src/__test__/samples.ts b/network/zeromq/src/__test__/samples.ts new file mode 100644 index 00000000000..dacd9362684 --- /dev/null +++ b/network/zeromq/src/__test__/samples.ts @@ -0,0 +1,27 @@ +import type { AccountUuid, WorkspaceUuid } from '@hcengineering/network' +import { StaticWorkspaceDiscovery } from '@hcengineering/network' + +export const workspaces = { + ws1: 'ws1' as WorkspaceUuid, + ws2: 'ws2' as WorkspaceUuid, + ws3: 'ws3' as WorkspaceUuid, + ws4: 'ws4' as WorkspaceUuid, + ws5: 'ws5' as WorkspaceUuid, + ws6: 'ws6' as WorkspaceUuid, + ws7: 'ws7' as WorkspaceUuid, + ws8: 'ws8' as WorkspaceUuid, + ws9: 'ws9' as WorkspaceUuid, + ws10: 'ws10' as WorkspaceUuid +} + +export const users = { + user1: 'user1' as AccountUuid, + user2: 'user2' as AccountUuid +} + +export const wsDiscovery = new StaticWorkspaceDiscovery({ + [users.user1]: [workspaces.ws1, workspaces.ws2, workspaces.ws3], + [users.user2]: [workspaces.ws4, workspaces.ws5, workspaces.ws6], + [workspaces.ws1]: [workspaces.ws7, workspaces.ws8], + [workspaces.ws8]: [workspaces.ws9, workspaces.ws10] +}) diff --git a/network/zeromq/src/__test__/zmq.spec.ts b/network/zeromq/src/__test__/zmq.spec.ts new file mode 100644 index 00000000000..5357cb9e8bf --- /dev/null +++ b/network/zeromq/src/__test__/zmq.spec.ts @@ -0,0 +1,237 @@ +import * as zmq from 'zeromq' + +describe('zmq-tests', () => { + it('check reconnect', async () => { + // Simulate a reconnect event + + const router = new zmq.Router() + + await router.bind('tcp://0.0.0.0:7654') + + const request = new zmq.Request() + request.connect('tcp://localhost:7654') + + await request.send('Hello') + + const data = await router.receive() + expect(data[2].toString()).toBe('Hello') + + await router.send([data[0], data[1], 'World']) + + const result2 = await request.receive() + expect(result2.toString()).toBe('World') + + const obs1 = new zmq.Observer(request) + + let closed = false + obs1.on('close', (dta) => { + closed = true + console.log('closed', dta.address) + }) + + router.close() + + // eslint-disable-next-line no-unmodified-loop-condition + while (!closed) { + await new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 10) + }) + } + + request.close() + }) + it('check diff order', async () => { + // Simulate a reconnect event + + const router = new zmq.Router() + + await router.bind('tcp://0.0.0.0:7654') + + const request = new zmq.Request() + request.connect('tcp://localhost:7654') + + const request2 = new zmq.Request() + request2.connect('tcp://localhost:7654') + + await request.send('Hello1') + await request2.send('Hello2') + const data = await router.receive() + const data2 = await router.receive() + + expect(data[2].toString()).toBe('Hello1') + + expect(data2[2].toString()).toBe('Hello2') + + await router.send([data2[0], data2[1], 'World2']) + await router.send([data[0], data[1], 'World1']) + + const result2 = await request.receive() + expect(result2.toString()).toBe('World1') + + const result3 = await request2.receive() + expect(result3.toString()).toBe('World2') + request.close() + request2.close() + router.close() + }) + + it('check multiple requests from same client', async () => { + // Create router socket (server) + const router = new zmq.Pull() + await router.bind('tcp://0.0.0.0:7654') + + const routerPub = new zmq.Publisher() + await routerPub.bind('tcp://0.0.0.0:7655') + + // Create request socket (client) + const client = new zmq.Push() + client.connect('tcp://localhost:7654') + + const clientSub = new zmq.Subscriber() + clientSub.connect('tcp://localhost:7655') + clientSub.subscribe('client1') + + await client.send('Hello1') + await client.send('Hello2') + await client.send('Hello3') + + const d1 = await router.receive() + const d2 = await router.receive() + const d3 = await router.receive() + + expect(d1[0].toString()).toBe('Hello1') + expect(d2[0].toString()).toBe('Hello2') + expect(d3[0].toString()).toBe('Hello3') + + await routerPub.send(['client1', '', 'World1']) + await routerPub.send(['client1', '', 'World2']) + await routerPub.send(['client1', '', 'World3']) + + const result1 = await clientSub.receive() + const result2 = await clientSub.receive() + const result3 = await clientSub.receive() + + expect(result1[2].toString()).toBe('World1') + expect(result2[2].toString()).toBe('World2') + expect(result3[2].toString()).toBe('World3') + + // Cleanup + client.close() + clientSub.close() + router.close() + routerPub.close() + }) + + it('random port text', async () => { + // Simulate a reconnect event + + const router = new zmq.Router() + + await router.bind('tcp://*:0') + + const reqEndpoint: string = router.lastEndpoint as string + expect(reqEndpoint).toBeDefined() + + const portMatch = reqEndpoint.match(/:(\d+)$/) + const port = portMatch != null ? parseInt(portMatch[1]) : 0 + + expect(port).toBeGreaterThan(0) + + const request = new zmq.Request() + request.connect(`tcp://localhost:${port}`) + + await request.send('Hello') + + const data = await router.receive() + expect(data[2].toString()).toBe('Hello') + + await router.send([data[0], data[1], 'World']) + + const result2 = await request.receive() + expect(result2.toString()).toBe('World') + + router.close() + request.close() + }) + + it('dealer check', async () => { + // Simulate a reconnect event + + const router = new zmq.Router() + + await router.bind('tcp://*:0') + + const reqEndpoint: string = router.lastEndpoint as string + expect(reqEndpoint).toBeDefined() + + const portMatch = reqEndpoint.match(/:(\d+)$/) + const port = portMatch != null ? parseInt(portMatch[1]) : 0 + + expect(port).toBeGreaterThan(0) + + const request = new zmq.Dealer() + request.connect(`tcp://localhost:${port}`) + + await request.send('Hello') + await request.send('Hello2') + + const data = await router.receive() + const data2 = await router.receive() + expect(data[1].toString()).toBe('Hello') + expect(data2[1].toString()).toBe('Hello2') + + await router.send([data2[0], 'World']) + await router.send([data[0], 'World']) + + let result2 = await request.receive() + expect(result2.toString()).toBe('World') + + result2 = await request.receive() + expect(result2.toString()).toBe('World') + + router.close() + request.close() + }) + + it('client broadcast check', async () => { + // Simulate a reconnect event + + const router = new zmq.Router() + + await router.bind('tcp://*:0') + + const reqEndpoint: string = router.lastEndpoint as string + expect(reqEndpoint).toBeDefined() + + const portMatch = reqEndpoint.match(/:(\d+)$/) + const port = portMatch != null ? parseInt(portMatch[1]) : 0 + + expect(port).toBeGreaterThan(0) + + const request = new zmq.Dealer() + request.connect(`tcp://localhost:${port}`) + + await request.send('Hello') + + const data = await router.receive() + expect(data[1].toString()).toBe('Hello') + + await router.send([data[0], 'World']) + await router.send([data[0], 'World2']) + await router.send([data[0], 'World3']) + + let result2 = await request.receive() + expect(result2.toString()).toBe('World') + + result2 = await request.receive() + expect(result2.toString()).toBe('World2') + + result2 = await request.receive() + expect(result2.toString()).toBe('World3') + + router.close() + request.close() + }) +}) diff --git a/network/zeromq/src/agent.ts b/network/zeromq/src/agent.ts new file mode 100644 index 00000000000..68a15a3b7ec --- /dev/null +++ b/network/zeromq/src/agent.ts @@ -0,0 +1,209 @@ +import { + type ClientUuid, + type ContainerConnection, + type ContainerEvent, + type ContainerUuid, + type NetworkAgent, + type TickManager +} from '@hcengineering/network' +import { + BackRPCClient, + BackRPCServer, + type BackRPCResponseSend, + type BackRPCServerHandler, + type ClientId +} from './backrpc' +import { agentDirectRef } from './endpoints' +import { opNames } from './types' + +/** + * A server for an agent with connection abilities. + * + * start method should be called before agent will be registered on network. + */ +export class NetworkAgentServer implements BackRPCServerHandler { + readonly rpcServer: BackRPCServer + agent: NetworkAgent | undefined + + constructor ( + tickMgr: TickManager, + readonly endpointHost: string, // An endpoint construction host, will be used to register + host: string = '*', // A socket visibility + port: number = 3738 // If 0, port will be free random one. + ) { + this.rpcServer = new BackRPCServer(this, tickMgr, host, port) + } + + async start (agent: NetworkAgent): Promise { + this.agent = agent + this.agent.endpoint = agentDirectRef(this.endpointHost, await this.rpcServer.getPort(), agent.uuid) + + // Now registration is possible, or update will be sent + await this.agent.onAgentUpdate?.() + } + + async onContainerUpdate (event: ContainerEvent): Promise { + // Handle container update + } + + async getPort (): Promise { + return await this.rpcServer.getPort() + } + + async close (): Promise { + this.rpcServer.close() + } + + async requestHandler (client: ClientUuid, method: string, params: any, send: BackRPCResponseSend): Promise { + if (this.agent === undefined) { + return + } + switch (method) { + case opNames.connect: { + const uuids: ContainerUuid[] = Array.isArray(params.uuid) ? params.uuid : [params.uuid] + const connected: number = 0 + for (const uuid of uuids) { + const container = await this.agent.getContainer(uuid) + if (container === undefined) { + console.error(`Container ${uuid} not found`) + continue + } + // Events will be routed via connectionId + container.connect(client, async (data) => { + await this.rpcServer.send(client, [uuid, data]) + }) + } + await send(connected) + break + } + case opNames.disconnect: { + const uuid = params.uuid as ContainerUuid + const container = await this.agent.getContainer(uuid) + if (container === undefined) { + throw new Error('Container not found') + } + container.disconnect(client) + await send('ok') + break + } + case opNames.sendContainer: { + const target: ContainerUuid = params[0] + const operation: string = params[1] + const data: any = params[2] + + const container = await this.agent.getContainer(target) + if (container === undefined) { + throw new Error('Container not found') + } + await send(await container.request(operation, data, client)) + break + } + + default: + throw new Error('Unknown method' + method) + } + } + + async helloHandler (clientId: ClientUuid): Promise { + console.log(`Client ${clientId} connected`) + } + + async handleTimeout (client: ClientUuid): Promise { + console.log(`Client ${client} timed out`) + } +} + +/** + * An routed connection to a container via agent. + */ +export class RoutedNetworkAgentConnectionImpl { + private readonly client: BackRPCClient + + containers = new Map() + + constructor ( + tickMgr: TickManager, + readonly clientId: ClientT, + readonly host: string, + readonly port: number + ) { + this.client = new BackRPCClient(this.clientId, this, host, port, tickMgr) + } + + async connect (containerUuid: ContainerUuid): Promise { + // Establish a connection to the specified container + await this.client.request(opNames.connect, { uuid: containerUuid }) + + const connection: ContainerConnection = { + containerId: containerUuid, + close: async () => { + await this.client.request(opNames.disconnect, { uuid: containerUuid }) + }, + request: async (operation, data) => + await this.client.request(opNames.sendContainer, [containerUuid, operation, data]) + } + this.containers.set(containerUuid, connection) + return connection + } + + async requestHandler (method: string, params: any, send: BackRPCResponseSend): Promise { + // No callback is required + } + + async onEvent (event: any): Promise { + const [container, data] = event + const connection = this.containers.get(container) + if (connection !== undefined) { + await connection.on?.(data) + } + } + + async close (): Promise { + this.client.close() + } + + async onRegister (): Promise { + // Handle registration logic here + } +} + +/** + * A direct connection to container + */ +export class NetworkDirectConnectionImpl implements ContainerConnection { + private readonly client: BackRPCClient + + on?: ((data: any) => Promise) | undefined + + containers: ContainerUuid[] = [] + + constructor ( + tickMgr: TickManager, + readonly clientId: ClientUuid, + readonly containerId: ContainerUuid, + readonly host: string, + readonly port: number + ) { + this.client = new BackRPCClient(this.clientId, this, host, port, tickMgr) + } + + async request (operation: string, data?: any): Promise { + return await this.client.request(operation, data) + } + + async requestHandler (method: string, params: any, send: BackRPCResponseSend): Promise { + // No callback is required + } + + async onEvent (event: any): Promise { + await this.on?.(event) + } + + async close (): Promise { + this.client.close() + } + + async onRegister (): Promise { + // No registration is required + } +} diff --git a/network/zeromq/src/backrpc.ts b/network/zeromq/src/backrpc.ts new file mode 100644 index 00000000000..c7b118cb960 --- /dev/null +++ b/network/zeromq/src/backrpc.ts @@ -0,0 +1,389 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { timeouts, type TickManager } from '@hcengineering/network' +import { v4 as uuidv4 } from 'uuid' +import * as zmq from 'zeromq' + +const backrpcOperations = { + hello: 0, + request: 1, + response: 2, + responseError: 3, + event: 4, + ping: 5, + pong: 6 +} + +export type ClientId = string & { __clientId: string } + +export interface BackRPCServerHandler { + requestHandler: ( + client: ClientT, + method: string, + params: any, + send: (response: any) => Promise + ) => Promise + helloHandler?: (client: ClientT) => Promise + handleTimeout?: (client: ClientT) => Promise +} + +export class BackRPCServer { + uuid = uuidv4() + + private readonly router: zmq.Router + + private requestCounter: number = 0 + + private readonly requests = new Map void, reject: (err: any) => void }>() + + private readonly clientMapping = new Map() + private readonly revClientMapping = new Map() + + private closed: boolean = false + + private bound: Promise | undefined + + constructor ( + private readonly handlers: BackRPCServerHandler, + private readonly tickMgr: TickManager, + readonly host: string = '*', + private readonly port: number = 0 + ) { + this.router = new zmq.Router() + + this.tickMgr.register(() => { + void this.checkAlive() + }, timeouts.pingInterval) + + void this.start() + } + + async checkAlive (): Promise { + const now = this.tickMgr.now() + // Handle outdated clients + for (const [clientId, clientRecord] of this.revClientMapping.entries()) { + const timeSinceLastSeen = now - clientRecord.lastSeen + + if (timeSinceLastSeen > timeouts.aliveTimeout * 1000) { + console.warn(`Client ${clientId} has been inactive for ${Math.round(timeSinceLastSeen / 1000)}s, marking as dead`) + await this.handlers.handleTimeout?.(clientRecord.id) + } + this.revClientMapping.delete(clientId) + this.clientMapping.delete(clientRecord.id) + } + } + + async getPort (): Promise { + await this.bound + const reqEndpoint = this.router.lastEndpoint + if (reqEndpoint === null) { + throw new Error('Router is not bound to an endpoint') + } + + const portMatch = reqEndpoint.match(/:(\d+)$/) + const port = portMatch != null ? parseInt(portMatch[1]) : undefined + if (port === undefined) { + throw new Error('Router is not bound to a port') + } + return port + } + + private async start (): Promise { + this.bound = this.router.bind(`tcp://${this.host}:${this.port}`) + await this.bound + + // Read messages from clients. + for await (const msg of this.router) { + if (this.closed) { + return + } + try { + const clientId = msg[0] + const clientIdText = clientId.toString('base64') + const [operation, reqId, payload] = [parseInt(msg[1].toString()), msg[2].toString(), msg[3]] + + const client = this.revClientMapping.get(clientIdText) + if (client !== undefined) { + client.lastSeen = this.tickMgr.now() + } + switch (operation) { + case backrpcOperations.hello: + // Remember clientId to be able to do back requests. + if (!this.clientMapping.has(reqId.toString() as ClientT)) { + await this.handlers.helloHandler?.(reqId.toString() as ClientT) + } + this.clientMapping.set(reqId.toString() as ClientT, clientId) + this.revClientMapping.set(clientIdText, { + id: reqId.toString() as ClientT, + lastSeen: this.tickMgr.now() + }) + await this.router.send([clientId, backrpcOperations.hello, this.uuid, '']) + break + case backrpcOperations.ping: { + await this.router.send([clientId, backrpcOperations.pong, this.uuid, '']) + break + } + case backrpcOperations.request: + { + const [method, params] = JSON.parse(payload.toString()) + if (client === undefined) { + console.error(`Client ${clientId.toString()} not found`) + } else { + const sendError = async (err: Error): Promise => { + await this.router.send([ + clientId, + backrpcOperations.responseError, + reqId, + JSON.stringify({ + message: err.message ?? '', + stack: err.stack + }) + ]) + } + void this.handlers.requestHandler( + client.id, + method, + params, + async (response: any) => { + await this.router.send([clientId, backrpcOperations.response, reqId, JSON.stringify(response)]) + } + ).catch((err) => { + void sendError(err) + }) + } + } + break + case backrpcOperations.response: { + const reqID = reqId.toString() + const req = this.requests.get(reqID) + try { + req?.resolve(JSON.parse(payload.toString())) + } catch (err: any) { + console.error(err) + } + this.requests.delete(reqID) + break + } + case backrpcOperations.responseError: { + const reqID = reqId.toString() + const req = this.requests.get(reqID) + try { + req?.reject(JSON.parse(payload.toString())) + } catch (err: any) { + console.error(err) + } + this.requests.delete(reqID) + break + } + } + } catch (err: any) { + console.error(err) + } + } + } + + async request (clientId: ClientT, method: string, params: any): Promise { + const clientIdentity = this.clientMapping.get(clientId) + if (clientIdentity === undefined) { + throw new Error(`Client ${clientId} not found`) + } + return await new Promise((resolve, reject) => { + const reqId = clientId + '-' + this.requestCounter++ + this.requests.set(reqId, { resolve, reject }) + + void this.router + .send([clientIdentity, backrpcOperations.request, reqId, JSON.stringify([method, params])]) + .catch((err) => { + reject(err) + }) + }) + } + + async send (clientId: ClientT, body: any): Promise { + const clientIdentity = this.clientMapping.get(clientId) + if (clientIdentity === undefined) { + throw new Error(`Client ${clientId as string} not found`) + } + await this.router.send([clientIdentity, backrpcOperations.event, '', JSON.stringify(body)]) + } + + async close (): Promise { + this.closed = true + this.router.close() + } +} + +export type BackRPCResponseSend = (response: any) => Promise +export interface BackRPCClientHandler { + requestHandler: ( + method: string, + params: any, + send: BackRPCResponseSend + ) => Promise + onRegister?: () => Promise + onEvent?: (event: any) => Promise +} + +export class BackRPCClient { + serverId: string | Promise + dealer: zmq.Dealer + + requestCounter: number = 0 + + requests = new Map void, reject: (err: any) => void }>() + closed: boolean = false + + observer: zmq.Observer + + setServerId: (serverId: string) => void + + constructor ( + readonly clientId: ClientT, + readonly client: BackRPCClientHandler, + readonly host: string, + readonly port: number, + readonly tickMgr: TickManager, + options?: zmq.SocketOptions + ) { + this.dealer = new zmq.Dealer(options) + this.dealer.connect(`tcp://${host}:${port}`) + + this.setServerId = () => {} + this.serverId = new Promise((resolve) => { + this.setServerId = (serverId) => { + this.serverId = serverId + resolve(serverId) + } + }) + + this.observer = new zmq.Observer(this.dealer) + this.observer.on('connect', (data) => { + void this.sendHello() + }) + void this.start() + + this.tickMgr.register(() => { + void this.checkAlive() + }, timeouts.pingInterval) + } + + async checkAlive (): Promise { + await this.dealer.send([backrpcOperations.ping, this.clientId as string, '', '']) + } + + private async sendHello (): Promise { + await this.dealer.send([backrpcOperations.hello, this.clientId as string, '', '']) + } + + private async start (): Promise { + // Read messages from clients. + for await (const msg of this.dealer) { + if (this.closed) { + return + } + try { + const [operation, reqId, payload] = [parseInt(msg[0].toString()), msg[1].toString(), msg[2]] + switch (operation) { + case backrpcOperations.hello: { + const serverUuid = reqId.toString() + if (this.serverId !== serverUuid) { + this.setServerId(serverUuid) + void this.client.onRegister?.()?.catch(err => { + console.error('Failed to register client', err) + }) + } + break + } + case backrpcOperations.request: + { + const [method, params] = JSON.parse(payload.toString()) + void this.client.requestHandler( + method, + params, + async (response: any) => { + await this.dealer.send([backrpcOperations.response, reqId, JSON.stringify(response)]) + } + ).catch(error => { + void this.dealer.send([ + backrpcOperations.responseError, + reqId, + JSON.stringify({ + message: error.message ?? '', + stack: error.stack ?? '' + }) + ]).catch(err2 => { + console.error('Failed to send error', err2, err2) + }) + }) + } + break + case backrpcOperations.response: { + const req = this.requests.get(reqId) + try { + req?.resolve(JSON.parse(payload.toString())) + } catch (err: any) { + console.error(err) + } + this.requests.delete(reqId) + break + } + case backrpcOperations.responseError: { + const req = this.requests.get(reqId) + try { + req?.reject(JSON.parse(payload.toString())) + } catch (err: any) { + console.error(err) + } + this.requests.delete(reqId) + break + } + case backrpcOperations.event: { + void this.client.onEvent?.(JSON.parse(payload.toString())).catch(err => { + console.error('Failed to handle event', err) + }) + break + } + } + } catch (err: any) { + console.error(err) + } + } + } + + async request(method: string, params: any): Promise { + if (this.serverId instanceof Promise) { + await this.serverId + } + return await new Promise((resolve, reject) => { + const reqId = this.clientId + '-' + this.requestCounter++ + this.requests.set(reqId, { resolve, reject }) + + void this.dealer.send([backrpcOperations.request, reqId, JSON.stringify([method, params])]).catch((err) => { + reject(err) + this.requests.delete(reqId) + }) + }) + } + + async send (body: any): Promise { + await this.dealer.send([backrpcOperations.event, body]) + } + + close (): void { + this.closed = true + this.dealer.close() + } +} diff --git a/network/zeromq/src/client.ts b/network/zeromq/src/client.ts new file mode 100644 index 00000000000..45eaac340fb --- /dev/null +++ b/network/zeromq/src/client.ts @@ -0,0 +1,276 @@ +import { + type AgentEndpointRef, + type AgentRecord, + type AgentUuid, + type ClientUuid, + type ContainerConnection, + type ContainerEndpointRef, + type ContainerEvent, + type ContainerKind, + type ContainerRecord, + type ContainerReference, + type ContainerRequest, + type ContainerUpdateListener, + type ContainerUuid, + type NetworkAgent, + type NetworkClient, + type TickManager +} from '@hcengineering/network' +import { v4 as uuidv4 } from 'uuid' +import { NetworkDirectConnectionImpl, RoutedNetworkAgentConnectionImpl } from './agent' +import { BackRPCClient, type BackRPCResponseSend } from './backrpc' +import { agentDirectRef, EndpointKind, parseEndpointRef } from './endpoints' +import { opNames } from './types' + +interface ClientAgentRecord { + agent: NetworkAgent + register: Promise + resolve: () => void +} +/** + * Huly Network client + * + * Some methods are omit clientId parameter. + */ +export class NetworkClientImpl implements NetworkClient { + clientId: ClientUuid = uuidv4() as ClientUuid + + private readonly client: BackRPCClient + + private readonly _agents = new Map() + + // A set of clients for individual containers or agent TORs + endpointConnections = new Map>() + agentConnections = new Map>() + + containerListeners: ContainerUpdateListener[] = [] + + references = new Map() + + registered: boolean = false + + constructor ( + readonly host: string, + port: number, + private readonly tickMgr: TickManager + ) { + this.client = new BackRPCClient(this.clientId, this, host, port, tickMgr) + } + + async close (): Promise { + this.client.close() + for (const agentConn of this.agentConnections.values()) { + await agentConn.close() + } + } + + async requestHandler (method: string, params: any, send: BackRPCResponseSend): Promise { + const [agentId, agentParams] = params + // Pass agent methods to a proper agent + const { agent } = this._agents.get(agentId) ?? { agent: undefined } + if (agent === undefined) { + await send({ error: `Agent ${agentId} not found` }) + return + } + switch (method) { + case opNames.getContainer: + await send(await agent.get(agentParams[0], agentParams[1])) + break + case opNames.listContainers: + await send(await agent.list(agentParams[0])) + break + case opNames.sendContainer: + await send(await agent.request(agentParams[0], agentParams[1], agentParams[2])) + break + default: + throw new Error('Unknown method') + } + } + + async onEvent (event: ContainerEvent): Promise { + // Handle container events + + await this.handleConnectionUpdates(event) + + // In case of container stopped, agent stopped or endpoint changed, we need to update direct connections to be re-established. + for (const listener of this.containerListeners) { + try { + await listener(event) + } catch (error) { + console.error('Error in container listener:', error) + } + } + } + + async onRegister (): Promise { + this.registered = true + // We need to re-register all our managed agents + for (const agent of this._agents.values()) { + await this.doRegister(agent.agent) + } + } + + /** + * Register a new agent, agent could or could not provide an endpoint for routed connections. + */ + async register (agent: NetworkAgent): Promise { + const rec: ClientAgentRecord = { + agent, + register: Promise.resolve(), + resolve: () => {} + } + rec.register = new Promise((resolve) => { + rec.resolve = resolve + }) + this._agents.set(agent.uuid, rec) + + agent.onUpdate = async (event) => { + await this.client.request(opNames.containerUpdate, event) + } + agent.onAgentUpdate = async () => { + await this.doRegister(agent) + } + + if (this.registered) { + await this.doRegister(agent) + } + await rec.register + } + + async doRegister (agent: NetworkAgent): Promise { + const containers: ContainerRecord[] = [] + for (const container of await agent.list()) { + containers.push({ + agentId: agent.uuid, + uuid: container.uuid, + endpoint: container.endpoint, + kind: container.kind, + lastVisit: container.lastVisit + } satisfies ContainerRecord) + } + const toClean = await this.client.request(opNames.register, { + uuid: agent.uuid, + containers, + kinds: agent.kinds, + endpoint: agent.endpoint + }) + for (const uuid of toClean) { + await agent.terminate(uuid) + } + this._agents.get(agent.uuid)?.resolve() + } + + async agents (): Promise { + // Return actual list of agents + return await this.client.request(opNames.getAgents, {}) + } + + async kinds (): Promise { + return await this.client.request(opNames.getKinds, {}) + } + + async get (uuid: ContainerUuid, request: ContainerRequest): Promise { + const existing = this.references.get(uuid) + if (existing !== undefined) { + return existing + } + const endpoint = await this.client.request(opNames.getContainer, { + uuid, + request + }) + const ref: ContainerReference = { + uuid, + endpoint, + close: async () => { + await this.release(uuid) + this.references.delete(uuid) + }, + request: (data) => this.request(uuid, data), + connect: (timeout?: number) => this.establishConnection(endpoint, timeout) + } + this.references.set(uuid, ref) + return ref + } + + private async establishConnection (endpoint: ContainerEndpointRef, timeout?: number): Promise { + let conn = this.endpointConnections.get(endpoint) + if (conn !== undefined) { + if (conn instanceof Promise) { + conn = await conn + this.endpointConnections.set(endpoint, conn) + } + return conn + } + // Check if connection is routed + const parsedRef = parseEndpointRef(endpoint) + if (parsedRef.uuid === undefined) { + throw new Error('Invalid endpoint reference') + } + if (parsedRef.kind === EndpointKind.noconnect) { + throw new Error('No connection available') + } + if (parsedRef.kind === EndpointKind.routed) { + const agentRef = agentDirectRef(parsedRef.host, parsedRef.port, parsedRef.agentId) + let agentConn = this.agentConnections.get(agentRef) + if (agentConn === undefined) { + agentConn = new RoutedNetworkAgentConnectionImpl( + this.tickMgr, + this.clientId, + parsedRef.host, + parsedRef.port + ) + this.agentConnections.set(agentRef, agentConn) + } + const conn = agentConn.connect(parsedRef.uuid) + this.endpointConnections.set(endpoint, conn) + return await conn + } + const directConn = new NetworkDirectConnectionImpl( + this.tickMgr, + this.clientId, + parsedRef.uuid, + parsedRef.host, + parsedRef.port + ) + this.endpointConnections.set(endpoint, directConn) + return directConn + } + + async handleConnectionUpdates (event: ContainerEvent): Promise { + // Handle connection updates + // TODO: Fix me + for (const updated of event.updated) { + if (this.references.has(updated.uuid)) { + const ref = this.references.get(updated.uuid) + if (ref !== undefined) { + ref.endpoint = updated.endpoint + } + } + } + for (const deleted of event.deleted) { + const ref = this.references.get(deleted.uuid) + if (ref !== undefined) { + // We need to re request endpoint and update both endpoint and connection + } + } + } + + async release (uuid: ContainerUuid): Promise { + await this.client.request(opNames.releaseContainer, { uuid }) + } + + async list (kind: ContainerKind): Promise { + return await this.client.request(opNames.listContainers, { + kind + }) + } + + // Send some data to container, using proxy connection. + async request (target: ContainerUuid, operation: string, data?: any): Promise { + return await this.client.request(opNames.sendContainer, [target, operation, data]) + } + + onContainerUpdate (listener: ContainerUpdateListener): void { + this.containerListeners.push(listener) + } +} diff --git a/network/zeromq/src/endpoints.ts b/network/zeromq/src/endpoints.ts new file mode 100644 index 00000000000..b64b181ab86 --- /dev/null +++ b/network/zeromq/src/endpoints.ts @@ -0,0 +1,60 @@ +import type { AgentEndpointRef, AgentUuid, ContainerEndpointRef, ContainerUuid } from '@hcengineering/network' + +export enum EndpointKind { + routed, // Container is routed via host as router, host is BackRPCServer, and send operation should be used to pass data to target. + direct, // A direct connection to container. + noconnect // No connection to container via network, only send on network. +} +// An Agent or Container endpoint referenfe to establish a direct connection to. +export interface EndpointRefData { + kind: EndpointKind + host: string + port: number + agentId: AgentUuid + uuid?: ContainerUuid +} + +export function agentDirectRef (host: string, port: number, uuid: AgentUuid): AgentEndpointRef { + return JSON.stringify({ + host, + port, + kind: EndpointKind.direct, + agentId: uuid + } satisfies EndpointRefData) as AgentEndpointRef +} + +export function agentNoConnectRef (uuid: AgentUuid): AgentEndpointRef { + return JSON.stringify({ + kind: EndpointKind.noconnect, + agentId: uuid, + host: '', + port: 0 + } satisfies EndpointRefData) as AgentEndpointRef +} + +export function containerOnAgentEndpointRef ( + agentEndpoint: AgentEndpointRef, + container: ContainerUuid +): ContainerEndpointRef { + const agentData = JSON.parse(agentEndpoint) as EndpointRefData + return JSON.stringify({ + kind: agentData.kind === EndpointKind.noconnect ? agentData.kind : EndpointKind.routed, + host: agentData.host, + port: agentData.port, + agentId: agentData.agentId, + uuid: container + } satisfies EndpointRefData) as ContainerEndpointRef +} + +export function parseEndpointRef (ref: AgentEndpointRef | ContainerEndpointRef): EndpointRefData { + return JSON.parse(ref) as EndpointRefData +} +export function containerDirectRef (host: string, port: number, uuid: ContainerUuid, agentId: AgentUuid): ContainerEndpointRef { + return JSON.stringify({ + kind: EndpointKind.direct, + host, + port, + uuid, + agentId + } satisfies EndpointRefData) as ContainerEndpointRef +} diff --git a/network/zeromq/src/index.ts b/network/zeromq/src/index.ts new file mode 100644 index 00000000000..fd7bbe7dba9 --- /dev/null +++ b/network/zeromq/src/index.ts @@ -0,0 +1,3 @@ +export * from './types' +export * from './client' +export * from './server' diff --git a/network/zeromq/src/server.ts b/network/zeromq/src/server.ts new file mode 100644 index 00000000000..0bd2e43d53b --- /dev/null +++ b/network/zeromq/src/server.ts @@ -0,0 +1,144 @@ +import { + type AgentEndpointRef, + type AgentUuid, + type ClientUuid, + type Container, + type ContainerEndpointRef, + type ContainerKind, + type ContainerRecord, + type ContainerRequest, + type ContainerUuid, + type Network, + type NetworkAgent, + type NetworkWithClients, + type TickManager +} from '@hcengineering/network' +import { BackRPCServer, type BackRPCResponseSend, type BackRPCServerHandler } from './backrpc' +import { opNames } from './types' + +class AgentCallbackHandler implements NetworkAgent { + constructor ( + readonly rpcServer: BackRPCServer, + readonly uuid: AgentUuid, + readonly endpoint: AgentEndpointRef, + readonly kinds: ContainerKind[], + readonly client: ClientUuid + ) {} + + async get (uuid: ContainerUuid, request: ContainerRequest): Promise { + return await this.rpcServer.request(this.client, opNames.getContainer, [this.uuid, [uuid, request]]) + } + + async list (kind?: ContainerKind): Promise { + return await this.rpcServer.request(this.client, opNames.listContainers, [this.uuid, [kind]]) + } + + async request (target: ContainerUuid, operation: string, data?: any): Promise { + return await this.rpcServer.request(this.client, opNames.sendContainer, [this.uuid, [target, operation, data]]) + } + + async terminate (): Promise { + // Ignore + } + + async getContainer (uuid: ContainerUuid): Promise { + return undefined + } +} + +export class NetworkServer implements BackRPCServerHandler { + rpcServer: BackRPCServer + constructor ( + readonly network: Network & NetworkWithClients, + readonly tickMgr: TickManager, + host: string = '*', + port: number = 3737 + ) { + this.rpcServer = new BackRPCServer(this, tickMgr, host, port) + } + + async close (): Promise { + this.rpcServer.close() + } + + async requestHandler (client: ClientUuid, method: string, params: any, send: BackRPCResponseSend): Promise { + switch (method) { + case opNames.register: { + // Handle register + await this.handleRegister(params, this.rpcServer, client, send) + break + } + case opNames.getAgents: { + await send(await this.network.agents()) + break + } + case opNames.getKinds: { + await send(await this.network.kinds()) + break + } + case opNames.getContainer: { + const uuid: ContainerUuid = params.uuid + const request: ContainerRequest = params.request + await send(await this.network.get(client, uuid, request)) + break + } + case opNames.releaseContainer: { + const uuid: ContainerUuid = params.uuid + await this.network.release(client, uuid) + await send('ok') + break + } + case opNames.listContainers: { + const kind: ContainerKind = params.kind + await send(await this.network.list(kind)) + break + } + case opNames.sendContainer: { + const target: ContainerUuid = params[0] + const operation: string = params[1] + const data: any = params[2] + await send(await this.network.request(target, operation, data)) + break + } + + default: + throw new Error('Unknown method' + method) + } + } + + async helloHandler (clientId: ClientUuid): Promise { + console.log(`Client ${clientId} connected`) + this.network.addClient(clientId, async (event) => { + console.log(`Client ${clientId} received container event:`, event) + + await await this.rpcServer.send(clientId, event) + }) + } + + async handleTimeout (client: ClientUuid): Promise { + console.log(`Client ${client} timed out`) + this.network.removeClient(client) + } + + private async handleRegister ( + params: any, + server: BackRPCServer, + client: ClientUuid, + send: (response: any) => Promise + ): Promise { + const agentUuid: AgentUuid = params.uuid + const containers: ContainerRecord[] = params.containers + const kinds: ContainerKind[] = params.kinds + const endpoint: AgentEndpointRef = params.endpoint + const res = await this.network.register( + { + agentId: agentUuid, + containers, + kinds, + endpoint + }, + new AgentCallbackHandler(server, agentUuid, endpoint, kinds, client) + ) + await send(res) + } +} diff --git a/network/zeromq/src/types.ts b/network/zeromq/src/types.ts new file mode 100644 index 00000000000..63eafa5931e --- /dev/null +++ b/network/zeromq/src/types.ts @@ -0,0 +1,16 @@ +export const opNames = { + // NetworkOperations + register: 'r', + unregister: 'u', + getAgents: 'a', + getKinds: 'k', + listContainers: 'l', + getContainer: 'g', + releaseContainer: 'r', + sendContainer: 's', + // Agent operations + containerUpdate: 'c', + terminate: 't', + connect: 'c!', + disconnect: 'd' +} diff --git a/network/zeromq/tsconfig.json b/network/zeromq/tsconfig.json new file mode 100644 index 00000000000..c6a877cf6c3 --- /dev/null +++ b/network/zeromq/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/rush.json b/rush.json index 33787b5ff3e..63794dbc864 100644 --- a/rush.json +++ b/rush.json @@ -2663,6 +2663,15 @@ "projectFolder": "models/billing", "shouldPublish": false }, + { + "packageName": "@hcengineering/network", + "projectFolder": "network/core", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/network-zeromq", + "projectFolder": "network/zeromq" + }, { "packageName": "@hcengineering/pod-process", "projectFolder": "services/process",