mega-framework 0.1.6 → 0.1.8

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 (248) hide show
  1. package/README.md +9 -0
  2. package/bin/mega-ws-hub.js +2 -2
  3. package/package.json +33 -9
  4. package/sample/crud/.env +10 -1
  5. package/sample/crud/.env.example +10 -1
  6. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  7. package/sample/crud/.mega/journal/snapshot.json +261 -0
  8. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  9. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  10. package/sample/crud/apps/main/locales/server/en.json +12 -1
  11. package/sample/crud/apps/main/locales/server/ko.json +12 -1
  12. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  13. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  14. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  15. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  16. package/sample/crud/apps/main/models/note-model.js +79 -0
  17. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  18. package/sample/crud/apps/main/models/user-model.js +146 -0
  19. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  20. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  21. package/sample/crud/apps/main/routes/users.js +55 -10
  22. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  23. package/sample/crud/apps/main/services/auth-service.js +39 -24
  24. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  25. package/sample/crud/apps/main/services/note-service.js +6 -6
  26. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  27. package/sample/crud/apps/main/services/user-service.js +62 -21
  28. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  29. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  30. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  31. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  32. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  33. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  34. package/sample/crud/mega.config.js +10 -2
  35. package/sample/crud/package.json +3 -3
  36. package/sample/crud/scripts/start-ws-hub.sh +20 -6
  37. package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  38. package/sample/simple/package.json +2 -2
  39. package/src/adapters/adapter-manager.js +2 -1
  40. package/src/adapters/adapter-options.js +44 -3
  41. package/src/adapters/file-adapter.js +9 -5
  42. package/src/adapters/file-session-adapter.js +4 -3
  43. package/src/adapters/maria-adapter.js +33 -7
  44. package/src/adapters/mega-cache-adapter.js +83 -6
  45. package/src/adapters/mega-db-adapter.js +10 -1
  46. package/src/adapters/mongo-adapter.js +40 -8
  47. package/src/adapters/postgres-adapter.js +33 -6
  48. package/src/adapters/redis-adapter.js +7 -3
  49. package/src/adapters/sqlite-adapter.js +26 -3
  50. package/src/cli/commands/console-cmd.js +3 -1
  51. package/src/cli/commands/new.js +13 -3
  52. package/src/cli/commands/scaffold.js +173 -33
  53. package/src/cli/generators/index.js +140 -3
  54. package/src/cli/index.js +437 -155
  55. package/src/cli/watch.js +188 -0
  56. package/src/core/ajv-mapper.js +30 -3
  57. package/src/core/boot.js +464 -245
  58. package/src/core/cluster-metrics.js +13 -4
  59. package/src/core/ctx-builder.js +65 -3
  60. package/src/core/envelope.js +119 -12
  61. package/src/core/hub-link.js +89 -18
  62. package/src/core/i18n.js +11 -1
  63. package/src/core/index.js +7 -3
  64. package/src/core/mega-app.js +253 -505
  65. package/src/core/mega-cluster.js +4 -1
  66. package/src/core/mega-server.js +40 -9
  67. package/src/core/migration/dialect-registry.js +107 -0
  68. package/src/core/migration/dialects/README.md +62 -0
  69. package/src/core/migration/dialects/maria.js +496 -0
  70. package/src/core/migration/dialects/mongo.js +824 -0
  71. package/src/core/migration/dialects/postgres.js +563 -0
  72. package/src/core/migration/dialects/sqlite.js +476 -0
  73. package/src/core/migration/differ.js +456 -0
  74. package/src/core/migration/generate.js +508 -0
  75. package/src/core/migration/journal.js +167 -0
  76. package/src/core/migration/model-scan.js +84 -0
  77. package/src/core/migration/mongo-migration-db.js +97 -0
  78. package/src/core/migration/schema-builder.js +400 -0
  79. package/src/core/migration/schema-validator.js +315 -0
  80. package/src/core/migration-lock.js +205 -0
  81. package/src/core/migration-runner.js +166 -38
  82. package/src/core/multipart.js +28 -5
  83. package/src/core/pipeline.js +131 -0
  84. package/src/core/router.js +70 -65
  85. package/src/core/scope-registry.js +1 -0
  86. package/src/core/security.js +70 -12
  87. package/src/core/session-store.js +14 -1
  88. package/src/core/workers-manager.js +12 -1
  89. package/src/core/ws-cluster.js +10 -3
  90. package/src/core/ws-message.js +48 -4
  91. package/src/core/ws-presence.js +636 -0
  92. package/src/core/ws-roster.js +50 -8
  93. package/src/core/ws-upgrade.js +223 -12
  94. package/src/index.js +1 -1
  95. package/src/lib/hub-protocol.js +29 -0
  96. package/src/lib/mega-circuit-breaker.js +5 -3
  97. package/src/lib/mega-health.js +35 -4
  98. package/src/lib/mega-job-queue.js +151 -34
  99. package/src/lib/mega-job.js +37 -1
  100. package/src/lib/mega-metrics.js +31 -13
  101. package/src/lib/mega-plugin.js +34 -3
  102. package/src/lib/mega-schedule.js +40 -22
  103. package/src/lib/mega-shutdown.js +114 -39
  104. package/src/lib/mega-tracing.js +66 -19
  105. package/src/lib/mega-worker.js +33 -6
  106. package/src/lib/otel-resource.js +36 -0
  107. package/src/{cli → lib}/ws-hub.js +139 -15
  108. package/src/models/crud-sql-builder.js +133 -0
  109. package/src/models/mega-model.js +82 -2
  110. package/src/models/model-crud.js +483 -0
  111. package/src/models/mongo-crud.js +285 -0
  112. package/templates/adr/code.tpl +23 -0
  113. package/templates/model/code-mongo.tpl +35 -0
  114. package/templates/model/code.tpl +15 -1
  115. package/templates/model/test-mongo.tpl +38 -0
  116. package/templates/model/test.tpl +4 -0
  117. package/types/adapters/adapter-manager.d.ts +95 -0
  118. package/types/adapters/adapter-options.d.ts +93 -0
  119. package/types/adapters/file-adapter.d.ts +105 -0
  120. package/types/adapters/file-session-adapter.d.ts +103 -0
  121. package/types/adapters/index.d.ts +20 -0
  122. package/types/adapters/maria-adapter.d.ts +117 -0
  123. package/types/adapters/mega-adapter.d.ts +215 -0
  124. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  125. package/types/adapters/mega-cache-adapter.d.ts +73 -0
  126. package/types/adapters/mega-db-adapter.d.ts +50 -0
  127. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  128. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  129. package/types/adapters/mega-session-adapter.d.ts +32 -0
  130. package/types/adapters/mongo-adapter.d.ts +150 -0
  131. package/types/adapters/nats-adapter.d.ts +108 -0
  132. package/types/adapters/postgres-adapter.d.ts +141 -0
  133. package/types/adapters/redis-adapter.d.ts +78 -0
  134. package/types/adapters/redis-session-adapter.d.ts +82 -0
  135. package/types/adapters/redlock-adapter.d.ts +149 -0
  136. package/types/adapters/registry.d.ts +46 -0
  137. package/types/adapters/sqlite-adapter.d.ts +112 -0
  138. package/types/auth/index.d.ts +24 -0
  139. package/types/cli/commands/console-cmd.d.ts +37 -0
  140. package/types/cli/commands/new.d.ts +16 -0
  141. package/types/cli/commands/routes.d.ts +36 -0
  142. package/types/cli/commands/scaffold.d.ts +78 -0
  143. package/types/cli/commands/test-cmd.d.ts +14 -0
  144. package/types/cli/generators/index.d.ts +122 -0
  145. package/types/cli/index.d.ts +234 -0
  146. package/types/cli/template-engine.d.ts +40 -0
  147. package/types/cli/watch.d.ts +59 -0
  148. package/types/core/ajv-mapper.d.ts +27 -0
  149. package/types/core/boot.d.ts +233 -0
  150. package/types/core/cluster-metrics.d.ts +52 -0
  151. package/types/core/config-loader.d.ts +13 -0
  152. package/types/core/config-validator.d.ts +30 -0
  153. package/types/core/ctx-builder.d.ts +103 -0
  154. package/types/core/envelope.d.ts +79 -0
  155. package/types/core/error-mapper.d.ts +17 -0
  156. package/types/core/formbody.d.ts +41 -0
  157. package/types/core/hub-link.d.ts +266 -0
  158. package/types/core/i18n.d.ts +178 -0
  159. package/types/core/index.d.ts +28 -0
  160. package/types/core/mega-app.d.ts +529 -0
  161. package/types/core/mega-cluster.d.ts +104 -0
  162. package/types/core/mega-server.d.ts +91 -0
  163. package/types/core/mega-service.d.ts +31 -0
  164. package/types/core/migration/dialect-registry.d.ts +22 -0
  165. package/types/core/migration/dialects/maria.d.ts +99 -0
  166. package/types/core/migration/dialects/mongo.d.ts +89 -0
  167. package/types/core/migration/dialects/postgres.d.ts +117 -0
  168. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  169. package/types/core/migration/differ.d.ts +47 -0
  170. package/types/core/migration/generate.d.ts +56 -0
  171. package/types/core/migration/journal.d.ts +52 -0
  172. package/types/core/migration/model-scan.d.ts +19 -0
  173. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  174. package/types/core/migration/schema-builder.d.ts +197 -0
  175. package/types/core/migration/schema-validator.d.ts +20 -0
  176. package/types/core/migration-lock.d.ts +33 -0
  177. package/types/core/migration-runner.d.ts +101 -0
  178. package/types/core/multipart.d.ts +86 -0
  179. package/types/core/openapi.d.ts +62 -0
  180. package/types/core/pipeline.d.ts +93 -0
  181. package/types/core/router.d.ts +159 -0
  182. package/types/core/routes-loader.d.ts +21 -0
  183. package/types/core/scope-registry.d.ts +14 -0
  184. package/types/core/security.d.ts +77 -0
  185. package/types/core/services-loader.d.ts +27 -0
  186. package/types/core/session-cleanup-schedule.d.ts +19 -0
  187. package/types/core/session-store.d.ts +25 -0
  188. package/types/core/session.d.ts +77 -0
  189. package/types/core/static-assets.d.ts +73 -0
  190. package/types/core/template.d.ts +106 -0
  191. package/types/core/workers-manager.d.ts +79 -0
  192. package/types/core/ws-cluster.d.ts +208 -0
  193. package/types/core/ws-compression.d.ts +112 -0
  194. package/types/core/ws-controller.d.ts +65 -0
  195. package/types/core/ws-message.d.ts +106 -0
  196. package/types/core/ws-presence.d.ts +273 -0
  197. package/types/core/ws-roster.d.ts +108 -0
  198. package/types/core/ws-upgrade.d.ts +260 -0
  199. package/types/errors/config-error.d.ts +10 -0
  200. package/types/errors/http-errors.d.ts +120 -0
  201. package/types/errors/index.d.ts +3 -0
  202. package/types/errors/mega-error.d.ts +32 -0
  203. package/types/index.d.ts +39 -0
  204. package/types/lib/asp/config.d.ts +49 -0
  205. package/types/lib/asp/crypto.d.ts +43 -0
  206. package/types/lib/asp/errors.d.ts +30 -0
  207. package/types/lib/asp/nonce-cache.d.ts +52 -0
  208. package/types/lib/asp/plugin.d.ts +30 -0
  209. package/types/lib/asp/ws-terminator.d.ts +45 -0
  210. package/types/lib/env-mapper.d.ts +14 -0
  211. package/types/lib/hub-protocol.d.ts +106 -0
  212. package/types/lib/index.d.ts +22 -0
  213. package/types/lib/logger/telegram-core.d.ts +104 -0
  214. package/types/lib/logger/telegram-transport.d.ts +45 -0
  215. package/types/lib/mega-brute-force.d.ts +66 -0
  216. package/types/lib/mega-circuit-breaker.d.ts +243 -0
  217. package/types/lib/mega-cron.d.ts +66 -0
  218. package/types/lib/mega-hash.d.ts +32 -0
  219. package/types/lib/mega-health.d.ts +48 -0
  220. package/types/lib/mega-job-queue.d.ts +188 -0
  221. package/types/lib/mega-job-worker.d.ts +130 -0
  222. package/types/lib/mega-job.d.ts +145 -0
  223. package/types/lib/mega-logger.d.ts +45 -0
  224. package/types/lib/mega-metrics.d.ts +285 -0
  225. package/types/lib/mega-plugin.d.ts +245 -0
  226. package/types/lib/mega-retry.d.ts +85 -0
  227. package/types/lib/mega-schedule.d.ts +260 -0
  228. package/types/lib/mega-shutdown.d.ts +135 -0
  229. package/types/lib/mega-tracing.d.ts +224 -0
  230. package/types/lib/mega-worker.d.ts +129 -0
  231. package/types/lib/otel-resource.d.ts +16 -0
  232. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  233. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  234. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  235. package/types/lib/ws-hub.d.ts +259 -0
  236. package/types/models/crud-sql-builder.d.ts +48 -0
  237. package/types/models/index.d.ts +1 -0
  238. package/types/models/mega-model.d.ts +138 -0
  239. package/types/models/model-crud.d.ts +82 -0
  240. package/types/models/mongo-crud.d.ts +59 -0
  241. package/types/test/index.d.ts +84 -0
  242. package/.env +0 -127
  243. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  244. package/sample/crud/apps/main/models/note.js +0 -71
  245. package/sample/crud/apps/main/models/user.js +0 -86
  246. package/sample/crud/package-lock.json +0 -5665
  247. package/sample/crud/yarn.lock +0 -2142
  248. package/sample/simple/package-lock.json +0 -1851
package/src/core/boot.js CHANGED
@@ -7,17 +7,13 @@
7
7
  * 본 모듈이 그 단일 지점이다 — `loadPlugins` 를 실 부팅 시퀀스에 끼우고 lifecycle hook(beforeBoot/
8
8
  * afterBoot/beforeShutdown)을 구동한다(ADR-123).
9
9
  *
10
- * # 부팅 시퀀스 (02-architecture §14 4·5단계 = 플러그인)
11
- * 1. `loadAndValidateConfig(projectRoot)` mega.config.js + app.config.js 로드·검증(1~3단계).
12
- * 4·5. `loadPlugins(global.plugins, host, { projectRoot })` resolveshape→apiVersion→install(순서대로).
13
- * 2. `buildFromGlobalConfig(global)` 어댑터 인스턴스화 + LIFO shutdown hook(**앱 생성보다 먼저**,
14
- * adapter-manager 종료 순서 주석).
15
- * 3. `connectAll({ ping })` — 어댑터 connect(+옵션 healthCheck). 실패 매니저가 LIFO cleanup 후 throw.
16
- * 7. `host.runLifecycle('beforeBoot', ctx)` 부팅 직전 hook(L-2 ctx 주입).
17
- * 6. 각 앱: `new MegaApp({ ...config, plugins, globalMiddlewares })` + 라우트 자동 로딩 + vhost mount.
18
- * 9. `server.listen({ port, host })`(listen=false 면 건너뜀 — 테스트/CLI 분기).
19
- * 10. `host.runLifecycle('afterBoot', ctx)` — 부팅 완료 hook.
20
- * + beforeShutdown hook 을 `MegaShutdown` 에 브리지(graceful, per-hook catch — ADR-122).
10
+ * # 부팅 시퀀스 — declarative step 배열이 정본 (02-architecture §14 코드화)
11
+ * - 공통 토대 = {@link PREPARE_STEP_NAMES}: config → plugins → adapters → health-auto-checks →
12
+ * metrics tracing workers ws-hub boot-context (`prepareRuntime`).
13
+ * - 서버 부팅 = {@link BOOT_STEP_NAMES}: runtime() before-boot-hook logger server apps
14
+ * cluster-transport listen → after-boot-hook → shutdown-bridge (`bootApp`).
15
+ * step prerequisite(`needs`선행 산출물 검증) on-fail 정책(`onFail` 기본 abort)을
16
+ * 명시한다({@link runBootSteps}). 순서를 바꾸거나 step 빼면 prerequisite 가 즉시 throw 한다.
21
17
  *
22
18
  * # ⚠️ install → buildFromGlobalConfig 순서 (config-driven driver 의 핵심 제약)
23
19
  * roadmap §224 검증기준("샘플 플러그인이 `adapters.register('sample')` → `services.databases.x.driver:
@@ -54,7 +50,9 @@ import { MegaWsCluster } from './ws-cluster.js'
54
50
  import { MegaWsRedisRoster } from './ws-roster.js'
55
51
  import * as MegaMetrics from '../lib/mega-metrics.js'
56
52
  import * as MegaTracing from '../lib/mega-tracing.js'
57
- import { MegaWsHub } from '../cli/ws-hub.js'
53
+ import * as MegaHealth from '../lib/mega-health.js'
54
+ import { MegaWsHub } from '../lib/ws-hub.js'
55
+ import { MegaConfigError } from '../errors/config-error.js'
58
56
 
59
57
  /**
60
58
  * 부팅 결과 핸들.
@@ -65,7 +63,7 @@ import { MegaWsHub } from '../cli/ws-hub.js'
65
63
  * @property {Array<{ name: string, config: Object }>} apps - 로드된 앱 config 목록.
66
64
  * @property {MegaApp[]} megaApps - 생성된 MegaApp 인스턴스(등록 순서).
67
65
  * @property {BootContext} ctx - lifecycle hook 에 넘긴 boot context.
68
- * @property {import('../cli/ws-hub.js').MegaWsHub | null} wsHub - embedded wsHub(ADR-137, `wsHub.enabled` OFF 면 null).
66
+ * @property {import('../lib/ws-hub.js').MegaWsHub | null} wsHub - embedded wsHub(ADR-137, `wsHub.enabled` OFF 면 null).
69
67
  * @property {import('pino').Logger | null} appLogger - 공유 pino 로거(ADR-141, logger 비활성이면 null). CLI 시작 메시지 등에 재사용.
70
68
  */
71
69
 
@@ -118,75 +116,217 @@ export function buildBootContext(global, logger) {
118
116
  }
119
117
 
120
118
  /**
121
- * 런타임 공통 준비 config 로드 플러그인 install → 어댑터 build → connect → boot ctx.
122
- * `bootApp`(서버)·`mega worker`/`mega scheduler`(CLI 호스트)가 **같은 순서**를 공유한다(install <
123
- * build — config-driven driver 제약, 위 주석 참조). lifecycle hook/앱 생성/ listen 직전까지의 토대.
119
+ * 공유 어댑터 전수의 `healthCheck()` `MegaHealth` `<domain>:<globalKey>` 이름으로 등록한다.
120
+ * `/health/ready` 어댑터 생사를 실제로 반영하게 하는 배선 — `prepareRuntime` 이 connect 직후
121
+ * 호출한다(`health.autoChecks:false` 옵트아웃). 등록은 이름 기준 덮어쓰기라 재부팅(테스트)에도 멱등.
124
122
  *
125
- * @param {string} projectRoot
126
- * @param {{ ping?: boolean, logger?: { debug?: Function, info?: Function, warn?: Function } }} [opts]
127
- * @returns {Promise<{ global: Object, apps: Array<{ name: string, config: Object }>, host: MegaPluginHost, ctx: BootContext, wsHub: (MegaWsHub | null) }>}
123
+ * @param {Array<{ domain: string, key: string, adapter: { healthCheck: () => Promise<{ ok: boolean }> } }>} [entries] -
124
+ * 대상 어댑터 엔트리(기본: 어댑터 매니저의 전체 등록분). 테스트 주입용.
125
+ * @returns {string[]} 등록한 체크 이름 목록.
128
126
  */
129
- export async function prepareRuntime(projectRoot, { ping = false, logger } = {}) {
130
- // 1~3단계: config 로드 + 검증.
131
- const { global, apps } = await loadAndValidateConfig(projectRoot)
132
- logger?.debug?.({ apps: apps.map((a) => a.name) }, 'boot.config loaded')
133
-
134
- // 4·5단계: 플러그인 로딩 + install — **어댑터 빌드보다 먼저**(config-driven driver, 상단 주석 참조).
135
- const host = new MegaPluginHost({ logger })
136
- await loadPlugins(/** @type {any} */ (global).plugins, host, { projectRoot, logger })
137
- logger?.debug?.({ plugins: host.loadedPlugins.map((p) => p.name) }, 'boot.plugins installed')
138
-
139
- // 어댑터 인스턴스화 + connect (MegaApp 생성보다 먼저 — LIFO shutdown 순서, adapter-manager 주석).
140
- buildFromGlobalConfig(global)
141
- await connectAll({ logger, ping })
142
- logger?.debug?.('boot.adapters connected')
143
-
144
- // 메트릭 SDK 옵트인 (ADR-072/131) — global `health.exposeMetrics:true` 면 MegaMetrics 초기화
145
- // + 어댑터 onCallEnd 일괄 구독(connect 직후 — 모든 공유 어댑터 호출이 mega_adapter_* 메트릭으로 집계).
146
- // 옵트인 OFF 면 init 미호출 → 모든 record* 가 0 비용. /metrics 라우트는 MegaApp 이 같은 config 로 등록.
147
- const healthCfg = /** @type {any} */ (global).health
148
- if (healthCfg && healthCfg.exposeMetrics === true && !MegaMetrics.isEnabled()) {
149
- MegaMetrics.init({
150
- serviceName: healthCfg.serviceName ?? /** @type {any} */ (global).server?.serviceName ?? process.env.MEGA_OTEL_SERVICE_NAME ?? 'mega-framework',
151
- version: /** @type {any} */ (global).server?.version,
152
- environment: process.env.NODE_ENV,
153
- })
154
- const subscribed = MegaMetrics.attachToManager({ entries: adapterEntries })
155
- MegaShutdown.register('mega-metrics', async () => {
156
- await MegaMetrics.shutdown()
157
- })
158
- logger?.debug?.({ subscribed }, 'boot.metrics enabled')
127
+ export function registerAdapterHealthChecks(entries = adapterEntries()) {
128
+ /** @type {string[]} */
129
+ const names = []
130
+ for (const e of entries) {
131
+ const name = `${e.domain}:${e.key}`
132
+ MegaHealth.register(name, () => e.adapter.healthCheck())
133
+ names.push(name)
159
134
  }
135
+ return names
136
+ }
160
137
 
161
- // 트레이싱 SDK 옵트인 (ADR-104/114/126/163) — `MEGA_OTEL_ENABLED=true` env 면 MegaTracing 초기화 + 어댑터
162
- // onCallEnd 일괄 구독(자동 span, ADR-114). 메트릭(위)과 대칭 `fromEnv` env 게이트(미설정/false=no-op,
163
- // 0 비용)한다. 미옵트인이면 mega-app 의 HTTP/WS 루트 span·`ctx.tracer.span` 이 모두 no-op. **부팅에서 한 번
164
- // 켜야 프로덕션 분산 트레이싱이 동작한다** 이전엔 boot 가 `MegaTracing.fromEnv()` 를 호출하지 않아(메트릭만
165
- // 배선) 프로덕션에서 트레이싱이 미작동했다(ADR-163 에서 boot 배선 추가). 재진입 방지로 isEnabled 가드.
166
- if (!MegaTracing.isEnabled() && MegaTracing.fromEnv() === true) {
167
- const tracedAdapters = MegaTracing.attachToManager({ entries: adapterEntries })
168
- MegaShutdown.register('mega-tracing', async () => {
169
- await MegaTracing.shutdown()
170
- })
171
- logger?.debug?.({ subscribed: tracedAdapters }, 'boot.tracing enabled')
138
+ /**
139
+ * 부트 step 정의부팅 시퀀스의 단계.
140
+ * @typedef {Object} BootStep
141
+ * @property {string} name - step 이름(로그·prerequisite 에러 식별자).
142
+ * @property {string[]} [needs] - 선행 step state 에 만들어 둬야 하는 키(prerequisite). 누락
143
+ * `boot.step_prerequisite` throw step 순서 변경·누락 같은 배선 실수를 즉시 드러낸다.
144
+ * @property {'abort'|'warn'} [onFail] - 실패 정책. `'abort'`(기본) = fail-fast throw(M-1 정리 경로),
145
+ * `'warn'` = best-effort(warn 로그 후 다음 step 계속 — 부팅을 막으면 안 되는 부가 단계용).
146
+ * @property {(state: Record<string, any>) => Promise<void>|void} run - 단계 본체. 산출물은 state 에 쓴다.
147
+ */
148
+
149
+ /**
150
+ * step 배열 실행기 — 각 step 의 prerequisite 검증 → 실행 → 길목 debug 로그(start/done/tookMs).
151
+ * 실패는 step 의 `onFail` 정책을 따른다(기본 abort — 현재 부트 시퀀스 전 단계가 fail-fast).
152
+ *
153
+ * @param {BootStep[]} steps - 실행할 step 정의(순서 = 시퀀스 정본).
154
+ * @param {Record<string, any>} state - step 간 공유 상태(산출물 버스).
155
+ * @param {{ debug?: Function, warn?: Function }} [logger]
156
+ * @returns {Promise<void>}
157
+ * @throws {MegaConfigError} prerequisite 누락(`boot.step_prerequisite`) — 배선 오류는 즉시 abort.
158
+ */
159
+ export async function runBootSteps(steps, state, logger) {
160
+ for (const step of steps) {
161
+ for (const key of step.needs ?? []) {
162
+ if (state[key] === undefined) {
163
+ throw new MegaConfigError(
164
+ 'boot.step_prerequisite',
165
+ `boot step '${step.name}' requires state '${key}' (produced by an earlier step).`,
166
+ { details: { step: step.name, missing: key } },
167
+ )
168
+ }
169
+ }
170
+ const startedAt = Date.now()
171
+ logger?.debug?.({ step: step.name }, 'boot.step start')
172
+ try {
173
+ await step.run(state)
174
+ } catch (err) {
175
+ if (step.onFail === 'warn') {
176
+ // best-effort step — 부가 단계 실패가 부팅을 막지 않는다(silent 금지, warn 가시화).
177
+ logger?.warn?.({ err, step: step.name }, 'boot.step failed (best-effort, continuing)')
178
+ continue
179
+ }
180
+ logger?.debug?.({ step: step.name }, 'boot.step failed — aborting boot')
181
+ throw err
182
+ }
183
+ logger?.debug?.({ step: step.name, tookMs: Date.now() - startedAt }, 'boot.step done')
172
184
  }
185
+ }
173
186
 
174
- // CPU 워커 풀(ADR-124) — 등록 소스 = `config.workers`(정적) + 플러그인 `host.listWorkers()`(동적,
175
- // ADR-134). jobs/schedules 듀얼 소스(`collectRegistrations`, ADR-123)와 동형이다. `buildWorkers`
176
- // `static name` 중복을 부팅 fail-fast 한다. 어댑터 connect **뒤**에 등록되므로 MegaShutdown LIFO 상
177
- // 워커가 어댑터보다 먼저 정리된다(워커가 어댑터를 쓰는 정리를 어댑터 종료 전에 끝냄).
178
- const workerClasses = [...(/** @type {any} */ (global).workers ?? []), ...host.listWorkers()]
179
- buildWorkers({ workers: /** @type {any} */ (workerClasses) }, { projectRoot })
180
- await startWorkers({ logger })
181
- logger?.debug?.({ workers: workerClasses.length }, 'boot.workers started')
187
+ /**
188
+ * 런타임 공통 준비 step 들 — `prepareRuntime` 시퀀스 정본. `bootApp`(서버)·`mega worker`/
189
+ * `mega scheduler`(CLI 호스트)가 같은 순서를 공유한다(install < build config-driven driver 제약,
190
+ * 모듈 상단 주석). state 입력 = { projectRoot, ping, logger }, 산출 = { global, apps, host, ctx, wsHub }.
191
+ * @type {BootStep[]}
192
+ */
193
+ const PREPARE_STEPS = [
194
+ {
195
+ name: 'config',
196
+ run: async (st) => {
197
+ // 1~3단계: config 로드 + 검증.
198
+ const { global, apps } = await loadAndValidateConfig(st.projectRoot)
199
+ st.global = global
200
+ st.apps = apps
201
+ st.logger?.debug?.({ apps: apps.map((/** @type {any} */ a) => a.name) }, 'boot.config loaded')
202
+ },
203
+ },
204
+ {
205
+ name: 'plugins',
206
+ needs: ['global'],
207
+ run: async (st) => {
208
+ // 4·5단계: 플러그인 로딩 + install — **어댑터 빌드보다 먼저**(config-driven driver, 상단 주석 참조).
209
+ const host = new MegaPluginHost({ logger: st.logger })
210
+ await loadPlugins(/** @type {any} */ (st.global).plugins, host, { projectRoot: st.projectRoot, logger: st.logger })
211
+ st.host = host
212
+ st.logger?.debug?.({ plugins: host.loadedPlugins.map((p) => p.name) }, 'boot.plugins installed')
213
+ },
214
+ },
215
+ {
216
+ name: 'adapters',
217
+ needs: ['global', 'host'],
218
+ run: async (st) => {
219
+ // 어댑터 인스턴스화 + connect (MegaApp 생성보다 먼저 — shutdown 'adapters' stage, adapter-manager 주석).
220
+ buildFromGlobalConfig(st.global)
221
+ await connectAll({ logger: st.logger, ping: st.ping })
222
+ st.logger?.debug?.('boot.adapters connected')
223
+ },
224
+ },
225
+ {
226
+ name: 'health-auto-checks',
227
+ needs: ['global'],
228
+ run: (st) => {
229
+ // readiness 자동 체크 — 공유 어댑터의 healthCheck 를 MegaHealth 에 등록한다(`health.autoChecks:false`
230
+ // 옵트아웃, ADR-189). 부팅 후 어댑터가 죽으면 /health/ready 가 503 으로 떨어져 LB/probe 가 트래픽을
231
+ // 회수한다. 같은 이름 재등록은 덮어쓰기라 멱등.
232
+ if (/** @type {any} */ (st.global).health?.autoChecks !== false) {
233
+ const registered = registerAdapterHealthChecks()
234
+ if (registered.length > 0) st.logger?.debug?.({ checks: registered }, 'boot.health autoChecks registered')
235
+ }
236
+ },
237
+ },
238
+ {
239
+ name: 'metrics',
240
+ needs: ['global'],
241
+ run: (st) => {
242
+ // 메트릭 SDK 옵트인 (ADR-072/131) — global `health.exposeMetrics:true` 면 MegaMetrics 초기화
243
+ // + 어댑터 onCallEnd 일괄 구독(connect 직후 — 모든 공유 어댑터 호출이 mega_adapter_* 메트릭으로 집계).
244
+ // 옵트인 OFF 면 init 미호출 → 모든 record* 가 0 비용. /metrics 라우트는 MegaApp 이 같은 config 로 등록.
245
+ const healthCfg = /** @type {any} */ (st.global).health
246
+ if (healthCfg && healthCfg.exposeMetrics === true && !MegaMetrics.isEnabled()) {
247
+ MegaMetrics.init({
248
+ serviceName: healthCfg.serviceName ?? /** @type {any} */ (st.global).server?.serviceName ?? process.env.MEGA_OTEL_SERVICE_NAME ?? 'mega-framework',
249
+ version: /** @type {any} */ (st.global).server?.version,
250
+ environment: process.env.NODE_ENV,
251
+ })
252
+ const subscribed = MegaMetrics.attachToManager({ entries: adapterEntries })
253
+ // 'telemetry' stage — 어댑터 disconnect 메트릭까지 기록된 뒤 SDK 를 내린다.
254
+ MegaShutdown.register('mega-metrics', async () => {
255
+ await MegaMetrics.shutdown()
256
+ }, { stage: 'telemetry' })
257
+ st.logger?.debug?.({ subscribed }, 'boot.metrics enabled')
258
+ }
259
+ },
260
+ },
261
+ {
262
+ name: 'tracing',
263
+ run: (st) => {
264
+ // 트레이싱 SDK 옵트인 (ADR-104/114/126/163) — `MEGA_OTEL_ENABLED=true` env 면 MegaTracing 초기화 + 어댑터
265
+ // onCallEnd 일괄 구독(자동 span, ADR-114). 메트릭(위)과 대칭 — `fromEnv` 가 env 게이트(미설정/false=no-op,
266
+ // 0 비용)한다. 미옵트인이면 mega-app 의 HTTP/WS 루트 span·`ctx.tracer.span` 이 모두 no-op. **부팅에서 한 번
267
+ // 켜야 프로덕션 분산 트레이싱이 동작한다** — 이전엔 boot 가 `MegaTracing.fromEnv()` 를 호출하지 않아(메트릭만
268
+ // 배선) 프로덕션에서 트레이싱이 미작동했다(ADR-163 에서 boot 배선 추가). 재진입 방지로 isEnabled 가드.
269
+ if (!MegaTracing.isEnabled() && MegaTracing.fromEnv() === true) {
270
+ const tracedAdapters = MegaTracing.attachToManager({ entries: adapterEntries })
271
+ // 'telemetry' stage — 어댑터 disconnect span 까지 export 된 뒤 SDK 를 내린다(이전 LIFO 는
272
+ // 트레이싱이 어댑터보다 먼저 내려가 disconnect span 이 유실됐다).
273
+ MegaShutdown.register('mega-tracing', async () => {
274
+ await MegaTracing.shutdown()
275
+ }, { stage: 'telemetry' })
276
+ st.logger?.debug?.({ subscribed: tracedAdapters }, 'boot.tracing enabled')
277
+ }
278
+ },
279
+ },
280
+ {
281
+ name: 'workers',
282
+ needs: ['global', 'host'],
283
+ run: async (st) => {
284
+ // CPU 워커 풀(ADR-124) — 등록 소스 = `config.workers`(정적) + 플러그인 `host.listWorkers()`(동적,
285
+ // ADR-134). jobs/schedules 듀얼 소스(`collectRegistrations`, ADR-123)와 동형이다. `buildWorkers` 가
286
+ // `static name` 중복을 부팅 시 fail-fast 한다. 어댑터 connect **뒤** 'workers' stage 에 등록되므로
287
+ // 종료 시 워커가 어댑터보다 먼저 정리된다(워커가 어댑터를 쓰는 정리를 어댑터 종료 전에 끝냄).
288
+ const workerClasses = [...(/** @type {any} */ (st.global).workers ?? []), ...st.host.listWorkers()]
289
+ buildWorkers({ workers: /** @type {any} */ (workerClasses) }, { projectRoot: st.projectRoot })
290
+ await startWorkers({ logger: st.logger })
291
+ st.logger?.debug?.({ workers: workerClasses.length }, 'boot.workers started')
292
+ },
293
+ },
294
+ {
295
+ name: 'ws-hub',
296
+ needs: ['global'],
297
+ run: async (st) => {
298
+ // embedded wsHub (ADR-137) — `global.wsHub.enabled=true` 면 같은 프로세스에 hub 를 띄운다(single-node).
299
+ // 어댑터 connect 뒤·앱 생성 전에 listen 상태로 만들어 앱이 bridgeHub(localhost)로 붙을 수 있게 한다.
300
+ // 빈 acceptedTokens 면 MegaWsHub 생성자가 fail-fast throw(ADR-059). SIGTERM 시 drain(4503) 종료.
301
+ st.wsHub = await startEmbeddedWsHub(/** @type {any} */ (st.global).wsHub, st.logger)
302
+ },
303
+ },
304
+ {
305
+ name: 'boot-context',
306
+ needs: ['global'],
307
+ run: (st) => {
308
+ st.ctx = buildBootContext(st.global, st.logger)
309
+ },
310
+ },
311
+ ]
182
312
 
183
- // embedded wsHub (ADR-137) — `global.wsHub.enabled=true` 면 같은 프로세스에 hub 를 띄운다(single-node).
184
- // 어댑터 connect 뒤·앱 생성 전에 listen 상태로 만들어 앱이 bridgeHub(localhost) 붙을 수 있게 한다.
185
- // 빈 acceptedTokens 면 MegaWsHub 생성자가 fail-fast throw(ADR-059). SIGTERM 시 drain(4503) 종료.
186
- const wsHub = await startEmbeddedWsHub(/** @type {any} */ (global).wsHub, logger)
313
+ /** prepareRuntime step 이름 시퀀스(정본) — introspection·테스트용. */
314
+ export const PREPARE_STEP_NAMES = Object.freeze(PREPARE_STEPS.map((s) => s.name))
187
315
 
188
- const ctx = buildBootContext(global, logger)
189
- return { global, apps, host, ctx, wsHub }
316
+ /**
317
+ * 런타임 공통 준비 — {@link PREPARE_STEPS}(config plugins adapters → health-auto-checks →
318
+ * metrics → tracing → workers → ws-hub → boot-context)를 {@link runBootSteps} 로 실행한다.
319
+ * `bootApp`(서버)·`mega worker`/`mega scheduler`(CLI 호스트)가 **같은 순서**를 공유한다.
320
+ *
321
+ * @param {string} projectRoot
322
+ * @param {{ ping?: boolean, logger?: { debug?: Function, info?: Function, warn?: Function } }} [opts]
323
+ * @returns {Promise<{ global: Object, apps: Array<{ name: string, config: Object }>, host: MegaPluginHost, ctx: BootContext, wsHub: (MegaWsHub | null) }>}
324
+ */
325
+ export async function prepareRuntime(projectRoot, { ping = false, logger } = {}) {
326
+ /** @type {Record<string, any>} */
327
+ const st = { projectRoot, ping, logger }
328
+ await runBootSteps(PREPARE_STEPS, st, logger)
329
+ return { global: st.global, apps: st.apps, host: st.host, ctx: st.ctx, wsHub: st.wsHub }
190
330
  }
191
331
 
192
332
  /**
@@ -197,15 +337,27 @@ export async function prepareRuntime(projectRoot, { ping = false, logger } = {})
197
337
  * trustedProxies(목록)가 있으면 그것을, 없으면 trustProxy(boolean/number/string)를 그대로 넘긴다.
198
338
  * - timeouts.requestMs → Fastify `requestTimeout`(요청 수신 제한, slow-loris 보호).
199
339
  * - keepAliveMs → Fastify `keepAliveTimeout`(keep-alive 소켓 idle 제한, ALB 정합).
340
+ *
341
+ * 프로덕션(`NODE_ENV==='production'`)에서 trustProxy/trustedProxies 미설정이면 **부팅 warn 1회**(ADR-186).
342
+ * 프록시/LB 뒤에서 미설정 시 `req.protocol`/`req.ip` 가 프록시 기준으로 잡혀 secure 쿠키 누락·rate-limit
343
+ * 키 왜곡·CSRF Origin 오판으로 번진다 — 직접 listen 배포면 무시해도 되는 안내성 경고다.
344
+ *
200
345
  * @param {any} server - `global.server` config(없으면 빈 객체).
346
+ * @param {{ logger?: { warn?: Function }, env?: Record<string, string|undefined> }} [opts] - warn 출력용
347
+ * logger 와 환경(테스트 주입용, 기본 `process.env`).
201
348
  * @returns {{ trustProxy?: any, requestTimeout?: number, keepAliveTimeout?: number }}
202
349
  */
203
- export function serverFastifyOptions(server) {
350
+ export function serverFastifyOptions(server, { logger, env = process.env } = {}) {
204
351
  const s = server ?? {}
205
352
  /** @type {any} */
206
353
  const out = {}
207
354
  const trust = s.trustedProxies !== undefined ? s.trustedProxies : s.trustProxy
208
355
  if (trust !== undefined) out.trustProxy = trust
356
+ else if (env.NODE_ENV === 'production') {
357
+ logger?.warn?.(
358
+ 'server.trustProxy/trustedProxies not set in production — if this app runs behind a proxy/LB, req.protocol/req.ip will reflect the proxy (secure cookies, rate-limit keys and CSRF origin checks depend on them). Set server.trustProxy (ADR-181/186) or ignore if listening directly.',
359
+ )
360
+ }
209
361
  if (s.timeouts && s.timeouts.requestMs !== undefined) out.requestTimeout = s.timeouts.requestMs
210
362
  if (s.keepAliveMs !== undefined) out.keepAliveTimeout = s.keepAliveMs
211
363
  return out
@@ -229,19 +381,245 @@ async function startEmbeddedWsHub(cfg, logger) {
229
381
  logger,
230
382
  })
231
383
  const addr = await hub.start({ port: cfg.port, host: cfg.host })
232
- MegaShutdown.register('mega-ws-hub-embedded', async () => hub.stop({ drain: true }))
384
+ // 'workers' stage — 앱(bridge) 정리 뒤·어댑터 disconnect 앞에 hub drain 종료한다.
385
+ MegaShutdown.register('mega-ws-hub-embedded', async () => hub.stop({ drain: true }), { stage: 'workers' })
233
386
  logger?.debug?.({ host: addr.host, port: addr.port, hubId: hub.hubId }, 'boot.wsHub embedded started')
234
387
  return hub
235
388
  }
236
389
 
237
390
  /**
238
- * 프로젝트를 부팅한다config→어댑터→플러그인→앱→listen §14 순서로 엮는다.
391
+ * 부트 step `bootApp` 시퀀스 정본(02-architecture §14 declarative 로 코드화).
392
+ * state 입력 = { projectRoot, ping, logger, listen, port, listenHost } + PREPARE 산출물.
393
+ * 전 step `onFail: 'abort'`(기본) — 어느 단계든 실패하면 throw 하고, 호출자(bin/mega.js M-1)가
394
+ * `MegaShutdown.now` 로 이미 등록된 hook(어댑터 disconnect 등)을 실행해 정리한다.
395
+ * @type {BootStep[]}
396
+ */
397
+ const BOOT_STEPS = [
398
+ {
399
+ name: 'runtime',
400
+ run: async (st) => {
401
+ // 공통 토대(PREPARE_STEPS) — config → plugins → adapters → … → boot-context.
402
+ const prepared = await prepareRuntime(st.projectRoot, { ping: st.ping, logger: st.logger })
403
+ Object.assign(st, prepared)
404
+ },
405
+ },
406
+ {
407
+ name: 'before-boot-hook',
408
+ needs: ['host', 'ctx'],
409
+ run: async (st) => {
410
+ // beforeBoot hook(부팅 직전, fail-fast).
411
+ await st.host.runLifecycle('beforeBoot', st.ctx)
412
+ },
413
+ },
414
+ {
415
+ name: 'logger',
416
+ needs: ['global'],
417
+ run: (st) => {
418
+ // pino 로거(ADR-023/141) — global.logger 로 인스턴스를 한 번 만들어 모든 앱이 공유한다(worker thread·
419
+ // 파일 핸들 1벌). 비활성(logger 미설정/sinks 없음)이면 null → 앱은 logger:false(무로그). graceful shutdown
420
+ // 시 flush(버퍼·worker transport drain)는 'logs' stage — 종료 시퀀스의 마지막이라 어댑터 disconnect 등
421
+ // 종료 과정 자체의 로그까지 flush 된다(07-sequence §6 "로거는 항상 마지막").
422
+ const appLogger = buildLogger(/** @type {any} */ (st.global).logger)
423
+ st.appLogger = appLogger ?? null
424
+ if (appLogger) {
425
+ // 전역 에러 핸들러(unhandledRejection/uncaughtException, ADR-178)가 fatal 로그에 쓸 공유 로거를 주입한다.
426
+ // process 레벨 핸들러는 이 DI 그래프 밖이라 MegaShutdown 모듈 스코프로 넘긴다(globalThis 오염 회피).
427
+ MegaShutdown.setLogger(appLogger)
428
+ MegaShutdown.register('mega-logger', async () => {
429
+ await new Promise((resolve) => appLogger.flush(() => resolve(undefined)))
430
+ }, { stage: 'logs' })
431
+ }
432
+ },
433
+ },
434
+ {
435
+ name: 'server',
436
+ needs: ['global'],
437
+ run: (st) => {
438
+ // listen 포트·호스트 해석 — CLI 인자(`--port`/`--host`) 우선, 없으면 정본 server config
439
+ // (`global.server.port`/`host`, 04-data-models §183: port 기본 3000·host 기본 '0.0.0.0').
440
+ // boot 가 config→listen 의 유일한 배선 지점이라 여기서 읽지 않으면 `server.port`/`PORT` 가 죽는다.
441
+ // 최종 폴백(3000/'0.0.0.0')은 MegaServer.listen 이 가진다(여기선 undefined 면 그대로 위임).
442
+ const serverCfg = /** @type {any} */ (st.global).server ?? {}
443
+ st.resolvedPort = st.port ?? serverCfg.port
444
+ st.resolvedHost = st.listenHost ?? serverCfg.host
445
+ // server 운영 옵션 → Fastify 인스턴스 옵션(ADR-181). 앱 루프 밖에서 1회 — prod trustProxy 미설정
446
+ // warn(ADR-186)이 앱 수만큼 반복되지 않게 한다. 모든 앱에 동일 주입(Global-only).
447
+ st.fastifyOptions = serverFastifyOptions(serverCfg, { logger: st.appLogger ?? st.logger })
448
+ // logger 주입 — 미매핑 host 404·upgrade 소켓 에러 같은 vhost 레벨 이벤트가 pino 로 남는다.
449
+ st.server = new MegaServer({ port: st.resolvedPort, host: st.resolvedHost, logger: st.appLogger ?? st.logger })
450
+ },
451
+ },
452
+ {
453
+ name: 'apps',
454
+ needs: ['global', 'apps', 'host', 'server'],
455
+ run: async (st) => {
456
+ // 각 앱 Fastify 인스턴스 생성 + 라우트 자동 로딩 + 플러그인 주입 + vhost mount.
457
+ /** @type {MegaApp[]} */
458
+ const megaApps = []
459
+ for (const { name, config } of st.apps) {
460
+ const app = new MegaApp({
461
+ ...config,
462
+ name, // config.name(검증됨) 위에 폴더명을 확정(둘은 동일, ADR-067).
463
+ logger: st.appLogger ?? false, // pino 로거 주입(공유 인스턴스, ADR-141).
464
+ // ASP masterSecret 은 global 스코프(시크릿, scope-registry)라 앱 asp 옵트인에 합성한다(ADR-127).
465
+ // 앱 config.asp 가 http.enabledPaths/websocket 등 옵트인 범위를, global.asp 가 masterSecret 을 제공.
466
+ asp: composeAspConfig(/** @type {any} */ (st.global).asp, /** @type {any} */ (config).asp),
467
+ // 세션 쿠키 HMAC 시크릿은 global 스코프(server.sessionSecret, scope-registry)라 앱에 주입한다
468
+ // (ASP masterSecret 합성과 동일 패턴, ADR-129). 앱 session.secret 명시 시 그쪽이 우선(MegaApp).
469
+ sessionSecret: /** @type {any} */ (st.global).server?.sessionSecret,
470
+ // 운영 관측성 config 는 Global-only(ADR-072/131) — global `health` 블록을 모든 앱에 주입한다(/metrics
471
+ // 라우트 등록 + 보안 면제 + IP allowList). sessionSecret 주입과 동일 패턴.
472
+ health: /** @type {any} */ (st.global).health,
473
+ // server 운영 옵션(trustProxy/requestTimeout/keepAliveTimeout) → Fastify 인스턴스 옵션(ADR-181).
474
+ // Global-only 라 모든 앱에 동일 주입. MegaApp 이 Fastify({ ...fastifyOptions }) 로 전달.
475
+ fastifyOptions: st.fastifyOptions,
476
+ plugins: st.host.fastifyPlugins,
477
+ globalMiddlewares: st.host.globalMiddlewares,
478
+ })
479
+ // OpenAPI 옵트인 시 배리어(ADR-140): @fastify/swagger 의 onRoute 수집 훅은 plugin 로드 후 설치되므로,
480
+ // 라우트 동기 등록 전에 swagger 를 먼저 로드시켜야 라우트가 명세에 수집된다. `after()` 가 생성자에서 큐된
481
+ // 등록(swagger 포함)을 flush 한다. 비-openapi 앱은 timing 변경 없이 건너뜀(_openapiPath=null).
482
+ if (app._openapiPath) await app.fastify.after()
483
+ // 서비스 자동 로딩(ADR-148) — apps/<name>/services/*.js 를 name→Class 레지스트리로 만들어 앱에 주입한다.
484
+ // 라우트 등록 전에 채워 두면, 요청 ctx 의 ctx.services.<name> lazy DI 가 첫 요청부터 동작한다.
485
+ const servicesDir = join(st.projectRoot, 'apps', name, 'services')
486
+ const serviceRegistry = await loadServices({ servicesDir, appName: name })
487
+ app.setServiceRegistry(serviceRegistry)
488
+ st.logger?.debug?.({ app: name, services: serviceRegistry.size }, 'boot.services loaded')
489
+
490
+ const routesDir = join(st.projectRoot, 'apps', name, 'routes')
491
+ const { filesLoaded } = await loadRoutes({ fastify: app.fastify, appName: name, routesDir, app })
492
+ st.logger?.debug?.({ app: name, filesLoaded }, 'boot.routes loaded')
493
+ st.server.mount(app)
494
+ megaApps.push(app)
495
+ }
496
+ st.megaApps = megaApps
497
+ },
498
+ },
499
+ {
500
+ name: 'cluster-transport',
501
+ needs: ['global', 'apps', 'megaApps'],
502
+ run: async (st) => {
503
+ // 클러스터 전송 자동배선 (ADR-176) — 앱당 **하나**(상호배타, config-validator 가 충돌 fail-fast). config 로 선택:
504
+ // - app.config `bridgeHub` → **WS Hub**(`app.connectHub`, 평문 12-타입). `mega ws-hub` 서버에 자동 연결.
505
+ // - global `wsCluster.bus`(NATS) → **MegaWsCluster**(NATS pub/sub broadcast + roster 동기화).
506
+ // 둘 다 개발자가 connectHub 같은 코드를 쓰지 않고 config 만으로 동작한다(ADR-176 이 ADR-137 의 자동배선
507
+ // 거부를 **명시 config 가 있을 때만** 번복 — 설정이 곧 의도라 "추측 배선" 아님). megaApps[i] 와 apps[i] 동순서.
508
+ const wsClusterCfg = /** @type {any} */ (st.global).wsCluster
509
+ const wsClusterBus = wsClusterCfg?.bus ? getAdapter('bus', wsClusterCfg.bus) : null
510
+ for (let i = 0; i < st.megaApps.length; i++) {
511
+ const app = st.megaApps[i]
512
+ const a = /** @type {any} */ (app) // _deliverBroadcast/_deliverDirect 는 framework-internal.
513
+ const bridgeHub = /** @type {any} */ (st.apps[i].config).bridgeHub
514
+ if (bridgeHub?.url) {
515
+ // ⚠️ 클러스터 워커마다 별개 브릿지라 bridgeId 가 **워커별로 유일**해야 한다(허브 sessionId global-unique
516
+ // 계약). 모든 워커가 같은 bridgeId 면 bridge-subscriber sessionId(`bridge:<id>#<ch>`, ws-presence
517
+ // _resyncPresence)가 충돌해 허브가 계속 재할당(thrashing)한다(L-3). 설정 bridgeId 를 베이스로 워커
518
+ // 식별자(cluster.worker.id, 단일 프로세스면 pid)를 붙여 유일화한다. instanceId 도 동일하게 유일화
519
+ // (hub-link 는 instanceId 미지정 시 bridgeId 로 폴백하므로).
520
+ const baseId = bridgeHub.bridgeId ?? app.name
521
+ const workerTag = nodeCluster.worker?.id ?? process.pid
522
+ const uniqueBridgeId = `${baseId}-w${workerTag}`
523
+ // WS Hub 자동연결(ADR-065/176). connectHub 가 hub link 를 세워 app.broadcast/joinSession 의 hub 경로를
524
+ // 활성화한다(shutdown hook 도 connectHub 가 등록). 허브 다운이어도 boot 를 막지 않는다 — 초기 연결
525
+ // 실패는 warn 하고(retry 설정 시 백그라운드 재연결, ADR-098) 앱은 계속 뜬다(로컬 전달은 동작).
526
+ try {
527
+ await app.connectHub({ ...bridgeHub, bridgeId: uniqueBridgeId, instanceId: bridgeHub.instanceId ?? uniqueBridgeId })
528
+ st.logger?.debug?.({ app: app.name, url: bridgeHub.url, bridgeId: uniqueBridgeId }, 'boot.bridgeHub connected (ADR-176)')
529
+ } catch (err) {
530
+ st.logger?.warn?.({ err, app: app.name, url: bridgeHub.url }, 'boot.bridgeHub initial connect failed (retry if configured)')
531
+ }
532
+ // redis roster 자동배선(ADR-177) — `bridgeHub.roster.driver==='redis'` 면 **채널별 접속자 목록**을 redis HASH 로
533
+ // 관리한다(broadcast 와 별개 — 멀티 허브에서도 정합, 신규/재연결 브릿지가 즉시 전체 명단). 캐시 어댑터의 raw
534
+ // ioredis(`.native`)를 쓰고, heartbeat 로 crash 워커 stale 정리. hub 연결 성패와 무관(roster 는 redis 독립).
535
+ const rosterCfg = /** @type {any} */ (bridgeHub).roster
536
+ if (rosterCfg?.driver === 'redis') {
537
+ const cacheAdapter = /** @type {any} */ (getAdapter('cache', rosterCfg.cache))
538
+ const redis = cacheAdapter?.native
539
+ if (!redis || typeof redis.hset !== 'function') {
540
+ st.logger?.warn?.({ app: app.name, cache: rosterCfg.cache }, 'boot.wsRoster: cache adapter has no native redis — roster disabled')
541
+ } else {
542
+ const roster = new MegaWsRedisRoster({ redis, getLocalMembers: () => a.localRosterMembers(), ttlMs: rosterCfg.ttlMs, keyPrefix: rosterCfg.keyPrefix, logger: st.logger })
543
+ roster.startHeartbeat()
544
+ a.setWsRoster(roster)
545
+ MegaShutdown.register(`mega-wsroster:${app.name}`, () => roster.stop())
546
+ st.logger?.debug?.({ app: app.name, cache: rosterCfg.cache }, 'boot.wsRoster connected (ADR-177, redis)')
547
+ }
548
+ }
549
+ } else if (wsClusterBus) {
550
+ const cluster = new MegaWsCluster({
551
+ bus: /** @type {any} */ (wsClusterBus),
552
+ appName: app.name,
553
+ deliverBroadcast: (/** @type {any} */ p) => a._deliverBroadcast(p),
554
+ deliverDirect: (/** @type {any} */ p) => a._deliverDirect(p),
555
+ subjectPrefix: wsClusterCfg.subjectPrefix,
556
+ roster: wsClusterCfg.roster,
557
+ logger: /** @type {any} */ (app.fastify.log),
558
+ })
559
+ await cluster.start()
560
+ app.setWsCluster(cluster)
561
+ // 'app' stage(기본) — 어댑터 disconnect 보다 먼저 정리(graceful LEAVE 가 NATS 끊기기 전에 나가게).
562
+ MegaShutdown.register(`mega-ws-cluster:${app.name}`, async () => cluster.stop())
563
+ }
564
+ }
565
+ if (wsClusterBus) {
566
+ st.logger?.debug?.({ bus: wsClusterCfg.bus, roster: wsClusterCfg.roster?.driver ?? 'none' }, 'boot.wsCluster wired (ADR-176)')
567
+ }
568
+ },
569
+ },
570
+ {
571
+ name: 'listen',
572
+ needs: ['server'],
573
+ run: async (st) => {
574
+ // HTTP listen(분기 — CLI/테스트가 listen=false 로 mount 까지만 받을 수 있음).
575
+ if (!st.listen) return
576
+ await st.server.listen({ port: st.resolvedPort, host: st.resolvedHost })
577
+ st.logger?.info?.({ hosts: st.server.hosts }, 'boot.listening')
578
+ },
579
+ },
580
+ {
581
+ name: 'after-boot-hook',
582
+ needs: ['host', 'ctx'],
583
+ run: async (st) => {
584
+ // afterBoot hook(부팅 완료).
585
+ await st.host.runLifecycle('afterBoot', st.ctx)
586
+ },
587
+ },
588
+ {
589
+ name: 'shutdown-bridge',
590
+ needs: ['host', 'ctx'],
591
+ run: (st) => {
592
+ // beforeShutdown hook 을 graceful 종료 경로에 브리지(per-hook catch — graceful 중 한 hook 실패가
593
+ // 나머지 정리를 막으면 안 됨, ADR-122). 'app' stage(기본)라 어댑터 disconnect 보다 먼저 실행된다
594
+ // (플러그인이 어댑터를 쓰는 정리를 어댑터 종료 전에 끝냄).
595
+ const shutdownHooks = st.host.lifecycleHooks('beforeShutdown')
596
+ if (shutdownHooks.length > 0) {
597
+ MegaShutdown.register('plugin:beforeShutdown', async () => {
598
+ for (const fn of shutdownHooks) {
599
+ try {
600
+ await fn(st.ctx)
601
+ } catch (err) {
602
+ st.logger?.warn?.({ err }, 'plugin beforeShutdown hook failed (continuing shutdown)')
603
+ }
604
+ }
605
+ })
606
+ }
607
+ },
608
+ },
609
+ ]
610
+
611
+ /** bootApp step 이름 시퀀스(정본) — introspection·테스트용. */
612
+ export const BOOT_STEP_NAMES = Object.freeze(BOOT_STEPS.map((s) => s.name))
613
+
614
+ /**
615
+ * 프로젝트를 부팅한다 — {@link BOOT_STEPS}(runtime → before-boot-hook → logger → server → apps →
616
+ * cluster-transport → listen → after-boot-hook → shutdown-bridge)를 {@link runBootSteps} 로 실행한다.
239
617
  *
240
- * 어느 단계든 실패하면 그대로 throw(fail-fast). 어댑터 connect 실패는 매니저가 LIFO cleanup 하고,
241
- * 그 외 단계 실패(예: server.listen EADDRINUSE, beforeBoot hook throw)는 어댑터가 이미 connect 돼
242
- * `'adapters:disconnect'` MegaShutdown hook 이 등록된 상태라, 호출자가 `MegaShutdown.now` 로 그 hook 을
243
- * LIFO 실행해 정리해야 한다 — `bin/mega.js` 의 catch 가 이 정리 경로를 배선한다(M-1). 그러지 않으면
244
- * 연결된 어댑터가 이벤트루프를 살려 프로세스가 hang 한다.
618
+ * 어느 step 이든 실패하면 그대로 throw(fail-fast, 전 step onFail='abort'). 어댑터 connect 실패는
619
+ * 매니저가 LIFO cleanup 하고, 그 외 단계 실패(예: server.listen EADDRINUSE, beforeBoot hook throw)는
620
+ * 어댑터가 이미 connect 돼 `'adapters:disconnect'` MegaShutdown hook 이 등록된 상태라, 호출자가
621
+ * `MegaShutdown.now` 로 그 hook 을 실행해 정리해야 한다 — `bin/mega.js` 의 catch 가 이 정리 경로를
622
+ * 배선한다(M-1). 그러지 않으면 연결된 어댑터가 이벤트루프를 살려 프로세스가 hang 한다.
245
623
  *
246
624
  * @param {string} projectRoot - 프로젝트 루트 절대 경로(mega.config.js 가 있는 곳).
247
625
  * @param {Object} [opts]
@@ -254,168 +632,9 @@ async function startEmbeddedWsHub(cfg, logger) {
254
632
  */
255
633
  export async function bootApp(projectRoot, { listen = true, port, host: listenHost, ping = false, logger } = {}) {
256
634
  logger?.debug?.({ projectRoot }, 'boot.start')
257
-
258
- // 1~5단계 + 어댑터 connect 공통 토대(config 플러그인 install → 어댑터 build → connect → boot ctx).
259
- const { global, apps, host, ctx, wsHub } = await prepareRuntime(projectRoot, { ping, logger })
260
-
261
- // 7단계: beforeBoot hook(부팅 직전, fail-fast).
262
- await host.runLifecycle('beforeBoot', ctx)
263
-
264
- // pino 로거(ADR-023/141) — global.logger 로 인스턴스를 한 번 만들어 모든 앱이 공유한다(worker thread·
265
- // 파일 핸들 1벌). 비활성(logger 미설정/sinks 없음)이면 null → 앱은 logger:false(무로그). graceful shutdown
266
- // 시 flush(버퍼·worker transport drain). MegaShutdown LIFO 라 어댑터/앱보다 나중 등록 = 가장 먼저 정리되지
267
- // 않게(로그가 종료 과정 끝까지 살아 있도록) — 마지막 flush 단계(07-sequence §6).
268
- const appLogger = buildLogger(/** @type {any} */ (global).logger)
269
- if (appLogger) {
270
- // 전역 에러 핸들러(unhandledRejection/uncaughtException, ADR-178)가 fatal 로그에 쓸 공유 로거를 주입한다.
271
- // process 레벨 핸들러는 이 DI 그래프 밖이라 MegaShutdown 모듈 스코프로 넘긴다(globalThis 오염 회피).
272
- MegaShutdown.setLogger(appLogger)
273
- MegaShutdown.register('mega-logger', async () => {
274
- await new Promise((resolve) => appLogger.flush(() => resolve(undefined)))
275
- })
276
- }
277
-
278
- // listen 포트·호스트 해석 — CLI 인자(`--port`/`--host`) 우선, 없으면 정본 server config
279
- // (`global.server.port`/`host`, 04-data-models §183: port 기본 3000·host 기본 '0.0.0.0').
280
- // boot 가 config→listen 의 유일한 배선 지점이라 여기서 읽지 않으면 `server.port`/`PORT` 가 죽는다.
281
- // 최종 폴백(3000/'0.0.0.0')은 MegaServer.listen 이 가진다(여기선 undefined 면 그대로 위임).
282
- const serverCfg = /** @type {any} */ (global).server ?? {}
283
- const resolvedPort = port ?? serverCfg.port
284
- const resolvedHost = listenHost ?? serverCfg.host
285
-
286
- // 6단계: 각 앱 Fastify 인스턴스 생성 + 라우트 자동 로딩 + 플러그인 주입 + vhost mount.
287
- const server = new MegaServer({ port: resolvedPort, host: resolvedHost })
288
- /** @type {MegaApp[]} */
289
- const megaApps = []
290
- for (const { name, config } of apps) {
291
- const app = new MegaApp({
292
- ...config,
293
- name, // config.name(검증됨) 위에 폴더명을 확정(둘은 동일, ADR-067).
294
- logger: appLogger ?? false, // pino 로거 주입(공유 인스턴스, ADR-141).
295
- // ASP masterSecret 은 global 스코프(시크릿, scope-registry)라 앱 asp 옵트인에 합성한다(ADR-127).
296
- // 앱 config.asp 가 http.enabledPaths/websocket 등 옵트인 범위를, global.asp 가 masterSecret 을 제공.
297
- asp: composeAspConfig(/** @type {any} */ (global).asp, /** @type {any} */ (config).asp),
298
- // 세션 쿠키 HMAC 시크릿은 global 스코프(server.sessionSecret, scope-registry)라 앱에 주입한다
299
- // (ASP masterSecret 합성과 동일 패턴, ADR-129). 앱 session.secret 명시 시 그쪽이 우선(MegaApp).
300
- sessionSecret: /** @type {any} */ (global).server?.sessionSecret,
301
- // 운영 관측성 config 는 Global-only(ADR-072/131) — global `health` 블록을 모든 앱에 주입한다(/metrics
302
- // 라우트 등록 + 보안 면제 + IP allowList). sessionSecret 주입과 동일 패턴.
303
- health: /** @type {any} */ (global).health,
304
- // server 운영 옵션(trustProxy/requestTimeout/keepAliveTimeout) → Fastify 인스턴스 옵션(ADR-181).
305
- // Global-only 라 모든 앱에 동일 주입. MegaApp 이 Fastify({ ...fastifyOptions }) 로 전달.
306
- fastifyOptions: serverFastifyOptions(serverCfg),
307
- plugins: host.fastifyPlugins,
308
- globalMiddlewares: host.globalMiddlewares,
309
- })
310
- // OpenAPI 옵트인 시 배리어(ADR-140): @fastify/swagger 의 onRoute 수집 훅은 plugin 로드 후 설치되므로,
311
- // 라우트 동기 등록 전에 swagger 를 먼저 로드시켜야 라우트가 명세에 수집된다. `after()` 가 생성자에서 큐된
312
- // 등록(swagger 포함)을 flush 한다. 비-openapi 앱은 timing 변경 없이 건너뜀(_openapiPath=null).
313
- if (app._openapiPath) await app.fastify.after()
314
- // 서비스 자동 로딩(ADR-148) — apps/<name>/services/*.js 를 name→Class 레지스트리로 만들어 앱에 주입한다.
315
- // 라우트 등록 전에 채워 두면, 요청 ctx 의 ctx.services.<name> lazy DI 가 첫 요청부터 동작한다.
316
- const servicesDir = join(projectRoot, 'apps', name, 'services')
317
- const serviceRegistry = await loadServices({ servicesDir, appName: name })
318
- app.setServiceRegistry(serviceRegistry)
319
- logger?.debug?.({ app: name, services: serviceRegistry.size }, 'boot.services loaded')
320
-
321
- const routesDir = join(projectRoot, 'apps', name, 'routes')
322
- const { filesLoaded } = await loadRoutes({ fastify: app.fastify, appName: name, routesDir, app })
323
- logger?.debug?.({ app: name, filesLoaded }, 'boot.routes loaded')
324
- server.mount(app)
325
- megaApps.push(app)
326
- }
327
-
328
- // 클러스터 전송 자동배선 (ADR-176) — 앱당 **하나**(상호배타, config-validator 가 충돌 fail-fast). config 로 선택:
329
- // - app.config `bridgeHub` → **WS Hub**(`app.connectHub`, 평문 12-타입). `mega ws-hub` 서버에 자동 연결.
330
- // - global `wsCluster.bus`(NATS) → **MegaWsCluster**(NATS pub/sub broadcast + roster 동기화).
331
- // 둘 다 개발자가 connectHub 같은 코드를 쓰지 않고 config 만으로 동작한다(ADR-176 이 ADR-137 의 자동배선
332
- // 거부를 **명시 config 가 있을 때만** 번복 — 설정이 곧 의도라 "추측 배선" 아님). megaApps[i] 와 apps[i] 동순서.
333
- const wsClusterCfg = /** @type {any} */ (global).wsCluster
334
- const wsClusterBus = wsClusterCfg?.bus ? getAdapter('bus', wsClusterCfg.bus) : null
335
- for (let i = 0; i < megaApps.length; i++) {
336
- const app = megaApps[i]
337
- const a = /** @type {any} */ (app) // _deliverBroadcast/_deliverDirect 는 framework-internal(@private).
338
- const bridgeHub = /** @type {any} */ (apps[i].config).bridgeHub
339
- if (bridgeHub?.url) {
340
- // ⚠️ 클러스터 워커마다 별개 브릿지라 bridgeId 가 **워커별로 유일**해야 한다(허브 sessionId global-unique
341
- // 계약). 모든 워커가 같은 bridgeId 면 bridge-subscriber sessionId(`bridge:<id>#<ch>`, mega-app
342
- // _resyncPresence)가 충돌해 허브가 계속 재할당(thrashing)한다(L-3). 설정 bridgeId 를 베이스로 워커
343
- // 식별자(cluster.worker.id, 단일 프로세스면 pid)를 붙여 유일화한다. instanceId 도 동일하게 유일화
344
- // (hub-link 는 instanceId 미지정 시 bridgeId 로 폴백하므로).
345
- const baseId = bridgeHub.bridgeId ?? app.name
346
- const workerTag = nodeCluster.worker?.id ?? process.pid
347
- const uniqueBridgeId = `${baseId}-w${workerTag}`
348
- // WS Hub 자동연결(ADR-065/176). connectHub 가 _hubLink 를 세워 app.broadcast/joinSession 의 hub 경로를
349
- // 활성화한다(shutdown hook 도 connectHub 가 등록). 허브 다운이어도 boot 를 막지 않는다 — 초기 연결
350
- // 실패는 warn 하고(retry 설정 시 백그라운드 재연결, ADR-098) 앱은 계속 뜬다(로컬 전달은 동작).
351
- try {
352
- await app.connectHub({ ...bridgeHub, bridgeId: uniqueBridgeId, instanceId: bridgeHub.instanceId ?? uniqueBridgeId })
353
- logger?.debug?.({ app: app.name, url: bridgeHub.url, bridgeId: uniqueBridgeId }, 'boot.bridgeHub connected (ADR-176)')
354
- } catch (err) {
355
- logger?.warn?.({ err, app: app.name, url: bridgeHub.url }, 'boot.bridgeHub initial connect failed (retry if configured)')
356
- }
357
- // redis roster 자동배선(ADR-177) — `bridgeHub.roster.driver==='redis'` 면 **채널별 접속자 목록**을 redis HASH 로
358
- // 관리한다(broadcast 와 별개 — 멀티 허브에서도 정합, 신규/재연결 브릿지가 즉시 전체 명단). 캐시 어댑터의 raw
359
- // ioredis(`.native`)를 쓰고, heartbeat 로 crash 워커 stale 정리. hub 연결 성패와 무관(roster 는 redis 독립).
360
- const rosterCfg = /** @type {any} */ (bridgeHub).roster
361
- if (rosterCfg?.driver === 'redis') {
362
- const cacheAdapter = /** @type {any} */ (getAdapter('cache', rosterCfg.cache))
363
- const redis = cacheAdapter?.native
364
- if (!redis || typeof redis.hset !== 'function') {
365
- logger?.warn?.({ app: app.name, cache: rosterCfg.cache }, 'boot.wsRoster: cache adapter has no native redis — roster disabled')
366
- } else {
367
- const roster = new MegaWsRedisRoster({ redis, getLocalMembers: () => a.localRosterMembers(), ttlMs: rosterCfg.ttlMs, keyPrefix: rosterCfg.keyPrefix, logger })
368
- roster.startHeartbeat()
369
- a.setWsRoster(roster)
370
- MegaShutdown.register(`mega-wsroster:${app.name}`, () => roster.stop())
371
- logger?.debug?.({ app: app.name, cache: rosterCfg.cache }, 'boot.wsRoster connected (ADR-177, redis)')
372
- }
373
- }
374
- } else if (wsClusterBus) {
375
- const cluster = new MegaWsCluster({
376
- bus: /** @type {any} */ (wsClusterBus),
377
- appName: app.name,
378
- deliverBroadcast: (/** @type {any} */ p) => a._deliverBroadcast(p),
379
- deliverDirect: (/** @type {any} */ p) => a._deliverDirect(p),
380
- subjectPrefix: wsClusterCfg.subjectPrefix,
381
- roster: wsClusterCfg.roster,
382
- logger: /** @type {any} */ (app.fastify.log),
383
- })
384
- await cluster.start()
385
- app.setWsCluster(cluster)
386
- // MegaShutdown LIFO — 어댑터 disconnect 보다 먼저 정리(graceful LEAVE 가 NATS 끊기기 전에 나가게).
387
- MegaShutdown.register(`mega-ws-cluster:${app.name}`, async () => cluster.stop())
388
- }
389
- }
390
- if (wsClusterBus) {
391
- logger?.debug?.({ bus: wsClusterCfg.bus, roster: wsClusterCfg.roster?.driver ?? 'none' }, 'boot.wsCluster wired (ADR-176)')
392
- }
393
-
394
- // 9단계: HTTP listen(분기 — CLI/테스트가 listen=false 로 mount 까지만 받을 수 있음).
395
- if (listen) {
396
- await server.listen({ port: resolvedPort, host: resolvedHost })
397
- logger?.info?.({ hosts: server.hosts }, 'boot.listening')
398
- }
399
-
400
- // 10단계: afterBoot hook(부팅 완료).
401
- await host.runLifecycle('afterBoot', ctx)
402
-
403
- // beforeShutdown hook 을 graceful 종료 경로에 브리지(per-hook catch — graceful 중 한 hook 실패가
404
- // 나머지 정리를 막으면 안 됨, ADR-122). MegaShutdown LIFO 라 어댑터 hook 보다 나중 등록 =
405
- // 어댑터 disconnect 보다 먼저 실행(플러그인이 어댑터를 쓰는 정리를 어댑터 종료 전에 끝냄).
406
- const shutdownHooks = host.lifecycleHooks('beforeShutdown')
407
- if (shutdownHooks.length > 0) {
408
- MegaShutdown.register('plugin:beforeShutdown', async () => {
409
- for (const fn of shutdownHooks) {
410
- try {
411
- await fn(ctx)
412
- } catch (err) {
413
- logger?.warn?.({ err }, 'plugin beforeShutdown hook failed (continuing shutdown)')
414
- }
415
- }
416
- })
417
- }
418
-
635
+ /** @type {Record<string, any>} */
636
+ const st = { projectRoot, ping, logger, listen, port, listenHost }
637
+ await runBootSteps(BOOT_STEPS, st, logger)
419
638
  logger?.debug?.('boot.done')
420
- return { server, host, config: global, apps, megaApps, ctx, wsHub, appLogger }
639
+ return { server: st.server, host: st.host, config: st.global, apps: st.apps, megaApps: st.megaApps, ctx: st.ctx, wsHub: st.wsHub, appLogger: st.appLogger }
421
640
  }