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,370 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaNatsAdapter — NATS 메시지 버스 어댑터 (`nats` 공식 driver 래퍼, ADR-112).
|
|
4
|
+
*
|
|
5
|
+
* **첫 bus 도메인 어댑터**(`MegaBusAdapter` 첫 구체). DB/cache 와 달리 pub/sub·req/reply·
|
|
6
|
+
* queue group 메시징 인터페이스를 구현한다.
|
|
7
|
+
*
|
|
8
|
+
* # 표준 표면 (MegaBusAdapter 상속)
|
|
9
|
+
* - `_connect()` — `connect({ servers, ...auth, ...options })` (driver 는 connect 시점 lazy import).
|
|
10
|
+
* connect() 자체가 서버 응답까지 기다리므로 별도 ping 불필요(실패 시 throw=검증).
|
|
11
|
+
* - `_disconnect()`— `nc.drain()` — 진행 중 메시지/구독을 모두 비운 뒤 **연결까지 닫는다**(공식 문서:
|
|
12
|
+
* "Finally, the connection is closed"). 그래서 별도 `close()` 는 호출하지 않는다.
|
|
13
|
+
* - `_native()` — `NatsConnection`(raw handle, ADR-009). 사용자가 직접 NATS API(jetstream 등) 접근.
|
|
14
|
+
* - `healthCheck()`— `nc.rtt()` (서버 왕복 ping)으로 실제 응답성 확인.
|
|
15
|
+
* - `getStats()` — 베이스 stats + nats 특화(server/연결 stats: inMsgs/outMsgs/inBytes/outBytes).
|
|
16
|
+
* - `publish/subscribe/request` + `enqueue/process` — 아래 인터페이스.
|
|
17
|
+
*
|
|
18
|
+
* # 직렬화 = JSONCodec
|
|
19
|
+
* payload 는 NATS wire 에서 `Uint8Array` 다. 본 어댑터는 `JSONCodec` 로 인코드/디코드를 표준화해
|
|
20
|
+
* 사용자는 JS 값을 그대로 publish/subscribe 한다(수신 측에서 같은 값으로 복원). `undefined` payload
|
|
21
|
+
* 는 `null` 로 정규화(JSONCodec 가 undefined 를 인코드하지 못함).
|
|
22
|
+
*
|
|
23
|
+
* # queue (job) 인터페이스 — 단순 publish + queue group (jetstream 미사용, ADR-112)
|
|
24
|
+
* `enqueue(job, msg)` = 해당 subject 로 **단순 publish**. `process(job, handler)` = **queue group**
|
|
25
|
+
* 구독(같은 queue 의 구독자끼리 메시지를 load-balance 분배). jetstream(영속 큐·재시도·DLQ)은
|
|
26
|
+
* `MegaJob` 백엔드에서 다룬다(ADR-028) — 코어 fan-out 까지.
|
|
27
|
+
*
|
|
28
|
+
* # 콜백 에러 표면화 (silent 금지)
|
|
29
|
+
* subscribe/process 콜백은 비동기 메시지 전달이라 호출자에게 직접 throw 할 수 없다. 구독 에러
|
|
30
|
+
* (NatsError) 또는 handler throw 는 **`console.error` 로 표면화**한다(로거 미주입 베이스).
|
|
31
|
+
* 트레이싱이 정식 logger 로 대체 가능.
|
|
32
|
+
*
|
|
33
|
+
* # pool 미지원
|
|
34
|
+
* NATS 클라이언트는 단일 멀티플렉스 연결이라 풀 개념이 없다. `config.pool` 지정 시 throw.
|
|
35
|
+
*
|
|
36
|
+
* # 설정 (services.buses.<key>) — 통합 옵션 구조 (ADR-109/112)
|
|
37
|
+
* ```js
|
|
38
|
+
* services: {
|
|
39
|
+
* buses: {
|
|
40
|
+
* main: {
|
|
41
|
+
* driver: 'nats',
|
|
42
|
+
* // (a) 연결 — url 또는 discrete (host/port) (+ token 또는 user/password 인증)
|
|
43
|
+
* url: 'nats://host:4222', // 또는 ↓
|
|
44
|
+
* host: 'localhost', port: 4222,
|
|
45
|
+
* token: '...', // 또는 user/password
|
|
46
|
+
* // (b) options — connect() passthrough (pool 은 미지원)
|
|
47
|
+
* options: { name: 'mega', reconnect: true, maxReconnectAttempts: -1, pingInterval: 30000,
|
|
48
|
+
* tls: {}, jwt: '...', nkey: '...' },
|
|
49
|
+
* },
|
|
50
|
+
* },
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
* `url`(또는 deprecated 별칭 `connectionString`) **XOR** discrete(host/port/user/password). `token` 은
|
|
54
|
+
* 별도 인증 축(discrete 모드 권장 — url 에 auth 포함 시 생략). 토큰/jwt/url 은 healthCheck/getStats/
|
|
55
|
+
* 에러 details 에 절대 노출하지 않는다.
|
|
56
|
+
*
|
|
57
|
+
* @module adapters/nats-adapter
|
|
58
|
+
*/
|
|
59
|
+
import { MegaValidationError, MegaInternalError } from '../errors/http-errors.js'
|
|
60
|
+
import { MegaBusAdapter } from './mega-bus-adapter.js'
|
|
61
|
+
import { resolveConnection, assertPlainObject } from './adapter-options.js'
|
|
62
|
+
import * as Registry from './registry.js'
|
|
63
|
+
|
|
64
|
+
/** NATS 기본 클라이언트 포트(discrete 모드에서 port 미지정 시). */
|
|
65
|
+
const DEFAULT_NATS_PORT = 4222
|
|
66
|
+
/** request() 기본 타임아웃(ms). */
|
|
67
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {object} NatsConfig
|
|
71
|
+
* @property {string} [driver] - 'nats' (매니저가 사용 — 어댑터는 무시).
|
|
72
|
+
* @property {string} [url] - `nats://host:port` 연결 문자열 (discrete 와 배타).
|
|
73
|
+
* @property {string} [connectionString] - `url` 의 deprecated 별칭 (하위 호환).
|
|
74
|
+
* @property {string} [host] @property {number} [port] @property {string} [user] @property {string} [password]
|
|
75
|
+
* @property {string} [token] - 토큰 인증 (별도 축 — url/discrete 어느 쪽과도 조합 가능).
|
|
76
|
+
* @property {any} [pool] - 미지원 — 지정 시 `adapter.invalid_option` throw (NATS 는 단일 연결, ADR-112).
|
|
77
|
+
* @property {Record<string, any>} [options] - connect() passthrough (name, reconnect, maxReconnectAttempts, pingInterval, tls, jwt, nkey, …).
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
export class MegaNatsAdapter extends MegaBusAdapter {
|
|
81
|
+
/** @type {import('nats').NatsConnection | null} 연결된 NatsConnection (connect 후에만). */
|
|
82
|
+
#nc = null
|
|
83
|
+
/** @type {import('nats').Codec<any> | null} JSONCodec (connect 시 생성). */
|
|
84
|
+
#codec = null
|
|
85
|
+
/** @type {import('nats').ConnectionOptions} _connect 에서 `connect()` 에 넘길 옵션(생성자에서 고정). */
|
|
86
|
+
#connectOptions
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {NatsConfig} [config] - services.buses.<key> 설정.
|
|
90
|
+
* @throws {MegaValidationError} `adapter.connection_required` - url·discrete 둘 다 없음.
|
|
91
|
+
* @throws {MegaValidationError} `adapter.connection_conflict` - url + discrete 동시 지정.
|
|
92
|
+
* @throws {MegaValidationError} `adapter.invalid_option` - pool 지정/옵션 타입 오류.
|
|
93
|
+
*/
|
|
94
|
+
constructor(config = /** @type {any} */ ({})) {
|
|
95
|
+
super(config)
|
|
96
|
+
|
|
97
|
+
// NATS 는 단일 멀티플렉스 연결이라 풀 개념이 없다 — pool 지정 명시 거부.
|
|
98
|
+
if (config.pool !== undefined) {
|
|
99
|
+
throw new MegaValidationError(
|
|
100
|
+
'adapter.invalid_option',
|
|
101
|
+
'nats: connection pooling is not supported (NATS uses a single multiplexed connection). Remove "pool".',
|
|
102
|
+
{ details: { driver: 'nats', option: 'pool' } },
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 연결 모드(url XOR discrete) 결정. token 은 별도 인증 축이라 connection 충돌 검사에 넣지 않는다.
|
|
107
|
+
const conn = resolveConnection(config, { driver: 'nats', dbConflictsWithUrl: false })
|
|
108
|
+
assertPlainObject('options', config.options, { driver: 'nats' })
|
|
109
|
+
|
|
110
|
+
/** @type {import('nats').ConnectionOptions} */
|
|
111
|
+
const connectOptions = {}
|
|
112
|
+
// options(passthrough) 먼저 — 아래 servers/auth 가 항상 이긴다(연결 필수값 보호).
|
|
113
|
+
if (config.options !== undefined) Object.assign(connectOptions, config.options)
|
|
114
|
+
|
|
115
|
+
// servers: url 모드면 url 그대로, discrete 모드면 host[:port].
|
|
116
|
+
if (conn.url !== undefined) {
|
|
117
|
+
connectOptions.servers = conn.url
|
|
118
|
+
} else {
|
|
119
|
+
const host = conn.host ?? 'localhost'
|
|
120
|
+
connectOptions.servers = conn.port !== undefined ? `${host}:${conn.port}` : `${host}:${DEFAULT_NATS_PORT}`
|
|
121
|
+
}
|
|
122
|
+
// 인증 — discrete user/password 또는 token(별도 축). 지정된 것만 설정.
|
|
123
|
+
if (conn.user !== undefined) connectOptions.user = conn.user
|
|
124
|
+
if (conn.password !== undefined) connectOptions.pass = conn.password
|
|
125
|
+
if (config.token !== undefined) connectOptions.token = config.token
|
|
126
|
+
|
|
127
|
+
this.#connectOptions = connectOptions
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `connect()` 로 NATS 연결 수립. connect() 가 서버 핸드셰이크까지 await 하므로 성공=연결 검증 완료
|
|
132
|
+
* (실패 시 throw → 베이스가 상태 failed). driver 는 **connect 시점에 lazy import**(DB 어댑터 정합).
|
|
133
|
+
*
|
|
134
|
+
* @protected
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
*/
|
|
137
|
+
async _connect() {
|
|
138
|
+
const { connect, JSONCodec } = await import('nats')
|
|
139
|
+
const nc = await connect(this.#connectOptions)
|
|
140
|
+
this.#nc = nc
|
|
141
|
+
this.#codec = JSONCodec()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* NATS 연결 drain — 진행 중 메시지/구독을 모두 비운 뒤 연결을 닫는다(공식 문서). 별도 close 불필요.
|
|
146
|
+
* 베이스 상태 머신이 connected 상태에서만 호출을 보장한다.
|
|
147
|
+
* @protected
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
async _disconnect() {
|
|
151
|
+
if (this.#nc !== null) {
|
|
152
|
+
const nc = this.#nc
|
|
153
|
+
this.#nc = null
|
|
154
|
+
this.#codec = null
|
|
155
|
+
// 이미 닫혀 있으면(서버측 절단 등) drain 이 throw 할 수 있어 가드.
|
|
156
|
+
if (!nc.isClosed()) await nc.drain()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* raw NatsConnection handle (ADR-009). `connect()` 후에만 베이스 `native` getter 가 호출한다.
|
|
162
|
+
* @protected
|
|
163
|
+
* @returns {import('nats').NatsConnection}
|
|
164
|
+
*/
|
|
165
|
+
_native() {
|
|
166
|
+
if (this.#nc === null) {
|
|
167
|
+
// 베이스 native getter 가 state 검증을 먼저 하므로 정상 경로에선 도달 안 함 — 방어.
|
|
168
|
+
return this._notImplemented('native')
|
|
169
|
+
}
|
|
170
|
+
return this.#nc
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 헬스 체크 — 실제 `rtt`(서버 왕복 ping)로 응답성 확인. 실패는 throw 없이 `ok:false` + 사유.
|
|
175
|
+
* @returns {Promise<{ ok: boolean, driver: 'nats', state: string, server?: string, rttMs?: number, error?: string }>}
|
|
176
|
+
*/
|
|
177
|
+
async healthCheck() {
|
|
178
|
+
if (this.state !== 'connected' || this.#nc === null || this.#nc.isClosed()) {
|
|
179
|
+
return { ok: false, driver: 'nats', state: this.state }
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const rttMs = await this.#nc.rtt()
|
|
183
|
+
return { ok: true, driver: 'nats', state: this.state, server: this.#nc.getServer(), rttMs }
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
driver: 'nats',
|
|
188
|
+
state: this.state,
|
|
189
|
+
error: err instanceof Error ? err.message : String(err),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 누적 통계 + nats 특화(server + 연결 stats). 연결 전이면 server/stats 는 undefined.
|
|
196
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, server: string | undefined, nats: import('nats').Stats | undefined }}
|
|
197
|
+
*/
|
|
198
|
+
getStats() {
|
|
199
|
+
const nc = this.#nc
|
|
200
|
+
return {
|
|
201
|
+
...super.getStats(),
|
|
202
|
+
driver: 'nats',
|
|
203
|
+
server: nc?.getServer(),
|
|
204
|
+
nats: nc ? nc.stats() : undefined,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
209
|
+
// MegaBusAdapter 인터페이스 — publish / subscribe / request + enqueue / process
|
|
210
|
+
// hook + 상태검증 + stats 는 `_instrument` 가 처리(ADR-077).
|
|
211
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* fire-and-forget 발행 (ack X). payload 는 JSONCodec 로 인코드.
|
|
215
|
+
* @param {string} subject
|
|
216
|
+
* @param {any} payload
|
|
217
|
+
* @returns {Promise<void>}
|
|
218
|
+
*/
|
|
219
|
+
async publish(subject, payload) {
|
|
220
|
+
return this._instrument('publish', { subject }, async () => {
|
|
221
|
+
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
222
|
+
nc.publish(subject, this.#encode(payload))
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 구독. `handler` 는 `(msg, replyFn?) => any` — msg 는 디코드된 값, replyFn 은 reply subject 가
|
|
228
|
+
* 있을 때만 제공(`request` 응답용). 반환된 핸들의 `unsubscribe()` 로 정리.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} subject
|
|
231
|
+
* @param {(msg: any, replyFn?: (payload: any) => void) => any} handler
|
|
232
|
+
* @returns {Promise<{ unsubscribe: () => Promise<void> }>}
|
|
233
|
+
*/
|
|
234
|
+
async subscribe(subject, handler) {
|
|
235
|
+
return this._instrument('subscribe', { subject }, async () => {
|
|
236
|
+
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
237
|
+
const sub = nc.subscribe(subject, {
|
|
238
|
+
callback: (err, msg) => this.#dispatch('subscribe', subject, handler, err, msg),
|
|
239
|
+
})
|
|
240
|
+
return {
|
|
241
|
+
unsubscribe: async () => {
|
|
242
|
+
sub.unsubscribe()
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* req/reply 패턴. `timeout`(ms) 초과 시 `bus.request_timeout`, 응답자 없으면 `bus.no_responders`.
|
|
250
|
+
* @param {string} subject
|
|
251
|
+
* @param {any} payload
|
|
252
|
+
* @param {{ timeout?: number }} [opts]
|
|
253
|
+
* @returns {Promise<any>} 디코드된 reply.
|
|
254
|
+
*/
|
|
255
|
+
async request(subject, payload, { timeout = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
|
|
256
|
+
return this._instrument('request', { subject, timeout }, async () => {
|
|
257
|
+
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
258
|
+
try {
|
|
259
|
+
const reply = await nc.request(subject, this.#encode(payload), { timeout })
|
|
260
|
+
return /** @type {import('nats').Codec<any>} */ (this.#codec).decode(reply.data)
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// NATS 타임아웃/무응답을 명시 에러 코드로 변환(silent 무시 X). 그 외 에러는 원본 전파.
|
|
263
|
+
const code = /** @type {any} */ (err)?.code
|
|
264
|
+
if (code === 'TIMEOUT') {
|
|
265
|
+
throw new MegaInternalError('bus.request_timeout', `nats request("${subject}") timed out after ${timeout}ms.`, {
|
|
266
|
+
details: { subject, timeout },
|
|
267
|
+
cause: err,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
if (code === '503') {
|
|
271
|
+
throw new MegaInternalError('bus.no_responders', `nats request("${subject}"): no responders subscribed to the subject.`, {
|
|
272
|
+
details: { subject },
|
|
273
|
+
cause: err,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
throw err
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 잡 enqueue — 단순 publish (queue group 분배는 `process` 측 책임, ADR-112).
|
|
283
|
+
* @param {string} jobName
|
|
284
|
+
* @param {any} payload
|
|
285
|
+
* @returns {Promise<void>}
|
|
286
|
+
*/
|
|
287
|
+
async enqueue(jobName, payload) {
|
|
288
|
+
return this._instrument('enqueue', { jobName }, async () => {
|
|
289
|
+
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
290
|
+
nc.publish(jobName, this.#encode(payload))
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 잡 처리 등록 — **queue group** 구독(같은 queue 구독자끼리 load-balance). 기본 queue 이름은 jobName
|
|
296
|
+
* (같은 잡의 모든 워커가 한 그룹).
|
|
297
|
+
*
|
|
298
|
+
* **subscription 핸들을 반환하지 않는 건 의도적**(L-2): worker 는 앱 수명 동안 상주하는 것이
|
|
299
|
+
* 정상이고(개별 잡 처리 등록을 런타임에 떼었다 붙였다 하는 패턴은 비표준), 정리는 disconnect 시
|
|
300
|
+
* `_disconnect()` 의 `nc.drain()` 이 **모든 구독을 일괄 비우며** 처리한다. 개별 unsubscribe 가
|
|
301
|
+
* 필요한 일시 구독은 `subscribe()`(핸들 반환)를 쓴다 — `process` 는 fire-and-forget `enqueue` 와
|
|
302
|
+
* 대칭인 상주 worker 용이다. 영속 큐/재시도/DLQ 가 필요하면 `MegaJob`(jetstream, ADR-028).
|
|
303
|
+
*
|
|
304
|
+
* @param {string} jobName
|
|
305
|
+
* @param {(payload: any) => any} handler
|
|
306
|
+
* @param {{ queue?: string }} [opts]
|
|
307
|
+
* @returns {Promise<void>}
|
|
308
|
+
*/
|
|
309
|
+
async process(jobName, handler, { queue = jobName } = {}) {
|
|
310
|
+
return this._instrument('process', { jobName, queue }, async () => {
|
|
311
|
+
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
312
|
+
nc.subscribe(jobName, {
|
|
313
|
+
queue,
|
|
314
|
+
callback: (err, msg) => this.#dispatch('process', jobName, (m) => handler(m), err, msg),
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* payload → Uint8Array (JSONCodec). undefined 는 null 로 정규화(JSONCodec 가 undefined 미지원).
|
|
321
|
+
* @param {any} payload @returns {Uint8Array}
|
|
322
|
+
*/
|
|
323
|
+
#encode(payload) {
|
|
324
|
+
return /** @type {import('nats').Codec<any>} */ (this.#codec).encode(payload === undefined ? null : payload)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 구독/잡 콜백 공통 디스패처 — 에러·디코드·handler 호출을 표면화한다(silent 금지).
|
|
329
|
+
* 구독 에러(NatsError)·디코드 실패·handler throw 를 모두 `console.error` 로 드러낸다.
|
|
330
|
+
*
|
|
331
|
+
* @param {'subscribe' | 'process'} kind
|
|
332
|
+
* @param {string} subject
|
|
333
|
+
* @param {(msg: any, replyFn?: (payload: any) => void) => any} handler
|
|
334
|
+
* @param {import('nats').NatsError | null} err
|
|
335
|
+
* @param {import('nats').Msg} msg
|
|
336
|
+
* @returns {void}
|
|
337
|
+
*/
|
|
338
|
+
#dispatch(kind, subject, handler, err, msg) {
|
|
339
|
+
if (err) {
|
|
340
|
+
// 구독 레벨 에러 — 콜백은 호출자에게 throw 못하므로 표면화.
|
|
341
|
+
console.error(`[MegaNatsAdapter] ${kind}("${subject}") subscription error:`, err)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
let decoded
|
|
345
|
+
try {
|
|
346
|
+
decoded = /** @type {import('nats').Codec<any>} */ (this.#codec).decode(msg.data)
|
|
347
|
+
} catch (decodeErr) {
|
|
348
|
+
console.error(`[MegaNatsAdapter] ${kind}("${subject}") payload decode failed:`, decodeErr)
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
// reply subject 가 있으면 replyFn 제공(request 응답용). subscribe 만 해당 — process 는 단방향.
|
|
352
|
+
const replyFn =
|
|
353
|
+
kind === 'subscribe' && msg.reply ? /** @param {any} p */ (p) => msg.respond(this.#encode(p)) : undefined
|
|
354
|
+
try {
|
|
355
|
+
const out = handler(decoded, replyFn)
|
|
356
|
+
// handler 가 async 면 reject 도 표면화(떠다니는 promise 가 silent 실패되지 않게).
|
|
357
|
+
if (out && typeof (/** @type {any} */ (out).then) === 'function') {
|
|
358
|
+
/** @type {Promise<any>} */ (out).catch((hErr) =>
|
|
359
|
+
console.error(`[MegaNatsAdapter] ${kind}("${subject}") async handler rejected:`, hErr),
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
} catch (hErr) {
|
|
363
|
+
console.error(`[MegaNatsAdapter] ${kind}("${subject}") handler threw:`, hErr)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 빌트인 driver 자기등록 (ADR-044) — 배럴(`adapters/index.js`)이 본 모듈을 import 하면 트리거된다.
|
|
369
|
+
// nats 는 _connect() 의 lazy import 까지 로드되지 않으므로 등록은 안전(미사용 환경 비강제).
|
|
370
|
+
Registry.register('nats', MegaNatsAdapter)
|