mega-framework 0.1.5 → 0.1.7
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/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +156 -8
- package/sample/crud/.env.example +153 -28
- 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/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 +63 -3
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +2 -2
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +30 -0
- package/src/adapters/maria-adapter.js +26 -3
- package/src/adapters/mega-db-adapter.js +7 -1
- package/src/adapters/mongo-adapter.js +19 -1
- package/src/adapters/postgres-adapter.js +25 -2
- package/src/adapters/sqlite-adapter.js +20 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +137 -33
- package/src/cli/generators/index.js +82 -2
- package/src/cli/index.js +478 -104
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +485 -237
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/config-validator.js +25 -0
- package/src/core/ctx-builder.js +6 -2
- package/src/core/envelope.js +112 -12
- package/src/core/hub-link.js +65 -4
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +6 -2
- package/src/core/mega-app.js +223 -481
- package/src/core/mega-cluster.js +54 -13
- 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 +129 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +0 -1
- package/src/core/security.js +67 -9
- 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 +624 -0
- package/src/core/ws-roster.js +4 -1
- package/src/core/ws-upgrade.js +118 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-health.js +25 -4
- package/src/lib/mega-job-queue.js +98 -21
- package/src/lib/mega-job.js +29 -0
- package/src/lib/mega-logger.js +1 -1
- package/src/lib/mega-metrics.js +3 -12
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +162 -49
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +5 -1
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +51 -8
- 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/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 +91 -0
- package/types/adapters/file-adapter.d.ts +94 -0
- package/types/adapters/file-session-adapter.d.ts +101 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +115 -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 +47 -0
- package/types/adapters/mega-db-adapter.d.ts +47 -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 +139 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +139 -0
- package/types/adapters/redis-adapter.d.ts +70 -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 +106 -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 +112 -0
- package/types/cli/index.d.ts +249 -0
- package/types/cli/template-engine.d.ts +40 -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 +80 -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 +264 -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 +92 -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 +18 -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 +96 -0
- package/types/core/ws-upgrade.d.ts +231 -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 +241 -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 +41 -0
- package/types/lib/mega-job-queue.d.ts +176 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +138 -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 +127 -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 +234 -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/mega-cluster.js
CHANGED
|
@@ -32,9 +32,14 @@ export class MegaCluster {
|
|
|
32
32
|
* @param {Object} [opts]
|
|
33
33
|
* @param {number | 'max'} [opts.instances] - 워커 수. 'max' 면 CPU 코어 수.
|
|
34
34
|
* @param {boolean} [opts.respawn=true] - 워커 crash 시 자동 재시작
|
|
35
|
-
* @param {number} [opts.gracePeriodMs=30000] - SIGTERM 후 강제 kill 까지
|
|
35
|
+
* @param {number} [opts.gracePeriodMs=30000] - SIGTERM 후 강제 kill 까지 대기. ⚠️ 워커가
|
|
36
|
+
* `MegaShutdown.setupSignals()` 를 쓰면 워커 종료 예산 상한은 hardKill(기본 60s)이다 — 마스터 grace 가
|
|
37
|
+
* 그보다 작으면 워커 drain 도중 SIGKILL 되는 예산 역전이 생긴다. `mega start` CLI 는
|
|
38
|
+
* `DEFAULT_HARD_KILL_MS + 5s` 로 위계화해 전달한다(직접 생성 시에도 동일 권장).
|
|
36
39
|
* @param {import('node:cluster').Cluster} [opts._cluster] - 테스트용 cluster 주입(기본 node:cluster). per ADR-165
|
|
37
40
|
* @param {NodeJS.Process} [opts._proc] - 테스트용 process 주입(기본 전역 process). per ADR-165
|
|
41
|
+
* @param {{ info?: Function, warn?: Function, error?: Function } | null} [opts.logger] - 조율 로그용 pino 로거.
|
|
42
|
+
* 미설정이면 console 폴백(마스터는 bootApp 을 안 해 pino 인스턴스가 없을 수 있다, ADR-180).
|
|
38
43
|
*/
|
|
39
44
|
constructor(opts = {}) {
|
|
40
45
|
this._instances = resolveInstances(opts.instances)
|
|
@@ -45,6 +50,8 @@ export class MegaCluster {
|
|
|
45
50
|
// cluster/process 를 주입 seam 으로 분리해 fake 로 in-process 단위 검증한다. per ADR-165
|
|
46
51
|
this._cluster = opts._cluster ?? cluster
|
|
47
52
|
this._proc = opts._proc ?? process
|
|
53
|
+
/** @type {{ info?: Function, warn?: Function, error?: Function } | null} 조율 로그용 로거(없으면 console). */
|
|
54
|
+
this._logger = opts.logger ?? null
|
|
48
55
|
/** @type {Set<MegaWorker>} */
|
|
49
56
|
this._workers = new Set()
|
|
50
57
|
/** @type {(() => Promise<void>) | null} */
|
|
@@ -92,12 +99,43 @@ export class MegaCluster {
|
|
|
92
99
|
return !this._cluster.isPrimary
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/**
|
|
103
|
+
* 조율 로그용 로거 주입(생성 후, start 전에 호출). 마스터는 bootApp 을 안 해 pino 가 없으므로 CLI 가
|
|
104
|
+
* config 로 만든 인스턴스를 넘긴다(ADR-180). 미주입이면 console 폴백.
|
|
105
|
+
* @param {{ info?: Function, warn?: Function, error?: Function } | null | undefined} logger
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
108
|
+
setLogger(logger) {
|
|
109
|
+
this._logger = logger ?? null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @private 조율 로그 — 주입 로거가 있으면 그 레벨로, 없으면 console 폴백(`[mega-cluster]` 접두).
|
|
114
|
+
* @param {'info'|'warn'|'error'} level @param {string} msg
|
|
115
|
+
*/
|
|
116
|
+
_log(level, msg) {
|
|
117
|
+
const log = this._logger
|
|
118
|
+
const text = `[mega-cluster] ${msg}`
|
|
119
|
+
if (log && typeof (/** @type {any} */ (log)[level]) === 'function') /** @type {any} */ (log)[level](text)
|
|
120
|
+
else (level === 'warn' ? console.warn : level === 'error' ? console.error : console.log)(text)
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
/**
|
|
96
124
|
* @private 마스터 프로세스 부팅 시퀀스.
|
|
97
125
|
* @param {() => Promise<void>} workerFn
|
|
98
126
|
*/
|
|
99
127
|
async _startPrimary(workerFn) {
|
|
100
128
|
this._workerFn = workerFn
|
|
129
|
+
// 멀티워커 + watch 방어(ADR-182) — 워커는 마스터의 `execArgv` 를 상속하므로, `--watch*` 가 있으면
|
|
130
|
+
// 제거해 각 워커가 자기-watch 로 폭주(파일 변경 시 제각각 재시작)하는 것을 막는다. dev 권장 경로는
|
|
131
|
+
// 단일 프로세스(`mega start --watch`)라 보통 안 타지만, 수동 `node --watch … mega start --cluster N`
|
|
132
|
+
// 케이스의 방어 가드다. 단언이 아닌 setupPrimary 로 fork 전에 1회 적용.
|
|
133
|
+
const execArgv = this._proc.execArgv ?? []
|
|
134
|
+
const cleaned = execArgv.filter((a) => !a.startsWith('--watch'))
|
|
135
|
+
if (cleaned.length !== execArgv.length && typeof this._cluster.setupPrimary === 'function') {
|
|
136
|
+
this._cluster.setupPrimary({ execArgv: cleaned })
|
|
137
|
+
this._log('warn', 'stripped --watch* from worker execArgv (multi-worker watch chaos guard, ADR-182)')
|
|
138
|
+
}
|
|
101
139
|
// workerFn 은 마스터에서 사용 안 함 (정보 전달용 placeholder)
|
|
102
140
|
for (let i = 0; i < this._instances; i++) {
|
|
103
141
|
this._forkWorker()
|
|
@@ -107,13 +145,13 @@ export class MegaCluster {
|
|
|
107
145
|
const onSignal = (/** @type {string} */ sig) => {
|
|
108
146
|
if (this._shuttingDown) return
|
|
109
147
|
this._shuttingDown = true
|
|
110
|
-
|
|
148
|
+
this._log('info', `received ${sig}, broadcasting shutdown to ${this._workers.size} workers (grace ${this._gracePeriodMs}ms)`)
|
|
111
149
|
this._broadcastShutdown()
|
|
112
150
|
// grace 초과 시 강제 kill. 핸들을 보관해 전원 정상종료(exit 핸들러) 시 취소 — 정상 종료가 이 타이머의
|
|
113
151
|
// exit(1) 과 경합해 exit code 가 흔들리지 않게 한다(k8s/systemd 종료 판정 노이즈 제거).
|
|
114
152
|
this._graceTimer = setTimeout(() => {
|
|
115
153
|
for (const w of this._workers) {
|
|
116
|
-
try { w.kill('SIGKILL') } catch (err) {
|
|
154
|
+
try { w.kill('SIGKILL') } catch (err) { this._log('warn', `SIGKILL failed: ${err.message}`) }
|
|
117
155
|
}
|
|
118
156
|
this._proc.exit(1)
|
|
119
157
|
}, this._gracePeriodMs)
|
|
@@ -134,7 +172,7 @@ export class MegaCluster {
|
|
|
134
172
|
if (this._workers.size === 0) {
|
|
135
173
|
if (this._graceTimer) clearTimeout(this._graceTimer) // 전원 정상종료 — 강제-kill 타이머 취소(exit code 경합 제거).
|
|
136
174
|
this._graceTimer = null
|
|
137
|
-
|
|
175
|
+
this._log('info', 'all workers exited, primary exiting 0')
|
|
138
176
|
this._proc.exit(0)
|
|
139
177
|
}
|
|
140
178
|
return
|
|
@@ -152,19 +190,21 @@ export class MegaCluster {
|
|
|
152
190
|
if (this._rapidCrashTimes.length >= this._maxRapidRespawn) {
|
|
153
191
|
// H-1 — crash-loop 포기 시 그냥 return 하면 이벤트루프가 비어 exit 0(성공)으로 보인다 →
|
|
154
192
|
// systemd/k8s 가 "정상 종료" 로 오판. 명시적 exit 1 로 비정상 종료를 알려 재시작을 유도한다.
|
|
155
|
-
|
|
156
|
-
|
|
193
|
+
this._log(
|
|
194
|
+
'error',
|
|
195
|
+
`rapid crash-loop detected (${this._rapidCrashTimes.length} rapid crashes within ${windowMs}ms), giving up — exiting 1. Last exit: code=${code}, signal=${signal}.`,
|
|
157
196
|
)
|
|
158
197
|
this._proc.exit(1)
|
|
159
198
|
return // 실 process.exit 은 즉시 종료하나, 주입된 fake 는 계속 실행되므로 명시적 중단
|
|
160
199
|
}
|
|
161
200
|
}
|
|
162
|
-
|
|
163
|
-
|
|
201
|
+
this._log(
|
|
202
|
+
'warn',
|
|
203
|
+
`worker ${worker.process.pid} died (code=${code}, signal=${signal}, lifetime=${lifetimeMs}ms), respawning (rapid-crashes=${this._rapidCrashTimes.length}/${this._maxRapidRespawn} in ${windowMs}ms)`,
|
|
164
204
|
)
|
|
165
205
|
this._forkWorker()
|
|
166
206
|
} else {
|
|
167
|
-
|
|
207
|
+
this._log('warn', `worker ${worker.process.pid} died (code=${code}, signal=${signal}), respawn disabled`)
|
|
168
208
|
}
|
|
169
209
|
})
|
|
170
210
|
}
|
|
@@ -176,7 +216,7 @@ export class MegaCluster {
|
|
|
176
216
|
w.send({ type: SHUTDOWN_MSG })
|
|
177
217
|
} catch (err) {
|
|
178
218
|
// 워커가 이미 disconnected — 무시 + 로그 (silent 금지)
|
|
179
|
-
|
|
219
|
+
this._log('warn', `failed to send shutdown to worker ${w.process.pid}: ${err.message}`)
|
|
180
220
|
}
|
|
181
221
|
}
|
|
182
222
|
}
|
|
@@ -199,8 +239,9 @@ export class MegaCluster {
|
|
|
199
239
|
// 핸들러가 있었으면 true, 없으면 false → 무핸들러 경고.
|
|
200
240
|
const hadListener = this._proc.emit('SIGTERM')
|
|
201
241
|
if (!hadListener) {
|
|
202
|
-
|
|
203
|
-
|
|
242
|
+
this._log(
|
|
243
|
+
'warn',
|
|
244
|
+
`worker ${this._proc.pid} has no SIGTERM handler registered. Will be force-killed by master after grace period. ` +
|
|
204
245
|
`Register a SIGTERM handler (or use MegaShutdown.setupSignals) to enable graceful shutdown.`,
|
|
205
246
|
)
|
|
206
247
|
}
|
|
@@ -211,7 +252,7 @@ export class MegaCluster {
|
|
|
211
252
|
try {
|
|
212
253
|
await workerFn()
|
|
213
254
|
} catch (err) {
|
|
214
|
-
|
|
255
|
+
this._log('error', `worker ${this._proc.pid} workerFn failed: ${/** @type {any} */ (err)?.stack ?? err}`)
|
|
215
256
|
this._proc.exit(1)
|
|
216
257
|
}
|
|
217
258
|
}
|
package/src/core/mega-server.js
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
import { createServer as createHttpServer } from 'node:http'
|
|
3
3
|
import { MegaConfigError } from '../errors/config-error.js'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Host 헤더에서 hostname 만 추출한다 (소문자 정규화). IPv6 리터럴(`[::1]:3000`)은 hostname 자체가
|
|
7
|
+
* 콜론을 포함하므로 단순 `split(':')` 으로는 `'['` 가 나와 vhost 매칭이 전부 깨진다 — 대괄호 형식이면
|
|
8
|
+
* 괄호 안(`::1`)을, 아니면 첫 콜론 앞(포트 제거)을 취한다.
|
|
9
|
+
* @param {unknown} hostHeader - `req.headers.host` 원본.
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function hostnameOf(hostHeader) {
|
|
13
|
+
const h = String(hostHeader || '')
|
|
14
|
+
if (h.startsWith('[')) {
|
|
15
|
+
const end = h.indexOf(']')
|
|
16
|
+
return (end === -1 ? h.slice(1) : h.slice(1, end)).toLowerCase()
|
|
17
|
+
}
|
|
18
|
+
return h.split(':')[0].toLowerCase()
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* MegaServer — 여러 MegaApp 을 하나의 HTTP 서버에서 호스트네임 기반으로 라우팅.
|
|
7
23
|
*
|
|
@@ -17,7 +33,9 @@ import { MegaConfigError } from '../errors/config-error.js'
|
|
|
17
33
|
*/
|
|
18
34
|
export class MegaServer {
|
|
19
35
|
/**
|
|
20
|
-
* @param {{ port?: number, host?: string }} [opts] -
|
|
36
|
+
* @param {{ port?: number, host?: string, logger?: { debug?: Function, warn?: Function } }} [opts] -
|
|
37
|
+
* listen 기본값 (listen() 인자로 override 가능). `logger` 는 pino 호환 로거(선택) — 미매핑 host
|
|
38
|
+
* 404·upgrade 소켓 에러 같은 비치명 이벤트를 구조적으로 남긴다(미주입 시 console 폴백).
|
|
21
39
|
*/
|
|
22
40
|
constructor(opts = {}) {
|
|
23
41
|
/** @type {Map<string, import('./mega-app.js').MegaApp>} */
|
|
@@ -26,6 +44,8 @@ export class MegaServer {
|
|
|
26
44
|
this._apps = []
|
|
27
45
|
/** @type {import('node:http').Server | null} */
|
|
28
46
|
this._httpServer = null
|
|
47
|
+
/** @type {{ debug?: Function, warn?: Function } | null} */
|
|
48
|
+
this._log = opts.logger ?? null
|
|
29
49
|
this._opts = opts
|
|
30
50
|
}
|
|
31
51
|
|
|
@@ -73,6 +93,10 @@ export class MegaServer {
|
|
|
73
93
|
if (this._apps.length === 0) {
|
|
74
94
|
throw new Error('MegaServer.listen: no MegaApps mounted')
|
|
75
95
|
}
|
|
96
|
+
// 재호출 가드 — 호출마다 새 http.Server 를 만들면 기존 서버(리스너·소켓)가 참조만 잃고 누수된다.
|
|
97
|
+
if (this._httpServer) {
|
|
98
|
+
throw new Error('MegaServer.listen: already listening — call close() first.')
|
|
99
|
+
}
|
|
76
100
|
|
|
77
101
|
// 모든 Fastify 인스턴스 ready (라우트 등록 완료 보장)
|
|
78
102
|
for (const app of this._apps) {
|
|
@@ -80,10 +104,12 @@ export class MegaServer {
|
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
this._httpServer = createHttpServer((req, res) => {
|
|
83
|
-
const
|
|
84
|
-
const hostname = hostHeader.split(':')[0].toLowerCase()
|
|
107
|
+
const hostname = hostnameOf(req.headers.host)
|
|
85
108
|
const app = this._hostMap.get(hostname)
|
|
86
109
|
if (!app) {
|
|
110
|
+
// 등록 host 목록은 응답에 싣지 않는다 — 임의 클라이언트에게 내부 도메인 구성이 노출된다.
|
|
111
|
+
// 운영자 디버그용 목록은 debug 로그로만 남긴다.
|
|
112
|
+
this._log?.debug?.({ hostname, knownHosts: [...this._hostMap.keys()] }, 'host.not_mounted (404)')
|
|
87
113
|
res.statusCode = 404
|
|
88
114
|
res.setHeader('content-type', 'application/json')
|
|
89
115
|
res.end(
|
|
@@ -91,7 +117,7 @@ export class MegaServer {
|
|
|
91
117
|
ok: false,
|
|
92
118
|
error: {
|
|
93
119
|
code: 'host.not_mounted',
|
|
94
|
-
message: `No app mounted for host '${hostname}'
|
|
120
|
+
message: `No app mounted for host '${hostname}'.`,
|
|
95
121
|
},
|
|
96
122
|
}),
|
|
97
123
|
)
|
|
@@ -106,16 +132,16 @@ export class MegaServer {
|
|
|
106
132
|
// WS HTTP Upgrade 핸들오프 — 호스트 분기 후 해당 MegaApp 에 위임.
|
|
107
133
|
// Fastify 는 (websocket 플러그인 없으면) 'upgrade' 를 듣지 않으므로 여기서 직접 배선한다.
|
|
108
134
|
this._httpServer.on('upgrade', (req, socket, head) => {
|
|
109
|
-
const
|
|
110
|
-
const hostname = hostHeader.split(':')[0].toLowerCase()
|
|
135
|
+
const hostname = hostnameOf(req.headers.host)
|
|
111
136
|
const app = this._hostMap.get(hostname)
|
|
112
137
|
if (!app) {
|
|
113
138
|
// 매핑 안 된 host 의 upgrade 는 거부 (소켓 파괴). HTTP 404 와 동일 정책.
|
|
114
139
|
// L4: destroy 전 error 가드. listener 없는 raw 소켓이 ECONNRESET 등으로 uncaught
|
|
115
|
-
// exception → 프로세스 크래시 하는 것을 막는다 (H1 과 동일 패턴).
|
|
116
|
-
//
|
|
140
|
+
// exception → 프로세스 크래시 하는 것을 막는다 (H1 과 동일 패턴). 비치명적 소켓 에러는
|
|
141
|
+
// 주입 로거(warn)로, 미주입이면 console.warn 폴백으로 명시 로그.
|
|
117
142
|
socket.on('error', (err) => {
|
|
118
|
-
|
|
143
|
+
if (this._log?.warn) this._log.warn({ err, hostname }, 'raw socket error on unmapped-host upgrade')
|
|
144
|
+
else console.warn('[MegaServer] raw socket error on unmapped-host upgrade:', err?.message ?? err)
|
|
119
145
|
})
|
|
120
146
|
socket.destroy()
|
|
121
147
|
return
|
|
@@ -134,6 +160,9 @@ export class MegaServer {
|
|
|
134
160
|
// 다른 쪽 리스너를 제거해 누수를 막는다.
|
|
135
161
|
const onError = (/** @type {Error} */ err) => {
|
|
136
162
|
httpServer.removeListener('listening', onListening)
|
|
163
|
+
// listen 실패(EADDRINUSE 등)한 서버는 참조를 비운다 — 남기면 재호출 가드가 "이미 listening"
|
|
164
|
+
// 으로 오인해 같은 인스턴스의 재시도(포트 바꿔 listen)가 막힌다.
|
|
165
|
+
this._httpServer = null
|
|
137
166
|
reject(err)
|
|
138
167
|
}
|
|
139
168
|
const onListening = () => {
|
|
@@ -167,6 +196,8 @@ export class MegaServer {
|
|
|
167
196
|
this._httpServer.close((err) => (err ? reject(err) : resolve(undefined)))
|
|
168
197
|
})
|
|
169
198
|
}
|
|
199
|
+
// 참조 해제 — listen() 재호출 가드가 "이미 listening" 과 "close 후 재기동" 을 구분할 수 있게 한다.
|
|
200
|
+
this._httpServer = null
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
/** 등록된 호스트 목록 (디버그·테스트용). */
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Dialect 레지스트리 (ADR-204/205) — adapter driver 이름 → SQL 렌더 dialect 매핑 + **contract 검증**.
|
|
4
|
+
*
|
|
5
|
+
* dialect 는 `dialects/README.md` 의 표준 인터페이스(함수 12 + 속성 7)를 전부 구현해야 한다 —
|
|
6
|
+
* differ/generate 는 이 표면만 호출하므로, M-6(maria/sqlite) 확장은 dialect 모듈 추가만으로 동작한다.
|
|
7
|
+
* 미구현 멤버는 등록 조회 시점에 fail-fast(`migration.dialect_contract`) — 런타임 한가운데서
|
|
8
|
+
* undefined 호출로 죽는 것을 차단.
|
|
9
|
+
*
|
|
10
|
+
* 미지원 driver 는 **명시 fail-fast** —
|
|
11
|
+
* 조용히 건너뛰면 사용자가 "스키마를 선언했는데 마이그레이션이 안 나온다" 를 모른다(P7).
|
|
12
|
+
*
|
|
13
|
+
* @module core/migration/dialect-registry
|
|
14
|
+
*/
|
|
15
|
+
import { MegaConfigError } from '../../errors/config-error.js'
|
|
16
|
+
import * as postgres from './dialects/postgres.js'
|
|
17
|
+
import * as maria from './dialects/maria.js'
|
|
18
|
+
import * as sqlite from './dialects/sqlite.js'
|
|
19
|
+
import * as mongo from './dialects/mongo.js'
|
|
20
|
+
|
|
21
|
+
/** 표준 인터페이스 — 함수 멤버 (dialects/README.md 정본). */
|
|
22
|
+
const CONTRACT_FNS = [
|
|
23
|
+
'renderOps', 'renderOp',
|
|
24
|
+
'quoteIdent', 'quoteLiteral', 'enumCheckExpr',
|
|
25
|
+
'indexName', 'resolveIndexName', 'uniqueName', 'fkName', 'checkName', 'pkName', 'inlinePkName',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** 표준 인터페이스 — 능력/한계 속성 멤버 (boolean | number | string). */
|
|
29
|
+
const CONTRACT_PROPS = [
|
|
30
|
+
'identifierMaxBytes',
|
|
31
|
+
'supportsConcurrentIndex',
|
|
32
|
+
'enumStrategy',
|
|
33
|
+
'supportsRenameInTx',
|
|
34
|
+
'canDropColumnInTx',
|
|
35
|
+
'dependsOnRebuild',
|
|
36
|
+
'supportsRenameConstraint',
|
|
37
|
+
'supportsAlterAddFk',
|
|
38
|
+
'requiresDropFkBeforeDropColumn',
|
|
39
|
+
'usesSqlDdl',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/** @type {Record<string, Record<string, any>>} driver → dialect. */
|
|
43
|
+
const DIALECTS = {
|
|
44
|
+
postgres,
|
|
45
|
+
mariadb: maria,
|
|
46
|
+
sqlite,
|
|
47
|
+
mongodb: mongo,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 자동 생성을 지원하는 driver 목록. */
|
|
51
|
+
export const SUPPORTED_DRIVERS = Object.keys(DIALECTS)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* dialect 의 contract 충족 검증 — 미구현 멤버 목록을 모아 한 번에 보고.
|
|
55
|
+
* @param {string} driver @param {Record<string, any>} dialect
|
|
56
|
+
* @throws {MegaConfigError} `migration.dialect_contract`
|
|
57
|
+
*/
|
|
58
|
+
export function assertDialectContract(driver, dialect) {
|
|
59
|
+
const missing = [
|
|
60
|
+
...CONTRACT_FNS.filter((f) => typeof dialect[f] !== 'function').map((f) => `${f}()`),
|
|
61
|
+
...CONTRACT_PROPS.filter((p) => dialect[p] === undefined),
|
|
62
|
+
]
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
throw new MegaConfigError(
|
|
65
|
+
'migration.dialect_contract',
|
|
66
|
+
`dialect '${driver}' 가 표준 인터페이스를 충족하지 않습니다 — 누락: [${missing.join(', ')}] (dialects/README.md 참조).`,
|
|
67
|
+
{ details: { driver, missing } },
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* driver 의 dialect 조회(+contract 검증).
|
|
74
|
+
* @param {string} driver - services.databases.<key>.driver 값.
|
|
75
|
+
* @returns {Record<string, any>}
|
|
76
|
+
* @throws {MegaConfigError} `migration.dialect_unsupported` | `migration.dialect_contract`
|
|
77
|
+
*/
|
|
78
|
+
export function getDialect(driver) {
|
|
79
|
+
const dialect = DIALECTS[driver]
|
|
80
|
+
if (dialect === undefined) {
|
|
81
|
+
throw new MegaConfigError(
|
|
82
|
+
'migration.dialect_unsupported',
|
|
83
|
+
`driver '${driver}' 는 마이그레이션 자동 생성을 아직 지원하지 않습니다 — 지원: [${SUPPORTED_DRIVERS.join(', ')}]. ` +
|
|
84
|
+
'해당 adapter 의 변경은 raw SQL 마이그레이션(mega generate migration)으로 작성하세요.',
|
|
85
|
+
{ details: { driver, supported: SUPPORTED_DRIVERS } },
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
assertDialectContract(driver, dialect)
|
|
89
|
+
return dialect
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** DML facet(ADR-212) — CRUD(model-crud)가 쓰는 dialect 보조 멤버. DDL 계약과 별개. */
|
|
93
|
+
const DML_CONTRACT_FNS = ['placeholder', 'parseReadResult', 'parseWriteResult', 'upsertClause']
|
|
94
|
+
const DML_CONTRACT_PROPS = ['paramStyle', 'supportsReturning', 'supportsBulkReturning']
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* dialect 가 DML facet 을 충족하는지 — **누락 멤버 배열**(빈 배열=충족)을 반환한다(throw 아님 — 호출부
|
|
98
|
+
* model-crud 가 `model.*` 에러로 표면화). mongo 등 DML facet 미구현 dialect 는 비어있지 않은 배열을 돌려준다.
|
|
99
|
+
* @param {Record<string, any>} dialect
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
export function dmlFacetMissing(dialect) {
|
|
103
|
+
return [
|
|
104
|
+
...DML_CONTRACT_FNS.filter((f) => typeof dialect?.[f] !== 'function').map((f) => `${f}()`),
|
|
105
|
+
...DML_CONTRACT_PROPS.filter((p) => dialect?.[p] === undefined),
|
|
106
|
+
]
|
|
107
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Migration Dialect Contract (ADR-205)
|
|
2
|
+
|
|
3
|
+
`mega migrate:generate` 의 diff 엔진(`differ.js`)과 호스트(`generate.js`)는 **이 인터페이스만**
|
|
4
|
+
호출한다. 새 dialect(maria/sqlite — M-6, mongodb — M-5)는 본 contract 를 구현한 모듈을
|
|
5
|
+
`dialect-registry.js` 의 `DIALECTS` 에 추가하는 것만으로 동작해야 한다. 누락 멤버는 `getDialect()` 가
|
|
6
|
+
`migration.dialect_contract` 로 fail-fast 한다. mongodb 는 SQL 이 아니라 **mongo command JS 문**을
|
|
7
|
+
렌더한다(`usesSqlDdl: false` — generate 가 파일 템플릿을 분기, ADR-209).
|
|
8
|
+
|
|
9
|
+
## 함수 (12)
|
|
10
|
+
|
|
11
|
+
| 멤버 | 의미 | postgres 구현 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `renderOps(ops)` | op 목록 → `{ up: string[], down: string[] }` (down 은 역순) | `dialects/postgres.js` |
|
|
14
|
+
| `renderOp(op)` | op 1개 → up/down 쌍. op 종류는 differ 가 정본(19종 + renameFk) | 〃 |
|
|
15
|
+
| `quoteIdent(name)` | 식별자 인용 깔때기 — **`identifierMaxBytes` 초과 fail-fast**(`migration.identifier_too_long`) 포함 | `"x"` (필요 시만) |
|
|
16
|
+
| `quoteLiteral(value)` | 값 literal 인용(injection 방지 깔때기). NaN/Infinity 거부 | `'..'` escape |
|
|
17
|
+
| `enumCheckExpr(col, values)` | enum 의 CHECK 식 — **유일한 enum 렌더 지점**(differ 는 values 만 전달) | `col IN ('a','b')` |
|
|
18
|
+
| `indexName(table, cols, unique?)` | 인덱스 명명 표준 | `idx_`/`uniq_` |
|
|
19
|
+
| `resolveIndexName(table, ix)` | IndexDef 의 유효 이름(명시 name 우선, 표현식은 name 필수) | 〃 |
|
|
20
|
+
| `uniqueName(table, col)` | UNIQUE 제약 명명 | `uniq_<t>_<c>` |
|
|
21
|
+
| `fkName(table, col, refTable)` | FK 제약 명명 | `fk_<t>_<c>_<r>` |
|
|
22
|
+
| `checkName(table, col)` | CHECK 제약 명명 | `chk_<t>_<c>` |
|
|
23
|
+
| `pkName(table)` | 명시(복합·후행) PK 명명 | `pk_<t>` |
|
|
24
|
+
| `inlinePkName(table)` | CREATE TABLE 인라인 단일 PK 의 **서버 자동 명명** | `<t>_pkey` |
|
|
25
|
+
|
|
26
|
+
명명 함수들은 diff 의 식별자이기도 하다 — 이름이 흔들리면 변경이 drop+add 로 오판되므로
|
|
27
|
+
**dialect 간에도 같은 표준**(`idx_/uniq_/fk_/chk_/pk_`)을 권장한다. 서버 자동 명명만
|
|
28
|
+
dialect 별로 다르다(`inlinePkName`).
|
|
29
|
+
|
|
30
|
+
## 능력/한계 속성 (10)
|
|
31
|
+
|
|
32
|
+
| 멤버 | 타입 | postgres | maria | sqlite | mongodb |
|
|
33
|
+
|---|---|---|---|---|---|
|
|
34
|
+
| `identifierMaxBytes` | number | 63 (byte) | 64 (**자** 단위 — utf8 멀티바이트도 64자, ADR-208 L-3) | `Infinity` | 64 (namespace 한도의 보수적 적용) |
|
|
35
|
+
| `supportsConcurrentIndex` | boolean | true (`CONCURRENTLY`, no-tx 필요) | true (`ALGORITHM=INPLACE, LOCK=NONE`) | false | true (4.2+ 항상 online — 표기일 뿐) |
|
|
36
|
+
| `enumStrategy` | string | `'check'` | `'enum-type'` (값 변경 = MODIFY COLUMN) | `'check'` | `'jsonschema-enum'` ($jsonSchema enum 키워드) |
|
|
37
|
+
| `supportsRenameInTx` | boolean | true | false (DDL 암묵 commit) | true (3.25+) | false (renameCollection 은 tx 불가) |
|
|
38
|
+
| `canDropColumnInTx` | boolean | true | false (DDL 암묵 commit) | true — 단 본 dialect 는 rebuild 사용 | false (DDL 자체가 tx 밖) |
|
|
39
|
+
| `dependsOnRebuild` | boolean | false | false | **true** — 트리거 op 는 rebuildTable 로 수렴 | **true** — validator 통짜 교체(collMod) 수렴 |
|
|
40
|
+
| `supportsRenameConstraint` | boolean | true (9.2+) | **false** (11.8 실측 — 구문 에러) → DROP+ADD | false (FK 인라인 — 동기화 불필요) | false (constraint 개념 없음) |
|
|
41
|
+
| `supportsAlterAddFk` | boolean | true | true (`ADD/DROP CONSTRAINT` 실측) | **false** — FK 는 CREATE TABLE 인라인, 변경은 rebuild | false (FK 없음 — `.references()` 는 명시 거부) |
|
|
42
|
+
| `requiresDropFkBeforeDropColumn` | boolean | false (DROP COLUMN 이 FK 동반 제거) | **true** — InnoDB 가 FK 인덱스 겸용 컬럼의 단독 DROP 을 거부(1553), differ 가 dropFk 선행 산출(ADR-208 H-1) | false (rebuild 경로) | false |
|
|
43
|
+
| `usesSqlDdl` | boolean | true | true | true | **false** — mongo command JS 문 렌더(파일의 `db` = mongodb `Db`, ADR-209) |
|
|
44
|
+
|
|
45
|
+
선택 멤버: `rebuildTriggerKinds`(Set) — `dependsOnRebuild` dialect 가 수렴 트리거 종류를 확장한다
|
|
46
|
+
(mongodb: addColumn/renameColumn/setComment 포함 — validator 가 통짜라 모든 컬럼 수준 변경이 수렴.
|
|
47
|
+
미지정 시 differ 의 sqlite 기본 집합).
|
|
48
|
+
|
|
49
|
+
## 구현 규칙
|
|
50
|
+
|
|
51
|
+
- **식별자/값은 반드시 `quoteIdent`/`quoteLiteral` 를 거친다** — differ 등 상위 계층은 SQL 조각을
|
|
52
|
+
직접 합성하지 않는다(H-1 재발 방지).
|
|
53
|
+
- 파괴적 변경(DROP TABLE/COLUMN)·위험 캐스트는 SQL 안에 `-- 경고`/`-- TODO` 주석을 동반한다
|
|
54
|
+
(silent 손실 금지 — 사용자가 적용 전 검토).
|
|
55
|
+
- `supportsRenameConstraint === false` 인 dialect 의 FK 이름 동기화는 differ 가 `dropFk + addFk` 로 산출한다.
|
|
56
|
+
- `dependsOnRebuild === true` 면 differ 가 ALTER 로 표현 못하는 테이블 변경 전체를 `rebuildTable`
|
|
57
|
+
op(`{ from, to, renames }`) 1개로 수렴한다 — sqlite 는 12-step 재생성을, mongodb 는 validator
|
|
58
|
+
교체(collMod 우선 → `$rename` — strict 검증이 update 결과를 현재 validator 로 평가하므로 순서가
|
|
59
|
+
정본, ADR-209)를 렌더한다. generate 는 rebuildTable 포함 파일(sqlite)·mongodb 파일 전체에
|
|
60
|
+
`export const transaction = false` 를 설정한다.
|
|
61
|
+
- op 의 형태(필드)는 `differ.js` 의 산출이 정본 — dialect 는 렌더만 한다. setNotNull/dropNotNull/
|
|
62
|
+
setDefault/dropDefault/setComment 는 대상 `def`(+`prevDef`)를 동반한다(maria MODIFY COLUMN 용).
|