diff --git a/Dockerfile b/Dockerfile index bdebf5eb..6733eca2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ ARG NODE_VERSION=20 FROM node:${NODE_VERSION}-alpine AS deps WORKDIR /var/www COPY package.json package-lock.json ./ +COPY plugins/*/package.json ./plugins/ # RUN --mount=type=cache,target=/root/.npm npm install RUN npm install @@ -15,6 +16,7 @@ FROM node:${NODE_VERSION}-alpine AS builder WORKDIR /var/www COPY --from=deps /var/www . COPY . . +COPY --from=deps /var/www/plugins/*/node_modules ./plugins/*/node_modules/ ENV NODE_ENV=production # RUN --mount=type=cache,target=./node_modules/.cache npm run build RUN npm run build diff --git a/example.env b/example.env index c913c5b1..3493186f 100644 --- a/example.env +++ b/example.env @@ -73,8 +73,10 @@ # VUE_APP_DOCHUB_BUILDING_CACHE= memory / filesystem # (BF) Определяет принцип кэширования ответов к Data Lake через backend -# none - не кэшируется (по умолчанию) -# memory - кеширование в памяти. При перезагрузке буде очищаться. +# none - не кэшируется (по умолчанию) +# redis - для кеширования используется СУБД Redis (https://redis.io/). +# Конфигурация подключения задается в переменной VUE_APP_DOCHUB_REDIS_URL +# memory - кеширование в памяти. При перезагрузке буде очищаться. # # Иное значение рассматривается как относительный путь к папке кеширования. Папка должна существовать! # В этом случае результат запроса будет сохраняться в файле. При перезагрузке кэш будет сохраняться. @@ -123,6 +125,21 @@ # VUE_APP_DOCHUB_BACKEND_EVENT_LOADING_ERRORS_FOUND=http://foo.local/error +# (B) Интеграция с СУБД Redis (https://redis.io/) +# Redis применяется для кэширования результатов запросов на стороне backend, а также для создания кластера DocHub. +# Формат URL подключения: redis[s]://[[username][:password]@][host][:port][/db-number] +# Пример: redis://alice:foobared@awesome.redis.server:6380 +# По умолчанию значение - redis://localhost:6379 +# VUE_APP_DOCHUB_REDIS_URL=redis://localhost:6379 + + +# (B) Кластер DocHub (beta) +# Обеспечивает высокую доступность сервиса. На производительность не влияет. +# Количество нод в кластере условно не ограничено. Для работы кластер требует Redis. +# По умолчанию кластер выключен (off). +# VUE_APP_DOCHUB_CLUSTER= on / off + + # *********************************************************** # Примеры конфигурирования @@ -140,6 +157,8 @@ # - Войдя на портал, пользователь увидит только документацию DocHub. # - Рендеринг PlantUML диаграмм будет осуществляться с использованием публичного сервера с ограничением размера контента. + + # (F)******** Портал с собственной документацией ***************** # Развертывание в режиме frontend (толстый клиент) @@ -157,6 +176,7 @@ # - Рендеринг PlantUML диаграмм будет осуществляться с использованием собственного сервера https://plantuml.local/svg/ (требуется развернуть). + # (F)* Портал с собственной документацией и без документации DocHub ** # Развертывание в режиме frontend (толстый клиент) @@ -173,6 +193,7 @@ # - Рендеринг PlantUML диаграмм будет осуществляться с использованием публичного сервера с ограничением размера контента. + # (F)* Портал с собственной документацией из GitLab без авторизации ** # Развертывание в режиме frontend (толстый клиент) @@ -191,6 +212,7 @@ # - Рендеринг PlantUML диаграмм будет осуществляться с использованием собственного сервера https://plantuml.local/svg/ (требуется развернуть). + # (F)* Портал с собственной документацией из GitLab с авторизацией ** # Развертывание в режиме frontend (толстый клиент) @@ -210,6 +232,7 @@ # - Рендеринг PlantUML диаграмм будет осуществляться с использованием публичного сервера с ограничением размера контента. + # (FB)******** Портал с документацией DocHub и backend ***************** # Развертывание в режиме frontend + backend (тонкий клиент) @@ -224,7 +247,8 @@ # - Рендеринг PlantUML диаграмм будет осуществляться с использованием публичного сервера с ограничением размера контента. -# (FB)* Портал с собственной документацией и backend и без документации DocHub** + +# (FB)* Портал с собственной документацией и backend, без документации DocHub** # Развертывание в режиме frontend + backend (тонкий клиент) # содержимое файла .env: @@ -242,5 +266,60 @@ # - Рендеринг PlantUML диаграмм будет осуществляться с использованием публичного сервера с ограничением размера контента. + +# (FB) Портал с собственной документацией и backend, без документации DocHub, с кешированием в Redis + +# Развертывание в режиме frontend + backend (тонкий клиент) + Redis +# содержимое файла .env: + +# VUE_APP_DOCHUB_ROOT_MANIFEST=workspace/sberauto/root.yaml +# VUE_APP_DOCHUB_APPEND_DOCHUB_DOCS=n +# VUE_APP_DOCHUB_DATALAKE_CACHE=redis +# VUE_APP_DOCHUB_REDIS_URL=redis://alice:foobared@awesome.redis.server:6380 + + +# Команда сборки: +# npm run backend +# Результат: +# - Будет собран frontend проект в режиме backend +# - Будет запущен nodejs сервер на порту 3030 (http://localhost:3030/), где будет поднят портал и backend сервер +# - Войдя на портал, пользователь увидит документацию размещенную по пути ./public/workspace/sberauto/* +# - Рендеринг PlantUML диаграмм будет осуществляться с использованием публичного сервера с ограничением размера контента. +# - Результаты запросов будут кэшироваться в Redis, что увеличит производительность + + + +# (FB) Портал с собственной документацией в кластере, без документации DocHub, с кешированием в Redis + +# Развертывание в режиме frontend + backend (тонкий клиент) + Redis + Cluster +# Для работы кластера необходимо самостоятельно развернуть балансировщик запросов. +# Например на nginx - https://nginx.org/ru/docs/http/ngx_http_upstream_module.html +# Недоступная нода определяется по коду ответа - 503. +# +# Конфигурация идентична для всех нод. Содержимое файла .env: + +## Доступ к ресурсам с манифестами должен быть у всех нод кластера +# VUE_APP_DOCHUB_ROOT_MANIFEST=workspace/sberauto/root.yaml +# VUE_APP_DOCHUB_APPEND_DOCHUB_DOCS=n +# VUE_APP_DOCHUB_DATALAKE_CACHE=redis +# VUE_APP_DOCHUB_REDIS_URL=redis://alice:foobared@awesome.redis.server:6380 +# VUE_APP_DOCHUB_CLUSTER=on +# VUE_APP_DOCHUB_BACKEND_PORT=3030 +## Вызов бэкенда должен происходить через балансировщик, который перенаправляет запросы на действующую ноду. +# VUE_APP_DOCHUB_BACKEND_URL=http://node-balancer:3030 + + +# Команда сборки: +# npm run backend +# Результат: +# - Будет собран frontend проект в режиме backend +# - Будет запущена нода кластера на порту 3030 (http://localhost:3030/), где будет поднят портал backend сервер +# - Войдя на портал, пользователь увидит документацию размещенную по пути ./public/workspace/sberauto/* +# - Рендеринг PlantUML диаграмм будет осуществляться с использованием публичного сервера с ограничением размера контента. +# - Результаты запросов будут кэшироваться в Redis, что увеличит производительность +# - Запросы к backend будут направляться на балансировщик, который перенаправит их на действующую ноду +# - Ноды backend будут работать в кластере, что увеличит надежность + + # Больше информации о переменных среды выполнения # https://cli.vuejs.org/ru/guide/mode-and-env.html diff --git a/package-lock.json b/package-lock.json index 6744de2e..d5a0c6eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "dochubcore", "version": "1.0.6", + "hasInstallScript": true, "dependencies": { "@asyncapi/react-component": "1.0.0-next.44", "@asyncapi/web-component": "0.24.23", @@ -19,9 +20,11 @@ "core-js": "3.26.1", "dateformat": "3.0.3", "jsonata": "2.0.3", + "md5": "2.3.0", "mermaid": "9.3.0", "monaco-editor": "0.34.1", "mustache": "4.2.0", + "object-hash": "^3.0.0", "semver": "7.5.4", "swagger-ui": "3.52.5", "uuid": "8.3.2", @@ -63,10 +66,10 @@ "jest": "29.4.1", "jest-environment-jsdom": "29.4.1", "jest-test-gen": "1.4.3", - "md5": "2.3.0", "nodemon": "2.0.20", "postcss-loader": "2.1.6", "raw-loader": "4.0.2", + "redis": "4.6.10", "ts-loader": "8.2.0", "ts-node": "10.9.1", "ts-node-dev": "2.0.0", @@ -4206,6 +4209,71 @@ "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "dev": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", + "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "dev": true, + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "dev": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "dev": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", + "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "dev": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "dev": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -8218,7 +8286,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true, "engines": { "node": "*" } @@ -8606,6 +8673,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmd-shim": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-5.0.0.tgz", @@ -9375,7 +9451,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true, "engines": { "node": "*" } @@ -12865,6 +12940,15 @@ "node": ">=8" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -14382,8 +14466,7 @@ "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-callable": { "version": "1.2.7", @@ -18382,7 +18465,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -20110,6 +20192,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -22906,6 +22996,20 @@ "node": ">=0.10.0" } }, + "node_modules/redis": { + "version": "4.6.10", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", + "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "dev": true, + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.11", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.6", + "@redis/search": "1.1.5", + "@redis/time-series": "1.0.5" + } + }, "node_modules/redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", @@ -33444,6 +33548,60 @@ "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "dev": true, + "requires": {} + }, + "@redis/client": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", + "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "dev": true, + "requires": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "dev": true, + "requires": {} + }, + "@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "dev": true, + "requires": {} + }, + "@redis/search": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", + "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "dev": true, + "requires": {} + }, + "@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "dev": true, + "requires": {} + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -36669,8 +36827,7 @@ "charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" }, "chokidar": { "version": "3.5.3", @@ -36967,6 +37124,12 @@ } } }, + "cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true + }, "cmd-shim": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-5.0.0.tgz", @@ -37585,8 +37748,7 @@ "crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" }, "crypto-random-string": { "version": "1.0.0", @@ -37882,7 +38044,7 @@ "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "requires": { - "es5-ext": "0.10.53", + "es5-ext": "^0.10.50", "type": "^1.0.1" }, "dependencies": { @@ -38863,7 +39025,7 @@ "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "requires": { "d": "1", - "es5-ext": "0.10.53", + "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" }, "dependencies": { @@ -38899,7 +39061,7 @@ "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "requires": { "d": "1", - "es5-ext": "0.10.53", + "es5-ext": "^0.10.46", "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.1" }, @@ -39366,7 +39528,7 @@ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "requires": { "d": "1", - "es5-ext": "0.10.53" + "es5-ext": "~0.10.14" }, "dependencies": { "es5-ext": { @@ -40284,6 +40446,12 @@ } } }, + "generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -41440,8 +41608,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { "version": "1.2.7", @@ -44272,7 +44439,7 @@ "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", "requires": { - "es5-ext": "0.10.53" + "es5-ext": "~0.10.2" }, "dependencies": { "es5-ext": { @@ -44490,7 +44657,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, "requires": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -44597,7 +44763,7 @@ "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", "requires": { "d": "^1.0.1", - "es5-ext": "0.10.53", + "es5-ext": "^0.10.53", "es6-weak-map": "^2.0.3", "event-emitter": "^0.3.5", "is-promise": "^2.2.2", @@ -45868,6 +46034,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -47926,6 +48097,20 @@ } } }, + "redis": { + "version": "4.6.10", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", + "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "dev": true, + "requires": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.11", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.6", + "@redis/search": "1.1.5", + "@redis/time-series": "1.0.5" + } + }, "redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", @@ -50391,7 +50576,7 @@ "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", "requires": { - "es5-ext": "0.10.53", + "es5-ext": "~0.10.46", "next-tick": "1" }, "dependencies": { diff --git a/package.json b/package.json index 5c90adbf..7999f7a7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "build:package": "node vue.lib.js", "lint": "vue-cli-service lint", "test": "jest --watch", - "meta-import": "node src/tools/meta/import.js" + "meta-import": "node src/tools/meta/import.js", + "postinstall": "find plugins -maxdepth 1 -type d -exec sh -c \"cd '{}' && if [ -f package.json ]; then npm install; fi\" \\;" }, "dependencies": { "@asyncapi/react-component": "1.0.0-next.44", @@ -38,6 +39,7 @@ "core-js": "3.26.1", "dateformat": "3.0.3", "jsonata": "2.0.3", + "md5": "2.3.0", "mermaid": "9.3.0", "monaco-editor": "0.34.1", "mustache": "4.2.0", @@ -82,10 +84,10 @@ "jest": "29.4.1", "jest-environment-jsdom": "29.4.1", "jest-test-gen": "1.4.3", - "md5": "2.3.0", "nodemon": "2.0.20", "postcss-loader": "2.1.6", "raw-loader": "4.0.2", + "redis": "4.6.10", "ts-loader": "8.2.0", "ts-node": "10.9.1", "ts-node-dev": "2.0.0", @@ -97,7 +99,8 @@ "vue-template-compiler": "2.7.14", "webpack-bundle-analyzer": "4.8.0", "webpack-pwa-manifest": "4.3.0", - "yo": "4.3.0" + "yo": "4.3.0", + "object-hash": "3.0.0" }, "overrides": { "es5-ext": "0.10.53" diff --git a/public/documentation/docs/manual/config/deployment.md b/public/documentation/docs/manual/config/deployment.md index 67620762..ca5a635e 100644 --- a/public/documentation/docs/manual/config/deployment.md +++ b/public/documentation/docs/manual/config/deployment.md @@ -4,7 +4,8 @@ DocHub поддерживает следующие режима разверты 1. **Plugin** - Плагин IDE (Idea / VSCode). Вся бизнес-логика располагается в плагине; 2. **Server Less** - Портал. Вся бизнес-логика располагается на клиенте. -2. **Client-Server** - Портал. Основная бизнес-логика располагается на сервере. +3. **Client-Server** - Портал. Основная бизнес-логика располагается на сервере. +4. **Cluster** - Ноды в режиме **Client-Server** функционируют в кластере. Режима имеют свои преимущества и недостатки. Необходимо их рассмотреть детально: @@ -31,8 +32,9 @@ DocHub поддерживает следующие режима разверты Прекрасно подходит для изучения инструмента. -Остается востребованным на всех этапах внедрения DocHub в организации, т.к. позволяет внедрить подход "Архитектура как код" -в ключевой этап производства - разработку кодовой базы приложений, сервисов и инфраструктуры. +Остается востребованным на всех этапах внедрения DocHub в организации, т.к. позволяет внедрить подход +"Архитектура как код" в ключевой этап производства - разработку кодовой базы приложений, сервисов +и инфраструктуры. ## Server Less @@ -48,15 +50,16 @@ DocHub поддерживает следующие режима разверты Файлы содержащие код архитектуры загружаются по доступным приложению URL при его исполнении в браузере. -Проще говоря, если вы развернули сайт на http://www.dochub.local, необходимо указать в переменной окружения корневой манифест, -например: +Проще говоря, если вы развернули сайт на http://www.dochub.local, необходимо указать в переменной +окружения корневой манифест, например: ``` VUE_APP_DOCHUB_ROOT_MANIFEST=workspace/service/root.yaml ``` -в этом случае, сразу после загрузки приложения в браузере, DocHub попытается загрузить файл http://www.dochub.local/workspace/service/root.yaml -и "выполнить" его. В корневом манифесте могут содержаться указания по подключению зависимостей. Например: +в этом случае, сразу после загрузки приложения в браузере, DocHub попытается загрузить файл +http://www.dochub.local/workspace/service/root.yaml и "выполнить" его. В корневом манифесте могут +содержаться указания по подключению зависимостей. Например: ```yaml imports: @@ -108,7 +111,8 @@ imports: 1. **/core/storage/jsonata/:query** - Доступ к кодовой базе (архитектурному Data Lake) через JSONata запросы; 2. **/core/storage/validators/** - Доступ к результату работы [валидаторов](/docs/dochub.rules.validators); -2. **/entities/:entity/presentations/:presentation*** - Доступ к презентациям [сущностей](/docs/dochub.entities) специального типа "upload". +2. **/entities/:entity/presentations/:presentation*** - Доступ к презентациям + [сущностей](/docs/dochub.entities) специального типа "upload". Простейшей сборкой DocHub в режиме Client-Server является команда: @@ -120,22 +124,192 @@ imports: [example.env](https://github.com/RabotaRu/DocHub/blob/master/example.env)). ### Преимущества -1. Заметно ускоряет работу пользовательского приложения за счет единоразовой подготовки архитектурной кодовой базы на стороне backend; -2. Позволяет кэшировать результаты запросов и разделять кэши между пользователями, что значительно сокращает отклик приложения на действия пользователя; +1. Заметно ускоряет работу пользовательского приложения за счет единоразовой подготовки архитектурной + кодовой базы на стороне backend; +2. Позволяет кэшировать результаты запросов и разделять кэши между пользователями, что значительно + сокращает отклик приложения на действия пользователя; 3. Позволяет регулировать доступ пользователя к объектам архитектурного учета (в разработке); 4. Скрывает для пользователя чувствительные конфигурационные денные интеграций, например с хранилищами GitLab; 5. Предоставляет API для встраивания DocHub в DevOps процессы. ### Недостатки -1. Требует более сложной конфигурации как самого DocHub так и серверных ресурсов для стабильного функционирования; -2. При сложных связях архитектурного кода, могут потребоваться специальные меры по инвалидации кэша (регулярно чистить кэш); -3. Перезапуск сервера, при значительных объемах кодовой базы может занимать заметное время, что воздействует на всех пользователей. +1. Требует более сложной конфигурации как самого DocHub так и серверных ресурсов для + стабильного функционирования; +2. При сложных связях архитектурного кода, могут потребоваться специальные меры по инвалидации + кэша (регулярно чистить кэш); +3. Перезапуск сервера, при значительных объемах кодовой базы может занимать заметное время, + что воздействует на всех пользователей. ### Рекомендации -Данный режим разумно использовать при существенных объемах кодовой базы и использовании DocHub как инструмента -встраиваемого в процессы производства (в DevOps процессы в частности). +Данный режим разумно использовать при существенных объемах кодовой базы и использовании DocHub +как инструмента встраиваемого в процессы производства (в DevOps процессы в частности). + + +## Cluster + +**ВНИМАНИЕ!** Данный режим проходит beta-тестирование. + +Этот режим создан для того, чтобы сервис DocHub был доступен в любое время. В кластере может быть запущено много нод +в режиме "Client-Server", что обеспечивает доступ к сервису, даже если все, кроме одной ноды, вышли из строя. + +Также режим позволяет обновлять данные манифестов без приостановки работы сервиса. + +Функционирование кластера обеспечивается синхронизацией нод через СУБД [Redis](https://redis.io/). + +### Компонентная схема работы кластера + +```plantuml +@startuml + actor "User" as user + component "Browser" as browser + component "Balancer" as balancer + + + package "DocHub кластер" { + component "Node 1" as node1 + component "Node 2" as node2 + component "Node N" as nodeN + } + + database "Redis" as redis + database "Хранилище\nманифестов" as storage + + user <-D- browser + browser <-D- balancer + + redis <-U-> node1 + redis <-U-> node2 + redis <-U-> nodeN + + storage -U-> node1 + storage -U-> node2 + storage -U-> nodeN + + balancer <-D- node1 + balancer <-D- node2 + balancer <-D- nodeN +@enduml +``` + +### Алгоритм работы кластера +```plantuml +@startuml +participant DocHubFront [ + =DocHub + ---- + ""Front end"" +] + +participant Balancer [ + =Balancer + ---- + ""Балансировщик"" +] + +participant DocHubNode1 [ + =DocHub + ---- + ""Server node1"" +] + +participant DocHubNode2 [ + =DocHub + ---- + ""Server node2"" +] + + +participant Storage [ + =Storage + ---- + ""Хранилище"" + ""манифестов"" +] + +participant Redis [ + =Redis + ---- + ""Медиатор"" + ""кластера"" +] + +group Старт или перезагрузка ноды Server node1 + DocHubFront -> Balancer: Запрос + Balancer -> DocHubNode1: Попытка запроса + + note over of DocHubNode1: Пока нода не готова, она будет отдавать на запросы код 503 + Balancer <- DocHubNode1: 503 временно недоступно + Balancer -> Balancer: Переключение на другую ноду + Balancer -> DocHubNode2: Попытка запроса + Balancer <- DocHubNode2: 200 результат + DocHubFront <- Balancer: Результат + + DocHubNode1 <-Storage: Загрузка манифестов + DocHubNode1 -> DocHubNode1: Вычисление HASH состояния + DocHubNode1 -> Redis: Запись HASH по ключу DocHub.cluster.hash + note over of DocHubNode1: По завершению загрузки нода будет отвечать с кодом 200 +end + +group Синхронизация Server node2 + DocHubFront -> Balancer: Запрос + Balancer -> DocHubNode2: Попытка запроса + DocHubNode2 <- Redis: Получение HASH + note right of DocHubNode2: При каждом запросе к ноде она\n сверяет локальный HASH состояния с Redis + DocHubNode2 <- DocHubNode2: Сверка HASH + DocHubNode2 <- DocHubNode2: Обнаружено расхождение + note right of DocHubNode2: При обнаружении расхождения,\nзапускается процесс обновления данных манифестов\nОтдается ответ 503 + Balancer <- DocHubNode2: 503 временно недоступно + + Balancer -> Balancer: Переключение на другую ноду + Balancer -> DocHubNode1: Попытка запроса + Balancer <- DocHubNode1: 200 результат + DocHubFront <- Balancer: Результат + + DocHubNode2 <-Storage: Загрузка манифестов + DocHubNode2 -> DocHubNode2: Вычисление HASH состояния + DocHubNode2 -> Redis: Запись HASH по ключу DocHub.cluster.hash + note over of DocHubNode2: По завершению загрузки нода будет отвечать с кодом 200 + +end + + +@enduml + +``` + +### Преимущества +1. Предоставляет все преимущества развертывания в режиме Client-Server; +2. Обеспечивает высокую надежность сервиса и дополнительное повышение производительности за + счет горизонтального масштабирования; +3. Обеспечивает работу сервиса без простоя в период обновления данных. + +### Недостатки +1. Требует развертывания инфраструктуры отказоустойчивости: балансировщик, Redis; +2. При сложных связях архитектурного кода, могут потребоваться специальные меры по инвалидации + кэша (регулярно чистить кэш). + + +### Рекомендации + +Кластер рекомендуется применять при высоких требованиях к уровню сервиса по надежности и производительности. + +### Конфигурирование + +Для перевода группы серверов в режиме Client-Server в кластер, необходимо: + +1. Развернуть СУБД [Redis](https://redis.io/); +2. Развернуть балансировщик и настроить его. Например, можно использовать + [nginx](https://nginx.org/ru/docs/http/ngx_http_upstream_module.html) ; +3. Добавить в конфигурацию серверов параметры: + +``` +# Формат URL подключения: redis[s]://[[username][:password]@][host][:port][/db-number] +VUE_APP_DOCHUB_REDIS_URL=redis://alice:foobared@awesome.redis.server:6380 +VUE_APP_DOCHUB_CLUSTER=on +``` +4. Перезапустить серверы. ## Файлы переменных среды исполнения @@ -149,7 +323,7 @@ imports: ``` [Больше информации](https://cli.vuejs.org/ru/guide/mode-and-env.html) -## Переменные среды исполнения +## Переменные среды исполнения и примеры конфигураций Актуальные параметры конфигурирования смотрите в файле [example.env](https://github.com/RabotaRu/DocHub/blob/master/example.env) diff --git a/public/documentation/docs/manual/imports.md b/public/documentation/docs/manual/imports.md index e11f356a..c306c0fc 100644 --- a/public/documentation/docs/manual/imports.md +++ b/public/documentation/docs/manual/imports.md @@ -48,6 +48,12 @@ components: ``` Подключение дополнительных манифестов возможно из сторонних репозиториев **того же** инстанса GitLab, -а также с web-ресурсов по http/https протоколу. +а также с web-ресурсов по http/https протоколу. Например: + +```yaml +imports: + - https://dochub.info/documentation/root.yaml + - gitlab:43847396:master@root.yaml +``` [Далее](/docs/dochub.components) diff --git a/public/documentation/root.yaml b/public/documentation/root.yaml index 32efb5f3..5d9cea29 100755 --- a/public/documentation/root.yaml +++ b/public/documentation/root.yaml @@ -4,4 +4,4 @@ imports: - docs/root.yaml - arch/root.yaml - entities/root.yaml - +testkey: 14 diff --git a/src/assets/libs/smartants.js b/src/assets/libs/smartants.js index 500b750a..0f1bcd44 100644 --- a/src/assets/libs/smartants.js +++ b/src/assets/libs/smartants.js @@ -1 +1 @@ -eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('3 V={2l:"25-1s-2k",1M:"24-1Y-1K",1R:"1Z-1l-1K",1H:"20-21-22",1A:"1B-O"},23=1y,1X=10,27=5;15 1v(t,e){C o=0,s=0;J(3 e 1s t)o+=e.w*e.h,s=g.S(s,e.w);3 r=[{x:0,y:0,w:g.S(g.28(g.29(o/.2a)),s,e||0),h:1/0,R:[]}];C h=0,i=0,a=[];J(3 e 1s t)J(C t=r.B-1;t>=0;t--){3 o=r[t];9(!(e.w>o.w||e.h>o.h&&r.1t)){9(e.h>o.h&&!r.1t){3 s=e.h-o.h,h=o.y+o.h,i=o.y;r.j((e=>{e.y>=h?e.y+=s:e.y>=i&&e.y+e.h>=h?e.h+=s:t&&(r.1t=!0)}))}9(e.x=o.x,e.y=o.y,a.E(e),i=g.S(i,e.y+e.h),h=g.S(h,e.x+e.w),e.w===o.w&&e.h===o.h){3 e=r.2c();tt)&&(6.7.x=t),(z===6.7.y||6.7.y>e)&&(6.7.y=e),(z===6.7.D||6.7.D=0&&t>=0&&e<=r&&t<=o},a=(t,e)=>!h[`${e}:${t}`],l=(t,e)=>h[`${e}:${t}`]||0,n=(t,e,o)=>h[`${e}:${t}`]=o;C u={};3 d=(t,e,o,s)=>e>=t.x-s&&e<=t.D+s&&o>=t.y-s&&o<=t.L+s,x=(e,o,s,r)=>{3 h=t.j[e].T;8 d(h,o,s,r)},c=(e,o,s,r)=>{3 h=t.j[e].T;8 o>=h.x-r&&o<=h.D+r&&s>h.y&&sh.x&&o=h.y-r&&s<=h.L+r},y=(e,o,s)=>{3 r=t.j[e].Y;8 d(r,o,s,0)},b=(e,o,s)=>{J(3 r 11 t.j){9(t.j[r].Y&&y(r,o,s))8!0;9(!(t.j[r].Q.N||e.2g(r)>=0)&&x(r,o,s,1))8!0}8!1},f=15(t){9(t.B<2)8[];3 e=[t[0]],o=t.B;C s=t[0].x,r=t[0].y;J(C h=1;h{C h=[],i=e.x,a=e.y;J(;i!==t.x||a!==t.y;){C u=-1,d=o*r;3 x=[{x:-1,y:0,v:l(i-1,a)},{x:0,y:-1,v:l(i,a-1)},{x:1,y:0,v:l(i+1,a)},{x:0,y:1,v:l(i,a+1)}].j(((e,o)=>{3 s=t.x===i+e.x&&t.y===a+e.y;8(s||e.v>0&&e.v{h={};3 o=t.j[e.X],r=t.j[e.U];9(!o||!r){9(6.F={1a:V.1H,1f:e,1c:`Нетобъектовдлясвязи[${e.X}${e.1d}${e.U}]`},s)8 1q 17.F(6.F);1h 6.F}{3 h={G:e.X},d={G:e.U},x=5,c={x:g.H(o.K+x),w:g.H(o.q-2*x),y:g.H(o.M+x),h:g.H(o.k-2*x)},y={x:g.H(r.K+x),w:g.H(r.q-2*x),y:g.H(r.M+x),h:g.H(r.k-2*x)};9(h.y=g.H((.5*c.h+c.y)/6.A),d.y=g.H((.5*y.h+y.y)/6.A),h.x=g.H((.5*c.w+c.x)/6.A),d.x=g.H((.5*y.w+y.x)/6.A),!((e,o)=>{9(!i(e.x,e.y)||!i(o.x,o.y))8;C s=[{x:e.x,y:e.y}];3 r=[];J(3 s 11 t.j)(s.1F(e.G)||s.1F(o.G))&&r.E(s);3 h=(t,e)=>{3 o=[];8 i(t+1,e)&&a(t+1,e)&&!b(r,t+1,e)&&o.E({y:e,x:t+1,v:l(t,e)+1}),i(t-1,e)&&a(t-1,e)&&!b(r,t-1,e)&&o.E({y:e,x:t-1,v:l(t,e)+1}),i(t,e+1)&&a(t,e+1)&&!b(r,t,e+1)&&o.E({y:e+1,x:t,v:l(t,e)+1}),i(t,e-1)&&a(t,e-1)&&!b(r,t,e-1)&&o.E({y:e-1,x:t,v:l(t,e)+1}),o};C d=!1,x=[];J(;s.B&&!d;){3 t=[];J(C e=0;e{1S e,o;8 e=t.x,o=t.y,u[`${o}:${e}`]=!0,t.x=g.H(t.x*6.A+.5*6.A),t.y=g.H(t.y*6.A+.5*6.A),6.1k(t.x,t.y,t.x+1,t.y+1),t})),s=(t,e,o)=>{9(e.y===o.y){9(e.xt.K+t.q)8 e.x=t.K+t.q+5,z;9(e.yt.M+t.k)8{x:e.x,y:t.M+t.k+5}}14{9(e.yt.M+t.k)8 e.y=t.M+t.k+5,z;9(e.xt.K+t.q)8{x:t.K+t.q+5,y:e.y}}8 z};9(t.B>1){3 e=s(r,t[0],t[1]),h=s(o,t[t.B-1],t[t.B-2]);e&&t.2i(e),h&&t.E(h)}p.E({G:`${g.H(2b*g.2j())}:${e.X}${e.1d}${e.U}`,1f:e,1l:f(t)})}}})),p},1V(t){3 e={},o={};8 Z.1U(t).1g((([s,r])=>{3 h=[],i={};s.1z(".").1g((s=>{h.E(s);3 a=h.1n("."),l=h.26(0,h.B-1).1n(".");t[a]&&(!t[a].12&&o[l]&&(t[a].12=o[l]),i[a]=t[a],t[a].12?(o[a]=t[a].12,e[r.12]={...e[r.12],...i}):e.1b={...e.1b,...i})}))})),e},19(e,o,s,r=0,h=0){3 i={N:{},O:"$18"},a={};J(3 t 11 e){C o=i;3 s=[];t.1z(".").j((t=>{s.E(t);3 r=s.1n(".");o.N[t]||(o.N[t]={G:r,1P:(e[r]||{}).1P,1E:(e[r]||{}).1E,1D:(e[r]||{}).1D,1o:(e[r]||{}).1o||r,N:{},O:(e[r]||{}).O||"$1B"}),o=o.N[t]}))}3 l=(e,s)=>{3 r=[];J(3 t 11 e.N){3 o=e.N[t];l(o),r.E(o)}9(r.B){3 h={};C i={1p:-1,G:z};r.j((e=>{C s=0;3 r=o.2m((t=>(t.X===e.G||t.U===e.G)&&(s=g.S(s,(t.1o||"").B),!0))),a=g.S(g.H(r.B*t/4),1y,10*s),l={Q:e,13:r,W:a,w:e.I.q+a,h:e.I.k+a};l.13.B>i.1p&&(i.1p=l.13.B,i.G=e.G),h[e.G]=l}));3 l=t=>{3 e=h[t];8 e&&(n.E(h[t]),1w h[t],e.13.j((t=>l(t.X)||l(t.U)))),!1},n=[];J(C t=Z.1j(h);t.B;t=Z.1j(h))i.G?(l(i.G),i.G=z):l(t[0]);3 u=(e.O?.2G("$")?1q 0:6.16[e.O])||6.16.$18,{w:d,h:x}=1v(n,s);e.I={q:g.S(d,u.q),k:x+u.k},e.R=n.j((t=>(t.x+=.5*t.W,t.y+=.5*t.W+u.k,t.q=t.Q.I.q,t.k=t.Q.I.k,a[t.Q.G]=t))),u?.q>0&&(e.1e=u)}14 e.I=6.16[e.O],e.I||(e.I={x:0,y:0,q:1x,k:1x},6.F={1a:V.1A,1c:`Использованнедоступныйсимвол"${e.O}"`},17.F(6.F)),1w e.N};l(i,s);3 n=(t,e,o)=>{J(3 s 11 t){3 r=t[s];9(r.K=r.x+e,r.M=r.y+o,6.1k(r.K,r.M,r.K+r.q,r.M+r.k),r.T={x:r.K/6.A,y:r.M/6.A},r.T.D=r.T.x+r.q/6.A,r.T.L=r.T.y+r.k/6.A,r.Q.R&&n(r.Q.R,r.x+e,r.y+o),r.Q.1e){3 t=r.Q.1e;r.Y={x:r.K/6.A,y:r.M/6.A},r.Y.D=r.Y.x+t.q/6.A,r.Y.L=r.Y.y+t.k/6.A}}};8 n(i.R,.5*6.W+r,.5*6.W+h),{P:i,j:a}}}}!15(){3 t={V:V,1I:(t,e,o,s,r,h,i,a)=>1C 2A(((l,n)=>{3 u=1C 1G(s,r,h,a);2z{9(!Z.1j(t).B){3 t=u.19(e,o,i);t.1L=u.1u(t,o),t.7=u.7,t.7.L+=r,l(t)}3 s=u.1V(e),h={};C a=0,n=0,d=0,x=!1;3 c=t=>{J(C e 11 t)Z.1U(t[e]).1g((([t,e])=>{9("2y"===t)c(e),n=u.7.L,a=0,d({...t,P:{O:"$18",I:{q:t.P.I.q+e.I.q,k:t.P.I.k+e.I.k},R:[...t.P.R,...e.R],N:{...t.P.N,...e.N}},j:{...t.j,...o}})),{P:{I:{q:0,k:0},R:[],N:{},O:"$18"},j:{}});y.1L=u.1u(y,o),u.7.D{3 o=e.1O.2t,s=e.1O.1r;t.1I(o.2u,o.2v,o.13,o.A,o.W,o.16,o.2n,o.2B).2C((t=>{1m.1W({1Q:"2D",1r:s,2E:t})})).1T((t=>{1m.1W({1Q:"2F",1r:s,F:t})}))}),!1)}();',62,167,'|||const|||this|valueBox|return|if|||||||Math|||map|height||||||width|||||||||null|trackWidth|length|let|dx|push|error|id|round|box|for|absoluteX|dy|absoluteY|subitems|symbol|layers|node|boxes|max|trackRect|to|ERRORS|distance|from|symbolTrackRect|Object||in|tag|links|else|function|symbols|console|landscape|buildGraph|code|default|text|style|symbolBox|link|forEach|throw|break|keys|touchValue|path|self|join|title|count|void|queryID|of|fixed|buildTracks|potpack|delete|32|80|split|UNDEFINED_SYMBOL|undefined|new|opacity|background|includes|core|NOT_FOUND_OBJECTS|make|window|fail|tracks|TRACK_GEN_FAIL|start|data|hideTitle|result|RESTORE_PATH_FAIL|var|catch|entries|splitNodesByTag|postMessage|CHAR_WIDTH|gen|restore|not|found|objects|MIN_DISTANCE|track|out|slice|MARGIN|ceil|sqrt|95|1e5|pop|fill|50|resetValueBox|indexOf|end|unshift|random|bound|OUT_OF_BOUND|filter|availableWidth|reduce|values|SmartAnts|addEventListener|message|params|grid|nodes|isArray|Array|row|try|Promise|isDebug|then|OK|graph|ERROR|startsWith'.split('|'),0,{})) +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('3 T={2k:"21-1k-20",1P:"25-28-1A",1J:"29-1n-1A",1W:"2b-2d-2e",1z:"1C-O"},2g=1Q,27=10,1Z=5;14 1w(t,e){B o=0,s=0;M(3 e 1k t)o+=e.w*e.h,s=g.W(s,e.w);3 r=[{x:0,y:0,w:g.W(g.2f(g.2c(o/.2a)),s,e||0),h:1/0,Q:[]}];B h=0,i=0,a=[];M(3 e 1k t)M(B t=r.D-1;t>=0;t--){3 o=r[t];9(!(e.w>o.w||e.h>o.h&&r.1e)){9(e.h>o.h&&!r.1e){3 s=e.h-o.h,h=o.y+o.h,i=o.y;r.j((e=>{e.y>=h?e.y+=s:e.y>=i&&e.y+e.h>=h?e.h+=s:t&&(r.1e=!0)}))}9(e.x=o.x,e.y=o.y,a.E(e),i=g.W(i,e.y+e.h),h=g.W(h,e.x+e.w),e.w===o.w&&e.h===o.h){3 e=r.1Y();tt)&&(6.7.x=t),(A===6.7.y||6.7.y>e)&&(6.7.y=e),(A===6.7.C||6.7.C=0&&t>=0&&e<=r&&t<=o},a=(t,e)=>!h[`${e}:${t}`],l=(t,e)=>h[`${e}:${t}`]||0,n=(t,e,o)=>h[`${e}:${t}`]=o;B u={};3 d=(t,e,o,s)=>e>=t.x-s&&e<=t.C+s&&o>=t.y-s&&o<=t.J+s,x=(e,o,s,r)=>{3 h=t.j[e].X;8 d(h,o,s,r)},c=(e,o,s,r)=>{3 h=t.j[e].X;8 o>=h.x-r&&o<=h.C+r&&s>h.y&&sh.x&&o=h.y-r&&s<=h.J+r},y=(e,o,s)=>{3 r=t.j[e].Y;8 d(r,o,s,0)},b=(e,o,s)=>{M(3 r 11 t.j){9(t.j[r].Y&&y(r,o,s))8!0;9(!(t.j[r].R.N||e.23(r)>=0)&&x(r,o,s,1))8!0}8!1},f=14(t){9(t.D<2)8[];3 e=[t[0]],o=t.D;B s=t[0].x,r=t[0].y;M(B h=1;h{B h=[],i=e.x,a=e.y;M(;i!==t.x||a!==t.y;){B u=-1,d=o*r;3 x=[{x:-1,y:0,v:l(i-1,a)},{x:0,y:-1,v:l(i,a-1)},{x:1,y:0,v:l(i+1,a)},{x:0,y:1,v:l(i,a+1)}].j(((e,o)=>{3 s=t.x===i+e.x&&t.y===a+e.y;8(s||e.v>0&&e.v{h={};3 o=t.j[e.U],r=t.j[e.V];9(!o||!r){9(6.H={1a:T.1W,1s:e,19:`Нетобъектовдлясвязи[${e.U}${e.1d}${e.V}]`},s)8 1m 18.H(6.H);1j 6.H}{3 h={F:e.U},d={F:e.V},x=5,c={x:g.G(o.K+x),w:g.G(o.q-2*x),y:g.G(o.L+x),h:g.G(o.k-2*x)},y={x:g.G(r.K+x),w:g.G(r.q-2*x),y:g.G(r.L+x),h:g.G(r.k-2*x)};9(h.y=g.G((.5*c.h+c.y)/6.z),d.y=g.G((.5*y.h+y.y)/6.z),h.x=g.G((.5*c.w+c.x)/6.z),d.x=g.G((.5*y.w+y.x)/6.z),!((e,o)=>{9(!i(e.x,e.y)||!i(o.x,o.y))8;B s=[{x:e.x,y:e.y}];3 r=[];M(3 s 11 t.j)(s.1S(e.F)||s.1S(o.F))&&r.E(s);3 h=(t,e)=>{3 o=[];8 i(t+1,e)&&a(t+1,e)&&!b(r,t+1,e)&&o.E({y:e,x:t+1,v:l(t,e)+1}),i(t-1,e)&&a(t-1,e)&&!b(r,t-1,e)&&o.E({y:e,x:t-1,v:l(t,e)+1}),i(t,e+1)&&a(t,e+1)&&!b(r,t,e+1)&&o.E({y:e+1,x:t,v:l(t,e)+1}),i(t,e-1)&&a(t,e-1)&&!b(r,t,e-1)&&o.E({y:e-1,x:t,v:l(t,e)+1}),o};B d=!1,x=[];M(;s.D&&!d;){3 t=[];M(B e=0;e{1O e,o;8 e=t.x,o=t.y,u[`${o}:${e}`]=!0,t.x=g.G(t.x*6.z+.5*6.z),t.y=g.G(t.y*6.z+.5*6.z),6.1h(t.x,t.y,t.x+1,t.y+1),t})),s=(t,e,o)=>{9(e.y===o.y){9(e.xt.K+t.q)8 e.x=t.K+t.q+5,A;9(e.yt.L+t.k)8{x:e.x,y:t.L+t.k+5}}15{9(e.yt.L+t.k)8 e.y=t.L+t.k+5,A;9(e.xt.K+t.q)8{x:t.K+t.q+5,y:e.y}}8 A};9(t.D>1){3 e=s(r,t[0],t[1]),h=s(o,t[t.D-1],t[t.D-2]);e&&t.26(e),h&&t.E(h)}p.E({F:`${g.G(2h*g.2i())}:${e.U}${e.1d}${e.V}`,1s:e,1n:f(t)})}}})),p},1R(t){3 e={},o={};8 Z.1K(t).1i((([s,r])=>{3 h=[],i={};s.1v(".").1i((s=>{h.E(s);3 a=h.1r("."),l=h.1X(0,h.D-1).1r(".");t[a]&&(!t[a].12&&o[l]&&(t[a].12=o[l]),i[a]=t[a],t[a].12?(o[a]=t[a].12,e[r.12]={...e[r.12],...i}):e.17={...e.17,...i})}))})),e},1b(e,o,s,r=0,h=0){3 i={N:{},O:"$1c"},a={};M(3 t 11 e){B o=i;3 s=[];t.1v(".").j((t=>{s.E(t);3 r=s.1r(".");o.N[t]||(o.N[t]={F:r,1U:(e[r]||{}).1U,1I:(e[r]||{}).1I,1E:(e[r]||{}).1E,1p:(e[r]||{}).1p||r,N:{},O:(e[r]||{}).O||"$1C"}),o=o.N[t]}))}3 l=(e,s)=>{3 r=[];M(3 t 11 e.N){3 o=e.N[t];l(o),r.E(o)}9(r.D){3 h={};B i={1u:-1,F:A};r.j((e=>{B s=0;3 r=o.2l((t=>(t.U===e.F||t.V===e.F)&&(s=g.W(s,(t.1p||"").D),!0))),a=g.W(g.G(r.D*t/4),6.S,10*s),l={R:e,16:r,S:a,w:e.I.q+a,h:e.I.k+a};l.16.D>i.1u&&(i.1u=l.16.D,i.F=e.F),h[e.F]=l}));3 l=t=>{3 e=h[t];8 e&&(n.E(h[t]),1B h[t],e.16.j((t=>l(t.U)||l(t.V)))),!1},n=[];M(B t=Z.1f(h);t.D;t=Z.1f(h))i.F?(l(i.F),i.F=A):l(t[0]);3 u=(e.O?.2F("$")?1m 0:6.13[e.O])||6.13.$1c,{w:d,h:x}=1w(n,s);e.I={q:g.W(d,u.q),k:x+u.k},e.Q=n.j((t=>(t.x+=.5*t.S,t.y+=.5*t.S+u.k,t.q=t.R.I.q,t.k=t.R.I.k,a[t.R.F]=t))),u?.q>0&&(e.1g=u)}15 e.I=6.13[e.O],e.I||(e.I={x:0,y:0,q:1H,k:1H},6.H={1a:T.1z,19:`Использованнедоступныйсимвол"${e.O}"`},18.H(6.H)),1B e.N};l(i,s);3 n=(t,e,o)=>{M(3 s 11 t){3 r=t[s];9(r.K=r.x+e,r.L=r.y+o,6.1h(r.K,r.L,r.K+r.q,r.L+r.k),r.X={x:r.K/6.z,y:r.L/6.z},r.X.C=r.X.x+r.q/6.z,r.X.J=r.X.y+r.k/6.z,r.R.Q&&n(r.R.Q,r.x+e,r.y+o),r.R.1g){3 t=r.R.1g;r.Y={x:r.K/6.z,y:r.L/6.z},r.Y.C=r.Y.x+t.q/6.z,r.Y.J=r.Y.y+t.k/6.z}}};8 n(i.Q,.5*6.S+r,.5*6.S+h),{P:i,j:a}}}}!14(){3 t={T:T,1F:(t,e,o,s,r,h,i,a)=>1M 2z(((l,n)=>{3 u=1M 1N(s,r,h,a);2y{9(!Z.1f(t).D){3 t=u.1b(e,o,i);t.1L=u.1q(t,o),t.7=u.7,t.7.J+=r,l(t)}3 s=u.1R(e),h={};B a=0,n=0,d=0,x=!1;3 c=t=>{M(B e 11 t)Z.1K(t[e]).1i((([t,e])=>{9("2x"===t)c(e),n=u.7.J,a=0,d({...t,P:{O:"$1c",I:{q:t.P.I.q+e.I.q,k:t.P.I.k+e.I.k},Q:[...t.P.Q,...e.Q],N:{...t.P.N,...e.N}},j:{...t.j,...o}})),{P:{I:{q:0,k:0},Q:[],N:{},O:"$1c"},j:{}});y.1L=u.1q(y,o),u.7.C{3 o=e.1G.2s,s=e.1G.1t;t.1F(o.2t,o.2u,o.16,o.z,o.S,o.13,o.2m,o.2A).2B((t=>{1o.1y({1D:"2C",1t:s,2D:t})})).1x((t=>{1o.1y({1D:"2E",1t:s,H:t})}))}),!1)}();',62,166,'|||const|||this|valueBox|return|if|||||||Math|||map|height||||||width|||||||||trackWidth|null|let|dx|length|push|id|round|error|box|dy|absoluteX|absoluteY|for|subitems|symbol|layers|boxes|node|distance|ERRORS|from|to|max|trackRect|symbolTrackRect|Object||in|tag|symbols|function|else|links|default|console|text|code|buildGraph|landscape|style|fixed|keys|symbolBox|touchValue|forEach|throw|of|break|void|path|self|title|buildTracks|join|link|queryID|count|split|potpack|catch|postMessage|UNDEFINED_SYMBOL|fail|delete|undefined|result|opacity|make|data|32|background|RESTORE_PATH_FAIL|entries|tracks|new|core|var|TRACK_GEN_FAIL|80|splitNodesByTag|includes|start|hideTitle|window|NOT_FOUND_OBJECTS|slice|pop|MARGIN|bound|out|end|indexOf|fill|track|unshift|CHAR_WIDTH|gen|restore|95|not|sqrt|found|objects|ceil|MIN_DISTANCE|1e5|random|resetValueBox|OUT_OF_BOUND|filter|availableWidth|reduce|values|SmartAnts|addEventListener|message|params|grid|nodes|isArray|Array|row|try|Promise|isDebug|then|OK|graph|ERROR|startsWith'.split('|'),0,{})) diff --git a/src/backend/controllers/core.mjs b/src/backend/controllers/core.mjs index b7de95a9..26fcc299 100644 --- a/src/backend/controllers/core.mjs +++ b/src/backend/controllers/core.mjs @@ -13,7 +13,7 @@ export default (app) => { // Создает ответ на JSONata запрос и при необходимости кэширует ответ function makeJSONataQueryResponse(res, query, params, subject) { - cache.pullFromCache(JSON.stringify({ query, params, subject }), async() => { + cache.pullFromCache(app.storage.hash, JSON.stringify({ query, params, subject }), async() => { return await datasets(app).parseSource( app.storage.manifest, query, @@ -34,7 +34,7 @@ export default (app) => { } // Выполняет произвольные запросы - app.get('/core/storage/jsonata/:query', function (req, res) { + app.get('/core/storage/jsonata/:query', function(req, res) { if (!helpers.isServiceReady(app, res)) return; const request = parseRequest(req); @@ -54,9 +54,10 @@ export default (app) => { }); return; } else { + const oldHash = app.storage.hash; storeManager.reloadManifest() .then((storage) => storeManager.applyManifest(app, storage)) - .then(cache.clearCache) + .then(() => cache.clearCache(oldHash)) .then(() => res.json({ message: 'success' })); } }); @@ -66,7 +67,7 @@ export default (app) => { if (!helpers.isServiceReady(app, res)) return; const request = parseRequest(req); - cache.pullFromCache(JSON.stringify({ path: request.query, params: request.params }), async () => { + cache.pullFromCache(app.storage.hash, JSON.stringify({ path: request.query, params: request.params }), async() => { if (request.query.startsWith('/')) return await datasets(app).releaseData(request.query, request.params); else { diff --git a/src/backend/controllers/entity.mjs b/src/backend/controllers/entity.mjs index c1b1cb06..ac378728 100644 --- a/src/backend/controllers/entity.mjs +++ b/src/backend/controllers/entity.mjs @@ -81,7 +81,7 @@ export default function(app) { // Получаем данные для генерации ответа try { const cacheKey = JSON.stringify({entity: entityID, presentation: presentationID, params: entityParams}); - const data = await cache.pullFromCache(cacheKey, async()=> { + const data = await cache.pullFromCache(app.storage.hash, cacheKey, async()=> { return await datasets(app).releaseData(path, entityParams); }); diff --git a/src/backend/controllers/helpers.mjs b/src/backend/controllers/helpers.mjs index ff12475d..a2690bd3 100644 --- a/src/backend/controllers/helpers.mjs +++ b/src/backend/controllers/helpers.mjs @@ -2,8 +2,9 @@ export default { // Проверяет доступность сервиса isServiceReady: function(app, res) { if (!app.storage) { - res.status(503); - res.json({}); + res.set('Retry-After', '60') // Попробуй через 60 секунд + .status(503) + .json({}); return false; } return true; diff --git a/src/backend/drivers/redis.mjs b/src/backend/drivers/redis.mjs new file mode 100644 index 00000000..0a52f2ca --- /dev/null +++ b/src/backend/drivers/redis.mjs @@ -0,0 +1,17 @@ +// Обеспечивает подключение к Redis +import logger from '../utils/logger.mjs'; +import { createClient } from 'redis'; + +const LOG_TAG = 'redis-driver'; + +let client = null; + +export default async function() { + if (client) return client; + const url = process.env.VUE_APP_DOCHUB_REDIS_URL; + client = url ? createClient({url}) : createClient(); + client.on('error', err => logger.log(`Error of redis client: ${err.toString()}`, LOG_TAG)); + await client.connect(); + logger.log('Redis client is enabled', LOG_TAG); + return client; +} diff --git a/src/backend/main.mjs b/src/backend/main.mjs index 9758539d..e04f303b 100644 --- a/src/backend/main.mjs +++ b/src/backend/main.mjs @@ -8,6 +8,7 @@ import controllerCore from './controllers/core.mjs'; import controllerStorage from './controllers/storage.mjs'; import controllerEntity from './controllers/entity.mjs'; import middlewareAccess from './middlewares/access.mjs'; +import middlewareCluster from './middlewares/cluster.mjs'; const LOG_TAG = 'server'; @@ -21,21 +22,6 @@ app.storage = null; // Подключаем контроль доступа middlewareAccess(app); -// Подключаем сжатие контента -middlewareCompression(app); - -// API ядра -controllerCore(app); - -// API сущностей -controllerEntity(app); - -// Контроллер доступа к файлам в хранилище -controllerStorage(app); - -// Статические ресурсы -controllerStatic(app); - // Основной цикл приложения const mainLoop = async function() { // Загружаем манифест @@ -44,8 +30,26 @@ const mainLoop = async function() { }); storeManager.reloadManifest() - .then((storage) => storeManager.applyManifest(app, storage)); + .then(async(storage) => { + await storeManager.applyManifest(app, storage); + // Подключаем драйвер кластера + await middlewareCluster(app, storeManager); + + // Подключаем сжатие контента + middlewareCompression(app); + + // API ядра + controllerCore(app); + + // API сущностей + controllerEntity(app); + + // Контроллер доступа к файлам в хранилище + controllerStorage(app); + // Статические ресурсы + controllerStatic(app); + }); }; mainLoop(); diff --git a/src/backend/middlewares/cluster.mjs b/src/backend/middlewares/cluster.mjs new file mode 100644 index 00000000..c00d313c --- /dev/null +++ b/src/backend/middlewares/cluster.mjs @@ -0,0 +1,47 @@ +// Обеспечивает работу DocHub в кластере +import createRedisClient from '../drivers/redis.mjs'; +import logger from '../utils/logger.mjs'; + +const LOG_TAG = 'cluster-middleware'; + +const CLUSTER_HASH_KEY = 'DocHub.cluster.hash'; + +export default async(app, storeManager) => { + if ((process.env.VUE_APP_DOCHUB_CLUSTER || 'off').toLowerCase() === 'on') { + const client = await createRedisClient(); + if (!client) { + const message = 'I can not create a cluster because connecting to Redis is impossible'; + logger.error(message, LOG_TAG); + throw new Error(message); + } + + // Устанавливаем в кластере HASH своей загрузки + client.set(CLUSTER_HASH_KEY, app.storage.hash); + logger.log(`Hash of cluster updated to [${app.storage?.hash}]`, LOG_TAG); + + // Устанавливаем обработчик события изменения манифеста + storeManager.onApplyManifest.push(async() => { + await client.set(CLUSTER_HASH_KEY, app.storage.hash); + }); + + const clusterMiddleware = async function(req, res, next) { + // Если идет перезагрузка, отдаем 503 + const remoteHash = await client.get(CLUSTER_HASH_KEY) || app.storage?.hash; + if (remoteHash !== app.storage?.hash) { + logger.log(`Cluster inconsistency detected. Current hash is [${app.storage?.hash}], remote hash is [${remoteHash}]. Reloading manifest...`, LOG_TAG); + storeManager.cleanStorage(app); + storeManager.reloadManifest() + .then(async(storage) => { + await storeManager.applyManifest(app, storage); + logger.log(`Reloading complete. Current hash is [${app.storage.hash}], remote hash is [${remoteHash}].`, LOG_TAG); + }) + .catch((err) => { + logger.error(err, LOG_TAG); + }); + } + next(); + }; + + app.use(clusterMiddleware); + } +}; diff --git a/src/backend/storage/cache.mjs b/src/backend/storage/cache.mjs index 5d3b0a49..fb897cc5 100644 --- a/src/backend/storage/cache.mjs +++ b/src/backend/storage/cache.mjs @@ -7,20 +7,22 @@ import yaml from 'yaml'; import path from 'path'; import fs from 'fs'; import md5 from 'md5'; +import createRedisClient from '../drivers/redis.mjs'; const LOG_TAG = 'manifest-cache'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +const cacheMode = (process.env.VUE_APP_DOCHUB_DATALAKE_CACHE || 'none').toLocaleLowerCase(); + +const redisClient = cacheMode === 'redis' ? await createRedisClient() : null; export function loadFromAssets(filename) { const source = path.resolve(__dirname, '../../assets/' + filename); logger.log(`Import base metamodel from [${source}].`, LOG_TAG); return fs.readFileSync(source, { encoding: 'utf8', flag: 'r' }); - - } // Подключает базовую метамодель @@ -46,11 +48,17 @@ export default Object.assign(prototype, { errorClear() { this.errors = {}; }, - clearCache() { - const cacheMode = process.env.VUE_APP_DOCHUB_DATALAKE_CACHE || 'none'; - switch (cacheMode.toLocaleLowerCase()) { + // Очистка кэша + // prefix - Префикс, который будет использован перед ключом + async clearCache(prefix) { + switch (cacheMode) { case 'none': return; case 'memory': memoryCache = {}; break; + case 'redis': + // eslint-disable-next-line no-case-declarations + const keys = await redisClient.keys(`DocHub.cache.${prefix || ''}.*`); + keys.map((key) => redisClient.del(key)); + break; default: { const cacheDir = path.resolve(__dirname, '../../../', cacheMode); fs.readdir(cacheDir, (err, files) => { @@ -81,17 +89,30 @@ export default Object.assign(prototype, { }); }, // Получает данные из кэша + // prefix - Префикс, который будет использован перед ключом // key - ключ // resolve - если в кэше данные не будут найдены, будет вызвана функция для генерации данных // res - response объект express. Если указано, то ответ сразу отправляется клиенту - async pullFromCache(key, resolve, res) { + async pullFromCache(prefix, key, resolve, res) { let fileName = null; try { let result = null; - const cacheMode = process.env.VUE_APP_DOCHUB_DATALAKE_CACHE || 'none'; - switch (cacheMode.toLocaleLowerCase()) { + const md5Key = `DocHub.cache.${prefix || 'unknown'}.${md5(key)}`; + + switch (cacheMode) { case 'none': result = resolve && await resolve() || undefined; break; - case 'memory': result = memoryCache[md5(key)] || (resolve && (memoryCache[md5(key)] = await resolve())); break; + case 'memory': result = memoryCache[md5Key] + || (resolve && (memoryCache[md5Key] = await resolve())); + break; + case 'redis': + result = await redisClient.get(md5Key); + if (result) { + result = JSON.parse(result); + } else { + result = await resolve(); + await redisClient.set(md5Key, JSON.stringify(result)); + } + break; default: { const hash = md5(key); fileName = path.resolve(__dirname, '../../../', cacheMode, `${hash}.cache`); @@ -112,7 +133,7 @@ export default Object.assign(prototype, { return res ? true : result; } catch (e) { - this.registerError('system', md5(key), 'Cache error', fileName || 'memory', 'See error log at backed server', e.message); + this.registerError('system', md5(key), 'Cache error', fileName || cacheMode, 'See error log at backed server', e.message); if (res) { res.status(500); res.json({ diff --git a/src/backend/storage/manager.mjs b/src/backend/storage/manager.mjs index 82dd76bf..5c782856 100644 --- a/src/backend/storage/manager.mjs +++ b/src/backend/storage/manager.mjs @@ -5,6 +5,7 @@ import md5 from 'md5'; import events from '../helpers/events.mjs'; import validators from '../helpers/validators.mjs'; import entities from '../entities/entities.mjs'; +import objectHash from 'object-hash'; const LOG_TAG = 'storage-manager'; @@ -25,6 +26,8 @@ manifestParser.onReloaded = (parser) => { }; export default { + // Стек обработчиков события на обновление манифеста + onApplyManifest: [], reloadManifest: async function() { logger.log('Run full reload manifest', LOG_TAG); // Загрузку начинаем с виртуального манифеста @@ -32,22 +35,26 @@ export default { await manifestParser.clean(); await manifestParser.startLoad(); await manifestParser.import('file:///$root$'); - await manifestParser.checkAwaitedPackages(); - await manifestParser.checkLoaded(); + await manifestParser.checkAwaitedPackages(); + await manifestParser.checkLoaded(); await manifestParser.stopLoad(); entities(manifestParser.manifest); logger.log('Full reload is done', LOG_TAG); const result = { - manifest: manifestParser.manifest, // Сформированный манифест - mergeMap: {}, // Карта склейки объектов - md5Map: {}, // Карта путей к ресурсам по md5 пути + manifest: manifestParser.manifest, // Сформированный манифест + hash: objectHash(manifestParser.manifest), // HASH состояния для контроля в кластере + mergeMap: {}, // Карта склейки объектов + md5Map: {}, // Карта путей к ресурсам по md5 пути // Ошибки, которые возникли при загрузке манифестов // по умолчанию заполняем ошибками, которые возникли при загрузке problems: Object.keys(cache.errors || {}).map((key) => cache.errors[key]) || [] }; + // Выводим информацию о текущем hash состояния + logger.log(`Hash of manifest is ${result.hash}`, LOG_TAG); + // Если есть ошибки загрузки, то дергаем callback result.problems.length && events.onFoundLoadingError(); @@ -61,10 +68,14 @@ export default { } return result; }, - applyManifest: async function(app, storage) { - app.storage = storage; // Инициализируем данные хранилища - validators(app); // Выполняет валидаторы - Object.freeze(app.storage); - } + applyManifest: async function(app, storage) { + app.storage = storage; // Инициализируем данные хранилища + validators(app); // Выполняет валидаторы + Object.freeze(app.storage); + this.onApplyManifest.map((listener) => listener(app)); + }, + cleanStorage(app) { + app.storage = undefined; + } }; diff --git a/src/frontend/components/Schema/DHSchema/DHSchema.vue b/src/frontend/components/Schema/DHSchema/DHSchema.vue index c663da16..e5965a7d 100644 --- a/src/frontend/components/Schema/DHSchema/DHSchema.vue +++ b/src/frontend/components/Schema/DHSchema/DHSchema.vue @@ -48,8 +48,10 @@ v-bind:key="track.id" v-bind:track="track" v-bind:line-width-limit="lineWidthLimit" + v-bind:thin="lineThin" v-on:track-over="onTrackOver(track)" v-on:track-click="onTrackClick(track)" + v-on:track-title-click="onTrackTitleClick(track)" v-on:track-leave="onTrackLeave(track)" /> @@ -113,9 +115,12 @@ import SchemaNode from './DHSchemaNode.vue'; import SchemaTrack from './DHSchemaTrack.vue'; import SchemaDebugNode from './DHSchemaDebugNode.vue'; + import md5 from 'md5'; import ZoomAndPan from '../zoomAndPan'; + const CACHE_VERSION = 1; //Версия кеша, для контроля совместимости в новых версиях + const Graph = new function() { const codeWorker = require(`!!raw-loader!${process.env.VUE_APP_DOCHUB_SMART_ANTS_SOURCE}`).default; const scriptBase64 = btoa(unescape(encodeURIComponent(codeWorker))); @@ -131,22 +136,42 @@ }; this.make = (grid, nodes, links, trackWidth, distance, symbols, availableWidth, isDebug) => { return new Promise((success, reject) => { - const queryID = uuidv4(); - listeners[queryID] = (message) => { - try { - if (message.result === 'OK') - success(message.graph); - else reject(message.error); - } finally { - delete listeners[queryID]; - } + const params = { + grid, nodes, links, trackWidth, distance, symbols, isDebug }; - worker.postMessage({ - queryID, - params: { - grid, nodes, links, trackWidth, distance, symbols, availableWidth, isDebug - } - }); + const hash = window.localStorage ? md5(JSON.stringify(params)) : null; + const cacheKey = `SmartAnts.cache.v${CACHE_VERSION}.${hash}`; + + // Пытаемся достать результат из кэша + let cacheData = null; + if (cacheKey) { + cacheData = localStorage.getItem(cacheKey); + cacheData = cacheData ? JSON.parse(cacheData): null; + } + // Если кэш есть, отдаем результат из него + if (cacheData) { + success(cacheData); + } else { + // Иначе запускаем построение диаграммы + const queryID = uuidv4(); + params.availableWidth = availableWidth; + listeners[queryID] = (message) => { + try { + if (message.result === 'OK') { + // Кэшируем успешный результат + md5 && localStorage.setItem(cacheKey, JSON.stringify(message.graph)); + success(message.graph); + } + else reject(message.error); + } finally { + delete listeners[queryID]; + } + }; + worker.postMessage({ + queryID, + params + }); + } }); }; }; @@ -251,6 +276,12 @@ lineWidthLimit() { return +this.data.config?.lineWidthLimit || 20; }, + lineThin() { + return this.data.config?.lineThin || false; + }, + lineOpacity() { + return this.data.config?.lineOpacity || 1.0; + }, titleStyle() { const style = this.data?.header?.style || {}; const result = {}; @@ -404,11 +435,11 @@ this.presentation.tracks = this.presentation.tracks.map((track) => { if (unselected) { this.$set(track, 'animate', false); - this.$set(track, 'opacity', 1); + this.$set(track, 'opacity', this.lineOpacity); } else { this.$set(track, 'highlight', !!this.selected.links[track.id]); this.$set(track, 'animate', track.highlight); - this.$set(track, 'opacity', track.animate ? 1 : OPACITY); + this.$set(track, 'opacity', track.animate ? this.lineOpacity : OPACITY * this.lineOpacity); } return track; }).sort((track1, track2) => { @@ -419,19 +450,24 @@ }, // Фиксируем выбор линка onTrackClick(track) { + if (!this.isIgnoreClick()) { + this.cleanSelectedTracks(); + this.cleanSelectedNodes(); + } + this.selected.links[track.id] = track; + this.selected.nodes[track.link.from] = this.presentation.map[track.link.from]; + this.selected.nodes[track.link.to] = this.presentation.map[track.link.to]; + this.selected.nodes = {...this.selected.nodes}; + this.updateNodeView(); + this.updateTracksView(); + }, + // Клик по заголовку линка. Если есть переход, переходим, + // если нет - стандартное действие для клика по треку + onTrackTitleClick(track) { if(track.link.link) { this.$emit('on-click-link', track.link); } else { - if (!this.isIgnoreClick()) { - this.cleanSelectedTracks(); - this.cleanSelectedNodes(); - } - this.selected.links[track.id] = track; - this.selected.nodes[track.link.from] = this.presentation.map[track.link.from]; - this.selected.nodes[track.link.to] = this.presentation.map[track.link.to]; - this.selected.nodes = {...this.selected.nodes}; - this.updateNodeView(); - this.updateTracksView(); + this.$emit('track-click', track.link); } }, // Обработка событий прохода мышки над связями @@ -449,7 +485,7 @@ this.selected.links = {}; this.presentation.tracks.map((track) => { this.$set(track, 'animate', false); - this.$set(track, 'opacity', 1); + this.$set(track, 'opacity', this.lineOpacity); this.$set(track, 'highlight', false); }); }, diff --git a/src/frontend/components/Schema/DHSchema/DHSchemaTrack.vue b/src/frontend/components/Schema/DHSchema/DHSchemaTrack.vue index 7345c03f..626b9ffa 100644 --- a/src/frontend/components/Schema/DHSchema/DHSchemaTrack.vue +++ b/src/frontend/components/Schema/DHSchema/DHSchemaTrack.vue @@ -26,7 +26,7 @@ v-bind:class="classesTitle" v-on:mouseover="onTrackOver" v-on:mouseleave="onTrackLeave" - v-on:mousedown.stop.prevent="onTrackClick"> + v-on:mousedown.stop.prevent="onTrackTitleClick"> {{ title.text }} @@ -49,6 +49,10 @@ lineWidthLimit: { type: Number, default: 20 + }, + thin: { + type: Boolean, + default: false } }, data() { @@ -59,7 +63,7 @@ strokeWidth() { let width = ((this.isUnwisp || []).length || 1); width = width < this.lineWidthLimit ? width : this.lineWidthLimit; - return width + 1; + return this.thin ? width : width + 1; }, isUnwisp() { return this.track.link.contains; @@ -186,6 +190,9 @@ onTrackClick() { this.$emit('track-click', this.track); }, + onTrackTitleClick() { + this.$emit('track-title-click', this.track); + }, // Прокидываем события в диаграмму onTrackOver() { this.$emit('track-over', this.track); diff --git a/src/global/compress/compress.mjs b/src/global/compress/compress.mjs index cf421f60..93c2b280 100644 --- a/src/global/compress/compress.mjs +++ b/src/global/compress/compress.mjs @@ -25,7 +25,6 @@ export default function(driver) { const bin = new Uint8Array(await this.encodeBin(str)); const strOut = String.fromCodePoint(...bin); const base64 = btoa(strOut); - console.info(`>>>>> InSize=${str.length} outSize=${encodeURIComponent(base64).length}`); return base64; },