mega-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sample-simple",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "NODE_ENV=development mega start",
|
|
11
|
+
"start": "NODE_ENV=production mega start",
|
|
12
|
+
"scheduler": "mega scheduler",
|
|
13
|
+
"worker": "mega worker",
|
|
14
|
+
"ws-hub": "mega-ws-hub",
|
|
15
|
+
"dev:all": "NODE_ENV=development concurrently -n server,scheduler,worker -c blue,green,magenta \"mega start\" \"mega scheduler\" \"mega worker\"",
|
|
16
|
+
"test": "mega test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"mega-framework": "file:../.."
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"concurrently": "^9.0.0",
|
|
23
|
+
"vitest": "^2.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { describe, test, expect } from 'vitest'
|
|
3
|
+
import routes from '../../../apps/main/routes/index.js'
|
|
4
|
+
|
|
5
|
+
describe('main app index route', () => {
|
|
6
|
+
test('GET / ๋ฑ๋ก + hello world ๋ฐํ', async () => {
|
|
7
|
+
/** @type {Function|undefined} */
|
|
8
|
+
let handler
|
|
9
|
+
routes({ http: { get: (/** @type {string} */ _p, /** @type {Function} */ h) => void (handler = h) } })
|
|
10
|
+
expect(typeof handler).toBe('function')
|
|
11
|
+
expect(await /** @type {any} */ (handler)({}, {}, {})).toEqual({ message: 'Hello from sample-simple!' })
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaAdapterManager โ ์ ์ญ ์ด๋ํฐ ์ธ์คํด์ค ๋งค๋์ (ADR-102).
|
|
4
|
+
*
|
|
5
|
+
* **๊ธ๋ก๋ฒ ๊ณต์ ๋ชจ๋ธ**(์ค๋ ํ์ ): ์ด๋ํฐ ์ธ์คํด์ค๋ `mega.config.js` ์
|
|
6
|
+
* `services.databases/caches/buses.<globalKey>` ์ **๋ฑ ํ ๋ฒ** ์ ์๋๊ณ , ๋ถํ
์ ๋ณธ ๋งค๋์ ๊ฐ
|
|
7
|
+
* driver ๋ ์ง์คํธ๋ฆฌ๋ก ์ธ์คํด์คํํด ๋ณด๊ดํ๋ค. ์ฌ๋ฌ ์ฑ์ด ๊ฐ์ backend ๋ฅผ ์จ๋ ์ธ์คํด์ค๋ 1๊ฐ
|
|
8
|
+
* (์ฑ์ `app.config.js` ์ `databases: { alias: globalKey }` ๋ก **๋ณ๋ช
์ฐธ์กฐ**๋ง ํจ, ADR-064).
|
|
9
|
+
*
|
|
10
|
+
* registry / mega-health / mega-shutdown ๊ณผ ๊ฐ์ **๋ชจ๋ ๋ ๋ฒจ ์ฑ๊ธํค** ํจํด์ด๋ค โ ์ ์ญ ์์์
|
|
11
|
+
* ํ ๊ตฐ๋ฐ์ ๊ด๋ฆฌํ๋ฏ๋ก import ๋ง์ผ๋ก ์ด๋์๋ ๊ฐ์ ์ธ์คํด์ค๋ฅผ ๋ณธ๋ค.
|
|
12
|
+
*
|
|
13
|
+
* # ๋ถํ
ยท์ข
๋ฃ (08-class-specs ยง3.2, docs/10 DisconnectingAdapters)
|
|
14
|
+
* 1. `buildFromGlobalConfig(global)` โ driver ๋งคํ์ผ๋ก `new AdapterClass(cfg)` (lifecycle 1๋จ๊ณ).
|
|
15
|
+
* ๋์์ LIFO shutdown hook ์ **MegaApp ์์ฑ๋ณด๋ค ๋จผ์ ** ๋ฑ๋กํ๋ค(์๋ "์ข
๋ฃ ์์" ์ฃผ์).
|
|
16
|
+
* 2. `connectAll({ ping })` โ ๋ชจ๋ ์ธ์คํด์ค `connect()` ๋ณ๋ ฌ. ํ๋๋ผ๋ ์คํจํ๋ฉด ๋ถํ
abort(fail-fast)
|
|
17
|
+
* ํ๋, ๊ทธ ์ ์ ์ด๋ฏธ connect ๋ ์ด๋ํฐ๋ฅผ LIFO ์๋ cleanup ํด leak ๋ฐฉ์ง(ADR-102 QA M-A).
|
|
18
|
+
* `ping:true` ๋ฉด connect ํ `healthCheck()` ๊น์ง ๊ฒ์ฆ.
|
|
19
|
+
* 3. `disconnectAll()` โ ๋ฑ๋ก **์ญ์(LIFO)** disconnect. shutdown ์ค์ด๋ฏ๋ก ๊ฐ๋ณ ์คํจ๋ warn ํ ๊ณ์.
|
|
20
|
+
*
|
|
21
|
+
* # `ctx.db/cache/bus` ์์ ๊ด๊ณ
|
|
22
|
+
* `ctx.<domain>(alias)` ๋ ์ฑ ๋ณ๋ช
์ globalKey ๋ก ๋ฐ๊พผ ๋ค {@link get} ์ผ๋ก ๋ณธ ๋งค๋์ ์ ๊ณต์
|
|
23
|
+
* ์ธ์คํด์ค๋ฅผ ๋๋ ค์ค๋ค (src/core/ctx-builder.js). ๋ฑ๋ก ์ ๋ ํค๋ ์ฆ์ throw (silent X).
|
|
24
|
+
*
|
|
25
|
+
* @module adapters/adapter-manager
|
|
26
|
+
*/
|
|
27
|
+
import { MegaConfigError } from '../errors/config-error.js'
|
|
28
|
+
import { MegaShutdown } from '../lib/mega-shutdown.js'
|
|
29
|
+
import { buildAdapterEnvConfig } from '../lib/env-mapper.js'
|
|
30
|
+
import * as Registry from './registry.js'
|
|
31
|
+
import { MegaDbAdapter } from './mega-db-adapter.js'
|
|
32
|
+
import { MegaCacheAdapter } from './mega-cache-adapter.js'
|
|
33
|
+
import { MegaBusAdapter } from './mega-bus-adapter.js'
|
|
34
|
+
import { MegaLockAdapter } from './mega-lock-adapter.js'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ctx ๋๋ฉ์ธ โ config ํค โ ๋๋ฉ์ธ ๋ฒ ์ด์ค ๋งคํ (canonical: db/cache/bus + lock, docs/03 ยง581 + ADR-113).
|
|
38
|
+
* `expect` ๋ driver ๊ฐ ์๋ฑํ ๋๋ฉ์ธ์ ๋ฑ๋ก๋๋ ์ฌ๊ณ ๋ฅผ ๋ถํ
์ ์ก๊ธฐ ์ํ instanceof ๊ฒ์ฆ์ฉ (๋๋ฉ์ธ ๋ฒ ์ด์ค).
|
|
39
|
+
* @type {Record<'db'|'cache'|'bus'|'lock', { configKey: 'databases'|'caches'|'buses'|'locks', expect: typeof import('./mega-adapter.js').MegaAdapter }>}
|
|
40
|
+
*/
|
|
41
|
+
const DOMAIN_SPEC = {
|
|
42
|
+
db: { configKey: 'databases', expect: MegaDbAdapter },
|
|
43
|
+
cache: { configKey: 'caches', expect: MegaCacheAdapter },
|
|
44
|
+
bus: { configKey: 'buses', expect: MegaBusAdapter },
|
|
45
|
+
lock: { configKey: 'locks', expect: MegaLockAdapter },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** shutdown hook ์ด๋ฆ โ ์ฌ๋น๋/๋ฆฌ์
์ ์ค๋ณต ๋ฑ๋ก ๋ฐฉ์ง์ฉ ๊ณ ์ ํค. */
|
|
49
|
+
const SHUTDOWN_HOOK = 'adapters:disconnect'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {object} AdapterEntry
|
|
53
|
+
* @property {'db'|'cache'|'bus'|'lock'} domain
|
|
54
|
+
* @property {string} key - globalKey (services.<configKey>.<key>).
|
|
55
|
+
* @property {string} driver
|
|
56
|
+
* @property {import('./mega-adapter.js').MegaAdapter} adapter
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/** @type {Map<string, AdapterEntry>} `${domain}:${key}` โ entry. */
|
|
60
|
+
let instances = new Map()
|
|
61
|
+
/** @type {string[]} ์ฝ์
(=connect) ์์ โ disconnect LIFO ์ฉ. */
|
|
62
|
+
let order = []
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `${domain}:${key}` ํฉ์ฑ id.
|
|
66
|
+
* @param {string} domain @param {string} key @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function idOf(domain, key) {
|
|
69
|
+
return `${domain}:${key}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ์ ์ญ config(`services.databases/caches/buses`)๋ก๋ถํฐ ์ด๋ํฐ ์ธ์คํด์ค๋ฅผ ๋ง๋ ๋ค (lifecycle 1๋จ๊ณ).
|
|
74
|
+
*
|
|
75
|
+
* ๊ฐ ๋๋ฉ์ธ์ `<globalKey>: { driver, ...opts }` ๋ง๋ค `registry.resolve(driver)` โ `new Cls(opts)`.
|
|
76
|
+
* driver ๋ฏธ๋ฑ๋ก(๋นํธ์ธ์ import ์ ์๊ธฐ๋ฑ๋ก)ยท๋ฏธ์ง์ ยท๋๋ฉ์ธ ๋ถ์ผ์น๋ ์ฆ์ throw (๋ถํ
abort, fail-fast).
|
|
77
|
+
*
|
|
78
|
+
* # ์ข
๋ฃ ์์ (์ค์)
|
|
79
|
+
* ๋ณธ ํจ์๊ฐ LIFO shutdown hook ์ ๋ฑ๋กํ๋ฏ๋ก, **MegaApp ์์ฑ๋ณด๋ค ๋จผ์ ** ํธ์ถํด์ผ ํ๋ค. MegaShutdown
|
|
80
|
+
* ์ LIFO(๋์ค ๋ฑ๋ก์ด ๋จผ์ ์คํ) โ ์ด๋ํฐ hook ์ ์ฑ hook ๋ณด๋ค ๋จผ์ ๋ฑ๋กํ๋ฉด, ์ข
๋ฃ ์ ์ฑ(HTTP/WS)์ด
|
|
81
|
+
* ๋จผ์ ๋ซํ๊ณ ๊ทธ ๋ค์ ์ด๋ํฐ๊ฐ ๋๊ธด๋ค(docs/10: ClosingHttp โ โฆ โ DisconnectingAdapters).
|
|
82
|
+
*
|
|
83
|
+
* @param {{ services?: Record<string, any> }} [globalConfig] - mega.config.js (default export).
|
|
84
|
+
* @param {{ registerShutdownHook?: boolean }} [opts] - ํ
์คํธ ๊ฒฉ๋ฆฌ์ฉ์ผ๋ก hook ๋ฑ๋ก์ ๋ ์ ์์(๊ธฐ๋ณธ on).
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
export function buildFromGlobalConfig(globalConfig, { registerShutdownHook = true } = {}) {
|
|
88
|
+
const services = globalConfig?.services ?? {}
|
|
89
|
+
for (const [domain, { configKey, expect }] of Object.entries(DOMAIN_SPEC)) {
|
|
90
|
+
const map = services[configKey]
|
|
91
|
+
if (map === undefined || map === null) continue
|
|
92
|
+
if (typeof map !== 'object' || Array.isArray(map)) {
|
|
93
|
+
throw new MegaConfigError(
|
|
94
|
+
'adapter.invalid_services',
|
|
95
|
+
`services.${configKey} must be a plain object of { key: { driver, ... } }.`,
|
|
96
|
+
{ details: { configKey, type: Array.isArray(map) ? 'array' : typeof map } },
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
for (const [key, cfg] of Object.entries(map)) {
|
|
100
|
+
if (!cfg || typeof cfg !== 'object' || typeof (/** @type {any} */ (cfg).driver) !== 'string') {
|
|
101
|
+
throw new MegaConfigError(
|
|
102
|
+
'adapter.missing_driver',
|
|
103
|
+
`services.${configKey}.${key} must specify a string "driver".`,
|
|
104
|
+
{ details: { domain, key } },
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
const driver = /** @type {any} */ (cfg).driver
|
|
108
|
+
const AdapterClass = Registry.resolve(driver) // ๋ฏธ๋ฑ๋ก โ adapter.unknown_driver
|
|
109
|
+
// .env ์๋ ์ฃผ์
(12-factor, ADR-109) โ `envPrefix` ์ง์ ์ MEGA_<PREFIX>_* ๋ฅผ ์ด๋ํฐ ์ต์
์ผ๋ก
|
|
110
|
+
// ๋งคํํด ํ์ผ config ์์ ๋ณํฉ(env ๊ฐ ์ฐ์ ). ๋ฏธ์ง์ ์ด๋ฉด ๋ฌด์(๊ธฐ์กด ๋์ ๋ฌด๋ณ๊ฒฝ).
|
|
111
|
+
const effectiveCfg = mergeEnvConfig(/** @type {Record<string, any>} */ (cfg))
|
|
112
|
+
const adapter = new AdapterClass(effectiveCfg)
|
|
113
|
+
// driver ๊ฐ ์๋ฑํ ๋๋ฉ์ธ ํค์ ๋ค์ด๊ฐ ์ฌ๊ณ (์: redis ๋ฅผ databases ์)๋ฅผ ๋ถํ
์ ์ก๋๋ค.
|
|
114
|
+
if (!(adapter instanceof expect)) {
|
|
115
|
+
throw new MegaConfigError(
|
|
116
|
+
'adapter.domain_mismatch',
|
|
117
|
+
`driver "${driver}" (services.${configKey}.${key}) is not a ${expect.name}.`,
|
|
118
|
+
{ details: { domain, key, driver, expected: expect.name } },
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
const id = idOf(domain, key)
|
|
122
|
+
instances.set(id, { domain: /** @type {'db'|'cache'|'bus'|'lock'} */ (domain), key, driver, adapter })
|
|
123
|
+
order.push(id)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (registerShutdownHook) {
|
|
128
|
+
// ์ฌ๋น๋ ์์ โ ํญ์ 1๊ฐ๋ง ์ ์ง(MegaApp ์ hublink hook ํจํด๊ณผ ๋์ผ).
|
|
129
|
+
MegaShutdown.unregister(SHUTDOWN_HOOK)
|
|
130
|
+
MegaShutdown.register(SHUTDOWN_HOOK, async () => disconnectAll())
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `envPrefix` ๊ฐ ์์ผ๋ฉด `MEGA_<PREFIX>_*` ํ๊ฒฝ๋ณ์๋ฅผ ์ด๋ํฐ ์ต์
์ผ๋ก ๋งคํํด ํ์ผ config ์์ ๋ณํฉํ๋ค
|
|
136
|
+
* (12-factor, ADR-109). env ๊ฐ ํ์ผ ๊ฐ์ **๋ฎ์ด์ด๋ค**(๋ฐฐํฌ๋ณ ์ค๋ฒ๋ผ์ด๋). pool/options ๋ 1๋จ๊ณ deep-merge.
|
|
137
|
+
* `envPrefix` ํค๋ ์ด๋ํฐ์ ๋๊ธฐ์ง ์๋๋ค(๋๋ผ์ด๋ฒ ๋ฌด๊ด ๋ฉํ).
|
|
138
|
+
*
|
|
139
|
+
* `cfg.driver` ๋ฅผ env-mapper ์ ์ ๋ฌํด OPTIONS_* ํค๋ฅผ driver ํ๊ธฐ(camel/snake)๋ก ๋ณํํ๋ค
|
|
140
|
+
* (camelCase ๋๋ผ์ด๋ฒ์ snake ํค๊ฐ ์กฐ์ฉํ ๋ฌด์๋๋ ๋ฒ๊ทธ ๊ต์ ).
|
|
141
|
+
*
|
|
142
|
+
* @param {Record<string, any>} cfg - services.<configKey>.<key> ์ค์ .
|
|
143
|
+
* @returns {Record<string, any>} ๋ณํฉ๋ config (envPrefix ์์ผ๋ฉด cfg ๊ทธ๋๋ก).
|
|
144
|
+
*/
|
|
145
|
+
function mergeEnvConfig(cfg) {
|
|
146
|
+
const envPrefix = cfg.envPrefix
|
|
147
|
+
if (typeof envPrefix !== 'string' || envPrefix.length === 0) return cfg
|
|
148
|
+
const envCfg = buildAdapterEnvConfig(envPrefix, process.env, { driver: cfg.driver })
|
|
149
|
+
const merged = { ...cfg, ...envCfg }
|
|
150
|
+
if (cfg.pool || envCfg.pool) merged.pool = { ...(cfg.pool ?? {}), ...(envCfg.pool ?? {}) }
|
|
151
|
+
if (cfg.options || envCfg.options) merged.options = { ...(cfg.options ?? {}), ...(envCfg.options ?? {}) }
|
|
152
|
+
delete merged.envPrefix
|
|
153
|
+
return merged
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* ๋๋ฉ์ธ+globalKey ๋ก ๊ณต์ ์ด๋ํฐ ์ธ์คํด์ค ์กฐํ. ์์ผ๋ฉด `adapter.not_registered` throw (silent X).
|
|
158
|
+
*
|
|
159
|
+
* @param {'db'|'cache'|'bus'|'lock'} domain
|
|
160
|
+
* @param {string} key - globalKey.
|
|
161
|
+
* @returns {import('./mega-adapter.js').MegaAdapter}
|
|
162
|
+
*/
|
|
163
|
+
export function get(domain, key) {
|
|
164
|
+
const entry = instances.get(idOf(domain, key))
|
|
165
|
+
if (entry === undefined) {
|
|
166
|
+
throw new MegaConfigError(
|
|
167
|
+
'adapter.not_registered',
|
|
168
|
+
`No ${domain} adapter registered for key "${key}". Registered ${domain}: [${listDomain(domain).join(', ') || '(none)'}].`,
|
|
169
|
+
{ details: { domain, key, registered: listDomain(domain) } },
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
return entry.adapter
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* ๋ฑ๋ก ์ฌ๋ถ (Boolean โ `has*`, ADR-036).
|
|
177
|
+
* @param {'db'|'cache'|'bus'|'lock'} domain @param {string} key @returns {boolean}
|
|
178
|
+
*/
|
|
179
|
+
export function has(domain, key) {
|
|
180
|
+
return instances.has(idOf(domain, key))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* ํ ๋๋ฉ์ธ์ ๋ฑ๋ก๋ globalKey ๋ชฉ๋ก.
|
|
185
|
+
* @param {'db'|'cache'|'bus'|'lock'} domain @returns {string[]}
|
|
186
|
+
*/
|
|
187
|
+
function listDomain(domain) {
|
|
188
|
+
return order.filter((id) => instances.get(id)?.domain === domain).map((id) => /** @type {AdapterEntry} */ (instances.get(id)).key)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* ์ ์ฒด ์ธ์คํด์ค ์ค๋
์ท (๋๋ฒ๊ทธยทํ
์คํธยทํฌ์ค์ฉ). ๋ฑ๋ก ์์๋๋ก.
|
|
193
|
+
* @returns {Array<{ domain: 'db'|'cache'|'bus'|'lock', key: string, driver: string, state: string }>}
|
|
194
|
+
*/
|
|
195
|
+
export function list() {
|
|
196
|
+
return order.map((id) => {
|
|
197
|
+
const e = /** @type {AdapterEntry} */ (instances.get(id))
|
|
198
|
+
return { domain: e.domain, key: e.key, driver: e.driver, state: e.adapter.state }
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* ์ ์ฒด ์ด๋ํฐ ์ํธ๋ฆฌ(์ธ์คํด์ค ํฌํจ)๋ฅผ ๋ฑ๋ก ์์๋๋ก ๋ฐํ โ ์ธ๋ถ ๋ถ์ฐฉ์(`MegaTracing`)๊ฐ
|
|
204
|
+
* ๋ชจ๋ ๊ณต์ ์ด๋ํฐ์ hook ๋ฆฌ์ค๋๋ฅผ ์ผ๊ด ๊ตฌ๋
ํ ๋ ์ด๋ค. {@link list} ์ ๋ฌ๋ฆฌ raw ์ด๋ํฐ ์ธ์คํด์ค๋ฅผ
|
|
205
|
+
* ๋
ธ์ถํ๋ฏ๋ก ๋๋ฒ๊ทธยทํธ๋ ์ด์ฑ ๋ถ์ฐฉ ์ธ ์ฉ๋๋ก๋ ์ ์คํ ์ฌ์ฉ.
|
|
206
|
+
*
|
|
207
|
+
* @returns {AdapterEntry[]}
|
|
208
|
+
*/
|
|
209
|
+
export function entries() {
|
|
210
|
+
return order.map((id) => /** @type {AdapterEntry} */ (instances.get(id)))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* ๋ชจ๋ ์ด๋ํฐ connect (๋ถํ
6๋จ๊ณ). ๋ณ๋ ฌ ์๋ํ๋ **fail-fast + ์๋ cleanup** (ADR-102 QA M-A).
|
|
215
|
+
* ํ๋๋ผ๋ ์คํจํ๋ฉด ๋ถํ
์ abort ํ๊ธฐ ์ํด ์ฒซ ์คํจ ์๋ฌ๋ฅผ throw ํ๋๋ฐ, ๊ทธ ์ ์ ์ด๋ฏธ connect ๋
|
|
216
|
+
* ์ด๋ํฐ๋ฅผ **๋ฑ๋ก ์ญ์(LIFO)์ผ๋ก best-effort disconnect** ํด์ ์ฐ๊ฒฐ ํ leak ์ ๋ง๋๋ค โ ํธ์ถ์๊ฐ
|
|
217
|
+
* catch ์ ํด๋ ๋งค๋์ ๋ ๋ฒจ์์ ์์์ ํ์ํ๋ค(call-site CLI ์ฑ
์์๋ง ์์กดํ์ง ์์).
|
|
218
|
+
*
|
|
219
|
+
* `ping:true` ๋ฉด connect ์งํ `healthCheck()` ๋ฅผ 1ํ ํธ์ถํด ok=false ๋ฉด `adapter.health_failed`
|
|
220
|
+
* throw (ํฌ์ค๊น์ง ๊ฒ์ฆ). ping ์คํจ ์ด๋ํฐ๋ connect ์์ฒด๋ ์ฑ๊ณตํ์ผ๋ฏ๋ก cleanup ๋์์ ํฌํจ๋๋ค.
|
|
221
|
+
*
|
|
222
|
+
* cleanup ์ค disconnect ๊ฐ ์คํจํด๋ ์๋ connect ์คํจ ์๋ฌ๋ฅผ ์ฐ์ ์ ํํ๋ค โ cleanup ์คํจ๋
|
|
223
|
+
* ๋น์น๋ช
์ ์ด๋ผ warn ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ ์ผํจ๋ค(๋ถํ
abort ์ ๋ฆฌ ์ค ํ ์ด๋ํฐ ์ ๋ฆฌ ์คํจ๊ฐ
|
|
224
|
+
* ์ง์ง ์์ธ(connect ์คํจ)์ ๋ฎ์ผ๋ฉด ์ ๋จ).
|
|
225
|
+
*
|
|
226
|
+
* @param {{ logger?: any, ping?: boolean }} [opts]
|
|
227
|
+
* @returns {Promise<void>}
|
|
228
|
+
*/
|
|
229
|
+
export async function connectAll({ logger, ping = false } = {}) {
|
|
230
|
+
/** @type {Set<number>} connect() ์ฑ๊ณตํ ์ด๋ํฐ์ order ์ธ๋ฑ์ค (ping ์ ๊ธฐ์ค โ ping ์คํจ๋ cleanup). */
|
|
231
|
+
const connectedIdx = new Set()
|
|
232
|
+
// canonical ๋ณ๋ ฌ์ด๋ reject ๋ฅผ ์ฆ์ ์ ํํ์ง ์๊ณ allSettled ๋ก ์ ๋ถ ์์ง โ ๋ถ๋ถ ์ฑ๊ณต๋ถ์
|
|
233
|
+
// cleanup ํ ๋ค ์ฒซ ์คํจ๋ฅผ ๋์ง๊ธฐ ์ํจ.
|
|
234
|
+
const results = await Promise.allSettled(
|
|
235
|
+
order.map(async (id, idx) => {
|
|
236
|
+
const e = /** @type {AdapterEntry} */ (instances.get(id))
|
|
237
|
+
await e.adapter.connect()
|
|
238
|
+
connectedIdx.add(idx) // connect ์ฑ๊ณต โ ping ์คํจํ๋๋ผ๋ cleanup ๋์.
|
|
239
|
+
if (ping) {
|
|
240
|
+
const health = await e.adapter.healthCheck()
|
|
241
|
+
if (!health?.ok) {
|
|
242
|
+
throw new MegaConfigError(
|
|
243
|
+
'adapter.health_failed',
|
|
244
|
+
`boot ping failed for ${e.domain}:${e.key} (${e.driver}).`,
|
|
245
|
+
{ details: { domain: e.domain, key: e.key, driver: e.driver, health } },
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
logger?.info?.(
|
|
250
|
+
{ adapter: e.adapter.constructor.name, domain: e.domain, key: e.key, driver: e.driver, state: e.adapter.state },
|
|
251
|
+
'adapter connected',
|
|
252
|
+
)
|
|
253
|
+
}),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const firstRejected = results.find((r) => r.status === 'rejected')
|
|
257
|
+
if (firstRejected === undefined) return // ์ ์ ์ฑ๊ณต
|
|
258
|
+
|
|
259
|
+
// fail-fast cleanup: ์ด๋ฏธ connect ๋ ์ด๋ํฐ๋ฅผ ๋ฑ๋ก ์ญ์(LIFO)์ผ๋ก disconnect (best-effort).
|
|
260
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
261
|
+
if (!connectedIdx.has(i)) continue
|
|
262
|
+
const e = /** @type {AdapterEntry} */ (instances.get(order[i]))
|
|
263
|
+
try {
|
|
264
|
+
await e.adapter.disconnect()
|
|
265
|
+
logger?.debug?.({ domain: e.domain, key: e.key }, 'adapter cleanup disconnected (connectAll abort)')
|
|
266
|
+
} catch (cleanupErr) {
|
|
267
|
+
// cleanup ์คํจ๋ ๋น์น๋ช
์ โ ์๋ณธ connect ์คํจ ์๋ฌ๋ฅผ ์ฐ์ ์ ํํด์ผ ํ๋ฏ๋ก ์ฌ๊ธฐ์ throw ํ์ง ์์.
|
|
268
|
+
logger?.warn?.(
|
|
269
|
+
{ err: cleanupErr, domain: e.domain, key: e.key },
|
|
270
|
+
'adapter cleanup disconnect failed during connectAll abort (continuing, original connect error wins)',
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// ์๋ ๋ถํ
์คํจ ์์ธ(์ฒซ connect/ping ์๋ฌ)์ ๊ทธ๋๋ก ์ ํ โ ํธ์ถ์๋ ๋์ผํ fail-fast ๋์์ ๋ณธ๋ค.
|
|
275
|
+
throw /** @type {PromiseRejectedResult} */ (firstRejected).reason
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* ๋ชจ๋ ์ด๋ํฐ disconnect โ ๋ฑ๋ก **์ญ์(LIFO)** (docs/10 ยง6, 07-sequence-diagrams ยง6). graceful
|
|
280
|
+
* shutdown ์ค์ด๋ฏ๋ก ๊ฐ๋ณ disconnect ์คํจ๋ ๋น์น๋ช
์ : warn ํ ๋ค์ ์ด๋ํฐ๋ก ๊ณ์ํ๋ค( * ํ ์ด๋ํฐ ์ ๋ฆฌ ์คํจ๊ฐ ๋๋จธ์ง ์ ๋ฆฌ๋ฅผ ๋ง์ผ๋ฉด ์ ๋จ).
|
|
281
|
+
*
|
|
282
|
+
* @param {{ logger?: any }} [opts]
|
|
283
|
+
* @returns {Promise<void>}
|
|
284
|
+
*/
|
|
285
|
+
export async function disconnectAll({ logger } = {}) {
|
|
286
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
287
|
+
const e = /** @type {AdapterEntry} */ (instances.get(order[i]))
|
|
288
|
+
try {
|
|
289
|
+
await e.adapter.disconnect()
|
|
290
|
+
logger?.debug?.({ domain: e.domain, key: e.key }, 'adapter disconnected')
|
|
291
|
+
} catch (err) {
|
|
292
|
+
logger?.warn?.({ err, domain: e.domain, key: e.key }, 'adapter disconnect failed (continuing shutdown)')
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* ํ
์คํธ์ฉ reset โ ์ธ์คํด์ค ๋น์ฐ๊ณ shutdown hook ํด์ .
|
|
299
|
+
* @returns {void}
|
|
300
|
+
*/
|
|
301
|
+
export function _reset() {
|
|
302
|
+
instances = new Map()
|
|
303
|
+
order = []
|
|
304
|
+
MegaShutdown.unregister(SHUTDOWN_HOOK)
|
|
305
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* ์ด๋ํฐ ์ต์
ํ์คํ ๊ณต์ฉ ํฌํผ (ADR-109).
|
|
4
|
+
*
|
|
5
|
+
* Postgres/Maria/Mongo 3 ์ด๋ํฐ๊ฐ ๊ณต์ ํ๋ **ํตํฉ ์ต์
๊ตฌ์กฐ**์ ํ์ฑยท๊ฒ์ฆ ๋ก์ง์ ํ ๊ณณ์ ๋ชจ์๋ค.
|
|
6
|
+
* ๊ฐ ์ด๋ํฐ๊ฐ ๊ฐ์ ์๊ทธ๋์ฒ๋ฅผ ๋ฐ๋๋ก ํด 12-factor(.env ์ธ๋ถ ์ฃผ์
) + advanced ์ต์
(pool/options) ์
|
|
7
|
+
* ์ผ๊ด๋๊ฒ ์ง์ํ๋ค.
|
|
8
|
+
*
|
|
9
|
+
* # ํตํฉ ์๊ทธ๋์ฒ
|
|
10
|
+
* ```js
|
|
11
|
+
* {
|
|
12
|
+
* driver, // ๋งค๋์ ๊ฐ ์ฌ์ฉ โ ์ด๋ํฐ๋ ๋ฌด์
|
|
13
|
+
* url? | connectionString?, // ์ฐ๊ฒฐ URL (connectionString ์ deprecated ๋ณ์นญ)
|
|
14
|
+
* host?, port?, user?, password?, // discrete ์ฐ๊ฒฐ (url ๊ณผ ์ํธ ๋ฐฐํ)
|
|
15
|
+
* database? | dbName?, // SQL=database / Mongo=dbName
|
|
16
|
+
* pool?: { min?, max?, idleTimeoutMs?, acquireTimeoutMs?, maxLifetimeMs? }, // ๊ณตํต ํ ์ธํฐํ์ด์ค
|
|
17
|
+
* options?: { ...driver-native passthrough } // ๋๋ผ์ด๋ฒ ํนํ ์ต์
๊ทธ๋๋ก ์ ๋ฌ
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* # ์ฐ๊ฒฐ ๋ชจ๋ ์ถฉ๋ยทํ์ (๋ชจํธํ๋ฉด ๋ถํ
์ fail-fast)
|
|
22
|
+
* - `url` + discrete ์ฐ๊ฒฐํ๋(host/port/user/password[/database]) ๋์ ์ง์ โ `adapter.connection_conflict`.
|
|
23
|
+
* - `url`ยทdiscrete ๋ ๋ค ์์ โ `adapter.connection_required`.
|
|
24
|
+
* - `url` + `pool`/`options` ์กฐํฉ์ **ํ์ฉ** โ url ์ connection ์ ๋ณด๋ง, pool/options ๋ ๋ณ๋ ์ถ.
|
|
25
|
+
*
|
|
26
|
+
* # ๊ณตํต ํ ์ธํฐํ์ด์ค โ ๋๋ผ์ด๋ฒ ํ ์ต์
๋งคํ (๋จ์ ์ฃผ์, ๋๋ผ์ด๋ฒ ํ์
์ง์ ํ์ธ)
|
|
27
|
+
* | ๊ณตํต ํค | pg | mariadb | mongodb |
|
|
28
|
+
* |--------------------|----------------------------|--------------------------|--------------------|
|
|
29
|
+
* | min | min | minimumIdle | minPoolSize |
|
|
30
|
+
* | max | max | connectionLimit | maxPoolSize |
|
|
31
|
+
* | idleTimeoutMs (ms) | idleTimeoutMillis (ms) | idleTimeout (**์ด**) | maxIdleTimeMS (ms) |
|
|
32
|
+
* | acquireTimeoutMs | connectionTimeoutMillis(ms)| acquireTimeout (ms) | waitQueueTimeoutMS |
|
|
33
|
+
* | maxLifetimeMs (ms) | maxLifetimeSeconds (**์ด**)| (๋ฏธ์ง์) | (๋ฏธ์ง์) |
|
|
34
|
+
* `idleTimeoutMs`โmariadbยท`maxLifetimeMs`โpg ๋ msโ์ด ๋ณํ(รท1000, floor). mariadb/mongo ๋
|
|
35
|
+
* ํ maxLifetime ๋๋ฑ๋ฌผ์ด ์์ด `maxLifetimeMs` ์ง์ ์ `adapter.invalid_option` throw(silent ๋ฌด์ X).
|
|
36
|
+
*
|
|
37
|
+
* @module adapters/adapter-options
|
|
38
|
+
*/
|
|
39
|
+
import { MegaValidationError } from '../errors/http-errors.js'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `adapter.invalid_option` ์์ฑ ํฌํผ.
|
|
43
|
+
* @param {string} message @param {Record<string, unknown>} details
|
|
44
|
+
* @returns {MegaValidationError}
|
|
45
|
+
*/
|
|
46
|
+
function invalidOption(message, details) {
|
|
47
|
+
return new MegaValidationError('adapter.invalid_option', message, { details })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ์์ ์ ์(> 0) ๊ฒ์ฆ (undefined ํต๊ณผ). ์๋ฐ ์ `adapter.invalid_option` throw.
|
|
52
|
+
* @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
export function assertPositiveInt(name, value, extra = {}) {
|
|
55
|
+
if (value === undefined) return
|
|
56
|
+
if (!Number.isInteger(value) || /** @type {number} */ (value) <= 0) {
|
|
57
|
+
throw invalidOption(`"${name}" must be a positive integer.`, { option: name, value, ...extra })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ์ ์๋ ์ ์(>= 0) ๊ฒ์ฆ (undefined ํต๊ณผ). 0 ์ ๋๋ผ์ด๋ฒ์์ "๋ฌด์ ํ/์ฆ์/never" ์๋ฏธ๋ก ํ์ฉ.
|
|
63
|
+
* @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
|
|
64
|
+
*/
|
|
65
|
+
export function assertNonNegativeInt(name, value, extra = {}) {
|
|
66
|
+
if (value === undefined) return
|
|
67
|
+
if (!Number.isInteger(value) || /** @type {number} */ (value) < 0) {
|
|
68
|
+
throw invalidOption(`"${name}" must be a non-negative integer.`, { option: name, value, ...extra })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ํ๋ ์ธ ๊ฐ์ฒด ๊ฒ์ฆ (undefined ํต๊ณผ). ๋ฐฐ์ดยทnullยทnon-object ๊ฑฐ๋ถ.
|
|
74
|
+
* @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
|
|
75
|
+
*/
|
|
76
|
+
export function assertPlainObject(name, value, extra = {}) {
|
|
77
|
+
if (value === undefined) return
|
|
78
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
79
|
+
throw invalidOption(`"${name}" must be a plain object.`, { option: name, type: Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value, ...extra })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* ์ฐ๊ฒฐ ๋ชจ๋ ๊ฒฐ์ (url XOR discrete) + ์ถฉ๋ยทํ์ ๊ฒ์ฆ.
|
|
85
|
+
*
|
|
86
|
+
* @param {Record<string, any>} config - ์ด๋ํฐ ์์ฑ์ config.
|
|
87
|
+
* @param {object} opts
|
|
88
|
+
* @param {string} opts.driver - ์๋ฌ details ์ฉ ๋๋ผ์ด๋ฒ๋ช
('postgres'|'mariadb'|'mongodb').
|
|
89
|
+
* @param {string} [opts.dbKey] - discrete db ํค ์ด๋ฆ(SQL='database', ๊ธฐ๋ณธ๊ฐ). Mongo ๋ 'dbName' ์
|
|
90
|
+
* url ๊ณผ ์ถฉ๋์ํค์ง ์์ผ๋ฏ๋ก ๋ณ๋ ์ฒ๋ฆฌ(์๋ dbConflictsWithUrl ์ฐธ์กฐ).
|
|
91
|
+
* @param {boolean} [opts.dbConflictsWithUrl] - db ํค๊ฐ url ๊ณผ ์ํธ ๋ฐฐํ์ธ์ง(SQL=true). Mongo ์ dbName ์
|
|
92
|
+
* url(=connection)๊ณผ ๋ณ๊ฐ(=db ์ ํ)๋ผ false.
|
|
93
|
+
* @returns {{ url?: string, host?: string, port?: number, user?: string, password?: string, database?: string }}
|
|
94
|
+
* url ๋ชจ๋๋ฉด `{ url }`, discrete ๋ชจ๋๋ฉด ์กด์ฌํ๋ ์ฐ๊ฒฐํ๋๋ง. (database ๋ dbConflictsWithUrl=true ์ผ ๋๋ง ํฌํจ.)
|
|
95
|
+
* @throws {MegaValidationError} `adapter.connection_conflict` / `adapter.connection_required` / `adapter.invalid_option`
|
|
96
|
+
*/
|
|
97
|
+
export function resolveConnection(config, { driver, dbKey = 'database', dbConflictsWithUrl = true }) {
|
|
98
|
+
// url ๋๋ deprecated ๋ณ์นญ connectionString.
|
|
99
|
+
const url = config.url ?? config.connectionString
|
|
100
|
+
const hasUrl = url !== undefined && url !== null
|
|
101
|
+
if (hasUrl && (typeof url !== 'string' || url.length === 0)) {
|
|
102
|
+
throw invalidOption(`${driver} "url" (or "connectionString") must be a non-empty string.`, { driver, option: 'url', type: typeof url })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const discreteKeys = ['host', 'port', 'user', 'password']
|
|
106
|
+
if (dbConflictsWithUrl) discreteKeys.push(dbKey)
|
|
107
|
+
const present = discreteKeys.filter((k) => config[k] !== undefined)
|
|
108
|
+
|
|
109
|
+
if (hasUrl && present.length > 0) {
|
|
110
|
+
// url ์ connection ์ ๋ณด๋ฅผ ๋ชจ๋ ๋ด์ผ๋ฏ๋ก discrete ์ ์์ผ๋ฉด ์ด๋ ์ชฝ์ด ์ด๊ธฐ๋์ง ๋ชจํธ โ ๋ช
์ ๊ฑฐ๋ถ.
|
|
111
|
+
throw new MegaValidationError(
|
|
112
|
+
'adapter.connection_conflict',
|
|
113
|
+
`${driver}: "url" cannot be combined with discrete connection fields (${present.join(', ')}). Use one connection mode (pool/options are allowed with either).`,
|
|
114
|
+
{ details: { driver, discreteKeys: present } },
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
if (!hasUrl && present.length === 0) {
|
|
118
|
+
throw new MegaValidationError(
|
|
119
|
+
'adapter.connection_required',
|
|
120
|
+
`${driver}: a "url" or discrete connection fields (host/port/user/password${dbConflictsWithUrl ? '/' + dbKey : ''}) are required.`,
|
|
121
|
+
{ details: { driver } },
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
assertPositiveInt('port', config.port, { driver })
|
|
126
|
+
|
|
127
|
+
/** @type {Record<string, any>} */
|
|
128
|
+
const out = {}
|
|
129
|
+
if (hasUrl) {
|
|
130
|
+
out.url = url
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
for (const k of ['host', 'port', 'user', 'password']) if (config[k] !== undefined) out[k] = config[k]
|
|
134
|
+
if (dbConflictsWithUrl && config[dbKey] !== undefined) out[dbKey] = config[dbKey]
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* pg ํ ๋งคํ โ ๊ฐ์ด null ์ด๋ฉด ๋ฏธ์ง์(throw), `{ key, divideBy? }` ๋ฉด ํค ์ด๋ฆ ๋ณ๊ฒฝ(+๋จ์ ๋ณํ).
|
|
140
|
+
* @type {Record<string, { key: string, divideBy?: number } | null>}
|
|
141
|
+
*/
|
|
142
|
+
export const PG_POOL_SPEC = {
|
|
143
|
+
min: { key: 'min' },
|
|
144
|
+
max: { key: 'max' },
|
|
145
|
+
idleTimeoutMs: { key: 'idleTimeoutMillis' },
|
|
146
|
+
acquireTimeoutMs: { key: 'connectionTimeoutMillis' },
|
|
147
|
+
maxLifetimeMs: { key: 'maxLifetimeSeconds', divideBy: 1000 }, // pg ๋ ์ด ๋จ์
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* mariadb ํ ๋งคํ โ idleTimeout ์ ์ด ๋จ์(msรท1000). maxLifetime ๋๋ฑ๋ฌผ ์์(๋ฏธ์ง์).
|
|
152
|
+
* @type {Record<string, { key: string, divideBy?: number } | null>}
|
|
153
|
+
*/
|
|
154
|
+
export const MARIA_POOL_SPEC = {
|
|
155
|
+
min: { key: 'minimumIdle' },
|
|
156
|
+
max: { key: 'connectionLimit' },
|
|
157
|
+
idleTimeoutMs: { key: 'idleTimeout', divideBy: 1000 }, // mariadb idleTimeout ์ ์ด ๋จ์
|
|
158
|
+
acquireTimeoutMs: { key: 'acquireTimeout' },
|
|
159
|
+
maxLifetimeMs: null, // ๋ฏธ์ง์
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* mongodb ํ ๋งคํ โ ์ ๋ถ ms. maxLifetime ๋๋ฑ๋ฌผ ์์(๋ฏธ์ง์).
|
|
164
|
+
* @type {Record<string, { key: string, divideBy?: number } | null>}
|
|
165
|
+
*/
|
|
166
|
+
export const MONGO_POOL_SPEC = {
|
|
167
|
+
min: { key: 'minPoolSize' },
|
|
168
|
+
max: { key: 'maxPoolSize' },
|
|
169
|
+
idleTimeoutMs: { key: 'maxIdleTimeMS' },
|
|
170
|
+
acquireTimeoutMs: { key: 'waitQueueTimeoutMS' },
|
|
171
|
+
maxLifetimeMs: null, // ๋ฏธ์ง์
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* ๊ณตํต ํ ์ธํฐํ์ด์ค(`{ min, max, idleTimeoutMs, acquireTimeoutMs, maxLifetimeMs }`)๋ฅผ ๋๋ผ์ด๋ฒ
|
|
176
|
+
* ํ ์ต์
๊ฐ์ฒด๋ก ๋ณํยท๊ฒ์ฆํ๋ค.
|
|
177
|
+
*
|
|
178
|
+
* - ์ ์ ์๋ ํ ํค โ `adapter.invalid_option`(์คํ fail-fast).
|
|
179
|
+
* - ๋๋ผ์ด๋ฒ ๋ฏธ์ง์ ํค(spec ๊ฐ null, ์: mariadb/mongo ์ maxLifetimeMs) โ `adapter.invalid_option`(silent ๋ฌด์ X).
|
|
180
|
+
* - `max` ๋ ์์ ์ ์, ๊ทธ ์ธ(minยท*Ms)๋ ์ ์๋ ์ ์.
|
|
181
|
+
* - `divideBy` ๊ฐ ์์ผ๋ฉด msโ์ด ๋ณํ(floor).
|
|
182
|
+
*
|
|
183
|
+
* @param {unknown} pool - config.pool.
|
|
184
|
+
* @param {Record<string, { key: string, divideBy?: number } | null>} spec - ๋๋ผ์ด๋ฒ ํ ๋งคํ.
|
|
185
|
+
* @param {string} driver - ์๋ฌ details ์ฉ ๋๋ผ์ด๋ฒ๋ช
.
|
|
186
|
+
* @returns {Record<string, number>} ๋๋ผ์ด๋ฒ ํ ์ต์
๊ฐ์ฒด(๋น ๊ฐ์ฒด ๊ฐ๋ฅ).
|
|
187
|
+
*/
|
|
188
|
+
export function normalizePool(pool, spec, driver) {
|
|
189
|
+
if (pool === undefined) return {}
|
|
190
|
+
assertPlainObject('pool', pool, { driver })
|
|
191
|
+
/** @type {Record<string, number>} */
|
|
192
|
+
const out = {}
|
|
193
|
+
for (const [key, value] of Object.entries(/** @type {Record<string, unknown>} */ (pool))) {
|
|
194
|
+
if (value === undefined) continue
|
|
195
|
+
const map = spec[key]
|
|
196
|
+
if (map === undefined) {
|
|
197
|
+
throw invalidOption(`${driver}: unknown pool option "${key}". Known: ${Object.keys(spec).join(', ')}.`, { driver, option: `pool.${key}` })
|
|
198
|
+
}
|
|
199
|
+
if (map === null) {
|
|
200
|
+
throw invalidOption(`${driver}: pool.${key} is not supported by the ${driver} driver.`, { driver, option: `pool.${key}` })
|
|
201
|
+
}
|
|
202
|
+
if (key === 'max') assertPositiveInt(`pool.${key}`, value, { driver })
|
|
203
|
+
else assertNonNegativeInt(`pool.${key}`, value, { driver })
|
|
204
|
+
const num = /** @type {number} */ (value)
|
|
205
|
+
out[map.key] = map.divideBy ? Math.floor(num / map.divideBy) : num
|
|
206
|
+
}
|
|
207
|
+
return out
|
|
208
|
+
}
|