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,25 @@
1
+ {
2
+ "name": "sample-simple",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "scripts": {
10
+ "dev": "NODE_ENV=development mega start",
11
+ "start": "NODE_ENV=production mega start",
12
+ "scheduler": "mega scheduler",
13
+ "worker": "mega worker",
14
+ "ws-hub": "mega-ws-hub",
15
+ "dev:all": "NODE_ENV=development concurrently -n server,scheduler,worker -c blue,green,magenta \"mega start\" \"mega scheduler\" \"mega worker\"",
16
+ "test": "mega test"
17
+ },
18
+ "dependencies": {
19
+ "mega-framework": "file:../.."
20
+ },
21
+ "devDependencies": {
22
+ "concurrently": "^9.0.0",
23
+ "vitest": "^2.0.0"
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ // @ts-check
2
+ import { describe, test, expect } from 'vitest'
3
+ import routes from '../../../apps/main/routes/index.js'
4
+
5
+ describe('main app index route', () => {
6
+ test('GET / ๋“ฑ๋ก + hello world ๋ฐ˜ํ™˜', async () => {
7
+ /** @type {Function|undefined} */
8
+ let handler
9
+ routes({ http: { get: (/** @type {string} */ _p, /** @type {Function} */ h) => void (handler = h) } })
10
+ expect(typeof handler).toBe('function')
11
+ expect(await /** @type {any} */ (handler)({}, {}, {})).toEqual({ message: 'Hello from sample-simple!' })
12
+ })
13
+ })
@@ -0,0 +1,8 @@
1
+ // @ts-check
2
+ import { defineConfig } from 'vitest/config'
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ include: ['test/**/*.test.js', 'apps/**/*.test.js', 'shared/**/*.test.js'],
7
+ },
8
+ })
@@ -0,0 +1,305 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaAdapterManager โ€” ์ „์—ญ ์–ด๋Œ‘ํ„ฐ ์ธ์Šคํ„ด์Šค ๋งค๋‹ˆ์ € (ADR-102).
4
+ *
5
+ * **๊ธ€๋กœ๋ฒŒ ๊ณต์œ  ๋ชจ๋ธ**(์˜ค๋„ˆ ํ™•์ •): ์–ด๋Œ‘ํ„ฐ ์ธ์Šคํ„ด์Šค๋Š” `mega.config.js` ์˜
6
+ * `services.databases/caches/buses.<globalKey>` ์— **๋”ฑ ํ•œ ๋ฒˆ** ์ •์˜๋˜๊ณ , ๋ถ€ํŒ… ์‹œ ๋ณธ ๋งค๋‹ˆ์ €๊ฐ€
7
+ * driver ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋กœ ์ธ์Šคํ„ด์Šคํ™”ํ•ด ๋ณด๊ด€ํ•œ๋‹ค. ์—ฌ๋Ÿฌ ์•ฑ์ด ๊ฐ™์€ backend ๋ฅผ ์จ๋„ ์ธ์Šคํ„ด์Šค๋Š” 1๊ฐœ
8
+ * (์•ฑ์€ `app.config.js` ์˜ `databases: { alias: globalKey }` ๋กœ **๋ณ„๋ช… ์ฐธ์กฐ**๋งŒ ํ•จ, ADR-064).
9
+ *
10
+ * registry / mega-health / mega-shutdown ๊ณผ ๊ฐ™์€ **๋ชจ๋“ˆ ๋ ˆ๋ฒจ ์‹ฑ๊ธ€ํ†ค** ํŒจํ„ด์ด๋‹ค โ€” ์ „์—ญ ์ž์›์„
11
+ * ํ•œ ๊ตฐ๋ฐ์„œ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ import ๋งŒ์œผ๋กœ ์–ด๋””์„œ๋“  ๊ฐ™์€ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ณธ๋‹ค.
12
+ *
13
+ * # ๋ถ€ํŒ…ยท์ข…๋ฃŒ (08-class-specs ยง3.2, docs/10 DisconnectingAdapters)
14
+ * 1. `buildFromGlobalConfig(global)` โ€” driver ๋งคํ•‘์œผ๋กœ `new AdapterClass(cfg)` (lifecycle 1๋‹จ๊ณ„).
15
+ * ๋™์‹œ์— LIFO shutdown hook ์„ **MegaApp ์ƒ์„ฑ๋ณด๋‹ค ๋จผ์ €** ๋“ฑ๋กํ•œ๋‹ค(์•„๋ž˜ "์ข…๋ฃŒ ์ˆœ์„œ" ์ฃผ์„).
16
+ * 2. `connectAll({ ping })` โ€” ๋ชจ๋“  ์ธ์Šคํ„ด์Šค `connect()` ๋ณ‘๋ ฌ. ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ๋ถ€ํŒ… abort(fail-fast)
17
+ * ํ•˜๋˜, ๊ทธ ์ „์— ์ด๋ฏธ connect ๋œ ์–ด๋Œ‘ํ„ฐ๋ฅผ LIFO ์ž๋™ cleanup ํ•ด leak ๋ฐฉ์ง€(ADR-102 QA M-A).
18
+ * `ping:true` ๋ฉด connect ํ›„ `healthCheck()` ๊นŒ์ง€ ๊ฒ€์ฆ.
19
+ * 3. `disconnectAll()` โ€” ๋“ฑ๋ก **์—ญ์ˆœ(LIFO)** disconnect. shutdown ์ค‘์ด๋ฏ€๋กœ ๊ฐœ๋ณ„ ์‹คํŒจ๋Š” warn ํ›„ ๊ณ„์†.
20
+ *
21
+ * # `ctx.db/cache/bus` ์™€์˜ ๊ด€๊ณ„
22
+ * `ctx.<domain>(alias)` ๋Š” ์•ฑ ๋ณ„๋ช…์„ globalKey ๋กœ ๋ฐ”๊พผ ๋’ค {@link get} ์œผ๋กœ ๋ณธ ๋งค๋‹ˆ์ €์˜ ๊ณต์œ 
23
+ * ์ธ์Šคํ„ด์Šค๋ฅผ ๋Œ๋ ค์ค€๋‹ค (src/core/ctx-builder.js). ๋“ฑ๋ก ์•ˆ ๋œ ํ‚ค๋Š” ์ฆ‰์‹œ throw (silent X).
24
+ *
25
+ * @module adapters/adapter-manager
26
+ */
27
+ import { MegaConfigError } from '../errors/config-error.js'
28
+ import { MegaShutdown } from '../lib/mega-shutdown.js'
29
+ import { buildAdapterEnvConfig } from '../lib/env-mapper.js'
30
+ import * as Registry from './registry.js'
31
+ import { MegaDbAdapter } from './mega-db-adapter.js'
32
+ import { MegaCacheAdapter } from './mega-cache-adapter.js'
33
+ import { MegaBusAdapter } from './mega-bus-adapter.js'
34
+ import { MegaLockAdapter } from './mega-lock-adapter.js'
35
+
36
+ /**
37
+ * ctx ๋„๋ฉ”์ธ โ†” config ํ‚ค โ†” ๋„๋ฉ”์ธ ๋ฒ ์ด์Šค ๋งคํ•‘ (canonical: db/cache/bus + lock, docs/03 ยง581 + ADR-113).
38
+ * `expect` ๋Š” driver ๊ฐ€ ์—‰๋šฑํ•œ ๋„๋ฉ”์ธ์— ๋“ฑ๋ก๋˜๋Š” ์‚ฌ๊ณ ๋ฅผ ๋ถ€ํŒ… ์‹œ ์žก๊ธฐ ์œ„ํ•œ instanceof ๊ฒ€์ฆ์šฉ (๋„๋ฉ”์ธ ๋ฒ ์ด์Šค).
39
+ * @type {Record<'db'|'cache'|'bus'|'lock', { configKey: 'databases'|'caches'|'buses'|'locks', expect: typeof import('./mega-adapter.js').MegaAdapter }>}
40
+ */
41
+ const DOMAIN_SPEC = {
42
+ db: { configKey: 'databases', expect: MegaDbAdapter },
43
+ cache: { configKey: 'caches', expect: MegaCacheAdapter },
44
+ bus: { configKey: 'buses', expect: MegaBusAdapter },
45
+ lock: { configKey: 'locks', expect: MegaLockAdapter },
46
+ }
47
+
48
+ /** shutdown hook ์ด๋ฆ„ โ€” ์žฌ๋นŒ๋“œ/๋ฆฌ์…‹ ์‹œ ์ค‘๋ณต ๋“ฑ๋ก ๋ฐฉ์ง€์šฉ ๊ณ ์ • ํ‚ค. */
49
+ const SHUTDOWN_HOOK = 'adapters:disconnect'
50
+
51
+ /**
52
+ * @typedef {object} AdapterEntry
53
+ * @property {'db'|'cache'|'bus'|'lock'} domain
54
+ * @property {string} key - globalKey (services.<configKey>.<key>).
55
+ * @property {string} driver
56
+ * @property {import('./mega-adapter.js').MegaAdapter} adapter
57
+ */
58
+
59
+ /** @type {Map<string, AdapterEntry>} `${domain}:${key}` โ†’ entry. */
60
+ let instances = new Map()
61
+ /** @type {string[]} ์‚ฝ์ž…(=connect) ์ˆœ์„œ โ€” disconnect LIFO ์šฉ. */
62
+ let order = []
63
+
64
+ /**
65
+ * `${domain}:${key}` ํ•ฉ์„ฑ id.
66
+ * @param {string} domain @param {string} key @returns {string}
67
+ */
68
+ function idOf(domain, key) {
69
+ return `${domain}:${key}`
70
+ }
71
+
72
+ /**
73
+ * ์ „์—ญ config(`services.databases/caches/buses`)๋กœ๋ถ€ํ„ฐ ์–ด๋Œ‘ํ„ฐ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ ๋‹ค (lifecycle 1๋‹จ๊ณ„).
74
+ *
75
+ * ๊ฐ ๋„๋ฉ”์ธ์˜ `<globalKey>: { driver, ...opts }` ๋งˆ๋‹ค `registry.resolve(driver)` โ†’ `new Cls(opts)`.
76
+ * driver ๋ฏธ๋“ฑ๋ก(๋นŒํŠธ์ธ์€ import ์‹œ ์ž๊ธฐ๋“ฑ๋ก)ยท๋ฏธ์ง€์ •ยท๋„๋ฉ”์ธ ๋ถˆ์ผ์น˜๋Š” ์ฆ‰์‹œ throw (๋ถ€ํŒ… abort, fail-fast).
77
+ *
78
+ * # ์ข…๋ฃŒ ์ˆœ์„œ (์ค‘์š”)
79
+ * ๋ณธ ํ•จ์ˆ˜๊ฐ€ LIFO shutdown hook ์„ ๋“ฑ๋กํ•˜๋ฏ€๋กœ, **MegaApp ์ƒ์„ฑ๋ณด๋‹ค ๋จผ์ €** ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค. MegaShutdown
80
+ * ์€ LIFO(๋‚˜์ค‘ ๋“ฑ๋ก์ด ๋จผ์ € ์‹คํ–‰) โ€” ์–ด๋Œ‘ํ„ฐ hook ์„ ์•ฑ hook ๋ณด๋‹ค ๋จผ์ € ๋“ฑ๋กํ•˜๋ฉด, ์ข…๋ฃŒ ์‹œ ์•ฑ(HTTP/WS)์ด
81
+ * ๋จผ์ € ๋‹ซํžˆ๊ณ  ๊ทธ ๋‹ค์Œ ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๋Š๊ธด๋‹ค(docs/10: ClosingHttp โ†’ โ€ฆ โ†’ DisconnectingAdapters).
82
+ *
83
+ * @param {{ services?: Record<string, any> }} [globalConfig] - mega.config.js (default export).
84
+ * @param {{ registerShutdownHook?: boolean }} [opts] - ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ์šฉ์œผ๋กœ hook ๋“ฑ๋ก์„ ๋Œ ์ˆ˜ ์žˆ์Œ(๊ธฐ๋ณธ on).
85
+ * @returns {void}
86
+ */
87
+ export function buildFromGlobalConfig(globalConfig, { registerShutdownHook = true } = {}) {
88
+ const services = globalConfig?.services ?? {}
89
+ for (const [domain, { configKey, expect }] of Object.entries(DOMAIN_SPEC)) {
90
+ const map = services[configKey]
91
+ if (map === undefined || map === null) continue
92
+ if (typeof map !== 'object' || Array.isArray(map)) {
93
+ throw new MegaConfigError(
94
+ 'adapter.invalid_services',
95
+ `services.${configKey} must be a plain object of { key: { driver, ... } }.`,
96
+ { details: { configKey, type: Array.isArray(map) ? 'array' : typeof map } },
97
+ )
98
+ }
99
+ for (const [key, cfg] of Object.entries(map)) {
100
+ if (!cfg || typeof cfg !== 'object' || typeof (/** @type {any} */ (cfg).driver) !== 'string') {
101
+ throw new MegaConfigError(
102
+ 'adapter.missing_driver',
103
+ `services.${configKey}.${key} must specify a string "driver".`,
104
+ { details: { domain, key } },
105
+ )
106
+ }
107
+ const driver = /** @type {any} */ (cfg).driver
108
+ const AdapterClass = Registry.resolve(driver) // ๋ฏธ๋“ฑ๋ก โ†’ adapter.unknown_driver
109
+ // .env ์ž๋™ ์ฃผ์ž… (12-factor, ADR-109) โ€” `envPrefix` ์ง€์ • ์‹œ MEGA_<PREFIX>_* ๋ฅผ ์–ด๋Œ‘ํ„ฐ ์˜ต์…˜์œผ๋กœ
110
+ // ๋งคํ•‘ํ•ด ํŒŒ์ผ config ์œ„์— ๋ณ‘ํ•ฉ(env ๊ฐ€ ์šฐ์„ ). ๋ฏธ์ง€์ •์ด๋ฉด ๋ฌด์‹œ(๊ธฐ์กด ๋™์ž‘ ๋ฌด๋ณ€๊ฒฝ).
111
+ const effectiveCfg = mergeEnvConfig(/** @type {Record<string, any>} */ (cfg))
112
+ const adapter = new AdapterClass(effectiveCfg)
113
+ // driver ๊ฐ€ ์—‰๋šฑํ•œ ๋„๋ฉ”์ธ ํ‚ค์— ๋“ค์–ด๊ฐ„ ์‚ฌ๊ณ (์˜ˆ: redis ๋ฅผ databases ์—)๋ฅผ ๋ถ€ํŒ… ์‹œ ์žก๋Š”๋‹ค.
114
+ if (!(adapter instanceof expect)) {
115
+ throw new MegaConfigError(
116
+ 'adapter.domain_mismatch',
117
+ `driver "${driver}" (services.${configKey}.${key}) is not a ${expect.name}.`,
118
+ { details: { domain, key, driver, expected: expect.name } },
119
+ )
120
+ }
121
+ const id = idOf(domain, key)
122
+ instances.set(id, { domain: /** @type {'db'|'cache'|'bus'|'lock'} */ (domain), key, driver, adapter })
123
+ order.push(id)
124
+ }
125
+ }
126
+
127
+ if (registerShutdownHook) {
128
+ // ์žฌ๋นŒ๋“œ ์•ˆ์ „ โ€” ํ•ญ์ƒ 1๊ฐœ๋งŒ ์œ ์ง€(MegaApp ์˜ hublink hook ํŒจํ„ด๊ณผ ๋™์ผ).
129
+ MegaShutdown.unregister(SHUTDOWN_HOOK)
130
+ MegaShutdown.register(SHUTDOWN_HOOK, async () => disconnectAll())
131
+ }
132
+ }
133
+
134
+ /**
135
+ * `envPrefix` ๊ฐ€ ์žˆ์œผ๋ฉด `MEGA_<PREFIX>_*` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์–ด๋Œ‘ํ„ฐ ์˜ต์…˜์œผ๋กœ ๋งคํ•‘ํ•ด ํŒŒ์ผ config ์œ„์— ๋ณ‘ํ•ฉํ•œ๋‹ค
136
+ * (12-factor, ADR-109). env ๊ฐ€ ํŒŒ์ผ ๊ฐ’์„ **๋ฎ์–ด์“ด๋‹ค**(๋ฐฐํฌ๋ณ„ ์˜ค๋ฒ„๋ผ์ด๋“œ). pool/options ๋Š” 1๋‹จ๊ณ„ deep-merge.
137
+ * `envPrefix` ํ‚ค๋Š” ์–ด๋Œ‘ํ„ฐ์— ๋„˜๊ธฐ์ง€ ์•Š๋Š”๋‹ค(๋“œ๋ผ์ด๋ฒ„ ๋ฌด๊ด€ ๋ฉ”ํƒ€).
138
+ *
139
+ * `cfg.driver` ๋ฅผ env-mapper ์— ์ „๋‹ฌํ•ด OPTIONS_* ํ‚ค๋ฅผ driver ํ‘œ๊ธฐ(camel/snake)๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค
140
+ * (camelCase ๋“œ๋ผ์ด๋ฒ„์— snake ํ‚ค๊ฐ€ ์กฐ์šฉํžˆ ๋ฌด์‹œ๋˜๋˜ ๋ฒ„๊ทธ ๊ต์ •).
141
+ *
142
+ * @param {Record<string, any>} cfg - services.<configKey>.<key> ์„ค์ •.
143
+ * @returns {Record<string, any>} ๋ณ‘ํ•ฉ๋œ config (envPrefix ์—†์œผ๋ฉด cfg ๊ทธ๋Œ€๋กœ).
144
+ */
145
+ function mergeEnvConfig(cfg) {
146
+ const envPrefix = cfg.envPrefix
147
+ if (typeof envPrefix !== 'string' || envPrefix.length === 0) return cfg
148
+ const envCfg = buildAdapterEnvConfig(envPrefix, process.env, { driver: cfg.driver })
149
+ const merged = { ...cfg, ...envCfg }
150
+ if (cfg.pool || envCfg.pool) merged.pool = { ...(cfg.pool ?? {}), ...(envCfg.pool ?? {}) }
151
+ if (cfg.options || envCfg.options) merged.options = { ...(cfg.options ?? {}), ...(envCfg.options ?? {}) }
152
+ delete merged.envPrefix
153
+ return merged
154
+ }
155
+
156
+ /**
157
+ * ๋„๋ฉ”์ธ+globalKey ๋กœ ๊ณต์œ  ์–ด๋Œ‘ํ„ฐ ์ธ์Šคํ„ด์Šค ์กฐํšŒ. ์—†์œผ๋ฉด `adapter.not_registered` throw (silent X).
158
+ *
159
+ * @param {'db'|'cache'|'bus'|'lock'} domain
160
+ * @param {string} key - globalKey.
161
+ * @returns {import('./mega-adapter.js').MegaAdapter}
162
+ */
163
+ export function get(domain, key) {
164
+ const entry = instances.get(idOf(domain, key))
165
+ if (entry === undefined) {
166
+ throw new MegaConfigError(
167
+ 'adapter.not_registered',
168
+ `No ${domain} adapter registered for key "${key}". Registered ${domain}: [${listDomain(domain).join(', ') || '(none)'}].`,
169
+ { details: { domain, key, registered: listDomain(domain) } },
170
+ )
171
+ }
172
+ return entry.adapter
173
+ }
174
+
175
+ /**
176
+ * ๋“ฑ๋ก ์—ฌ๋ถ€ (Boolean โ€” `has*`, ADR-036).
177
+ * @param {'db'|'cache'|'bus'|'lock'} domain @param {string} key @returns {boolean}
178
+ */
179
+ export function has(domain, key) {
180
+ return instances.has(idOf(domain, key))
181
+ }
182
+
183
+ /**
184
+ * ํ•œ ๋„๋ฉ”์ธ์— ๋“ฑ๋ก๋œ globalKey ๋ชฉ๋ก.
185
+ * @param {'db'|'cache'|'bus'|'lock'} domain @returns {string[]}
186
+ */
187
+ function listDomain(domain) {
188
+ return order.filter((id) => instances.get(id)?.domain === domain).map((id) => /** @type {AdapterEntry} */ (instances.get(id)).key)
189
+ }
190
+
191
+ /**
192
+ * ์ „์ฒด ์ธ์Šคํ„ด์Šค ์Šค๋ƒ…์ƒท (๋””๋ฒ„๊ทธยทํ…Œ์ŠคํŠธยทํ—ฌ์Šค์šฉ). ๋“ฑ๋ก ์ˆœ์„œ๋Œ€๋กœ.
193
+ * @returns {Array<{ domain: 'db'|'cache'|'bus'|'lock', key: string, driver: string, state: string }>}
194
+ */
195
+ export function list() {
196
+ return order.map((id) => {
197
+ const e = /** @type {AdapterEntry} */ (instances.get(id))
198
+ return { domain: e.domain, key: e.key, driver: e.driver, state: e.adapter.state }
199
+ })
200
+ }
201
+
202
+ /**
203
+ * ์ „์ฒด ์–ด๋Œ‘ํ„ฐ ์—”ํŠธ๋ฆฌ(์ธ์Šคํ„ด์Šค ํฌํ•จ)๋ฅผ ๋“ฑ๋ก ์ˆœ์„œ๋Œ€๋กœ ๋ฐ˜ํ™˜ โ€” ์™ธ๋ถ€ ๋ถ€์ฐฉ์ž(`MegaTracing`)๊ฐ€
204
+ * ๋ชจ๋“  ๊ณต์œ  ์–ด๋Œ‘ํ„ฐ์— hook ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ผ๊ด„ ๊ตฌ๋…ํ•  ๋•Œ ์“ด๋‹ค. {@link list} ์™€ ๋‹ฌ๋ฆฌ raw ์–ด๋Œ‘ํ„ฐ ์ธ์Šคํ„ด์Šค๋ฅผ
205
+ * ๋…ธ์ถœํ•˜๋ฏ€๋กœ ๋””๋ฒ„๊ทธยทํŠธ๋ ˆ์ด์‹ฑ ๋ถ€์ฐฉ ์™ธ ์šฉ๋„๋กœ๋Š” ์‹ ์ค‘ํžˆ ์‚ฌ์šฉ.
206
+ *
207
+ * @returns {AdapterEntry[]}
208
+ */
209
+ export function entries() {
210
+ return order.map((id) => /** @type {AdapterEntry} */ (instances.get(id)))
211
+ }
212
+
213
+ /**
214
+ * ๋ชจ๋“  ์–ด๋Œ‘ํ„ฐ connect (๋ถ€ํŒ… 6๋‹จ๊ณ„). ๋ณ‘๋ ฌ ์‹œ๋„ํ•˜๋˜ **fail-fast + ์ž๋™ cleanup** (ADR-102 QA M-A).
215
+ * ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ๋ถ€ํŒ…์„ abort ํ•˜๊ธฐ ์œ„ํ•ด ์ฒซ ์‹คํŒจ ์—๋Ÿฌ๋ฅผ throw ํ•˜๋Š”๋ฐ, ๊ทธ ์ „์— ์ด๋ฏธ connect ๋œ
216
+ * ์–ด๋Œ‘ํ„ฐ๋ฅผ **๋“ฑ๋ก ์—ญ์ˆœ(LIFO)์œผ๋กœ best-effort disconnect** ํ•ด์„œ ์—ฐ๊ฒฐ ํ’€ leak ์„ ๋ง‰๋Š”๋‹ค โ€” ํ˜ธ์ถœ์ž๊ฐ€
217
+ * catch ์•ˆ ํ•ด๋„ ๋งค๋‹ˆ์ € ๋ ˆ๋ฒจ์—์„œ ์ž์›์„ ํšŒ์ˆ˜ํ•œ๋‹ค(call-site CLI ์ฑ…์ž„์—๋งŒ ์˜์กดํ•˜์ง€ ์•Š์Œ).
218
+ *
219
+ * `ping:true` ๋ฉด connect ์งํ›„ `healthCheck()` ๋ฅผ 1ํšŒ ํ˜ธ์ถœํ•ด ok=false ๋ฉด `adapter.health_failed`
220
+ * throw (ํ—ฌ์Šค๊นŒ์ง€ ๊ฒ€์ฆ). ping ์‹คํŒจ ์–ด๋Œ‘ํ„ฐ๋„ connect ์ž์ฒด๋Š” ์„ฑ๊ณตํ–ˆ์œผ๋ฏ€๋กœ cleanup ๋Œ€์ƒ์— ํฌํ•จ๋œ๋‹ค.
221
+ *
222
+ * cleanup ์ค‘ disconnect ๊ฐ€ ์‹คํŒจํ•ด๋„ ์›๋ž˜ connect ์‹คํŒจ ์—๋Ÿฌ๋ฅผ ์šฐ์„  ์ „ํŒŒํ•œ๋‹ค โ€” cleanup ์‹คํŒจ๋Š”
223
+ * ๋น„์น˜๋ช…์ ์ด๋ผ warn ๋กœ๊ทธ๋งŒ ๋‚จ๊ธฐ๊ณ  ์‚ผํ‚จ๋‹ค(๋ถ€ํŒ… abort ์ •๋ฆฌ ์ค‘ ํ•œ ์–ด๋Œ‘ํ„ฐ ์ •๋ฆฌ ์‹คํŒจ๊ฐ€
224
+ * ์ง„์งœ ์›์ธ(connect ์‹คํŒจ)์„ ๋ฎ์œผ๋ฉด ์•ˆ ๋จ).
225
+ *
226
+ * @param {{ logger?: any, ping?: boolean }} [opts]
227
+ * @returns {Promise<void>}
228
+ */
229
+ export async function connectAll({ logger, ping = false } = {}) {
230
+ /** @type {Set<number>} connect() ์„ฑ๊ณตํ•œ ์–ด๋Œ‘ํ„ฐ์˜ order ์ธ๋ฑ์Šค (ping ์ „ ๊ธฐ์ค€ โ€” ping ์‹คํŒจ๋„ cleanup). */
231
+ const connectedIdx = new Set()
232
+ // canonical ๋ณ‘๋ ฌ์ด๋˜ reject ๋ฅผ ์ฆ‰์‹œ ์ „ํŒŒํ•˜์ง€ ์•Š๊ณ  allSettled ๋กœ ์ „๋ถ€ ์ˆ˜์ง‘ โ€” ๋ถ€๋ถ„ ์„ฑ๊ณต๋ถ„์„
233
+ // cleanup ํ•œ ๋’ค ์ฒซ ์‹คํŒจ๋ฅผ ๋˜์ง€๊ธฐ ์œ„ํ•จ.
234
+ const results = await Promise.allSettled(
235
+ order.map(async (id, idx) => {
236
+ const e = /** @type {AdapterEntry} */ (instances.get(id))
237
+ await e.adapter.connect()
238
+ connectedIdx.add(idx) // connect ์„ฑ๊ณต โ€” ping ์‹คํŒจํ•˜๋”๋ผ๋„ cleanup ๋Œ€์ƒ.
239
+ if (ping) {
240
+ const health = await e.adapter.healthCheck()
241
+ if (!health?.ok) {
242
+ throw new MegaConfigError(
243
+ 'adapter.health_failed',
244
+ `boot ping failed for ${e.domain}:${e.key} (${e.driver}).`,
245
+ { details: { domain: e.domain, key: e.key, driver: e.driver, health } },
246
+ )
247
+ }
248
+ }
249
+ logger?.info?.(
250
+ { adapter: e.adapter.constructor.name, domain: e.domain, key: e.key, driver: e.driver, state: e.adapter.state },
251
+ 'adapter connected',
252
+ )
253
+ }),
254
+ )
255
+
256
+ const firstRejected = results.find((r) => r.status === 'rejected')
257
+ if (firstRejected === undefined) return // ์ „์› ์„ฑ๊ณต
258
+
259
+ // fail-fast cleanup: ์ด๋ฏธ connect ๋œ ์–ด๋Œ‘ํ„ฐ๋ฅผ ๋“ฑ๋ก ์—ญ์ˆœ(LIFO)์œผ๋กœ disconnect (best-effort).
260
+ for (let i = order.length - 1; i >= 0; i--) {
261
+ if (!connectedIdx.has(i)) continue
262
+ const e = /** @type {AdapterEntry} */ (instances.get(order[i]))
263
+ try {
264
+ await e.adapter.disconnect()
265
+ logger?.debug?.({ domain: e.domain, key: e.key }, 'adapter cleanup disconnected (connectAll abort)')
266
+ } catch (cleanupErr) {
267
+ // cleanup ์‹คํŒจ๋Š” ๋น„์น˜๋ช…์  โ€” ์›๋ณธ connect ์‹คํŒจ ์—๋Ÿฌ๋ฅผ ์šฐ์„  ์ „ํŒŒํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„  throw ํ•˜์ง€ ์•Š์Œ.
268
+ logger?.warn?.(
269
+ { err: cleanupErr, domain: e.domain, key: e.key },
270
+ 'adapter cleanup disconnect failed during connectAll abort (continuing, original connect error wins)',
271
+ )
272
+ }
273
+ }
274
+ // ์›๋ž˜ ๋ถ€ํŒ… ์‹คํŒจ ์›์ธ(์ฒซ connect/ping ์—๋Ÿฌ)์„ ๊ทธ๋Œ€๋กœ ์ „ํŒŒ โ†’ ํ˜ธ์ถœ์ž๋Š” ๋™์ผํ•œ fail-fast ๋™์ž‘์„ ๋ณธ๋‹ค.
275
+ throw /** @type {PromiseRejectedResult} */ (firstRejected).reason
276
+ }
277
+
278
+ /**
279
+ * ๋ชจ๋“  ์–ด๋Œ‘ํ„ฐ disconnect โ€” ๋“ฑ๋ก **์—ญ์ˆœ(LIFO)** (docs/10 ยง6, 07-sequence-diagrams ยง6). graceful
280
+ * shutdown ์ค‘์ด๋ฏ€๋กœ ๊ฐœ๋ณ„ disconnect ์‹คํŒจ๋Š” ๋น„์น˜๋ช…์ : warn ํ›„ ๋‹ค์Œ ์–ด๋Œ‘ํ„ฐ๋กœ ๊ณ„์†ํ•œ๋‹ค( * ํ•œ ์–ด๋Œ‘ํ„ฐ ์ •๋ฆฌ ์‹คํŒจ๊ฐ€ ๋‚˜๋จธ์ง€ ์ •๋ฆฌ๋ฅผ ๋ง‰์œผ๋ฉด ์•ˆ ๋จ).
281
+ *
282
+ * @param {{ logger?: any }} [opts]
283
+ * @returns {Promise<void>}
284
+ */
285
+ export async function disconnectAll({ logger } = {}) {
286
+ for (let i = order.length - 1; i >= 0; i--) {
287
+ const e = /** @type {AdapterEntry} */ (instances.get(order[i]))
288
+ try {
289
+ await e.adapter.disconnect()
290
+ logger?.debug?.({ domain: e.domain, key: e.key }, 'adapter disconnected')
291
+ } catch (err) {
292
+ logger?.warn?.({ err, domain: e.domain, key: e.key }, 'adapter disconnect failed (continuing shutdown)')
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * ํ…Œ์ŠคํŠธ์šฉ reset โ€” ์ธ์Šคํ„ด์Šค ๋น„์šฐ๊ณ  shutdown hook ํ•ด์ œ.
299
+ * @returns {void}
300
+ */
301
+ export function _reset() {
302
+ instances = new Map()
303
+ order = []
304
+ MegaShutdown.unregister(SHUTDOWN_HOOK)
305
+ }
@@ -0,0 +1,208 @@
1
+ // @ts-check
2
+ /**
3
+ * ์–ด๋Œ‘ํ„ฐ ์˜ต์…˜ ํ‘œ์ค€ํ™” ๊ณต์šฉ ํ—ฌํผ (ADR-109).
4
+ *
5
+ * Postgres/Maria/Mongo 3 ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๊ณต์œ ํ•˜๋Š” **ํ†ตํ•ฉ ์˜ต์…˜ ๊ตฌ์กฐ**์˜ ํŒŒ์‹ฑยท๊ฒ€์ฆ ๋กœ์ง์„ ํ•œ ๊ณณ์— ๋ชจ์€๋‹ค.
6
+ * ๊ฐ ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๊ฐ™์€ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๋ฐ›๋„๋ก ํ•ด 12-factor(.env ์™ธ๋ถ€ ์ฃผ์ž…) + advanced ์˜ต์…˜(pool/options) ์„
7
+ * ์ผ๊ด€๋˜๊ฒŒ ์ง€์›ํ•œ๋‹ค.
8
+ *
9
+ * # ํ†ตํ•ฉ ์‹œ๊ทธ๋‹ˆ์ฒ˜
10
+ * ```js
11
+ * {
12
+ * driver, // ๋งค๋‹ˆ์ €๊ฐ€ ์‚ฌ์šฉ โ€” ์–ด๋Œ‘ํ„ฐ๋Š” ๋ฌด์‹œ
13
+ * url? | connectionString?, // ์—ฐ๊ฒฐ URL (connectionString ์€ deprecated ๋ณ„์นญ)
14
+ * host?, port?, user?, password?, // discrete ์—ฐ๊ฒฐ (url ๊ณผ ์ƒํ˜ธ ๋ฐฐํƒ€)
15
+ * database? | dbName?, // SQL=database / Mongo=dbName
16
+ * pool?: { min?, max?, idleTimeoutMs?, acquireTimeoutMs?, maxLifetimeMs? }, // ๊ณตํ†ต ํ’€ ์ธํ„ฐํŽ˜์ด์Šค
17
+ * options?: { ...driver-native passthrough } // ๋“œ๋ผ์ด๋ฒ„ ํŠนํ™” ์˜ต์…˜ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ
18
+ * }
19
+ * ```
20
+ *
21
+ * # ์—ฐ๊ฒฐ ๋ชจ๋“œ ์ถฉ๋Œยทํ•„์ˆ˜ (๋ชจํ˜ธํ•˜๋ฉด ๋ถ€ํŒ… ์‹œ fail-fast)
22
+ * - `url` + discrete ์—ฐ๊ฒฐํ•„๋“œ(host/port/user/password[/database]) ๋™์‹œ ์ง€์ • โ†’ `adapter.connection_conflict`.
23
+ * - `url`ยทdiscrete ๋‘˜ ๋‹ค ์—†์Œ โ†’ `adapter.connection_required`.
24
+ * - `url` + `pool`/`options` ์กฐํ•ฉ์€ **ํ—ˆ์šฉ** โ€” url ์€ connection ์ •๋ณด๋งŒ, pool/options ๋Š” ๋ณ„๋„ ์ถ•.
25
+ *
26
+ * # ๊ณตํ†ต ํ’€ ์ธํ„ฐํŽ˜์ด์Šค โ†’ ๋“œ๋ผ์ด๋ฒ„ ํ’€ ์˜ต์…˜ ๋งคํ•‘ (๋‹จ์œ„ ์ฃผ์˜, ๋“œ๋ผ์ด๋ฒ„ ํƒ€์ž… ์ง์ ‘ ํ™•์ธ)
27
+ * | ๊ณตํ†ต ํ‚ค | pg | mariadb | mongodb |
28
+ * |--------------------|----------------------------|--------------------------|--------------------|
29
+ * | min | min | minimumIdle | minPoolSize |
30
+ * | max | max | connectionLimit | maxPoolSize |
31
+ * | idleTimeoutMs (ms) | idleTimeoutMillis (ms) | idleTimeout (**์ดˆ**) | maxIdleTimeMS (ms) |
32
+ * | acquireTimeoutMs | connectionTimeoutMillis(ms)| acquireTimeout (ms) | waitQueueTimeoutMS |
33
+ * | maxLifetimeMs (ms) | maxLifetimeSeconds (**์ดˆ**)| (๋ฏธ์ง€์›) | (๋ฏธ์ง€์›) |
34
+ * `idleTimeoutMs`โ†’mariadbยท`maxLifetimeMs`โ†’pg ๋Š” msโ†’์ดˆ ๋ณ€ํ™˜(รท1000, floor). mariadb/mongo ๋Š”
35
+ * ํ’€ maxLifetime ๋™๋“ฑ๋ฌผ์ด ์—†์–ด `maxLifetimeMs` ์ง€์ • ์‹œ `adapter.invalid_option` throw(silent ๋ฌด์‹œ X).
36
+ *
37
+ * @module adapters/adapter-options
38
+ */
39
+ import { MegaValidationError } from '../errors/http-errors.js'
40
+
41
+ /**
42
+ * `adapter.invalid_option` ์ƒ์„ฑ ํ—ฌํผ.
43
+ * @param {string} message @param {Record<string, unknown>} details
44
+ * @returns {MegaValidationError}
45
+ */
46
+ function invalidOption(message, details) {
47
+ return new MegaValidationError('adapter.invalid_option', message, { details })
48
+ }
49
+
50
+ /**
51
+ * ์–‘์˜ ์ •์ˆ˜(> 0) ๊ฒ€์ฆ (undefined ํ†ต๊ณผ). ์œ„๋ฐ˜ ์‹œ `adapter.invalid_option` throw.
52
+ * @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
53
+ */
54
+ export function assertPositiveInt(name, value, extra = {}) {
55
+ if (value === undefined) return
56
+ if (!Number.isInteger(value) || /** @type {number} */ (value) <= 0) {
57
+ throw invalidOption(`"${name}" must be a positive integer.`, { option: name, value, ...extra })
58
+ }
59
+ }
60
+
61
+ /**
62
+ * ์Œ ์•„๋‹Œ ์ •์ˆ˜(>= 0) ๊ฒ€์ฆ (undefined ํ†ต๊ณผ). 0 ์€ ๋“œ๋ผ์ด๋ฒ„์—์„œ "๋ฌด์ œํ•œ/์ฆ‰์‹œ/never" ์˜๋ฏธ๋กœ ํ—ˆ์šฉ.
63
+ * @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
64
+ */
65
+ export function assertNonNegativeInt(name, value, extra = {}) {
66
+ if (value === undefined) return
67
+ if (!Number.isInteger(value) || /** @type {number} */ (value) < 0) {
68
+ throw invalidOption(`"${name}" must be a non-negative integer.`, { option: name, value, ...extra })
69
+ }
70
+ }
71
+
72
+ /**
73
+ * ํ”Œ๋ ˆ์ธ ๊ฐ์ฒด ๊ฒ€์ฆ (undefined ํ†ต๊ณผ). ๋ฐฐ์—ดยทnullยทnon-object ๊ฑฐ๋ถ€.
74
+ * @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
75
+ */
76
+ export function assertPlainObject(name, value, extra = {}) {
77
+ if (value === undefined) return
78
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
79
+ throw invalidOption(`"${name}" must be a plain object.`, { option: name, type: Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value, ...extra })
80
+ }
81
+ }
82
+
83
+ /**
84
+ * ์—ฐ๊ฒฐ ๋ชจ๋“œ ๊ฒฐ์ •(url XOR discrete) + ์ถฉ๋Œยทํ•„์ˆ˜ ๊ฒ€์ฆ.
85
+ *
86
+ * @param {Record<string, any>} config - ์–ด๋Œ‘ํ„ฐ ์ƒ์„ฑ์ž config.
87
+ * @param {object} opts
88
+ * @param {string} opts.driver - ์—๋Ÿฌ details ์šฉ ๋“œ๋ผ์ด๋ฒ„๋ช…('postgres'|'mariadb'|'mongodb').
89
+ * @param {string} [opts.dbKey] - discrete db ํ‚ค ์ด๋ฆ„(SQL='database', ๊ธฐ๋ณธ๊ฐ’). Mongo ๋Š” 'dbName' ์„
90
+ * url ๊ณผ ์ถฉ๋Œ์‹œํ‚ค์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ณ„๋„ ์ฒ˜๋ฆฌ(์•„๋ž˜ dbConflictsWithUrl ์ฐธ์กฐ).
91
+ * @param {boolean} [opts.dbConflictsWithUrl] - db ํ‚ค๊ฐ€ url ๊ณผ ์ƒํ˜ธ ๋ฐฐํƒ€์ธ์ง€(SQL=true). Mongo ์˜ dbName ์€
92
+ * url(=connection)๊ณผ ๋ณ„๊ฐœ(=db ์„ ํƒ)๋ผ false.
93
+ * @returns {{ url?: string, host?: string, port?: number, user?: string, password?: string, database?: string }}
94
+ * url ๋ชจ๋“œ๋ฉด `{ url }`, discrete ๋ชจ๋“œ๋ฉด ์กด์žฌํ•˜๋Š” ์—ฐ๊ฒฐํ•„๋“œ๋งŒ. (database ๋Š” dbConflictsWithUrl=true ์ผ ๋•Œ๋งŒ ํฌํ•จ.)
95
+ * @throws {MegaValidationError} `adapter.connection_conflict` / `adapter.connection_required` / `adapter.invalid_option`
96
+ */
97
+ export function resolveConnection(config, { driver, dbKey = 'database', dbConflictsWithUrl = true }) {
98
+ // url ๋˜๋Š” deprecated ๋ณ„์นญ connectionString.
99
+ const url = config.url ?? config.connectionString
100
+ const hasUrl = url !== undefined && url !== null
101
+ if (hasUrl && (typeof url !== 'string' || url.length === 0)) {
102
+ throw invalidOption(`${driver} "url" (or "connectionString") must be a non-empty string.`, { driver, option: 'url', type: typeof url })
103
+ }
104
+
105
+ const discreteKeys = ['host', 'port', 'user', 'password']
106
+ if (dbConflictsWithUrl) discreteKeys.push(dbKey)
107
+ const present = discreteKeys.filter((k) => config[k] !== undefined)
108
+
109
+ if (hasUrl && present.length > 0) {
110
+ // url ์€ connection ์ •๋ณด๋ฅผ ๋ชจ๋‘ ๋‹ด์œผ๋ฏ€๋กœ discrete ์™€ ์„ž์œผ๋ฉด ์–ด๋А ์ชฝ์ด ์ด๊ธฐ๋Š”์ง€ ๋ชจํ˜ธ โ†’ ๋ช…์‹œ ๊ฑฐ๋ถ€.
111
+ throw new MegaValidationError(
112
+ 'adapter.connection_conflict',
113
+ `${driver}: "url" cannot be combined with discrete connection fields (${present.join(', ')}). Use one connection mode (pool/options are allowed with either).`,
114
+ { details: { driver, discreteKeys: present } },
115
+ )
116
+ }
117
+ if (!hasUrl && present.length === 0) {
118
+ throw new MegaValidationError(
119
+ 'adapter.connection_required',
120
+ `${driver}: a "url" or discrete connection fields (host/port/user/password${dbConflictsWithUrl ? '/' + dbKey : ''}) are required.`,
121
+ { details: { driver } },
122
+ )
123
+ }
124
+
125
+ assertPositiveInt('port', config.port, { driver })
126
+
127
+ /** @type {Record<string, any>} */
128
+ const out = {}
129
+ if (hasUrl) {
130
+ out.url = url
131
+ return out
132
+ }
133
+ for (const k of ['host', 'port', 'user', 'password']) if (config[k] !== undefined) out[k] = config[k]
134
+ if (dbConflictsWithUrl && config[dbKey] !== undefined) out[dbKey] = config[dbKey]
135
+ return out
136
+ }
137
+
138
+ /**
139
+ * pg ํ’€ ๋งคํ•‘ โ€” ๊ฐ’์ด null ์ด๋ฉด ๋ฏธ์ง€์›(throw), `{ key, divideBy? }` ๋ฉด ํ‚ค ์ด๋ฆ„ ๋ณ€๊ฒฝ(+๋‹จ์œ„ ๋ณ€ํ™˜).
140
+ * @type {Record<string, { key: string, divideBy?: number } | null>}
141
+ */
142
+ export const PG_POOL_SPEC = {
143
+ min: { key: 'min' },
144
+ max: { key: 'max' },
145
+ idleTimeoutMs: { key: 'idleTimeoutMillis' },
146
+ acquireTimeoutMs: { key: 'connectionTimeoutMillis' },
147
+ maxLifetimeMs: { key: 'maxLifetimeSeconds', divideBy: 1000 }, // pg ๋Š” ์ดˆ ๋‹จ์œ„
148
+ }
149
+
150
+ /**
151
+ * mariadb ํ’€ ๋งคํ•‘ โ€” idleTimeout ์€ ์ดˆ ๋‹จ์œ„(msรท1000). maxLifetime ๋™๋“ฑ๋ฌผ ์—†์Œ(๋ฏธ์ง€์›).
152
+ * @type {Record<string, { key: string, divideBy?: number } | null>}
153
+ */
154
+ export const MARIA_POOL_SPEC = {
155
+ min: { key: 'minimumIdle' },
156
+ max: { key: 'connectionLimit' },
157
+ idleTimeoutMs: { key: 'idleTimeout', divideBy: 1000 }, // mariadb idleTimeout ์€ ์ดˆ ๋‹จ์œ„
158
+ acquireTimeoutMs: { key: 'acquireTimeout' },
159
+ maxLifetimeMs: null, // ๋ฏธ์ง€์›
160
+ }
161
+
162
+ /**
163
+ * mongodb ํ’€ ๋งคํ•‘ โ€” ์ „๋ถ€ ms. maxLifetime ๋™๋“ฑ๋ฌผ ์—†์Œ(๋ฏธ์ง€์›).
164
+ * @type {Record<string, { key: string, divideBy?: number } | null>}
165
+ */
166
+ export const MONGO_POOL_SPEC = {
167
+ min: { key: 'minPoolSize' },
168
+ max: { key: 'maxPoolSize' },
169
+ idleTimeoutMs: { key: 'maxIdleTimeMS' },
170
+ acquireTimeoutMs: { key: 'waitQueueTimeoutMS' },
171
+ maxLifetimeMs: null, // ๋ฏธ์ง€์›
172
+ }
173
+
174
+ /**
175
+ * ๊ณตํ†ต ํ’€ ์ธํ„ฐํŽ˜์ด์Šค(`{ min, max, idleTimeoutMs, acquireTimeoutMs, maxLifetimeMs }`)๋ฅผ ๋“œ๋ผ์ด๋ฒ„
176
+ * ํ’€ ์˜ต์…˜ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ยท๊ฒ€์ฆํ•œ๋‹ค.
177
+ *
178
+ * - ์•Œ ์ˆ˜ ์—†๋Š” ํ’€ ํ‚ค โ†’ `adapter.invalid_option`(์˜คํƒ€ fail-fast).
179
+ * - ๋“œ๋ผ์ด๋ฒ„ ๋ฏธ์ง€์› ํ‚ค(spec ๊ฐ’ null, ์˜ˆ: mariadb/mongo ์˜ maxLifetimeMs) โ†’ `adapter.invalid_option`(silent ๋ฌด์‹œ X).
180
+ * - `max` ๋Š” ์–‘์˜ ์ •์ˆ˜, ๊ทธ ์™ธ(minยท*Ms)๋Š” ์Œ ์•„๋‹Œ ์ •์ˆ˜.
181
+ * - `divideBy` ๊ฐ€ ์žˆ์œผ๋ฉด msโ†’์ดˆ ๋ณ€ํ™˜(floor).
182
+ *
183
+ * @param {unknown} pool - config.pool.
184
+ * @param {Record<string, { key: string, divideBy?: number } | null>} spec - ๋“œ๋ผ์ด๋ฒ„ ํ’€ ๋งคํ•‘.
185
+ * @param {string} driver - ์—๋Ÿฌ details ์šฉ ๋“œ๋ผ์ด๋ฒ„๋ช….
186
+ * @returns {Record<string, number>} ๋“œ๋ผ์ด๋ฒ„ ํ’€ ์˜ต์…˜ ๊ฐ์ฒด(๋นˆ ๊ฐ์ฒด ๊ฐ€๋Šฅ).
187
+ */
188
+ export function normalizePool(pool, spec, driver) {
189
+ if (pool === undefined) return {}
190
+ assertPlainObject('pool', pool, { driver })
191
+ /** @type {Record<string, number>} */
192
+ const out = {}
193
+ for (const [key, value] of Object.entries(/** @type {Record<string, unknown>} */ (pool))) {
194
+ if (value === undefined) continue
195
+ const map = spec[key]
196
+ if (map === undefined) {
197
+ throw invalidOption(`${driver}: unknown pool option "${key}". Known: ${Object.keys(spec).join(', ')}.`, { driver, option: `pool.${key}` })
198
+ }
199
+ if (map === null) {
200
+ throw invalidOption(`${driver}: pool.${key} is not supported by the ${driver} driver.`, { driver, option: `pool.${key}` })
201
+ }
202
+ if (key === 'max') assertPositiveInt(`pool.${key}`, value, { driver })
203
+ else assertNonNegativeInt(`pool.${key}`, value, { driver })
204
+ const num = /** @type {number} */ (value)
205
+ out[map.key] = map.divideBy ? Math.floor(num / map.divideBy) : num
206
+ }
207
+ return out
208
+ }