mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 21:27:46 +02:00
@@ -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
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"svelte.svelte-vscode",
|
||||
"esbenp.prettier-vscode",
|
||||
"firsttris.vscode-jest-runner"
|
||||
]
|
||||
}
|
||||
Vendored
+995
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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}
|
||||
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
// ]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
// }
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
Generated
+5733
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||
{
|
||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||
}
|
||||
@@ -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"
|
||||
// }
|
||||
]
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
]
|
||||
@@ -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
|
||||
Executable
+29
@@ -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
|
||||
Executable
+7
@@ -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} .
|
||||
Executable
+54
@@ -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
|
||||
Executable
+8
@@ -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
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@hcengineering/scripts",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"format": "echo \"No format specified\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.24.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "node"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './types'
|
||||
export * from './server'
|
||||
export * from './client'
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "node"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "node"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const timeouts = {
|
||||
aliveTimeout: 3, // seconds - timeout for detecting dead agents
|
||||
pingInterval: 1 // seconds - how often to ping agents
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "node"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from './server'
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -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" ]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "node"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ['./src'],
|
||||
coverageReporters: ['text-summary', 'html']
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user