mega-framework 0.1.7 → 0.1.8
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/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- 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/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/README.md
CHANGED
|
@@ -39,6 +39,15 @@ npm test # 전체 테스트(실 인프라). .env 자동 로드(ADR-
|
|
|
39
39
|
npm run infra:down # 인프라 종료
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
> **워크스페이스 안내 (ADR-197)** — `sample/crud`·`sample/simple` 은 npm workspaces 라서 모든 의존성이
|
|
43
|
+
> **루트 `node_modules` 로 hoist** 된다. `sample/crud/node_modules` 가 안 생기는 것이 **정상**이며
|
|
44
|
+
> (sample 안에서 `npm install` 을 실행해도 루트에 설치된다), `mega-framework` 는
|
|
45
|
+
> `node_modules/mega-framework → ..` self-link 로 해석된다. 확인: `npm ls --workspace sample-crud`.
|
|
46
|
+
>
|
|
47
|
+
> **yarn 미지원** — yarn v1 은 workspaces 에 루트 `"private": true` 를 요구하는데 본 패키지는 npm 배포
|
|
48
|
+
> 대상(`private: false`)이라 `yarn install` 이 `Workspaces can only be enabled in private projects` 로
|
|
49
|
+
> 실패한다. **npm 만 사용**한다(lockfile 정본 = `package-lock.json` 1벌).
|
|
50
|
+
|
|
42
51
|
프로젝트 루트에 `mega.config.js`(글로벌) + 앱별 `apps/<name>/app.config.js` 를 둔다.
|
|
43
52
|
|
|
44
53
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mega-framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Node.js 마이크로프레임웍 + CLI — Slim의 가벼움 + Rails의 관례",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"test": "vitest run",
|
|
70
70
|
"test:watch": "vitest",
|
|
71
71
|
"test:coverage": "vitest run --coverage",
|
|
72
|
-
"lint": "eslint src test",
|
|
72
|
+
"lint": "eslint src test scripts",
|
|
73
73
|
"typecheck": "tsc -p jsconfig.json --noEmit",
|
|
74
74
|
"build:types": "rm -rf types && tsc -p tsconfig.build.json",
|
|
75
75
|
"format": "prettier -w src test packages",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"infra:reset": "docker compose down -v && docker compose up -d --wait",
|
|
80
80
|
"test:integration": "npm run infra:up && vitest run test/integration && npm run infra:down",
|
|
81
81
|
"test:unit": "vitest run test/unit",
|
|
82
|
+
"adr:index": "node scripts/build-adr-index.js",
|
|
82
83
|
"prepublishOnly": "npm run build:types && npm run lint && npm run typecheck && npm run test:unit",
|
|
83
|
-
"prepare": "npm run build:types",
|
|
84
84
|
"release": "bash scripts/publish.sh"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
package/sample/crud/.env
CHANGED
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
# NODE_ENV=development
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
# ── dev watch ignore (mega.config.js > watch, ADR-220) ───────────────────────
|
|
24
|
+
# `mega start --watch` 의 ignore 목록(콤마 구분 glob) — **여기가 정본**(숨은 코드 디폴트 없음).
|
|
25
|
+
# "런타임이 스스로 쓰거나 재시작 없이 반영되는" 영역을 나열한다. 폴더명을 바꿨으면 이 줄만 고친다.
|
|
26
|
+
# locales: dev 의 i18n saveMissing 자동 기입(안 빼면 재시작 무한 루프) / views: EJS 는 dev 에서
|
|
27
|
+
# 매 요청 재컴파일 / public: 정적 자산 / uploads·var: 사용자 업로드 등 런타임 데이터 /
|
|
28
|
+
# .mega: 마이그레이션 journal / .env*: 시크릿 변경은 수동 재시작.
|
|
29
|
+
# 비우면 모든 변경이 재시작 대상이다(기동 시 경고 1줄).
|
|
30
|
+
WATCH_IGNORE=**/locales/**,**/views/**,**/public/**,**/uploads/**,var/**,logs/**,tmp/**,.mega/**,**/node_modules/**,**/.git/**,**/*.test.js,**/*.spec.js,.env,.env.*,**/templates/**,data/**
|
|
31
|
+
|
|
23
32
|
# ── 서버 (mega.config.js > server) ───────────────────────────────────────────
|
|
24
33
|
# HTTP listen 포트 — server.port. CLI `--port N` 이 우선(ADR-146).
|
|
25
34
|
PORT=3000
|
package/sample/crud/.env.example
CHANGED
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
# NODE_ENV=development
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
# ── dev watch ignore (mega.config.js > watch, ADR-220) ───────────────────────
|
|
24
|
+
# `mega start --watch` 의 ignore 목록(콤마 구분 glob) — **여기가 정본**(숨은 코드 디폴트 없음).
|
|
25
|
+
# "런타임이 스스로 쓰거나 재시작 없이 반영되는" 영역을 나열한다. 폴더명을 바꿨으면 이 줄만 고친다.
|
|
26
|
+
# locales: dev 의 i18n saveMissing 자동 기입(안 빼면 재시작 무한 루프) / views: EJS 는 dev 에서
|
|
27
|
+
# 매 요청 재컴파일 / public: 정적 자산 / uploads·var: 사용자 업로드 등 런타임 데이터 /
|
|
28
|
+
# .mega: 마이그레이션 journal / .env*: 시크릿 변경은 수동 재시작.
|
|
29
|
+
# 비우면 모든 변경이 재시작 대상이다(기동 시 경고 1줄).
|
|
30
|
+
WATCH_IGNORE=**/locales/**,**/views/**,**/public/**,**/uploads/**,var/**,logs/**,tmp/**,.mega/**,**/node_modules/**,**/.git/**,**/*.test.js,**/*.spec.js,.env,.env.*
|
|
31
|
+
|
|
23
32
|
# ── 서버 (mega.config.js > server) ───────────────────────────────────────────
|
|
24
33
|
# HTTP listen 포트 — server.port. CLI `--port N` 이 우선(ADR-146).
|
|
25
34
|
PORT=3000
|
|
@@ -313,5 +313,16 @@
|
|
|
313
313
|
"csrf": {
|
|
314
314
|
"invalid_token": "Invalid csrf token",
|
|
315
315
|
"missing_secret": "Missing csrf secret"
|
|
316
|
-
}
|
|
316
|
+
},
|
|
317
|
+
"field_username": "아이디",
|
|
318
|
+
"field_username_ph": "예: hong123",
|
|
319
|
+
"field_username_required": "아이디를 입력하세요.",
|
|
320
|
+
"field_nickname": "별명",
|
|
321
|
+
"field_nickname_ph": "예: 길동이",
|
|
322
|
+
"field_nickname_required": "별명을 입력하세요.",
|
|
323
|
+
"optional": "선택",
|
|
324
|
+
"field_phone": "전화번호",
|
|
325
|
+
"field_phone_ph": "예: 010-1234-5678",
|
|
326
|
+
"col_username": "아이디",
|
|
327
|
+
"col_nickname": "별명"
|
|
317
328
|
}
|
|
@@ -313,5 +313,16 @@
|
|
|
313
313
|
},
|
|
314
314
|
"guide": {
|
|
315
315
|
"not_found": "가이드를 찾을 수 없습니다."
|
|
316
|
-
}
|
|
316
|
+
},
|
|
317
|
+
"field_username": "아이디",
|
|
318
|
+
"field_username_ph": "예: hong123",
|
|
319
|
+
"field_username_required": "아이디를 입력하세요.",
|
|
320
|
+
"field_nickname": "별명",
|
|
321
|
+
"field_nickname_ph": "예: 길동이",
|
|
322
|
+
"field_nickname_required": "별명을 입력하세요.",
|
|
323
|
+
"optional": "선택",
|
|
324
|
+
"field_phone": "전화번호",
|
|
325
|
+
"field_phone_ph": "예: 010-1234-5678",
|
|
326
|
+
"col_username": "아이디",
|
|
327
|
+
"col_nickname": "별명"
|
|
317
328
|
}
|
|
@@ -175,4 +175,11 @@ export default {
|
|
|
175
175
|
// OpenTelemetry 분산 트레이싱 — **config 블록이 아니라 .env 의 MEGA_OTEL_\*** 로 설정한다
|
|
176
176
|
// (MegaTracing.fromEnv, boot.js). MEGA_OTEL_ENABLED='true' + MEGA_OTEL_SERVICE_NAME 필수.
|
|
177
177
|
// (`tracing` 키는 스키마엔 있으나 현재 부팅이 소비하지 않음 — 죽은 설정 회피 위해 블록을 두지 않음.)
|
|
178
|
+
|
|
179
|
+
// dev watch(`mega start --watch`) ignore — .env 의 WATCH_IGNORE(콤마 구분 glob)가 정본이다
|
|
180
|
+
// (ADR-220, 숨은 코드 디폴트 없음). 폴더명을 바꾼 프로젝트는 .env 의 그 줄만 고치면 된다.
|
|
181
|
+
// 미설정이면 ignore 0 — 모든 변경이 재시작 대상(기동 시 경고 1줄).
|
|
182
|
+
watch: {
|
|
183
|
+
ignore: (process.env.WATCH_IGNORE ?? '').split(',').map((p) => p.trim()).filter(Boolean),
|
|
184
|
+
},
|
|
178
185
|
}
|
package/sample/crud/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"node": ">=20"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"dev": "mega start --watch",
|
|
10
|
+
"dev": "NODE_ENV=development mega start --watch",
|
|
11
11
|
"start": "NODE_ENV=production mega start",
|
|
12
12
|
"migrate": "mega migrate",
|
|
13
13
|
"migrate:down": "mega migrate:down",
|
|
@@ -25,4 +25,4 @@
|
|
|
25
25
|
"concurrently": "^9.0.0",
|
|
26
26
|
"vitest": "^4.1.8"
|
|
27
27
|
}
|
|
28
|
-
}
|
|
28
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# sample/crud — WS Hub 서버 기동 스크립트 (ADR-032/176)
|
|
4
4
|
#
|
|
5
5
|
# 별도 hub 프로세스(`mega-ws-hub` 바이너리, mega-framework bin)를 localhost:3100 에 띄운다.
|
|
6
|
-
# 앱(`
|
|
6
|
+
# 앱(`npm run dev` = `mega start`)이 app.config 의 `bridgeHub` 로 이 허브에 **자동 연결**한다(ADR-176).
|
|
7
7
|
#
|
|
8
8
|
# ⚠️ `mega-ws-hub` 는 `mega start` 와 달리 `.env` 를 자동 로드하지 않는다(직접 process.env 만 읽음,
|
|
9
9
|
# src/lib/ws-hub.js). 그래서 Node 20.6+ 의 `--env-file` 로 .env 를 주입한다.
|
|
@@ -22,15 +22,29 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
22
22
|
cd "$ROOT"
|
|
23
23
|
|
|
24
24
|
ENV_FILE=".env"
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
# mega-ws-hub 바이너리 탐색 — 두 설치 형상을 모두 지원한다:
|
|
27
|
+
# ① 독립 프로젝트(mega new 스캐폴드): 자기 node_modules 에 설치됨.
|
|
28
|
+
# ② 모노레포 workspaces(ADR-197): 의존성이 레포 루트로 hoist 되어 sample/crud 에는 node_modules 가
|
|
29
|
+
# 없다(정상). 루트의 mega-framework self-link 를 따라간다.
|
|
30
|
+
BIN=""
|
|
31
|
+
for candidate in \
|
|
32
|
+
"node_modules/mega-framework/bin/mega-ws-hub.js" \
|
|
33
|
+
"../../node_modules/mega-framework/bin/mega-ws-hub.js"; do
|
|
34
|
+
if [ -f "$candidate" ]; then
|
|
35
|
+
BIN="$candidate"
|
|
36
|
+
break
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
26
39
|
|
|
27
40
|
# 사전 점검 — 누락 시 silent 진행 금지, 이유를 명확히 알린다(P4/P7).
|
|
28
41
|
[ -f "$ENV_FILE" ] || {
|
|
29
42
|
echo "✗ $ROOT/$ENV_FILE 가 없습니다 — MEGA_WSHUB_TOKENS 등 허브 설정이 필요합니다." >&2
|
|
30
43
|
exit 1
|
|
31
44
|
}
|
|
32
|
-
[ -
|
|
33
|
-
echo "✗
|
|
45
|
+
[ -n "$BIN" ] || {
|
|
46
|
+
echo "✗ mega-ws-hub.js 를 찾지 못했습니다(로컬/루트 node_modules 모두) — 'npm install' 로 mega-framework 를 설치하세요." >&2
|
|
47
|
+
echo " (모노레포에서는 레포 루트에서 실행해야 합니다. yarn 은 미지원 — npm 사용.)" >&2
|
|
34
48
|
exit 1
|
|
35
49
|
}
|
|
36
50
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.8","results":[[":test/apps/main/index.test.js",{"duration":1.6905000000000001,"failed":false}]]}
|
|
@@ -165,6 +165,9 @@ export function resolveConnection(config, { driver, dbKey = 'database', dbConfli
|
|
|
165
165
|
return out
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** pool acquire 대기 한도 프레임워크 디폴트(ms) — 명시 0 으로 드라이버 무한 대기 옵트인(ADR-216). */
|
|
169
|
+
export const DEFAULT_ACQUIRE_TIMEOUT_MS = 10_000
|
|
170
|
+
|
|
168
171
|
/**
|
|
169
172
|
* pg 풀 매핑 — 값이 null 이면 미지원(throw), `{ key, divideBy? }` 면 키 이름 변경(+단위 변환).
|
|
170
173
|
* @type {Record<string, { key: string, divideBy?: number } | null>}
|
|
@@ -216,11 +219,11 @@ export const MONGO_POOL_SPEC = {
|
|
|
216
219
|
* @returns {Record<string, number>} 드라이버 풀 옵션 객체(빈 객체 가능).
|
|
217
220
|
*/
|
|
218
221
|
export function normalizePool(pool, spec, driver) {
|
|
219
|
-
if (pool === undefined) return {}
|
|
220
|
-
assertPlainObject('pool', pool, { driver })
|
|
221
222
|
/** @type {Record<string, number>} */
|
|
222
223
|
const out = {}
|
|
223
|
-
|
|
224
|
+
if (pool !== undefined) assertPlainObject('pool', pool, { driver })
|
|
225
|
+
const entries = pool === undefined ? [] : Object.entries(/** @type {Record<string, unknown>} */ (pool))
|
|
226
|
+
for (const [key, value] of entries) {
|
|
224
227
|
if (value === undefined) continue
|
|
225
228
|
const map = spec[key]
|
|
226
229
|
if (map === undefined) {
|
|
@@ -234,5 +237,13 @@ export function normalizePool(pool, spec, driver) {
|
|
|
234
237
|
const num = /** @type {number} */ (value)
|
|
235
238
|
out[map.key] = map.divideBy ? Math.floor(num / map.divideBy) : num
|
|
236
239
|
}
|
|
240
|
+
// 프레임워크 디폴트(ADR-216 G2 H-1): acquire 무한 대기(pg connectionTimeoutMillis=0 ·
|
|
241
|
+
// mongo waitQueueTimeoutMS=0 드라이버 디폴트)는 연결 leak 1건을 전체 서비스 행으로 키운다 —
|
|
242
|
+
// 미지정 시 10s 를 기본 적용한다(maria 드라이버 디폴트 10s 와 정렬). 명시 `acquireTimeoutMs: 0`
|
|
243
|
+
// 은 드라이버 무한 대기 의미 그대로 통과(무한 옵트인).
|
|
244
|
+
const acquireMap = spec.acquireTimeoutMs
|
|
245
|
+
if (acquireMap !== null && acquireMap !== undefined && out[acquireMap.key] === undefined) {
|
|
246
|
+
out[acquireMap.key] = DEFAULT_ACQUIRE_TIMEOUT_MS
|
|
247
|
+
}
|
|
237
248
|
return out
|
|
238
249
|
}
|
|
@@ -75,6 +75,8 @@ const KNOWN_OPTIONS = new Set(['serializer', 'extension'])
|
|
|
75
75
|
* @property {string} [basePath] - 캐시 파일 저장 디렉토리 (필수).
|
|
76
76
|
* @property {string} [dir] - `basePath` 의 별칭 (ADR-082 정합, 하위 호환).
|
|
77
77
|
* @property {{ serializer?: 'json' | 'raw', extension?: string }} [options]
|
|
78
|
+
* @property {string} [namespace] - 캐시 키 자동 prefix `mega:cache:<namespace>:` (ADR-064/213, 베이스 처리).
|
|
79
|
+
* @property {number} [defaultTtlSec] - `set` ttl 미지정 시 디폴트(초). 0 = 무한 옵트인. (ADR-216, 베이스 처리)
|
|
78
80
|
*/
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -94,7 +96,8 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
94
96
|
#extension
|
|
95
97
|
|
|
96
98
|
/**
|
|
97
|
-
* @param {FileConfig} [config] - services.caches.<key> 설정.
|
|
99
|
+
* @param {FileConfig} [config] - services.caches.<key> 설정. 베이스(MegaCacheAdapter)의
|
|
100
|
+
* `namespace`(ADR-064 자동 prefix)/`defaultTtlSec`(ADR-216) 도 여기서 받는다.
|
|
98
101
|
* @throws {MegaValidationError} `adapter.basepath_required` - basePath/dir 누락.
|
|
99
102
|
* @throws {MegaValidationError} `adapter.invalid_option` - 옵션 타입/미지원 키 오류.
|
|
100
103
|
*/
|
|
@@ -245,7 +248,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
245
248
|
*/
|
|
246
249
|
async get(key) {
|
|
247
250
|
return this._instrument('get', { key }, async () => {
|
|
248
|
-
const path = this.#pathFor(key)
|
|
251
|
+
const path = this.#pathFor(this._cacheKey(key))
|
|
249
252
|
let raw
|
|
250
253
|
try {
|
|
251
254
|
raw = await readFile(path, 'utf8')
|
|
@@ -278,6 +281,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
278
281
|
// TTL·직렬화 검증은 의도적으로 `_instrument` **밖**(fail-fast). 잘못된 인자는 디스크 I/O·hook·stats
|
|
279
282
|
// 이전에 거부 — 프로그래밍 오류를 instrumented 호출 통계에 섞지 않는다(L-1, redis 어댑터와 동일 결정).
|
|
280
283
|
this._assertTtl(ttl)
|
|
284
|
+
ttl = this._resolveTtl(ttl, key) // 미지정 → defaultTtlSec(ADR-216). 디폴트 값은 생성자에서 검증됨.
|
|
281
285
|
if (this.#serializer === 'raw' && typeof value !== 'string') {
|
|
282
286
|
throw new MegaValidationError('cache.unserializable', `file set("${key}"): serializer='raw' requires a string value (got ${typeof value}).`, {
|
|
283
287
|
details: { key, type: typeof value, serializer: 'raw' },
|
|
@@ -300,7 +304,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
300
304
|
value,
|
|
301
305
|
expiresAt: ttl !== undefined ? Date.now() + ttl * 1000 : null,
|
|
302
306
|
}
|
|
303
|
-
await this.#atomicWrite(this.#pathFor(key), envelope)
|
|
307
|
+
await this.#atomicWrite(this.#pathFor(this._cacheKey(key)), envelope)
|
|
304
308
|
})
|
|
305
309
|
}
|
|
306
310
|
|
|
@@ -312,7 +316,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
312
316
|
async del(key) {
|
|
313
317
|
return this._instrument('del', { key }, async () => {
|
|
314
318
|
try {
|
|
315
|
-
await unlink(this.#pathFor(key))
|
|
319
|
+
await unlink(this.#pathFor(this._cacheKey(key)))
|
|
316
320
|
} catch (err) {
|
|
317
321
|
// ENOENT = 이미 없음(del idempotent — 정상). 그 외 I/O 에러는 전파(무차별 삼킴 X).
|
|
318
322
|
if (/** @type {any} */ (err)?.code !== 'ENOENT') throw err
|
|
@@ -331,7 +335,7 @@ export class MegaFileAdapter extends MegaCacheAdapter {
|
|
|
331
335
|
*/
|
|
332
336
|
async has(key) {
|
|
333
337
|
return this._instrument('has', { key }, async () => {
|
|
334
|
-
const path = this.#pathFor(key)
|
|
338
|
+
const path = this.#pathFor(this._cacheKey(key))
|
|
335
339
|
let raw
|
|
336
340
|
try {
|
|
337
341
|
raw = await readFile(path, 'utf8')
|
|
@@ -199,11 +199,12 @@ export class MegaFileSessionAdapter extends MegaSessionAdapter {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* 누적 통계 + file 세션 특화.
|
|
203
|
-
*
|
|
202
|
+
* 누적 통계 + file 세션 특화. `cleanupIntervalMs` 노출(0=내부 타이머 off) — 만료 스캔 주기의
|
|
203
|
+
* 적용 여부를 운영/테스트가 확인하는 단일 창구(ADR-215).
|
|
204
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, basePath: string, ttlMs: number, cleanupIntervalMs: number }}
|
|
204
205
|
*/
|
|
205
206
|
getStats() {
|
|
206
|
-
return { ...super.getStats(), driver: 'file', basePath: this.#basePath, ttlMs: this.#ttlMs }
|
|
207
|
+
return { ...super.getStats(), driver: 'file', basePath: this.#basePath, ttlMs: this.#ttlMs, cleanupIntervalMs: this.#cleanupIntervalMs }
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
/**
|
|
@@ -298,19 +298,22 @@ export class MegaMariaAdapter extends MegaDbAdapter {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
|
-
* 누적 통계 + 풀 통계
|
|
302
|
-
*
|
|
301
|
+
* 누적 통계 + 풀 통계 — 공통 코어 키 `{ total, active, idle, waiting }` 은 4 driver 동일
|
|
302
|
+
* 형태(ADR-216 G2 H-2). `queue` 는 기존 소비자 하위 호환 별칭(= waiting). 연결 전이면 0.
|
|
303
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, active: number, idle: number, waiting: number, queue: number } }}
|
|
303
304
|
*/
|
|
304
305
|
getStats() {
|
|
305
306
|
const pool = this.#pool
|
|
307
|
+
const waiting = pool?.taskQueueSize() ?? 0
|
|
306
308
|
return {
|
|
307
309
|
...super.getStats(),
|
|
308
310
|
driver: 'mariadb',
|
|
309
311
|
pool: {
|
|
310
312
|
total: pool?.totalConnections() ?? 0,
|
|
311
|
-
idle: pool?.idleConnections() ?? 0,
|
|
312
313
|
active: pool?.activeConnections() ?? 0,
|
|
313
|
-
|
|
314
|
+
idle: pool?.idleConnections() ?? 0,
|
|
315
|
+
waiting,
|
|
316
|
+
queue: waiting,
|
|
314
317
|
},
|
|
315
318
|
}
|
|
316
319
|
}
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
* `ctx.cache(key)` 가 본 베이스 인스턴스를 반환. 구체: `MegaRedisAdapter` /
|
|
6
6
|
* `MegaFileAdapter` (ADR-082).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* **키 네임스페이스(ADR-064, 옵트인 — ADR-216 구현)**: `services.caches.<key>.namespace: '<name>'`
|
|
9
|
+
* 을 주면 get/set/del/has 의 키에 `mega:cache:<name>:` 가 자동 prefix 된다 — 멀티앱이 같은 redis 를
|
|
10
|
+
* 공유할 때의 키 충돌·상호 evict 차단(세션 `mega:sess:`·roster `ws:roster:` 와 대칭). 미지정 시
|
|
11
|
+
* 기존과 동일하게 raw key(하위 호환 — 기본 ON 전환은 메이저에서 재평가).
|
|
12
|
+
*
|
|
13
|
+
* **디폴트 TTL(ADR-216)**: `defaultTtlSec: <초>` 를 주면 `set` 의 ttl 미지정 호출에 적용된다 —
|
|
14
|
+
* 동적 키에 ttl 을 빠뜨려 무한 증가하는 풋건 방어. `defaultTtlSec: 0` = 무한 저장을 의식적으로
|
|
15
|
+
* 선택(경고 없음). 둘 다 미지정인 채 ttl 없는 set 이 호출되면 **인스턴스당 1회** process.emitWarning
|
|
16
|
+
* 으로 표면화한다(동작은 기존과 동일 — 무한 저장).
|
|
12
17
|
*
|
|
13
18
|
* @module adapters/mega-cache-adapter
|
|
14
19
|
*/
|
|
@@ -16,8 +21,21 @@ import { MegaInternalError, MegaValidationError } from '../errors/http-errors.js
|
|
|
16
21
|
import { MegaAdapter } from './mega-adapter.js'
|
|
17
22
|
|
|
18
23
|
export class MegaCacheAdapter extends MegaAdapter {
|
|
24
|
+
/** @type {string | null} ADR-064 자동 prefix (`mega:cache:<namespace>:`) — 미지정이면 null(raw key). */
|
|
25
|
+
#keyPrefix = null
|
|
26
|
+
|
|
27
|
+
/** @type {number | null} set 의 ttl 미지정 시 적용할 디폴트(초). null = 미설정. */
|
|
28
|
+
#defaultTtlSec = null
|
|
29
|
+
|
|
30
|
+
/** @type {boolean} `defaultTtlSec: 0` — 무한 저장 의식적 옵트인(경고 억제). */
|
|
31
|
+
#isInfiniteTtlOptIn = false
|
|
32
|
+
|
|
33
|
+
/** @type {boolean} 무기한 set 경고를 인스턴스당 1회로 제한. */
|
|
34
|
+
#hasWarnedNoTtl = false
|
|
35
|
+
|
|
19
36
|
/**
|
|
20
|
-
* @param {
|
|
37
|
+
* @param {{ namespace?: string, defaultTtlSec?: number } & Record<string, any>} [config]
|
|
38
|
+
* @throws {MegaValidationError} `adapter.invalid_option` - namespace/defaultTtlSec 형식 오류.
|
|
21
39
|
*/
|
|
22
40
|
constructor(config) {
|
|
23
41
|
super(config)
|
|
@@ -28,6 +46,65 @@ export class MegaCacheAdapter extends MegaAdapter {
|
|
|
28
46
|
{ details: { class: 'MegaCacheAdapter' } },
|
|
29
47
|
)
|
|
30
48
|
}
|
|
49
|
+
const ns = config?.namespace
|
|
50
|
+
if (ns !== undefined) {
|
|
51
|
+
// 콜론·공백은 prefix 구조(`mega:cache:<ns>:<key>`)의 구분자 혼동을 만든다 — 명시 거부.
|
|
52
|
+
if (typeof ns !== 'string' || ns.length === 0 || /[\s:]/.test(ns)) {
|
|
53
|
+
throw new MegaValidationError(
|
|
54
|
+
'adapter.invalid_option',
|
|
55
|
+
`cache "namespace" must be a non-empty string without spaces/colons (got ${JSON.stringify(ns)}).`,
|
|
56
|
+
{ details: { namespace: ns ?? null } },
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
this.#keyPrefix = `mega:cache:${ns}:`
|
|
60
|
+
}
|
|
61
|
+
const ttl = config?.defaultTtlSec
|
|
62
|
+
if (ttl !== undefined) {
|
|
63
|
+
if (ttl === 0) {
|
|
64
|
+
this.#isInfiniteTtlOptIn = true
|
|
65
|
+
} else if (!Number.isInteger(ttl) || ttl < 0) {
|
|
66
|
+
throw new MegaValidationError(
|
|
67
|
+
'adapter.invalid_option',
|
|
68
|
+
`cache "defaultTtlSec" must be a non-negative integer (0 = 무한 저장 옵트인, got ${JSON.stringify(ttl)}).`,
|
|
69
|
+
{ details: { defaultTtlSec: ttl } },
|
|
70
|
+
)
|
|
71
|
+
} else {
|
|
72
|
+
this.#defaultTtlSec = ttl
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 네임스페이스 적용 키 — 구체 어댑터의 get/set/del/has 가 저장소 접근 직전에 경유한다(ADR-064).
|
|
79
|
+
* @protected
|
|
80
|
+
* @param {string} key
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
_cacheKey(key) {
|
|
84
|
+
return this.#keyPrefix === null ? key : this.#keyPrefix + key
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `set` 의 유효 TTL 해석 — 명시 ttl 우선, 없으면 defaultTtlSec. 둘 다 없으면 무한 저장이며
|
|
89
|
+
* `defaultTtlSec: 0` 옵트인이 아닌 한 인스턴스당 1회 경고를 낸다(키 무한 증가 풋건 표면화 —
|
|
90
|
+
* 동작은 기존과 동일, ADR-216).
|
|
91
|
+
* @protected
|
|
92
|
+
* @param {number} [ttl] - 호출자가 명시한 ttl(초).
|
|
93
|
+
* @param {string} [key] - 경고 메시지용 예시 키.
|
|
94
|
+
* @returns {number | undefined} 적용할 ttl(초) — undefined 면 무한.
|
|
95
|
+
*/
|
|
96
|
+
_resolveTtl(ttl, key) {
|
|
97
|
+
if (ttl !== undefined) return ttl
|
|
98
|
+
if (this.#defaultTtlSec !== null) return this.#defaultTtlSec
|
|
99
|
+
if (!this.#isInfiniteTtlOptIn && !this.#hasWarnedNoTtl) {
|
|
100
|
+
this.#hasWarnedNoTtl = true
|
|
101
|
+
process.emitWarning(
|
|
102
|
+
`cache.set("${key ?? '?'}") without ttl and no defaultTtlSec configured — keys never expire and can grow unbounded. ` +
|
|
103
|
+
`Set services.caches.<key>.defaultTtlSec (or pass { ttl }), or defaultTtlSec: 0 to opt in to unbounded storage.`,
|
|
104
|
+
{ code: 'MEGA_CACHE_NO_TTL' },
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
return undefined
|
|
31
108
|
}
|
|
32
109
|
|
|
33
110
|
/**
|
|
@@ -41,7 +118,7 @@ export class MegaCacheAdapter extends MegaAdapter {
|
|
|
41
118
|
/**
|
|
42
119
|
* @param {string} _key
|
|
43
120
|
* @param {any} _value
|
|
44
|
-
* @param {{ ttl?: number }} [_opts] - `ttl` 미지정 시 무한
|
|
121
|
+
* @param {{ ttl?: number }} [_opts] - `ttl` 미지정 시 defaultTtlSec, 그것도 없으면 무한(초 단위, ADR-216).
|
|
45
122
|
* @returns {Promise<void>}
|
|
46
123
|
*/
|
|
47
124
|
async set(_key, _value, _opts = {}) {
|
|
@@ -30,7 +30,10 @@ export class MegaDbAdapter extends MegaAdapter {
|
|
|
30
30
|
* driver 별 구현 (postgres `BEGIN/COMMIT/ROLLBACK`, MongoDB `session.withTransaction`).
|
|
31
31
|
* nested 호출은 driver 별 (postgres SAVEPOINT, MongoDB throw `adapter.nested_transaction_unsupported`).
|
|
32
32
|
*
|
|
33
|
-
* `opts.isolation`(ADR-190) — SQL 격리수준 옵트인.
|
|
33
|
+
* `opts.isolation`(ADR-190) — SQL 격리수준 옵트인. **미지정이면 driver 디폴트가 적용되며 그
|
|
34
|
+
* 디폴트는 driver 마다 다르다**(ADR-216 G2 M-2): postgres = READ COMMITTED,
|
|
35
|
+
* mariadb(InnoDB) = REPEATABLE READ — 같은 `withTransaction(fn)` 코드가 driver 에 따라 다른
|
|
36
|
+
* 동시성 의미를 가진다. 이식성 있는 동시성 가정이 필요하면 isolation 을 명시할 것. driver 별 지원:
|
|
34
37
|
* postgres/maria 는 top-level 트랜잭션에 `SET TRANSACTION ISOLATION LEVEL` 로 반영(nested 엔 불가 —
|
|
35
38
|
* `adapter.nested_isolation_unsupported`), sqlite 는 항상 SERIALIZABLE 동작이라 'serializable' 만 수용,
|
|
36
39
|
* mongodb 는 SQL 격리수준 개념이 없어 지정 시 `adapter.invalid_option`.
|
|
@@ -137,6 +137,7 @@ function buildMongoUri({ host, port, user, password }) {
|
|
|
137
137
|
* @typedef {object} PoolCounters
|
|
138
138
|
* @property {number} created @property {number} closed
|
|
139
139
|
* @property {number} checkedOut @property {number} checkedIn
|
|
140
|
+
* @property {number} checkOutStarted @property {number} checkOutFailed
|
|
140
141
|
*/
|
|
141
142
|
|
|
142
143
|
export class MegaMongoAdapter extends MegaDbAdapter {
|
|
@@ -162,7 +163,7 @@ export class MegaMongoAdapter extends MegaDbAdapter {
|
|
|
162
163
|
*/
|
|
163
164
|
#txContext = new AsyncLocalStorage()
|
|
164
165
|
/** @type {PoolCounters} CMAP 이벤트 누적 풀 카운터. */
|
|
165
|
-
#pool = { created: 0, closed: 0, checkedOut: 0, checkedIn: 0 }
|
|
166
|
+
#pool = { created: 0, closed: 0, checkedOut: 0, checkedIn: 0, checkOutStarted: 0, checkOutFailed: 0 }
|
|
166
167
|
/**
|
|
167
168
|
* 등록한 CMAP 리스너 — disconnect 시 정확히 제거하기 위해 보관(누수·재연결 시 중복 방지).
|
|
168
169
|
* @type {Array<[string, (...args: any[]) => void]>}
|
|
@@ -296,23 +297,33 @@ export class MegaMongoAdapter extends MegaDbAdapter {
|
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
/**
|
|
299
|
-
* 누적 통계 + mongo 특화(driver/dbName + CMAP 풀 카운터).
|
|
300
|
-
*
|
|
300
|
+
* 누적 통계 + mongo 특화(driver/dbName + CMAP 풀 카운터). 공통 코어 키
|
|
301
|
+
* `{ total, active, idle, waiting }` 은 4 driver 동일 형태(ADR-216 G2 H-2 — CMAP 누적
|
|
302
|
+
* 카운터에서 파생: total=created-closed, active=checkedOut-checkedIn,
|
|
303
|
+
* waiting=checkOutStarted-(checkedOut+checkOutFailed)). 누적 원본도 유지(하위 호환).
|
|
304
|
+
* 연결 전이면 전부 0.
|
|
305
|
+
* @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 } }}
|
|
301
306
|
*/
|
|
302
307
|
getStats() {
|
|
303
|
-
const { created, closed, checkedOut, checkedIn } = this.#pool
|
|
308
|
+
const { created, closed, checkedOut, checkedIn, checkOutStarted, checkOutFailed } = this.#pool
|
|
309
|
+
const total = created - closed
|
|
310
|
+
const active = checkedOut - checkedIn
|
|
304
311
|
return {
|
|
305
312
|
...super.getStats(),
|
|
306
313
|
driver: 'mongodb',
|
|
307
314
|
dbName: this.#dbName,
|
|
308
315
|
pool: {
|
|
316
|
+
total,
|
|
317
|
+
active,
|
|
318
|
+
idle: total - active,
|
|
319
|
+
waiting: checkOutStarted - checkedOut - checkOutFailed,
|
|
309
320
|
created,
|
|
310
321
|
closed,
|
|
311
322
|
checkedOut,
|
|
312
323
|
checkedIn,
|
|
313
|
-
// 파생값 — open=살아있는 연결 수, inUse=현재 체크아웃된(작업 중) 연결 수.
|
|
314
|
-
open:
|
|
315
|
-
inUse:
|
|
324
|
+
// 파생값(기존 표면 유지) — open=살아있는 연결 수, inUse=현재 체크아웃된(작업 중) 연결 수.
|
|
325
|
+
open: total,
|
|
326
|
+
inUse: active,
|
|
316
327
|
},
|
|
317
328
|
}
|
|
318
329
|
}
|
|
@@ -389,6 +400,9 @@ export class MegaMongoAdapter extends MegaDbAdapter {
|
|
|
389
400
|
['connectionClosed', () => (this.#pool.closed += 1)],
|
|
390
401
|
['connectionCheckedOut', () => (this.#pool.checkedOut += 1)],
|
|
391
402
|
['connectionCheckedIn', () => (this.#pool.checkedIn += 1)],
|
|
403
|
+
// 공통 풀 키의 waiting 파생용(ADR-216) — started - (out + failed) = 현재 대기 수.
|
|
404
|
+
['connectionCheckOutStarted', () => (this.#pool.checkOutStarted += 1)],
|
|
405
|
+
['connectionCheckOutFailed', () => (this.#pool.checkOutFailed += 1)],
|
|
392
406
|
]
|
|
393
407
|
for (const [event, handler] of listeners) {
|
|
394
408
|
client.on(event, handler)
|
|
@@ -216,17 +216,21 @@ export class MegaPostgresAdapter extends MegaDbAdapter {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
|
-
* 누적 통계 + 풀 통계
|
|
220
|
-
*
|
|
219
|
+
* 누적 통계 + 풀 통계 — 공통 코어 키 `{ total, active, idle, waiting }` 은 4 driver 동일
|
|
220
|
+
* 형태(ADR-216 G2 H-2: 운영 대시보드가 driver 무관 코드로 "풀 포화" 를 묻게). 연결 전이면 0.
|
|
221
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, active: number, idle: number, waiting: number } }}
|
|
221
222
|
*/
|
|
222
223
|
getStats() {
|
|
223
224
|
const pool = this.#pool
|
|
225
|
+
const total = pool?.totalCount ?? 0
|
|
226
|
+
const idle = pool?.idleCount ?? 0
|
|
224
227
|
return {
|
|
225
228
|
...super.getStats(),
|
|
226
229
|
driver: 'postgres',
|
|
227
230
|
pool: {
|
|
228
|
-
total
|
|
229
|
-
|
|
231
|
+
total,
|
|
232
|
+
active: total - idle,
|
|
233
|
+
idle,
|
|
230
234
|
waiting: pool?.waitingCount ?? 0,
|
|
231
235
|
},
|
|
232
236
|
}
|
|
@@ -61,6 +61,8 @@ import * as Registry from './registry.js'
|
|
|
61
61
|
* @property {string} [host] @property {number} [port] @property {string} [user] @property {string} [password]
|
|
62
62
|
* @property {number} [db] - 논리 DB 번호 0~15 (connection 과 별개 축, url path 보다 우선).
|
|
63
63
|
* @property {any} [pool] - 미지원 — 지정 시 `adapter.invalid_option` throw (Redis 는 풀 모델 아님, ADR-110).
|
|
64
|
+
* @property {string} [namespace] - 캐시 키 자동 prefix `mega:cache:<namespace>:` (ADR-064/213 — 멀티앱 충돌 차단, 옵트인).
|
|
65
|
+
* @property {number} [defaultTtlSec] - `set` 의 ttl 미지정 시 적용할 디폴트(초). 0 = 무한 저장 옵트인(경고 억제). (ADR-216)
|
|
64
66
|
* @property {Record<string, any>} [options] - ioredis passthrough (keyPrefix, commandTimeout, tls, retryStrategy, keepAlive, …).
|
|
65
67
|
*/
|
|
66
68
|
|
|
@@ -267,7 +269,7 @@ export class MegaRedisAdapter extends MegaCacheAdapter {
|
|
|
267
269
|
*/
|
|
268
270
|
async get(key) {
|
|
269
271
|
return this._instrument('get', { key }, async () => {
|
|
270
|
-
const raw = await /** @type {import('ioredis').Redis} */ (this.#client).get(key)
|
|
272
|
+
const raw = await /** @type {import('ioredis').Redis} */ (this.#client).get(this._cacheKey(key))
|
|
271
273
|
if (raw === null) return null // miss
|
|
272
274
|
return JSON.parse(raw)
|
|
273
275
|
})
|
|
@@ -287,6 +289,8 @@ export class MegaRedisAdapter extends MegaCacheAdapter {
|
|
|
287
289
|
// I/O·hook·stats 누적 이전에 fail-fast 로 거부하는 게 맞다(인자 오류는 어댑터 "호출"이 아니라
|
|
288
290
|
// 프로그래밍 오류 — instrumented 호출 통계에 섞이면 안 됨). 정상 경로의 실제 I/O 만 _instrument 가 감싼다.
|
|
289
291
|
this._assertTtl(ttl)
|
|
292
|
+
ttl = this._resolveTtl(ttl, key) // 미지정 → defaultTtlSec(ADR-216). 디폴트 값은 생성자에서 검증됨.
|
|
293
|
+
key = this._cacheKey(key)
|
|
290
294
|
const raw = JSON.stringify(value)
|
|
291
295
|
if (raw === undefined) {
|
|
292
296
|
// JSON.stringify(undefined/함수/심볼) === undefined — 저장 시 silent 손상 대신 명시 거부.
|
|
@@ -309,7 +313,7 @@ export class MegaRedisAdapter extends MegaCacheAdapter {
|
|
|
309
313
|
*/
|
|
310
314
|
async del(key) {
|
|
311
315
|
return this._instrument('del', { key }, async () => {
|
|
312
|
-
await /** @type {import('ioredis').Redis} */ (this.#client).del(key)
|
|
316
|
+
await /** @type {import('ioredis').Redis} */ (this.#client).del(this._cacheKey(key))
|
|
313
317
|
})
|
|
314
318
|
}
|
|
315
319
|
|
|
@@ -320,7 +324,7 @@ export class MegaRedisAdapter extends MegaCacheAdapter {
|
|
|
320
324
|
*/
|
|
321
325
|
async has(key) {
|
|
322
326
|
return this._instrument('has', { key }, async () => {
|
|
323
|
-
const n = await /** @type {import('ioredis').Redis} */ (this.#client).exists(key)
|
|
327
|
+
const n = await /** @type {import('ioredis').Redis} */ (this.#client).exists(this._cacheKey(key))
|
|
324
328
|
return n === 1
|
|
325
329
|
})
|
|
326
330
|
}
|
|
@@ -219,10 +219,11 @@ export class MegaSqliteAdapter extends MegaDbAdapter {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* 누적 통계 + sqlite 특화
|
|
223
|
-
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, filename: string, inMemory: boolean, readonly: boolean, journalMode: string | undefined }}
|
|
222
|
+
* 누적 통계 + sqlite 특화 필드 + 공통 풀 코어 키(합성 — 단일 연결, ADR-216).
|
|
223
|
+
* @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 } }}
|
|
224
224
|
*/
|
|
225
225
|
getStats() {
|
|
226
|
+
const connected = this.state === 'connected'
|
|
226
227
|
return {
|
|
227
228
|
...super.getStats(),
|
|
228
229
|
driver: 'sqlite',
|
|
@@ -231,6 +232,9 @@ export class MegaSqliteAdapter extends MegaDbAdapter {
|
|
|
231
232
|
readonly: this.#openOptions.readonly,
|
|
232
233
|
// _connect() 에서 캐시한 불변값 사용 — 미연결이면 undefined(매 호출 PRAGMA 실행 제거, L-4).
|
|
233
234
|
journalMode: this.#journalMode,
|
|
235
|
+
// 공통 풀 코어 키(ADR-216 G2 H-2) — sqlite 는 connection-per-Database 단일 동기 연결이라
|
|
236
|
+
// 풀이 없다. 쿼리가 동기라 stats 를 읽는 시점엔 항상 유휴(active 0/waiting 0) — 합성 값.
|
|
237
|
+
pool: connected ? { total: 1, active: 0, idle: 1, waiting: 0 } : { total: 0, active: 0, idle: 0, waiting: 0 },
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
|