mega-framework 0.1.7 → 0.1.9
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/package.json +3 -3
- package/sample/crud/.env +9 -0
- package/sample/crud/.env.example +9 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +28 -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/routes/upload.js +20 -1
- package/sample/crud/apps/main/services/guide-service.js +4 -3
- package/sample/crud/apps/main/services/upload-demo-service.js +6 -5
- package/sample/crud/apps/main/views/upload/index.ejs +4 -1
- package/sample/crud/docs/guide/01-cli.md +587 -0
- package/sample/crud/docs/guide/02-router-controller.md +497 -0
- package/sample/crud/docs/guide/03-service-model-db.md +929 -0
- package/sample/crud/docs/guide/04-websocket-asp.md +632 -0
- package/sample/crud/docs/guide/05-scheduler-job-worker.md +400 -0
- package/sample/crud/docs/guide/06-config-auth-session-security.md +495 -0
- package/sample/crud/docs/guide/07-view-i18n-static-multipart.md +462 -0
- package/sample/crud/docs/guide/08-observability.md +373 -0
- package/sample/crud/mega.config.js +7 -0
- package/sample/crud/package.json +2 -2
- package/sample/crud/scripts/start-ws-hub.sh +18 -4
- package/sample/crud/var/uploads/(/355/212/271/352/270/260/354/236/220) 2026 /353/251/264/354/240/221/354/225/210/353/202/264-mqbnwq5v-d2125aa8.txt" +1 -0
- package/sample/crud/var/uploads/(/355/212/271/352/270/260/354/236/220) 2026 /353/251/264/354/240/221/354/225/210/353/202/264-mqbo0nbf-842b6135.txt" +1 -0
- package/sample/crud/var/uploads/00_b-mqbnh7lc-c9790ab8.png +0 -0
- package/sample/crud/var/uploads/2025-07-22 13 35 56-mqbngvvk-e545e90e.png +0 -0
- package/sample/crud/var/uploads/big-file-mqbo1h9e-2957eaf5.png +0 -0
- package/sample/crud/var/uploads//341/204/200/341/205/247/341/206/274/341/204/200/341/205/265/341/204/211/341/205/265/341/206/257/341/204/214/341/205/245/341/206/250/341/204/214/341/205/263/341/206/274/341/204/206/341/205/247/341/206/274/341/204/211/341/205/245-mqbo5yxh-5288d8ef.pdf +0 -0
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/adapters/adapter-options.js +14 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +7 -4
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +4 -1
- package/src/adapters/mongo-adapter.js +21 -7
- package/src/adapters/postgres-adapter.js +8 -4
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +6 -2
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/scaffold.js +38 -2
- package/src/cli/generators/index.js +58 -1
- package/src/cli/index.js +88 -59
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +3 -1
- package/src/core/ctx-builder.js +59 -1
- package/src/core/envelope.js +9 -2
- package/src/core/hub-link.js +24 -14
- package/src/core/index.js +1 -1
- package/src/core/mega-app.js +55 -45
- package/src/core/pipeline.js +8 -6
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +3 -3
- package/src/core/session-store.js +14 -1
- package/src/core/ws-presence.js +17 -5
- package/src/core/ws-roster.js +49 -10
- package/src/core/ws-upgrade.js +105 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +10 -0
- package/src/lib/mega-job-queue.js +53 -13
- package/src/lib/mega-job.js +8 -1
- package/src/lib/mega-metrics.js +28 -1
- package/src/lib/mega-plugin.js +2 -2
- package/src/lib/mega-worker.js +28 -5
- package/src/lib/ws-hub.js +90 -9
- package/templates/adr/code.tpl +23 -0
- package/types/adapters/adapter-options.d.ts +2 -0
- package/types/adapters/file-adapter.d.ts +12 -1
- package/types/adapters/file-session-adapter.d.ts +4 -2
- package/types/adapters/maria-adapter.d.ts +5 -3
- package/types/adapters/mega-cache-adapter.d.ts +27 -1
- package/types/adapters/mega-db-adapter.d.ts +4 -1
- package/types/adapters/mongo-adapter.d.ts +13 -2
- package/types/adapters/postgres-adapter.d.ts +4 -2
- package/types/adapters/redis-adapter.d.ts +8 -0
- package/types/adapters/sqlite-adapter.d.ts +8 -2
- package/types/cli/generators/index.d.ts +11 -1
- package/types/cli/index.d.ts +12 -27
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ctx-builder.d.ts +23 -0
- package/types/core/hub-link.d.ts +3 -1
- package/types/core/index.d.ts +1 -1
- package/types/core/mega-app.d.ts +1 -1
- package/types/core/pipeline.d.ts +2 -1
- package/types/core/security.d.ts +3 -3
- package/types/core/session-store.d.ts +7 -0
- package/types/core/ws-roster.d.ts +13 -1
- package/types/core/ws-upgrade.d.ts +29 -0
- package/types/lib/mega-circuit-breaker.d.ts +4 -2
- package/types/lib/mega-health.d.ts +7 -0
- package/types/lib/mega-job-queue.d.ts +16 -4
- package/types/lib/mega-job.d.ts +8 -1
- package/types/lib/mega-plugin.d.ts +1 -1
- package/types/lib/mega-worker.d.ts +3 -1
- package/types/lib/ws-hub.d.ts +27 -2
package/src/cli/index.js
CHANGED
|
@@ -19,26 +19,20 @@
|
|
|
19
19
|
*
|
|
20
20
|
* @module cli/index
|
|
21
21
|
*/
|
|
22
|
+
// 무거운 런타임 그래프(boot→fastify·OTel·pino·ejs·보안 플러그인, 클러스터, 마이그레이션 러너)는
|
|
23
|
+
// 정적 import 하지 않는다 — `mega help`/`g`/`new` 같은 비부팅 명령이 전체 그래프를 평가해 4배
|
|
24
|
+
// 느려지는 것을 막기 위해, 각 런타임 명령의 action/호스트 함수 내부에서 dynamic import 한다.
|
|
25
|
+
// 모듈 레벨 싱글톤(adapter-manager·MegaShutdown 등)은 dynamic import 도 같은 인스턴스를 본다.
|
|
22
26
|
import { existsSync } from 'node:fs'
|
|
23
27
|
import { join } from 'node:path'
|
|
24
28
|
import os from 'node:os'
|
|
25
29
|
import { spawn as nodeSpawn } from 'node:child_process'
|
|
26
|
-
import { bootApp, prepareRuntime } from '../core/boot.js'
|
|
27
|
-
import { MegaCluster } from '../core/mega-cluster.js'
|
|
28
|
-
import { installPrimaryAggregator, installWorkerResponder } from '../core/cluster-metrics.js'
|
|
29
30
|
import { loadAndValidateConfig } from '../core/config-loader.js'
|
|
30
|
-
import { migrateUp, migrateDown, migrateStatus } from '../core/migration-runner.js'
|
|
31
|
-
import { withMigrationLock } from '../core/migration-lock.js'
|
|
32
|
-
import { runMigrateGenerate } from '../core/migration/generate.js'
|
|
33
31
|
import { buildFromGlobalConfig, connectAll, disconnectAll, get as getAdapter } from '../adapters/adapter-manager.js'
|
|
34
32
|
import { MegaDbAdapter } from '../adapters/mega-db-adapter.js'
|
|
35
33
|
import { MegaConfigError } from '../errors/config-error.js'
|
|
36
34
|
import { MegaPluginHost, loadPlugins } from '../lib/mega-plugin.js'
|
|
37
|
-
import { MegaJobWorker } from '../lib/mega-job-worker.js'
|
|
38
|
-
import { MegaScheduler } from '../lib/mega-schedule.js'
|
|
39
35
|
import { MegaShutdown, DEFAULT_HARD_KILL_MS } from '../lib/mega-shutdown.js'
|
|
40
|
-
import { buildLogger } from '../lib/mega-logger.js'
|
|
41
|
-
import * as MegaMetrics from '../lib/mega-metrics.js'
|
|
42
36
|
import { Command } from 'commander'
|
|
43
37
|
import { registerScaffoldCommands, commanderErrorToExitCode } from './commands/scaffold.js'
|
|
44
38
|
|
|
@@ -240,37 +234,35 @@ export function shouldReexecForWatch(flags, env) {
|
|
|
240
234
|
}
|
|
241
235
|
|
|
242
236
|
/**
|
|
243
|
-
* `mega start --watch`
|
|
237
|
+
* `mega start --watch` supervisor 의 **자식 인자**를 빌드한다 (ADR-220, ADR-182 개정).
|
|
244
238
|
*
|
|
245
|
-
* - `--watch`
|
|
239
|
+
* - `--watch`/`--watch-ignore`(값 포함)는 부모(supervisor) 전용 — 자식 인자에서 제거한다
|
|
240
|
+
* (가드 env 와 이중 안전으로 무한재귀 방지).
|
|
246
241
|
* - **단일 프로세스 강제**(`--cluster 1`) — 멀티워커 watch 카오스 방지(기존 `--cluster` 값은 무시).
|
|
247
|
-
* - watchPaths 가 있으면 `--watch-path=…`(이게 모듈 그래프 watch 를 **대체**하므로 앱 소스·전역설정 경로를
|
|
248
|
-
* 명시해야 한다 — 실측 확인). 없으면 plain `--watch`(모듈 그래프 폴백).
|
|
249
242
|
*
|
|
250
|
-
* @param {
|
|
251
|
-
* @
|
|
252
|
-
* @param {string[]} o.watchPaths - 감시할 절대경로(존재하는 것만).
|
|
253
|
-
* @param {string} o.selfPath - mega 진입 스크립트(`process.argv[1]`).
|
|
254
|
-
* @param {string} o.execPath - node 바이너리(`process.execPath`).
|
|
255
|
-
* @returns {{ command: string, args: string[] }}
|
|
243
|
+
* @param {string[]} argv - 원본 mega argv(예: `['start','--watch','--port','3000']`).
|
|
244
|
+
* @returns {string[]} 자식 `mega` 인자(예: `['start','--port','3000','--cluster','1']`).
|
|
256
245
|
*/
|
|
257
|
-
export function
|
|
258
|
-
|
|
259
|
-
Array.isArray(watchPaths) && watchPaths.length > 0 ? watchPaths.map((p) => `--watch-path=${p}`) : ['--watch']
|
|
260
|
-
/** @type {string[]} mega 인자 — `--watch` 제거 + `--cluster`(값 포함) 제거. */
|
|
246
|
+
export function buildWatchChildArgs(argv) {
|
|
247
|
+
/** @type {string[]} */
|
|
261
248
|
const megaArgs = []
|
|
262
249
|
for (let i = 0; i < argv.length; i++) {
|
|
263
250
|
const tok = argv[i]
|
|
264
251
|
if (tok === '--watch' || tok.startsWith('--watch=')) continue
|
|
265
|
-
if (tok === '--
|
|
252
|
+
if (tok === '--watch-ignore') {
|
|
266
253
|
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i++ // 값 토큰도 건너뜀.
|
|
267
254
|
continue
|
|
268
255
|
}
|
|
256
|
+
if (tok.startsWith('--watch-ignore=')) continue
|
|
257
|
+
if (tok === '--cluster') {
|
|
258
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i++
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
269
261
|
if (tok.startsWith('--cluster=')) continue
|
|
270
262
|
megaArgs.push(tok)
|
|
271
263
|
}
|
|
272
264
|
megaArgs.push('--cluster', '1') // 단일 프로세스 강제.
|
|
273
|
-
return
|
|
265
|
+
return megaArgs
|
|
274
266
|
}
|
|
275
267
|
|
|
276
268
|
/**
|
|
@@ -338,38 +330,47 @@ export async function runCli(argv, { out = console.log, err = console.error, cwd
|
|
|
338
330
|
|
|
339
331
|
/**
|
|
340
332
|
* `mega start`/`serve` 본체 — dev watch 재실행 또는 부팅+listen(단일/클러스터).
|
|
341
|
-
* @param {{ port?: string, host?: string, cluster?: string | boolean, watch?: boolean }} opts - commander 옵션.
|
|
333
|
+
* @param {{ port?: string, host?: string, cluster?: string | boolean, watch?: boolean, watchIgnore?: string }} opts - commander 옵션.
|
|
342
334
|
* @param {{ argv: string[], out: (msg: string) => void, projectRoot: string, logger?: { debug?: Function, info?: Function, warn?: Function }, spawn: Function, selfPath: string }} ctx
|
|
343
335
|
* @returns {Promise<number>} exit code.
|
|
344
336
|
*/
|
|
345
337
|
async function runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath }) {
|
|
346
338
|
{
|
|
347
|
-
// dev watch (ADR-182) — `--watch` 면
|
|
348
|
-
//
|
|
349
|
-
//
|
|
339
|
+
// dev watch (ADR-220, ADR-182 개정) — `--watch` 면 자체 supervisor 가 자식(`mega start`)을 띄우고
|
|
340
|
+
// fs.watch(recursive) + ignore 패턴 + debounce 로 재시작을 조율한다. node 내장 --watch 는 ignore 가
|
|
341
|
+
// 없어 런타임 산출물(locales saveMissing·업로드·journal)이 재시작 루프를 만들었다. 단일 프로세스
|
|
342
|
+
// 강제 + NODE_ENV 미설정 시 development 기본 + 가드 env 로 무한재귀 방지는 기존과 동일.
|
|
350
343
|
if (shouldReexecForWatch({ watch: opts.watch === true }, process.env)) {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
const { mergeWatchIgnore, startWatchSupervisor, defaultWatchPaths } = await import('./watch.js')
|
|
345
|
+
// config 의 watch.ignore 는 best-effort — dev 루프는 config 가 깨진 동안에도 살아 있어야
|
|
346
|
+
// 하므로(고치면 재시작) 로드 실패 시 디폴트 ignore 로 진행하고 이유를 알린다.
|
|
347
|
+
/** @type {{ ignore?: unknown, ignoreDefaults?: unknown }} */
|
|
348
|
+
let watchConfig = {}
|
|
349
|
+
try {
|
|
350
|
+
const { global } = await loadAndValidateConfig(projectRoot)
|
|
351
|
+
watchConfig = /** @type {any} */ (global).watch ?? {}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
out(`mega: watch — mega.config.js 를 읽지 못해 디폴트 ignore 로 감시합니다(${/** @type {any} */ (e).message?.split('\n')[0] ?? e}).`)
|
|
354
|
+
}
|
|
355
|
+
const ignore = mergeWatchIgnore(watchConfig, typeof opts.watchIgnore === 'string' ? opts.watchIgnore : undefined)
|
|
356
|
+
const watchPaths = defaultWatchPaths(projectRoot)
|
|
357
|
+
const childArgs = buildWatchChildArgs(argv)
|
|
358
|
+
out(`mega: watch mode (single process) — watching [${watchPaths.join(', ')}], ignore ${ignore.length} patterns`)
|
|
359
|
+
if (ignore.length === 0) {
|
|
360
|
+
// 숨은 디폴트가 없으므로(ADR-220) 미설정은 "모든 변경이 재시작" — locales(saveMissing 자동
|
|
361
|
+
// 기입)·업로드 같은 런타임 쓰기 영역이 있으면 루프가 날 수 있어 가시화한다(P4).
|
|
362
|
+
out('mega: watch ignore 가 비어 있습니다 — locales/업로드 등 런타임 쓰기 폴더가 있으면 .env 의 WATCH_IGNORE(또는 config watch.ignore) 설정을 권장합니다.')
|
|
366
363
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
364
|
+
return await startWatchSupervisor({
|
|
365
|
+
projectRoot,
|
|
366
|
+
watchPaths,
|
|
367
|
+
ignore,
|
|
368
|
+
out,
|
|
369
|
+
spawnChild: () =>
|
|
370
|
+
spawn(process.execPath, [selfPath, ...childArgs], {
|
|
371
|
+
stdio: 'inherit',
|
|
372
|
+
env: { ...process.env, MEGA_WATCH_REEXEC: '1', NODE_ENV: process.env.NODE_ENV ?? 'development' },
|
|
373
|
+
}),
|
|
373
374
|
})
|
|
374
375
|
}
|
|
375
376
|
|
|
@@ -394,6 +395,15 @@ async function runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath
|
|
|
394
395
|
else out(`mega: ${msg}`)
|
|
395
396
|
}
|
|
396
397
|
|
|
398
|
+
// 런타임 그래프 lazy 로드 — 부팅 명령에서만 프레임워크 전체를 지불한다(모듈 상단 주석).
|
|
399
|
+
const [{ bootApp }, { MegaCluster }, { installPrimaryAggregator, installWorkerResponder }, { buildLogger }] =
|
|
400
|
+
await Promise.all([
|
|
401
|
+
import('../core/boot.js'),
|
|
402
|
+
import('../core/mega-cluster.js'),
|
|
403
|
+
import('../core/cluster-metrics.js'),
|
|
404
|
+
import('../lib/mega-logger.js'),
|
|
405
|
+
])
|
|
406
|
+
|
|
397
407
|
// 워커 부팅 함수 — 단일/클러스터 모드 공통. 클러스터에선 각 워커 프로세스가 이 함수를 실행한다.
|
|
398
408
|
const bootListen = async () => {
|
|
399
409
|
const { server, appLogger } = await bootApp(projectRoot, { listen: true, port, host, logger })
|
|
@@ -485,7 +495,8 @@ function buildProgram({ argv, out, err, projectRoot, logger, spawn, selfPath, se
|
|
|
485
495
|
'--cluster [n]',
|
|
486
496
|
"워커 프로세스 수 — 정수 N 또는 'max'(CPU 코어 수, 베어 플래그도 max). 우선순위: 플래그 > MEGA_CLUSTER_WORKERS env > server.cluster config. 1/미지정 = 단일 프로세스",
|
|
487
497
|
)
|
|
488
|
-
.option('--watch', 'dev watch — 파일 변경 시 자동 재시작(
|
|
498
|
+
.option('--watch', 'dev watch — 파일 변경 시 자동 재시작(단일 프로세스 강제). locales/views/public/uploads/var/.mega/.env* 등은 디폴트 ignore')
|
|
499
|
+
.option('--watch-ignore <globs>', 'watch 추가 ignore(콤마 구분 glob, 디폴트에 더해짐). config watch.ignore 와 병합')
|
|
489
500
|
.action(async (/** @type {any} */ opts) => {
|
|
490
501
|
setExit(await runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath }))
|
|
491
502
|
})
|
|
@@ -539,6 +550,7 @@ function buildProgram({ argv, out, err, projectRoot, logger, spawn, selfPath, se
|
|
|
539
550
|
.option('--adapter <key>', '특정 adapter(services.databases 키)만 처리(기본: 전부)')
|
|
540
551
|
.option('--app <name>', '마이그레이션 파일을 둘 앱(기본: main)', 'main')
|
|
541
552
|
.action(async (/** @type {string | undefined} */ name, /** @type {any} */ opts) => {
|
|
553
|
+
const { runMigrateGenerate } = await import('../core/migration/generate.js')
|
|
542
554
|
const r = await runMigrateGenerate(projectRoot, {
|
|
543
555
|
name,
|
|
544
556
|
renames: opts.renames,
|
|
@@ -662,10 +674,11 @@ export function collectRegistrations(global, host, kind) {
|
|
|
662
674
|
* 주입(테스트)이 있으면 그쪽을 그대로 쓰고 pino 는 만들지 않는다(side-effect 회피).
|
|
663
675
|
* @param {Object} global - global config(`logger` 보유 가능).
|
|
664
676
|
* @param {{ info?: Function, warn?: Function } | undefined} injectedLogger - 주입 로거(테스트) 또는 undefined.
|
|
665
|
-
* @returns {{ info?: Function, warn?: Function } | null} 호스트 로그에 쓸 로거(없으면 null).
|
|
677
|
+
* @returns {Promise<{ info?: Function, warn?: Function } | null>} 호스트 로그에 쓸 로거(없으면 null).
|
|
666
678
|
*/
|
|
667
|
-
function wireHostLogger(global, injectedLogger) {
|
|
679
|
+
async function wireHostLogger(global, injectedLogger) {
|
|
668
680
|
if (injectedLogger) return injectedLogger
|
|
681
|
+
const { buildLogger } = await import('../lib/mega-logger.js') // pino 그래프 — 호스트 경로에서만 로드.
|
|
669
682
|
const appLogger = buildLogger(/** @type {any} */ (global).logger)
|
|
670
683
|
if (appLogger) {
|
|
671
684
|
// 종료 시퀀스 로그를 pino 로(ADR-178) + graceful 시 로그 버퍼 flush. 'logs' stage 라 종료
|
|
@@ -683,12 +696,17 @@ function wireHostLogger(global, injectedLogger) {
|
|
|
683
696
|
* 등록 소스 = `config.jobs` + 플러그인 `mega.jobs.register` 분(`collectRegistrations`, ADR-123).
|
|
684
697
|
* @param {string} projectRoot
|
|
685
698
|
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
686
|
-
* @returns {Promise<MegaJobWorker>}
|
|
699
|
+
* @returns {Promise<import('../lib/mega-job-worker.js').MegaJobWorker>}
|
|
687
700
|
*/
|
|
688
701
|
export async function runWorkerHost(projectRoot, logger) {
|
|
689
|
-
// bootApp 과 같은 토대(config → 플러그인 install → 어댑터 connect → ctx).
|
|
702
|
+
// bootApp 과 같은 토대(config → 플러그인 install → 어댑터 connect → ctx). 런타임 그래프는 lazy.
|
|
703
|
+
const [{ prepareRuntime }, { MegaJobWorker }, MegaMetrics] = await Promise.all([
|
|
704
|
+
import('../core/boot.js'),
|
|
705
|
+
import('../lib/mega-job-worker.js'),
|
|
706
|
+
import('../lib/mega-metrics.js'),
|
|
707
|
+
])
|
|
690
708
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
691
|
-
const log = wireHostLogger(global, logger)
|
|
709
|
+
const log = await wireHostLogger(global, logger)
|
|
692
710
|
const worker = new MegaJobWorker({ ctx })
|
|
693
711
|
// 잡 메트릭 (ADR-132) — prepareRuntime 이 health.exposeMetrics 시 이미 MegaMetrics.init 했고,
|
|
694
712
|
// 옵트인 OFF 면 subscribeJobs 는 no-op() 을 반환한다. start 전에 구독해 enqueue(dispatch)부터 잡는다. 구독은
|
|
@@ -712,11 +730,15 @@ export async function runWorkerHost(projectRoot, logger) {
|
|
|
712
730
|
* `mega scheduler` 호스트 골격 — config + 어댑터 connect + ctx + `MegaScheduler` 인스턴스 + graceful.
|
|
713
731
|
* @param {string} projectRoot
|
|
714
732
|
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
715
|
-
* @returns {Promise<MegaScheduler>}
|
|
733
|
+
* @returns {Promise<import('../lib/mega-schedule.js').MegaScheduler>}
|
|
716
734
|
*/
|
|
717
735
|
export async function runSchedulerHost(projectRoot, logger) {
|
|
736
|
+
const [{ prepareRuntime }, { MegaScheduler }] = await Promise.all([
|
|
737
|
+
import('../core/boot.js'),
|
|
738
|
+
import('../lib/mega-schedule.js'),
|
|
739
|
+
])
|
|
718
740
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
719
|
-
const log = wireHostLogger(global, logger)
|
|
741
|
+
const log = await wireHostLogger(global, logger)
|
|
720
742
|
const scheduler = new MegaScheduler({ ctx })
|
|
721
743
|
// 등록 소스(ADR-123) = config.schedules(정적) + 플러그인 host.listSchedules()(동적). register 가
|
|
722
744
|
// cron 미선언·중복을 부팅 시 fail-fast.
|
|
@@ -807,6 +829,13 @@ export async function runMigrateHost(projectRoot, { direction = 'up', dbKey, log
|
|
|
807
829
|
info: (/** @type {any} */ o, /** @type {string} */ msg) => out(`[migrate] ${fmt(o, msg)}`),
|
|
808
830
|
warn: (/** @type {any} */ o, /** @type {string} */ msg) => out(`[migrate] 경고: ${fmt(o, msg)}`),
|
|
809
831
|
}
|
|
832
|
+
// 빌트인 어댑터 배럴을 명시 로드(ADR-150) — migrate 는 boot.js 를 거치지 않고 buildFromGlobalConfig
|
|
833
|
+
// 를 직접 부르므로, boot 정적 import 가 사라진 lazy CLI 에선 여기서 driver 자기등록을 보장해야 한다.
|
|
834
|
+
const [{ migrateUp, migrateDown, migrateStatus }, { withMigrationLock }] = await Promise.all([
|
|
835
|
+
import('../core/migration-runner.js'),
|
|
836
|
+
import('../core/migration-lock.js'),
|
|
837
|
+
import('../adapters/index.js'),
|
|
838
|
+
])
|
|
810
839
|
const { global, apps } = await loadAndValidateConfig(projectRoot)
|
|
811
840
|
// 플러그인 install 을 어댑터 build 보다 먼저(config-driven driver 제약, boot.js 주석 참조).
|
|
812
841
|
const host = new MegaPluginHost({ logger })
|
package/src/cli/watch.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `mega start --watch` 의 dev watch supervisor (ADR-220, ADR-182 개정).
|
|
4
|
+
*
|
|
5
|
+
* Node 내장 `--watch-path` 재실행(구 ADR-182)은 **ignore 패턴이 없어** 런타임이 쓰는 파일까지
|
|
6
|
+
* 재시작을 유발했다 — dev 의 i18n saveMissing 이 `locales/*.json` 자동 기입(재시작 무한 루프),
|
|
7
|
+
* 업로드가 `uploads/`/`var/` 에 파일 저장(사용자 입력이 서버를 재시작), 마이그레이션 generate 가
|
|
8
|
+
* `.mega/` journal 갱신. 본 모듈이 그 재실행을 **자체 supervisor**(fs.watch recursive + 미니 glob
|
|
9
|
+
* ignore + debounce + SIGTERM respawn)로 대체한다 — 신규 dep 0(chokidar 미도입: 필요한 매칭이
|
|
10
|
+
* `**`/`*`/`?` 수준이라 풀스펙 glob 라이브러리가 과함).
|
|
11
|
+
*
|
|
12
|
+
* ignore 합성 = config `watch.ignore` + CLI `--watch-ignore`. **숨은 코드 디폴트는 없다**(사용자
|
|
13
|
+
* 결정 — 개발자가 보지 못하는 목록은 폴더명이 다른 프로젝트에서 오히려 혼란) — 권장 목록은 스캐폴드
|
|
14
|
+
* `.env` 의 `WATCH_IGNORE` 가 실값으로 명시하고 `mega.config.js` 가 읽어 `watch.ignore` 로 넘긴다.
|
|
15
|
+
* ignore 0 이면 supervisor 가 기동 시 경고 1줄을 낸다(locales saveMissing 루프 등 위험 가시화).
|
|
16
|
+
*
|
|
17
|
+
* @module cli/watch
|
|
18
|
+
*/
|
|
19
|
+
import { watch as fsWatch, existsSync, statSync } from 'node:fs'
|
|
20
|
+
import { join, relative, sep } from 'node:path'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 미니 glob → RegExp. 지원: 더블스타+슬래시(0개 이상의 디렉토리), 더블스타(경로 전체), `*`(세그먼트
|
|
24
|
+
* 내), `?`(1글자). 그 외 문자는 리터럴(정규식 특수문자 escape). 매칭 대상은 루트 상대 POSIX 경로
|
|
25
|
+
* 전체(^…$). 패턴 예시는 스캐폴드 `.env` 의 `WATCH_IGNORE` 참조 — JS 블록 주석 안에는
|
|
26
|
+
* 더블스타+슬래시 시퀀스를 쓸 수 없어(주석 조기 종료) 여기 직접 못 적는다.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} pattern - glob 패턴(예 `.env.*`).
|
|
29
|
+
* @returns {RegExp}
|
|
30
|
+
*/
|
|
31
|
+
export function globToRegExp(pattern) {
|
|
32
|
+
let re = ''
|
|
33
|
+
let i = 0
|
|
34
|
+
while (i < pattern.length) {
|
|
35
|
+
const ch = pattern[i]
|
|
36
|
+
if (ch === '*') {
|
|
37
|
+
if (pattern[i + 1] === '*') {
|
|
38
|
+
// `**/` 는 0개 이상의 디렉토리(루트 레벨 포함), 그 외 `**` 는 임의 문자열.
|
|
39
|
+
if (pattern[i + 2] === '/') {
|
|
40
|
+
re += '(?:.*/)?'
|
|
41
|
+
i += 3
|
|
42
|
+
} else {
|
|
43
|
+
re += '.*'
|
|
44
|
+
i += 2
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
re += '[^/]*'
|
|
48
|
+
i += 1
|
|
49
|
+
}
|
|
50
|
+
} else if (ch === '?') {
|
|
51
|
+
re += '[^/]'
|
|
52
|
+
i += 1
|
|
53
|
+
} else {
|
|
54
|
+
re += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
55
|
+
i += 1
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return new RegExp(`^${re}$`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ignore 패턴 합성 — config(`watch.ignore`) + CLI(`--watch-ignore a,b`). 숨은 디폴트 없음(모듈
|
|
63
|
+
* docstring 참조). 잘못된 모양은 fail-fast(silent 무시 금지).
|
|
64
|
+
*
|
|
65
|
+
* @param {{ ignore?: unknown }} [watchConfig] - mega.config.js 의 `watch`.
|
|
66
|
+
* @param {string | undefined} [cliIgnore] - `--watch-ignore` 값(콤마 구분).
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
* @throws {TypeError} config `watch.ignore` 가 문자열 배열이 아닐 때.
|
|
69
|
+
*/
|
|
70
|
+
export function mergeWatchIgnore(watchConfig = {}, cliIgnore = undefined) {
|
|
71
|
+
/** @type {string[]} */
|
|
72
|
+
const merged = []
|
|
73
|
+
if (watchConfig.ignore !== undefined) {
|
|
74
|
+
if (!Array.isArray(watchConfig.ignore) || watchConfig.ignore.some((p) => typeof p !== 'string' || p.length === 0)) {
|
|
75
|
+
throw new TypeError('config watch.ignore 는 비어있지 않은 문자열 배열이어야 합니다.')
|
|
76
|
+
}
|
|
77
|
+
merged.push(...watchConfig.ignore)
|
|
78
|
+
}
|
|
79
|
+
if (typeof cliIgnore === 'string' && cliIgnore.length > 0) {
|
|
80
|
+
merged.push(...cliIgnore.split(',').map((p) => p.trim()).filter((p) => p.length > 0))
|
|
81
|
+
}
|
|
82
|
+
return merged
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 루트 상대 경로가 ignore 에 걸리는지 (Boolean — `is*`).
|
|
87
|
+
* @param {string} rel - 루트 상대 POSIX 경로(예 `apps/main/locales/ko.json`).
|
|
88
|
+
* @param {RegExp[]} regexps - {@link globToRegExp} 컴파일 결과.
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
export function isIgnoredPath(rel, regexps) {
|
|
92
|
+
return regexps.some((r) => r.test(rel))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* watch supervisor — 감시 경로의 변경(ignore 통과분)을 debounce 후 자식(`mega start`)을
|
|
97
|
+
* SIGTERM → respawn 한다. 자식이 스스로 죽으면(crash) 재시작하지 않고 다음 변경을 기다린다
|
|
98
|
+
* (crash-loop 방지 — nodemon 의 "app crashed - waiting for file changes" 와 동형).
|
|
99
|
+
*
|
|
100
|
+
* @param {object} o
|
|
101
|
+
* @param {string} o.projectRoot
|
|
102
|
+
* @param {string[]} o.watchPaths - 감시할 절대경로(존재하는 것만 — 호출측이 필터).
|
|
103
|
+
* @param {string[]} o.ignore - glob 패턴 목록.
|
|
104
|
+
* @param {() => import('node:child_process').ChildProcess} o.spawnChild - 자식 기동 팩토리(주입 — 테스트).
|
|
105
|
+
* @param {(msg: string) => void} o.out
|
|
106
|
+
* @param {number} [o.debounceMs=500] - 연속 변경 디바운스.
|
|
107
|
+
* @param {(dir: string, opts: object, cb: (event: string, filename: string | Buffer | null) => void) => { close(): void }} [o.watchFn] -
|
|
108
|
+
* fs.watch 주입(테스트). 기본 node:fs watch.
|
|
109
|
+
* @returns {Promise<number>} 부모(supervisor)의 exit code — SIGINT/SIGTERM 수신 시 자식 정리 후 0.
|
|
110
|
+
*/
|
|
111
|
+
export function startWatchSupervisor({ projectRoot, watchPaths, ignore, spawnChild, out, debounceMs = 500, watchFn = /** @type {any} */ (fsWatch) }) {
|
|
112
|
+
const regexps = ignore.map(globToRegExp)
|
|
113
|
+
/** @type {import('node:child_process').ChildProcess | null} */
|
|
114
|
+
let child = null
|
|
115
|
+
/** @type {NodeJS.Timeout | null} */
|
|
116
|
+
let pending = null
|
|
117
|
+
let shuttingDown = false
|
|
118
|
+
let restarting = false
|
|
119
|
+
|
|
120
|
+
const start = () => {
|
|
121
|
+
child = spawnChild()
|
|
122
|
+
child.on('exit', (code, signal) => {
|
|
123
|
+
if (shuttingDown) return
|
|
124
|
+
if (restarting) {
|
|
125
|
+
restarting = false
|
|
126
|
+
start()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
// 자식이 스스로 종료(crash/정상) — 재시작 폭주 대신 다음 변경 대기(P4: 이유 출력).
|
|
130
|
+
out(`mega: app exited (code=${code ?? `signal:${signal}`}) — waiting for file changes before restart`)
|
|
131
|
+
child = null
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const scheduleRestart = (/** @type {string} */ rel) => {
|
|
136
|
+
if (pending) clearTimeout(pending)
|
|
137
|
+
pending = setTimeout(() => {
|
|
138
|
+
pending = null
|
|
139
|
+
out(`mega: restarting due to changes (${rel})`)
|
|
140
|
+
if (child && child.exitCode === null && !child.killed) {
|
|
141
|
+
restarting = true
|
|
142
|
+
child.kill('SIGTERM') // graceful — exit 핸들러가 respawn.
|
|
143
|
+
} else {
|
|
144
|
+
start() // crash 후 대기 상태 — 변경으로 재기동.
|
|
145
|
+
}
|
|
146
|
+
}, debounceMs)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @type {Array<{ close(): void }>} */
|
|
150
|
+
const watchers = []
|
|
151
|
+
for (const abs of watchPaths) {
|
|
152
|
+
const isDir = statSync(abs).isDirectory()
|
|
153
|
+
const watcher = watchFn(abs, { recursive: isDir }, (_event, filename) => {
|
|
154
|
+
// filename 은 감시 루트 상대(파일 watch 면 basename) — 루트 상대로 정규화해 ignore 매칭.
|
|
155
|
+
const name = typeof filename === 'string' ? filename : (filename ? String(filename) : '')
|
|
156
|
+
const rel = relative(projectRoot, isDir ? join(abs, name) : abs).split(sep).join('/')
|
|
157
|
+
if (isIgnoredPath(rel, regexps)) return
|
|
158
|
+
scheduleRestart(rel)
|
|
159
|
+
})
|
|
160
|
+
watchers.push(watcher)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
start()
|
|
164
|
+
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
/** @type {NodeJS.Signals[]} */
|
|
167
|
+
const sigs = ['SIGINT', 'SIGTERM']
|
|
168
|
+
const onSignal = () => {
|
|
169
|
+
shuttingDown = true
|
|
170
|
+
if (pending) clearTimeout(pending)
|
|
171
|
+
for (const w of watchers) w.close()
|
|
172
|
+
for (const s of sigs) process.removeListener(s, onSignal)
|
|
173
|
+
if (child && child.exitCode === null && !child.killed) {
|
|
174
|
+
child.once('exit', () => resolve(0))
|
|
175
|
+
child.kill('SIGTERM')
|
|
176
|
+
} else {
|
|
177
|
+
resolve(0)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const s of sigs) process.on(s, onSignal)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** {@link startWatchSupervisor} 의 watchPaths 기본 집합 — 존재하는 것만.
|
|
185
|
+
* @param {string} projectRoot @returns {string[]} */
|
|
186
|
+
export function defaultWatchPaths(projectRoot) {
|
|
187
|
+
return [join(projectRoot, 'apps'), join(projectRoot, 'shared'), join(projectRoot, 'mega.config.js')].filter(existsSync)
|
|
188
|
+
}
|
package/src/core/ajv-mapper.js
CHANGED
|
@@ -74,7 +74,9 @@ function ajvItemToDetail(e) {
|
|
|
74
74
|
if (e.keyword === 'required' && missing) {
|
|
75
75
|
field = field ? `${field}.${missing}` : missing
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// 정규식 1회 실행 (G1 M-2, ADR-214) — field·missingProperty 를 합쳐 한 번만 검사한다. allErrors
|
|
78
|
+
// (ADR-193) 이후 400 1건당 detail × 2회 실행되던 비용 절반화. 매칭 의미는 동일(부분 일치 OR).
|
|
79
|
+
const isSensitive = SENSITIVE_FIELD_RE.test(missing ? `${field} ${missing}` : field)
|
|
78
80
|
return {
|
|
79
81
|
field,
|
|
80
82
|
rule: e.keyword || 'unknown',
|
package/src/core/ctx-builder.js
CHANGED
|
@@ -37,6 +37,9 @@ function passthroughT(key, defaultValue) {
|
|
|
37
37
|
/** 요청당 ctx 를 캐싱하는 비열거 키. 글로벌 미들웨어와 라우트 핸들러가 같은 ctx 를 공유하도록 한다. */
|
|
38
38
|
const HTTP_CTX_KEY = Symbol('mega.httpCtx')
|
|
39
39
|
|
|
40
|
+
/** 요청당 lazy ctx 프록시 캐싱 키 — 미들웨어·핸들러가 같은 프록시 객체를 받도록 한다 (ADR-214). */
|
|
41
|
+
const LAZY_CTX_KEY = Symbol('mega.lazyHttpCtx')
|
|
42
|
+
|
|
40
43
|
/**
|
|
41
44
|
* `ctx.services.<name>` lazy DI proxy (ADR-148) — 요청별 서비스 인스턴스화·캐시.
|
|
42
45
|
*
|
|
@@ -252,6 +255,61 @@ export function getHttpCtx({ app, req, reply }) {
|
|
|
252
255
|
const cached = /** @type {any} */ (req)[HTTP_CTX_KEY]
|
|
253
256
|
if (cached) return cached
|
|
254
257
|
const ctx = buildHttpCtx({ app, req, reply })
|
|
255
|
-
|
|
258
|
+
// 심볼 직접 대입 (G1 H-2, ADR-214) — 심볼 키는 어차피 Object.keys/spread/JSON 에 안 잡히므로
|
|
259
|
+
// 비열거 defineProperty 의 실익이 없고, defineProperty 는 hidden class 전이를 유발해 17배 비싸다(실측).
|
|
260
|
+
;/** @type {any} */ (req)[HTTP_CTX_KEY] = ctx
|
|
256
261
|
return ctx
|
|
257
262
|
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* **lazy** HTTP ctx — 실제 ctx 빌드를 첫 속성 접근 시점까지 미루는 요청당 프록시 (G1 H-1, ADR-214).
|
|
266
|
+
*
|
|
267
|
+
* hot path 단일 최대 프레임워크 비용이 "핸들러가 ctx 를 안 써도 매 요청 무조건 buildHttpCtx"
|
|
268
|
+
* (1,615 B + ≈0.7 µs/req, CPU 2.3% — audit-G1 실측)였다. 이 프록시는 생성 비용이 객체 1개 수준이고,
|
|
269
|
+
* 핸들러·미들웨어가 ctx 의 속성을 **처음 만질 때만** {@link getHttpCtx} 로 실 ctx 를 만든다(이후 캐시).
|
|
270
|
+
*
|
|
271
|
+
* 호환성: canonical 시그니처 `(req, reply, ctx)` 는 그대로 — 모든 핸들러가 여전히 3번째 인자를 받고,
|
|
272
|
+
* 참조하는 순간부터는 기존과 동일한 실 ctx 표면(ADR-134 요청당 공유 포함)이다. 프록시 자체도 요청당
|
|
273
|
+
* 1개로 캐시되어 미들웨어와 핸들러가 **동일 객체**(`===`)를 받는다. 모든 트랩이 실 ctx 로 위임되므로
|
|
274
|
+
* `in`/spread/`Object.keys`/getter·setter(`ctx.user=`) 동작이 보존된다.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} args
|
|
277
|
+
* @param {import('./mega-app.js').MegaApp | null} args.app
|
|
278
|
+
* @param {import('fastify').FastifyRequest} args.req
|
|
279
|
+
* @param {import('fastify').FastifyReply} args.reply
|
|
280
|
+
* @returns {Record<string, any>}
|
|
281
|
+
*/
|
|
282
|
+
export function getLazyHttpCtx({ app, req, reply }) {
|
|
283
|
+
const cached = /** @type {any} */ (req)[LAZY_CTX_KEY]
|
|
284
|
+
if (cached) return cached
|
|
285
|
+
/** 첫 트랩에서 실 ctx 를 만들고 이후 재사용 (getHttpCtx 가 req 단위 캐시를 겸한다). */
|
|
286
|
+
const real = () => getHttpCtx({ app, req, reply })
|
|
287
|
+
const proxy = new Proxy(
|
|
288
|
+
{},
|
|
289
|
+
{
|
|
290
|
+
get(_t, prop) {
|
|
291
|
+
return Reflect.get(real(), prop)
|
|
292
|
+
},
|
|
293
|
+
set(_t, prop, value) {
|
|
294
|
+
return Reflect.set(real(), prop, value)
|
|
295
|
+
},
|
|
296
|
+
has(_t, prop) {
|
|
297
|
+
return Reflect.has(real(), prop)
|
|
298
|
+
},
|
|
299
|
+
ownKeys() {
|
|
300
|
+
return Reflect.ownKeys(real())
|
|
301
|
+
},
|
|
302
|
+
getOwnPropertyDescriptor(_t, prop) {
|
|
303
|
+
return Object.getOwnPropertyDescriptor(real(), prop)
|
|
304
|
+
},
|
|
305
|
+
defineProperty(_t, prop, desc) {
|
|
306
|
+
return Reflect.defineProperty(real(), prop, desc)
|
|
307
|
+
},
|
|
308
|
+
deleteProperty(_t, prop) {
|
|
309
|
+
return Reflect.deleteProperty(real(), prop)
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
;/** @type {any} */ (req)[LAZY_CTX_KEY] = proxy
|
|
314
|
+
return proxy
|
|
315
|
+
}
|
package/src/core/envelope.js
CHANGED
|
@@ -22,13 +22,20 @@ export const REPLY_START_SYMBOL = Symbol('mega.reply.start')
|
|
|
22
22
|
export const ENVELOPE_MARK = Symbol('mega.envelope')
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* envelope 객체에
|
|
25
|
+
* envelope 객체에 마킹을 부착한다.
|
|
26
|
+
*
|
|
27
|
+
* 심볼 **직접 대입** (G1 M-1, ADR-214) — `Object.defineProperty` 대비 17배 빠르고(136→8 ns/op 실측)
|
|
28
|
+
* allocation 도 작다. 심볼 키는 어차피 `JSON.stringify`/`Object.keys` 에 안 나오므로 와이어·열거
|
|
29
|
+
* 동작은 동일하다. 의미 차이는 하나 — spread 복사(`{...env}`)가 마크를 함께 복사한다. 복사본도
|
|
30
|
+
* "이미 envelope" 로 취급되는데, 이는 이중 wrap 방지(ADR-184)에 오히려 부합한다(복사 후 필드를
|
|
31
|
+
* 고쳐 재전송하는 패턴도 envelope 그대로 나간다 — 의도된 동작으로 확정, ADR-214).
|
|
32
|
+
*
|
|
26
33
|
* @template {Record<string, any>} T
|
|
27
34
|
* @param {T} env
|
|
28
35
|
* @returns {T} 같은 객체 (마킹됨).
|
|
29
36
|
*/
|
|
30
37
|
function markEnvelope(env) {
|
|
31
|
-
|
|
38
|
+
;/** @type {any} */ (env)[ENVELOPE_MARK] = true
|
|
32
39
|
return env
|
|
33
40
|
}
|
|
34
41
|
|
package/src/core/hub-link.js
CHANGED
|
@@ -189,7 +189,9 @@ export class MegaHubLink {
|
|
|
189
189
|
/**
|
|
190
190
|
* hub 연결 + REGISTER 핸드셰이크. register_ok 수신 시 resolve.
|
|
191
191
|
*
|
|
192
|
-
*
|
|
192
|
+
* 초기 연결은 retry 유무와 무관하게 **단 1회** 시도한다. `retry` 옵션이 있으면 실패 시 백그라운드
|
|
193
|
+
* 재연결(지수 백오프)로 전환하고 reject 한다 — 성공 시 RECONNECTED, 소진 시 RECONNECT_FAILED emit.
|
|
194
|
+
* 호출부(boot)는 reject 를 warn 으로 받고 부팅을 계속한다(허브 다운이 부팅을 막지 않는 계약).
|
|
193
195
|
*
|
|
194
196
|
* @param {Object} [opts]
|
|
195
197
|
* @param {number} [opts.connectTimeoutMs] - register_ok 대기 한도 override(M4). **이 값은 인스턴스에
|
|
@@ -214,19 +216,27 @@ export class MegaHubLink {
|
|
|
214
216
|
throw err
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
|
-
// retry 활성 —
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
219
|
+
// retry 활성 — 초기 연결은 **단 1회**만 시도하고, 실패하면 백그라운드 재연결로 전환한 뒤 throw 한다.
|
|
220
|
+
// 예전엔 withRetry 가 최초 연결까지 백오프 전체를 await 해 호출부(boot 의 cluster-transport step)가
|
|
221
|
+
// 분 단위로 블로킹됐다(crud retry 기준 약 4.7분) — "허브 다운이어도 boot 를 막지 않는다" 는 호출부
|
|
222
|
+
// 계약과 정반대 동작. 이제 호출부는 즉시 실패를 보고(warn) 부팅을 계속하고, 백그라운드 _reconnect 가
|
|
223
|
+
// 같은 retry 백오프로 재시도한다 — 성공 시 RECONNECTED 이벤트가 presence 재동기화를 트리거하고,
|
|
224
|
+
// 소진 시 RECONNECT_FAILED 를 emit 한다. 치명 에러(인증 거부 등 fatal)는 재시도 무의미라 그대로 전파.
|
|
225
|
+
try {
|
|
226
|
+
return await this._connectOnce({ connectTimeoutMs: timeout })
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// 레거시 hub 폴백(1회) — protocolVersion 필드 거부 판정 후 v1 register 로 즉시 재시도.
|
|
229
|
+
if (err instanceof Error && err.message === 'hub.legacy_register_fallback') {
|
|
230
|
+
try {
|
|
231
|
+
return await this._connectOnce({ connectTimeoutMs: timeout })
|
|
232
|
+
} catch (err2) {
|
|
233
|
+
if (retryUnlessFatal({ error: err2 })) void this._reconnect()
|
|
234
|
+
throw err2
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (retryUnlessFatal({ error: err })) void this._reconnect()
|
|
238
|
+
throw err
|
|
239
|
+
}
|
|
230
240
|
}
|
|
231
241
|
|
|
232
242
|
/**
|
package/src/core/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { buildErrorHandler } from './error-mapper.js'
|
|
|
16
16
|
export { ajvErrorToValidationError, MAX_VALIDATION_DETAILS } from './ajv-mapper.js'
|
|
17
17
|
export { loadAndValidateConfig } from './config-loader.js'
|
|
18
18
|
// 요청 ctx 빌더 — db/cache/bus 접근자 배선 (ADR-102)
|
|
19
|
-
export { buildHttpCtx, getHttpCtx, buildAdapterAccessors } from './ctx-builder.js'
|
|
19
|
+
export { buildHttpCtx, getHttpCtx, getLazyHttpCtx, buildAdapterAccessors } from './ctx-builder.js'
|
|
20
20
|
// WS envelope + 컨트롤러 베이스 (ADR-015 / ADR-074)
|
|
21
21
|
export {
|
|
22
22
|
createWsMessage,
|