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.
Files changed (95) hide show
  1. package/README.md +9 -0
  2. package/package.json +3 -3
  3. package/sample/crud/.env +9 -0
  4. package/sample/crud/.env.example +9 -0
  5. package/sample/crud/apps/main/controllers/upload-controller.js +28 -5
  6. package/sample/crud/apps/main/locales/server/en.json +12 -1
  7. package/sample/crud/apps/main/locales/server/ko.json +12 -1
  8. package/sample/crud/apps/main/routes/upload.js +20 -1
  9. package/sample/crud/apps/main/services/guide-service.js +4 -3
  10. package/sample/crud/apps/main/services/upload-demo-service.js +6 -5
  11. package/sample/crud/apps/main/views/upload/index.ejs +4 -1
  12. package/sample/crud/docs/guide/01-cli.md +587 -0
  13. package/sample/crud/docs/guide/02-router-controller.md +497 -0
  14. package/sample/crud/docs/guide/03-service-model-db.md +929 -0
  15. package/sample/crud/docs/guide/04-websocket-asp.md +632 -0
  16. package/sample/crud/docs/guide/05-scheduler-job-worker.md +400 -0
  17. package/sample/crud/docs/guide/06-config-auth-session-security.md +495 -0
  18. package/sample/crud/docs/guide/07-view-i18n-static-multipart.md +462 -0
  19. package/sample/crud/docs/guide/08-observability.md +373 -0
  20. package/sample/crud/mega.config.js +7 -0
  21. package/sample/crud/package.json +2 -2
  22. package/sample/crud/scripts/start-ws-hub.sh +18 -4
  23. 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
  24. 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
  25. package/sample/crud/var/uploads/00_b-mqbnh7lc-c9790ab8.png +0 -0
  26. package/sample/crud/var/uploads/2025-07-22 13 35 56-mqbngvvk-e545e90e.png +0 -0
  27. package/sample/crud/var/uploads/big-file-mqbo1h9e-2957eaf5.png +0 -0
  28. 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
  29. package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  30. package/src/adapters/adapter-options.js +14 -3
  31. package/src/adapters/file-adapter.js +9 -5
  32. package/src/adapters/file-session-adapter.js +4 -3
  33. package/src/adapters/maria-adapter.js +7 -4
  34. package/src/adapters/mega-cache-adapter.js +83 -6
  35. package/src/adapters/mega-db-adapter.js +4 -1
  36. package/src/adapters/mongo-adapter.js +21 -7
  37. package/src/adapters/postgres-adapter.js +8 -4
  38. package/src/adapters/redis-adapter.js +7 -3
  39. package/src/adapters/sqlite-adapter.js +6 -2
  40. package/src/cli/commands/console-cmd.js +3 -1
  41. package/src/cli/commands/scaffold.js +38 -2
  42. package/src/cli/generators/index.js +58 -1
  43. package/src/cli/index.js +88 -59
  44. package/src/cli/watch.js +188 -0
  45. package/src/core/ajv-mapper.js +3 -1
  46. package/src/core/ctx-builder.js +59 -1
  47. package/src/core/envelope.js +9 -2
  48. package/src/core/hub-link.js +24 -14
  49. package/src/core/index.js +1 -1
  50. package/src/core/mega-app.js +55 -45
  51. package/src/core/pipeline.js +8 -6
  52. package/src/core/scope-registry.js +1 -0
  53. package/src/core/security.js +3 -3
  54. package/src/core/session-store.js +14 -1
  55. package/src/core/ws-presence.js +17 -5
  56. package/src/core/ws-roster.js +49 -10
  57. package/src/core/ws-upgrade.js +105 -0
  58. package/src/lib/mega-circuit-breaker.js +5 -3
  59. package/src/lib/mega-health.js +10 -0
  60. package/src/lib/mega-job-queue.js +53 -13
  61. package/src/lib/mega-job.js +8 -1
  62. package/src/lib/mega-metrics.js +28 -1
  63. package/src/lib/mega-plugin.js +2 -2
  64. package/src/lib/mega-worker.js +28 -5
  65. package/src/lib/ws-hub.js +90 -9
  66. package/templates/adr/code.tpl +23 -0
  67. package/types/adapters/adapter-options.d.ts +2 -0
  68. package/types/adapters/file-adapter.d.ts +12 -1
  69. package/types/adapters/file-session-adapter.d.ts +4 -2
  70. package/types/adapters/maria-adapter.d.ts +5 -3
  71. package/types/adapters/mega-cache-adapter.d.ts +27 -1
  72. package/types/adapters/mega-db-adapter.d.ts +4 -1
  73. package/types/adapters/mongo-adapter.d.ts +13 -2
  74. package/types/adapters/postgres-adapter.d.ts +4 -2
  75. package/types/adapters/redis-adapter.d.ts +8 -0
  76. package/types/adapters/sqlite-adapter.d.ts +8 -2
  77. package/types/cli/generators/index.d.ts +11 -1
  78. package/types/cli/index.d.ts +12 -27
  79. package/types/cli/watch.d.ts +59 -0
  80. package/types/core/ctx-builder.d.ts +23 -0
  81. package/types/core/hub-link.d.ts +3 -1
  82. package/types/core/index.d.ts +1 -1
  83. package/types/core/mega-app.d.ts +1 -1
  84. package/types/core/pipeline.d.ts +2 -1
  85. package/types/core/security.d.ts +3 -3
  86. package/types/core/session-store.d.ts +7 -0
  87. package/types/core/ws-roster.d.ts +13 -1
  88. package/types/core/ws-upgrade.d.ts +29 -0
  89. package/types/lib/mega-circuit-breaker.d.ts +4 -2
  90. package/types/lib/mega-health.d.ts +7 -0
  91. package/types/lib/mega-job-queue.d.ts +16 -4
  92. package/types/lib/mega-job.d.ts +8 -1
  93. package/types/lib/mega-plugin.d.ts +1 -1
  94. package/types/lib/mega-worker.d.ts +3 -1
  95. package/types/lib/ws-hub.d.ts +27 -2
@@ -96,29 +96,16 @@ export function resolveDb(setting: string | boolean | undefined | null): string
96
96
  */
97
97
  export function shouldReexecForWatch(flags: Record<string, string | boolean>, env: Record<string, string | undefined>): boolean;
98
98
  /**
99
- * `mega start --watch` `node --watch <self> start … --cluster 1` 재실행 명령을 빌드한다 (ADR-182).
99
+ * `mega start --watch` supervisor **자식 인자**를 빌드한다 (ADR-220, ADR-182 개정).
100
100
  *
101
- * - `--watch` 인자는 **node 플래그**로 옮기고 mega 인자에선 제거한다.
101
+ * - `--watch`/`--watch-ignore`(값 포함)는 부모(supervisor) 전용 자식 인자에서 제거한다
102
+ * (가드 env 와 이중 안전으로 무한재귀 방지).
102
103
  * - **단일 프로세스 강제**(`--cluster 1`) — 멀티워커 watch 카오스 방지(기존 `--cluster` 값은 무시).
103
- * - watchPaths 가 있으면 `--watch-path=…`(이게 모듈 그래프 watch 를 **대체**하므로 앱 소스·전역설정 경로를
104
- * 명시해야 한다 실측 확인). 없으면 plain `--watch`(모듈 그래프 폴백).
105
- *
106
- * @param {Object} o
107
- * @param {string[]} o.argv - 원본 mega argv(예: `['start','--watch','--port','3000']`).
108
- * @param {string[]} o.watchPaths - 감시할 절대경로(존재하는 것만).
109
- * @param {string} o.selfPath - mega 진입 스크립트(`process.argv[1]`).
110
- * @param {string} o.execPath - node 바이너리(`process.execPath`).
111
- * @returns {{ command: string, args: string[] }}
112
- */
113
- export function buildWatchCommand({ argv, watchPaths, selfPath, execPath }: {
114
- argv: string[];
115
- watchPaths: string[];
116
- selfPath: string;
117
- execPath: string;
118
- }): {
119
- command: string;
120
- args: string[];
121
- };
104
+ *
105
+ * @param {string[]} argv - 원본 mega argv(예: `['start','--watch','--port','3000']`).
106
+ * @returns {string[]} 자식 `mega` 인자(예: `['start','--port','3000','--cluster','1']`).
107
+ */
108
+ export function buildWatchChildArgs(argv: string[]): string[];
122
109
  /**
123
110
  * `mega` CLI 진입점. 파싱 → 명령 분기. **이 함수는 process.exit 를 호출하지 않는다**(테스트 가능) —
124
111
  * exit code 를 반환하고, bin 래퍼가 `process.exitCode` 로 반영한다.
@@ -178,24 +165,24 @@ export function collectRegistrations(global: Object, host: import("../lib/mega-p
178
165
  * 등록 소스 = `config.jobs` + 플러그인 `mega.jobs.register` 분(`collectRegistrations`, ADR-123).
179
166
  * @param {string} projectRoot
180
167
  * @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
181
- * @returns {Promise<MegaJobWorker>}
168
+ * @returns {Promise<import('../lib/mega-job-worker.js').MegaJobWorker>}
182
169
  */
183
170
  export function runWorkerHost(projectRoot: string, logger?: {
184
171
  debug?: Function;
185
172
  info?: Function;
186
173
  warn?: Function;
187
- }): Promise<MegaJobWorker>;
174
+ }): Promise<import("../lib/mega-job-worker.js").MegaJobWorker>;
188
175
  /**
189
176
  * `mega scheduler` 호스트 골격 — config + 어댑터 connect + ctx + `MegaScheduler` 인스턴스 + graceful.
190
177
  * @param {string} projectRoot
191
178
  * @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
192
- * @returns {Promise<MegaScheduler>}
179
+ * @returns {Promise<import('../lib/mega-schedule.js').MegaScheduler>}
193
180
  */
194
181
  export function runSchedulerHost(projectRoot: string, logger?: {
195
182
  debug?: Function;
196
183
  info?: Function;
197
184
  warn?: Function;
198
- }): Promise<MegaScheduler>;
185
+ }): Promise<import("../lib/mega-schedule.js").MegaScheduler>;
199
186
  /**
200
187
  * 마이그레이션 대상 DB 어댑터 해석 — `services.databases` 의 globalKey 로 조회한다. `--db <key>` 미지정
201
188
  * 시 유일 선언 db, 그게 아니면 `primary`, 둘 다 아니면(다중·미선언) fail-fast 로 명시를 요구한다.
@@ -245,5 +232,3 @@ export const BUILTIN_COMMANDS: Set<string>;
245
232
  /** CLI 사용법 텍스트 — commander 자동 생성(ADR-195) + 플러그인 명령 안내. 명령·옵션 정의가 곧 도움말의
246
233
  * 단일 정본이라 수기 USAGE 와의 드리프트가 없다. */
247
234
  export const USAGE: string;
248
- import { MegaJobWorker } from '../lib/mega-job-worker.js';
249
- import { MegaScheduler } from '../lib/mega-schedule.js';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 미니 glob → RegExp. 지원: 더블스타+슬래시(0개 이상의 디렉토리), 더블스타(경로 전체), `*`(세그먼트
3
+ * 내), `?`(1글자). 그 외 문자는 리터럴(정규식 특수문자 escape). 매칭 대상은 루트 상대 POSIX 경로
4
+ * 전체(^…$). 패턴 예시는 스캐폴드 `.env` 의 `WATCH_IGNORE` 참조 — JS 블록 주석 안에는
5
+ * 더블스타+슬래시 시퀀스를 쓸 수 없어(주석 조기 종료) 여기 직접 못 적는다.
6
+ *
7
+ * @param {string} pattern - glob 패턴(예 `.env.*`).
8
+ * @returns {RegExp}
9
+ */
10
+ export function globToRegExp(pattern: string): RegExp;
11
+ /**
12
+ * ignore 패턴 합성 — config(`watch.ignore`) + CLI(`--watch-ignore a,b`). 숨은 디폴트 없음(모듈
13
+ * docstring 참조). 잘못된 모양은 fail-fast(silent 무시 금지).
14
+ *
15
+ * @param {{ ignore?: unknown }} [watchConfig] - mega.config.js 의 `watch`.
16
+ * @param {string | undefined} [cliIgnore] - `--watch-ignore` 값(콤마 구분).
17
+ * @returns {string[]}
18
+ * @throws {TypeError} config `watch.ignore` 가 문자열 배열이 아닐 때.
19
+ */
20
+ export function mergeWatchIgnore(watchConfig?: {
21
+ ignore?: unknown;
22
+ }, cliIgnore?: string | undefined): string[];
23
+ /**
24
+ * 루트 상대 경로가 ignore 에 걸리는지 (Boolean — `is*`).
25
+ * @param {string} rel - 루트 상대 POSIX 경로(예 `apps/main/locales/ko.json`).
26
+ * @param {RegExp[]} regexps - {@link globToRegExp} 컴파일 결과.
27
+ * @returns {boolean}
28
+ */
29
+ export function isIgnoredPath(rel: string, regexps: RegExp[]): boolean;
30
+ /**
31
+ * watch supervisor — 감시 경로의 변경(ignore 통과분)을 debounce 후 자식(`mega start`)을
32
+ * SIGTERM → respawn 한다. 자식이 스스로 죽으면(crash) 재시작하지 않고 다음 변경을 기다린다
33
+ * (crash-loop 방지 — nodemon 의 "app crashed - waiting for file changes" 와 동형).
34
+ *
35
+ * @param {object} o
36
+ * @param {string} o.projectRoot
37
+ * @param {string[]} o.watchPaths - 감시할 절대경로(존재하는 것만 — 호출측이 필터).
38
+ * @param {string[]} o.ignore - glob 패턴 목록.
39
+ * @param {() => import('node:child_process').ChildProcess} o.spawnChild - 자식 기동 팩토리(주입 — 테스트).
40
+ * @param {(msg: string) => void} o.out
41
+ * @param {number} [o.debounceMs=500] - 연속 변경 디바운스.
42
+ * @param {(dir: string, opts: object, cb: (event: string, filename: string | Buffer | null) => void) => { close(): void }} [o.watchFn] -
43
+ * fs.watch 주입(테스트). 기본 node:fs watch.
44
+ * @returns {Promise<number>} 부모(supervisor)의 exit code — SIGINT/SIGTERM 수신 시 자식 정리 후 0.
45
+ */
46
+ export function startWatchSupervisor({ projectRoot, watchPaths, ignore, spawnChild, out, debounceMs, watchFn }: {
47
+ projectRoot: string;
48
+ watchPaths: string[];
49
+ ignore: string[];
50
+ spawnChild: () => import("node:child_process").ChildProcess;
51
+ out: (msg: string) => void;
52
+ debounceMs?: number;
53
+ watchFn?: (dir: string, opts: object, cb: (event: string, filename: string | Buffer | null) => void) => {
54
+ close(): void;
55
+ };
56
+ }): Promise<number>;
57
+ /** {@link startWatchSupervisor} 의 watchPaths 기본 집합 — 존재하는 것만.
58
+ * @param {string} projectRoot @returns {string[]} */
59
+ export function defaultWatchPaths(projectRoot: string): string[];
@@ -63,6 +63,29 @@ export function getHttpCtx({ app, req, reply }: {
63
63
  req: import("fastify").FastifyRequest;
64
64
  reply: import("fastify").FastifyReply;
65
65
  }): Record<string, any>;
66
+ /**
67
+ * **lazy** HTTP ctx — 실제 ctx 빌드를 첫 속성 접근 시점까지 미루는 요청당 프록시 (G1 H-1, ADR-214).
68
+ *
69
+ * hot path 단일 최대 프레임워크 비용이 "핸들러가 ctx 를 안 써도 매 요청 무조건 buildHttpCtx"
70
+ * (1,615 B + ≈0.7 µs/req, CPU 2.3% — audit-G1 실측)였다. 이 프록시는 생성 비용이 객체 1개 수준이고,
71
+ * 핸들러·미들웨어가 ctx 의 속성을 **처음 만질 때만** {@link getHttpCtx} 로 실 ctx 를 만든다(이후 캐시).
72
+ *
73
+ * 호환성: canonical 시그니처 `(req, reply, ctx)` 는 그대로 — 모든 핸들러가 여전히 3번째 인자를 받고,
74
+ * 참조하는 순간부터는 기존과 동일한 실 ctx 표면(ADR-134 요청당 공유 포함)이다. 프록시 자체도 요청당
75
+ * 1개로 캐시되어 미들웨어와 핸들러가 **동일 객체**(`===`)를 받는다. 모든 트랩이 실 ctx 로 위임되므로
76
+ * `in`/spread/`Object.keys`/getter·setter(`ctx.user=`) 동작이 보존된다.
77
+ *
78
+ * @param {object} args
79
+ * @param {import('./mega-app.js').MegaApp | null} args.app
80
+ * @param {import('fastify').FastifyRequest} args.req
81
+ * @param {import('fastify').FastifyReply} args.reply
82
+ * @returns {Record<string, any>}
83
+ */
84
+ export function getLazyHttpCtx({ app, req, reply }: {
85
+ app: import("./mega-app.js").MegaApp | null;
86
+ req: import("fastify").FastifyRequest;
87
+ reply: import("fastify").FastifyReply;
88
+ }): Record<string, any>;
66
89
  /**
67
90
  * - 앱의 `app.config.js` 별명 선언 (alias → globalKey).
68
91
  */
@@ -117,7 +117,9 @@ export class MegaHubLink {
117
117
  /**
118
118
  * hub 연결 + REGISTER 핸드셰이크. register_ok 수신 시 resolve.
119
119
  *
120
- * `retry` 옵션이 있으면 최초 연결도 지수 백오프로 재시도한다(ADR-098). 없으면 단발 시도.
120
+ * 초기 연결은 retry 유무와 무관하게 **단 1회** 시도한다. `retry` 옵션이 있으면 실패 시 백그라운드
121
+ * 재연결(지수 백오프)로 전환하고 reject 한다 — 성공 시 RECONNECTED, 소진 시 RECONNECT_FAILED emit.
122
+ * 호출부(boot)는 reject 를 warn 으로 받고 부팅을 계속한다(허브 다운이 부팅을 막지 않는 계약).
121
123
  *
122
124
  * @param {Object} [opts]
123
125
  * @param {number} [opts.connectTimeoutMs] - register_ok 대기 한도 override(M4). **이 값은 인스턴스에
@@ -14,7 +14,7 @@ export { bootApp, buildBootContext } from "./boot.js";
14
14
  export { wrapEnvelope, errorEnvelope, synthesizeEnvelopeResponseSchema, ENVELOPE_MARK } from "./envelope.js";
15
15
  export { buildHttpPipeline, wrapPreHandler, composeTransform, composeAfter } from "./pipeline.js";
16
16
  export { ajvErrorToValidationError, MAX_VALIDATION_DETAILS } from "./ajv-mapper.js";
17
- export { buildHttpCtx, getHttpCtx, buildAdapterAccessors } from "./ctx-builder.js";
17
+ export { buildHttpCtx, getHttpCtx, getLazyHttpCtx, buildAdapterAccessors } from "./ctx-builder.js";
18
18
  export { createWsMessage, validateWsMessage, parseWsMessage, generateMessageId, WS_MESSAGE_SCHEMA, WS_PROTOCOL_VERSION, WS_TYPE_PATTERN } from "./ws-message.js";
19
19
  export { MegaWsConnection, driveWsConnection, createPlainCodec, createAspCodec, rejectUpgrade, CLOSE_CODE_DECRYPT_FAILED, CLOSE_CODE_INTERNAL_ERROR } from "./ws-upgrade.js";
20
20
  export { buildPerMessageDeflate, checkCompressionConfig, COMPRESSION_DEFAULTS } from "./ws-compression.js";
@@ -35,7 +35,7 @@ export class MegaApp {
35
35
  * @param {Object|false} [opts.helmet] - fastify-helmet 옵션 (보안 헤더). false=미등록, undefined=디폴트 ON
36
36
  * (ADR-047/127). 라우트 옵션 오버라이드는 완전 교체(ADR-073).
37
37
  * @param {Object|false} [opts.cors] - fastify-cors 옵션. false=미등록, undefined=origin:false(교차출처 거부, 안전 디폴트).
38
- * @param {Object|false} [opts.rateLimit] - fastify-rate-limit 옵션. false=미등록, undefined=디폴트(IP당 100/min, ADR-048).
38
+ * @param {Object|false} [opts.rateLimit] - fastify-rate-limit 옵션. false=미등록, undefined=디폴트(IP당 1000/min, ADR-048/197).
39
39
  * 멀티 인스턴스 분산 카운팅은 `caches.rate` 별명(Redis), 미선언 시 in-memory 폴백.
40
40
  * @param {Object|false} [opts.csrf] - fastify-csrf-protection 옵션. false=미등록, undefined=디폴트 ON.
41
41
  * ADR-051: JSON 요청은 토큰 면제+Origin 검증, 폼 요청만 토큰 검증.
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * 미들웨어를 Fastify 가 async preHandler 로 인식하는 arity-2 래퍼로 감싸고, canonical ctx 를
3
3
  * 3번째 인자로 주입한다 — 핸들러·글로벌 미들웨어와 동일한 `(req, reply, ctx)` 계약(ADR-134/184).
4
- * getHttpCtx요청당 캐싱이라 같은 요청의 모든 미들웨어·핸들러가 동일 ctx 객체를 공유한다.
4
+ * ctx**lazy 프록시**(ADR-214) 미들웨어가 실제로 속성을 만질 때만 빌드되고, 요청당 캐싱이라
5
+ * 같은 요청의 모든 미들웨어·핸들러가 동일 ctx 객체를 공유한다.
5
6
  *
6
7
  * @param {Function} fn - `(req, reply, ctx?)` 미들웨어 (arity-2 도 하위 호환 — 3번째 인자 무시).
7
8
  * @param {import('./mega-app.js').MegaApp | null} [app] - ctx 의 어댑터·서비스 접근자 출처(없으면 null).
@@ -21,7 +21,7 @@
21
21
  * @param {string[]} [opts.hosts] - 앱 도메인 목록(CSRF Origin 검증 allowlist, ADR-051).
22
22
  * @param {Object|false} [opts.helmet] - fastify-helmet 옵션. false=미등록, undefined=디폴트 ON.
23
23
  * @param {Object|false} [opts.cors] - fastify-cors 옵션. false=미등록, undefined=origin:false(교차출처 거부).
24
- * @param {Object|false} [opts.rateLimit] - fastify-rate-limit 옵션. false=미등록, undefined=디폴트(100/min).
24
+ * @param {Object|false} [opts.rateLimit] - fastify-rate-limit 옵션. false=미등록, undefined=디폴트(1000/min, ADR-214).
25
25
  * @param {Object|false} [opts.csrf] - fastify-csrf-protection 옵션. false=미등록, undefined=디폴트 ON.
26
26
  * @param {{ masterSecret?: string, http?: { enabledPaths?: string[], driftMs?: number, headerSignal?: string, timestampHeader?: string, nonceCache?: any } }} [opts.asp] -
27
27
  * ASP 설정. `masterSecret` + `http.enabledPaths`(비어있지 않음)가 둘 다 있을 때만 HTTP ASP 등록.
@@ -53,9 +53,9 @@ export function registerSecurityPlugins(fastify: import("fastify").FastifyInstan
53
53
  warn?: Function;
54
54
  };
55
55
  }): SecuritySummary;
56
- /** rate-limit 기본값 (ADR-048: IP 당 100 req/min). 사용자 config 로 완전 교체(ADR-073). */
56
+ /** rate-limit 기본값 (IP 당 1000 req/min — ADR-048 의 100/min 은 평상 트래픽도 429 를 만드는 운영 함정이라 상향, ADR-214). 사용자 config 로 완전 교체(ADR-073). */
57
57
  export const DEFAULT_RATE_LIMIT: Readonly<{
58
- max: 100;
58
+ max: 1000;
59
59
  timeWindow: "1 minute";
60
60
  }>;
61
61
  /**
@@ -14,5 +14,12 @@ export function createSessionStore(storeConfig: {
14
14
  }, defaults?: {
15
15
  ttlMs?: number;
16
16
  }): import("../adapters/mega-session-adapter.js").MegaSessionAdapter;
17
+ /**
18
+ * file driver 만료 스캔 cleanup 기본 주기(1시간, ms) — ADR-046 의 "file 모드 cleanup 자동" 약속 구현(ADR-215).
19
+ * 어댑터 자체 디폴트는 0(off)이라, 팩토리 경유 생성(=프레임워크 부팅 경로)에서만 기본 주기를 주입한다.
20
+ * 만료 파일의 lazy 삭제는 같은 sid 재접근 시에만 일어나므로, 주기 스캔이 없으면 미접근 만료 세션이
21
+ * 디스크에 무한 누적된다(G5 audit M-3). `cleanupIntervalMs: 0` 명시로 옵트아웃(외부 cron/scheduler 운용 시).
22
+ */
23
+ export const DEFAULT_FILE_CLEANUP_INTERVAL_MS: 3600000;
17
24
  /** 지원 세션 driver 목록(테스트·문서용). */
18
25
  export const SESSION_STORE_DRIVERS: readonly string[];
@@ -57,6 +57,15 @@ export class MegaWsRedisRoster {
57
57
  * @param {string} channel @param {string} sessionId @param {RosterMember} member @returns {Promise<void>}
58
58
  */
59
59
  add(channel: string, sessionId: string, member: RosterMember): Promise<void>;
60
+ /**
61
+ * ioredis pipeline.exec() 결과(`[err, res][]`)에서 첫 명령 오류를 throw 한다 — pipeline 은 명령별
62
+ * 오류를 reject 가 아니라 결과 배열에 싣기 때문에, 묵히면 호출부의 기존 실패 처리(catch+warn)가
63
+ * 더는 동작하지 않는다(직렬 await 시절의 throw 계약 유지).
64
+ * @param {Array<[Error | null, any]> | null} results
65
+ * @returns {void}
66
+ * @private
67
+ */
68
+ private _throwFirstPipelineError;
60
69
  /**
61
70
  * 채널 roster 에서 멤버 제거(disconnect).
62
71
  * @param {string} channel @param {string} sessionId @returns {Promise<void>}
@@ -78,7 +87,10 @@ export class MegaWsRedisRoster {
78
87
  */
79
88
  startHeartbeat(): void;
80
89
  /**
81
- * 로컬 멤버 전체를 다시 add(=expiresAt 갱신). @returns {Promise<void>} @private
90
+ * 로컬 멤버 전체의 `expiresAt` 을 한 pipeline 으로 일괄 갱신. 직렬 add 루프(멤버당 2 RT) 멤버
91
+ * 1,000 에 주기당 수백 ms — 갱신 지연이 TTL 윈도를 침식해 살아있는 세션이 lazy 만료될 수 있었다.
92
+ * pipeline 이면 단일 왕복 수준(실측 ~200×). 채널 키 PEXPIRE 는 채널당 1회만 싣는다.
93
+ * @returns {Promise<void>} @private
82
94
  */
83
95
  private _refreshLocal;
84
96
  /**
@@ -1,3 +1,23 @@
1
+ /**
2
+ * 프로세스 합산 send budget 을 조정한다(ADR-215). 부팅/운영 튜닝 진입점.
3
+ * @param {{ maxTotalBufferedBytes?: number }} [opts] - 바이트 budget. `0` = 무제한(옵트아웃).
4
+ * @returns {{ maxTotalBufferedBytes: number }} 적용된 현재 값.
5
+ * @throws {TypeError} 음수/비숫자 — 설정 실수 fail-fast.
6
+ */
7
+ export function configureWsSendBudget({ maxTotalBufferedBytes: max }?: {
8
+ maxTotalBufferedBytes?: number;
9
+ }): {
10
+ maxTotalBufferedBytes: number;
11
+ };
12
+ /**
13
+ * 테스트 격리용 — budget 상태 초기화(추적 Set·스로틀·budget 디폴트 복원).
14
+ * @returns {void}
15
+ */
16
+ export function _resetWsSendBudget(): void;
17
+ /** 테스트용 — 연결을 budget 추적에 등록. @param {MegaWsConnection} conn @returns {void} */
18
+ export function _trackWsSendBudget(conn: MegaWsConnection): void;
19
+ /** 테스트용 — 스로틀 우회 가능한 스윕 직접 호출. @param {number} [now] @returns {void} */
20
+ export function _sweepWsSendBudget(now?: number): void;
1
21
  /**
2
22
  * 라우트 opts 의 `heartbeatMs` 를 검증해 반환한다. 미지정 → 기본 30초, `0` = liveness 끔.
3
23
  * 무효값(음수/비정수/비숫자)은 운영 실수 — 조용히 보정하지 않고 warn 로그 후 기본값을 쓴다
@@ -116,6 +136,15 @@ export const CLOSE_CODE_SLOW_CONSUMER: 1013;
116
136
  export const CLOSE_CODE_REQUEUE: 4503;
117
137
  /** send 버퍼(bufferedAmount) 기본 상한(바이트). 초과 = 소비자가 ack 를 못 따라옴 → 연결 종료(서버 OOM 방지). */
118
138
  export const DEFAULT_MAX_BUFFERED_BYTES: number;
139
+ /**
140
+ * 프로세스 **합산** send 버퍼 기본 budget(바이트, ADR-215 — G5 audit M-6).
141
+ *
142
+ * per-conn 상한(위 16MiB)은 연결 1개의 OOM 방어일 뿐이라, 느린 소비자 N 개가 각자 cap 직전까지
143
+ * 쌓으면 합산은 무제한이었다(이론상 1,000개 × 16MiB = 16GB). 합산이 본 budget 을 넘으면 **가장 큰
144
+ * 송신 큐를 보유한 연결부터** 1013(slow consumer)으로 종료해 budget 아래로 회수한다.
145
+ * `configureWsSendBudget({ maxTotalBufferedBytes: 0 })` 으로 무제한 옵트아웃.
146
+ */
147
+ export const DEFAULT_MAX_TOTAL_BUFFERED_BYTES: number;
119
148
  /**
120
149
  * 클라↔bridge ping/pong liveness 기본 주기(ms) — 30초. 주기마다 ping 을 보내고 직전 주기의 pong 이
121
150
  * 없으면 half-open(상대 사망·네트워크 단절) 으로 보고 terminate 한다 — 좀비 연결이 OS TCP 타임아웃까지
@@ -182,8 +182,10 @@ export type MegaCircuitBreakerOptions = {
182
182
  */
183
183
  rollingCountBuckets?: number;
184
184
  /**
185
- * - 이 횟수만큼 호출이 쌓이기 전엔 실패율이 높아도 open 안 함
186
- * (표본 부족으로 인한 조기 trip 방지). 0=비활성.
185
+ * - 롤링 윈도우에 이 횟수만큼 호출이 쌓이기 전엔 실패율이
186
+ * 높아도 open 안 함(표본 부족 조기 trip 방지). 0=비활성. ⚠️ opossum 정본값(0)과 다른 프레임워크
187
+ * 디폴트 — 0 이면 부팅 직후/저빈도 호출에서 **첫 실패 1건**이 실패율 100% 가 돼 즉시 30s 차단되는
188
+ * 풋건이라 5 로 올렸다. opossum 원 동작이 필요하면 명시적으로 0 을 지정.
187
189
  */
188
190
  volumeThreshold?: number;
189
191
  /**
@@ -12,6 +12,13 @@ export function register(name: string, fn: () => Promise<{
12
12
  }, opts?: {
13
13
  timeoutMs?: number;
14
14
  }): void;
15
+ /**
16
+ * 등록된 체크 제거(이름 기준). 닫힌 자원(예: hub link)의 체크가 stale 클로저로 남지 않게
17
+ * 소유자가 register 와 짝맞춰 부른다.
18
+ * @param {string} name
19
+ * @returns {boolean} 제거됐으면 true(미등록 이름이면 false).
20
+ */
21
+ export function unregister(name: string): boolean;
15
22
  /**
16
23
  * 모든 체크 실행 (병렬). 하나라도 false 면 전체 ok=false.
17
24
  * @returns {Promise<{ ok: boolean, checks: Record<string, { ok: boolean, error?: string, [key: string]: any }> }>}
@@ -12,6 +12,8 @@ export const DEFAULT_RUN_TIMEOUT_MS: number;
12
12
  * 하트비트가 이 값의 절반마다 lease 를 갱신한다.
13
13
  * @property {number} [maxDeliver=5] - JetStream 최대 전달 횟수(워커 크래시 재전달 backstop).
14
14
  * @property {number} [heartbeatMs] - `working()` 전송 주기(ms). 기본 `max(1000, ackWaitMs/2)`.
15
+ * 양의 정수 + `< ackWaitMs` 필수(생성자 fail-fast) — 이상이면 lease 갱신이 늦어 정상 처리 중
16
+ * 중복 재전달, 0 이하면 working() 플러딩.
15
17
  * @property {string} [streamPrefix='MEGA_JOBS'] - 스트림 이름 접두사.
16
18
  * @property {number} [dlqMaxAgeMs=604800000] - DLQ 스트림 메시지 보존 기한(ms, 디폴트 7일). 초과한 실패
17
19
  * 잡은 NATS 가 자동 만료시킨다(무한 적재 방지, ADR-134). `0` 이면 무제한(끔 — 영구 보존). **신규 DLQ
@@ -73,10 +75,18 @@ export class MegaJobQueue extends EventEmitter<any> {
73
75
  */
74
76
  ensureStream(JobClass: typeof MegaJob): Promise<void>;
75
77
  /**
76
- * 잡을 큐에 넣는다(JetStream publish — 서버에 영속 저장). 스트림이 없으면 만든다.
77
- * @param {typeof MegaJob} JobClass @param {any} payload @returns {Promise<{ seq: number, stream: string, duplicate: boolean }>}
78
- */
79
- enqueue(JobClass: typeof MegaJob, payload: any): Promise<{
78
+ * 잡을 큐에 넣는다(JetStream publish — 서버에 영속 저장). 스트림이 없으면 만든다(subject 별 1회 확인 후 캐시).
79
+ *
80
+ * @param {typeof MegaJob} JobClass @param {any} payload
81
+ * @param {{ msgID?: string }} [opts] - `msgID` = JetStream `Nats-Msg-Id` dedup 키(옵트인). 스트림
82
+ * duplicate window(NATS 기본 2분 — 운영자가 NATS CLI 로 스트림별 조정) 안의 같은 msgID 재발행은
83
+ * 적재되지 않고 `duplicate: true` 로 반환된다. 비즈니스 멱등키(주문 ID 등)를 권장. 미지정 시
84
+ * dedup 없음 — `duplicate` 는 항상 false.
85
+ * @returns {Promise<{ seq: number, stream: string, duplicate: boolean }>}
86
+ */
87
+ enqueue(JobClass: typeof MegaJob, payload: any, { msgID }?: {
88
+ msgID?: string;
89
+ }): Promise<{
80
90
  seq: number;
81
91
  stream: string;
82
92
  duplicate: boolean;
@@ -129,6 +139,8 @@ export type MegaJobQueueOptions = {
129
139
  maxDeliver?: number;
130
140
  /**
131
141
  * - `working()` 전송 주기(ms). 기본 `max(1000, ackWaitMs/2)`.
142
+ * 양의 정수 + `< ackWaitMs` 필수(생성자 fail-fast) — 이상이면 lease 갱신이 늦어 정상 처리 중
143
+ * 중복 재전달, 0 이하면 working() 플러딩.
132
144
  */
133
145
  heartbeatMs?: number;
134
146
  /**
@@ -92,7 +92,14 @@ export class MegaJob {
92
92
  static subject: string | undefined;
93
93
  /** @type {string|undefined} bus 별명(`ctx.bus(alias)`). 워커 배선이 nc 해석에 사용. */
94
94
  static bus: string | undefined;
95
- /** @type {number} 동시 처리 메시지 수(consumer max_ack_pending). 기본 1(순차·안전). */
95
+ /**
96
+ * @type {number} 동시 처리 메시지 수. 기본 1(순차·안전).
97
+ *
98
+ * ⚠️ 이 값은 durable consumer 의 `max_ack_pending` 으로 들어가므로 **워커 그룹(같은 subject 를
99
+ * 소비하는 모든 인스턴스) 전체의 합산 in-flight 상한**이다 — `mega worker` 인스턴스를 늘려도
100
+ * 합산 동시 처리는 이 값을 넘지 못한다. 처리량을 늘리려면 인스턴스 증설과 **함께** concurrency 를
101
+ * 키워야 한다(실측: c=1→c=32 에서 처리량 ~10배).
102
+ */
96
103
  static concurrency: number;
97
104
  /** @type {number} run 실패 시 **추가** 재시도 횟수(첫 시도 제외). 기본 3(OQ-012). */
98
105
  static retries: number;
@@ -53,7 +53,7 @@ export const CORE_API_VERSION: string;
53
53
  * @property {Array<{ path: string, template: string }>} files
54
54
  * @property {string} [description] - `mega help` 가 병기하는 한 줄 설명(선택).
55
55
  */
56
- /** 빌트인 generator 13종 이름 — 플러그인 scaffold 가 점유 금지(빌트인이 우선이라 silent shadow 가 됨).
56
+ /** 빌트인 generator 이름 — 플러그인 scaffold 가 점유 금지(빌트인이 우선이라 silent shadow 가 됨).
57
57
  * cli/generators 의 `GENERATOR_KINDS` 와 동일해야 한다(레이어 역전 회피를 위해 미러 — 동기화는
58
58
  * mega-plugin 단위 테스트가 GENERATOR_KINDS 와 집합 비교로 강제한다). */
59
59
  export const RESERVED_GENERATOR_NAMES: Set<string>;
@@ -37,8 +37,10 @@ export class MegaWorker extends EventEmitter<any> {
37
37
  get mode(): "thread" | "process";
38
38
  /** @returns {number} 풀 크기. */
39
39
  get poolSize(): number;
40
- /** @returns {number} crash 교체 허용 총량. */
40
+ /** @returns {number} 윈도우({@link MegaWorker#restartWindowMs}) 내 crash 교체 허용 횟수. */
41
41
  get maxRestarts(): number;
42
+ /** @returns {number} crash 교체 판정 슬라이딩 윈도우(ms). `static restartWindowMs` 로 조정. */
43
+ get restartWindowMs(): number;
42
44
  /** @returns {boolean} start() 후 stop() 전이면 true. */
43
45
  get isStarted(): boolean;
44
46
  /** @param {'dispatch'|'done'|'fail'|'crash'|'stopped'} event @param {(payload: any) => void} listener @returns {this} */
@@ -69,11 +69,12 @@ export class MegaWsHub {
69
69
  _wss: WebSocketServer | null;
70
70
  /** heartbeat liveness 체크 interval (M3). @type {ReturnType<typeof setInterval> | null} */
71
71
  _livenessTimer: ReturnType<typeof setInterval> | null;
72
- /** 등록된 bridge 연결. connId → { socket, lastSeen, protocolVersion }. @type {Map<string, { socket: import('ws').WebSocket, lastSeen: number, protocolVersion?: number }>} */
72
+ /** 등록된 bridge 연결. connId → { socket, lastSeen, protocolVersion, presenceFanout }. @type {Map<string, { socket: import('ws').WebSocket, lastSeen: number, protocolVersion?: number, presenceFanout?: boolean }>} */
73
73
  _bridges: Map<string, {
74
74
  socket: import("ws").WebSocket;
75
75
  lastSeen: number;
76
76
  protocolVersion?: number;
77
+ presenceFanout?: boolean;
77
78
  }>;
78
79
  /** presence. sessionId → { bridgeConnId, userId, channels:Set, metadata }. @type {Map<string, { bridgeConnId: string, userId: string, channels: Set<string>, metadata: Object }>} */
79
80
  _sessions: Map<string, {
@@ -86,6 +87,13 @@ export class MegaWsHub {
86
87
  _channelSessions: Map<string, Set<string>>;
87
88
  /** userId → sessionId 집합 (DIRECT fan-out, ADR-035). @type {Map<string, Set<string>>} */
88
89
  _userSessions: Map<string, Set<string>>;
90
+ /**
91
+ * 역인덱스: channel → (bridgeConnId → 그 bridge 의 채널 내 세션 수). BROADCAST 의 bridge 선정을
92
+ * 세션 수 무관 O(bridge 수)로 만든다 — 멤버 10k 채널에서 메시지당 수백 µs 가 µs 대로 떨어진다.
93
+ * `_addSession`/`_removeSession` 이 증분 유지(카운트 0 → 키 제거), 메모리는 채널×bridge 수준.
94
+ * @type {Map<string, Map<string, number>>}
95
+ */
96
+ _channelBridges: Map<string, Map<string, number>>;
89
97
  /** hub 식별자. */
90
98
  get hubId(): string;
91
99
  /** 등록된 bridge 수 (테스트/관측용). */
@@ -148,6 +156,12 @@ export class MegaWsHub {
148
156
  * @private
149
157
  */
150
158
  private _addSession;
159
+ /**
160
+ * channel→bridge 역인덱스 증분 갱신. 카운트가 0 이 되면 키를 지워 인덱스가 stale 하지 않게 한다.
161
+ * @param {string} channel @param {string} connId @param {1|-1} delta
162
+ * @private
163
+ */
164
+ private _bumpChannelBridge;
151
165
  /**
152
166
  * presence 에서 세션 제거. 빈 인덱스는 정리.
153
167
  * @param {string} sessionId
@@ -162,12 +176,23 @@ export class MegaWsHub {
162
176
  */
163
177
  private _handleBridgeGone;
164
178
  /**
165
- * origin 을 제외한 모든 등록 bridge 로 송신 (presence 공유용). envelope 는 1회만 직렬화(L5).
179
+ * origin 을 제외한 모든 등록 bridge 로 송신. envelope 는 1회만 직렬화(L5).
180
+ * BULK_LEAVE(bridge-gone 정리 통지)처럼 전 bridge 가 받아야 하는 통지에 쓴다.
166
181
  * @param {string} exceptConnId
167
182
  * @param {Object} envelope
168
183
  * @private
169
184
  */
170
185
  private _fanOutToOthers;
186
+ /**
187
+ * presence(JOIN/LEAVE/METADATA) fan-out — `presence-fanout` capability 를 선언한 bridge 에만 송신.
188
+ * roster 가 redis 로 분리(ADR-177)된 뒤 표준 bridge 는 이 타입들을 no-op 으로 버리므로, 옵트인하지
189
+ * 않은 bridge 에는 보내지 않는다 — 세션 churn × bridge 수 만큼의 죽은 프레임을 제거한다.
190
+ * 구버전 bridge 호환: hub presence 를 실제로 쓰려는 bridge 는 capability 로 명시 선언한다.
191
+ * @param {string} exceptConnId
192
+ * @param {Object} envelope
193
+ * @private
194
+ */
195
+ private _fanOutPresence;
171
196
  /**
172
197
  * 한 채널의 세션을 가진 bridge 들로 fan-out (origin 제외, 중복 bridge 1회). 직렬화 1회(L5).
173
198
  *