mega-framework 0.1.1 → 0.1.2
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/.env +1 -1
- package/package.json +2 -2
- package/sample/crud/.env +10 -1
- package/sample/crud/apps/main/app.config.js +12 -0
- package/sample/crud/mega.config.js +12 -8
- package/sample/crud/scripts/start-ws-hub.sh +40 -0
- package/sample/crud/yarn.lock +1 -1
- package/sample/simple/package.json +1 -1
- package/src/core/boot.js +25 -13
- package/src/core/config-validator.js +24 -0
package/.env
CHANGED
|
@@ -92,7 +92,7 @@ NATS_JOBS_URL=nats://localhost:4222
|
|
|
92
92
|
# WS Hub (mega ws-hub CLI — src/cli/ws-hub.js, runWsHubCli)
|
|
93
93
|
# 코드가 읽는 키는 MEGA_WSHUB_* (언더스코어 없는 단일 service 토큰). TOKENS 만 필수.
|
|
94
94
|
# 비밀 토큰은 운영에서 교체. hub 는 `mega ws-hub` 실행 시에만 기동(자동 기동 없음).
|
|
95
|
-
MEGA_WSHUB_TOKENS=change-me
|
|
95
|
+
MEGA_WSHUB_TOKENS=dev-bridge-token-change-me
|
|
96
96
|
MEGA_WSHUB_PORT=3100
|
|
97
97
|
MEGA_WSHUB_HOST=0.0.0.0
|
|
98
98
|
MEGA_WSHUB_HEARTBEAT_MS=25000
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mega-framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Node.js 마이크로프레임웍 + CLI — Slim의 가벼움 + Rails의 관례",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -118,4 +118,4 @@
|
|
|
118
118
|
"redlock": "5.0.0-beta.2",
|
|
119
119
|
"ws": "^8.21.0"
|
|
120
120
|
}
|
|
121
|
-
}
|
|
121
|
+
}
|
package/sample/crud/.env
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
PORT=
|
|
1
|
+
PORT=3000
|
|
2
2
|
DATABASE_URL=postgres://mega:dkTkqkfl12@localhost:5432/mega_test
|
|
3
3
|
MEGA_CLUSTER_WORKERS=8
|
|
4
4
|
SESSION_SECRET=Zz4VoSzf0sYMEoqASu8G_wx5l3uKi2MlHsxDK3MSkoE
|
|
@@ -9,6 +9,13 @@ REDIS_LOCK_URL=redis://:dkTkqkfl12@localhost:6379/3
|
|
|
9
9
|
MONGO_URL=mongodb://mega:dkTkqkfl12@localhost:27017/mega_test?authSource=admin
|
|
10
10
|
NATS_JOBS_URL=nats://localhost:4222
|
|
11
11
|
ASP_MASTER_SECRET=demo-asp-master-7Qe2mWzR1tYbN8sLpKvX0cAfH4dG6jU
|
|
12
|
+
|
|
13
|
+
MEGA_WSHUB_TOKENS=dev-bridge-token-change-me
|
|
14
|
+
MEGA_WSHUB_TOKEN=dev-bridge-token-change-me
|
|
15
|
+
MEGA_WSHUB_PORT=3100
|
|
16
|
+
MEGA_WSHUB_HOST=0.0.0.0
|
|
17
|
+
MEGA_WSHUB_URL=ws://localhost:3100
|
|
18
|
+
|
|
12
19
|
MEGA_OTEL_ENABLED=true
|
|
13
20
|
MEGA_OTEL_SERVICE_NAME=sample-crud
|
|
14
21
|
MEGA_OTEL_ENDPOINT=http://localhost:4318/v1/traces
|
|
@@ -16,3 +23,5 @@ MEGA_OTEL_EXPORTER=otlp
|
|
|
16
23
|
MEGA_OTEL_SAMPLING_RATIO=1.0
|
|
17
24
|
MEGA_OTEL_ZIPKIN_API=http://localhost:9411/api/v2
|
|
18
25
|
DEMO_UPLOAD_DIR=var/uploads
|
|
26
|
+
|
|
27
|
+
BRIDGE_ID=crud-1
|
|
@@ -72,6 +72,18 @@ export default {
|
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
|
|
75
|
+
// WS Hub 브릿지(ADR-065/176) — 별도 `mega ws-hub` 서버(localhost:3100, .env MEGA_WSHUB_*)에 연결.
|
|
76
|
+
// boot 가 이 블록을 보고 **app.connectHub 를 자동 호출**한다(ADR-176 자동배선 — connectHub 코드 불요).
|
|
77
|
+
// 채팅(/ws/chat)은 app.broadcast → hub fan-out 으로 클러스터 전파된다. retry 로 허브 재시작·drain(4503)·
|
|
78
|
+
// 네트워크 단절 시 지수 백오프 재연결(ADR-098). ⚠️ global wsCluster(NATS)와 **동시 사용 불가**(부팅 fail-fast).
|
|
79
|
+
bridgeHub: {
|
|
80
|
+
url: process.env.MEGA_WSHUB_URL ?? 'ws://localhost:3100',
|
|
81
|
+
token: process.env.MEGA_WSHUB_TOKEN,
|
|
82
|
+
bridgeId: process.env.BRIDGE_ID ?? 'main-1',
|
|
83
|
+
channels: ['chat'],
|
|
84
|
+
retry: { retries: 30, minTimeout: 1000, maxTimeout: 10_000 },
|
|
85
|
+
},
|
|
86
|
+
|
|
75
87
|
// rate limit 상향(ADR-073) — /demo/worker 데모는 1초 간격 하트비트 ping(메인 스레드 non-block 시연)으로
|
|
76
88
|
// 폴링한다. 기본 한도(100/분)는 폴링 + 일반 탐색이 겹치면 쉽게 넘어 데모가 429 로 끊긴다. 데모 앱이라
|
|
77
89
|
// 폴링을 허용하도록 한도를 넉넉히 둔다(여전히 ON — 무제한 아님). 운영 앱은 엔드포인트별로 더 낮게 잡는다.
|
|
@@ -79,14 +79,18 @@ export default {
|
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
// app.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
wsCluster
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// ── 클러스터 전송 선택(ADR-176, 앱당 하나·상호배타) ───────────────────────────────────────────
|
|
83
|
+
// 이 샘플은 현재 **WS Hub**(app.config 의 bridgeHub → `mega ws-hub` 서버, localhost:3100)로 채팅을
|
|
84
|
+
// 클러스터 전파한다. boot 가 bridgeHub 를 보고 app.connectHub 를 자동 호출한다(개발자 배선 불요).
|
|
85
|
+
//
|
|
86
|
+
// ⚠️ NATS 로 다시 전환하려면: app.config 의 `bridgeHub` 블록을 제거(또는 주석)하고 아래 wsCluster 를
|
|
87
|
+
// 되살린다. **둘을 동시에 두면 부팅 시 config.cluster_transport_conflict 로 fail-fast**(이중 전파 방지).
|
|
88
|
+
//
|
|
89
|
+
// // NATS wsCluster (대안 — bridgeHub 와 동시 사용 불가):
|
|
90
|
+
// wsCluster: {
|
|
91
|
+
// bus: 'jobs', // services.buses 의 NATS 키 재사용
|
|
92
|
+
// roster: { driver: 'nats', ttlMs: 15_000 }, // 접속자목록도 NATS 동기화(crash 정리 heartbeat). 'none'=로컬만
|
|
93
|
+
// },
|
|
90
94
|
|
|
91
95
|
// 정기 작업(ADR-028/118) — `mega scheduler` 프로세스가 등록·실행한다(config.schedules, ADR-123).
|
|
92
96
|
schedules: [CronCounterSchedule],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# sample/crud — WS Hub 서버 기동 스크립트 (ADR-032/176)
|
|
4
|
+
#
|
|
5
|
+
# 별도 hub 프로세스(`mega-ws-hub` 바이너리, mega-framework bin)를 localhost:3100 에 띄운다.
|
|
6
|
+
# 앱(`yarn dev` = `mega start`)이 app.config 의 `bridgeHub` 로 이 허브에 **자동 연결**한다(ADR-176).
|
|
7
|
+
#
|
|
8
|
+
# ⚠️ `mega-ws-hub` 는 `mega start` 와 달리 `.env` 를 자동 로드하지 않는다(직접 process.env 만 읽음,
|
|
9
|
+
# src/cli/ws-hub.js). 그래서 Node 20.6+ 의 `--env-file` 로 .env 를 주입한다.
|
|
10
|
+
# 읽는 env(src/cli/ws-hub.js runWsHubCli): MEGA_WSHUB_TOKENS(필수, 콤마구분) /
|
|
11
|
+
# MEGA_WSHUB_PORT(기본 3100) / MEGA_WSHUB_HOST(기본 0.0.0.0) / MEGA_WSHUB_HEARTBEAT_MS.
|
|
12
|
+
#
|
|
13
|
+
# 사용:
|
|
14
|
+
# sample/crud/scripts/start-ws-hub.sh # .env 값으로 기동(localhost:3100)
|
|
15
|
+
# MEGA_WSHUB_PORT=4100 scripts/start-ws-hub.sh # 일부 오버라이드(실 env 가 .env 보다 우선)
|
|
16
|
+
# scripts/start-ws-hub.sh --some-extra-flag # 추가 인자는 mega-ws-hub 로 전달
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
# sample/crud 루트로 이동(스크립트 위치 기준 — 어디서 호출해도 동작).
|
|
21
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
22
|
+
cd "$ROOT"
|
|
23
|
+
|
|
24
|
+
ENV_FILE=".env"
|
|
25
|
+
BIN="node_modules/mega-framework/bin/mega-ws-hub.js"
|
|
26
|
+
|
|
27
|
+
# 사전 점검 — 누락 시 silent 진행 금지, 이유를 명확히 알린다(P4/P7).
|
|
28
|
+
[ -f "$ENV_FILE" ] || {
|
|
29
|
+
echo "✗ $ROOT/$ENV_FILE 가 없습니다 — MEGA_WSHUB_TOKENS 등 허브 설정이 필요합니다." >&2
|
|
30
|
+
exit 1
|
|
31
|
+
}
|
|
32
|
+
[ -f "$BIN" ] || {
|
|
33
|
+
echo "✗ $BIN 없음 — 먼저 'yarn install --ignore-engines'(또는 npm install) 로 mega-framework 를 설치/동기화하세요." >&2
|
|
34
|
+
exit 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
echo "▶ WS Hub 기동 (mega-ws-hub) — host=${MEGA_WSHUB_HOST:-(.env)} port=${MEGA_WSHUB_PORT:-(.env, 기본 3100)}. Ctrl+C 로 종료."
|
|
38
|
+
# --env-file: .env 를 process.env 로 로드(이미 export 된 실 env 가 우선 — --env-file 표준 동작).
|
|
39
|
+
# exec 로 교체 실행 → 시그널(SIGINT/SIGTERM)이 mega-ws-hub 로 그대로 전달되어 graceful 종료(drain 4503).
|
|
40
|
+
exec node --env-file="$ENV_FILE" "$BIN" "$@"
|
package/sample/crud/yarn.lock
CHANGED
|
@@ -1234,7 +1234,7 @@ marked@^18.0.5:
|
|
|
1234
1234
|
integrity sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==
|
|
1235
1235
|
|
|
1236
1236
|
"mega-framework@file:../..":
|
|
1237
|
-
version "0.1.
|
|
1237
|
+
version "0.1.1"
|
|
1238
1238
|
dependencies:
|
|
1239
1239
|
"@fastify/cookie" "^11.0.2"
|
|
1240
1240
|
"@fastify/cors" "^11.2.0"
|
package/src/core/boot.js
CHANGED
|
@@ -294,17 +294,30 @@ export async function bootApp(projectRoot, { listen = true, port, host: listenHo
|
|
|
294
294
|
megaApps.push(app)
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
//
|
|
298
|
-
// app.
|
|
299
|
-
//
|
|
300
|
-
//
|
|
297
|
+
// 클러스터 전송 자동배선 (ADR-176) — 앱당 **하나**(상호배타, config-validator 가 충돌 fail-fast). config 로 선택:
|
|
298
|
+
// - app.config `bridgeHub` → **WS Hub**(`app.connectHub`, 평문 12-타입). `mega ws-hub` 서버에 자동 연결.
|
|
299
|
+
// - global `wsCluster.bus`(NATS) → **MegaWsCluster**(NATS pub/sub broadcast + roster 동기화).
|
|
300
|
+
// 둘 다 개발자가 connectHub 같은 코드를 쓰지 않고 config 만으로 동작한다(ADR-176 이 ADR-137 의 자동배선
|
|
301
|
+
// 거부를 **명시 config 가 있을 때만** 번복 — 설정이 곧 의도라 "추측 배선" 아님). megaApps[i] 와 apps[i] 동순서.
|
|
301
302
|
const wsClusterCfg = /** @type {any} */ (global).wsCluster
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
const wsClusterBus = wsClusterCfg?.bus ? getAdapter('bus', wsClusterCfg.bus) : null
|
|
304
|
+
for (let i = 0; i < megaApps.length; i++) {
|
|
305
|
+
const app = megaApps[i]
|
|
306
|
+
const a = /** @type {any} */ (app) // _deliverBroadcast/_deliverDirect 는 framework-internal(@private).
|
|
307
|
+
const bridgeHub = /** @type {any} */ (apps[i].config).bridgeHub
|
|
308
|
+
if (bridgeHub?.url) {
|
|
309
|
+
// WS Hub 자동연결(ADR-065/176). connectHub 가 _hubLink 를 세워 app.broadcast/joinSession 의 hub 경로를
|
|
310
|
+
// 활성화한다(shutdown hook 도 connectHub 가 등록). 허브 다운이어도 boot 를 막지 않는다 — 초기 연결
|
|
311
|
+
// 실패는 warn 하고(retry 설정 시 백그라운드 재연결, ADR-098) 앱은 계속 뜬다(로컬 전달은 동작).
|
|
312
|
+
try {
|
|
313
|
+
await app.connectHub(bridgeHub)
|
|
314
|
+
logger?.debug?.({ app: app.name, url: bridgeHub.url }, 'boot.bridgeHub connected (ADR-176)')
|
|
315
|
+
} catch (err) {
|
|
316
|
+
logger?.warn?.({ err, app: app.name, url: bridgeHub.url }, 'boot.bridgeHub initial connect failed (retry if configured)')
|
|
317
|
+
}
|
|
318
|
+
} else if (wsClusterBus) {
|
|
306
319
|
const cluster = new MegaWsCluster({
|
|
307
|
-
bus: /** @type {any} */ (
|
|
320
|
+
bus: /** @type {any} */ (wsClusterBus),
|
|
308
321
|
appName: app.name,
|
|
309
322
|
deliverBroadcast: (/** @type {any} */ p) => a._deliverBroadcast(p),
|
|
310
323
|
deliverDirect: (/** @type {any} */ p) => a._deliverDirect(p),
|
|
@@ -317,10 +330,9 @@ export async function bootApp(projectRoot, { listen = true, port, host: listenHo
|
|
|
317
330
|
// MegaShutdown LIFO — 어댑터 disconnect 보다 먼저 정리(graceful LEAVE 가 NATS 끊기기 전에 나가게).
|
|
318
331
|
MegaShutdown.register(`mega-ws-cluster:${app.name}`, async () => cluster.stop())
|
|
319
332
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
333
|
+
}
|
|
334
|
+
if (wsClusterBus) {
|
|
335
|
+
logger?.debug?.({ bus: wsClusterCfg.bus, roster: wsClusterCfg.roster?.driver ?? 'none' }, 'boot.wsCluster wired (ADR-176)')
|
|
324
336
|
}
|
|
325
337
|
|
|
326
338
|
// 9단계: HTTP listen(분기 — CLI/테스트가 listen=false 로 mount 까지만 받을 수 있음).
|
|
@@ -265,6 +265,30 @@ export function validateAppConfig(appConfig, expectedFolderName, globalConfig) {
|
|
|
265
265
|
)
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// 2b) bridgeHub(WS Hub 브릿지, ADR-065/176) 검증 + wsCluster 상호배타.
|
|
269
|
+
// 클러스터 전송은 앱당 **하나**다 — bridgeHub(WS Hub) 와 global wsCluster(NATS) 를 동시에 쓰면
|
|
270
|
+
// app.broadcast 가 양쪽으로 나가 이중 전파된다. 부팅 시 fail-fast 로 막는다(boot 가 둘 중 하나만 배선).
|
|
271
|
+
if (appConfig.bridgeHub !== undefined) {
|
|
272
|
+
const bh = appConfig.bridgeHub
|
|
273
|
+
if (typeof bh !== 'object' || bh === null || Array.isArray(bh)) {
|
|
274
|
+
throw new MegaConfigError('config.bridgeHub_invalid', `app '${expectedFolderName}': bridgeHub must be an object ({ url, token, ... }).`, {
|
|
275
|
+
details: { app: expectedFolderName },
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
if (typeof bh.url !== 'string' || bh.url.length === 0) {
|
|
279
|
+
throw new MegaConfigError('config.bridgeHub_url_required', `app '${expectedFolderName}': bridgeHub.url (ws:// hub address) is required.`, {
|
|
280
|
+
details: { app: expectedFolderName },
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
if (globalConfig?.wsCluster?.bus) {
|
|
284
|
+
throw new MegaConfigError(
|
|
285
|
+
'config.cluster_transport_conflict',
|
|
286
|
+
`app '${expectedFolderName}': bridgeHub(WS Hub) 와 global wsCluster(NATS) 를 동시에 쓸 수 없다 — 클러스터 전송은 앱당 하나만 선택하세요(ADR-176).`,
|
|
287
|
+
{ details: { app: expectedFolderName, bridgeHubUrl: bh.url, wsClusterBus: globalConfig.wsCluster.bus } },
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
268
292
|
// 3) Shared-Reference 키 검증 — 참조하는 키가 globalConfig.services 에 존재해야 함
|
|
269
293
|
for (const refKey of SHARED_REFERENCE_KEYS) {
|
|
270
294
|
if (!appConfig[refKey]) continue
|