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/lib/mega-job.js
CHANGED
|
@@ -73,7 +73,14 @@ export class MegaJob {
|
|
|
73
73
|
/** @type {string|undefined} bus 별명(`ctx.bus(alias)`). 워커 배선이 nc 해석에 사용. */
|
|
74
74
|
static bus = undefined
|
|
75
75
|
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* @type {number} 동시 처리 메시지 수. 기본 1(순차·안전).
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ 이 값은 durable consumer 의 `max_ack_pending` 으로 들어가므로 **워커 그룹(같은 subject 를
|
|
80
|
+
* 소비하는 모든 인스턴스) 전체의 합산 in-flight 상한**이다 — `mega worker` 인스턴스를 늘려도
|
|
81
|
+
* 합산 동시 처리는 이 값을 넘지 못한다. 처리량을 늘리려면 인스턴스 증설과 **함께** concurrency 를
|
|
82
|
+
* 키워야 한다(실측: c=1→c=32 에서 처리량 ~10배).
|
|
83
|
+
*/
|
|
77
84
|
static concurrency = 1
|
|
78
85
|
|
|
79
86
|
/** @type {number} run 실패 시 **추가** 재시도 횟수(첫 시도 제외). 기본 3(OQ-012). */
|
package/src/lib/mega-metrics.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* @see https://opentelemetry.io/docs/specs/otel/metrics/ (OTel metrics)
|
|
37
37
|
* @see https://prometheus.io/docs/instrumenting/exposition_formats/ (Prometheus 텍스트 포맷)
|
|
38
38
|
*/
|
|
39
|
+
import { getHeapStatistics } from 'node:v8'
|
|
39
40
|
import { MeterProvider } from '@opentelemetry/sdk-metrics'
|
|
40
41
|
import { PrometheusExporter, PrometheusSerializer } from '@opentelemetry/exporter-prometheus'
|
|
41
42
|
import { buildOtelResource } from './otel-resource.js'
|
|
@@ -547,7 +548,7 @@ function buildInstruments(meter) {
|
|
|
547
548
|
*/
|
|
548
549
|
function registerSystemGauges(meter) {
|
|
549
550
|
const memory = meter.createObservableGauge('mega_process_memory_bytes', {
|
|
550
|
-
description: '프로세스 메모리 사용량(바이트) — kind=rss|heap_used|heap_total|external 라벨.',
|
|
551
|
+
description: '프로세스 메모리 사용량(바이트) — kind=rss|heap_used|heap_total|external|array_buffers 라벨.',
|
|
551
552
|
unit: 'By',
|
|
552
553
|
})
|
|
553
554
|
memory.addCallback((result) => {
|
|
@@ -556,6 +557,32 @@ function registerSystemGauges(meter) {
|
|
|
556
557
|
result.observe(mu.heapUsed, { kind: 'heap_used' })
|
|
557
558
|
result.observe(mu.heapTotal, { kind: 'heap_total' })
|
|
558
559
|
result.observe(mu.external, { kind: 'external' })
|
|
560
|
+
// arrayBuffers 분리 노출(ADR-215, G5 M-5) — mongo driver 등 ArrayBuffer 상주분을 heap 과 구분해
|
|
561
|
+
// "RSS 만 보는" 관측 함정(burst 후 V8 페이지 미반환 = 정상 평형)을 운영자가 분해해 읽을 수 있게.
|
|
562
|
+
result.observe(mu.arrayBuffers ?? 0, { kind: 'array_buffers' })
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// V8 힙 통계(ADR-215, G5 M-5) — heap_size_limit(OOM 한계)·total_available_size(여유)·physical 등
|
|
566
|
+
// process.memoryUsage 가 못 보여주는 V8 내부 수위. kind 는 고정 enum 이라 카디널리티 안전.
|
|
567
|
+
const V8_HEAP_KINDS = Object.freeze([
|
|
568
|
+
'total_heap_size',
|
|
569
|
+
'total_physical_size',
|
|
570
|
+
'total_available_size',
|
|
571
|
+
'used_heap_size',
|
|
572
|
+
'heap_size_limit',
|
|
573
|
+
'malloced_memory',
|
|
574
|
+
'peak_malloced_memory',
|
|
575
|
+
'external_memory',
|
|
576
|
+
])
|
|
577
|
+
const v8Heap = meter.createObservableGauge('mega_v8_heap_bytes', {
|
|
578
|
+
description: `V8 힙 통계(바이트, v8.getHeapStatistics) — kind=${V8_HEAP_KINDS.join('|')} 라벨.`,
|
|
579
|
+
unit: 'By',
|
|
580
|
+
})
|
|
581
|
+
v8Heap.addCallback((result) => {
|
|
582
|
+
const hs = /** @type {Record<string, number>} */ (/** @type {unknown} */ (getHeapStatistics()))
|
|
583
|
+
for (const kind of V8_HEAP_KINDS) {
|
|
584
|
+
if (typeof hs[kind] === 'number') result.observe(hs[kind], { kind })
|
|
585
|
+
}
|
|
559
586
|
})
|
|
560
587
|
|
|
561
588
|
const uptime = meter.createObservableGauge('mega_process_uptime_seconds', {
|
package/src/lib/mega-plugin.js
CHANGED
|
@@ -62,12 +62,12 @@ const LIFECYCLE_EVENTS = /** @type {const} */ (['beforeBoot', 'afterBoot', 'befo
|
|
|
62
62
|
* @property {string} [description] - `mega help` 가 병기하는 한 줄 설명(선택).
|
|
63
63
|
*/
|
|
64
64
|
|
|
65
|
-
/** 빌트인 generator
|
|
65
|
+
/** 빌트인 generator 이름 — 플러그인 scaffold 가 점유 금지(빌트인이 우선이라 silent shadow 가 됨).
|
|
66
66
|
* cli/generators 의 `GENERATOR_KINDS` 와 동일해야 한다(레이어 역전 회피를 위해 미러 — 동기화는
|
|
67
67
|
* mega-plugin 단위 테스트가 GENERATOR_KINDS 와 집합 비교로 강제한다). */
|
|
68
68
|
export const RESERVED_GENERATOR_NAMES = new Set([
|
|
69
69
|
'app', 'controller', 'channel', 'service', 'model', 'middleware', 'route',
|
|
70
|
-
'schedule', 'job', 'worker', 'locale', 'adapter', 'migration',
|
|
70
|
+
'schedule', 'job', 'worker', 'locale', 'adapter', 'migration', 'adr',
|
|
71
71
|
])
|
|
72
72
|
|
|
73
73
|
/**
|
package/src/lib/mega-worker.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* 프로세스 메모리 격리), process=`child_process.fork`(완전 격리, 더 무거움). 둘 다 node 빌트인(의존성 0).
|
|
17
17
|
* - **풀 정책** — `static poolSize`(디폴트 `os.cpus().length - 1`, 최소 1). 작업 큐 + 가용 워커에 디스패치.
|
|
18
18
|
* - **crash 자동 재시작** — 워커가 예기치 않게 죽으면 in-flight task 를 `worker.crashed` 로 reject 하고
|
|
19
|
-
* `static maxRestarts`(디폴트 5)
|
|
19
|
+
* `static maxRestarts`(디폴트 5)/`static restartWindowMs`(디폴트 60s) **슬라이딩 윈도우** 안에서만
|
|
20
|
+
* 교체 워커를 띄운다 — 윈도우 내 한도 초과 = crash-loop 으로 보고 포기(MegaCluster 정책 통일).
|
|
21
|
+
* 산발 crash(윈도우 밖)는 한도를 소모하지 않아 장수 프로세스의 풀이 영구 축소되지 않는다.
|
|
20
22
|
* - **graceful shutdown** — `stop()`: 새 `run()` 거부 + 큐 대기분 `worker.stopped` reject + in-flight 완료
|
|
21
23
|
* 대기(allSettled) → 워커 terminate. `MegaShutdown` 통합은 workers-manager 가 배선.
|
|
22
24
|
*
|
|
@@ -57,6 +59,14 @@ const DEFAULT_POOL_SIZE = Math.max(1, os.cpus().length - 1)
|
|
|
57
59
|
/** crash 시 풀 전체에서 허용하는 교체 워커 재시작 총량 디폴트. */
|
|
58
60
|
const DEFAULT_MAX_RESTARTS = 5
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* crash 교체 판정 윈도우 기본값(ms) — 60초. `maxRestarts` 는 이 윈도우 **안의** 교체 횟수 상한이다
|
|
64
|
+
* (수명 누적이 아님). 누적 카운터면 몇 주 간격의 산발 crash 도 한도를 소모해 장수 프로세스의 풀이
|
|
65
|
+
* 영구 축소되다 `pool_exhausted` 로 죽는다 — crash-loop(즉시 연속 사망)만 막으면 되는 안전망이므로
|
|
66
|
+
* 시간 윈도우 판정이 맞다(MegaCluster 의 rapid-crash 슬라이딩 윈도우와 정책 통일).
|
|
67
|
+
*/
|
|
68
|
+
const DEFAULT_RESTART_WINDOW_MS = 60_000
|
|
69
|
+
|
|
60
70
|
/**
|
|
61
71
|
* @typedef {object} WorkerHandle - 풀 안의 워커 1개 핸들.
|
|
62
72
|
* @property {number} id - 핸들 식별자(로그/디버그).
|
|
@@ -94,7 +104,8 @@ export class MegaWorker extends EventEmitter {
|
|
|
94
104
|
/** @type {boolean} */ #stopping = false
|
|
95
105
|
/** @type {number} */ #nextTaskId = 1
|
|
96
106
|
/** @type {number} */ #nextHandleId = 1
|
|
97
|
-
/** @type {number} crash 교체
|
|
107
|
+
/** @type {number[]} crash 교체 시각(epoch ms) 슬라이딩 윈도우 — restartWindowMs 밖은 판정 시 제거. */
|
|
108
|
+
#restartTimes = []
|
|
98
109
|
/** @type {number} 진행 중인 교체 spawn 수(일시적 풀 0 상태에서 run 을 큐잉할지 판단). */ #pendingRespawns = 0
|
|
99
110
|
|
|
100
111
|
/**
|
|
@@ -130,12 +141,18 @@ export class MegaWorker extends EventEmitter {
|
|
|
130
141
|
return typeof p === 'number' ? p : DEFAULT_POOL_SIZE
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
/** @returns {number} crash 교체 허용
|
|
144
|
+
/** @returns {number} 윈도우({@link MegaWorker#restartWindowMs}) 내 crash 교체 허용 횟수. */
|
|
134
145
|
get maxRestarts() {
|
|
135
146
|
const r = /** @type {any} */ (this.constructor).maxRestarts
|
|
136
147
|
return Number.isInteger(r) && r >= 0 ? r : DEFAULT_MAX_RESTARTS
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
/** @returns {number} crash 교체 판정 슬라이딩 윈도우(ms). `static restartWindowMs` 로 조정. */
|
|
151
|
+
get restartWindowMs() {
|
|
152
|
+
const w = /** @type {any} */ (this.constructor).restartWindowMs
|
|
153
|
+
return Number.isInteger(w) && w > 0 ? w : DEFAULT_RESTART_WINDOW_MS
|
|
154
|
+
}
|
|
155
|
+
|
|
139
156
|
/** @returns {boolean} start() 후 stop() 전이면 true. */
|
|
140
157
|
get isStarted() {
|
|
141
158
|
return this.#started
|
|
@@ -511,8 +528,14 @@ export class MegaWorker extends EventEmitter {
|
|
|
511
528
|
* @returns {void}
|
|
512
529
|
*/
|
|
513
530
|
#scheduleRespawn() {
|
|
514
|
-
if (this.#stopping
|
|
515
|
-
|
|
531
|
+
if (this.#stopping) return
|
|
532
|
+
// 슬라이딩 윈도우 판정 — 윈도우 밖 기록을 비우고 남은 횟수가 한도면 crash-loop 으로 보고 포기.
|
|
533
|
+
// (수명 누적이 아니라서 산발 crash 는 풀을 영구 축소시키지 않는다 — MegaCluster 정책 통일.)
|
|
534
|
+
const now = Date.now()
|
|
535
|
+
const windowStart = now - this.restartWindowMs
|
|
536
|
+
this.#restartTimes = this.#restartTimes.filter((t) => t >= windowStart)
|
|
537
|
+
if (this.#restartTimes.length >= this.maxRestarts) return
|
|
538
|
+
this.#restartTimes.push(now)
|
|
516
539
|
this.#pendingRespawns++
|
|
517
540
|
this.#spawn()
|
|
518
541
|
.then((h) => {
|
package/src/lib/ws-hub.js
CHANGED
|
@@ -103,7 +103,7 @@ export class MegaWsHub {
|
|
|
103
103
|
this._wss = null
|
|
104
104
|
/** heartbeat liveness 체크 interval (M3). @type {ReturnType<typeof setInterval> | null} */
|
|
105
105
|
this._livenessTimer = null
|
|
106
|
-
/** 등록된 bridge 연결. connId → { socket, lastSeen, protocolVersion }. @type {Map<string, { socket: import('ws').WebSocket, lastSeen: number, protocolVersion?: number }>} */
|
|
106
|
+
/** 등록된 bridge 연결. connId → { socket, lastSeen, protocolVersion, presenceFanout }. @type {Map<string, { socket: import('ws').WebSocket, lastSeen: number, protocolVersion?: number, presenceFanout?: boolean }>} */
|
|
107
107
|
this._bridges = new Map()
|
|
108
108
|
/** presence. sessionId → { bridgeConnId, userId, channels:Set, metadata }. @type {Map<string, { bridgeConnId: string, userId: string, channels: Set<string>, metadata: Object }>} */
|
|
109
109
|
this._sessions = new Map()
|
|
@@ -111,6 +111,13 @@ export class MegaWsHub {
|
|
|
111
111
|
this._channelSessions = new Map()
|
|
112
112
|
/** userId → sessionId 집합 (DIRECT fan-out, ADR-035). @type {Map<string, Set<string>>} */
|
|
113
113
|
this._userSessions = new Map()
|
|
114
|
+
/**
|
|
115
|
+
* 역인덱스: channel → (bridgeConnId → 그 bridge 의 채널 내 세션 수). BROADCAST 의 bridge 선정을
|
|
116
|
+
* 세션 수 무관 O(bridge 수)로 만든다 — 멤버 10k 채널에서 메시지당 수백 µs 가 µs 대로 떨어진다.
|
|
117
|
+
* `_addSession`/`_removeSession` 이 증분 유지(카운트 0 → 키 제거), 메모리는 채널×bridge 수준.
|
|
118
|
+
* @type {Map<string, Map<string, number>>}
|
|
119
|
+
*/
|
|
120
|
+
this._channelBridges = new Map()
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
/** hub 식별자. */
|
|
@@ -170,6 +177,20 @@ export class MegaWsHub {
|
|
|
170
177
|
let isRegistered = false
|
|
171
178
|
this._log?.debug?.({ connId }, 'ws-hub connection (awaiting register)')
|
|
172
179
|
|
|
180
|
+
// 미등록 연결 register 타임아웃 — liveness(_checkLiveness)는 등록된 bridge 만 스캔하므로,
|
|
181
|
+
// REGISTER 를 안 보내는 연결(오설정 bridge·포트 스캐너·half-open)은 이 타이머가 없으면
|
|
182
|
+
// fd/메모리로 영구 잔존한다. heartbeatMs 안에 등록 못 하면 1008 로 닫는다(fail-closed).
|
|
183
|
+
const registerTimer = setTimeout(() => {
|
|
184
|
+
if (isRegistered) return
|
|
185
|
+
this._log?.warn?.({ connId, timeoutMs: this._heartbeatMs }, 'ws-hub register timeout — closing unregistered connection (1008)')
|
|
186
|
+
try {
|
|
187
|
+
socket.close(1008, 'register timeout')
|
|
188
|
+
} catch (err) {
|
|
189
|
+
this._log?.debug?.({ err, connId }, 'ws-hub register-timeout close failed (already closing)')
|
|
190
|
+
}
|
|
191
|
+
}, this._heartbeatMs)
|
|
192
|
+
registerTimer.unref?.()
|
|
193
|
+
|
|
173
194
|
socket.on('message', (raw) => {
|
|
174
195
|
const frame = Array.isArray(raw) ? Buffer.concat(raw).toString('utf8') : raw.toString('utf8')
|
|
175
196
|
let msg
|
|
@@ -196,12 +217,14 @@ export class MegaWsHub {
|
|
|
196
217
|
return
|
|
197
218
|
}
|
|
198
219
|
isRegistered = this._handleRegister(connId, socket, msg)
|
|
220
|
+
if (isRegistered) clearTimeout(registerTimer)
|
|
199
221
|
return
|
|
200
222
|
}
|
|
201
223
|
this._route(connId, socket, msg)
|
|
202
224
|
})
|
|
203
225
|
|
|
204
226
|
socket.on('close', () => {
|
|
227
|
+
clearTimeout(registerTimer) // 미등록 타임아웃 정리(등록 전 절단·정상 종료 공통).
|
|
205
228
|
if (isRegistered) this._handleBridgeGone(connId)
|
|
206
229
|
this._log?.debug?.({ connId }, 'ws-hub connection closed')
|
|
207
230
|
})
|
|
@@ -234,7 +257,10 @@ export class MegaWsHub {
|
|
|
234
257
|
const protocolVersion = hasVersionRequest
|
|
235
258
|
? negotiateHubProtocolVersion(payload.protocolVersion)
|
|
236
259
|
: HUB_PROTOCOL_VERSION
|
|
237
|
-
|
|
260
|
+
// presence fan-out 은 옵트인 — roster 가 redis 로 분리(ADR-177)된 뒤 bridge 는 JOIN/LEAVE/METADATA
|
|
261
|
+
// 를 no-op 으로 버리므로, `presence-fanout` capability 를 선언한 bridge 에만 보낸다(죽은 트래픽 제거).
|
|
262
|
+
const presenceFanout = Array.isArray(payload.capabilities) && payload.capabilities.includes('presence-fanout')
|
|
263
|
+
this._bridges.set(connId, { socket, lastSeen: Date.now(), protocolVersion, presenceFanout })
|
|
238
264
|
this._log?.info?.({ connId, instanceId: payload.instanceId, hubId: this._hubId, protocolVersion }, 'ws-hub bridge registered')
|
|
239
265
|
this._safeSend(socket, createHubMessage({
|
|
240
266
|
type: T.REGISTER_OK,
|
|
@@ -262,12 +288,12 @@ export class MegaWsHub {
|
|
|
262
288
|
switch (type) {
|
|
263
289
|
case T.JOIN:
|
|
264
290
|
this._addSession(connId, payload)
|
|
265
|
-
// 클러스터 presence 공유 —
|
|
266
|
-
this.
|
|
291
|
+
// 클러스터 presence 공유 — `presence-fanout` 선언 bridge 에만 (07 §2 + ADR-177 후 죽은 트래픽 제거).
|
|
292
|
+
this._fanOutPresence(connId, msg)
|
|
267
293
|
break
|
|
268
294
|
case T.LEAVE: {
|
|
269
295
|
const removed = this._removeSession(payload.sessionId)
|
|
270
|
-
if (removed) this.
|
|
296
|
+
if (removed) this._fanOutPresence(connId, msg)
|
|
271
297
|
break
|
|
272
298
|
}
|
|
273
299
|
case T.BULK_LEAVE: {
|
|
@@ -289,7 +315,7 @@ export class MegaWsHub {
|
|
|
289
315
|
const session = this._sessions.get(payload.sessionId)
|
|
290
316
|
if (session) {
|
|
291
317
|
session.metadata = payload.metadata
|
|
292
|
-
this.
|
|
318
|
+
this._fanOutPresence(connId, msg) // presence 메타 동기화 — 옵트인 bridge 만.
|
|
293
319
|
}
|
|
294
320
|
break
|
|
295
321
|
}
|
|
@@ -360,6 +386,7 @@ export class MegaWsHub {
|
|
|
360
386
|
this._channelSessions.set(ch, set)
|
|
361
387
|
}
|
|
362
388
|
set.add(entry.sessionId)
|
|
389
|
+
this._bumpChannelBridge(ch, connId, +1)
|
|
363
390
|
}
|
|
364
391
|
let uset = this._userSessions.get(entry.userId)
|
|
365
392
|
if (!uset) {
|
|
@@ -369,6 +396,27 @@ export class MegaWsHub {
|
|
|
369
396
|
uset.add(entry.sessionId)
|
|
370
397
|
}
|
|
371
398
|
|
|
399
|
+
/**
|
|
400
|
+
* channel→bridge 역인덱스 증분 갱신. 카운트가 0 이 되면 키를 지워 인덱스가 stale 하지 않게 한다.
|
|
401
|
+
* @param {string} channel @param {string} connId @param {1|-1} delta
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
_bumpChannelBridge(channel, connId, delta) {
|
|
405
|
+
let counts = this._channelBridges.get(channel)
|
|
406
|
+
if (!counts) {
|
|
407
|
+
if (delta < 0) return // 제거인데 인덱스 없음 — 정합상 없을 일이지만 방어.
|
|
408
|
+
counts = new Map()
|
|
409
|
+
this._channelBridges.set(channel, counts)
|
|
410
|
+
}
|
|
411
|
+
const next = (counts.get(connId) ?? 0) + delta
|
|
412
|
+
if (next > 0) {
|
|
413
|
+
counts.set(connId, next)
|
|
414
|
+
} else {
|
|
415
|
+
counts.delete(connId)
|
|
416
|
+
if (counts.size === 0) this._channelBridges.delete(channel)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
372
420
|
/**
|
|
373
421
|
* presence 에서 세션 제거. 빈 인덱스는 정리.
|
|
374
422
|
* @param {string} sessionId
|
|
@@ -385,6 +433,7 @@ export class MegaWsHub {
|
|
|
385
433
|
set.delete(sessionId)
|
|
386
434
|
if (set.size === 0) this._channelSessions.delete(ch)
|
|
387
435
|
}
|
|
436
|
+
this._bumpChannelBridge(ch, session.bridgeConnId, -1)
|
|
388
437
|
}
|
|
389
438
|
const uset = this._userSessions.get(session.userId)
|
|
390
439
|
if (uset) {
|
|
@@ -415,7 +464,8 @@ export class MegaWsHub {
|
|
|
415
464
|
}
|
|
416
465
|
|
|
417
466
|
/**
|
|
418
|
-
* origin 을 제외한 모든 등록 bridge 로
|
|
467
|
+
* origin 을 제외한 모든 등록 bridge 로 송신. envelope 는 1회만 직렬화(L5).
|
|
468
|
+
* BULK_LEAVE(bridge-gone 정리 통지)처럼 전 bridge 가 받아야 하는 통지에 쓴다.
|
|
419
469
|
* @param {string} exceptConnId
|
|
420
470
|
* @param {Object} envelope
|
|
421
471
|
* @private
|
|
@@ -427,6 +477,25 @@ export class MegaWsHub {
|
|
|
427
477
|
}
|
|
428
478
|
}
|
|
429
479
|
|
|
480
|
+
/**
|
|
481
|
+
* presence(JOIN/LEAVE/METADATA) fan-out — `presence-fanout` capability 를 선언한 bridge 에만 송신.
|
|
482
|
+
* roster 가 redis 로 분리(ADR-177)된 뒤 표준 bridge 는 이 타입들을 no-op 으로 버리므로, 옵트인하지
|
|
483
|
+
* 않은 bridge 에는 보내지 않는다 — 세션 churn × bridge 수 만큼의 죽은 프레임을 제거한다.
|
|
484
|
+
* 구버전 bridge 호환: hub presence 를 실제로 쓰려는 bridge 는 capability 로 명시 선언한다.
|
|
485
|
+
* @param {string} exceptConnId
|
|
486
|
+
* @param {Object} envelope
|
|
487
|
+
* @private
|
|
488
|
+
*/
|
|
489
|
+
_fanOutPresence(exceptConnId, envelope) {
|
|
490
|
+
/** @type {string | null} 첫 대상에서 1회 직렬화(L5) — 대상 0 이면 직렬화도 안 함. */
|
|
491
|
+
let data = null
|
|
492
|
+
for (const [connId, bridge] of this._bridges) {
|
|
493
|
+
if (connId === exceptConnId || !(/** @type {any} */ (bridge).presenceFanout)) continue
|
|
494
|
+
if (data === null) data = JSON.stringify(envelope)
|
|
495
|
+
this._sendSerialized(bridge.socket, data)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
430
499
|
/**
|
|
431
500
|
* 한 채널의 세션을 가진 bridge 들로 fan-out (origin 제외, 중복 bridge 1회). 직렬화 1회(L5).
|
|
432
501
|
*
|
|
@@ -441,12 +510,24 @@ export class MegaWsHub {
|
|
|
441
510
|
* @private
|
|
442
511
|
*/
|
|
443
512
|
_fanOutChannel(channel, exceptConnId, envelope, exceptSessionIds) {
|
|
513
|
+
const except = Array.isArray(exceptSessionIds) && exceptSessionIds.length > 0 ? new Set(exceptSessionIds) : null
|
|
514
|
+
if (!except) {
|
|
515
|
+
// 일반 경로(대부분의 메시지): channel→bridge 역인덱스로 선정이 O(bridge 수) — 세션 수 무관.
|
|
516
|
+
const counts = this._channelBridges.get(channel)
|
|
517
|
+
if (!counts || counts.size === 0) return
|
|
518
|
+
const data = JSON.stringify(envelope)
|
|
519
|
+
for (const connId of counts.keys()) {
|
|
520
|
+
if (connId !== exceptConnId) this._sendTo(connId, data)
|
|
521
|
+
}
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
// exceptSessionIds 경로(드묾): "제외 세션만 가진 bridge 통째 스킵" 판정에 세션 단위 정보가
|
|
525
|
+
// 필요하므로 기존 멤버 순회를 유지한다.
|
|
444
526
|
const sids = this._channelSessions.get(channel)
|
|
445
527
|
if (!sids) return
|
|
446
|
-
const except = Array.isArray(exceptSessionIds) && exceptSessionIds.length > 0 ? new Set(exceptSessionIds) : null
|
|
447
528
|
const targets = new Set()
|
|
448
529
|
for (const sid of sids) {
|
|
449
|
-
if (except
|
|
530
|
+
if (except.has(sid)) continue // 제외 세션은 bridge 선정에 기여하지 않음
|
|
450
531
|
const session = this._sessions.get(sid)
|
|
451
532
|
if (session && session.bridgeConnId !== exceptConnId) targets.add(session.bridgeConnId)
|
|
452
533
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
number: {{number}}
|
|
3
|
+
title: {{title}}
|
|
4
|
+
date: {{date}}
|
|
5
|
+
status: accepted
|
|
6
|
+
supersedes: []
|
|
7
|
+
superseded_by: null
|
|
8
|
+
tags: []
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ADR-{{number}}: {{title}}
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
(결정이 필요했던 배경 — 문제·제약·관련 ADR 링크)
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
(무엇을 결정했는가 — 한 문장 요지 + 세부 사항)
|
|
20
|
+
|
|
21
|
+
## Consequences
|
|
22
|
+
|
|
23
|
+
(트레이드오프·후속 작업·영향 파일·검증 결과)
|
|
@@ -65,6 +65,8 @@ export function normalizePool(pool: unknown, spec: Record<string, {
|
|
|
65
65
|
key: string;
|
|
66
66
|
divideBy?: number;
|
|
67
67
|
} | null>, driver: string): Record<string, number>;
|
|
68
|
+
/** pool acquire 대기 한도 프레임워크 디폴트(ms) — 명시 0 으로 드라이버 무한 대기 옵트인(ADR-216). */
|
|
69
|
+
export const DEFAULT_ACQUIRE_TIMEOUT_MS: 10000;
|
|
68
70
|
/**
|
|
69
71
|
* pg 풀 매핑 — 값이 null 이면 미지원(throw), `{ key, divideBy? }` 면 키 이름 변경(+단위 변환).
|
|
70
72
|
* @type {Record<string, { key: string, divideBy?: number } | null>}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @property {string} [basePath] - 캐시 파일 저장 디렉토리 (필수).
|
|
5
5
|
* @property {string} [dir] - `basePath` 의 별칭 (ADR-082 정합, 하위 호환).
|
|
6
6
|
* @property {{ serializer?: 'json' | 'raw', extension?: string }} [options]
|
|
7
|
+
* @property {string} [namespace] - 캐시 키 자동 prefix `mega:cache:<namespace>:` (ADR-064/213, 베이스 처리).
|
|
8
|
+
* @property {number} [defaultTtlSec] - `set` ttl 미지정 시 디폴트(초). 0 = 무한 옵트인. (ADR-216, 베이스 처리)
|
|
7
9
|
*/
|
|
8
10
|
/**
|
|
9
11
|
* @typedef {object} CacheEnvelope - 디스크에 저장되는 단일 파일 포맷.
|
|
@@ -14,7 +16,8 @@
|
|
|
14
16
|
*/
|
|
15
17
|
export class MegaFileAdapter extends MegaCacheAdapter {
|
|
16
18
|
/**
|
|
17
|
-
* @param {FileConfig} [config] - services.caches.<key> 설정.
|
|
19
|
+
* @param {FileConfig} [config] - services.caches.<key> 설정. 베이스(MegaCacheAdapter)의
|
|
20
|
+
* `namespace`(ADR-064 자동 prefix)/`defaultTtlSec`(ADR-216) 도 여기서 받는다.
|
|
18
21
|
* @throws {MegaValidationError} `adapter.basepath_required` - basePath/dir 누락.
|
|
19
22
|
* @throws {MegaValidationError} `adapter.invalid_option` - 옵션 타입/미지원 키 오류.
|
|
20
23
|
*/
|
|
@@ -69,6 +72,14 @@ export type FileConfig = {
|
|
|
69
72
|
serializer?: "json" | "raw";
|
|
70
73
|
extension?: string;
|
|
71
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* - 캐시 키 자동 prefix `mega:cache:<namespace>:` (ADR-064/213, 베이스 처리).
|
|
77
|
+
*/
|
|
78
|
+
namespace?: string;
|
|
79
|
+
/**
|
|
80
|
+
* - `set` ttl 미지정 시 디폴트(초). 0 = 무한 옵트인. (ADR-216, 베이스 처리)
|
|
81
|
+
*/
|
|
82
|
+
defaultTtlSec?: number;
|
|
72
83
|
};
|
|
73
84
|
/**
|
|
74
85
|
* - 디스크에 저장되는 단일 파일 포맷.
|
|
@@ -41,13 +41,15 @@ export class MegaFileSessionAdapter extends MegaSessionAdapter {
|
|
|
41
41
|
error?: string;
|
|
42
42
|
}>;
|
|
43
43
|
/**
|
|
44
|
-
* 누적 통계 + file 세션 특화.
|
|
45
|
-
*
|
|
44
|
+
* 누적 통계 + file 세션 특화. `cleanupIntervalMs` 노출(0=내부 타이머 off) — 만료 스캔 주기의
|
|
45
|
+
* 적용 여부를 운영/테스트가 확인하는 단일 창구(ADR-215).
|
|
46
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, basePath: string, ttlMs: number, cleanupIntervalMs: number }}
|
|
46
47
|
*/
|
|
47
48
|
getStats(): ReturnType<import("./mega-adapter.js").MegaAdapter["getStats"]> & {
|
|
48
49
|
driver: string;
|
|
49
50
|
basePath: string;
|
|
50
51
|
ttlMs: number;
|
|
52
|
+
cleanupIntervalMs: number;
|
|
51
53
|
};
|
|
52
54
|
#private;
|
|
53
55
|
}
|
|
@@ -27,15 +27,17 @@ export class MegaMariaAdapter extends MegaDbAdapter {
|
|
|
27
27
|
error?: string;
|
|
28
28
|
}>;
|
|
29
29
|
/**
|
|
30
|
-
* 누적 통계 + 풀 통계
|
|
31
|
-
*
|
|
30
|
+
* 누적 통계 + 풀 통계 — 공통 코어 키 `{ total, active, idle, waiting }` 은 4 driver 동일
|
|
31
|
+
* 형태(ADR-216 G2 H-2). `queue` 는 기존 소비자 하위 호환 별칭(= waiting). 연결 전이면 0.
|
|
32
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, active: number, idle: number, waiting: number, queue: number } }}
|
|
32
33
|
*/
|
|
33
34
|
getStats(): ReturnType<import("./mega-adapter.js").MegaAdapter["getStats"]> & {
|
|
34
35
|
driver: string;
|
|
35
36
|
pool: {
|
|
36
37
|
total: number;
|
|
37
|
-
idle: number;
|
|
38
38
|
active: number;
|
|
39
|
+
idle: number;
|
|
40
|
+
waiting: number;
|
|
39
41
|
queue: number;
|
|
40
42
|
};
|
|
41
43
|
};
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
export class MegaCacheAdapter extends MegaAdapter {
|
|
2
|
+
/**
|
|
3
|
+
* @param {{ namespace?: string, defaultTtlSec?: number } & Record<string, any>} [config]
|
|
4
|
+
* @throws {MegaValidationError} `adapter.invalid_option` - namespace/defaultTtlSec 형식 오류.
|
|
5
|
+
*/
|
|
6
|
+
constructor(config?: {
|
|
7
|
+
namespace?: string;
|
|
8
|
+
defaultTtlSec?: number;
|
|
9
|
+
} & Record<string, any>);
|
|
10
|
+
/**
|
|
11
|
+
* 네임스페이스 적용 키 — 구체 어댑터의 get/set/del/has 가 저장소 접근 직전에 경유한다(ADR-064).
|
|
12
|
+
* @protected
|
|
13
|
+
* @param {string} key
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
protected _cacheKey(key: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* `set` 의 유효 TTL 해석 — 명시 ttl 우선, 없으면 defaultTtlSec. 둘 다 없으면 무한 저장이며
|
|
19
|
+
* `defaultTtlSec: 0` 옵트인이 아닌 한 인스턴스당 1회 경고를 낸다(키 무한 증가 풋건 표면화 —
|
|
20
|
+
* 동작은 기존과 동일, ADR-216).
|
|
21
|
+
* @protected
|
|
22
|
+
* @param {number} [ttl] - 호출자가 명시한 ttl(초).
|
|
23
|
+
* @param {string} [key] - 경고 메시지용 예시 키.
|
|
24
|
+
* @returns {number | undefined} 적용할 ttl(초) — undefined 면 무한.
|
|
25
|
+
*/
|
|
26
|
+
protected _resolveTtl(ttl?: number, key?: string): number | undefined;
|
|
2
27
|
/**
|
|
3
28
|
* @param {string} _key
|
|
4
29
|
* @returns {Promise<any>} 값 — 없으면 `null` (throw X).
|
|
@@ -7,7 +32,7 @@ export class MegaCacheAdapter extends MegaAdapter {
|
|
|
7
32
|
/**
|
|
8
33
|
* @param {string} _key
|
|
9
34
|
* @param {any} _value
|
|
10
|
-
* @param {{ ttl?: number }} [_opts] - `ttl` 미지정 시 무한
|
|
35
|
+
* @param {{ ttl?: number }} [_opts] - `ttl` 미지정 시 defaultTtlSec, 그것도 없으면 무한(초 단위, ADR-216).
|
|
11
36
|
* @returns {Promise<void>}
|
|
12
37
|
*/
|
|
13
38
|
set(_key: string, _value: any, _opts?: {
|
|
@@ -43,5 +68,6 @@ export class MegaCacheAdapter extends MegaAdapter {
|
|
|
43
68
|
allowZero?: boolean;
|
|
44
69
|
allowInfinity?: boolean;
|
|
45
70
|
}): void;
|
|
71
|
+
#private;
|
|
46
72
|
}
|
|
47
73
|
import { MegaAdapter } from './mega-adapter.js';
|
|
@@ -4,7 +4,10 @@ export class MegaDbAdapter extends MegaAdapter {
|
|
|
4
4
|
* driver 별 구현 (postgres `BEGIN/COMMIT/ROLLBACK`, MongoDB `session.withTransaction`).
|
|
5
5
|
* nested 호출은 driver 별 (postgres SAVEPOINT, MongoDB throw `adapter.nested_transaction_unsupported`).
|
|
6
6
|
*
|
|
7
|
-
* `opts.isolation`(ADR-190) — SQL 격리수준 옵트인.
|
|
7
|
+
* `opts.isolation`(ADR-190) — SQL 격리수준 옵트인. **미지정이면 driver 디폴트가 적용되며 그
|
|
8
|
+
* 디폴트는 driver 마다 다르다**(ADR-216 G2 M-2): postgres = READ COMMITTED,
|
|
9
|
+
* mariadb(InnoDB) = REPEATABLE READ — 같은 `withTransaction(fn)` 코드가 driver 에 따라 다른
|
|
10
|
+
* 동시성 의미를 가진다. 이식성 있는 동시성 가정이 필요하면 isolation 을 명시할 것. driver 별 지원:
|
|
8
11
|
* postgres/maria 는 top-level 트랜잭션에 `SET TRANSACTION ISOLATION LEVEL` 로 반영(nested 엔 불가 —
|
|
9
12
|
* `adapter.nested_isolation_unsupported`), sqlite 는 항상 SERIALIZABLE 동작이라 'serializable' 만 수용,
|
|
10
13
|
* mongodb 는 SQL 격리수준 개념이 없어 지정 시 `adapter.invalid_option`.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @typedef {object} PoolCounters
|
|
10
10
|
* @property {number} created @property {number} closed
|
|
11
11
|
* @property {number} checkedOut @property {number} checkedIn
|
|
12
|
+
* @property {number} checkOutStarted @property {number} checkOutFailed
|
|
12
13
|
*/
|
|
13
14
|
export class MegaMongoAdapter extends MegaDbAdapter {
|
|
14
15
|
/**
|
|
@@ -41,13 +42,21 @@ export class MegaMongoAdapter extends MegaDbAdapter {
|
|
|
41
42
|
error?: string;
|
|
42
43
|
}>;
|
|
43
44
|
/**
|
|
44
|
-
* 누적 통계 + mongo 특화(driver/dbName + CMAP 풀 카운터).
|
|
45
|
-
*
|
|
45
|
+
* 누적 통계 + mongo 특화(driver/dbName + CMAP 풀 카운터). 공통 코어 키
|
|
46
|
+
* `{ total, active, idle, waiting }` 은 4 driver 동일 형태(ADR-216 G2 H-2 — CMAP 누적
|
|
47
|
+
* 카운터에서 파생: total=created-closed, active=checkedOut-checkedIn,
|
|
48
|
+
* waiting=checkOutStarted-(checkedOut+checkOutFailed)). 누적 원본도 유지(하위 호환).
|
|
49
|
+
* 연결 전이면 전부 0.
|
|
50
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, dbName: string, pool: { total: number, active: number, idle: number, waiting: number, created: number, closed: number, checkedOut: number, checkedIn: number, open: number, inUse: number } }}
|
|
46
51
|
*/
|
|
47
52
|
getStats(): ReturnType<import("./mega-adapter.js").MegaAdapter["getStats"]> & {
|
|
48
53
|
driver: string;
|
|
49
54
|
dbName: string;
|
|
50
55
|
pool: {
|
|
56
|
+
total: number;
|
|
57
|
+
active: number;
|
|
58
|
+
idle: number;
|
|
59
|
+
waiting: number;
|
|
51
60
|
created: number;
|
|
52
61
|
closed: number;
|
|
53
62
|
checkedOut: number;
|
|
@@ -135,5 +144,7 @@ export type PoolCounters = {
|
|
|
135
144
|
closed: number;
|
|
136
145
|
checkedOut: number;
|
|
137
146
|
checkedIn: number;
|
|
147
|
+
checkOutStarted: number;
|
|
148
|
+
checkOutFailed: number;
|
|
138
149
|
};
|
|
139
150
|
import { MegaDbAdapter } from './mega-db-adapter.js';
|
|
@@ -42,13 +42,15 @@ export class MegaPostgresAdapter extends MegaDbAdapter {
|
|
|
42
42
|
error?: string;
|
|
43
43
|
}>;
|
|
44
44
|
/**
|
|
45
|
-
* 누적 통계 + 풀 통계
|
|
46
|
-
*
|
|
45
|
+
* 누적 통계 + 풀 통계 — 공통 코어 키 `{ total, active, idle, waiting }` 은 4 driver 동일
|
|
46
|
+
* 형태(ADR-216 G2 H-2: 운영 대시보드가 driver 무관 코드로 "풀 포화" 를 묻게). 연결 전이면 0.
|
|
47
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, active: number, idle: number, waiting: number } }}
|
|
47
48
|
*/
|
|
48
49
|
getStats(): ReturnType<import("./mega-adapter.js").MegaAdapter["getStats"]> & {
|
|
49
50
|
driver: string;
|
|
50
51
|
pool: {
|
|
51
52
|
total: number;
|
|
53
|
+
active: number;
|
|
52
54
|
idle: number;
|
|
53
55
|
waiting: number;
|
|
54
56
|
};
|
|
@@ -62,6 +62,14 @@ export type RedisConfig = {
|
|
|
62
62
|
* - 미지원 — 지정 시 `adapter.invalid_option` throw (Redis 는 풀 모델 아님, ADR-110).
|
|
63
63
|
*/
|
|
64
64
|
pool?: any;
|
|
65
|
+
/**
|
|
66
|
+
* - 캐시 키 자동 prefix `mega:cache:<namespace>:` (ADR-064/213 — 멀티앱 충돌 차단, 옵트인).
|
|
67
|
+
*/
|
|
68
|
+
namespace?: string;
|
|
69
|
+
/**
|
|
70
|
+
* - `set` 의 ttl 미지정 시 적용할 디폴트(초). 0 = 무한 저장 옵트인(경고 억제). (ADR-216)
|
|
71
|
+
*/
|
|
72
|
+
defaultTtlSec?: number;
|
|
65
73
|
/**
|
|
66
74
|
* - ioredis passthrough (keyPrefix, commandTimeout, tls, retryStrategy, keepAlive, …).
|
|
67
75
|
*/
|
|
@@ -37,8 +37,8 @@ export class MegaSqliteAdapter extends MegaDbAdapter {
|
|
|
37
37
|
error?: string;
|
|
38
38
|
}>;
|
|
39
39
|
/**
|
|
40
|
-
* 누적 통계 + sqlite 특화
|
|
41
|
-
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, filename: string, inMemory: boolean, readonly: boolean, journalMode: string | undefined }}
|
|
40
|
+
* 누적 통계 + sqlite 특화 필드 + 공통 풀 코어 키(합성 — 단일 연결, ADR-216).
|
|
41
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, filename: string, inMemory: boolean, readonly: boolean, journalMode: string | undefined, pool: { total: number, active: number, idle: number, waiting: number } }}
|
|
42
42
|
*/
|
|
43
43
|
getStats(): ReturnType<import("./mega-adapter.js").MegaAdapter["getStats"]> & {
|
|
44
44
|
driver: string;
|
|
@@ -46,6 +46,12 @@ export class MegaSqliteAdapter extends MegaDbAdapter {
|
|
|
46
46
|
inMemory: boolean;
|
|
47
47
|
readonly: boolean;
|
|
48
48
|
journalMode: string | undefined;
|
|
49
|
+
pool: {
|
|
50
|
+
total: number;
|
|
51
|
+
active: number;
|
|
52
|
+
idle: number;
|
|
53
|
+
waiting: number;
|
|
54
|
+
};
|
|
49
55
|
};
|
|
50
56
|
/**
|
|
51
57
|
* 명시적 트랜잭션 경계 (ADR-010, ADR-105). manual `BEGIN/COMMIT/ROLLBACK` 으로
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* @returns {Artifact[]}
|
|
14
14
|
*/
|
|
15
15
|
export function planArtifacts(kind: string, rawName: string, opts: Record<string, any>, projectRoot: string): Artifact[];
|
|
16
|
+
/**
|
|
17
|
+
* 다음 ADR 번호를 해석한다 — `docs/adr/NNNN-*.md` 파일명 + 레거시 `docs/09` 헤딩(`### ADR-N:`) +
|
|
18
|
+
* 호출측이 모은 추가 번호(예: `git ls-tree origin/main` 의 원격 파일 — 병렬 task 충돌 회피)의
|
|
19
|
+
* 최댓값 + 1. 아무 ADR 도 없으면 1.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} projectRoot
|
|
22
|
+
* @param {number[]} [extraNumbers] - 로컬 밖에서 관측한 번호(원격 스캔 등).
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function nextAdrNumber(projectRoot: string, extraNumbers?: number[]): number;
|
|
16
26
|
/**
|
|
17
27
|
* 계획된 artifact 를 디스크에 쓴다. 이미 있으면 건너뛴다(force 면 덮어씀).
|
|
18
28
|
* @param {Artifact[]} artifacts
|
|
@@ -83,7 +93,7 @@ export function generate(kind: string, rawName: string, opts?: object, projectRo
|
|
|
83
93
|
skipped: string[];
|
|
84
94
|
};
|
|
85
95
|
/** 지원 generator 종류(roadmap §337 — 13종). */
|
|
86
|
-
export const GENERATOR_KINDS: readonly ["app", "controller", "channel", "service", "model", "middleware", "route", "schedule", "job", "worker", "locale", "adapter", "migration"];
|
|
96
|
+
export const GENERATOR_KINDS: readonly ["app", "controller", "channel", "service", "model", "middleware", "route", "schedule", "job", "worker", "locale", "adapter", "migration", "adr"];
|
|
87
97
|
/**
|
|
88
98
|
* 플러그인 scaffold manifest 의 토큰 계약(ADR-199) — `files[].path`/`files[].template` 의 `{{token}}`
|
|
89
99
|
* 에 쓸 수 있는 이름과 의미. 빌트인 generator 의 base 토큰과 동일 집합이라 템플릿 작성 관례가 하나다.
|