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,511 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaAdapter — 모든 어댑터의 추상 베이스 (ADR-027, ADR-045, ADR-077).
|
|
4
|
+
*
|
|
5
|
+
* 표준 lifecycle 5종 + 관찰성 hook 2종을 강제해서 코어가 부팅·종료·헬스체크·트레이싱을
|
|
6
|
+
* **균일하게** 처리하도록 한다. 도메인 베이스(`MegaDbAdapter` 등)와 구체 어댑터
|
|
7
|
+
* (`MegaPostgresAdapter` 등)가 이 베이스를 상속한다.
|
|
8
|
+
*
|
|
9
|
+
* # 표준 표면 (08-class-specs §3.2)
|
|
10
|
+
* - `connect()` / `disconnect()` — lifecycle. 상태 머신은 베이스가 관리.
|
|
11
|
+
* - `healthCheck()` — `/health/ready` 가 모든 어댑터의 `ok` 를 AND.
|
|
12
|
+
* - `getStats()` — 동기. 호출 횟수·에러·latency 누적 스냅샷.
|
|
13
|
+
* - `get native` — driver native handle (pg Pool / MongoClient 등). connect 후에만 유효.
|
|
14
|
+
* - `onCallStart(callName, attrs)` / `onCallEnd(callName, attrs, err, scope)` — 관찰성 hook.
|
|
15
|
+
* 디폴트는 `addHookListener` 로 등록된 리스너에 디스패치(없으면 no-op·0 비용).
|
|
16
|
+
* 트레이싱(`MegaTracing`)이 `addHookListener` 로 구독해 자동 span 으로 변환 (ADR-077).
|
|
17
|
+
* `onCallStart` 이 스코프 토큰을 반환하면 `_instrument` 가 `fn` 을 그 토큰으로 `run()` 안에서
|
|
18
|
+
* 실행해 span 중첩(트랜잭션 안의 query)을 정확히 만든다 — ADR-077 의 유일한 _instrument seam.
|
|
19
|
+
*
|
|
20
|
+
* # 구체 어댑터가 구현할 것 (override)
|
|
21
|
+
* - `_connect()` / `_disconnect()` — 실제 연결/해제 (디폴트 no-op).
|
|
22
|
+
* - `_native()` — native handle 반환 (디폴트 throw `adapter.not_implemented`).
|
|
23
|
+
* - 도메인 메서드(`query` / `get` / `publish` 등) — 도메인 베이스에서 stub 제공, 구체가 override.
|
|
24
|
+
* - 도메인 메서드 내부에서 {@link MegaAdapter#_instrument} 로 감싸면 hook + 상태 검증 + stats
|
|
25
|
+
* 누적이 한 번에 처리된다 (roadmap "boilerplate 패턴"). 직접 `onCallStart/onCallEnd` 호출도 가능.
|
|
26
|
+
*
|
|
27
|
+
* # 상태 머신
|
|
28
|
+
* created → connected ⇄ disconnected, 실패 시 → failed, 종료 진행 중 → disconnecting.
|
|
29
|
+
*
|
|
30
|
+
* @module adapters/mega-adapter
|
|
31
|
+
*/
|
|
32
|
+
import { performance } from 'node:perf_hooks'
|
|
33
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
34
|
+
import { MegaInternalError } from '../errors/http-errors.js'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 어댑터 lifecycle 상태.
|
|
38
|
+
* @typedef {'created' | 'connected' | 'disconnecting' | 'disconnected' | 'failed'} AdapterState
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 관찰성 hook 리스너 (ADR-077 보강 + span 중첩 seam).
|
|
43
|
+
* - `onCallStart(callName, attrs)` — **스코프 토큰을 반환할 수 있다**(예: 트레이서의 OTel span
|
|
44
|
+
* context). 반환하면 베이스가 도메인 `fn` 을 그 토큰으로 `run()` 안에서 실행해, 중첩 호출이
|
|
45
|
+
* 토큰을 부모로 보게 한다(span 계층 정확). 반환 안 하면(`undefined`) 래핑 없이 실행(0 비용).
|
|
46
|
+
* - `onCallEnd(callName, attrs, err?, scope?)` — `scope` 는 짝이 되는 `onCallStart` 의 반환 토큰.
|
|
47
|
+
* 계약: **throw 금지**(베이스가 격리하지만 리스너 측도 자체 보호 권장). 도메인 결과·통계 영향 X.
|
|
48
|
+
* @typedef {(callName: string, attrs?: object, err?: Error, scope?: any) => any} HookListener
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
export class MegaAdapter {
|
|
52
|
+
/**
|
|
53
|
+
* 도메인 호출 스코프 토큰 저장소 (ADR-077 보강, span 중첩 seam). `onCallStart` 이 반환한
|
|
54
|
+
* 토큰을 도메인 `fn` 실행 동안 전파해, 중첩 호출(트랜잭션 안의 query)이 부모 토큰을 보게 한다.
|
|
55
|
+
* `AsyncLocalStorage.run()` 격리라 동시 top-level 호출도 서로 섞이지 않는다(`enterWith` 누수 회피).
|
|
56
|
+
* **전 어댑터 공유**(static) — "현재 호출 컨텍스트"는 async 실행 흐름 전역이라, 어댑터 A 의
|
|
57
|
+
* 트랜잭션 안에서 어댑터 B 를 호출하면 B 의 span 이 A 의 span 밑으로 정확히 묶인다.
|
|
58
|
+
* 토큰이 없으면(옵트인 OFF) 아예 쓰이지 않아 0 비용.
|
|
59
|
+
* @type {AsyncLocalStorage<any>}
|
|
60
|
+
*/
|
|
61
|
+
static #callScope = new AsyncLocalStorage()
|
|
62
|
+
|
|
63
|
+
/** @type {object} driver 별 옵션 (생성자 인자). */
|
|
64
|
+
#config
|
|
65
|
+
/** @type {AdapterState} */
|
|
66
|
+
#state = 'created'
|
|
67
|
+
/** @type {{ requests: number, errors: number, totalLatencyMs: number }} */
|
|
68
|
+
#stats = { requests: 0, errors: 0, totalLatencyMs: 0 }
|
|
69
|
+
/** @type {Promise<void> | null} 진행 중 connect — 동시 connect race 가드(L1). */
|
|
70
|
+
#connectPromise = null
|
|
71
|
+
/**
|
|
72
|
+
* 관찰성 hook 리스너 (ADR-077 보강). 외부 트레이서(`MegaTracing`)가 코어/어댑터
|
|
73
|
+
* 코드 변경 없이 `onCallStart`/`onCallEnd` 를 구독하는 채널이다. 디폴트 `null` — 리스너가 없으면
|
|
74
|
+
* 디스패치가 즉시 빠져나가 옵트인 OFF 시 0 비용을 보장한다(첫 `addHookListener` 때만 Set 할당).
|
|
75
|
+
* @type {{ onCallStart: Set<HookListener> | null, onCallEnd: Set<HookListener> | null }}
|
|
76
|
+
*/
|
|
77
|
+
#hookListeners = { onCallStart: null, onCallEnd: null }
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {object} [config] - driver 별 설정 (url / pool / file 등).
|
|
81
|
+
*/
|
|
82
|
+
constructor(config = {}) {
|
|
83
|
+
// 추상 클래스 — 직접 인스턴스화 금지 (08-class-specs §3.2 불변식).
|
|
84
|
+
if (new.target === MegaAdapter) {
|
|
85
|
+
throw new MegaInternalError(
|
|
86
|
+
'adapter.abstract_instantiation',
|
|
87
|
+
'MegaAdapter is abstract — instantiate a concrete adapter (e.g. MegaPostgresAdapter), not the base class.',
|
|
88
|
+
{ details: { class: 'MegaAdapter' } },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
this.#config = config ?? {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 현재 lifecycle 상태 (읽기 전용). 테스트·헬스체크용.
|
|
96
|
+
* @returns {AdapterState}
|
|
97
|
+
*/
|
|
98
|
+
get state() {
|
|
99
|
+
return this.#state
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* driver 설정 (서브클래스 전용 — `_` 접두사 = framework-internal).
|
|
104
|
+
* @protected
|
|
105
|
+
* @returns {object}
|
|
106
|
+
*/
|
|
107
|
+
get _config() {
|
|
108
|
+
return this.#config
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
112
|
+
// lifecycle (베이스가 상태 머신 관리, 실제 연결은 _connect/_disconnect 위임)
|
|
113
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 연결 수립. 부팅 시퀀스 6번째 단계에서 호출 (Fastify 인스턴스 생성 직전, ADR-045).
|
|
117
|
+
* 이미 connected 면 idempotent no-op. disconnecting 중이면 throw.
|
|
118
|
+
* `_connect()` 가 throw 하면 상태 `failed` 로 두고 원본 에러 전파 (부팅 abort).
|
|
119
|
+
*
|
|
120
|
+
* 동시에 두 번 호출되면(아직 첫 호출이 resolve 되기 전) 첫 호출만 `_connect()` 를 실행하고
|
|
121
|
+
* 나머지는 같은 promise 를 await 한다 — 이중 연결/이중 pool 생성을 막는다(L1).
|
|
122
|
+
*
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async connect() {
|
|
126
|
+
if (this.#state === 'connected') return // idempotent (connect 두 번)
|
|
127
|
+
this.#assertNotDisconnecting('connect')
|
|
128
|
+
// L1: 동시 connect race 가드. 정본 §3.2 상태 집합에 'connecting' 이 없으므로 공개 상태는
|
|
129
|
+
// 추가하지 않고, 진행 중 promise 를 공유해 _connect() 가 정확히 1회만 실행되게 한다.
|
|
130
|
+
if (this.#connectPromise) return this.#connectPromise
|
|
131
|
+
this.#connectPromise = (async () => {
|
|
132
|
+
try {
|
|
133
|
+
await this._connect()
|
|
134
|
+
this.#state = 'connected'
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.#state = 'failed'
|
|
137
|
+
throw err
|
|
138
|
+
} finally {
|
|
139
|
+
this.#connectPromise = null
|
|
140
|
+
}
|
|
141
|
+
})()
|
|
142
|
+
return this.#connectPromise
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 연결 해제. graceful shutdown 시 등록 역순으로 호출 (07-sequence-diagrams §6).
|
|
147
|
+
* 아직 연결 안 했거나 이미 끊긴 상태면 idempotent no-op. disconnecting 중 재호출은 throw.
|
|
148
|
+
* `_disconnect()` 가 throw 하면 상태 `failed` 로 두고 원본 에러 전파(L4) — 정리 실패를
|
|
149
|
+
* `disconnected`(정상)로 덮지 않는다.
|
|
150
|
+
*
|
|
151
|
+
* @returns {Promise<void>}
|
|
152
|
+
*/
|
|
153
|
+
async disconnect() {
|
|
154
|
+
if (this.#state === 'created' || this.#state === 'disconnected') return // idempotent
|
|
155
|
+
this.#assertNotDisconnecting('disconnect')
|
|
156
|
+
this.#state = 'disconnecting'
|
|
157
|
+
try {
|
|
158
|
+
await this._disconnect()
|
|
159
|
+
this.#state = 'disconnected'
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// L4: _disconnect() 실패는 'failed' 로 남긴다(정본 상태 집합의 failed, §3.2). 'disconnected'
|
|
162
|
+
// 로 덮으면 정리 실패가 silent 정상처럼 보임. 원본 에러는 그대로 전파 — shutdown 측이 로깅.
|
|
163
|
+
this.#state = 'failed'
|
|
164
|
+
throw err
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 헬스 체크. 디폴트는 연결 상태 반영 — 구체 어댑터가 실제 ping 으로 override 권장.
|
|
170
|
+
* @returns {Promise<{ ok: boolean, [k: string]: any }>}
|
|
171
|
+
*/
|
|
172
|
+
async healthCheck() {
|
|
173
|
+
return { ok: this.#state === 'connected', state: this.#state }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 누적 통계 스냅샷 (동기). Prometheus `/metrics` 가 수집 (옵트인, 후속 Phase).
|
|
178
|
+
* @returns {{ state: AdapterState, requests: number, errors: number, totalLatencyMs: number, avgLatencyMs: number }}
|
|
179
|
+
*/
|
|
180
|
+
getStats() {
|
|
181
|
+
const { requests, errors, totalLatencyMs } = this.#stats
|
|
182
|
+
return {
|
|
183
|
+
state: this.#state,
|
|
184
|
+
requests,
|
|
185
|
+
errors,
|
|
186
|
+
totalLatencyMs,
|
|
187
|
+
avgLatencyMs: requests > 0 ? totalLatencyMs / requests : 0,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* driver native handle (pg Pool / MongoClient / ioredis Client / NATS Connection 등, ADR-009).
|
|
193
|
+
* `connect()` 후에만 유효 — 그 외엔 throw (08-class-specs §3.2 불변식).
|
|
194
|
+
* @returns {any}
|
|
195
|
+
*/
|
|
196
|
+
get native() {
|
|
197
|
+
if (this.#state !== 'connected') {
|
|
198
|
+
throw new MegaInternalError(
|
|
199
|
+
'adapter.not_connected',
|
|
200
|
+
`native handle of "${this.constructor.name}" is only available after connect() (state=${this.#state}).`,
|
|
201
|
+
{ details: { adapter: this.constructor.name, state: this.#state } },
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
return this._native()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
208
|
+
// 관찰성 hook (ADR-077) — 디폴트 no-op. 트레이싱이 구독.
|
|
209
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 현재 호출 스코프 토큰(부모) — 트레이서(`MegaTracing`)가 `onCallStart` 에서 읽어 새 span 의 parent
|
|
213
|
+
* 로 삼는다. 진행 중인 바깥 도메인 호출이 없으면 `undefined`(= 루트). {@link _instrument} 가 `run()`
|
|
214
|
+
* 으로 설정. `_` 접두사 = framework-internal(외부 트레이서 lib 가 읽어야 해 OOP protected 는 아님).
|
|
215
|
+
* @returns {any}
|
|
216
|
+
*/
|
|
217
|
+
get _currentCallScope() {
|
|
218
|
+
return MegaAdapter.#callScope.getStore()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 도메인 메서드 호출 시작 hook. 디폴트 동작 = 등록된 hook 리스너(ADR-077 보강)로 디스패치.
|
|
223
|
+
* 구체 어댑터의 도메인 메서드(`query`/`publish`/`get` 등) before 에서 호출됨(또는 `_instrument`).
|
|
224
|
+
* 리스너가 없으면(옵트인 OFF) 즉시 빠져나간다(0 비용). 트레이싱(`MegaTracing`)이
|
|
225
|
+
* `addHookListener('onCallStart', fn)` 으로 구독해 span 시작으로 변환 (ADR-077).
|
|
226
|
+
*
|
|
227
|
+
* **스코프 토큰**: 리스너가 값을 반환하면 그 중 **첫 정의된 반환**을 스코프 토큰으로 돌려준다.
|
|
228
|
+
* `_instrument` 는 이 토큰으로 도메인 `fn` 을 `run()` 안에서 실행해 span 중첩을 정확히 만든다.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} callName - 'query' | 'publish' | 'get' | ...
|
|
231
|
+
* @param {object} [attrs] - span 속성. 예: `{ key, statement, args }`.
|
|
232
|
+
* @returns {any} 스코프 토큰(트레이서의 span context) 또는 `undefined`(중첩 래핑 없음).
|
|
233
|
+
*/
|
|
234
|
+
onCallStart(callName, attrs) {
|
|
235
|
+
const set = this.#hookListeners.onCallStart
|
|
236
|
+
if (set === null) return undefined // 옵트인 OFF — 0 비용 fast path.
|
|
237
|
+
let scope
|
|
238
|
+
for (const fn of set) {
|
|
239
|
+
const r = fn(callName, attrs)
|
|
240
|
+
if (scope === undefined && r !== undefined) scope = r // 첫 정의된 반환 = 스코프 토큰.
|
|
241
|
+
}
|
|
242
|
+
return scope
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 도메인 메서드 호출 종료 hook. 디폴트 동작 = 등록된 hook 리스너로 디스패치(`onCallStart` 대칭).
|
|
247
|
+
*
|
|
248
|
+
* @param {string} callName
|
|
249
|
+
* @param {object} [attrs] - 결과 속성. 예: `{ rowCount, latencyMs, hit }`.
|
|
250
|
+
* @param {Error} [err] - 있으면 호출 실패 — span status = ERROR.
|
|
251
|
+
* @param {any} [scope] - 짝이 되는 `onCallStart` 가 반환한 스코프 토큰(트레이서가 span 종료에 사용).
|
|
252
|
+
* @returns {void}
|
|
253
|
+
*/
|
|
254
|
+
onCallEnd(callName, attrs, err, scope) {
|
|
255
|
+
const set = this.#hookListeners.onCallEnd
|
|
256
|
+
if (set === null) return // 옵트인 OFF — 0 비용 fast path.
|
|
257
|
+
for (const fn of set) fn(callName, attrs, err, scope)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
261
|
+
// hook 리스너 API (ADR-077 보강) — 외부 트레이서가 코어 변경 없이 구독.
|
|
262
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 관찰성 hook 리스너 등록 (옵션 B — `_instrument` 무변경, 베이스에 구독 채널만 추가).
|
|
266
|
+
*
|
|
267
|
+
* `MegaTracing` 같은 외부 트레이서가 어댑터 코드를 건드리지 않고 `onCallStart`/`onCallEnd`
|
|
268
|
+
* 발화를 구독한다. 같은 리스너 함수의 중복 등록은 `Set` 이라 1회로 합쳐진다.
|
|
269
|
+
*
|
|
270
|
+
* @param {'onCallStart' | 'onCallEnd'} hookName
|
|
271
|
+
* @param {HookListener} listener
|
|
272
|
+
* @returns {() => void} 등록 해제 함수 (구독 취소 시 호출).
|
|
273
|
+
* @throws {MegaInternalError} `adapter.invalid_hook` - hookName/listener 가 잘못된 타입.
|
|
274
|
+
*/
|
|
275
|
+
addHookListener(hookName, listener) {
|
|
276
|
+
if (hookName !== 'onCallStart' && hookName !== 'onCallEnd') {
|
|
277
|
+
throw new MegaInternalError(
|
|
278
|
+
'adapter.invalid_hook',
|
|
279
|
+
`addHookListener: hookName must be 'onCallStart' or 'onCallEnd' (got '${hookName}').`,
|
|
280
|
+
{ details: { hookName } },
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
if (typeof listener !== 'function') {
|
|
284
|
+
throw new MegaInternalError(
|
|
285
|
+
'adapter.invalid_hook',
|
|
286
|
+
`addHookListener('${hookName}'): listener must be a function (got ${typeof listener}).`,
|
|
287
|
+
{ details: { hookName, type: typeof listener } },
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
if (this.#hookListeners[hookName] === null) this.#hookListeners[hookName] = new Set()
|
|
291
|
+
this.#hookListeners[hookName].add(listener)
|
|
292
|
+
return () => this.removeHookListener(hookName, listener)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 관찰성 hook 리스너 해제. 마지막 리스너가 빠지면 Set 을 `null` 로 되돌려 0 비용 fast path 복구.
|
|
297
|
+
*
|
|
298
|
+
* @param {'onCallStart' | 'onCallEnd'} hookName
|
|
299
|
+
* @param {HookListener} listener
|
|
300
|
+
* @returns {boolean} 실제로 제거됐으면 true (없던 리스너면 false).
|
|
301
|
+
*/
|
|
302
|
+
removeHookListener(hookName, listener) {
|
|
303
|
+
const set = hookName === 'onCallStart' || hookName === 'onCallEnd' ? this.#hookListeners[hookName] : null
|
|
304
|
+
if (set === null || set === undefined) return false
|
|
305
|
+
const removed = set.delete(listener)
|
|
306
|
+
if (set.size === 0) this.#hookListeners[hookName] = null // fast path 복구
|
|
307
|
+
return removed
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 해당 hook 에 리스너가 1개 이상 붙어 있는지 (Boolean — `has*`, ADR-036).
|
|
312
|
+
* @param {'onCallStart' | 'onCallEnd'} hookName
|
|
313
|
+
* @returns {boolean}
|
|
314
|
+
*/
|
|
315
|
+
hasHookListeners(hookName) {
|
|
316
|
+
return (hookName === 'onCallStart' || hookName === 'onCallEnd') && this.#hookListeners[hookName] !== null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
320
|
+
// 서브클래스 헬퍼 (protected, `_` 접두사) — 구체 어댑터 구현 지원
|
|
321
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 실제 연결 수립. 디폴트 no-op (예: 로그 sink 는 연결 개념 없음).
|
|
325
|
+
* DB/cache/bus 구체 어댑터는 driver 연결 로직으로 override.
|
|
326
|
+
* @protected
|
|
327
|
+
* @returns {Promise<void>}
|
|
328
|
+
*/
|
|
329
|
+
async _connect() {}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 실제 연결 해제. 디폴트 no-op. 구체 어댑터가 driver close 로 override.
|
|
333
|
+
* @protected
|
|
334
|
+
* @returns {Promise<void>}
|
|
335
|
+
*/
|
|
336
|
+
async _disconnect() {}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* native handle 반환. 디폴트는 미구현 throw — 구체 어댑터가 override.
|
|
340
|
+
* @protected
|
|
341
|
+
* @returns {any}
|
|
342
|
+
*/
|
|
343
|
+
_native() {
|
|
344
|
+
return this._notImplemented('native')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 도메인 메서드를 hook + 상태 검증 + stats 누적으로 감싸는 표준 보일러플레이트 (roadmap, ADR-077).
|
|
349
|
+
*
|
|
350
|
+
* connect 전 호출이면 `adapter.not_connected` throw. 성공/실패 모두 `onCallEnd` 호출 후
|
|
351
|
+
* 결과 반환 / 원본 에러 re-throw (08-class-specs §3.2 "도메인 메서드 throw → onCallEnd(.., err) 후 re-throw").
|
|
352
|
+
*
|
|
353
|
+
* **hook 격리(M1)**: `onCallStart`/`onCallEnd` 는 관찰성 hook 일 뿐 도메인 결과·통계에
|
|
354
|
+
* 영향을 주면 안 된다. hook 은 **throw 금지**가 계약이지만, 위반(트레이서 버그 등) 시에도
|
|
355
|
+
* 도메인 호출이 깨지지 않도록 hook 호출을 별도 try/catch 로 격리해 `console.warn` 후 무시한다.
|
|
356
|
+
* 통계(requests/errors/latency)는 항상 fn 본체의 성공/실패 기준으로만 누적된다.
|
|
357
|
+
*
|
|
358
|
+
* **결과 기반 종료 속성**(`extractEndAttrs`): 도메인 호출 결과에서 span 종료 속성을 뽑아야 하는
|
|
359
|
+
* 경우(예: `query` 의 `rows_affected`)에 쓴다. 성공 시 `extractEndAttrs(result)` 의 반환 객체를
|
|
360
|
+
* 종료 attrs 에 머지한다. 결과 의존 attr 이 없으면 생략 — 기존 호출부(`withTransaction` 등)는
|
|
361
|
+
* 4번째 인자 없이 그대로 동작한다(0 변경). 추출기 throw 는 도메인 결과를 깨지 않도록 격리(M1 정합).
|
|
362
|
+
*
|
|
363
|
+
* @protected
|
|
364
|
+
* @template T
|
|
365
|
+
* @param {string} callName - 도메인 호출 이름 (span 이름 기반).
|
|
366
|
+
* @param {object} attrs - 시작 span 속성 (`{ key, statement, ... }`).
|
|
367
|
+
* @param {() => Promise<T>} fn - 실제 driver 호출.
|
|
368
|
+
* @param {(result: T) => object} [extractEndAttrs] - 결과에서 종료 span 속성 추출(선택). 성공 시만 호출.
|
|
369
|
+
* @returns {Promise<T>}
|
|
370
|
+
*/
|
|
371
|
+
async _instrument(callName, attrs, fn, extractEndAttrs) {
|
|
372
|
+
this._assertConnected(callName)
|
|
373
|
+
// onCallStart 가 "스코프 토큰"(트레이서의 span context)을 반환하면, 도메인 fn 을 그 토큰으로
|
|
374
|
+
// `#callScope.run` 안에서 실행한다 — 중첩 호출(트랜잭션 안의 query)이 토큰을 부모로 보게 해
|
|
375
|
+
// span 계층이 정확해진다(ADR-077 보강). 토큰이 없으면(옵트인 OFF) fn 을 그대로 실행
|
|
376
|
+
// — run 래퍼가 없어 0 비용. **이 한 줄(run 위임)이 ADR-077 의 _instrument 에 추가된 유일한 seam.**
|
|
377
|
+
const scope = this.#safeHookStart(callName, attrs)
|
|
378
|
+
this.#stats.requests += 1
|
|
379
|
+
const start = performance.now()
|
|
380
|
+
const exec = scope === undefined ? fn : () => MegaAdapter.#callScope.run(scope, fn)
|
|
381
|
+
let result
|
|
382
|
+
try {
|
|
383
|
+
result = await exec()
|
|
384
|
+
} catch (err) {
|
|
385
|
+
// 실패 경로: 통계는 fn 본체 기준으로만(에러 1회), hook 은 격리 호출.
|
|
386
|
+
const latencyMs = performance.now() - start
|
|
387
|
+
this.#stats.totalLatencyMs += latencyMs
|
|
388
|
+
this.#stats.errors += 1
|
|
389
|
+
this.#safeHookEnd(callName, { ...attrs, latencyMs }, /** @type {Error} */ (err), scope)
|
|
390
|
+
throw err
|
|
391
|
+
}
|
|
392
|
+
// 성공 경로: onCallEnd 가 throw 해도 #safeHookEnd 가 흡수 → 성공이 에러로 오기록되지 않는다(M1).
|
|
393
|
+
const latencyMs = performance.now() - start
|
|
394
|
+
this.#stats.totalLatencyMs += latencyMs
|
|
395
|
+
const endAttrs = { ...attrs, latencyMs }
|
|
396
|
+
if (typeof extractEndAttrs === 'function') {
|
|
397
|
+
Object.assign(endAttrs, this.#safeExtractEnd(callName, extractEndAttrs, result))
|
|
398
|
+
}
|
|
399
|
+
this.#safeHookEnd(callName, endAttrs, undefined, scope)
|
|
400
|
+
return result
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* `extractEndAttrs` 격리 호출 — 추출기가 throw 해도 도메인 결과·통계를 보호한다(M1 정합).
|
|
405
|
+
* throw 시 빈 객체를 돌려 종료 attr 보강 없이 진행. 로거 미주입 베이스라 `console.warn`.
|
|
406
|
+
*
|
|
407
|
+
* @param {string} callName
|
|
408
|
+
* @param {(result: any) => object} extractEndAttrs
|
|
409
|
+
* @param {any} result
|
|
410
|
+
* @returns {object}
|
|
411
|
+
*/
|
|
412
|
+
#safeExtractEnd(callName, extractEndAttrs, result) {
|
|
413
|
+
try {
|
|
414
|
+
const out = extractEndAttrs(result)
|
|
415
|
+
return out && typeof out === 'object' ? out : {}
|
|
416
|
+
} catch (extractErr) {
|
|
417
|
+
console.warn(
|
|
418
|
+
`[MegaAdapter] ${this.constructor.name} extractEndAttrs('${callName}') threw and was ignored — must not throw.`,
|
|
419
|
+
extractErr,
|
|
420
|
+
)
|
|
421
|
+
return {}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* `onCallStart` 격리 호출 — hook 이 throw 해도 도메인 호출·통계를 보호하고(M1) 스코프 토큰을 돌려준다.
|
|
427
|
+
* throw 시 토큰 없이(`undefined`) 진행해 도메인 fn 은 래핑 없이 실행된다. hook throw 는 `console.warn`.
|
|
428
|
+
*
|
|
429
|
+
* 로거 미주입 베이스라 `console.warn` 사용 — MegaServer 의 raw 소켓 가드와 동일 패턴( * 비치명적, 이유 주석 + 로그).
|
|
430
|
+
*
|
|
431
|
+
* @param {string} callName
|
|
432
|
+
* @param {object} [attrs]
|
|
433
|
+
* @returns {any} 스코프 토큰 또는 `undefined`.
|
|
434
|
+
*/
|
|
435
|
+
#safeHookStart(callName, attrs) {
|
|
436
|
+
try {
|
|
437
|
+
return this.onCallStart(callName, attrs)
|
|
438
|
+
} catch (hookErr) {
|
|
439
|
+
console.warn(
|
|
440
|
+
`[MegaAdapter] ${this.constructor.name}.onCallStart('${callName}') threw and was ignored — observability hooks must not throw.`,
|
|
441
|
+
hookErr,
|
|
442
|
+
)
|
|
443
|
+
return undefined
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* `onCallEnd` 격리 호출 — `#safeHookStart` 대칭. hook throw 는 흡수 후 `console.warn`(M1 보호).
|
|
449
|
+
*
|
|
450
|
+
* @param {string} callName
|
|
451
|
+
* @param {object} [attrs]
|
|
452
|
+
* @param {Error} [err]
|
|
453
|
+
* @param {any} [scope] - 짝이 되는 `onCallStart` 가 반환한 스코프 토큰.
|
|
454
|
+
* @returns {void}
|
|
455
|
+
*/
|
|
456
|
+
#safeHookEnd(callName, attrs, err, scope) {
|
|
457
|
+
try {
|
|
458
|
+
this.onCallEnd(callName, attrs, err, scope)
|
|
459
|
+
} catch (hookErr) {
|
|
460
|
+
console.warn(
|
|
461
|
+
`[MegaAdapter] ${this.constructor.name}.onCallEnd('${callName}') threw and was ignored — observability hooks must not throw.`,
|
|
462
|
+
hookErr,
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 도메인 메서드가 connect 전 호출됐는지 검증. 아니면 `adapter.not_connected` throw.
|
|
469
|
+
* @protected
|
|
470
|
+
* @param {string} callName
|
|
471
|
+
* @returns {void}
|
|
472
|
+
*/
|
|
473
|
+
_assertConnected(callName) {
|
|
474
|
+
if (this.#state !== 'connected') {
|
|
475
|
+
throw new MegaInternalError(
|
|
476
|
+
'adapter.not_connected',
|
|
477
|
+
`Adapter "${this.constructor.name}" method "${callName}" called before connect() (state=${this.#state}).`,
|
|
478
|
+
{ details: { adapter: this.constructor.name, call: callName, state: this.#state } },
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 미구현 도메인 메서드 stub 용 — 명확한 `adapter.not_implemented` throw.
|
|
485
|
+
* @protected
|
|
486
|
+
* @param {string} method
|
|
487
|
+
* @returns {never}
|
|
488
|
+
*/
|
|
489
|
+
_notImplemented(method) {
|
|
490
|
+
throw new MegaInternalError(
|
|
491
|
+
'adapter.not_implemented',
|
|
492
|
+
`${this.constructor.name}.${method}() is not implemented.`,
|
|
493
|
+
{ details: { adapter: this.constructor.name, method } },
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* disconnecting 진행 중 재호출 가드.
|
|
499
|
+
* @param {string} call
|
|
500
|
+
* @returns {void}
|
|
501
|
+
*/
|
|
502
|
+
#assertNotDisconnecting(call) {
|
|
503
|
+
if (this.#state === 'disconnecting') {
|
|
504
|
+
throw new MegaInternalError(
|
|
505
|
+
'adapter.disconnecting',
|
|
506
|
+
`Adapter "${this.constructor.name}" is disconnecting — "${call}" rejected.`,
|
|
507
|
+
{ details: { adapter: this.constructor.name, call } },
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaBusAdapter — publish / subscribe / request / job queue 표준 인터페이스 (추상, 08-class-specs §3.5).
|
|
4
|
+
*
|
|
5
|
+
* `ctx.bus(key)` 가 본 베이스 인스턴스를 반환. 구체: `MegaNatsAdapter`
|
|
6
|
+
*. subject prefix `mega.<appName>.<event>` 자동 (옵션, ADR-064).
|
|
7
|
+
* `enqueue / process` 는 `MegaJob` 백엔드 (ADR-028).
|
|
8
|
+
*
|
|
9
|
+
* @module adapters/mega-bus-adapter
|
|
10
|
+
*/
|
|
11
|
+
import { MegaInternalError } from '../errors/http-errors.js'
|
|
12
|
+
import { MegaAdapter } from './mega-adapter.js'
|
|
13
|
+
|
|
14
|
+
export class MegaBusAdapter extends MegaAdapter {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} [config]
|
|
17
|
+
*/
|
|
18
|
+
constructor(config) {
|
|
19
|
+
super(config)
|
|
20
|
+
if (new.target === MegaBusAdapter) {
|
|
21
|
+
throw new MegaInternalError(
|
|
22
|
+
'adapter.abstract_instantiation',
|
|
23
|
+
'MegaBusAdapter is abstract — use a concrete bus adapter (MegaNatsAdapter).',
|
|
24
|
+
{ details: { class: 'MegaBusAdapter' } },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* fire-and-forget 발행 (ack X).
|
|
31
|
+
* @param {string} _subject
|
|
32
|
+
* @param {any} _payload
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
async publish(_subject, _payload) {
|
|
36
|
+
return this._notImplemented('publish')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 구독. handler 는 `async (msg, replyFn) => void`. 반환된 핸들로 cleanup.
|
|
41
|
+
* @param {string} _subject
|
|
42
|
+
* @param {(msg: any, replyFn?: (payload: any) => void) => any} _handler
|
|
43
|
+
* @returns {Promise<{ unsubscribe: () => Promise<void> }>}
|
|
44
|
+
*/
|
|
45
|
+
async subscribe(_subject, _handler) {
|
|
46
|
+
return this._notImplemented('subscribe')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* req/reply 패턴. timeout 초과 시 `bus.request_timeout` throw.
|
|
51
|
+
* @param {string} _subject
|
|
52
|
+
* @param {any} _payload
|
|
53
|
+
* @param {{ timeout?: number }} [_opts]
|
|
54
|
+
* @returns {Promise<any>} reply.
|
|
55
|
+
*/
|
|
56
|
+
async request(_subject, _payload, _opts = {}) {
|
|
57
|
+
return this._notImplemented('request')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 잡 enqueue (MegaJob 백엔드, ADR-028).
|
|
62
|
+
* @param {string} _jobName
|
|
63
|
+
* @param {any} _payload
|
|
64
|
+
* @param {object} [_opts]
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
async enqueue(_jobName, _payload, _opts = {}) {
|
|
68
|
+
return this._notImplemented('enqueue')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 잡 처리 등록 (MegaJob 백엔드, ADR-028).
|
|
73
|
+
* @param {string} _jobName
|
|
74
|
+
* @param {(payload: any) => any} _handler
|
|
75
|
+
* @param {object} [_opts]
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
async process(_jobName, _handler, _opts = {}) {
|
|
79
|
+
return this._notImplemented('process')
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaCacheAdapter — key-value 캐시 표준 인터페이스 (추상, 08-class-specs §3.4).
|
|
4
|
+
*
|
|
5
|
+
* `ctx.cache(key)` 가 본 베이스 인스턴스를 반환. 구체: `MegaRedisAdapter` /
|
|
6
|
+
* `MegaFileAdapter` (ADR-082). 사용자 `key` 에 자동 prefix
|
|
7
|
+
* `mega:cache:<appName>:<key>` 가 코어에서 부착됨 (ADR-064).
|
|
8
|
+
*
|
|
9
|
+
* @module adapters/mega-cache-adapter
|
|
10
|
+
*/
|
|
11
|
+
import { MegaInternalError, MegaValidationError } from '../errors/http-errors.js'
|
|
12
|
+
import { MegaAdapter } from './mega-adapter.js'
|
|
13
|
+
|
|
14
|
+
export class MegaCacheAdapter extends MegaAdapter {
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} [config]
|
|
17
|
+
*/
|
|
18
|
+
constructor(config) {
|
|
19
|
+
super(config)
|
|
20
|
+
if (new.target === MegaCacheAdapter) {
|
|
21
|
+
throw new MegaInternalError(
|
|
22
|
+
'adapter.abstract_instantiation',
|
|
23
|
+
'MegaCacheAdapter is abstract — use a concrete cache adapter (MegaRedisAdapter, MegaFileAdapter).',
|
|
24
|
+
{ details: { class: 'MegaCacheAdapter' } },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} _key
|
|
31
|
+
* @returns {Promise<any>} 값 — 없으면 `null` (throw X).
|
|
32
|
+
*/
|
|
33
|
+
async get(_key) {
|
|
34
|
+
return this._notImplemented('get')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} _key
|
|
39
|
+
* @param {any} _value
|
|
40
|
+
* @param {{ ttl?: number }} [_opts] - `ttl` 미지정 시 무한 (초 단위).
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async set(_key, _value, _opts = {}) {
|
|
44
|
+
return this._notImplemented('set')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} _key
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async del(_key) {
|
|
52
|
+
return this._notImplemented('del')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 키 존재 여부 (Boolean — `has*` 접두사, ADR-036). `get` 과 race 가능 — 보장 X.
|
|
57
|
+
* @param {string} _key
|
|
58
|
+
* @returns {Promise<boolean>}
|
|
59
|
+
*/
|
|
60
|
+
async has(_key) {
|
|
61
|
+
return this._notImplemented('has')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ttl 값 검증 헬퍼 (구체 어댑터 `set` 에서 사용). 초 단위 양의 정수만 통과(L3).
|
|
66
|
+
*
|
|
67
|
+
* `undefined` = 무한(검증 통과, no-expiry). 그 외엔 다음을 거부하고 `cache.invalid_ttl` throw:
|
|
68
|
+
* - 숫자 아님 / `NaN`
|
|
69
|
+
* - `Infinity`(무한은 `undefined` 로 표현 — 옵션 `allowInfinity` 로 driver override 가능)
|
|
70
|
+
* - 정수 아님(소수 초 — Redis `EX` 등 대부분 정수 초만 허용)
|
|
71
|
+
* - 음수
|
|
72
|
+
* - `0`(즉시 만료 의미가 driver 마다 달라 모호 — 옵션 `allowZero` 로 override 가능)
|
|
73
|
+
*
|
|
74
|
+
* @protected
|
|
75
|
+
* @param {number} [ttl] - 초 단위 TTL.
|
|
76
|
+
* @param {{ allowZero?: boolean, allowInfinity?: boolean }} [opts] - driver 별 완화 옵션.
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
_assertTtl(ttl, { allowZero = false, allowInfinity = false } = {}) {
|
|
80
|
+
if (ttl === undefined) return // 무한(no-expiry)
|
|
81
|
+
/** @param {string} why */
|
|
82
|
+
const reject = (why) => {
|
|
83
|
+
throw new MegaValidationError('cache.invalid_ttl', `Invalid cache ttl (${why}): ${ttl}`, {
|
|
84
|
+
details: { ttl },
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
if (typeof ttl !== 'number' || Number.isNaN(ttl)) reject('not a number')
|
|
88
|
+
else if (ttl === Infinity) {
|
|
89
|
+
if (!allowInfinity) reject('Infinity — use undefined for no-expiry')
|
|
90
|
+
} else if (!Number.isInteger(ttl)) reject('not an integer number of seconds')
|
|
91
|
+
else if (ttl < 0) reject('negative')
|
|
92
|
+
else if (ttl === 0 && !allowZero) reject('zero')
|
|
93
|
+
}
|
|
94
|
+
}
|