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.
Files changed (322) hide show
  1. package/.env +127 -0
  2. package/.env.example +186 -0
  3. package/.prettierrc.json +8 -0
  4. package/CHANGELOG.md +259 -0
  5. package/LICENSE +21 -0
  6. package/README.md +153 -0
  7. package/bin/mega-ws-hub.js +15 -0
  8. package/bin/mega.js +38 -0
  9. package/docker-compose.yml +201 -0
  10. package/eslint.config.js +57 -0
  11. package/infra/otel-collector-config.yaml +43 -0
  12. package/jsconfig.json +18 -0
  13. package/package.json +121 -0
  14. package/sample/crud/.env +18 -0
  15. package/sample/crud/.env.example +50 -0
  16. package/sample/crud/README.md +85 -0
  17. package/sample/crud/apps/main/app.config.js +114 -0
  18. package/sample/crud/apps/main/channels/chat-bus.js +115 -0
  19. package/sample/crud/apps/main/channels/chat-channel.js +145 -0
  20. package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
  21. package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
  22. package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
  23. package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
  24. package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
  25. package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
  26. package/sample/crud/apps/main/controllers/note-controller.js +116 -0
  27. package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
  28. package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
  29. package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
  30. package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
  31. package/sample/crud/apps/main/controllers/user-controller.js +34 -0
  32. package/sample/crud/apps/main/controllers/web-controller.js +137 -0
  33. package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
  34. package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
  35. package/sample/crud/apps/main/jobs/email-job.js +72 -0
  36. package/sample/crud/apps/main/locales/client/en.json +3 -0
  37. package/sample/crud/apps/main/locales/client/ko.json +3 -0
  38. package/sample/crud/apps/main/locales/server/en.json +316 -0
  39. package/sample/crud/apps/main/locales/server/ko.json +316 -0
  40. package/sample/crud/apps/main/middleware/web-auth.js +40 -0
  41. package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
  42. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
  43. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
  44. package/sample/crud/apps/main/models/note.js +71 -0
  45. package/sample/crud/apps/main/models/user.js +86 -0
  46. package/sample/crud/apps/main/public/css/app.css +101 -0
  47. package/sample/crud/apps/main/public/css/guide.css +137 -0
  48. package/sample/crud/apps/main/public/js/app.js +54 -0
  49. package/sample/crud/apps/main/public/js/perf.js +129 -0
  50. package/sample/crud/apps/main/public/js/theme-init.js +12 -0
  51. package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
  52. package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
  53. package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
  54. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  55. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  56. package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
  57. package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
  58. package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
  59. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
  60. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
  61. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
  62. package/sample/crud/apps/main/routes/auth.js +15 -0
  63. package/sample/crud/apps/main/routes/cron.js +14 -0
  64. package/sample/crud/apps/main/routes/guide.js +25 -0
  65. package/sample/crud/apps/main/routes/jobs.js +14 -0
  66. package/sample/crud/apps/main/routes/logs.js +28 -0
  67. package/sample/crud/apps/main/routes/metrics.js +13 -0
  68. package/sample/crud/apps/main/routes/notes.js +19 -0
  69. package/sample/crud/apps/main/routes/perf.js +47 -0
  70. package/sample/crud/apps/main/routes/redis.js +14 -0
  71. package/sample/crud/apps/main/routes/tracing.js +14 -0
  72. package/sample/crud/apps/main/routes/upload.js +16 -0
  73. package/sample/crud/apps/main/routes/users.js +54 -0
  74. package/sample/crud/apps/main/routes/web.js +23 -0
  75. package/sample/crud/apps/main/routes/worker.js +15 -0
  76. package/sample/crud/apps/main/routes/ws.js +30 -0
  77. package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
  78. package/sample/crud/apps/main/services/auth-service.js +74 -0
  79. package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
  80. package/sample/crud/apps/main/services/guide-service.js +145 -0
  81. package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
  82. package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
  83. package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
  84. package/sample/crud/apps/main/services/note-service.js +75 -0
  85. package/sample/crud/apps/main/services/perf-service.js +302 -0
  86. package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
  87. package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
  88. package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
  89. package/sample/crud/apps/main/services/user-service.js +65 -0
  90. package/sample/crud/apps/main/views/auth/login.ejs +57 -0
  91. package/sample/crud/apps/main/views/auth/register.ejs +71 -0
  92. package/sample/crud/apps/main/views/cron/index.ejs +92 -0
  93. package/sample/crud/apps/main/views/guide/index.ejs +24 -0
  94. package/sample/crud/apps/main/views/guide/page.ejs +64 -0
  95. package/sample/crud/apps/main/views/home.ejs +82 -0
  96. package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
  97. package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
  98. package/sample/crud/apps/main/views/logs/index.ejs +80 -0
  99. package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
  100. package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
  101. package/sample/crud/apps/main/views/notes/list.ejs +74 -0
  102. package/sample/crud/apps/main/views/notes/new.ejs +45 -0
  103. package/sample/crud/apps/main/views/perf/index.ejs +90 -0
  104. package/sample/crud/apps/main/views/redis/index.ejs +65 -0
  105. package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
  106. package/sample/crud/apps/main/views/upload/index.ejs +79 -0
  107. package/sample/crud/apps/main/views/users/edit.ejs +48 -0
  108. package/sample/crud/apps/main/views/users/list.ejs +81 -0
  109. package/sample/crud/apps/main/views/users/new.ejs +48 -0
  110. package/sample/crud/apps/main/views/worker/index.ejs +70 -0
  111. package/sample/crud/apps/main/views/ws/index.ejs +62 -0
  112. package/sample/crud/apps/main/workers/hash-worker.js +17 -0
  113. package/sample/crud/apps/main/workers/hash.task.js +22 -0
  114. package/sample/crud/ecosystem.config.cjs +9 -0
  115. package/sample/crud/mega.config.js +105 -0
  116. package/sample/crud/package-lock.json +5665 -0
  117. package/sample/crud/package.json +28 -0
  118. package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
  119. package/sample/crud/test/apps/main/auth-service.test.js +93 -0
  120. package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
  121. package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
  122. package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
  123. package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
  124. package/sample/crud/test/apps/main/email-job.test.js +76 -0
  125. package/sample/crud/test/apps/main/guide-service.test.js +68 -0
  126. package/sample/crud/test/apps/main/hash-task.test.js +30 -0
  127. package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
  128. package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
  129. package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
  130. package/sample/crud/test/apps/main/note-service.test.js +68 -0
  131. package/sample/crud/test/apps/main/perf-service.test.js +121 -0
  132. package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
  133. package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
  134. package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
  135. package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
  136. package/sample/crud/test/apps/main/user-service.test.js +65 -0
  137. package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
  138. package/sample/crud/vitest.config.js +8 -0
  139. package/sample/crud/yarn.lock +2142 -0
  140. package/sample/simple/.env.example +15 -0
  141. package/sample/simple/README.md +52 -0
  142. package/sample/simple/apps/main/app.config.js +35 -0
  143. package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
  144. package/sample/simple/apps/main/locales/client/en.json +3 -0
  145. package/sample/simple/apps/main/locales/client/ko.json +3 -0
  146. package/sample/simple/apps/main/locales/server/en.json +23 -0
  147. package/sample/simple/apps/main/locales/server/ko.json +23 -0
  148. package/sample/simple/apps/main/public/css/app.css +101 -0
  149. package/sample/simple/apps/main/public/hello.txt +1 -0
  150. package/sample/simple/apps/main/public/js/app.js +54 -0
  151. package/sample/simple/apps/main/public/js/theme-init.js +12 -0
  152. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  153. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  154. package/sample/simple/apps/main/routes/index.js +9 -0
  155. package/sample/simple/apps/main/routes/pages.js +12 -0
  156. package/sample/simple/apps/main/views/index.ejs +56 -0
  157. package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
  158. package/sample/simple/ecosystem.config.cjs +10 -0
  159. package/sample/simple/mega.config.js +27 -0
  160. package/sample/simple/package-lock.json +1851 -0
  161. package/sample/simple/package.json +25 -0
  162. package/sample/simple/test/apps/main/index.test.js +13 -0
  163. package/sample/simple/vitest.config.js +8 -0
  164. package/src/adapters/adapter-manager.js +305 -0
  165. package/src/adapters/adapter-options.js +208 -0
  166. package/src/adapters/file-adapter.js +350 -0
  167. package/src/adapters/file-session-adapter.js +363 -0
  168. package/src/adapters/index.js +38 -0
  169. package/src/adapters/maria-adapter.js +425 -0
  170. package/src/adapters/mega-adapter.js +511 -0
  171. package/src/adapters/mega-bus-adapter.js +81 -0
  172. package/src/adapters/mega-cache-adapter.js +94 -0
  173. package/src/adapters/mega-db-adapter.js +72 -0
  174. package/src/adapters/mega-lock-adapter.js +118 -0
  175. package/src/adapters/mega-log-sink-adapter.js +46 -0
  176. package/src/adapters/mega-session-adapter.js +72 -0
  177. package/src/adapters/mongo-adapter.js +396 -0
  178. package/src/adapters/nats-adapter.js +370 -0
  179. package/src/adapters/postgres-adapter.js +341 -0
  180. package/src/adapters/redis-adapter.js +331 -0
  181. package/src/adapters/redis-session-adapter.js +261 -0
  182. package/src/adapters/redlock-adapter.js +385 -0
  183. package/src/adapters/registry.js +157 -0
  184. package/src/adapters/sqlite-adapter.js +309 -0
  185. package/src/auth/index.js +103 -0
  186. package/src/cli/commands/console-cmd.js +56 -0
  187. package/src/cli/commands/new.js +101 -0
  188. package/src/cli/commands/routes.js +107 -0
  189. package/src/cli/commands/scaffold.js +120 -0
  190. package/src/cli/commands/test-cmd.js +45 -0
  191. package/src/cli/generators/index.js +368 -0
  192. package/src/cli/index.js +472 -0
  193. package/src/cli/template-engine.js +72 -0
  194. package/src/cli/ws-hub.js +582 -0
  195. package/src/core/ajv-mapper.js +80 -0
  196. package/src/core/boot.js +323 -0
  197. package/src/core/cluster-metrics.js +278 -0
  198. package/src/core/config-loader.js +115 -0
  199. package/src/core/config-validator.js +322 -0
  200. package/src/core/ctx-builder.js +253 -0
  201. package/src/core/envelope.js +88 -0
  202. package/src/core/error-mapper.js +116 -0
  203. package/src/core/formbody.js +69 -0
  204. package/src/core/hub-link.js +552 -0
  205. package/src/core/i18n.js +525 -0
  206. package/src/core/index.js +63 -0
  207. package/src/core/mega-app.js +1138 -0
  208. package/src/core/mega-cluster.js +232 -0
  209. package/src/core/mega-server.js +176 -0
  210. package/src/core/mega-service.js +41 -0
  211. package/src/core/migration-runner.js +196 -0
  212. package/src/core/multipart.js +282 -0
  213. package/src/core/openapi.js +114 -0
  214. package/src/core/router.js +388 -0
  215. package/src/core/routes-loader.js +57 -0
  216. package/src/core/scope-registry.js +53 -0
  217. package/src/core/security.js +275 -0
  218. package/src/core/services-loader.js +98 -0
  219. package/src/core/session-cleanup-schedule.js +57 -0
  220. package/src/core/session-store.js +55 -0
  221. package/src/core/session.js +414 -0
  222. package/src/core/static-assets.js +126 -0
  223. package/src/core/template.js +294 -0
  224. package/src/core/workers-manager.js +193 -0
  225. package/src/core/ws-compression.js +112 -0
  226. package/src/core/ws-controller.js +109 -0
  227. package/src/core/ws-message.js +176 -0
  228. package/src/core/ws-upgrade.js +445 -0
  229. package/src/errors/config-error.js +16 -0
  230. package/src/errors/http-errors.js +130 -0
  231. package/src/errors/index.js +19 -0
  232. package/src/errors/mega-error.js +34 -0
  233. package/src/eslint-plugin/index.js +15 -0
  234. package/src/eslint-plugin/no-direct-model-import.js +113 -0
  235. package/src/index.js +131 -0
  236. package/src/lib/asp/config.js +83 -0
  237. package/src/lib/asp/crypto.js +145 -0
  238. package/src/lib/asp/errors.js +49 -0
  239. package/src/lib/asp/nonce-cache.js +94 -0
  240. package/src/lib/asp/plugin.js +263 -0
  241. package/src/lib/asp/ws-terminator.js +101 -0
  242. package/src/lib/env-mapper.js +222 -0
  243. package/src/lib/hub-protocol.js +322 -0
  244. package/src/lib/index.js +42 -0
  245. package/src/lib/logger/telegram-core.js +150 -0
  246. package/src/lib/logger/telegram-transport.js +126 -0
  247. package/src/lib/mega-brute-force.js +225 -0
  248. package/src/lib/mega-circuit-breaker.js +412 -0
  249. package/src/lib/mega-cron.js +169 -0
  250. package/src/lib/mega-hash.js +179 -0
  251. package/src/lib/mega-health.js +91 -0
  252. package/src/lib/mega-job-queue.js +600 -0
  253. package/src/lib/mega-job-worker.js +295 -0
  254. package/src/lib/mega-job.js +140 -0
  255. package/src/lib/mega-logger.js +128 -0
  256. package/src/lib/mega-metrics.js +661 -0
  257. package/src/lib/mega-plugin.js +650 -0
  258. package/src/lib/mega-retry.js +95 -0
  259. package/src/lib/mega-schedule.js +507 -0
  260. package/src/lib/mega-shutdown.js +176 -0
  261. package/src/lib/mega-tracing.js +715 -0
  262. package/src/lib/mega-worker.js +653 -0
  263. package/src/lib/worker-runner/process-entry.js +30 -0
  264. package/src/lib/worker-runner/task-dispatch.js +72 -0
  265. package/src/lib/worker-runner/thread-entry.js +26 -0
  266. package/src/models/index.js +7 -0
  267. package/src/models/mega-model.js +151 -0
  268. package/src/test/index.js +288 -0
  269. package/templates/adapter/code.tpl +40 -0
  270. package/templates/adapter/test.tpl +13 -0
  271. package/templates/app/app.config.tpl +10 -0
  272. package/templates/app/route.tpl +10 -0
  273. package/templates/app/test.tpl +13 -0
  274. package/templates/channel/code.tpl +38 -0
  275. package/templates/channel/test.tpl +19 -0
  276. package/templates/controller/code.tpl +16 -0
  277. package/templates/controller/route.tpl +9 -0
  278. package/templates/controller/test.tpl +14 -0
  279. package/templates/job/code.tpl +23 -0
  280. package/templates/job/test.tpl +17 -0
  281. package/templates/locale/code.tpl +3 -0
  282. package/templates/locale/test.tpl +13 -0
  283. package/templates/middleware/code.tpl +13 -0
  284. package/templates/middleware/test.tpl +11 -0
  285. package/templates/migration/code.tpl +20 -0
  286. package/templates/migration/test.tpl +14 -0
  287. package/templates/model/code.tpl +21 -0
  288. package/templates/model/test.tpl +29 -0
  289. package/templates/project/app.config.tpl +8 -0
  290. package/templates/project/app.config.views.tpl +37 -0
  291. package/templates/project/ecosystem.config.tpl +10 -0
  292. package/templates/project/env.tpl +12 -0
  293. package/templates/project/gitignore.tpl +8 -0
  294. package/templates/project/locales/client/en.json.tpl +3 -0
  295. package/templates/project/locales/client/ko.json.tpl +3 -0
  296. package/templates/project/locales/server/en.json.tpl +17 -0
  297. package/templates/project/locales/server/ko.json.tpl +17 -0
  298. package/templates/project/mega.config.tpl +11 -0
  299. package/templates/project/package.tpl +25 -0
  300. package/templates/project/public/css/app.css +101 -0
  301. package/templates/project/public/js/app.js +54 -0
  302. package/templates/project/public/js/theme-init.js +12 -0
  303. package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  304. package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
  305. package/templates/project/readme.tpl +48 -0
  306. package/templates/project/route.test.tpl +13 -0
  307. package/templates/project/route.test.views.tpl +15 -0
  308. package/templates/project/route.tpl +10 -0
  309. package/templates/project/route.views.tpl +10 -0
  310. package/templates/project/views/index.ejs.tpl +58 -0
  311. package/templates/project/views/layout.ejs.tpl +73 -0
  312. package/templates/project/vitest.config.tpl +8 -0
  313. package/templates/route/code.tpl +11 -0
  314. package/templates/route/test.tpl +26 -0
  315. package/templates/schedule/code.tpl +19 -0
  316. package/templates/schedule/test.tpl +17 -0
  317. package/templates/service/code.tpl +18 -0
  318. package/templates/service/test.tpl +17 -0
  319. package/templates/worker/code.tpl +14 -0
  320. package/templates/worker/task.tpl +13 -0
  321. package/templates/worker/test.tpl +18 -0
  322. 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
+ }