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.
- package/README.md +9 -0
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +33 -9
- package/sample/crud/.env +10 -1
- package/sample/crud/.env.example +10 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +10 -2
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +20 -6
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +44 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +33 -7
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +10 -1
- package/src/adapters/mongo-adapter.js +40 -8
- package/src/adapters/postgres-adapter.js +33 -6
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +26 -3
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +173 -33
- package/src/cli/generators/index.js +140 -3
- package/src/cli/index.js +437 -155
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +30 -3
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +65 -3
- package/src/core/envelope.js +119 -12
- package/src/core/hub-link.js +89 -18
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +7 -3
- package/src/core/mega-app.js +253 -505
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +131 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +70 -12
- package/src/core/session-store.js +14 -1
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +636 -0
- package/src/core/ws-roster.js +50 -8
- package/src/core/ws-upgrade.js +223 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +35 -4
- package/src/lib/mega-job-queue.js +151 -34
- package/src/lib/mega-job.js +37 -1
- package/src/lib/mega-metrics.js +31 -13
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +33 -6
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +139 -15
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/adr/code.tpl +23 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +93 -0
- package/types/adapters/file-adapter.d.ts +105 -0
- package/types/adapters/file-session-adapter.d.ts +103 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +117 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +73 -0
- package/types/adapters/mega-db-adapter.d.ts +50 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +150 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +141 -0
- package/types/adapters/redis-adapter.d.ts +78 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +112 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +122 -0
- package/types/cli/index.d.ts +234 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +103 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +266 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +93 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +25 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +108 -0
- package/types/core/ws-upgrade.d.ts +260 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +243 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +48 -0
- package/types/lib/mega-job-queue.d.ts +188 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +145 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +129 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +259 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- 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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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('../
|
|
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
|
-
*
|
|
122
|
-
* `
|
|
123
|
-
*
|
|
119
|
+
* 공유 어댑터 전수의 `healthCheck()` 를 `MegaHealth` 에 `<domain>:<globalKey>` 이름으로 등록한다.
|
|
120
|
+
* `/health/ready` 가 어댑터 생사를 실제로 반영하게 하는 배선 — `prepareRuntime` 이 connect 직후
|
|
121
|
+
* 호출한다(`health.autoChecks:false` 옵트아웃). 등록은 이름 기준 덮어쓰기라 재부팅(테스트)에도 멱등.
|
|
124
122
|
*
|
|
125
|
-
* @param {string}
|
|
126
|
-
*
|
|
127
|
-
* @returns {
|
|
123
|
+
* @param {Array<{ domain: string, key: string, adapter: { healthCheck: () => Promise<{ ok: boolean }> } }>} [entries] -
|
|
124
|
+
* 대상 어댑터 엔트리(기본: 어댑터 매니저의 전체 등록분). 테스트 주입용.
|
|
125
|
+
* @returns {string[]} 등록한 체크 이름 목록.
|
|
128
126
|
*/
|
|
129
|
-
export
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* 어느
|
|
241
|
-
* 그 외 단계 실패(예: server.listen EADDRINUSE, beforeBoot hook throw)는
|
|
242
|
-
* `'adapters:disconnect'` MegaShutdown hook 이 등록된 상태라, 호출자가
|
|
243
|
-
*
|
|
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
|
-
|
|
259
|
-
|
|
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
|
}
|