Initial commit

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-09-08 20:31:55 +07:00
committed by Andrey Sobolev
commit 00bbbf2b4f
99 changed files with 14809 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# Don't allow people to merge changes to these generated files, because the result
# may be invalid. You need to run "rush update" again.
pnpm-lock.yaml merge=text
shrinkwrap.yaml merge=binary
npm-shrinkwrap.json merge=binary
yarn.lock merge=binary
# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
# may also require a special configuration to allow comments in JSON.
#
# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088
#
*.json linguist-language=JSON-with-Comments
+26
View File
@@ -0,0 +1,26 @@
name: CI
on:
push:
branches: ['main']
tags:
- 'v0.7.*'
pull_request:
branches: ['main']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- uses: actions/setup-node@v3
with:
node-version: 22
- name: Verify Change Logs
run: node common/scripts/install-run-rush.js change --verify
- name: Rush Install
run: node common/scripts/install-run-rush.js install
- name: Rush validate
run: node common/scripts/install-run-rush.js validate --verbose
+115
View File
@@ -0,0 +1,115 @@
.heft/
lib/
_api-extractor-temp/
temp/
.idea
pods/workspace/init/
pods/workspace/init-scripts/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*./rush-logs
*tests/sanity/screenshots
# Runtime data
*.pid
*.seed
*.pid.lock
# VS Code settings
.vscode/settings.json
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
# .env
# next.js build output
.next
# OS X temporary files
.DS_Store
# Rush temporary files
common/deploy/
common/temp/
common/autoinstallers/*/.npmrc
**/.rush/temp/
bundle.js
bundle/*.js
dist
.build
typings
types
.validate
tsconfig.tsbuildinfo
ingest-attachment-*.zip
tsdoc-metadata.json
pods/front/dist
*.cpuprofile
*.pyc
metrics.txt
dev/tool/report*.csv
tests/db_dump
.build
.format
tools/apm/apm.js
deploy
metrics.txt
services/github/pod-github/src/github.graphql
.build
.format
dev/tool/report.csv
bundle/*
bundle.js.map
tests/profiles
**/bundle/model.json
.wrangler
dump
**/logs/**
dev/tool/history.json
.aider*
/combined_dependencies
.tmp
ws-tests/docker-compose.override.yml
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
"bracketSpacing": true,
"proseWrap": "preserve",
"plugins": [],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"svelte.svelte-vscode",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner"
]
}
+995
View File
@@ -0,0 +1,995 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}",
"name": "Attach to Remote",
"port": 9229,
"request": "attach",
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"],
"type": "node"
},
{
"name": "Python: Embeddings",
"type": "python",
"request": "launch",
"program": "./pods/embeddings/server.py",
"args": ["--model", "sentence-transformers/all-MiniLM-L6-v2", "--device", "cpu"],
"console": "integratedTerminal",
"justMyCode": true,
"env": {
"PYTORCH_ENABLE_MPS_FALLBACK": "1"
}
},
{
"name": "Debug server",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
// "FULLTEXT_URL": "http://localhost:4700",
"FULLTEXT_URL": "http://huly.local:4702",
// "MONGO_URL": "mongodb://localhost:27017",
"DB_URL": "mongodb://localhost:27017",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
// "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
// "GREEN_URL": "http://huly.local:6767?token=secret",
"SERVER_PORT": "3333",
"APM_SERVER_URL2": "http://localhost:8200",
"METRICS_CONSOLE": "false",
"METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds.,
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"SERVER_SECRET": "secret",
"ENABLE_CONSOLE": "true",
"COLLABORATOR_URL": "ws://localhost:3078",
"REKONI_URL": "http://localhost:4004",
"FRONT_URL": "http://localhost:8080",
"ACCOUNTS_URL": "http://localhost:3000",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
// "SERVER_PROVIDER":"uweb"
"SERVER_PROVIDER": "ws",
"MODEL_VERSION": "0.7.48",
// "VERSION": "0.6.289",
"ELASTIC_INDEX_NAME": "local_storage_index",
"UPLOAD_URL": "/files",
"AI_BOT_URL": "http://localhost:4010",
"STATS_URL": "http://huly.local:4900",
"STREAM_URL": "http://huly.local:1080/recording",
"QUEUE_CONFIG": "localhost:19092"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"showAsyncStacks": true,
"outputCapture": "std",
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/server",
"protocol": "inspector"
},
{
"name": "Debug server (CR)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
// "FULLTEXT_URL": "http://localhost:4700",
"FULLTEXT_URL": "http://huly.local:4702",
// "MONGO_URL": "mongodb://localhost:27017",
// "DB_URL": "mongodb://localhost:27017",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable",
// "GREEN_URL": "http://huly.local:6767?token=secret",
"SERVER_PORT": "3332",
"APM_SERVER_URL2": "http://localhost:8200",
"METRICS_CONSOLE": "false",
"METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds.,
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"SERVER_SECRET": "secret",
"ENABLE_CONSOLE": "true",
"COLLABORATOR_URL": "ws://localhost:3078",
"REKONI_URL": "http://localhost:4004",
"FRONT_URL": "http://localhost:8080",
"ACCOUNTS_URL": "http://localhost:3000",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
// "SERVER_PROVIDER":"uweb"
"SERVER_PROVIDER": "ws",
"MODEL_VERSION": "0.7.186",
// "VERSION": "0.6.289",
"COMMUNICATION_API_ENABLED": "true",
"ELASTIC_INDEX_NAME": "local_storage_index",
"UPLOAD_URL": "/files",
"AI_BOT_URL": "http://localhost:4010",
"STATS_URL": "http://huly.local:4900",
"QUEUE_CONFIG": "localhost:19092",
"FILES_URL": "http://huly.local:4030/blob/:workspace/:blobId/:filename",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://huly.local:4318/v1/traces"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"showAsyncStacks": true,
"outputCapture": "std",
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/server",
"protocol": "inspector"
},
{
"name": "Debug server(Test)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"FULLTEXT_URL": "http://localhost:4710",
// "DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
// "GREEN_URL": "http://huly.local:6767?token=secret",
"SERVER_PORT": "3334",
"METRICS_CONSOLE": "false",
"DEBUG_PRINT_SQL": "true",
"METRICS_FILE": "${workspaceRoot}/metrics.txt", // Show metrics in console evert 30 seconds.,
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"SERVER_SECRET": "secret",
"ENABLE_CONSOLE": "true",
"COLLABORATOR_URL": "ws://localhost:3079",
"FRONT_URL": "http://localhost:8083",
"ACCOUNTS_URL": "http://localhost:3003",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
"MODEL_VERSION": "0.7.173",
"STATS_URL": "http://huly.local:4901",
"QUEUE_CONFIG": "localhost:19093"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "22",
"showAsyncStacks": true,
"outputCapture": "std",
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/server",
"protocol": "inspector"
},
{
"name": "Debug Fulltext",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
// "PORT": "4700",// For mongo
"PORT": "4710", // for cockroach
"FULLTEXT_DB_URL": "http://localhost:9201",
// "DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"SERVER_SECRET": "secret",
"REKONI_URL": "http://localhost:4004",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
"ELASTIC_INDEX_NAME": "local_storage_index",
"REGION": "",
"STATS_URL": "http://huly.local:4901",
"ACCOUNTS_URL": "http://huly.local:3003",
"QUEUE_CONFIG": "localhost:19093;-staging"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"showAsyncStacks": true,
"outputCapture": "std",
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/fulltext",
"protocol": "inspector"
},
{
"name": "Debug Account",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"MONGO_URL": "mongodb://localhost:27017",
// "DB_URL": "mongodb://localhost:27017",
"DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
"SERVER_SECRET": "secret",
"REGION_INFO": "|Mongo;cockroach|CockroachDB",
"TRANSACTOR_URL": "ws://huly.local:3333,ws://huly.local:3332;;cockroach",
"ACCOUNTS_URL": "http://localhost:3000",
"ACCOUNT_PORT": "3000",
// "FRONT_URL": "http://huly.local:8080",
"FRONT_URL": "http://huly.local:8087",
"STATS_URL": "http://huly.local:4900",
"MAIL_URL": "",
// "DB_NS": "account-2",
// "WS_LIVENESS_DAYS": "1",
// "WORKSPACE_LIMIT_PER_USER": "1",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost",
// "DISABLE_SIGNUP": "true",
"OTLP_ENDPOINT": "http://huly.local:4318/v1/traces"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector"
},
{
"name": "Debug Account(Staging)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
// "MONGO_URL": "mongodb://localhost:27018",
// "DB_URL": "mongodb://localhost:27018",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"DB_URL": "postgresql://root@huly.local:26258/defaultdb?sslmode=disable",
"SERVER_SECRET": "secret",
"REGION_INFO": "|Mongo;pg|Postgres;cockroach|CockroachDB",
"TRANSACTOR_URL": "ws://transactor:3334;ws://localhost:3334",
"ACCOUNTS_URL": "http://localhost:3003",
"ACCOUNT_PORT": "3003",
"FRONT_URL": "http://localhost:8083",
"STATS_URL": "http://huly.local:4901",
"MAIL_URL": "",
// "DB_NS": "account-2",
// "WS_LIVENESS_DAYS": "1",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost"
// "DISABLE_SIGNUP": "true",
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector"
},
{
"name": "Debug Stats",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"PORT": "4901",
"SERVER_SECRET": "secret"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/stats",
"protocol": "inspector"
},
{
"name": "Debug Workspace(mongo)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"DB_URL": "mongodb://localhost:27017",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
// "DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
"REGION": "",
"SERVER_SECRET": "secret",
"TRANSACTOR_URL": "ws://localhost:3333",
"ACCOUNTS_URL": "http://localhost:3000",
"FRONT_URL": "http://localhost:8080",
"MAIL_URL": "",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost",
"MODEL_VERSION": "v0.7.75",
"WS_OPERATION": "all+backup",
"BACKUP_STORAGE": "minio|minio?accessKey=minioadmin&secretKey=minioadmin",
"BACKUP_BUCKET": "dev-backups",
"INIT_REPO_DIR": "${workspaceRoot}/pods/workspace/init",
"INIT_WORKSPACE": "staging-dev",
"QUEUE_CONFIG": "huly.local:19092",
"QUEUE_REGION": ""
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/workspace",
"protocol": "inspector"
},
{
"name": "Debug Workspace(cockroach)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
// "DB_URL": "mongodb://localhost:27017",
// "DB_URL": "postgresql://postgres:example@localhost:5432",
"DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
"FULLTEXT_URL": "http://huly.local:4702",
"REGION": "cockroach",
"SERVER_SECRET": "secret",
"TRANSACTOR_URL": "ws://localhost:3332",
"ACCOUNTS_URL": "http://localhost:3000",
"FRONT_URL": "http://localhost:8080",
"MAIL_URL": "",
"STORAGE_CONFIG": "datalake|http://localhost:4030",
"MODEL_VERSION": "0.7.153",
"WS_OPERATION": "all+backup",
"BACKUP_STORAGE": "minio|minio?accessKey=minioadmin&secretKey=minioadmin",
"BACKUP_BUCKET": "dev-backups",
"QUEUE_CONFIG": "huly.local:19092"
// "INIT_REPO_DIR": "${workspaceRoot}/pods/workspace/init",
// "INIT_WORKSPACE": "staging-dev"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/workspace",
"protocol": "inspector"
},
{
"name": "Debug Front",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"MONGO_URL": "mongodb://localhost:27017",
"METRICS_CONSOLE": "false",
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"SERVER_SECRET": "secret",
"REKONI_URL": "http://localhost:4004",
"FRONT_URL": "http://localhost:8080",
"ACCOUNTS_URL": "http://localhost:3000",
"UPLOAD_URL": "/files",
"SERVER_PORT": "8087",
"COLLABORATOR_URL": "ws://localhost:3078",
"CALENDAR_URL": "http://localhost:8095",
"GMAIL_URL": "http://localhost:8088",
"TELEGRAM_URL": "http://localhost:8086",
"MODEL_VERSION": "",
"VERSION": ""
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"showAsyncStacks": true,
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/front",
"protocol": "inspector",
"outputCapture": "std"
},
{
"name": "Debug Collaborator",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"SECRET": "secret",
"METRICS_CONSOLE": "true",
"ACCOUNTS_URL": "http://localhost:3000",
"MONGO_URL": "mongodb://localhost:27017",
"STORAGE_CONFIG": "datalake|http://localhost:4030"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/collaborator",
"protocol": "inspector"
},
{
"type": "node",
"request": "launch",
"name": "Debug Jest tests",
"program": "${fileDirname}/../../node_modules/@rushstack/heft/lib/start.js",
"cwd": "${fileDirname}/../../",
"args": ["--debug", "test", "--clean", "--test-path-pattern", "${file}"],
"console": "integratedTerminal",
"sourceMaps": true,
"protocol": "inspector"
},
{
"name": "Debug generator",
"type": "node",
"request": "launch",
"args": ["src/index.ts", "gen-recruit", "ws1", "20"],
"env": {
"ACCOUNTS_URL": "ws://localhost:3000",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceRoot}/dev/generator",
"protocol": "inspector"
},
{
"name": "Debug backup tool",
"type": "node",
"request": "launch",
"args": [
"src/__start.ts",
"backup-restore",
"/Users/haiodo/Develop/private/platform/tests/sanity-ws",
"sanity-ws",
"--upgrade"
],
"env": {
"MODEL_VERSION": "0.7.153",
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"ACCOUNTS_URL": "http://localhost:3003",
"TRANSACTOR_URL": "ws://localhost:3334",
"ACCOUNT_DB_URL": "postgresql://root@localhost:26258/defaultdb?sslmode=disable",
"MONGO_URL": "mongodb://localhost:27018",
"ELASTIC_URL": "http://localhost:9201",
"SERVER_SECRET": "secret",
"DB_URL": "postgresql://root@localhost:26258/defaultdb?sslmode=disable",
"QUEUE_CONFIG": "localhost:19093;-staging"
},
"smartStep": false,
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"sourceMapRenames": false,
"skipFiles": ["<node_internals>/**"],
"cwd": "${workspaceRoot}/dev/tool",
"protocol": "inspector",
"outputCapture": "std",
"runtimeVersion": "22",
"showAsyncStacks": true
},
{
"name": "Debug tool upgrade",
"type": "node",
"request": "launch",
"args": ["src/__start.ts", "migrate-github-account", "--region", "cockroach", "--db", "%github"],
"env": {
"SERVER_SECRET": "secret",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost:9000",
"TRANSACTOR_URL": "ws://localhost:3333",
"MONGO_URL": "mongodb://localhost:27017",
"DB_URL": "mongodb://localhost:27017",
"ACCOUNTS_URL": "http://127.0.0.1:3000",
"ACCOUNT_DB_URL": "mongodb://localhost:27017",
"TELEGRAM_DATABASE": "telegram-service",
"REKONI_URL": "http://localhost:4004",
"MODEL_VERSION": "0.7.1"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/tool"
},
{
"name": "Debug tool upgrade PG(Cockroach)",
"type": "node",
"request": "launch",
"args": ["src/__start.ts", "upgrade-workspace", "platform"],
"env": {
"SERVER_SECRET": "secret",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost:9000",
"TRANSACTOR_URL": "ws://localhost:3332",
"ACCOUNTS_URL": "http://localhost:3000",
"ACCOUNT_DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
// "ACCOUNT_DB_URL": "postgresql://postgres:example@localhost:5433",
// "DB_URL": "postgresql://postgres:example@localhost:5433",
"DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable",
"MONGO_URL": "mongodb://localhost:27017",
"TELEGRAM_DATABASE": "telegram-service",
"REKONI_URL": "http://localhost:4004",
"MODEL_VERSION": "0.7.75"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/tool"
},
{
"name": "Debug tool move",
"type": "node",
"request": "launch",
"args": ["src/__start.ts", "move-to-pg"],
"env": {
"SERVER_SECRET": "secret",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost",
"TRANSACTOR_URL": "ws://localhost:3333",
"MONGO_URL": "mongodb://localhost:27017",
"DB_URL": "postgresql://postgres:example@localhost:5432",
"ACCOUNTS_URL": "http://localhost:3000",
"TELEGRAM_DATABASE": "telegram-service",
"REKONI_URL": "http://localhost:4004"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/tool"
},
{
"name": "Debug backup",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"STORAGE": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"WORKSPACE_STORAGE": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"DB_URL": "postgresql://root@host.docker.internal:26257/defaultdb?sslmode=disable",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
"SECRET": "secret",
"REGION": "cockroach",
"BUCKET_NAME": "backups",
"INTERVAL": "300"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"showAsyncStacks": true,
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/backup",
"protocol": "inspector"
},
{
"name": "Debug backup API",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"STORAGE": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"SECRET": "secret",
"BUCKET_NAME": "backups"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"showAsyncStacks": true,
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/services/backup/backup-api-pod",
"protocol": "inspector"
},
{
"name": "Debug archive tool",
"type": "node",
"request": "launch",
"args": ["src/__start.ts", "archive-workspaces-mongo", "--timeout", "7", "--remove"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"STORAGE": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"WORKSPACE_STORAGE": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"MONGO_URL": "mongodb://localhost:27017",
"DB_URL": "mongodb://localhost:27017",
"SERVER_SECRET": "secret",
"SECRET": "secret",
"BUCKET_NAME": "backups"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"showAsyncStacks": true,
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/tool",
"protocol": "inspector"
},
{
"name": "Debug restore-all tool",
"type": "node",
"request": "launch",
"args": ["src/__start.ts", "restore-all"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"STORAGE": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
// "DB_URL": "mongodb://localhost:27017",
"DB_URL": "postgresql://root@host.docker.internal:26258/defaultdb?sslmode=disable",
"SERVER_SECRET": "secret",
"BUCKET_NAME": "dev-backups"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"showAsyncStacks": true,
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/tool",
"protocol": "inspector"
},
{
"name": "Debug Github integration",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"SERVER_SECRET": "secret",
"ACCOUNTS_URL": "http://localhost:3000",
"APP_ID": "${env:POD_GITHUB_APPID}",
"CLIENT_ID": "${env:POD_GITHUB_CLIENTID}",
"CLIENT_SECRET": "${env:POD_GITHUB_CLIENT_SECRET}",
"PRIVATE_KEY": "${env:POD_GITHUB_PRIVATE_KEY}",
"COLLABORATOR_URL": "ws://huly.local:3078",
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"PLATFORM_OPERATION_LOGGING": "true",
"FRONT_URL": "http://localhost:8080",
"PORT": "3500",
"STATS_URL": "http://huly.local:4900",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://huly.local:4318/v1/traces"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/github/pod-github",
"protocol": "inspector",
"outputCapture": "std"
},
{
"name": "Debug qms workers",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"SECRET": "secret",
"DOCS_RELEASE_INTERVAL": "10000",
"DOCS_IN_REVIEW_CHECK_INTERVAL": "10000",
"DOCS_REVIEW_PERIOD_IN_MONTHS": "1"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"showAsyncStacks": true,
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/workers"
},
{
"name": "Debug doc import tool",
"type": "node",
"request": "launch",
"args": ["src/__start.ts", "import", "--spec", "toc.json", "docx", "ws", "person-owner"],
"env": {
"SERVER_SECRET": "secret",
"ACCOUNTS_URL": "http://localhost:3000",
"COLLABORATOR_URL": "ws://localhost:3078",
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"MONGO_URL": "mongodb://localhost:27017"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"showAsyncStacks": true,
"sourceMaps": true,
"cwd": "${workspaceRoot}/dev/doc-import-tool",
"protocol": "inspector"
},
{
"name": "Debug process",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"SECRET": "secret",
"ACCOUNTS_URL": "http://localhost:3000",
"QUEUE_CONFIG": "localhost:19092",
"QUEUE_REGION": "cockroach",
"TEMPORAL_ADDRESS": "localhost:7233"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/process"
},
{
"name": "Debug print",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"PORT": "4005",
"SECRET": "secret",
"MONGO_URL": "mongodb://localhost:27017",
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/print/pod-print"
},
{
"name": "Debug sign",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"PORT": "4006",
"SECRET": "secret",
"CERTIFICATE_PATH": "${workspaceRoot}/services/sign/pod-sign/debug/certificate.p12",
"MONGO_URL": "mongodb://localhost:27017",
"MINIO_ENDPOINT": "localhost",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"SERVICE_ID": "sign-service",
"ACCOUNTS_URL": "http://localhost:3000",
"BRANDING_PATH": "${workspaceRoot}/services/sign/pod-sign/debug/branding.json"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/sign/pod-sign"
},
{
"name": "Debug rekoni",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"PORT": "4004",
"SERVICE_ID": "rekoni-service",
"SECRET": "secret"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/rekoni",
"protocol": "inspector"
},
{
"name": "Debug analytics collector",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"PORT": "4017",
"SECRET": "secret",
"MONGO_URL": "mongodb://localhost:27017",
"MINIO_ENDPOINT": "localhost",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"SERVER_SECRET": "secret",
"SERVICE_ID": "analytics-collector",
"ACCOUNTS_URL": "http://localhost:3000"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"showAsyncStacks": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/services/analytics-collector/pod-analytics-collector"
},
{
"name": "Debug calendar",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"Credentials": "",
"KVS_URL": "http://localhost:8094",
"SECRET": "secret",
"WATCH_URL": "https://calendar.hc.engineering/push"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"showAsyncStacks": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/services/calendar/pod-calendar"
},
{
"name": "Debug AI bot",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"MONGO_URL": "mongodb://localhost:27017",
"PORT": "4010",
"SERVER_SECRET": "secret",
"FIRST_NAME": "Jolie",
"LAST_NAME": "AI",
"PASSWORD": "password",
"AVATAR_PATH": "./assets/avatar.png",
"AVATAR_CONTENT_TYPE": ".png",
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"LOVE_ENDPOINT": "http://localhost:8096"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"runtimeVersion": "20",
"cwd": "${workspaceRoot}/services/ai-bot/pod-ai-bot",
"protocol": "inspector",
"outputCapture": "std"
},
{
"name": "Debug telegram bot",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"PORT": "4020",
"BOT_TOKEN": "token",
"MONGO_URL": "mongodb://localhost:27017",
"MONGO_DB": "telegram-bot",
"SECRET": "secret",
"ACCOUNTS_URL": "http://huly.local:3000",
"SERVICE_ID": "telegram-bot-service",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_ENDPOINT": "localhost",
"QUEUE_CONFIG": "localhost:19092",
"QUEUE_REGION": "cockroach",
"DB_URL": "postgresql://root@huly.local:26257/defaultdb?sslmode=disable"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/telegram-bot/pod-telegram-bot"
},
{
"name": "Debug datalake",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"PORT": "4030",
"SECRET": "secret",
"DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable",
"BUCKETS": "blobs,eu|http://localhost:9000?accessKey=minioadmin&secretKey=minioadmin",
"ACCOUNTS_URL": "http://localhost:3000",
"STATS_URL": "http://huly.local:4900",
"QUEUE_CONFIG": "localhost:19092"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/datalake/pod-datalake"
},
{
"name": "Debug media",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://huly.local:3000",
"SECRET": "secret",
"REGION": "cockroach",
"QUEUE_CONFIG": "localhost:19092"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/media"
},
{
"name": "Debug preview",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://huly.local:3000",
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"SECRET": "secret",
"CACHE_PATH": "/tmp/preview"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeVersion": "20",
"sourceMaps": true,
"cwd": "${workspaceRoot}/pods/preview"
},
{
"type": "chrome",
"name": "Attach to Browser",
"request": "attach",
"urlFilter": "http://localhost:8080/*",
"port": 9222,
"sourceMapPathOverrides": {
"webpack://*": "${workspaceFolder}/*"
}
},
{
"name": "Debug Notion import",
"type": "node",
"request": "launch",
"args": [
"src/__start.ts",
"import-notion-to-teamspace",
"/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743",
"-u",
"user1",
"-pw",
"1234",
"-ws",
"ws5",
"-ts",
"notion"
],
// "args": ["src/__start.ts", "import-notion-with-teamspaces", "/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743", "-u", "user1", "-pw", "1234", "-ws", "ws1"],
"env": {
"FRONT_URL": "http://localhost:8087"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/import-tool"
},
{
"name": "Debug ClickUp import",
"type": "node",
"request": "launch",
"args": [
"src/__start.ts",
"import-clickup-tasks",
"/home/anna/work/clickup/aleksandr/debug/mentions.csv",
"-u",
"user1",
"-pw",
"1234",
"-ws",
"ws10"
],
"env": {
"FRONT_URL": "http://localhost:8087"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/import-tool"
},
{
"name": "Debug Huly import",
"type": "node",
"request": "launch",
"args": [
"src/__start.ts",
"import",
"/home/anna/huly/platform/dev/import-tool/docs/huly/example-workspace",
"-u",
"user1",
"-pw",
"1234",
"-ws",
"ws1"
],
"env": {
"FRONT_URL": "http://localhost:8087"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/dev/import-tool"
},
{
"name": "Debug Export service",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"PORT": "4009",
"SECRET": "secret",
"ACCOUNTS_URL": "http://localhost:3000",
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"TRANSACTOR_URL": "ws://localhost:3333",
"SERVICE_ID": "export-service"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceRoot}/services/export/pod-export"
},
{
"name": "Msg2File",
"type": "node",
"request": "launch",
"args": ["src/index.ts"],
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable",
"PORT": "9087",
"SECRET": "secret",
"SERVICE_ID": "msg2file-service",
"STORAGE_CONFIG": "datalake|http://huly.local:4030"
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/services/msg2file"
}
]
}
+277
View File
@@ -0,0 +1,277 @@
Eclipse Public License - v 2.0
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial content
Distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from
and are Distributed by that particular Contributor. A Contribution
"originates" from a Contributor if it was added to the Program by
such Contributor itself or anyone acting on such Contributor's behalf.
Contributions do not include changes or additions to the Program that
are not Modified Works.
"Contributor" means any person or entity that Distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which
are necessarily infringed by the use or sale of its Contribution alone
or when combined with the Program.
"Program" means the Contributions Distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement
or any Secondary License (as applicable), including Contributors.
"Derivative Works" shall mean any work, whether in Source Code or other
form, that is based on (or derived from) the Program and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship.
"Modified Works" shall mean any work in Source Code or other form that
results from an addition to, deletion from, or modification of the
contents of the Program, including, for purposes of clarity any new file
in Source Code form that contains any contents of the Program. Modified
Works shall not include works that contain only declarations,
interfaces, types, classes, structures, or files of the Program solely
in each case in order to link to, bind by name, or subclass the Program
or Modified Works thereof.
"Distribute" means the acts of a) distributing or b) making available
in any manner that enables the transfer of a copy.
"Source Code" means the form of a Program preferred for making
modifications, including but not limited to software source code,
documentation source, and configuration files.
"Secondary License" means either the GNU General Public License,
Version 2.0, or any later versions of that license, including any
exceptions or additional permissions as identified by the initial
Contributor.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free copyright
license to reproduce, prepare Derivative Works of, publicly display,
publicly perform, Distribute and sublicense the Contribution of such
Contributor, if any, and such Derivative Works.
b) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free patent
license under Licensed Patents to make, use, sell, offer to sell,
import and otherwise transfer the Contribution of such Contributor,
if any, in Source Code or other form. This patent license shall
apply to the combination of the Contribution and the Program if, at
the time the Contribution is added by the Contributor, such addition
of the Contribution causes such combination to be covered by the
Licensed Patents. The patent license shall not apply to any other
combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the
licenses to its Contributions set forth herein, no assurances are
provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity.
Each Contributor disclaims any liability to Recipient for claims
brought by any other entity based on infringement of intellectual
property rights or otherwise. As a condition to exercising the
rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual
property rights needed, if any. For example, if a third party
patent license is required to allow Recipient to Distribute the
Program, it is Recipient's responsibility to acquire that license
before distributing the Program.
d) Each Contributor represents that to its knowledge it has
sufficient copyright rights in its Contribution, if any, to grant
the copyright license set forth in this Agreement.
e) Notwithstanding the terms of any Secondary License, no
Contributor makes additional grants to any Recipient (other than
those set forth in this Agreement) as a result of such Recipient's
receipt of the Program under the terms of a Secondary License
(if permitted under the terms of Section 3).
3. REQUIREMENTS
3.1 If a Contributor Distributes the Program in any form, then:
a) the Program must also be made available as Source Code, in
accordance with section 3.2, and the Contributor must accompany
the Program with a statement that the Source Code for the Program
is available under this Agreement, and informs Recipients how to
obtain it in a reasonable manner on or through a medium customarily
used for software exchange; and
b) the Contributor may Distribute the Program under a license
different than this Agreement, provided that such license:
i) effectively disclaims on behalf of all other Contributors all
warranties and conditions, express and implied, including
warranties or conditions of title and non-infringement, and
implied warranties or conditions of merchantability and fitness
for a particular purpose;
ii) effectively excludes on behalf of all other Contributors all
liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;
iii) does not attempt to limit or alter the recipients' rights
in the Source Code under section 3.2; and
iv) requires any subsequent distribution of the Program by any
party to be under a license that satisfies the requirements
of this section 3.
3.2 When the Program is Distributed as Source Code:
a) it must be made available under this Agreement, or if the
Program (i) is combined with other material in a separate file or
files made available under a Secondary License, and (ii) the initial
Contributor attached to the Source Code the notice described in
Exhibit A of this Agreement, then the Program may be made available
under the terms of such Secondary Licenses, and
b) a copy of this Agreement must be included with each copy of
the Program.
3.3 Contributors may not remove or alter any copyright, patent,
trademark, attribution notices, disclaimers of warranty, or limitations
of liability ("notices") contained within the Program from any copy of
the Program which they Distribute, provided that Contributors may add
their own appropriate notices.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities
with respect to end users, business partners and the like. While this
license is intended to facilitate the commercial use of the Program,
the Contributor who includes the Program in a commercial product
offering should do so in a manner which does not create potential
liability for other Contributors. Therefore, if a Contributor includes
the Program in a commercial product offering, such Contributor
("Commercial Contributor") hereby agrees to defend and indemnify every
other Contributor ("Indemnified Contributor") against any losses,
damages and costs (collectively "Losses") arising from claims, lawsuits
and other legal actions brought by a third party against the Indemnified
Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program
in a commercial product offering. The obligations in this section do not
apply to any claims or Losses relating to any actual or alleged
intellectual property infringement. In order to qualify, an Indemnified
Contributor must: a) promptly notify the Commercial Contributor in
writing of such claim, and b) allow the Commercial Contributor to control,
and cooperate with the Commercial Contributor in, the defense and any
related settlement negotiations. The Indemnified Contributor may
participate in any such claim at its own expense.
For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those performance
claims and warranties, and if a court requires any other Contributor to
pay any damages as a result, the Commercial Contributor must pay
those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all
risks associated with its exercise of rights under this Agreement,
including but not limited to the risks and costs of program errors,
compliance with applicable laws, damage to or loss of data, programs
or equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further
action by the parties hereto, such provision shall be reformed to the
minimum extent necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other software
or hardware) infringes such Recipient's patent(s), then such Recipient's
rights granted under Section 2(b) shall terminate as of the date such
litigation is filed.
All Recipient's rights under this Agreement shall terminate if it
fails to comply with any of the material terms or conditions of this
Agreement and does not cure such failure in a reasonable period of
time after becoming aware of such noncompliance. If all Recipient's
rights under this Agreement terminate, Recipient agrees to cease use
and distribution of the Program as soon as reasonably practicable.
However, Recipient's obligations under this Agreement and any licenses
granted by Recipient relating to the Program shall continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement,
but in order to avoid inconsistency the Agreement is copyrighted and
may only be modified in the following manner. The Agreement Steward
reserves the right to publish new versions (including revisions) of
this Agreement from time to time. No one other than the Agreement
Steward has the right to modify this Agreement. The Eclipse Foundation
is the initial Agreement Steward. The Eclipse Foundation may assign the
responsibility to serve as the Agreement Steward to a suitable separate
entity. Each new version of the Agreement will be given a distinguishing
version number. The Program (including Contributions) may always be
Distributed subject to the version of the Agreement under which it was
received. In addition, after a new version of the Agreement is published,
Contributor may elect to Distribute the Program (including its
Contributions) under the new version.
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
receives no rights or licenses to the intellectual property of any
Contributor under this Agreement, whether expressly, by implication,
estoppel or otherwise. All rights in the Program not expressly granted
under this Agreement are reserved. Nothing in this Agreement is intended
to be enforceable by any entity that is not a Contributor or Recipient.
No third-party beneficiary rights are created under this Agreement.
Exhibit A - Form of Secondary Licenses Notice
"This Source Code may also be made available under the following
Secondary Licenses when the conditions for such availability set forth
in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
version(s), and exceptions or additional permissions here}."
Simply including a copy of this Agreement, including this Exhibit A
is not sufficient to license the Source Code under Secondary Licenses.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to
look for such a notice.
You may add additional accurate notices of copyright ownership.
+449
View File
@@ -0,0 +1,449 @@
# Huly Virtual Network
A distributed, scalable virtual network architecture that enables fault-tolerant communication across distributed containers and agents.
## 🚀 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 and round-robin distribution across multiple physical nodes
- **Multi-Tenant Architecture**: Secure isolation of containers for user sessions, query engines, and transaction processors
- **Fault Tolerance**: Automatic failover, health checks, and recovery mechanisms with orphaned container handling
- **Dynamic Scaling**: Horizontal scaling with automatic container lifecycle management
- **Real-time Communication**: Event-driven architecture with ZeroMQ-based messaging and broadcast capabilities
## 📦 Packages
This monorepo contains four main packages and deployment pods:
### Packages
- **`@hcengineering/network-core`**: Core network implementation, agent management, and container orchestration
- **`@hcengineering/network-backrpc`**: ZeroMQ-based RPC communication layer with bidirectional messaging
- **`@hcengineering/network-client`**: Client libraries for connecting to the network and managing containers
- **`@hcengineering/network-server`**: Network server implementation with multi-client support
### Deployment Pods
- **`@hcengineering/network-pod`**: Dockerized network server for production deployment
## 🏗️ Architecture
### Core Concepts
The Huly Virtual Network operates on three main concepts:
1. **Network**: Central coordinator that manages agents and routes container requests
2. **Agents**: Worker nodes that host and manage containers of specific kinds
3. **Containers**: Individual service instances that handle business logic
### Agents & Containers
**Agents** are the foundation for container management. They register with the network, advertise supported container kinds, and manage container lifecycles. Each agent can host multiple containers and provides:
- Container startup and termination
- Health monitoring and keep-alive functionality
- Local container routing and communication
- Resource management and isolation
**Containers** are the workhorses of the system. They can be:
- Located by `{kind + uuid}` or labels
- Communicated with via direct messages or request/response patterns
- Referenced by clients (containers remain active while referenced)
- Automatically terminated after a timeout when unreferenced
### Communication Patterns
Containers support two communication patterns:
1. **Fire-and-forget messaging**: Send data to containers without expecting responses
2. **Request/Response**: Establish connections for bidirectional communication with response handling
The network maintains references to containers. Referenced containers stay active, while unreferenced containers are kept alive for a configurable timeout before termination.
```mermaid
flowchart TB
subgraph Network["Huly Network"]
NetworkCore["Network Core"]
Router["Message Router"]
Registry["Container Registry"]
end
subgraph Agent1["Agent 1"]
A1_Session["Session Containers"]
A1_Query["Query Containers"]
end
subgraph Agent2["Agent 2"]
A2_Transactor["Transactor Containers"]
A2_Workspace["Workspace Containers"]
end
subgraph Client["Clients"]
WebClient["Web Client"]
APIClient["API Client"]
end
Client --> Network
Network --> Agent1
Network --> Agent2
Agent1 --> A1_Session
Agent1 --> A1_Query
Agent2 --> A2_Transactor
Agent2 --> A2_Workspace
```
## 🛠️ Technology Stack
- **Language**: TypeScript 5.8+
- **Runtime**: Node.js 22+
- **Communication**: ZeroMQ 6.5+ for high-performance messaging
- **Build System**: Rush.js for monorepo management
- **Package Manager**: PNPM 10.15+
- **Testing**: Jest 29+ with comprehensive test coverage
- **License**: Eclipse Public License 2.0
## 🚀 Getting Started
### Prerequisites
- Node.js 22.00.0+
- PNPM (automatically installed via Rush)
- ZeroMQ native dependencies
### Installation
```bash
# Clone the repository
git clone https://github.com/hcengineering/huly.net.git
cd huly.net
# Install dependencies and build
node common/scripts/install-run-rush.js install
node common/scripts/install-run-rush.js build
```
### Docker Deployment
The network-pod provides a Dockerized version for production deployment:
```bash
# Build the Docker image
cd pods/network-pod
npm run docker:build
# Run the container
docker run -p 3737:3737 hardcoreeng/network-pod
```
### Quick Start Example
#### Setting up a Network Server
```typescript
import { NetworkImpl, TickManagerImpl } from '@hcengineering/network-core'
import { NetworkServer } from '@hcengineering/network-server'
// Create network components
const tickManager = new TickManagerImpl(1000) // 1000 ticks per second
const network = new NetworkImpl(tickManager)
const server = new NetworkServer(network, tickManager, '*', 3737)
console.log('Network server started on port 3737')
```
#### Creating and Registering an Agent
```typescript
import { AgentImpl } from '@hcengineering/network-core'
import { NetworkAgentServer } from '@hcengineering/network-client'
// Define container factory
const containerFactory = {
session: async (uuid, request) => {
const container = new SessionContainer(uuid)
const endpoint = await container.start()
return [container, endpoint]
}
}
// Create and start agent
const agent = new AgentImpl('agent-1', containerFactory)
const agentServer = new NetworkAgentServer(tickManager, 'localhost', '*', 3738)
await agentServer.start(agent)
await networkClient.register(agent)
```
#### Connecting and Using Containers
```typescript
import { createNetworkClient } from '@hcengineering/network-client'
// Connect to network
const client = createNetworkClient('localhost', 3737)
// Get or create a container
const containerRef = await client.get('user-session-1', {
kind: 'session',
labels: { userId: 'user1' }
})
// Establish connection and communicate
const connection = await containerRef.connect()
const response = await connection.request('processData', { data: 'example' })
// Handle events
connection.on = async (event) => {
console.log('Received event:', event)
}
// Cleanup
await connection.close()
await containerRef.release()
```
## 📚 API Reference
### Network Interface
The core `Network` interface provides:
- `register(record, agent)`: Register an agent with the network
- `unregister(agentId)`: Unregister an agent from the network
- `ping(agentId | clientId)`: Mark an agent or client as alive
- `get(client, uuid, request)`: Get or create a container
- `list(kind)`: List containers of a specific kind
- `release(client, uuid)`: Release a container reference
- `request(target, operation, data)`: Send request to container
- `agents()`: Get all registered agents
- `kinds()`: Get all supported container kinds
### Agent Interface
The `NetworkAgent` interface defines:
- `get(uuid, request)`: Start or get a container
- `getContainer(uuid)`: Get low-level container reference
- `list(kind?)`: List agent's containers
- `request(target, operation, data)`: Send request to container
- `terminate(endpoint)`: Terminate a container
- Properties: `uuid`, `endpoint`, `kinds`, `onUpdate`, `onAgentUpdate`
### Container Interface
The `Container` interface includes:
- `request(operation, data, clientId?)`: Handle requests
- `connect(clientId, broadcast)`: Accept client connections with broadcast callback
- `disconnect(clientId)`: Remove client connections
- `terminate()`: Cleanup and shutdown
- `ping()`: Health check response
- Optional: `onTerminated()`: Cleanup callback
### TickManager Interface
The `TickManager` handles time-based operations:
- `now()`: Get current timestamp
- `register(handler, interval)`: Register periodic handler (interval in seconds)
- `start()`: Start the tick manager
- `stop()`: Stop the tick manager
- `waitTick(ticks)`: Wait for specific number of ticks
## 🏗️ Building Applications
### Container Types
The network supports different container kinds for various use cases, every agent could provide a list of supported container kinds:
Examples of container kinds:
- **Session Containers**: Handle user sessions and authentication
- **Query Containers**: Process database queries and searches
- **Transactor Containers**: Manage transactions and data modifications
- **Workspace Containers**: Provide workspace-specific business logic
### Container Communication Patterns
#### Request/Response Pattern
```typescript
// Direct request to container
const result = await network.request(containerUuid, 'operation', data)
// Via connection
const connection = await containerRef.connect()
const result = await connection.request('operation', data)
```
#### Event Broadcasting
```typescript
// Container broadcasts events to connected clients
class MyContainer implements Container {
private handlers = new Map<ClientUuid, Function>()
connect(clientId: ClientUuid, handler: Function) {
this.handlers.set(clientId, handler)
}
private broadcast(event: any) {
for (const handler of this.handlers.values()) {
handler(event)
}
}
}
```
### Health Monitoring
The network includes built-in health monitoring with configurable timeouts:
- **Agent Health Checks**: Automatic detection of inactive agents (default: 3-second timeout)
- **Ping Interval**: Regular ping to maintain agent connections (default: 1-second interval)
- **Dead Agent Detection**: Automatic cleanup of disconnected agents and their containers
- **Orphaned Container Recovery**: Cleanup of containers whose agents have disconnected
- **Container Reference Tracking**: Automatic lifecycle management based on client references
```typescript
import { timeouts } from '@hcengineering/network-core'
console.log('Alive timeout:', timeouts.aliveTimeout, 'seconds') // 3
console.log('Ping interval:', timeouts.pingInterval, 'seconds') // 1
```
## 🧪 Testing
Run the test suite:
```bash
# Install dependencies first
node common/scripts/install-run-rush.js install
# Run all tests
node common/scripts/install-run-rush.js test
# Run tests for specific package
cd packages/core && npm test
cd packages/backrpc && npm test
cd packages/client && npm test
cd packages/server && npm test
# Run build
node common/scripts/install-run-rush.js build
# Run with watch mode
node common/scripts/install-run-rush.js build:watch
```
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Make your changes and add tests
4. Install dependencies: `node common/scripts/install-run-rush.js install`
5. Ensure all tests pass: `node common/scripts/install-run-rush.js test`
6. Format code: `node common/scripts/install-run-rush.js format`
7. Build project: `node common/scripts/install-run-rush.js build`
8. Commit changes: `git commit -s -m 'Add amazing feature'`
9. Push to branch: `git push origin feature/amazing-feature`
10. Open a Pull Request
### Development Setup
The project uses Rush.js for monorepo management:
- All packages share common build configuration
- Dependencies are managed at the workspace level
- Incremental builds and caching for faster development
## 📄 License
This project is licensed under the Eclipse Public License 2.0 - see the [LICENSE](LICENSE) file for details.
## 🔗 Related Projects
- [Huly Platform](https://github.com/hcengineering/platform) - The main Huly platform
- [ZeroMQ](https://zeromq.org/) - High-performance messaging library
---
**Note**: This is a foundational networking library for the Huly ecosystem. For application-level documentation, please refer to the main Huly platform repository.
## Huly on Network example
## 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
parsonal-ws:user1 -..->|event's| Endpoint
query:europe -..->|resp| parsonal-ws:user1
parsonal-ws:user1 -..->|response chunks| Endpoint
parsonal-ws:user1 -..->|tx| transactor:ws1
transactor:ws1 -..->|event's| HulyPulse
transactor:ws1 -..->|event's| parsonal-ws:user1
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
Indexer -..->|indexed tx| parsonal-ws:user1
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}
```
+33
View File
@@ -0,0 +1,33 @@
# Rush uses this file to configure the NPM package registry during installation. It is applicable
# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install",
# "rush update", and the "install-run.js" scripts.
#
# NOTE: The "rush publish" command uses .npmrc-publish instead.
#
# Before invoking the package manager, Rush will generate an .npmrc in the folder where installation
# is performed. This generated file will omit any config lines that reference environment variables
# that are undefined in that session; this avoids problems that would otherwise result due to
# a missing variable being replaced by an empty string.
#
# If "subspacesEnabled" is true in subspaces.json, the generated file will merge settings from
# "common/config/rush/.npmrc" and "common/config/subspaces/<name>/.npmrc", with the latter taking
# precedence.
#
# * * * SECURITY WARNING * * *
#
# It is NOT recommended to store authentication tokens in a text file on a lab machine, because
# other unrelated processes may be able to read that file. Also, the file may persist indefinitely,
# for example if the machine loses power. A safer practice is to pass the token via an
# environment variable, which can be referenced from .npmrc using ${} expansion. For example:
#
# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}
#
# Explicitly specify the NPM registry that "rush install" and "rush update" will use by default:
registry=https://registry.npmjs.org/
# Optionally provide an authentication token for the above registry URL (if it is a private registry):
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
# Change this to "true" if your registry requires authentication for read-only operations:
always-auth=false
+29
View File
@@ -0,0 +1,29 @@
# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish
# is used by the "rush publish" command, as publishing often involves different credentials
# and registries than other operations.
#
# Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc"
# and then temporarily map that folder as the "home directory" for the current user account.
# This enables the same settings to apply for each project folder that gets published. The copied file
# will omit any config lines that reference environment variables that are undefined in that session;
# this avoids problems that would otherwise result due to a missing variable being replaced by
# an empty string.
#
# * * * SECURITY WARNING * * *
#
# It is NOT recommended to store authentication tokens in a text file on a lab machine, because
# other unrelated processes may be able to read the file. Also, the file may persist indefinitely,
# for example if the machine loses power. A safer practice is to pass the token via an
# environment variable, which can be referenced from .npmrc using ${} expansion. For example:
#
# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}
#
# Explicitly specify the NPM registry that "rush publish" will use by default:
registry=https://registry.npmjs.org/
# Optionally provide an authentication token for the above registry URL (if it is a private registry):
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
# Change this to "true" if your registry requires authentication for read-only operations:
always-auth=false
+38
View File
@@ -0,0 +1,38 @@
'use strict';
/**
* When using the PNPM package manager, you can use pnpmfile.js to workaround
* dependencies that have mistakes in their package.json file. (This feature is
* functionally similar to Yarn's "resolutions".)
*
* For details, see the PNPM documentation:
* https://pnpm.io/pnpmfile#hooks
*
* IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE
* ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run
* "rush update --full" so that PNPM will recalculate all version selections.
*/
module.exports = {
hooks: {
readPackage
}
};
/**
* This hook is invoked during installation before a package's dependencies
* are selected.
* The `packageJson` parameter is the deserialized package.json
* contents for the package that is about to be installed.
* The `context` parameter provides a log() function.
* The return value is the updated object.
*/
function readPackage(packageJson, context) {
// // The karma types have a missing dependency on typings from the log4js package.
// if (packageJson.name === '@types/karma') {
// context.log('Fixed up dependencies for @types/karma');
// packageJson.dependencies['log4js'] = '0.6.38';
// }
return packageJson;
}
+109
View File
@@ -0,0 +1,109 @@
/**
* This configuration file manages Rush integration with JFrog Artifactory services.
* More documentation is available on the Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json",
"packageRegistry": {
/**
* (Required) Set this to "true" to enable Rush to manage tokens for an Artifactory NPM registry.
* When enabled, "rush install" will automatically detect when the user's ~/.npmrc
* authentication token is missing or expired. And "rush setup" will prompt the user to
* renew their token.
*
* The default value is false.
*/
"enabled": false,
/**
* (Required) Specify the URL of your NPM registry. This is the same URL that appears in
* your .npmrc file. It should look something like this example:
*
* https://your-company.jfrog.io/your-project/api/npm/npm-private/
*/
"registryUrl": "",
/**
* A list of custom strings that "rush setup" should add to the user's ~/.npmrc file at the time
* when the token is updated. This could be used for example to configure the company registry
* to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush
* operations like "rush add" and "rush install", which get their mappings from the monorepo's
* common/config/rush/.npmrc file).
*
* NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful
* about adding settings that may interfere with other work outside the monorepo.
*/
"userNpmrcLinesToAdd": [
// "@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/"
],
/**
* (Required) Specifies the URL of the Artifactory control panel where the user can generate
* an API key. This URL is printed after the "visitWebsite" message.
* It should look something like this example: https://your-company.jfrog.io/
* Specify an empty string to suppress this line entirely.
*/
"artifactoryWebsiteUrl": "",
/**
* Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file.
* The default is "password", which means the user's API token will be traded in for an
* npm password specific to that registry. Optionally you can specify "authToken", which
* will save the user's API token as credentials instead.
*/
// "credentialType": "password",
/**
* These settings allow the "rush setup" interactive prompts to be customized, for
* example with messages specific to your team or configuration. Specify an empty string
* to suppress that message entirely.
*/
"messageOverrides": {
/**
* Overrides the message that normally says:
* "This monorepo consumes packages from an Artifactory private NPM registry."
*/
// "introduction": "",
/**
* Overrides the message that normally says:
* "Please contact the repository maintainers for help with setting up an Artifactory user account."
*/
// "obtainAnAccount": "",
/**
* Overrides the message that normally says:
* "Please open this URL in your web browser:"
*
* The "artifactoryWebsiteUrl" string is printed after this message.
*/
// "visitWebsite": "",
/**
* Overrides the message that normally says:
* "Your user name appears in the upper-right corner of the JFrog website."
*/
// "locateUserName": "",
/**
* Overrides the message that normally says:
* "Click 'Edit Profile' on the JFrog website. Click the 'Generate API Key'
* button if you haven't already done so previously."
*/
// "locateApiKey": ""
/**
* Overrides the message that normally prompts:
* "What is your Artifactory user name?"
*/
// "userNamePrompt": ""
/**
* Overrides the message that normally prompts:
* "What is your Artifactory API key?"
*/
// "apiKeyPrompt": ""
}
}
}
+160
View File
@@ -0,0 +1,160 @@
/**
* This configuration file manages Rush's build cache feature.
* More documentation is available on the Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json",
/**
* (Required) EXPERIMENTAL - Set this to true to enable the build cache feature.
*
* See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature.
*/
"buildCacheEnabled": false,
/**
* (Required) Choose where project build outputs will be cached.
*
* Possible values: "local-only", "azure-blob-storage", "amazon-s3"
*/
"cacheProvider": "local-only",
/**
* Setting this property overrides the cache entry ID. If this property is set, it must contain
* a [hash] token.
*
* Other available tokens:
* - [projectName] Example: "@my-scope/my-project"
* - [projectName:normalize] Example: "my-scope+my-project"
* - [phaseName] Example: "_phase:test/api"
* - [phaseName:normalize] Example: "_phase:test+api"
* - [phaseName:trimPrefix] Example: "test/api"
* - [os] Example: "win32"
* - [arch] Example: "x64"
*/
// "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]"
/**
* (Optional) Salt to inject during calculation of the cache key. This can be used to invalidate the cache for all projects when the salt changes.
*/
// "cacheHashSalt": "1",
/**
* Use this configuration with "cacheProvider"="azure-blob-storage"
*/
"azureBlobStorageConfiguration": {
/**
* (Required) The name of the the Azure storage account to use for build cache.
*/
// "storageAccountName": "example",
/**
* (Required) The name of the container in the Azure storage account to use for build cache.
*/
// "storageContainerName": "my-container",
/**
* The Azure environment the storage account exists in. Defaults to AzurePublicCloud.
*
* Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment"
*/
// "azureEnvironment": "AzurePublicCloud",
/**
* An optional prefix for cache item blob names.
*/
// "blobPrefix": "my-prefix",
/**
* If set to true, allow writing to the cache. Defaults to false.
*/
// "isCacheWriteAllowed": true,
/**
* The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise.
*/
// "loginFlow": "InteractiveBrowser",
/**
* If set to true, reading the cache requires authentication. Defaults to false.
*/
// "readRequiresAuthentication": true
},
/**
* Use this configuration with "cacheProvider"="amazon-s3"
*/
"amazonS3Configuration": {
/**
* (Required unless s3Endpoint is specified) The name of the bucket to use for build cache.
* Example: "my-bucket"
*/
// "s3Bucket": "my-bucket",
/**
* (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache.
* This should not include any path; use the s3Prefix to set the path.
* Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000"
*/
// "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com",
/**
* (Required) The Amazon S3 region of the bucket to use for build cache.
* Example: "us-east-1"
*/
// "s3Region": "us-east-1",
/**
* An optional prefix ("folder") for cache items. It should not start with "/".
*/
// "s3Prefix": "my-prefix",
/**
* If set to true, allow writing to the cache. Defaults to false.
*/
// "isCacheWriteAllowed": true
},
/**
* Use this configuration with "cacheProvider"="http"
*/
"httpConfiguration": {
/**
* (Required) The URL of the server that stores the caches.
* Example: "https://build-cacches.example.com/"
*/
// "url": "https://build-cacches.example.com/",
/**
* (Optional) The HTTP method to use when writing to the cache (defaults to PUT).
* Should be one of PUT, POST, or PATCH.
* Example: "PUT"
*/
// "uploadMethod": "PUT",
/**
* (Optional) HTTP headers to pass to the cache server.
* Example: { "X-HTTP-Company-Id": "109283" }
*/
// "headers": {},
/**
* (Optional) Shell command that prints the authorization token needed to communicate with the
* cache server, and exits with exit code 0. This command will be executed from the root of
* the monorepo.
* Example: { "exec": "node", "args": ["common/scripts/auth.js"] }
*/
// "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] },
/**
* (Optional) Prefix for cache keys.
* Example: "my-company-"
*/
// "cacheKeyPrefix": "",
/**
* (Optional) If set to true, allow writing to the cache. Defaults to false.
*/
// "isCacheWriteAllowed": true
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* This configuration file manages Rush's cobuild feature.
* More documentation is available on the Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json",
/**
* (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature.
* RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string,
* otherwise the cobuild feature will be disabled.
*/
"cobuildFeatureEnabled": false,
/**
* (Required) Choose where cobuild lock will be acquired.
*
* The lock provider is registered by the rush plugins.
* For example, @rushstack/rush-redis-cobuild-plugin registers the "redis" lock provider.
*/
"cobuildLockProvider": "redis"
}
+524
View File
@@ -0,0 +1,524 @@
/**
* This configuration file defines custom commands for the "rush" command-line.
* More documentation is available on the Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json",
"phases": [
{
"name": "_phase:build",
"dependencies": {
"upstream": ["_phase:build"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": false
},
{
"name": "_phase:validate",
"dependencies": {
"self": ["_phase:build"],
"upstream": ["_phase:validate", "_phase:build"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": false
},
{
"name": "_phase:test",
"dependencies": {
"self": ["_phase:build"],
"upstream": ["_phase:validate"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": true
},
{
"name": "_phase:lint",
"dependencies": {
"self": ["_phase:build"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": false
},
{
"name": "_phase:bundle",
"dependencies": {
"self": ["_phase:build"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": false
},
{
"name": "_phase:format",
"dependencies": {
"self": ["_phase:build"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": false
},
{
"name": "_phase:svelte-check",
"dependencies": {
"self": ["_phase:build"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": true
},
{
"name": "_phase:package",
"dependencies": {
"self": ["_phase:build"],
"upstream": ["_phase:package"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": false
},
{
"name": "_phase:docker-build",
"dependencies": {
"self": ["_phase:build", "_phase:package", "_phase:bundle"],
"upstream": ["_phase:build", "_phase:bundle", "_phase:package"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": true
},
{
"name": "_phase:docker-staging",
"dependencies": {
"self": ["_phase:build", "_phase:package", "_phase:bundle"]
},
"ignoreMissingScript": true,
"allowWarningsOnSuccess": true
}
],
"commands": [
{
"commandKind": "bulk",
"name": "format",
"summary": "Format",
"description": "Perform a formatting",
"enableParallelism": true,
"incremental": false,
"ignoreMissingScript": true,
"safeForSimultaneousRushProcesses": true,
"disableBuildCache": true
},
{
"commandKind": "global",
"name": "doformat",
"summary": "Do a format and show errors after it",
"description": "Do a format and show errors after it",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "(rush format && true) && ./common/scripts/format-show.sh"
},
{
"commandKind": "phased",
"name": "build:watch",
"summary": "Build and watch",
"phases": ["_phase:build", "_phase:validate"],
"description": "Perform build with tsc and watch for changes with rush",
"enableParallelism": true,
"incremental": true,
"safeForSimultaneousRushProcesses": true,
"watchOptions": {
"alwaysWatch": true,
"watchPhases": ["_phase:build", "_phase:validate"]
}
},
{
"summary": "svelte-check",
"commandKind": "phased",
"name": "svelte-check",
"phases": ["_phase:build", "_phase:svelte-check"],
"enableParallelism": true,
"incremental": true
},
{
"commandKind": "phased",
"name": "build",
"summary": "build",
"phases": ["_phase:build"],
"enableParallelism": true,
"incremental": true,
"watchOptions": {
"alwaysWatch": false,
"watchPhases": ["_phase:build"]
}
},
{
"commandKind": "phased",
"name": "validate",
"phases": ["_phase:validate"],
"summary": "validate",
"enableParallelism": true,
"incremental": true
},
{
"commandKind": "phased",
"name": "rebuild",
"summary": "ReBuild and test all projects.",
"phases": ["_phase:build"],
"enableParallelism": true,
"incremental": false
},
{
"commandKind": "phased",
"name": "dorevalidate",
"phases": ["_phase:validate"],
"summary": "dorevalidate",
"enableParallelism": true,
"incremental": false
},
{
"commandKind": "phased",
"summary": "Do testing",
"name": "test",
"phases": ["_phase:build", "_phase:test"],
"enableParallelism": true,
"incremental": true
},
{
"commandKind": "phased",
"name": "retest",
"summary": "Build and test all projects.",
"phases": ["_phase:build", "_phase:test"],
"enableParallelism": true,
"incremental": false
},
{
"commandKind": "phased",
"summary": "Do bundle",
"name": "bundle",
"phases": ["_phase:build", "_phase:bundle"],
"enableParallelism": true,
"incremental": true
},
{
"commandKind": "phased",
"summary": "Do packaging",
"name": "package",
"phases": ["_phase:build", "_phase:package"],
"enableParallelism": true,
"incremental": true
},
{
"summary": "docker:build",
"commandKind": "phased",
"name": "docker:build",
"phases": [
"_phase:build",
"_phase:bundle",
"_phase:package",
"_phase:docker-build"
],
"enableParallelism": true,
"incremental": true
},
{
"summary": "docker:rebuild",
"commandKind": "phased",
"name": "docker:rebuild",
"phases": [
"_phase:build",
"_phase:bundle",
"_phase:package",
"_phase:docker-build"
],
"enableParallelism": true,
"incremental": false
},
{
"summary": "docker:staging",
"commandKind": "phased",
"name": "docker:staging",
"phases": [
"_phase:build",
"_phase:bundle",
"_phase:package",
"_phase:docker-staging"
],
"enableParallelism": true,
"incremental": true
},
{
"commandKind": "bulk",
"name": "docker:push",
"summary": "docker:push",
"description": "Push docker release images",
"enableParallelism": true,
"incremental": false,
"ignoreDependencyOrder": false,
"ignoreMissingScript": true,
"disableBuildCache": true,
"allowWarningsInSuccessfulBuild": true
},
{
"commandKind": "global",
"name": "docker",
"summary": "Build docker with platform",
"description": "use to build all docker containers required for platform",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "./common/scripts/docker.sh"
},
{
"commandKind": "global",
"name": "docker:up",
"summary": "Up development build",
"description": "Up development build",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev && docker compose up -d --force-recreate"
},
{
"commandKind": "global",
"name": "docker:local",
"summary": "Up development build",
"description": "Up development build",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev/local-mongo && docker compose -p dev up -d --force-recreate"
},
{
"commandKind": "global",
"name": "tool:upgrade",
"summary": "Upgrade all local models",
"description": "Upgrade all local models",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev/tool && rushx run-local upgrade -f"
},
{
"commandKind": "global",
"name": "apply-templates",
"summary": "Update all package.json according to templates matched from templates folder",
"description": "Use to update all projects to templates",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "node templates/apply.js"
},
{
"commandKind": "global",
"name": "ts-clean",
"summary": "Clean tsconfig.tsbuildinfo",
"description": "Clean typescript incremental cache",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "find .|grep tsconfig.tsbuildinfo | xargs rm | pwd"
},
{
"commandKind": "global",
"name": "revalidate",
"summary": "Clean Validate cache and to validate again",
"description": "Clean typescript incremental cache",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "find .|grep tsBuildInfoFile.info | xargs rm | pwd && rush dorevalidate"
},
{
"commandKind": "bulk",
"name": "remove-ts-types",
"summary": "Clean validate types",
"description": "Clean validate types",
"shellCommand": "rm -rf ./types",
"enableParallelism": true,
"incremental": false,
"ignoreDependencyOrder": true,
"ignoreMissingScript": true,
"disableBuildCache": true,
"allowWarningsInSuccessfulBuild": true
},
{
"commandKind": "global",
"name": "model-version",
"summary": "show model version",
"description": "show model version",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "npx node ./common/scripts/show_version.js"
},
{
"commandKind": "global",
"name": "show-model",
"summary": "show model",
"description": "show model",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./models/all && rushx show-model"
},
{
"commandKind": "global",
"name": "deps-clean",
"summary": "Clean package-deps-*.json files",
"description": "Clean package-deps-*.json files",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "find .|grep .rush/temp/package-deps_ | xargs rm"
},
{
"commandKind": "global",
"name": "fast-format",
"summary": "Format changed projects",
"description": "Format and autofix linting issues in changed projects",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "./common/scripts/fast-format.sh"
},
{
"commandKind": "global",
"name": "desktop",
"summary": "Build desktop build",
"description": "use to build all docker containers required for platform",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "rush package -p 20 --to desktop && cd ./desktop-package && rushx dist"
}
],
/**
* Custom "parameters" introduce new parameters for specified Rush command-line commands.
* For example, you might define a "--production" parameter for the "rush build" command.
*/
"parameters": [
{
"parameterKind": "flag",
"longName": "--lite",
"shortName": "-l",
"description": "Enable Heft lite building option, will skip some phases.",
"associatedCommands": ["build", "rebuild"]
},
{
"parameterKind": "flag",
"longName": "--clean",
"description": "Enable Heft clean building option",
"associatedCommands": ["build", "rebuild"]
},
{
"parameterKind": "flag",
"longName": "--force",
"shortName": "-f",
"description": "Force formatting",
"associatedCommands": ["format"]
},
{
"parameterKind": "string",
"argumentName": "BRANCH",
"required": false,
"associatedPhases": [],
"shortName": "-b",
"longName": "--branch",
"description": "Force formatting of branch",
"associatedCommands": ["fast-format"]
}
// {
// /**
// * (Required) Determines the type of custom parameter.
// * A "flag" is a custom command-line parameter whose presence acts as an on/off switch.
// */
// "parameterKind": "flag",
//
// /**
// * (Required) The long name of the parameter. It must be lower-case and use dash delimiters.
// */
// "longName": "--my-flag",
//
// /**
// * An optional alternative short name for the parameter. It must be a dash followed by a single
// * lower-case or upper-case letter, which is case-sensitive.
// *
// * NOTE: The Rush developers recommend that automation scripts should always use the long name
// * to improve readability. The short name is only intended as a convenience for humans.
// * The alphabet letters run out quickly, and are difficult to memorize, so *only* use
// * a short name if you expect the parameter to be needed very often in everyday operations.
// */
// "shortName": "-m",
//
// /**
// * (Required) A long description to be shown in the command-line help.
// *
// * Whenever you introduce commands/parameters, taking a little time to write meaningful
// * documentation can make a big difference for the developer experience in your repo.
// */
// "description": "A custom flag parameter that is passed to the scripts that are invoked when building projects",
//
// /**
// * (Required) A list of custom commands and/or built-in Rush commands that this parameter may
// * be used with. The parameter will be appended to the shell command that Rush invokes.
// */
// "associatedCommands": ["build", "rebuild"]
// },
//
// {
// /**
// * (Required) Determines the type of custom parameter.
// * A "string" is a custom command-line parameter whose value is a simple text string.
// */
// "parameterKind": "string",
// "longName": "--my-string",
// "description": "A custom string parameter for the \"my-global-command\" custom command",
//
// "associatedCommands": ["my-global-command"],
//
// /**
// * The name of the argument, which will be shown in the command-line help.
// *
// * For example, if the parameter name is '--count" and the argument name is "NUMBER",
// * then the command-line help would display "--count NUMBER". The argument name must
// * be comprised of upper-case letters, numbers, and underscores. It should be kept short.
// */
// "argumentName": "SOME_TEXT",
//
// /**
// * If true, this parameter must be included with the command. The default is false.
// */
// "required": false
// },
//
// {
// /**
// * (Required) Determines the type of custom parameter.
// * A "choice" is a custom command-line parameter whose argument must be chosen from a list of
// * allowable alternatives.
// */
// "parameterKind": "choice",
// "longName": "--my-choice",
// "description": "A custom choice parameter for the \"my-global-command\" custom command",
//
// "associatedCommands": ["my-global-command"],
//
// /**
// * If true, this parameter must be included with the command. The default is false.
// */
// "required": false,
//
// /**
// * Normally if a parameter is omitted from the command line, it will not be passed
// * to the shell command. this value will be inserted by default. Whereas if a "defaultValue"
// * is defined, the parameter will always be passed to the shell command, and will use the
// * default value if unspecified. The value must be one of the defined alternatives.
// */
// "defaultValue": "vanilla",
//
// /**
// * (Required) A list of alternative argument values that can be chosen for this parameter.
// */
// "alternatives": [
// {
// /**
// * A token that is one of the alternatives that can be used with the choice parameter,
// * e.g. "vanilla" in "--flavor vanilla".
// */
// "name": "vanilla",
//
// /**
// * A detailed description for the alternative that can be shown in the command-line help.
// *
// * Whenever you introduce commands/parameters, taking a little time to write meaningful
// * documentation can make a big difference for the developer experience in your repo.
// */
// "description": "Use the vanilla flavor (the default)"
// },
//
// {
// "name": "chocolate",
// "description": "Use the chocolate flavor"
// },
//
// {
// "name": "strawberry",
// "description": "Use the strawberry flavor"
// }
// ]
// }
]
}
+77
View File
@@ -0,0 +1,77 @@
/**
* This configuration file specifies NPM dependency version selections that affect all projects
* in a Rush repo. More documentation is available on the Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json",
/**
* A table that specifies a "preferred version" for a given NPM package. This feature is typically used
* to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies.
*
* The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into
* the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager
* will calculate versions. The specific effect depends on your package manager. Generally it will have no
* effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be
* achieved using the pnpmfile.js hook. See the Rush documentation for more details.
*
* After modifying this field, it's recommended to run "rush update --full" so that the package manager
* will recalculate all version selections.
*/
"preferredVersions": {
/**
* When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo,
* instead of the latest version.
*/
// "some-library": "1.2.3"
},
/**
* When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions,
* except in cases where different projects specify different version ranges for a given dependency. For older
* package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause
* trouble for indirect dependencies with incompatible peerDependencies ranges.
*
* The default value is true. If you're encountering installation errors related to peer dependencies,
* it's recommended to set this to false.
*
* After modifying this field, it's recommended to run "rush update --full" so that the package manager
* will recalculate all version selections.
*/
// "implicitlyPreferredVersions": false,
/**
* If you would like the version specifiers for your dependencies to be consistent, then
* uncomment this line. This is effectively similar to running "rush check" before any
* of the following commands:
*
* rush install, rush update, rush link, rush version, rush publish
*
* In some cases you may want this turned on, but need to allow certain packages to use a different
* version. In those cases, you will need to add an entry to the "allowedAlternativeVersions"
* section of the common-versions.json.
*
* In the case that subspaces is enabled, this setting will take effect at a subspace level.
*/
// "ensureConsistentVersions": true,
/**
* The "rush check" command can be used to enforce that every project in the repo must specify
* the same SemVer range for a given dependency. However, sometimes exceptions are needed.
* The allowedAlternativeVersions table allows you to list other SemVer ranges that will be
* accepted by "rush check" for a given dependency.
*
* IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE
* USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO).
* This design avoids unnecessary churn in this file.
*/
"allowedAlternativeVersions": {
/**
* For example, allow some projects to use an older TypeScript compiler
* (in addition to whatever "usual" version is being used by other projects in the repo):
*/
// "typescript": [
// "~2.4.0"
// ]
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* This configuration file allows repo maintainers to configure extra details to be
* printed alongside certain Rush messages. More documentation is available on the
* Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json",
/**
* Custom tips allow you to annotate Rush's console messages with advice tailored for
* your specific monorepo.
*/
"customTips": [
// {
// /**
// * (REQUIRED) An identifier indicating a message that may be printed by Rush.
// * If that message is printed, then this custom tip will be shown.
// * The list of available tip identifiers can be found on this page:
// * https://rushjs.io/pages/maintainer/custom_tips/
// */
// "tipId": "TIP_RUSH_INCONSISTENT_VERSIONS",
//
// /**
// * (REQUIRED) The message text to be displayed for this tip.
// */
// "message": "For additional troubleshooting information, refer this wiki article:\n\nhttps://intranet.contoso.com/docs/pnpm-mismatch"
// }
]
}
+121
View File
@@ -0,0 +1,121 @@
/**
* This configuration file allows repo maintainers to enable and disable experimental
* Rush features. More documentation is available on the Rush website: https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json",
/**
* By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'.
* Set this option to true to pass '--frozen-lockfile' instead for faster installs.
*/
// "usePnpmFrozenLockfileForRushInstall": true,
/**
* By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'.
* Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes.
*/
// "usePnpmPreferFrozenLockfileForRushUpdate": true,
/**
* By default, 'rush update' runs as a single operation.
* Set this option to true to instead update the lockfile with `--lockfile-only`, then perform a `--frozen-lockfile` install.
* Necessary when using the `afterAllResolved` hook in .pnpmfile.cjs.
*/
// "usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate": true,
/**
* If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies.
* Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not
* cause hash changes.
*/
// "omitImportersFromPreventManualShrinkwrapChanges": true,
/**
* If true, the chmod field in temporary project tar headers will not be normalized.
* This normalization can help ensure consistent tarball integrity across platforms.
*/
// "noChmodFieldInTarHeaderNormalization": true,
/**
* If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings.
* This will not replay warnings from the cached build.
*/
// "buildCacheWithAllowWarningsInSuccessfulBuild": true,
/**
* If true, build skipping will respect the allowWarningsInSuccessfulBuild flag and skip builds with warnings.
* This will not replay warnings from the skipped build.
*/
// "buildSkipWithAllowWarningsInSuccessfulBuild": true,
/**
* If true, perform a clean install after when running `rush install` or `rush update` if the
* `.npmrc` file has changed since the last install.
*/
// "cleanInstallAfterNpmrcChanges": true,
/**
* If true, print the outputs of shell commands defined in event hooks to the console.
*/
// "printEventHooksOutputToConsole": true,
/**
* If true, Rush will not allow node_modules in the repo folder or in parent folders.
*/
// "forbidPhantomResolvableNodeModulesFolders": true,
/**
* (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot
* correctly satisfy versioning requirements without installing duplicate copies of a package inside the
* node_modules folder. This poses a problem for "workspace:*" dependencies, as they are normally
* installed by making a symlink to the local project source folder. PNPM's "injected dependencies"
* feature provides a model for copying the local project folder into node_modules, however copying
* must occur AFTER the dependency project is built and BEFORE the consuming project starts to build.
* The "pnpm-sync" tool manages this operation; see its documentation for details.
* Enable this experiment if you want "rush" and "rushx" commands to resync injected dependencies
* by invoking "pnpm-sync" during the build.
*/
// "usePnpmSyncForInjectedDependencies": true,
/**
* If set to true, Rush will generate a `project-impact-graph.yaml` file in the repository root during `rush update`.
*/
// "generateProjectImpactGraphDuringRushUpdate": true,
/**
* If true, when running in watch mode, Rush will check for phase scripts named `_phase:<name>:ipc` and run them instead
* of `_phase:<name>` if they exist. The created child process will be provided with an IPC channel and expected to persist
* across invocations.
*/
// "useIPCScriptsInWatchMode": true,
/**
* (UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers
* working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands.
* This ensures that important notices will be seen by anyone doing active development, since people often
* ignore normal discussion group messages or don't know to subscribe.
*/
// "rushAlerts": true,
/**
* When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache.
*/
// "allowCobuildWithoutCache": true,
/**
* By default, rush perform a full scan of the entire repository. For example, Rush runs `git status` to check for local file changes.
* When this toggle is enabled, Rush will only scan specific paths, significantly speeding up Git operations.
*/
// "enableSubpathScan": true,
/**
* Rush has a policy that normally requires Rush projects to specify `workspace:*` in package.json when depending
* on other projects in the workspace, unless they are explicitly declared as `decoupledLocalDependencies`
* in rush.json. Enabling this experiment will remove that requirement for dependencies belonging to a different
* subspace. This is useful for large product groups who work in separate subspaces and generally prefer to consume
* each other's packages via the NPM registry.
*/
// "exemptDecoupledDependenciesBetweenSubspaces": false
}
+331
View File
@@ -0,0 +1,331 @@
/**
* This configuration file provides settings specific to the PNPM package manager.
* More documentation is available on the Rush website: https://rushjs.io
*
* Rush normally looks for this file in `common/config/rush/pnpm-config.json`. However,
* if `subspacesEnabled` is true in subspaces.json, then Rush will instead first look
* for `common/config/subspaces/<name>/pnpm-config.json`. (If the file exists in both places,
* then the file under `common/config/rush` is ignored.)
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json",
/**
* If true, then `rush install` and `rush update` will use the PNPM workspaces feature
* to perform the install, instead of the old model where Rush generated the symlinks
* for each projects's node_modules folder.
*
* When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing
* all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements
* Rush-specific features such as preferred versions. The user's `common/config/rush/.pnpmfile.cjs`
* is invoked by the shim.
*
* This option is strongly recommended. The default value is false.
*/
"useWorkspaces": true,
/**
* This setting determines how PNPM chooses version numbers during `rush update`.
* For example, suppose `lib-x@3.0.0` depends on `"lib-y": "^1.2.3"` whose latest major
* releases are `1.8.9` and `2.3.4`. The resolution mode `lowest-direct` might choose
* `lib-y@1.2.3`, wheres `highest` will choose 1.8.9, and `time-based` will pick the
* highest compatible version at the time when `lib-x@3.0.0` itself was published (ensuring
* that the version could have been tested by the maintainer of "lib-x"). For local workspace
* projects, `time-based` instead works like `lowest-direct`, avoiding upgrades unless
* they are explicitly requested. Although `time-based` is the most robust option, it may be
* slightly slower with registries such as npmjs.com that have not implemented an optimization.
*
* IMPORTANT: Be aware that PNPM 8.0.0 initially defaulted to `lowest-direct` instead of
* `highest`, but PNPM reverted this decision in 8.6.12 because it caused confusion for users.
* Rush version 5.106.0 and newer avoids this confusion by consistently defaulting to
* `highest` when `resolutionMode` is not explicitly set in pnpm-config.json or .npmrc,
* regardless of your PNPM version.
*
* PNPM documentation: https://pnpm.io/npmrc#resolution-mode
*
* Possible values are: `highest`, `time-based`, and `lowest-direct`.
* The default is `highest`.
*/
// "resolutionMode": "time-based",
/**
* This setting determines whether PNPM will automatically install (non-optional)
* missing peer dependencies instead of reporting an error. Doing so conveniently
* avoids the need to specify peer versions in package.json, but in a large monorepo
* this often creates worse problems. The reason is that peer dependency behavior
* is inherently complicated, and it is easier to troubleshoot consequences of an explicit
* version than an invisible heuristic. The original NPM RFC discussion pointed out
* some other problems with this feature: https://github.com/npm/rfcs/pull/43
* IMPORTANT: Without Rush, the setting defaults to true for PNPM 8 and newer; however,
* as of Rush version 5.109.0 the default is always false unless `autoInstallPeers`
* is specified in pnpm-config.json or .npmrc, regardless of your PNPM version.
* PNPM documentation: https://pnpm.io/npmrc#auto-install-peers
* The default value is false.
*/
// "autoInstallPeers": false,
/**
* If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when
* invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies,
* which is an invalid state that can cause build failures or incompatible dependency versions.
* (For historical reasons, JavaScript package managers generally do not treat this invalid
* state as an error.)
*
* PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies
*
* The default value is false to avoid legacy compatibility issues.
* It is strongly recommended to set `strictPeerDependencies=true`.
*/
// "strictPeerDependencies": true,
/**
* Environment variables that will be provided to PNPM.
*/
// "environmentVariables": {
// "NODE_OPTIONS": {
// "value": "--max-old-space-size=4096",
// "override": false
// }
// },
/**
* Specifies the location of the PNPM store. There are two possible values:
*
* - `local` - use the `pnpm-store` folder in the current configured temp folder:
* `common/temp/pnpm-store` by default.
* - `global` - use PNPM's global store, which has the benefit of being shared
* across multiple repo folders, but the disadvantage of less isolation for builds
* (for example, bugs or incompatibilities when two repos use different releases of PNPM)
*
* In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`.
*
* The default value is `local`.
*/
// "pnpmStore": "global",
/**
* If true, then `rush install` will report an error if manual modifications
* were made to the PNPM shrinkwrap file without running `rush update` afterwards.
*
* This feature protects against accidental inconsistencies that may be introduced
* if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this
* feature is enabled, `rush update` will append a hash to the file as a YAML comment,
* and then `rush update` and `rush install` will validate the hash. Note that this
* does not prohibit manual modifications, but merely requires `rush update` be run
* afterwards, ensuring that PNPM can report or repair any potential inconsistencies.
*
* To temporarily disable this validation when invoking `rush install`, use the
* `--bypass-policy` command-line parameter.
*
* The default value is false.
*/
// "preventManualShrinkwrapChanges": true,
/**
* When a project uses `workspace:` to depend on another Rush project, PNPM normally installs
* it by creating a symlink under `node_modules`. This generally works well, but in certain
* cases such as differing `peerDependencies` versions, symlinking may cause trouble
* such as incorrectly satisfied versions. For such cases, the dependency can be declared
* as "injected", causing PNPM to copy its built output into `node_modules` like a real
* install from a registry. Details here: https://rushjs.io/pages/advanced/injected_deps/
*
* When using Rush subspaces, these sorts of versioning problems are much more likely if
* `workspace:` refers to a project from a different subspace. This is because the symlink
* would point to a separate `node_modules` tree installed by a different PNPM lockfile.
* A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`,
* which automatically treats all projects from other subspaces as injected dependencies
* without having to manually configure them.
*
* NOTE: Use carefully -- excessive file copying can slow down the `rush install` and
* `pnpm-sync` operations if too many dependencies become injected.
*
* The default value is false.
*/
// "alwaysInjectDependenciesFromOtherSubspaces": false,
/**
* Defines the policies to be checked for the `pnpm-lock.yaml` file.
*/
"pnpmLockfilePolicies": {
/**
* This policy will cause "rush update" to report an error if `pnpm-lock.yaml` contains
* any SHA1 integrity hashes.
*
* For each NPM dependency, `pnpm-lock.yaml` normally stores an `integrity` hash. Although
* its main purpose is to detect corrupted or truncated network requests, this hash can also
* serve as a security fingerprint to protect against attacks that would substitute a
* malicious tarball, for example if a misconfigured .npmrc caused a machine to accidentally
* download a matching package name+version from npmjs.com instead of the private NPM registry.
* NPM originally used a SHA1 hash; this was insecure because an attacker can too easily craft
* a tarball with a matching fingerprint. For this reason, NPM later deprecated SHA1 and
* instead adopted a cryptographically strong SHA512 hash. Nonetheless, SHA1 hashes can
* occasionally reappear during "rush update", for example due to missing metadata fallbacks
* (https://github.com/orgs/pnpm/discussions/6194) or an incompletely migrated private registry.
* The `disallowInsecureSha1` policy prevents this, avoiding potential security/compliance alerts.
*/
// "disallowInsecureSha1": {
// /**
// * Enables the "disallowInsecureSha1" policy. The default value is false.
// */
// "enabled": true,
//
// /**
// * In rare cases, a private NPM registry may continue to serve SHA1 hashes for very old
// * package versions, perhaps due to a caching issue or database migration glitch. To avoid
// * having to disable the "disallowInsecureSha1" policy for the entire monorepo, the problematic
// * package versions can be individually ignored. The "exemptPackageVersions" key is the
// * package name, and the array value lists exact version numbers to be ignored.
// */
// "exemptPackageVersions": {
// "example1": ["1.0.0"],
// "example2": ["2.0.0", "2.0.1"]
// }
// }
},
/**
* The "globalOverrides" setting provides a simple mechanism for overriding version selections
* for all dependencies of all projects in the monorepo workspace. The settings are copied
* into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated
* by Rush during installation.
*
* Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by
* `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,
* and `globalOverrides` has lowest precedence.
*
* PNPM documentation: https://pnpm.io/package_json#pnpmoverrides
*/
"globalOverrides": {
// "example1": "^1.0.0",
// "example2": "npm:@company/example2@^1.0.0"
},
/**
* The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors
* that are reported during installation with `strictPeerDependencies=true`. The settings are copied
* into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated
* by Rush during installation.
*
* Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by
* `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,
* and `globalOverrides` has lowest precedence.
*
* https://pnpm.io/package_json#pnpmpeerdependencyrules
*/
"globalPeerDependencyRules": {
// "ignoreMissing": ["@eslint/*"],
// "allowedVersions": { "react": "17" },
// "allowAny": ["@babel/*"]
},
/**
* The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields
* for any PNPM dependency of the monorepo. The settings are copied into the `pnpm.packageExtensions`
* field of the `common/temp/package.json` file that is generated by Rush during installation.
* The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without
* the downsides of an executable script (nondeterminism, unreliable caching, performance concerns).
*
* Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by
* `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,
* and `globalOverrides` has lowest precedence.
*
* PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions
*/
"globalPackageExtensions": {
// "fork-ts-checker-webpack-plugin": {
// "dependencies": {
// "@babel/core": "1"
// },
// "peerDependencies": {
// "eslint": ">= 6"
// },
// "peerDependenciesMeta": {
// "eslint": {
// "optional": true
// }
// }
// }
},
/**
* The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall`
* lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices
* such as downloading large binaries without retries or attempting to invoke OS tools such as
* a C++ compiler. (PNPM's terminology refers to these lifecycle events as "building" a package;
* it has nothing to do with build system operations such as `rush build` or `rushx build`.)
* The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json`
* file that is generated by Rush during installation.
*
* PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies
*/
"globalNeverBuiltDependencies": [
// "fsevents"
],
/**
* The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM
* dependencies specified in the list. This is useful when certain optional dependencies are
* not needed in your environment, such as platform-specific packages or dependencies that
* fail during installation but are not critical to your project.
* These settings are copied into the `pnpm.overrides` field of the `common/temp/package.json`
* file that is generated by Rush during installation, instructing PNPM to ignore the specified
* optional dependencies.
*
* PNPM documentation: https://pnpm.io/package_json#pnpmignoredoptionaldependencies
*/
"globalIgnoredOptionalDependencies": [
// "fsevents"
],
/**
* The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package
* versions that the NPM registry reports as being deprecated. This is useful if the
* deprecated package is an indirect dependency of an external package that has not released a fix.
* The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json`
* file that is generated by Rush during installation.
*
* PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions
*
* If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions`
* in the package.json file for individual Rush projects.
*/
"globalAllowedDeprecatedVersions": {
// "request": "*"
},
/**
* (THIS FIELD IS MACHINE GENERATED) The "globalPatchedDependencies" field is updated automatically
* by the `rush-pnpm patch-commit` command. It is a dictionary, where the key is an NPM package name
* and exact version, and the value is a relative path to the associated patch file.
*
* PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies
*/
"globalPatchedDependencies": { },
/**
* (USE AT YOUR OWN RISK) This is a free-form property bag that will be copied into
* the `common/temp/package.json` file that is generated by Rush during installation.
* This provides a way to experiment with new PNPM features. These settings will override
* any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`.
*
* USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH
* TO MALFUNCTION. If you encounter a missing PNPM setting that you believe should
* be supported, please create a GitHub issue or PR. Note that Rush does not aim to
* support every possible PNPM setting, but rather to promote a battle-tested installation
* strategy that is known to provide a good experience for large teams with lots of projects.
*/
"unsupportedPackageJsonSettings": {
// "dependencies": {
// "not-a-good-practice": "*"
// },
// "scripts": {
// "do-something": "echo Also not a good practice"
// },
// "pnpm": { "futurePnpmFeature": true }
}
}
+5733
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
+29
View File
@@ -0,0 +1,29 @@
/**
* This configuration file manages Rush's plugin feature.
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json",
"plugins": [
/**
* Each item configures a plugin to be loaded by Rush.
*/
// {
// /**
// * The name of the NPM package that provides the plugin.
// */
// "packageName": "@scope/my-rush-plugin",
// /**
// * The name of the plugin. This can be found in the "pluginName"
// * field of the "rush-plugin-manifest.json" file in the NPM package folder.
// */
// "pluginName": "my-plugin-name",
// /**
// * The name of a Rush autoinstaller that will be used for installation, which
// * can be created using "rush init-autoinstaller". Add the plugin's NPM package
// * to the package.json "dependencies" of your autoinstaller, then run
// * "rush update-autoinstaller".
// */
// "autoinstallerName": "rush-plugins"
// }
]
}
+35
View File
@@ -0,0 +1,35 @@
/**
* This configuration file manages the experimental "subspaces" feature for Rush,
* which allows multiple PNPM lockfiles to be used in a single Rush workspace.
* For full documentation, please see https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json",
/**
* Set this flag to "true" to enable usage of subspaces.
*/
"subspacesEnabled": false,
/**
* (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype
* of this feature: https://github.com/microsoft/rushstack/pull/3481
* It allows subspaces with only one project to store their config files in the project folder.
*/
"splitWorkspaceCompatibility": false,
/**
* When a command such as "rush update" is invoked without the "--subspace" or "--to"
* parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces,
* this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this
* mistake by always requiring selection parameters for commands such as "rush update".
*/
"preventSelectingAllSubspaces": false,
/**
* The list of subspace names, which should be lowercase alphanumeric words separated by
* hyphens, for example "my-subspace". The corresponding config files will have paths
* such as "common/config/subspaces/my-subspace/package-lock.yaml".
*/
"subspaceNames": []
}
+102
View File
@@ -0,0 +1,102 @@
/**
* This is configuration file is used for advanced publishing configurations with Rush.
* More documentation is available on the Rush website: https://rushjs.io
*/
/**
* A list of version policy definitions. A "version policy" is a custom package versioning
* strategy that affects "rush change", "rush version", and "rush publish". The strategy applies
* to a set of projects that are specified using the "versionPolicyName" field in rush.json.
*/
[
// {
// /**
// * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion").
// *
// * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This
// * strategy is appropriate for a set of packages that act as selectable components of a
// * unified product. The entire set of packages are always published together, and always share
// * the same NPM version number. When the packages depend on other packages in the set, the
// * SemVer range is usually restricted to a single version.
// */
// "definitionName": "lockStepVersion",
//
// /**
// * (Required) The name that will be used for the "versionPolicyName" field in rush.json.
// * This name is also used command-line parameters such as "--version-policy"
// * and "--to-version-policy".
// */
// "policyName": "MyBigFramework",
//
// /**
// * (Required) The current version. All packages belonging to the set should have this version
// * in the current branch. When bumping versions, Rush uses this to determine the next version.
// * (The "version" field in package.json is NOT considered.)
// */
// "version": "1.0.0",
//
// /**
// * (Required) The type of bump that will be performed when publishing the next release.
// * When creating a release branch in Git, this field should be updated according to the
// * type of release.
// *
// * Valid values are: "prerelease", "preminor", "minor", "patch", "major"
// */
// "nextBump": "prerelease",
//
// /**
// * (Optional) If specified, all packages in the set share a common CHANGELOG.md file.
// * This file is stored with the specified "main" project, which must be a member of the set.
// *
// * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each
// * package in the set.
// */
// "mainProject": "my-app",
//
// /**
// * (Optional) If enabled, the "rush change" command will prompt the user for their email address
// * and include it in the JSON change files. If an organization maintains multiple repos, tracking
// * this contact information may be useful for a service that automatically upgrades packages and
// * needs to notify engineers whose change may be responsible for a downstream build break. It might
// * also be useful for crediting contributors. Rush itself does not do anything with the collected
// * email addresses. The default value is "false".
// */
// // "includeEmailInChangeFile": true
// },
//
// {
// /**
// * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion").
// *
// * The "individualVersion" mode specifies that the projects will use "individual versioning".
// * This is the typical NPM model where each package has an independent version number
// * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the
// * packages, they otherwise don't have any special relationship. The version bumping will
// * depend on how developers answer the "rush change" questions for each package that
// * is changed.
// */
// "definitionName": "individualVersion",
//
// "policyName": "MyRandomLibraries",
//
// /**
// * (Optional) This can be used to enforce that all packages in the set must share a common
// * major version number, e.g. because they are from the same major release branch.
// * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes
// * inappropriately. The minor/patch version parts will be bumped independently according
// * to the types of changes made to each project, according to the "rush change" command.
// */
// "lockedMajor": 3,
//
// /**
// * (Optional) When publishing is managed by Rush, by default the "rush change" command will
// * request changes for any projects that are modified by a pull request. These change entries
// * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates
// * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects
// * belonging to this version policy.
// */
// "exemptFromRushChange": false,
//
// // "includeEmailInChangeFile": true
// }
]
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
#
# This is an example Git hook for use with Rush. To enable this hook, rename this file
# to "commit-msg" and then run "rush install", which will copy it from common/git-hooks
# to the .git/hooks folder.
#
# TO LEARN MORE ABOUT GIT HOOKS
#
# The Git documentation is here: https://git-scm.com/docs/githooks
# Some helpful resources: https://githooks.com
#
# ABOUT THIS EXAMPLE
#
# The commit-msg hook is called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero status after issuing
# an appropriate message if it wants to stop the commit. The hook is allowed to edit
# the commit message file.
# This example enforces that commit message should contain a minimum amount of
# description text.
if [ `cat $1 | wc -w` -lt 3 ]; then
echo ""
echo "Invalid commit message: The message must contain at least 3 words."
exit 1
fi
+29
View File
@@ -0,0 +1,29 @@
rush docker:build -p 20 \
--to @hcengineering/pod-server \
--to @hcengineering/pod-front \
--to @hcengineering/prod \
--to @hcengineering/pod-account \
--to @hcengineering/pod-workspace \
--to @hcengineering/pod-collaborator \
--to @hcengineering/tool \
--to @hcengineering/pod-print \
--to @hcengineering/pod-sign \
--to @hcengineering/pod-analytics-collector \
--to @hcengineering/rekoni-service \
--to @hcengineering/pod-ai-bot \
--to @hcengineering/import-tool \
--to @hcengineering/pod-stats \
--to @hcengineering/pod-fulltext \
--to @hcengineering/pod-love \
--to @hcengineering/pod-mail \
--to @hcengineering/pod-datalake \
--to @hcengineering/pod-mail-worker \
--to @hcengineering/pod-export \
--to @hcengineering/pod-msg2file \
--to @hcengineering/pod-media \
--to @hcengineering/pod-preview \
--to @hcengineering/pod-external \
--to @hcengineering/pod-backup \
--to @hcengineering/backup-api-pod \
--to @hcengineering/pod-billing \
--to @hcengineering/pod-process
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
version=$(git rev-parse HEAD)
echo "Building version: $version"
docker build -t "$1" -t "$1:$version" ${DOCKER_EXTRA} .
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
version=$(git describe --tags --abbrev=0)
rev_version=$(git rev-parse HEAD)
if [ "x$2" = "xstaging" ]
then
a=( ${version//./ } )
c=( ${a[2]//[^0-9]*/ } )
((c++))
version="${a[0]}.${a[1]}.${c}-staging"
echo "Tagging staging $1 with version ${version}"
docker tag "$1:$rev_version" "$1:$version"
for n in {1..25}; do
docker push "$1:$version" && break
if (( $n < 25 ))
then
echo 'Docker failed to push, wait 5 second'
sleep 5
else
echo '25 push attempts failed, exiting with failure'
exit 1
fi
done
else
echo "Tagging release $1 with version ${version}"
docker tag "$1:$rev_version" "$1:$version"
# docker tag "$1:$rev_version" "$1:latest"
for n in {1..25}; do
docker push "$1:$version" && break
if (( $n < 25 ))
then
echo 'Docker failed to push, wait 5 second'
sleep 5
else
echo '25 push attempts failed, exiting with failure'
exit 1
fi
done
# for n in {1..25}; do
# docker push "$1:latest" && break
# if (( $n < 25 ))
# then
# echo 'Docker failed to push, wait 5 second'
# sleep 5
# else
# echo '25 push attempts failed, exiting with failure'
# exit 1
# fi
# done
fi
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
echo "Tagging release $1 with version $2"
docker tag "$1" "$1:$2"
for n in {1..25}; do
docker push "$1:$2" && break
echo 'Docker failed to push, wait 5 seconds'
sleep 5
done
+112
View File
@@ -0,0 +1,112 @@
const esbuild = require('esbuild')
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')
const SCRIPT_DIR = __dirname
const defaultConfig = {
entryPoint: 'src/index.ts',
outdir: 'bundle',
platform: 'node',
minify: false,
keepNames: false,
sourcemap: false,
logLevel: 'error',
external: ['snappy'],
define: {}
}
function getGitRevision() {
try {
return '"' + execSync('git describe --all --long').toString().trim() + '"'
} catch (error) {
console.warn('Failed to get git revision:', error.message)
return ''
}
}
function getVersionFromScript(scriptPath) {
try {
const absoluteScriptPath = path.resolve(SCRIPT_DIR, scriptPath)
return execSync(`node "${absoluteScriptPath}"`).toString().trim()
} catch (error) {
console.warn(`Failed to get version from ${scriptPath}:`, error.message)
return ''
}
}
async function bundle(config) {
// Ensure output directory exists
fs.mkdirSync(config.outdir, { recursive: true })
try {
await esbuild.build({
entryPoints: [config.entryPoint],
bundle: true,
platform: config.platform,
outfile: path.join(config.outdir, 'bundle.js'),
logLevel: config.logLevel,
minify: config.minify,
keepNames: config.keepNames,
sourcemap: config.sourcemap,
external: config.external,
define: config.define
})
console.log('Build completed successfully!')
} catch (error) {
console.error('Build failed:', error.message)
process.exit(1)
}
}
async function main() {
const args = process.argv.slice(2)
const config = { ...defaultConfig }
const define = {}
args.forEach((arg) => {
if (arg.startsWith('--')) {
const [key, value] = arg.slice(2).split('=')
switch (key) {
case 'entry':
config.entryPoint = value
break
case 'minify':
config.minify = value !== 'false'
break
case 'keep-names':
config.keepNames = value !== 'false'
break
case 'external':
config.external.push(value)
break
case 'sourcemap':
config.sourcemap = value !== 'false'
break
case 'define':
define[value] = true
break
}
}
})
const env = {
GIT_REVISION: define['GIT_REVISION'] ? getGitRevision() : undefined
}
Object.entries(env).forEach(([key, value]) => {
if (value) {
config.define[`process.env.${key}`] = value
}
})
await bundle(config)
}
main().catch((error) => {
console.error('Unexpected error:', error)
process.exit(1)
})
+31
View File
@@ -0,0 +1,31 @@
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where the Rush command may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the
// rush-pnpm command.
//
// An example usage would be:
//
// node common/scripts/install-run-rush-pnpm.js pnpm-command
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
//
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for details.
/******/ (() => { // webpackBootstrap
/******/ "use strict";
var __webpack_exports__ = {};
/*!*****************************************************!*\
!*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***!
\*****************************************************/
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
require('./install-run-rush');
//# sourceMappingURL=install-run-rush-pnpm.js.map
module.exports = __webpack_exports__;
/******/ })()
;
//# sourceMappingURL=install-run-rush-pnpm.js.map
+218
View File
@@ -0,0 +1,218 @@
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where the Rush command may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run-rush.js install
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
//
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for details.
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 16928:
/*!***********************!*\
!*** external "path" ***!
\***********************/
/***/ ((module) => {
module.exports = require("path");
/***/ }),
/***/ 179896:
/*!*********************!*\
!*** external "fs" ***!
\*********************/
/***/ ((module) => {
module.exports = require("fs");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
(() => {
/*!************************************************!*\
!*** ./lib-esnext/scripts/install-run-rush.js ***!
\************************************************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 16928);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896);
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
/* eslint-disable no-console */
const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run');
const PACKAGE_NAME = '@microsoft/rush';
const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION';
const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH';
function _getRushVersion(logger) {
const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION];
if (rushPreviewVersion !== undefined) {
logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`);
return rushPreviewVersion;
}
const rushJsonFolder = findRushJsonFolder();
const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME);
try {
const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8');
// Use a regular expression to parse out the rushVersion value because rush.json supports comments,
// but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script.
const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/);
return rushJsonMatches[1];
}
catch (e) {
throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` +
`The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` +
'using an unexpected syntax.');
}
}
function _getBin(scriptName) {
switch (scriptName.toLowerCase()) {
case 'install-run-rush-pnpm.js':
return 'rush-pnpm';
case 'install-run-rushx.js':
return 'rushx';
default:
return 'rush';
}
}
function _run() {
const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv;
// Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the
// appropriate binary inside the rush package to run
const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath);
const bin = _getBin(scriptName);
if (!nodePath || !scriptPath) {
throw new Error('Unexpected exception: could not detect node path or script path');
}
let commandFound = false;
let logger = { info: console.log, error: console.error };
for (const arg of packageBinArgs) {
if (arg === '-q' || arg === '--quiet') {
// The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress
// any normal informational/diagnostic information printed during startup.
//
// To maintain the same user experience, the install-run* scripts pass along this
// flag but also use it to suppress any diagnostic information normally printed
// to stdout.
logger = {
info: () => { },
error: console.error
};
}
else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') {
// We either found something that looks like a command (i.e. - doesn't start with a "-"),
// or we found the -h/--help flag, which can be run without a command
commandFound = true;
}
}
if (!commandFound) {
console.log(`Usage: ${scriptName} <command> [args...]`);
if (scriptName === 'install-run-rush-pnpm.js') {
console.log(`Example: ${scriptName} pnpm-command`);
}
else if (scriptName === 'install-run-rush.js') {
console.log(`Example: ${scriptName} build --to myproject`);
}
else {
console.log(`Example: ${scriptName} custom-command`);
}
process.exit(1);
}
runWithErrorAndStatusCode(logger, () => {
const version = _getRushVersion(logger);
logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`);
const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE];
if (lockFilePath) {
logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`);
}
return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath);
});
}
_run();
//# sourceMappingURL=install-run-rush.js.map
})();
module.exports = __webpack_exports__;
/******/ })()
;
//# sourceMappingURL=install-run-rush.js.map
+31
View File
@@ -0,0 +1,31 @@
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where the Rush command may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the
// rushx command.
//
// An example usage would be:
//
// node common/scripts/install-run-rushx.js custom-command
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
//
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for details.
/******/ (() => { // webpackBootstrap
/******/ "use strict";
var __webpack_exports__ = {};
/*!*************************************************!*\
!*** ./lib-esnext/scripts/install-run-rushx.js ***!
\*************************************************/
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
require('./install-run-rush');
//# sourceMappingURL=install-run-rushx.js.map
module.exports = __webpack_exports__;
/******/ })()
;
//# sourceMappingURL=install-run-rushx.js.map
+778
View File
@@ -0,0 +1,778 @@
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where a Node tool may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the specified
// version of the specified tool (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
//
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for details.
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 16928:
/*!***********************!*\
!*** external "path" ***!
\***********************/
/***/ ((module) => {
module.exports = require("path");
/***/ }),
/***/ 179896:
/*!*********************!*\
!*** external "fs" ***!
\*********************/
/***/ ((module) => {
module.exports = require("fs");
/***/ }),
/***/ 370857:
/*!*********************!*\
!*** external "os" ***!
\*********************/
/***/ ((module) => {
module.exports = require("os");
/***/ }),
/***/ 535317:
/*!********************************!*\
!*** external "child_process" ***!
\********************************/
/***/ ((module) => {
module.exports = require("child_process");
/***/ }),
/***/ 832286:
/*!************************************************!*\
!*** ./lib-esnext/utilities/npmrcUtilities.js ***!
\************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ isVariableSetInNpmrcFile: () => (/* binding */ isVariableSetInNpmrcFile),
/* harmony export */ syncNpmrc: () => (/* binding */ syncNpmrc),
/* harmony export */ trimNpmrcFileLines: () => (/* binding */ trimNpmrcFileLines)
/* harmony export */ });
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 179896);
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 16928);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__);
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
// IMPORTANT - do not use any non-built-in libraries in this file
/**
* This function reads the content for given .npmrc file path, and also trims
* unusable lines from the .npmrc file.
*
* @returns
* The text of the the .npmrc.
*/
// create a global _combinedNpmrc for cache purpose
const _combinedNpmrcMap = new Map();
function _trimNpmrcFile(options) {
const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options;
const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath);
if (combinedNpmrcFromCache !== undefined) {
return combinedNpmrcFromCache;
}
let npmrcFileLines = [];
if (linesToPrepend) {
npmrcFileLines.push(...linesToPrepend);
}
if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) {
npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n'));
}
if (linesToAppend) {
npmrcFileLines.push(...linesToAppend);
}
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
const resultLines = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax);
const combinedNpmrc = resultLines.join('\n');
//save the cache
_combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc);
return combinedNpmrc;
}
/**
*
* @param npmrcFileLines The npmrc file's lines
* @param env The environment variables object
* @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}`
* @returns
*/
function trimNpmrcFileLines(npmrcFileLines, env, supportEnvVarFallbackSyntax) {
var _a;
const resultLines = [];
// This finds environment variable tokens that look like "${VAR_NAME}"
const expansionRegExp = /\$\{([^\}]+)\}/g;
// Comment lines start with "#" or ";"
const commentRegExp = /^\s*[#;]/;
// Trim out lines that reference environment variables that aren't defined
for (let line of npmrcFileLines) {
let lineShouldBeTrimmed = false;
//remove spaces before or after key and value
line = line
.split('=')
.map((lineToTrim) => lineToTrim.trim())
.join('=');
// Ignore comment lines
if (!commentRegExp.test(line)) {
const environmentVariables = line.match(expansionRegExp);
if (environmentVariables) {
for (const token of environmentVariables) {
/**
* Remove the leading "${" and the trailing "}" from the token
*
* ${nameString} -> nameString
* ${nameString-fallbackString} -> name-fallbackString
* ${nameString:-fallbackString} -> name:-fallbackString
*/
const nameWithFallback = token.substring(2, token.length - 1);
let environmentVariableName;
let fallback;
if (supportEnvVarFallbackSyntax) {
/**
* Get the environment variable name and fallback value.
*
* name fallback
* nameString -> nameString undefined
* nameString-fallbackString -> nameString fallbackString
* nameString:-fallbackString -> nameString fallbackString
*/
const matched = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/);
// matched: [originStr, variableName, fallback]
environmentVariableName = (_a = matched === null || matched === void 0 ? void 0 : matched[1]) !== null && _a !== void 0 ? _a : nameWithFallback;
fallback = matched === null || matched === void 0 ? void 0 : matched[2];
}
else {
environmentVariableName = nameWithFallback;
}
// Is the environment variable and fallback value defined.
if (!env[environmentVariableName] && !fallback) {
// No, so trim this line
lineShouldBeTrimmed = true;
break;
}
}
}
}
if (lineShouldBeTrimmed) {
// Example output:
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
}
else {
resultLines.push(line);
}
}
return resultLines;
}
function _copyAndTrimNpmrcFile(options) {
const { logger, sourceNpmrcPath, targetNpmrcPath } = options;
logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose
logger.info(` --> "${targetNpmrcPath}"`);
const combinedNpmrc = _trimNpmrcFile(options);
fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc);
return combinedNpmrc;
}
function syncNpmrc(options) {
const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = {
// eslint-disable-next-line no-console
info: console.log,
// eslint-disable-next-line no-console
error: console.error
}, createIfMissing = false } = options;
const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc');
try {
if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) {
// Ensure the target folder exists
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) {
fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true });
}
return _copyAndTrimNpmrcFile({
sourceNpmrcPath,
targetNpmrcPath,
logger,
...options
});
}
else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) {
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
logger.info(`Deleting ${targetNpmrcPath}`); // Verbose
fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath);
}
}
catch (e) {
throw new Error(`Error syncing .npmrc file: ${e}`);
}
}
function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey, supportEnvVarFallbackSyntax) {
const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`;
//if .npmrc file does not exist, return false directly
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) {
return false;
}
const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax });
const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm');
return trimmedNpmrcFile.match(variableKeyRegExp) !== null;
}
//# sourceMappingURL=npmrcUtilities.js.map
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
(() => {
/*!*******************************************!*\
!*** ./lib-esnext/scripts/install-run.js ***!
\*******************************************/
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ RUSH_JSON_FILENAME: () => (/* binding */ RUSH_JSON_FILENAME),
/* harmony export */ findRushJsonFolder: () => (/* binding */ findRushJsonFolder),
/* harmony export */ getNpmPath: () => (/* binding */ getNpmPath),
/* harmony export */ installAndRun: () => (/* binding */ installAndRun),
/* harmony export */ runWithErrorAndStatusCode: () => (/* binding */ runWithErrorAndStatusCode)
/* harmony export */ });
/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 535317);
/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896);
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 370857);
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 16928);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 832286);
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
/* eslint-disable no-console */
const RUSH_JSON_FILENAME = 'rush.json';
const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';
const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH';
const INSTALLED_FLAG_FILENAME = 'installed.flag';
const NODE_MODULES_FOLDER_NAME = 'node_modules';
const PACKAGE_JSON_FILENAME = 'package.json';
/**
* Parse a package specifier (in the form of name\@version) into name and version parts.
*/
function _parsePackageSpecifier(rawPackageSpecifier) {
rawPackageSpecifier = (rawPackageSpecifier || '').trim();
const separatorIndex = rawPackageSpecifier.lastIndexOf('@');
let name;
let version = undefined;
if (separatorIndex === 0) {
// The specifier starts with a scope and doesn't have a version specified
name = rawPackageSpecifier;
}
else if (separatorIndex === -1) {
// The specifier doesn't have a version
name = rawPackageSpecifier;
}
else {
name = rawPackageSpecifier.substring(0, separatorIndex);
version = rawPackageSpecifier.substring(separatorIndex + 1);
}
if (!name) {
throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);
}
return { name, version };
}
let _npmPath = undefined;
/**
* Get the absolute path to the npm executable
*/
function getNpmPath() {
if (!_npmPath) {
try {
if (_isWindows()) {
// We're on Windows
const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString();
const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line);
// take the last result, we are looking for a .cmd command
// see https://github.com/microsoft/rushstack/issues/759
_npmPath = lines[lines.length - 1];
}
else {
// We aren't on Windows - assume we're on *NIX or Darwin
_npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString();
}
}
catch (e) {
throw new Error(`Unable to determine the path to the NPM tool: ${e}`);
}
_npmPath = _npmPath.trim();
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) {
throw new Error('The NPM executable does not exist');
}
}
return _npmPath;
}
function _ensureFolder(folderPath) {
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) {
const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath);
_ensureFolder(parentDir);
fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath);
}
}
/**
* Create missing directories under the specified base directory, and return the resolved directory.
*
* Does not support "." or ".." path segments.
* Assumes the baseFolder exists.
*/
function _ensureAndJoinPath(baseFolder, ...pathSegments) {
let joinedPath = baseFolder;
try {
for (let pathSegment of pathSegments) {
pathSegment = pathSegment.replace(/[\\\/]/g, '+');
joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment);
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) {
fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath);
}
}
}
catch (e) {
throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`);
}
return joinedPath;
}
function _getRushTempFolder(rushCommonFolder) {
const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME];
if (rushTempFolder !== undefined) {
_ensureFolder(rushTempFolder);
return rushTempFolder;
}
else {
return _ensureAndJoinPath(rushCommonFolder, 'temp');
}
}
/**
* Compare version strings according to semantic versioning.
* Returns a positive integer if "a" is a later version than "b",
* a negative integer if "b" is later than "a",
* and 0 otherwise.
*/
function _compareVersionStrings(a, b) {
const aParts = a.split(/[.-]/);
const bParts = b.split(/[.-]/);
const numberOfParts = Math.max(aParts.length, bParts.length);
for (let i = 0; i < numberOfParts; i++) {
if (aParts[i] !== bParts[i]) {
return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0);
}
}
return 0;
}
/**
* Resolve a package specifier to a static version
*/
function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) {
if (!version) {
version = '*'; // If no version is specified, use the latest version
}
if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) {
// If the version contains only characters that we recognize to be used in static version specifiers,
// pass the version through
return version;
}
else {
// version resolves to
try {
const rushTempFolder = _getRushTempFolder(rushCommonFolder);
const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');
(0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({
sourceNpmrcFolder,
targetNpmrcFolder: rushTempFolder,
logger,
supportEnvVarFallbackSyntax: false
});
const npmPath = getNpmPath();
// This returns something that looks like:
// ```
// [
// "3.0.0",
// "3.0.1",
// ...
// "3.0.20"
// ]
// ```
//
// if multiple versions match the selector, or
//
// ```
// "3.0.0"
// ```
//
// if only a single version matches.
const spawnSyncOptions = {
cwd: rushTempFolder,
stdio: [],
shell: _isWindows()
};
const platformNpmPath = _getPlatformPath(npmPath);
const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions);
if (npmVersionSpawnResult.status !== 0) {
throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
}
const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();
const parsedVersionOutput = JSON.parse(npmViewVersionOutput);
const versions = Array.isArray(parsedVersionOutput)
? parsedVersionOutput
: [parsedVersionOutput];
let latestVersion = versions[0];
for (let i = 1; i < versions.length; i++) {
const latestVersionCandidate = versions[i];
if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) {
latestVersion = latestVersionCandidate;
}
}
if (!latestVersion) {
throw new Error('No versions found for the specified version range.');
}
return latestVersion;
}
catch (e) {
throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);
}
}
}
let _rushJsonFolder;
/**
* Find the absolute path to the folder containing rush.json
*/
function findRushJsonFolder() {
if (!_rushJsonFolder) {
let basePath = __dirname;
let tempPath = __dirname;
do {
const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME);
if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) {
_rushJsonFolder = basePath;
break;
}
else {
basePath = tempPath;
}
} while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root
if (!_rushJsonFolder) {
throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`);
}
}
return _rushJsonFolder;
}
/**
* Detects if the package in the specified directory is installed
*/
function _isPackageAlreadyInstalled(packageInstallFolder) {
try {
const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) {
return false;
}
const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString();
return fileContents.trim() === process.version;
}
catch (e) {
return false;
}
}
/**
* Delete a file. Fail silently if it does not exist.
*/
function _deleteFile(file) {
try {
fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file);
}
catch (err) {
if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
throw err;
}
}
}
/**
* Removes the following files and directories under the specified folder path:
* - installed.flag
* -
* - node_modules
*/
function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) {
try {
const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
_deleteFile(flagFile);
const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json');
if (lockFilePath) {
fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile);
}
else {
// Not running `npm ci`, so need to cleanup
_deleteFile(packageLockFile);
const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) {
const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler');
fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`));
}
}
}
catch (e) {
throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
}
}
function _createPackageJson(packageInstallFolder, name, version) {
try {
const packageJsonContents = {
name: 'ci-rush',
version: '0.0.0',
dependencies: {
[name]: version
},
description: "DON'T WARN",
repository: "DON'T WARN",
license: 'MIT'
};
const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME);
fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));
}
catch (e) {
throw new Error(`Unable to create package.json: ${e}`);
}
}
/**
* Run "npm install" in the package install folder.
*/
function _installPackage(logger, packageInstallFolder, name, version, command) {
try {
logger.info(`Installing ${name}...`);
const npmPath = getNpmPath();
const platformNpmPath = _getPlatformPath(npmPath);
const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], {
stdio: 'inherit',
cwd: packageInstallFolder,
env: process.env,
shell: _isWindows()
});
if (result.status !== 0) {
throw new Error(`"npm ${command}" encountered an error`);
}
logger.info(`Successfully installed ${name}@${version}`);
}
catch (e) {
throw new Error(`Unable to install package: ${e}`);
}
}
/**
* Get the ".bin" path for the package.
*/
function _getBinPath(packageInstallFolder, binName) {
const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName;
return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName);
}
/**
* Returns a cross-platform path - windows must enclose any path containing spaces within double quotes.
*/
function _getPlatformPath(platformPath) {
return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath;
}
function _isWindows() {
return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32';
}
/**
* Write a flag file to the package's install directory, signifying that the install was successful.
*/
function _writeFlagFile(packageInstallFolder) {
try {
const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version);
}
catch (e) {
throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
}
}
function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common');
const rushTempFolder = _getRushTempFolder(rushCommonFolder);
const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`);
if (!_isPackageAlreadyInstalled(packageInstallFolder)) {
// The package isn't already installed
_cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath);
const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');
(0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({
sourceNpmrcFolder,
targetNpmrcFolder: packageInstallFolder,
logger,
supportEnvVarFallbackSyntax: false
});
_createPackageJson(packageInstallFolder, packageName, packageVersion);
const command = lockFilePath ? 'ci' : 'install';
_installPackage(logger, packageInstallFolder, packageName, packageVersion, command);
_writeFlagFile(packageInstallFolder);
}
const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;
const statusMessageLine = new Array(statusMessage.length + 1).join('-');
logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n');
const binPath = _getBinPath(packageInstallFolder, packageBinName);
const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
// Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to
// assign via the process.env proxy to ensure that we append to the right PATH key.
const originalEnvPath = process.env.PATH || '';
let result;
try {
// `npm` bin stubs on Windows are `.cmd` files
// Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true`
const platformBinPath = _getPlatformPath(binPath);
process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter);
result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, {
stdio: 'inherit',
windowsVerbatimArguments: false,
shell: _isWindows(),
cwd: process.cwd(),
env: process.env
});
}
finally {
process.env.PATH = originalEnvPath;
}
if (result.status !== null) {
return result.status;
}
else {
throw result.error || new Error('An unknown error occurred.');
}
}
function runWithErrorAndStatusCode(logger, fn) {
process.exitCode = 1;
try {
const exitCode = fn();
process.exitCode = exitCode;
}
catch (e) {
logger.error('\n\n' + e.toString() + '\n\n');
}
}
function _run() {
const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;
if (!nodePath) {
throw new Error('Unexpected exception: could not detect node path');
}
if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') {
// If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
// to the script that (presumably) imported this file
return;
}
if (process.argv.length < 4) {
console.log('Usage: install-run.js <package>@<version> <command> [args...]');
console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');
process.exit(1);
}
const logger = { info: console.log, error: console.error };
runWithErrorAndStatusCode(logger, () => {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common');
const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier);
const name = packageSpecifier.name;
const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier);
if (packageSpecifier.version !== version) {
console.log(`Resolved to ${name}@${version}`);
}
return installAndRun(logger, name, version, packageBinName, packageBinArgs);
});
}
_run();
//# sourceMappingURL=install-run.js.map
})();
module.exports = __webpack_exports__;
/******/ })()
;
//# sourceMappingURL=install-run.js.map
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@hcengineering/scripts",
"version": "0.1.0",
"scripts": {
"format": "echo \"No format specified\""
},
"devDependencies": {
"esbuild": "^0.24.2"
},
"private": true
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "node"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@hcengineering/network-backrpc",
"version": "0.7.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.7.4",
"@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/network-core": "workspace:^0.7.0",
"zeromq": "^6.5.0",
"uuid": "^8.3.2"
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,489 @@
import { TickManagerImpl } from '@hcengineering/network-core'
import { BackRPCServer } from '../server'
import { BackRPCClient } from '../client'
import type { ClientId } from '../types'
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()
await 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<void> = new Promise<void>((resolve) => {
doResolve = () => {
resolve()
}
})
let doResolve2: () => void
const p2: Promise<void> = new Promise<void>((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)
await 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()
await server.close()
})
it('test error handling', async () => {
const tickMgr = new TickManagerImpl(10)
const server = new BackRPCServer(
{
requestHandler: async (client, method, params, send) => {
switch (method) {
case 'error':
throw new Error('Test error')
case 'success':
await send('OK')
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) => {
await send('client response')
},
onRegister: async () => {
console.log('Client registered')
}
},
'localhost',
await server.getPort(),
tickMgr
)
// Test successful request
const response = await client.request('success', 'test')
expect(response).toBe('OK')
// Test error handling
try {
await client.request('error', 'test')
fail('Should have thrown an error')
} catch (error: any) {
expect(error.message).toBe('Test error')
}
client.close()
await server.close()
})
it('test server to client request', async () => {
const tickMgr = new TickManagerImpl(10)
let clientRequestReceived = false
const server = new BackRPCServer(
{
requestHandler: async (client, method, params, send) => {
if (method === 'trigger-client-request') {
const clientResponse = await server.request(client, 'client-method', 'server-data')
await send(clientResponse)
} else {
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 === 'client-method') {
clientRequestReceived = true
expect(param).toBe('server-data')
await send('client-processed-' + param)
} else {
await send('unknown-client-method')
}
},
onRegister: async () => {
console.log('Client registered')
}
},
'localhost',
await server.getPort(),
tickMgr
)
const response = await client.request('trigger-client-request', 'test')
expect(response).toBe('client-processed-server-data')
expect(clientRequestReceived).toBe(true)
client.close()
await server.close()
})
it('test events', async () => {
const tickMgr = new TickManagerImpl(10)
const eventsReceived: any[] = []
const server = new BackRPCServer(
{
requestHandler: async (client, method, params, send) => {
if (method === 'send-event') {
await server.send(client, { type: 'test-event', data: params })
await send('event-sent')
} else {
await send('unknown')
}
},
helloHandler: async (clientId) => {
console.log(`Client ${clientId} connected`)
}
},
tickMgr
)
const client = new BackRPCClient(
'client1' as ClientId,
{
requestHandler: async (method, param, send) => {
await send('client response')
},
onRegister: async () => {
console.log('Client registered')
},
onEvent: async (event) => {
eventsReceived.push(event)
}
},
'localhost',
await server.getPort(),
tickMgr
)
const response = await client.request('send-event', 'test-data')
expect(response).toBe('event-sent')
// Wait a bit for the event to be processed
await new Promise(resolve => setTimeout(resolve, 100))
expect(eventsReceived).toHaveLength(1)
expect(eventsReceived[0]).toEqual({ type: 'test-event', data: 'test-data' })
client.close()
await server.close()
})
it('test multiple clients', async () => {
const tickMgr = new TickManagerImpl(10)
const connectedClients = new Set<string>()
const server = new BackRPCServer(
{
requestHandler: async (client, method, params, send) => {
switch (method) {
case 'get-client-id':
await send(client)
break
case 'get-connected-count':
await send(connectedClients.size)
break
default:
await send('unknown')
}
},
helloHandler: async (clientId) => {
connectedClients.add(clientId)
console.log(`Client ${clientId} connected`)
}
},
tickMgr
)
const client1 = new BackRPCClient(
'client1' as ClientId,
{
requestHandler: async (method, param, send) => {
await send('client1 response')
},
onRegister: async () => {
console.log('Client1 registered')
}
},
'localhost',
await server.getPort(),
tickMgr
)
const client2 = new BackRPCClient(
'client2' as ClientId,
{
requestHandler: async (method, param, send) => {
await send('client2 response')
},
onRegister: async () => {
console.log('Client2 registered')
}
},
'localhost',
await server.getPort(),
tickMgr
)
// Wait for both clients to connect
await new Promise(resolve => setTimeout(resolve, 100))
const client1Id = await client1.request('get-client-id', '')
const client2Id = await client2.request('get-client-id', '')
const connectedCount = await client1.request('get-connected-count', '')
expect(client1Id).toBe('client1')
expect(client2Id).toBe('client2')
expect(connectedCount).toBe(2)
client1.close()
client2.close()
await server.close()
})
it('test client not found error', async () => {
const tickMgr = new TickManagerImpl(10)
const server = new BackRPCServer(
{
requestHandler: async (client, method, params, send) => {
await send('OK')
},
helloHandler: async (clientId) => {
console.log(`Client ${clientId} connected`)
}
},
tickMgr
)
// Try to send request to non-existent client
try {
await server.request('non-existent-client' as ClientId, 'test', 'data')
fail('Should have thrown an error')
} catch (error: any) {
expect(error.message).toBe('Client non-existent-client not found')
}
// Try to send event to non-existent client
try {
await server.send('non-existent-client' as ClientId, { test: 'data' })
fail('Should have thrown an error')
} catch (error: any) {
expect(error.message).toBe('Client non-existent-client not found')
}
await server.close()
})
it('test concurrent requests', async () => {
const tickMgr = new TickManagerImpl(10)
const server = new BackRPCServer(
{
requestHandler: async (client, method, params, send) => {
// Simulate some async work
await new Promise(resolve => setTimeout(resolve, 50))
await send(`processed-${params}`)
},
helloHandler: async (clientId) => {
console.log(`Client ${clientId} connected`)
}
},
tickMgr
)
const client = new BackRPCClient(
'client1' as ClientId,
{
requestHandler: async (method, param, send) => {
await send('client response')
},
onRegister: async () => {
console.log('Client registered')
}
},
'localhost',
await server.getPort(),
tickMgr
)
// Send multiple concurrent requests
const promises = []
for (let i = 0; i < 5; i++) {
promises.push(client.request('test', `request-${i}`))
}
const responses = await Promise.all(promises)
expect(responses).toHaveLength(5)
for (let i = 0; i < 5; i++) {
expect(responses[i]).toBe(`processed-request-${i}`)
}
client.close()
await server.close()
})
it('test timeout handling', async () => {
const tickMgr = new TickManagerImpl(10)
let timeoutHandlerCalled = false
const server = new BackRPCServer(
{
requestHandler: async (client, method, params, send) => {
if (method === 'slow-request') {
// Don't send response to simulate timeout
return
}
await send('OK')
},
helloHandler: async (clientId) => {
console.log(`Client ${clientId} connected`)
},
handleTimeout: async (clientId) => {
timeoutHandlerCalled = true
console.log(`Timeout for client ${clientId}`)
}
},
tickMgr
)
const client = new BackRPCClient(
'client1' as ClientId,
{
requestHandler: async (method, param, send) => {
await send('client response')
},
onRegister: async () => {
console.log('Client registered')
}
},
'localhost',
await server.getPort(),
tickMgr
)
// This should work normally
const response1 = await client.request('normal', 'test')
expect(response1).toBe('OK')
// The timeout handler exists but may not be easily testable without manipulating internal state
// So we just verify the server accepts the timeout handler
expect(timeoutHandlerCalled).toBe(false) // Not called yet
client.close()
await server.close()
})
})
+237
View File
@@ -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<void>((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()
})
})
+211
View File
@@ -0,0 +1,211 @@
//
// 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-core'
import * as zmq from 'zeromq'
import { backrpcOperations, type ClientId } from './types'
export type BackRPCResponseSend = (response: any) => Promise<void>
export interface BackRPCClientHandler {
requestHandler: (method: string, params: any, send: BackRPCResponseSend) => Promise<void>
onRegister?: () => Promise<void>
onEvent?: (event: any) => Promise<void>
}
interface RequestInfo {
resolve: (data: any) => void
reject: (err: any) => void
method: string
params: any
}
export class BackRPCClient<ClientT extends string = ClientId> {
serverId: string | Promise<string>
dealer: zmq.Dealer
requestCounter: number = 0
requests = new Map<string, RequestInfo>()
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<zmq.Dealer>
) {
this.dealer = new zmq.Dealer(options)
this.dealer.connect(`tcp://${host}:${port}`)
this.setServerId = () => {}
this.serverId = new Promise<string>((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<void> {
await this.dealer.send([backrpcOperations.ping, this.clientId as string, '', ''])
}
private async sendHello (): Promise<void> {
await this.dealer.send([backrpcOperations.hello, this.clientId as string, '', ''])
}
private async start (): Promise<void> {
// 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)
})
// We need to resend all pending requests.
void this.resendRequests()
}
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
}
case backrpcOperations.retry: {
const req = this.requests.get(reqId)
if (req !== undefined) {
const count = JSON.parse(payload.toString())
void this.tickMgr.waitTick(count).then(() => {
void this.dealer
.send([backrpcOperations.request, reqId, JSON.stringify([req.method, req.params])])
.catch((err) => {
console.error('Failed to resend request', err)
})
})
}
}
}
} catch (err: any) {
console.error(err)
}
}
}
private async resendRequests (): Promise<void> {
for (const [reqId, req] of Array.from(this.requests.entries())) {
try {
await this.dealer.send([backrpcOperations.request, reqId, JSON.stringify([req.method, req.params])])
} catch (err: any) {
console.error('Failed to resend request', err)
}
}
}
async request<T>(method: string, params: any): Promise<T> {
if (this.serverId instanceof Promise) {
await this.serverId
}
return await new Promise<T>((resolve, reject) => {
const reqId = this.clientId + '-' + this.requestCounter++
this.requests.set(reqId, { resolve, reject, method, params })
void this.dealer.send([backrpcOperations.request, reqId, JSON.stringify([method, params])]).catch((err) => {
this.requests.delete(reqId) // Cleanup on failure
reject(err)
})
})
}
async send (body: any): Promise<any> {
await this.dealer.send([backrpcOperations.event, body])
}
close (): void {
this.closed = true
this.dealer.close()
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './types'
export * from './server'
export * from './client'
+253
View File
@@ -0,0 +1,253 @@
//
// 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-core'
import { v4 as uuidv4 } from 'uuid'
import * as zmq from 'zeromq'
import { backrpcOperations, type ClientId } from './types'
export interface BackRPCServerHandler<ClientT> {
requestHandler: (
client: ClientT,
method: string,
params: any,
send: (response: any) => Promise<void>
) => Promise<void>
helloHandler?: (client: ClientT) => Promise<void>
handleTimeout?: (client: ClientT) => Promise<void>
}
interface RPCClientInfo<ClientT extends string> {
id: ClientT
lastSeen: number
requests: Set<string>
requestsTotal: number
requestsTime: number
helloCounter: number
}
export class BackRPCServer<ClientT extends string = ClientId> {
uuid = uuidv4()
private readonly router: zmq.Router
private requestCounter: number = 0
private readonly backRequests = new Map<string, { resolve: (data: any) => void, reject: (err: any) => void }>()
private readonly clientMapping = new Map<ClientT, zmq.Message>()
private readonly revClientMapping = new Map<string, RPCClientInfo<ClientT>>()
private closed: boolean = false
private bound: Promise<void> | undefined
// A limit of requests per one client.
requestsLimit: number = 25
constructor (
private readonly handlers: BackRPCServerHandler<ClientT>,
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<void> {
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<number> {
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<void> {
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)
const clientInfo: RPCClientInfo<ClientT> =
this.revClientMapping.get(clientIdText) ??
({
id: reqId.toString() as ClientT,
lastSeen: this.tickMgr.now(),
requests: new Set(),
requestsTime: 0,
requestsTotal: 0,
helloCounter: 0
} satisfies RPCClientInfo<ClientT>)
clientInfo.helloCounter++
this.revClientMapping.set(clientIdText, clientInfo)
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:
{
if (client === undefined) {
// No Client, requests are not possible
await this.router.send([clientId, backrpcOperations.retry, reqId, JSON.stringify(1)])
continue
}
if (client.requests.has(reqId)) {
// We already had this request, so continue waiting for response.
continue
}
if (client.requests.size > this.requestsLimit) {
// No Client, requests are not possible
await this.router.send([clientId, backrpcOperations.retry, reqId, JSON.stringify(client.requests.size)])
continue
}
client.requestsTotal++
const [method, params] = JSON.parse(payload.toString())
const sendError = async (err: Error): Promise<void> => {
await this.router.send([
clientId,
backrpcOperations.responseError,
reqId,
JSON.stringify({
message: err.message ?? '',
stack: err.stack
})
])
}
client.requests.add(reqId)
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.backRequests.get(reqID)
try {
req?.resolve(JSON.parse(payload.toString()))
} catch (err: any) {
console.error(err)
}
this.backRequests.delete(reqID)
break
}
case backrpcOperations.responseError: {
const reqID = reqId.toString()
const req = this.backRequests.get(reqID)
try {
req?.reject(JSON.parse(payload.toString()))
} catch (err: any) {
console.error(err)
}
this.backRequests.delete(reqID)
break
}
}
} catch (err: any) {
console.error(err)
}
}
}
async request (clientId: ClientT, method: string, params: any): Promise<any> {
const clientIdentity = this.clientMapping.get(clientId)
if (clientIdentity === undefined) {
throw new Error(`Client ${clientId} not found`)
}
return await new Promise<any>((resolve, reject) => {
const reqId = clientId + '-' + this.requestCounter++
this.backRequests.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<any> {
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<void> {
this.closed = true
this.router.close()
}
}
+12
View File
@@ -0,0 +1,12 @@
export const backrpcOperations = {
hello: 0,
request: 1,
response: 2,
responseError: 3,
event: 4,
ping: 5,
pong: 6,
retry: 7
}
export type ClientId = string & { __clientId: string }
+12
View File
@@ -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"]
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "node"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@hcengineering/network-client",
"version": "0.7.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.7.4",
"@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/uuid": "^8.3.1",
"@types/node": "^22.15.29"
},
"dependencies": {
"@hcengineering/network-core": "workspace:^0.7.0",
"@hcengineering/network-backrpc": "workspace:^0.7.0",
"uuid": "^8.3.2"
},
"publishConfig": {
"access": "public"
}
}
+265
View File
@@ -0,0 +1,265 @@
import {
agentDirectRef,
type ClientUuid,
type ContainerConnection,
type ContainerEvent,
type ContainerUuid,
type NetworkAgent,
type TickManager
} from '@hcengineering/network-core'
import {
BackRPCClient,
BackRPCServer,
type BackRPCResponseSend,
type BackRPCServerHandler,
type ClientId
} from '@hcengineering/network-backrpc'
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<ClientUuid> {
readonly rpcServer: BackRPCServer<ClientUuid>
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<ClientUuid>(this, tickMgr, host, port)
}
async start (agent: NetworkAgent): Promise<void> {
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<void> {
// Handle container update
}
async getPort (): Promise<number> {
return await this.rpcServer.getPort()
}
async close (): Promise<void> {
await this.rpcServer.close()
}
async requestHandler (client: ClientUuid, method: string, params: any, send: BackRPCResponseSend): Promise<void> {
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<void> {
console.log(`Client ${clientId} connected`)
}
async handleTimeout (client: ClientUuid): Promise<void> {
console.log(`Client ${client} timed out`)
}
}
/**
* An routed connection to a container via agent.
*/
export class RoutedNetworkAgentConnectionImpl<ClientT extends string = ClientId> {
private readonly client: BackRPCClient<ClientT>
containers = new Map<ContainerUuid, ContainerConnection>()
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<ContainerConnection> {
// Establish a connection to the specified container
await this.client.request(opNames.connect, { uuid: containerUuid })
const connection: ContainerConnection = {
containerId: containerUuid,
close: async () => {
try {
await this.client.request(opNames.disconnect, { uuid: containerUuid })
} catch (err: any) {
// Ignore if disconnected already
}
},
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<void> {
// No callback is required
}
async onEvent (event: any): Promise<void> {
const [container, data] = event
const connection = this.containers.get(container)
if (connection !== undefined) {
await connection.on?.(data)
}
}
async close (): Promise<void> {
this.client.close()
}
async onRegister (): Promise<void> {
// Handle registration logic here
}
}
/**
* A direct connection to container
*/
export class NetworkDirectConnectionImpl implements ContainerConnection {
private readonly client: BackRPCClient<ClientUuid>
on?: ((data: any) => Promise<void>) | 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<any> {
return await this.client.request(operation, data)
}
async requestHandler (method: string, params: any, send: BackRPCResponseSend): Promise<void> {
// No callback is required
}
async onEvent (event: any): Promise<void> {
await this.on?.(event)
}
async close (): Promise<void> {
this.client.close()
}
async onRegister (): Promise<void> {
// No registration is required
}
}
/**
* A direct connection to container
*/
export class ContainerConnectionImpl implements ContainerConnection {
private connection!: ContainerConnection | Promise<ContainerConnection>
on?: ((data: any) => Promise<void>) | undefined
constructor (
readonly containerId: ContainerUuid,
connection: Promise<ContainerConnection> | ContainerConnection
) {
this.setConnection(connection)
}
async connect (): Promise<void> {
if (this.connection instanceof Promise) {
await this.connection
}
}
setConnection (connection: Promise<ContainerConnection> | ContainerConnection): void {
this.connection = connection
if (connection instanceof Promise) {
void connection.then((res) => {
this.connection = res
res.on = async (event) => {
void this.on?.(event)
}
})
} else {
connection.on = async (event) => {
void this.on?.(event)
}
}
}
async request (operation: string, data?: any): Promise<any> {
if (this.connection instanceof Promise) {
this.connection = await this.connection
}
return await this.connection.request(operation, data)
}
async close (): Promise<void> {
if (this.connection instanceof Promise) {
this.connection = await this.connection
}
await this.connection.close()
}
}
+331
View File
@@ -0,0 +1,331 @@
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-core'
import { v4 as uuidv4 } from 'uuid'
import { ContainerConnectionImpl, NetworkDirectConnectionImpl, RoutedNetworkAgentConnectionImpl } from './agent'
import { BackRPCClient, type BackRPCResponseSend } from '@hcengineering/network-backrpc'
import { agentDirectRef, EndpointKind, parseEndpointRef } from '@hcengineering/network-core'
import { opNames } from './types'
interface ClientAgentRecord {
agent: NetworkAgent
register: Promise<void>
resolve: () => void
}
class ContainerReferenceImpl implements ContainerReference {
constructor (
readonly uuid: ContainerUuid,
private readonly client: NetworkClientImpl,
private readonly _request: ContainerRequest
) {}
get endpoint (): ContainerEndpointRef {
const ref = this.client.references.get(this.uuid)
if (ref === undefined) {
throw new Error('Reference not found')
}
return ref.endpoint
}
async close (): Promise<void> {
await this.client.release(this.uuid)
this.client.references.delete(this.uuid)
}
async request (operation: string, data?: any): Promise<any> {
return await this.client.request(this.uuid, operation, data)
}
async connect (): Promise<ContainerConnection> {
let conn = this.client.containerConnections.get(this.uuid)
if (conn !== undefined) {
return conn
}
const endpoint = await this.client.getContainerRef(this.uuid, this._request)
conn = this.client.establishConnection(this.uuid, endpoint)
await conn.connect()
return conn
}
}
/**
* Huly Network client
*
* Some methods are omit clientId parameter.
*/
export class NetworkClientImpl implements NetworkClient {
clientId: ClientUuid = uuidv4() as ClientUuid
private readonly client: BackRPCClient<ClientUuid>
private readonly _agents = new Map<AgentUuid, ClientAgentRecord>()
// A set of clients for individual containers or agent TORs
containerConnections = new Map<ContainerUuid, ContainerConnectionImpl>()
agentConnections = new Map<AgentEndpointRef, RoutedNetworkAgentConnectionImpl<ClientUuid>>()
containerListeners: ContainerUpdateListener[] = []
references = new Map<
ContainerUuid,
{ ref: ContainerReference, request: ContainerRequest, endpoint: ContainerEndpointRef }
>()
registered: boolean = false
constructor (
readonly host: string,
port: number,
private readonly tickMgr: TickManager
) {
this.client = new BackRPCClient<ClientUuid>(this.clientId, this, host, port, tickMgr)
}
async close (): Promise<void> {
for (const directConn of this.containerConnections.values()) {
void directConn.close()
}
for (const agentConn of this.agentConnections.values()) {
void agentConn.close()
}
for (const agent of this._agents.values()) {
await this.client.request<ContainerEndpointRef[]>(opNames.unregister, {
uuid: agent.agent.uuid
})
}
this.client.close()
}
async requestHandler (method: string, params: any, send: BackRPCResponseSend): Promise<void> {
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<void> {
// Handle container events
// In case of container stopped, agent stopped or endpoint changed, we need to update direct connections to be re-established.
await this.handleConnectionUpdates(event)
for (const listener of this.containerListeners) {
try {
await listener(event)
} catch (error) {
console.error('Error in container listener:', error)
}
}
}
async handleRefUpdate (
uuid: ContainerUuid,
endpoint: ContainerEndpointRef | Promise<ContainerEndpointRef>
): Promise<void> {
const ref = this.references.get(uuid)
if (ref !== undefined) {
const refEndpoint = endpoint instanceof Promise ? await endpoint : endpoint
const conn = this.containerConnections.get(ref.ref.uuid)
if (conn !== undefined && ref.endpoint !== refEndpoint) {
conn.setConnection(this.establishConnection(ref.ref.uuid, refEndpoint))
} else {
ref.endpoint = refEndpoint
}
}
}
async onRegister (): Promise<void> {
for (const [uuid, ref] of this.references.entries()) {
await this.handleRefUpdate(uuid, await this.getContainerRef(uuid, ref.request))
}
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<void> {
const rec: ClientAgentRecord = {
agent,
register: Promise.resolve(),
resolve: () => {}
}
rec.register = new Promise<void>((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<void> {
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<ContainerEndpointRef[]>(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<AgentRecord[]> {
// Return actual list of agents
return await this.client.request<AgentRecord[]>(opNames.getAgents, {})
}
async kinds (): Promise<ContainerKind[]> {
return await this.client.request<ContainerKind[]>(opNames.getKinds, {})
}
async get (uuid: ContainerUuid, request: ContainerRequest): Promise<ContainerReference> {
const existing = this.references.get(uuid)
if (existing !== undefined) {
return existing.ref
}
const endpoint = await this.getContainerRef(uuid, request)
const ref: ContainerReference = new ContainerReferenceImpl(uuid, this, request)
this.references.set(uuid, { ref, request, endpoint })
return ref
}
establishConnection (uuid: ContainerUuid, endpoint: ContainerEndpointRef): ContainerConnectionImpl {
// 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<ClientUuid>(
this.tickMgr,
this.clientId,
parsedRef.host,
parsedRef.port
)
this.agentConnections.set(agentRef, agentConn)
}
let conn = this.containerConnections.get(uuid)
if (conn === undefined) {
conn = new ContainerConnectionImpl(uuid, agentConn.connect(parsedRef.uuid))
} else {
conn.setConnection(agentConn.connect(parsedRef.uuid))
}
this.containerConnections.set(uuid, conn)
return conn
}
const directConn = new NetworkDirectConnectionImpl(
this.tickMgr,
this.clientId,
parsedRef.uuid,
parsedRef.host,
parsedRef.port
)
let conn = this.containerConnections.get(uuid)
if (conn === undefined) {
conn = new ContainerConnectionImpl(uuid, directConn)
} else {
conn.setConnection(directConn)
}
this.containerConnections.set(uuid, conn)
return conn
}
async handleConnectionUpdates (event: ContainerEvent): Promise<void> {
// Handle connection updates
for (const updated of event.updated) {
await this.handleRefUpdate(updated.uuid, updated.endpoint)
}
for (const deleted of event.deleted) {
await this.handleRefUpdate(deleted.uuid, deleted.endpoint)
}
}
async getContainerRef (uuid: ContainerUuid, request: ContainerRequest): Promise<ContainerEndpointRef> {
return await this.client.request<ContainerEndpointRef>(opNames.getContainer, { uuid, request })
}
async release (uuid: ContainerUuid): Promise<void> {
await this.client.request<any>(opNames.releaseContainer, { uuid })
}
async list (kind: ContainerKind): Promise<ContainerRecord[]> {
return await this.client.request<ContainerRecord[]>(opNames.listContainers, {
kind
})
}
// Send some data to container, using proxy connection.
async request (target: ContainerUuid, operation: string, data?: any): Promise<any> {
return await this.client.request<any>(opNames.sendContainer, [target, operation, data])
}
onContainerUpdate (listener: ContainerUpdateListener): void {
this.containerListeners.push(listener)
}
}
+12
View File
@@ -0,0 +1,12 @@
import { TickManagerImpl, timeouts, type NetworkClient } from '@hcengineering/network-core'
import { NetworkClientImpl } from './client'
export * from './types'
export * from './client'
export * from './agent'
const tickMgr = new TickManagerImpl(timeouts.pingInterval * 2)
export function createNetworkClient (host: string, port: number): NetworkClient {
return new NetworkClientImpl(host, port, tickMgr)
}
+16
View File
@@ -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'
}
+12
View File
@@ -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"]
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "node"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+52
View File
@@ -0,0 +1,52 @@
{
"name": "@hcengineering/network-core",
"version": "0.7.0",
"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.7.4",
"@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/uuid": "^8.3.1",
"@types/node": "^22.15.29"
},
"dependencies": {
"uuid": "^8.3.2"
},
"repository": "https://github.com/hcengineering/platform",
"exports": {
".": {
"types": "./types/index.d.ts",
"require": "./lib/index.js",
"import": "./lib/index.js"
}
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,45 @@
import type { AgentUuid } from '../api/types'
import { timeouts } from '../api/timeouts'
import { AgentImpl } from '../agent'
import { TickManagerImpl } from '../utils'
import { NetworkImpl } from '../server'
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()
})
})
@@ -0,0 +1,66 @@
import { AgentImpl } from '../agent'
import type { AgentUuid, ClientUuid, ContainerEndpointRef, ContainerKind } from '../api/types'
import type { Container } from '../containers'
import { NetworkImpl } from '../server'
import { TickManagerImpl } from '../utils'
// class DummyConnectionManager implements ConnectionManager {
// async connect (endpoint: ContainerEndpointRef): Promise<ContainerConnection> {
// throw new Error('Method not implemented.')
// }
// }
class DummyContainer implements Container {
lastVisit: number = 0
onTerminated?: (() => void) | undefined
async request (operation: string, data: any): Promise<any> {
return undefined
}
async terminate (): Promise<void> {}
async ping (): Promise<void> {
this.lastVisit = Date.now()
}
connect (clientId: ClientUuid, handler: (data: any) => Promise<void>): void {}
disconnect (clientId: ClientUuid): void {}
}
const agents = {
agent1: 'agent1' as AgentUuid,
agent2: 'agent2' as AgentUuid,
agent3: 'agent3' as AgentUuid
}
const kinds: Record<string, ContainerKind> = {
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()
})
})
@@ -0,0 +1,25 @@
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)
})
})
+118
View File
@@ -0,0 +1,118 @@
import type { NetworkAgent } from './api/agent'
import type { Network } from './api/server'
import type {
AgentEndpointRef,
AgentUuid,
ContainerEndpointRef,
ContainerKind,
ContainerRecord,
ContainerRequest,
ContainerUuid
} from './api/types'
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<ContainerUuid, ContainerRecordImpl | Promise<ContainerRecordImpl>>()
private readonly _containers = new Map<ContainerEndpointRef, ContainerRecordImpl>()
endpoint?: AgentEndpointRef | undefined
constructor (
readonly uuid: AgentUuid,
private readonly factory: Record<ContainerKind, ContainerFactory>
) {}
async register (network: Network): Promise<void> {
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<ContainerRecord[]> {
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<ContainerRecordImpl | undefined> {
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<Container | undefined> {
return (await this.getContainerImpl(uuid))?.container
}
async get (uuid: ContainerUuid, request: ContainerRequest): Promise<ContainerEndpointRef> {
const current = await this.getContainerImpl(uuid)
if (current !== undefined) {
return current.endpoint
}
let container: ContainerRecordImpl | Promise<ContainerRecordImpl> = 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<void> {
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<any> {
const container = await this.getContainer(target)
if (container === undefined) {
throw new Error(`Container ${target} not found`)
}
return await container.request(operation, data)
}
}
+57
View File
@@ -0,0 +1,57 @@
import type { Container } from '../containers'
import type {
AgentEndpointRef,
AgentUuid,
ContainerEndpointRef,
ContainerEvent,
ContainerKind,
ContainerRecord,
ContainerRequest,
ContainerUuid
} from './types'
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[]
}
/**
* 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<void>
// Send agent update info to network, if applicable.
onAgentUpdate?: () => Promise<void>
// Get/Start of required container kind on agent
get: (uuid: ContainerUuid, request: ContainerRequest) => Promise<ContainerEndpointRef>
// A low level reference to container
getContainer: (uuid: ContainerUuid) => Promise<Container | undefined>
// List of active containers
list: (kind?: ContainerKind) => Promise<ContainerRecord[]>
// Send some data to container
request: (target: ContainerUuid, operation: string, data?: any) => Promise<any>
// ask for immediate termination for container
terminate: (container: ContainerEndpointRef) => Promise<void>
}
+80
View File
@@ -0,0 +1,80 @@
import type { AgentRecord, NetworkAgent } from './agent'
import type {
ContainerEndpointRef,
ContainerEvent,
ContainerKind,
ContainerRecord,
ContainerRequest,
ContainerUuid
} from './types'
export type ContainerUpdateListener = (event: ContainerEvent) => Promise<void>
/**
* 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<void>
agents: () => Promise<AgentRecord[]>
// A full uniq set of supported container kinds.
kinds: () => Promise<ContainerKind[]>
/*
* 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<ContainerReference>
list: (kind: ContainerKind) => Promise<ContainerRecord[]>
// Send some data to container, using proxy connection.
request: (target: ContainerUuid, operation: string, data?: any) => Promise<any>
// Register on container update listener
onContainerUpdate: (listener: ContainerUpdateListener) => void
close: () => Promise<void>
}
export interface ConnectionManager {
connect: (endpoint: ContainerEndpointRef) => Promise<ContainerConnection>
}
/**
* 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<void>
connect: () => Promise<ContainerConnection>
request: (operation: string, data?: any) => Promise<any>
}
// A request/reponse interface to container.
export interface ContainerConnection {
containerId: ContainerUuid
// A simple request/response to container.
request: (operation: string, data?: any) => Promise<any>
// A chunk streaming of results
// stream: (data: any) => Iterable<any>
// Recieve not a requests but also any kind of notifications.
on?: (data: any) => Promise<void>
close: () => Promise<void>
}
+55
View File
@@ -0,0 +1,55 @@
import type { AgentRecord, NetworkAgent } from './agent'
import type {
ContainerEndpointRef,
AgentUuid,
ClientUuid,
ContainerKind,
ContainerUuid,
ContainerRequest,
ContainerRecord,
ContainerEvent
} from './types'
/**
* 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<ContainerEndpointRef[]>
// Unregister an agent from the network.
// Will call terminate for every connection/references.
unregister: (agentId: AgentUuid) => Promise<void>
// Mark an agent as alive (updates lastSeen timestamp)
ping: (agentId: AgentUuid | ClientUuid) => void
agents: () => Promise<AgentRecord[]>
// A full uniq set of supported container kinds.
kinds: () => Promise<ContainerKind[]>
/*
* 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<ContainerEndpointRef>
/**
* 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<void>
list: (kind: ContainerKind) => Promise<ContainerRecord[]>
// Send some data to container, using proxy connection.
request: (target: ContainerUuid, operation: string, data?: any) => Promise<any>
}
export interface NetworkWithClients {
addClient: (clientUuid: ClientUuid, onContainer?: (event: ContainerEvent) => Promise<void>) => void
removeClient: (clientUuid: ClientUuid) => void
}
+4
View File
@@ -0,0 +1,4 @@
export const timeouts = {
aliveTimeout: 3, // seconds - timeout for detecting dead agents
pingInterval: 1 // seconds - how often to ping agents
}
+31
View File
@@ -0,0 +1,31 @@
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<string, any> // Extra parameters for container start
labels?: string[]
}
export interface ContainerEvent {
added: ContainerRecord[]
deleted: ContainerRecord[]
updated: ContainerRecord[]
}
export interface ContainerRequest {
kind: ContainerKind
extra?: Record<string, any> // Extra parameters for container start
labels?: string[]
}
+16
View File
@@ -0,0 +1,16 @@
export type TickHandler = () => void | Promise<void>
export interface TickManager {
now: () => number
// Interval in seconds
register: (handler: TickHandler, interval: number) => void
// Start tick manager
start: () => void
// Stop tick manager
stop: () => void
waitTick: (ticks: number) => Promise<void>
}
+27
View File
@@ -0,0 +1,27 @@
import type { ClientUuid, ContainerEndpointRef, ContainerRecord, ContainerRequest, ContainerUuid } from './api/types'
export interface Container {
request: (operation: string, data?: any, clientId?: ClientUuid) => Promise<any>
// Called when the container is terminated
onTerminated?: () => void
terminate: () => Promise<void>
ping: () => Promise<void>
connect: (clientId: ClientUuid, broadcast: (data: any) => Promise<void>) => void
disconnect: (clientId: ClientUuid) => void
}
export type ContainerFactory = (
uuid: ContainerUuid,
request: ContainerRequest
) => Promise<[Container, ContainerEndpointRef]>
export interface ContainerRecordImpl {
record: ContainerRecord
endpoint: ContainerEndpointRef | Promise<ContainerEndpointRef>
clients: Set<ClientUuid>
}
+65
View File
@@ -0,0 +1,65 @@
import type { AgentEndpointRef, AgentUuid, ContainerEndpointRef, ContainerUuid } from './api/types'
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
}
+11
View File
@@ -0,0 +1,11 @@
export * from './api/types'
export * from './api/utils'
export * from './api/agent'
export * from './api/client'
export * from './api/server'
export * from './api/timeouts'
export * from './utils'
export * from './endpoints'
export * from './agent'
export * from './containers'
export * from './server'
+430
View File
@@ -0,0 +1,430 @@
import type {
AgentEndpointRef,
AgentUuid,
ClientUuid,
ContainerEndpointRef,
ContainerEvent,
ContainerKind,
ContainerRecord,
ContainerRequest,
ContainerUuid
} from './api/types'
import { timeouts } from './api/timeouts'
import type { TickManager } from './api/utils'
import type { ContainerRecordImpl } from './containers'
import type { AgentRecord, NetworkAgent } from './api/agent'
import type { Network, NetworkWithClients } from './api/server'
interface AgentRecordImpl {
api: NetworkAgent
containers: Map<ContainerUuid, ContainerRecordImpl>
endpoint?: AgentEndpointRef
kinds: ContainerKind[]
lastSeen: number // Last time when agent was seen
}
interface ClientRecordImpl {
lastSeen: number
containers: Set<ContainerUuid>
onContainer?: (event: ContainerEvent) => Promise<void>
}
/**
* Server network implementation.
*/
export class NetworkImpl implements Network, NetworkWithClients {
private idx: number = 0
private readonly _agents = new Map<AgentUuid, AgentRecordImpl>()
private readonly _containers = new Map<ContainerUuid, AgentUuid>()
private readonly _clients = new Map<ClientUuid, ClientRecordImpl>()
private readonly _orphanedContainers = new Map<ContainerEndpointRef, ContainerRecordImpl>()
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<AgentRecord[]> {
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<ContainerKind[]> {
return Array.from(
this._agents
.values()
.map((it) => it.kinds)
.flatMap((it) => it)
)
}
async list (kind: ContainerKind): Promise<ContainerRecord[]> {
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<any> {
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<ContainerEndpointRef[]> {
const containers: ContainerRecord[] = record.containers
const newContainers = new Map<ContainerUuid, ContainerRecordImpl>(
containers.map((record) => [
record.uuid,
{
record,
request: { kind: record.kind },
endpoint: record.endpoint,
clients: new Set<ClientUuid>([])
}
])
)
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 unregister (agentId: AgentUuid): Promise<void> {
const agent = this._agents.get(agentId)
if (agent === undefined) {
return // Fine to ignore
}
await this.processDeadAgent(agentId)
}
async sendEvents (): Promise<void> {
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>): 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<ContainerEndpointRef> {
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<ContainerRecordImpl> {
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 suitableAgents = Array.from(this._agents.values().filter((it) => it.kinds.includes(request.kind)))
if (suitableAgents.length === 0) {
throw new Error(`No suitable agents found for container ${uuid}`)
}
const agent = Array.from(suitableAgents)[++this.idx % suitableAgents.length]
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<void> {
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<void> {
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<void> {
// 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<void> {
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<void> {
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) {
this._orphanedContainers.delete(container.record.endpoint)
// We just send container is deleted, so clients should re-request whem again.
containerEvent.deleted.push(container.record)
}
if (affectedContainers.length > 0) {
this.eventQueue.push(containerEvent)
}
}
}
+120
View File
@@ -0,0 +1,120 @@
import type { ContainerUuid } from './api/types'
import type { TickHandler, TickManager } from './api/utils'
export function groupByArray<T, K> (array: T[], keyProvider: (item: T) => K): Map<K, T[]> {
const result = new Map<K, T[]>()
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
tickListeners = new Map<number, (() => void)[]>()
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<void> {
this._tick++
// Handle tick listeners
if (this.tickListeners.size > 0) {
const listeners = this.tickListeners.get(this._tick) ?? []
this.tickListeners.delete(this._tick)
for (const listener of listeners) {
try {
listener()
} catch (err: any) {
console.error('Error in tick listener:', err)
}
}
}
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: () => void = () => {}
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)
}
}
async waitTick (ticks: number): Promise<void> {
if (ticks < 1) {
throw new Error('Ticks must be >= 1')
}
const targetTick = this._tick + ticks
await new Promise<void>((resolve) => {
this.tickListeners.set(targetTick, [...(this.tickListeners.get(targetTick) ?? []), resolve])
})
}
}
export function composeCID (prefix: string, id: string): ContainerUuid {
return `${prefix}_${id}` as ContainerUuid
}
+12
View File
@@ -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"]
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "node"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@hcengineering/network-server",
"version": "0.7.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.7.4",
"@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/network-core": "workspace:^0.7.0",
"@hcengineering/network-backrpc": "workspace:^0.7.0",
"@hcengineering/network-client": "workspace:^0.7.0",
"uuid": "^8.3.2"
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,32 @@
import type { Container, ClientUuid } from '@hcengineering/network-core'
export class DummySessionContainer implements Container {
async request (operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
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<void> {}
async ping (): Promise<void> {}
connect (clientId: ClientUuid, handler: (data: any) => Promise<void>): void {
this.eventHandlers.set(clientId, handler)
}
disconnect (clientId: ClientUuid): void {
this.eventHandlers.delete(clientId)
}
private readonly eventHandlers = new Map<ClientUuid, (data: any) => Promise<void>>()
}
@@ -0,0 +1,63 @@
import { BackRPCServer } from '@hcengineering/network-backrpc'
import {
type Container,
type ClientUuid,
type ContainerUuid,
type AgentUuid,
type NetworkClient,
type TickManager,
type ContainerEndpointRef,
containerDirectRef
} from '@hcengineering/network-core'
export class DummyWorkspaceContainer implements Container {
server!: BackRPCServer<ClientUuid>
constructor (
readonly uuid: ContainerUuid,
readonly agentId: AgentUuid,
readonly networkClient: NetworkClient
) {}
async start (tickMgr: TickManager): Promise<ContainerEndpointRef> {
this.server = new BackRPCServer<ClientUuid>(
{
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<any> {
return ''
}
// Called when the container is terminated
onTerminated?: () => void
async terminate (): Promise<void> {
await this.server.close()
}
async ping (): Promise<void> {}
connect (clientId: ClientUuid, handler: (data: any) => Promise<void>): void {
this.eventHandlers.set(clientId, handler)
}
disconnect (clientId: ClientUuid): void {
this.eventHandlers.delete(clientId)
}
private readonly eventHandlers = new Map<ClientUuid, (data: any) => Promise<void>>()
}
@@ -0,0 +1,120 @@
import {
AgentImpl,
composeCID,
containerOnAgentEndpointRef,
EndpointKind,
NetworkImpl,
parseEndpointRef,
TickManagerImpl,
type AgentEndpointRef,
type AgentUuid,
type ContainerConnection,
type ContainerKind,
type NetworkClient
} from '@hcengineering/network-core'
import { NetworkServer } from '../server'
import { DummySessionContainer } from './dummySession'
import { DummyWorkspaceContainer } from './dummyWorkspace'
import { NetworkClientImpl, NetworkAgentServer } from '@hcengineering/network-client'
const agents = {
agent1: 'agent1' as AgentUuid,
agent2: 'agent2' as AgentUuid
}
const kinds = {
session: 'session' as ContainerKind,
workspace: 'workspace' as ContainerKind
}
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
}
describe('check network server is working fine', () => {
it('check agent 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 _kinds = await networkClient.kinds()
expect(_kinds.length).toEqual(0)
const _agents = await networkClient.agents()
expect(_agents.length).toEqual(0)
await networkClient.close()
await network.close()
})
it('check routed connection and requests', 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<void>((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()
})
})
+1
View File
@@ -0,0 +1 @@
export * from './server'
+154
View File
@@ -0,0 +1,154 @@
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-core'
import { BackRPCServer, type BackRPCResponseSend, type BackRPCServerHandler } from '@hcengineering/network-backrpc'
import { opNames } from '@hcengineering/network-client'
class AgentCallbackHandler implements NetworkAgent {
constructor (
readonly rpcServer: BackRPCServer<ClientUuid>,
readonly uuid: AgentUuid,
readonly endpoint: AgentEndpointRef,
readonly kinds: ContainerKind[],
readonly client: ClientUuid
) {}
async get (uuid: ContainerUuid, request: ContainerRequest): Promise<ContainerEndpointRef> {
return await this.rpcServer.request(this.client, opNames.getContainer, [this.uuid, [uuid, request]])
}
async list (kind?: ContainerKind): Promise<ContainerRecord[]> {
return await this.rpcServer.request(this.client, opNames.listContainers, [this.uuid, [kind]])
}
async request (target: ContainerUuid, operation: string, data?: any): Promise<any> {
return await this.rpcServer.request(this.client, opNames.sendContainer, [this.uuid, [target, operation, data]])
}
async terminate (): Promise<void> {
// Ignore
}
async getContainer (uuid: ContainerUuid): Promise<Container | undefined> {
return undefined
}
}
export class NetworkServer implements BackRPCServerHandler<ClientUuid> {
rpcServer: BackRPCServer<ClientUuid>
constructor (
readonly network: Network & NetworkWithClients,
readonly tickMgr: TickManager,
host: string = '*',
port: number = 3737
) {
this.rpcServer = new BackRPCServer<ClientUuid>(this, tickMgr, host, port)
}
async close (): Promise<void> {
await this.rpcServer.close()
}
async requestHandler (client: ClientUuid, method: string, params: any, send: BackRPCResponseSend): Promise<void> {
switch (method) {
case opNames.register: {
// Handle register
await this.handleRegister(params, this.rpcServer, client, send)
break
}
case opNames.unregister: {
await this.handleUnregister(params, 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<void> {
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<void> {
console.log(`Client ${client} timed out`)
this.network.removeClient(client)
}
private async handleRegister (
params: any,
server: BackRPCServer<ClientUuid>,
client: ClientUuid,
send: (response: any) => Promise<void>
): Promise<void> {
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)
}
private async handleUnregister (params: any, send: (response: any) => Promise<void>): Promise<void> {
const agentUuid: AgentUuid = params.uuid
await this.network.unregister(agentUuid)
await send('ok')
}
}
+12
View File
@@ -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"]
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+8
View File
@@ -0,0 +1,8 @@
FROM hardcoreeng/base:v20250310
WORKDIR /usr/src/app
COPY bundle/bundle.js ./
COPY bundle/bundle.js.map ./
EXPOSE 8080
CMD [ "node", "./bundle.js" ]
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "node"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ['./src'],
coverageReporters: ['text-summary', 'html']
}
+70
View File
@@ -0,0 +1,70 @@
{
"name": "@hcengineering/network-pod",
"version": "0.7.0",
"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": {
"start": "rush bundle --to @hcengineering/network-pod && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json STATS_URL=http://huly.local:4900 node --inspect bundle/bundle.js",
"start-cr": "rush bundle --to @hcengineering/network-pod && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json STATS_URL=http://huly.local:4900 FULLTEXT_URL=http://huly.local:4702 SERVER_PORT=3332 node --inspect bundle/bundle.js",
"start-flame": "rush bundle --to @hcengineering/network-pod && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret MODEL_JSON=../../models/all/bundle/model.json clinic flame --dest ./out -- node --nolazy -r ts-node/register --enable-source-maps src/__start.ts",
"start-raw": "ts-node src/__start.ts",
"build": "compile",
"_phase:bundle": "rushx bundle",
"_phase:docker-build": "rushx docker:build",
"_phase:docker-staging": "rushx docker:staging",
"bundle": "node ../../common/scripts/esbuild.js --entry=src/index.ts --keep-names=true --bundle=true --sourcemap=external --external=*.node --external=bufferutil",
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/network-pod",
"docker:tbuild": "docker build -t hardcoreeng/network-pod . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/network-pod",
"docker:abuild": "docker build -t hardcoreeng/network-pod . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/network-pod",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/network-pod staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/network-pod",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent --forceExit",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent --forceExit",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "0.7.4",
"@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",
"@types/dockerode": "^3.3.19",
"esbuild": "^0.24.2"
},
"dependencies": {
"@hcengineering/network-core": "workspace:^0.7.0",
"@hcengineering/network-server": "workspace:^0.7.0",
"dockerode": "^3.3.5",
"uuid": "^8.3.2"
},
"repository": "https://github.com/hcengineering/platform",
"exports": {
".": {
"types": "./types/index.d.ts",
"require": "./lib/index.js",
"import": "./lib/index.js"
}
},
"publishConfig": {
"access": "public"
}
}
+42
View File
@@ -0,0 +1,42 @@
import { NetworkImpl, TickManagerImpl } from '@hcengineering/network-core'
import { NetworkServer } from '@hcengineering/network-server'
/**
* Main entry point for the network pod
*/
async function main(): Promise<void> {
console.log('Starting Huly Network Pod...')
// Create tick manager
const tickManager = new TickManagerImpl(20)
// Create Docker-based network service
const network = new NetworkImpl(tickManager)
const port = process.env.NETWORK_POD_PORT != null ? parseInt(process.env.NETWORK_POD_PORT) : 3737
// Create and start network server
const server = new NetworkServer(network, tickManager, process.env.NETWORK_POD_HOST ?? '*', port)
console.log(`Network Pod started on port ${port}`)
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('Shutting down Network Pod...')
await server.close()
process.exit(0)
})
process.on('SIGTERM', async () => {
console.log('Shutting down Network Pod...')
await server.close()
process.exit(0)
})
}
if (require.main === module) {
main().catch((err) => {
console.error('Failed to start Network Pod:', err)
process.exit(1)
})
}
+12
View File
@@ -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"]
}
+364
View File
@@ -0,0 +1,364 @@
/**
* This is the main configuration file for Rush.
* For full documentation, please see https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json",
/**
* (Required) This specifies the version of the Rush engine to be used in this repo.
* Rush's "version selector" feature ensures that the globally installed tool will
* behave like this release, regardless of which version is installed globally.
*
* The common/scripts/install-run-rush.js automation script also uses this version.
*
* NOTE: If you upgrade to a new major version of Rush, you should replace the "v5"
* path segment in the "$schema" field for all your Rush config files. This will ensure
* correct error-underlining and tab-completion for editors such as VS Code.
*/
"rushVersion": "5.158.1",
/**
* The next field selects which package manager should be installed and determines its version.
* Rush installs its own local copy of the package manager to ensure that your build process
* is fully isolated from whatever tools are present in the local environment.
*
* Specify one of: "pnpmVersion", "npmVersion", or "yarnVersion". See the Rush documentation
* for details about these alternatives.
*/
"pnpmVersion": "10.15.1",
// "npmVersion": "6.14.15",
// "yarnVersion": "1.9.4",
/**
* Older releases of the Node.js engine may be missing features required by your system.
* Other releases may have bugs. In particular, the "latest" version will not be a
* Long Term Support (LTS) version and is likely to have regressions.
*
* Specify a SemVer range to ensure developers use a Node.js version that is appropriate
* for your repo.
*
* LTS schedule: https://nodejs.org/en/about/releases/
* LTS versions: https://nodejs.org/en/download/releases/
*/
"nodeSupportedVersionRange": ">=18.20.3 <19.0.0 || >=20.14.0 <25.0.0",
/**
* If the version check above fails, Rush will display a message showing the current
* node version and the supported version range. You can use this setting to provide
* additional instructions that will display below the warning, if there's a specific
* tool or script you'd like the user to use to get in line with the expected version.
*/
// "nodeSupportedVersionInstructions": "Run 'nvs use' to switch to the expected node version.",
/**
* Odd-numbered major versions of Node.js are experimental. Even-numbered releases
* spend six months in a stabilization period before the first Long Term Support (LTS) version.
* For example, 8.9.0 was the first LTS version of Node.js 8. Pre-LTS versions are not recommended
* for production usage because they frequently have bugs. They may cause Rush itself
* to malfunction.
*
* Rush normally prints a warning if it detects a pre-LTS Node.js version. If you are testing
* pre-LTS versions in preparation for supporting the first LTS version, you can use this setting
* to disable Rush's warning.
*/
// "suppressNodeLtsWarning": false,
/**
* Rush normally prints a warning if it detects that the current version is not one published to the
* public npmjs.org registry. If you need to block calls to the npm registry, you can use this setting to disable
* Rush's check.
*/
// "suppressRushIsPublicVersionCheck": false,
/**
* Large monorepos can become intimidating for newcomers if project folder paths don't follow
* a consistent and recognizable pattern. When the system allows nested folder trees,
* we've found that teams will often use subfolders to create islands that isolate
* their work from others ("shipping the org"). This hinders collaboration and code sharing.
*
* The Rush developers recommend a "category folder" model, where buildable project folders
* must always be exactly two levels below the repo root. The parent folder acts as the category.
* This provides a basic facility for grouping related projects (e.g. "apps", "libraries",
* "tools", "prototypes") while still encouraging teams to organize their projects into
* a unified taxonomy. Limiting to 2 levels seems very restrictive at first, but if you have
* 20 categories and 20 projects in each category, this scheme can easily accommodate hundreds
* of projects. In practice, you will find that the folder hierarchy needs to be rebalanced
* occasionally, but if that's painful, it's a warning sign that your development style may
* discourage refactoring. Reorganizing the categories should be an enlightening discussion
* that brings people together, and maybe also identifies poor coding practices (e.g. file
* references that reach into other project's folders without using Node.js module resolution).
*
* The defaults are projectFolderMinDepth=1 and projectFolderMaxDepth=2.
*
* To remove these restrictions, you could set projectFolderMinDepth=1
* and set projectFolderMaxDepth to a large number.
*/
// "projectFolderMinDepth": 2,
// "projectFolderMaxDepth": 2,
/**
* Today the npmjs.com registry enforces fairly strict naming rules for packages, but in the early
* days there was no standard and hardly any enforcement. A few large legacy projects are still using
* nonstandard package names, and private registries sometimes allow it. Set "allowMostlyStandardPackageNames"
* to true to relax Rush's enforcement of package names. This allows upper case letters and in the future may
* relax other rules, however we want to minimize these exceptions. Many popular tools use certain punctuation
* characters as delimiters, based on the assumption that they will never appear in a package name; thus if we relax
* the rules too much it is likely to cause very confusing malfunctions.
*
* The default value is false.
*/
// "allowMostlyStandardPackageNames": true,
/**
* This feature helps you to review and approve new packages before they are introduced
* to your monorepo. For example, you may be concerned about licensing, code quality,
* performance, or simply accumulating too many libraries with overlapping functionality.
* The approvals are tracked in two config files "browser-approved-packages.json"
* and "nonbrowser-approved-packages.json". See the Rush documentation for details.
*/
// "approvedPackagesPolicy": {
// /**
// * The review categories allow you to say for example "This library is approved for usage
// * in prototypes, but not in production code."
// *
// * Each project can be associated with one review category, by assigning the "reviewCategory" field
// * in the "projects" section of rush.json. The approval is then recorded in the files
// * "common/config/rush/browser-approved-packages.json" and "nonbrowser-approved-packages.json"
// * which are automatically generated during "rush update".
// *
// * Designate categories with whatever granularity is appropriate for your review process,
// * or you could just have a single category called "default".
// */
// "reviewCategories": [
// // Some example categories:
// "production", // projects that ship to production
// "tools", // non-shipping projects that are part of the developer toolchain
// "prototypes" // experiments that should mostly be ignored by the review process
// ],
//
// /**
// * A list of NPM package scopes that will be excluded from review.
// * We recommend to exclude TypeScript typings (the "@types" scope), because
// * if the underlying package was already approved, this would imply that the typings
// * are also approved.
// */
// // "ignoredNpmScopes": ["@types"]
// },
/**
* If you use Git as your version control system, this section has some additional
* optional features you can use.
*/
"gitPolicy": {
/**
* Work at a big company? Tired of finding Git commits at work with unprofessional Git
* emails such as "beer-lover@my-college.edu"? Rush can validate people's Git email address
* before they get started.
*
* Define a list of regular expressions describing allowable e-mail patterns for Git commits.
* They are case-insensitive anchored JavaScript RegExps. Example: ".*@example\.com"
*
* IMPORTANT: Because these are regular expressions encoded as JSON string literals,
* RegExp escapes need two backslashes, and ordinary periods should be "\\.".
*/
// "allowedEmailRegExps": [
// "[^@]+@users\\.noreply\\.github\\.com",
// "rush-bot@example\\.org"
// ],
/**
* When Rush reports that the address is malformed, the notice can include an example
* of a recommended email. Make sure it conforms to one of the allowedEmailRegExps
* expressions.
*/
// "sampleEmail": "example@users.noreply.github.com",
/**
* The commit message to use when committing changes during 'rush publish'.
*
* For example, if you want to prevent these commits from triggering a CI build,
* you might configure your system's trigger to look for a special string such as "[skip-ci]"
* in the commit message, and then customize Rush's message to contain that string.
*/
// "versionBumpCommitMessage": "Bump versions [skip ci]",
/**
* The commit message to use when committing changes during 'rush version'.
*
* For example, if you want to prevent these commits from triggering a CI build,
* you might configure your system's trigger to look for a special string such as "[skip-ci]"
* in the commit message, and then customize Rush's message to contain that string.
*/
// "changeLogUpdateCommitMessage": "Update changelogs [skip ci]",
/**
* The commit message to use when committing changefiles during 'rush change --commit'
*
* If no commit message is set it will default to 'Rush change'
*/
// "changefilesCommitMessage": "Rush change"
},
"repository": {
/**
* The URL of this Git repository, used by "rush change" to determine the base branch for your PR.
*
* The "rush change" command needs to determine which files are affected by your PR diff.
* If you merged or cherry-picked commits from the main branch into your PR branch, those commits
* should be excluded from this diff (since they belong to some other PR). In order to do that,
* Rush needs to know where to find the base branch for your PR. This information cannot be
* determined from Git alone, since the "pull request" feature is not a Git concept. Ideally
* Rush would use a vendor-specific protocol to query the information from GitHub, Azure DevOps, etc.
* But to keep things simple, "rush change" simply assumes that your PR is against the "main" branch
* of the Git remote indicated by the repository.url setting in rush.json. If you are working in
* a GitHub "fork" of the real repo, this setting will be different from the repository URL of your
* your PR branch, and in this situation "rush change" will also automatically invoke "git fetch"
* to retrieve the latest activity for the remote main branch.
*/
// "url": "https://github.com/microsoft/rush-example",
/**
* The default branch name. This tells "rush change" which remote branch to compare against.
* The default value is "main"
*/
// "defaultBranch": "main",
/**
* The default remote. This tells "rush change" which remote to compare against if the remote URL is
* not set or if a remote matching the provided remote URL is not found.
*/
// "defaultRemote": "origin"
},
/**
* Event hooks are customized script actions that Rush executes when specific events occur
*/
"eventHooks": {
/**
* A list of shell commands to run before "rush install" or "rush update" starts installation
*/
"preRushInstall": [
// "common/scripts/pre-rush-install.js"
],
/**
* A list of shell commands to run after "rush install" or "rush update" finishes installation
*/
"postRushInstall": [],
/**
* A list of shell commands to run before "rush build" or "rush rebuild" starts building
*/
"preRushBuild": [],
/**
* A list of shell commands to run after "rush build" or "rush rebuild" finishes building
*/
"postRushBuild": [],
/**
* A list of shell commands to run before the "rushx" command starts
*/
"preRushx": [],
/**
* A list of shell commands to run after the "rushx" command finishes
*/
"postRushx": []
},
/**
* Installation variants allow you to maintain a parallel set of configuration files that can be
* used to build the entire monorepo with an alternate set of dependencies. For example, suppose
* you upgrade all your projects to use a new release of an important framework, but during a transition period
* you intend to maintain compatibility with the old release. In this situation, you probably want your
* CI validation to build the entire repo twice: once with the old release, and once with the new release.
*
* Rush "installation variants" correspond to sets of config files located under this folder:
*
* common/config/rush/variants/<variant_name>
*
* The variant folder can contain an alternate common-versions.json file. Its "preferredVersions" field can be used
* to select older versions of dependencies (within a loose SemVer range specified in your package.json files).
* To install a variant, run "rush install --variant <variant_name>".
*
* For more details and instructions, see this article: https://rushjs.io/pages/advanced/installation_variants/
*/
"variants": [
// {
// /**
// * The folder name for this variant.
// */
// "variantName": "old-sdk",
//
// /**
// * An informative description
// */
// "description": "Build this repo using the previous release of the SDK"
// }
],
/**
* Rush can collect anonymous telemetry about everyday developer activity such as
* success/failure of installs, builds, and other operations. You can use this to identify
* problems with your toolchain or Rush itself. THIS TELEMETRY IS NOT SHARED WITH MICROSOFT.
* It is written into JSON files in the common/temp folder. It's up to you to write scripts
* that read these JSON files and do something with them. These scripts are typically registered
* in the "eventHooks" section.
*/
// "telemetryEnabled": false,
/**
* Allows creation of hotfix changes. This feature is experimental so it is disabled by default.
* If this is set, 'rush change' only allows a 'hotfix' change type to be specified. This change type
* will be used when publishing subsequent changes from the monorepo.
*/
// "hotfixChangeEnabled": false,
/**
* This is an optional, but recommended, list of allowed tags that can be applied to Rush projects
* using the "tags" setting in this file. This list is useful for preventing mistakes such as misspelling,
* and it also provides a centralized place to document your tags. If "allowedProjectTags" list is
* not specified, then any valid tag is allowed. A tag name must be one or more words
* separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits,
* ".", and "@" characters.
*/
// "allowedProjectTags": [ "tools", "frontend-team", "1.0.0-release" ],
/**
* (Required) This is the inventory of projects to be managed by Rush.
*
* Rush does not automatically scan for projects using wildcards, for a few reasons:
* 1. Depth-first scans are expensive, particularly when tools need to repeatedly collect the list.
* 2. On a caching CI machine, scans can accidentally pick up files left behind from a previous build.
* 3. It's useful to have a centralized inventory of all projects and their important metadata.
*/
"projects": [
{
"packageName": "@hcengineering/network-core",
"projectFolder": "packages/core",
"shouldPublish": true
},
{
"packageName": "@hcengineering/network-backrpc",
"projectFolder": "packages/backrpc",
"shouldPublish": true
},
{
"packageName": "@hcengineering/network-client",
"projectFolder": "packages/client",
"shouldPublish": true
},
{
"packageName": "@hcengineering/network-server",
"projectFolder": "packages/server",
"shouldPublish": true
},
{
"packageName": "@hcengineering/network-pod",
"projectFolder": "pods/network-pod",
"shouldPublish": false
},
{
"packageName": "@hcengineering/scripts",
"projectFolder": "common/scripts",
"shouldPublish": false
}
]
}
+23
View File
@@ -0,0 +1,23 @@
# Problems not solve
- [x] Basic functional implementaion and zeromq transport based implementation.
- [x] basic tests
- [x] basic rate limit logic
- Add opentelementry for monitoring/logging
- Add docker + real network benchmark test
- Retry logic - need more carefully track request/response retry.
- Rate limit logic not implemented, unclear how to manage it now.
- Memory overhelming issues
1. Allow network based 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.