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,109 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaWebSocketController — WS 채널 **필수 베이스** (ADR-074, 03-api-spec §2).
|
|
4
|
+
*
|
|
5
|
+
* HTTP 컨트롤러 베이스는 폐기됐지만(ADR-074), WS 는 N 개 메시지 타입을 한 객체로 묶고
|
|
6
|
+
* instance 가 namespace 별 상태를 가지는 게 본질이라 클래스를 강제한다. 라이프사이클 3 훅
|
|
7
|
+
* (`onConnect` / `onMessage` / `onDisconnect`) + 메시지 `type` 기반 **자동 디스패치** (ADR-015):
|
|
8
|
+
* `{ type: 'chat.send' }` → `this['chat.send'](sock, msg, ctx)`, 해당 메서드 없으면 onMessage 폴백.
|
|
9
|
+
*
|
|
10
|
+
* # 이 Step 의 범위
|
|
11
|
+
* 베이스 계약 + 디스패치 로직만 제공한다. 실 소켓 upgrade 연결과 ctx/services 주입 wiring 은
|
|
12
|
+
* 후속 Step(`router.ws`/`app.ws` upgrade, hub 통합) 에서 {@link MegaWebSocketController#_bind}
|
|
13
|
+
* 을 통해 붙는다. 본 Step 에서는 디스패치 라우팅을 실 소켓 없이 단위 테스트로 검증한다.
|
|
14
|
+
*
|
|
15
|
+
* @module core/ws-controller
|
|
16
|
+
*/
|
|
17
|
+
import { WS_TYPE_PATTERN } from './ws-message.js'
|
|
18
|
+
|
|
19
|
+
export class MegaWebSocketController {
|
|
20
|
+
constructor() {
|
|
21
|
+
/** @type {any} */ this._ctxRef = null
|
|
22
|
+
/** @type {any} */ this._appRef = null
|
|
23
|
+
/** @type {any} */ this._logRef = null
|
|
24
|
+
/** @type {any} */ this._servicesRef = null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 프레임워크가 채널 인스턴스에 컨텍스트를 바인딩한다 (후속 Step 의 upgrade 처리에서 호출).
|
|
29
|
+
* **사용자 코드는 호출하지 않는다** (framework-internal).
|
|
30
|
+
*
|
|
31
|
+
* @param {{ ctx?: any, app?: any, log?: any, services?: any }} [binding]
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
_bind(binding = {}) {
|
|
35
|
+
this._ctxRef = binding.ctx ?? null
|
|
36
|
+
this._appRef = binding.app ?? null
|
|
37
|
+
this._logRef = binding.log ?? null
|
|
38
|
+
this._servicesRef = binding.services ?? null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 현재 채널 컨텍스트 (미바인딩 시 null). */
|
|
42
|
+
get ctx() {
|
|
43
|
+
return this._ctxRef
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 소속 MegaApp (미바인딩 시 null). */
|
|
47
|
+
get app() {
|
|
48
|
+
return this._appRef
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** request_id 자동 주입 로거 (미바인딩 시 null). */
|
|
52
|
+
get log() {
|
|
53
|
+
return this._logRef
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 자동 DI 핸들 — `ctx.services` 와 동일 (미바인딩 시 null). */
|
|
57
|
+
get services() {
|
|
58
|
+
return this._servicesRef
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 연결 수립 시 1회 호출. 기본 no-op — 서브클래스가 override.
|
|
63
|
+
* @param {any} _sock - WS 소켓.
|
|
64
|
+
* @param {any} _ctx - 채널 컨텍스트.
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
async onConnect(_sock, _ctx) {}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 디스패치되지 않은 메시지의 폴백 핸들러. 기본 no-op — 서브클래스가 override.
|
|
71
|
+
* @param {any} _sock - WS 소켓.
|
|
72
|
+
* @param {Object} _msg - 검증된 WS envelope.
|
|
73
|
+
* @param {any} _ctx - 채널 컨텍스트.
|
|
74
|
+
* @returns {Promise<any>}
|
|
75
|
+
*/
|
|
76
|
+
async onMessage(_sock, _msg, _ctx) {}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 연결 종료 시 호출. 기본 no-op — 서브클래스가 override.
|
|
80
|
+
* @param {any} _sock - WS 소켓.
|
|
81
|
+
* @param {any} _ctx - 채널 컨텍스트.
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async onDisconnect(_sock, _ctx) {}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 수신 메시지 자동 디스패치 (ADR-015). 프레임워크(후속 Step)가 소켓 message 마다 호출한다.
|
|
88
|
+
*
|
|
89
|
+
* `msg.type` 이 `domain.action` 패턴({@link WS_TYPE_PATTERN}) 이고 동명 메서드가 있으면 그
|
|
90
|
+
* 메서드를, 아니면 {@link MegaWebSocketController#onMessage} 폴백을 호출한다. 패턴이 점(`.`)을
|
|
91
|
+
* 강제하므로 `constructor` / `onMessage` 같은 베이스 멤버명은 type 으로 매칭될 수 없다 —
|
|
92
|
+
* prototype 오염 / 의도치 않은 베이스 호출을 구조적으로 차단.
|
|
93
|
+
*
|
|
94
|
+
* @param {any} sock - WS 소켓.
|
|
95
|
+
* @param {{ type?: string }} msg - 검증된 WS envelope (ws-message.js).
|
|
96
|
+
* @param {any} ctx - 채널 컨텍스트.
|
|
97
|
+
* @returns {Promise<any>} 핸들러 반환값.
|
|
98
|
+
*/
|
|
99
|
+
async dispatch(sock, msg, ctx) {
|
|
100
|
+
const type = msg?.type
|
|
101
|
+
if (typeof type === 'string' && WS_TYPE_PATTERN.test(type)) {
|
|
102
|
+
const handler = /** @type {any} */ (this)[type]
|
|
103
|
+
if (typeof handler === 'function') {
|
|
104
|
+
return handler.call(this, sock, msg, ctx)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return this.onMessage(sock, msg, ctx)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* WS 메시지 envelope (ADR-015, 04-data-models §6.2 정본).
|
|
4
|
+
*
|
|
5
|
+
* 모든 WebSocket 메시지는 단일 envelope `{ v, id, type, ts, ns?, payload?, error?, ref? }`
|
|
6
|
+
* 로 통일된다. 이 envelope 은 두 곳의 공통 바탕이다:
|
|
7
|
+
* (1) 클라이언트 ↔ bridge 비즈니스 메시지
|
|
8
|
+
* (2) bridge ↔ hub 12-타입 프로토콜 (03-api-spec §7) — `type` 값만 12종 중 하나로 얹음
|
|
9
|
+
*
|
|
10
|
+
* HTTP 응답 envelope (`{ ok, data, meta }`, core/envelope.js) 와는 **별개** — transport 가 다르다.
|
|
11
|
+
*
|
|
12
|
+
* # 설계 메모
|
|
13
|
+
* - 검증은 AJV 의존 없이 고정 shape 를 직접 검사한다 (신규 의존성 회피). bridge↔hub 단계에서
|
|
14
|
+
* 스키마 컴파일이 필요해지면 {@link WS_MESSAGE_SCHEMA} 를 재사용 (별도 결정).
|
|
15
|
+
* - `id` 는 ULID (정렬 가능 + 충돌 거의 없음). 정식 `MegaIdGen` 라이브러리화는 OQ-014 로 남김 —
|
|
16
|
+
* 본 모듈은 zero-dep private 생성기만 둔다.
|
|
17
|
+
*
|
|
18
|
+
* @module core/ws-message
|
|
19
|
+
*/
|
|
20
|
+
import { randomBytes } from 'node:crypto'
|
|
21
|
+
|
|
22
|
+
/** 현 프로토콜 버전 (04-data-models §6.2 `v: { const: 1 }`). */
|
|
23
|
+
export const WS_PROTOCOL_VERSION = 1
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 메시지 `type` / `error.code` 패턴 — `domain.action[.result]` (ADR-016, §6.2/§6.3).
|
|
27
|
+
* 점(`.`)이 최소 1개 강제 → 베이스 메서드명(`onMessage` 등, 점 없음) 과 절대 충돌하지 않음.
|
|
28
|
+
*/
|
|
29
|
+
export const WS_TYPE_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
|
|
30
|
+
|
|
31
|
+
/** envelope 허용 키 집합 (additionalProperties: false 검증용). */
|
|
32
|
+
const WS_ALLOWED_KEYS = new Set(['v', 'id', 'type', 'ts', 'ns', 'payload', 'error', 'ref'])
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* WS 메시지 JSON Schema (04-data-models §6.2 정본 그대로).
|
|
36
|
+
* 후속 단계(bridge↔hub, AJV 컴파일)에서 재사용할 수 있도록 export.
|
|
37
|
+
* @type {Object}
|
|
38
|
+
*/
|
|
39
|
+
export const WS_MESSAGE_SCHEMA = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
required: ['v', 'id', 'type', 'ts'],
|
|
42
|
+
properties: {
|
|
43
|
+
v: { const: WS_PROTOCOL_VERSION },
|
|
44
|
+
id: { type: 'string' },
|
|
45
|
+
type: { type: 'string', pattern: WS_TYPE_PATTERN.source },
|
|
46
|
+
ts: { type: 'integer' },
|
|
47
|
+
ns: { type: 'string' },
|
|
48
|
+
payload: { type: 'object', additionalProperties: true },
|
|
49
|
+
error: { type: 'object', additionalProperties: true },
|
|
50
|
+
ref: { type: 'string' },
|
|
51
|
+
},
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Crockford base32 알파벳 (ULID 표준). 혼동 문자(I, L, O, U) 제외.
|
|
57
|
+
* 256 = 8 × 32 이므로 `byte % 32` 는 편향 없이 균일 분포.
|
|
58
|
+
*/
|
|
59
|
+
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 메시지 id 생성 — ULID (48-bit timestamp + 80-bit randomness = 26자 Crockford base32).
|
|
63
|
+
*
|
|
64
|
+
* 정식 `MegaIdGen` 라이브러리화는 OQ-014 로 예약 — 본 helper 는 envelope 전용 zero-dep 구현.
|
|
65
|
+
*
|
|
66
|
+
* @param {number} [timestamp] - epoch ms (테스트 주입용; 미지정 시 `Date.now()`).
|
|
67
|
+
* @returns {string} 26자 ULID (앞 10자 = 시간, 뒤 16자 = 랜덤).
|
|
68
|
+
*/
|
|
69
|
+
export function generateMessageId(timestamp = Date.now()) {
|
|
70
|
+
// 48-bit 타임스탬프 → 10자 (5bit × 10 = 50bit 여유, 실사용 48bit). 2^48 < 2^53 이라 정수 안전.
|
|
71
|
+
let time = timestamp
|
|
72
|
+
let timeChars = ''
|
|
73
|
+
for (let i = 0; i < 10; i++) {
|
|
74
|
+
timeChars = CROCKFORD[time % 32] + timeChars
|
|
75
|
+
time = Math.floor(time / 32)
|
|
76
|
+
}
|
|
77
|
+
// 80-bit 랜덤 → 16자 (char 당 5bit 균일). byte % 32 무편향 (256 = 8×32).
|
|
78
|
+
const rand = randomBytes(16)
|
|
79
|
+
let randChars = ''
|
|
80
|
+
for (let i = 0; i < 16; i++) randChars += CROCKFORD[rand[i] % 32]
|
|
81
|
+
return timeChars + randChars
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* WS 메시지 envelope 생성. `v` / `id` / `ts` 자동 채움 (§6.2).
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} [fields]
|
|
88
|
+
* @param {string} [fields.type] - `domain.action` 패턴 (검증됨, 위반 시 throw). 누락 시 throw.
|
|
89
|
+
* @param {string} [fields.ns] - 채널 namespace (예: `'chat'`).
|
|
90
|
+
* @param {Object} [fields.payload] - 페이로드.
|
|
91
|
+
* @param {Object} [fields.error] - 에러 객체 (§6.3 모양 권장).
|
|
92
|
+
* @param {string} [fields.ref] - 요청-응답 매칭용. 서버 푸시는 생략.
|
|
93
|
+
* @param {{ id?: string, ts?: number }} [opts] - 테스트/재현용 주입 (미지정 시 자동).
|
|
94
|
+
* @returns {{ v: number, id: string, type: string, ts: number }} 완성된 envelope.
|
|
95
|
+
* @throws {Error} `type` 이 {@link WS_TYPE_PATTERN} 위반.
|
|
96
|
+
* @example
|
|
97
|
+
* createWsMessage({ type: 'chat.send', ns: 'chat', payload: { text: 'hi' } })
|
|
98
|
+
* // → { v: 1, id: '01HV...', type: 'chat.send', ts: 1735..., ns: 'chat', payload: { text: 'hi' } }
|
|
99
|
+
*/
|
|
100
|
+
export function createWsMessage({ type, ns, payload, error, ref } = {}, opts = {}) {
|
|
101
|
+
if (typeof type !== 'string' || !WS_TYPE_PATTERN.test(type)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`createWsMessage: 'type' must match ${WS_TYPE_PATTERN.source} (ADR-016). Got: ${JSON.stringify(type)}`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
const ts = opts.ts ?? Date.now()
|
|
107
|
+
/** @type {Record<string, any>} */
|
|
108
|
+
const msg = { v: WS_PROTOCOL_VERSION, id: opts.id ?? generateMessageId(ts), type, ts }
|
|
109
|
+
if (ns !== undefined) msg.ns = ns
|
|
110
|
+
if (payload !== undefined) msg.payload = payload
|
|
111
|
+
if (error !== undefined) msg.error = error
|
|
112
|
+
if (ref !== undefined) msg.ref = ref
|
|
113
|
+
return /** @type {{ v: number, id: string, type: string, ts: number }} */ (msg)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 평탄 객체 판별 (배열·null 제외). payload/error 타입 검증용.
|
|
118
|
+
* @param {any} v
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
function isPlainObject(v) {
|
|
122
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* WS 메시지 envelope 검증 (§6.2 schema). 위반 사유 배열을 반환한다 (빈 배열 = 유효).
|
|
127
|
+
*
|
|
128
|
+
* 던지지 않고 사유 목록을 돌려주는 이유: 호출부(디스패처/hub)가 사유를 ERROR envelope 의
|
|
129
|
+
* `details` 로 그대로 실어 보낼 수 있도록 (ADR-075 배열 표준).
|
|
130
|
+
*
|
|
131
|
+
* @param {any} msg
|
|
132
|
+
* @returns {string[]} 위반 메시지 목록 (없으면 빈 배열).
|
|
133
|
+
*/
|
|
134
|
+
export function validateWsMessage(msg) {
|
|
135
|
+
if (!isPlainObject(msg)) return ['message must be a non-null object']
|
|
136
|
+
const errors = []
|
|
137
|
+
if (msg.v !== WS_PROTOCOL_VERSION) errors.push(`v must be ${WS_PROTOCOL_VERSION}`)
|
|
138
|
+
if (typeof msg.id !== 'string') errors.push('id must be a string')
|
|
139
|
+
if (typeof msg.type !== 'string' || !WS_TYPE_PATTERN.test(msg.type)) {
|
|
140
|
+
errors.push(`type must match ${WS_TYPE_PATTERN.source}`)
|
|
141
|
+
}
|
|
142
|
+
if (!Number.isInteger(msg.ts)) errors.push('ts must be an integer')
|
|
143
|
+
if (msg.ns !== undefined && typeof msg.ns !== 'string') errors.push('ns must be a string')
|
|
144
|
+
if (msg.payload !== undefined && !isPlainObject(msg.payload)) errors.push('payload must be an object')
|
|
145
|
+
if (msg.error !== undefined && !isPlainObject(msg.error)) errors.push('error must be an object')
|
|
146
|
+
if (msg.ref !== undefined && typeof msg.ref !== 'string') errors.push('ref must be a string')
|
|
147
|
+
for (const key of Object.keys(msg)) {
|
|
148
|
+
if (!WS_ALLOWED_KEYS.has(key)) errors.push(`unknown key: ${key}`)
|
|
149
|
+
}
|
|
150
|
+
return errors
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* JSON 문자열 → 검증된 WS 메시지 envelope. 파싱/검증 실패 시 throw (silent 금지).
|
|
155
|
+
*
|
|
156
|
+
* @param {string} json - wire 평문 JSON (ASP 복호화 후 또는 `P:` 평문).
|
|
157
|
+
* @returns {Object} 검증 통과한 envelope.
|
|
158
|
+
* @throws {Error} JSON 파싱 실패 또는 schema 위반 (메시지에 사유 포함).
|
|
159
|
+
*/
|
|
160
|
+
export function parseWsMessage(json) {
|
|
161
|
+
let obj
|
|
162
|
+
try {
|
|
163
|
+
obj = JSON.parse(json)
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// 파싱 실패를 묻지 않고 wrap 후 throw. 호출부가 ERROR envelope 로 변환.
|
|
166
|
+
throw new Error(
|
|
167
|
+
`parseWsMessage: invalid JSON — ${err instanceof Error ? err.message : String(err)}`,
|
|
168
|
+
{ cause: err },
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
const errors = validateWsMessage(obj)
|
|
172
|
+
if (errors.length > 0) {
|
|
173
|
+
throw new Error(`parseWsMessage: invalid WS message — ${errors.join('; ')}`)
|
|
174
|
+
}
|
|
175
|
+
return obj
|
|
176
|
+
}
|