mega-framework 0.1.6 → 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.
Files changed (248) hide show
  1. package/README.md +9 -0
  2. package/bin/mega-ws-hub.js +2 -2
  3. package/package.json +33 -9
  4. package/sample/crud/.env +10 -1
  5. package/sample/crud/.env.example +10 -1
  6. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  7. package/sample/crud/.mega/journal/snapshot.json +261 -0
  8. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  9. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  10. package/sample/crud/apps/main/locales/server/en.json +12 -1
  11. package/sample/crud/apps/main/locales/server/ko.json +12 -1
  12. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  13. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  14. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  15. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  16. package/sample/crud/apps/main/models/note-model.js +79 -0
  17. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  18. package/sample/crud/apps/main/models/user-model.js +146 -0
  19. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  20. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  21. package/sample/crud/apps/main/routes/users.js +55 -10
  22. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  23. package/sample/crud/apps/main/services/auth-service.js +39 -24
  24. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  25. package/sample/crud/apps/main/services/note-service.js +6 -6
  26. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  27. package/sample/crud/apps/main/services/user-service.js +62 -21
  28. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  29. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  30. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  31. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  32. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  33. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  34. package/sample/crud/mega.config.js +10 -2
  35. package/sample/crud/package.json +3 -3
  36. package/sample/crud/scripts/start-ws-hub.sh +20 -6
  37. package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  38. package/sample/simple/package.json +2 -2
  39. package/src/adapters/adapter-manager.js +2 -1
  40. package/src/adapters/adapter-options.js +44 -3
  41. package/src/adapters/file-adapter.js +9 -5
  42. package/src/adapters/file-session-adapter.js +4 -3
  43. package/src/adapters/maria-adapter.js +33 -7
  44. package/src/adapters/mega-cache-adapter.js +83 -6
  45. package/src/adapters/mega-db-adapter.js +10 -1
  46. package/src/adapters/mongo-adapter.js +40 -8
  47. package/src/adapters/postgres-adapter.js +33 -6
  48. package/src/adapters/redis-adapter.js +7 -3
  49. package/src/adapters/sqlite-adapter.js +26 -3
  50. package/src/cli/commands/console-cmd.js +3 -1
  51. package/src/cli/commands/new.js +13 -3
  52. package/src/cli/commands/scaffold.js +173 -33
  53. package/src/cli/generators/index.js +140 -3
  54. package/src/cli/index.js +437 -155
  55. package/src/cli/watch.js +188 -0
  56. package/src/core/ajv-mapper.js +30 -3
  57. package/src/core/boot.js +464 -245
  58. package/src/core/cluster-metrics.js +13 -4
  59. package/src/core/ctx-builder.js +65 -3
  60. package/src/core/envelope.js +119 -12
  61. package/src/core/hub-link.js +89 -18
  62. package/src/core/i18n.js +11 -1
  63. package/src/core/index.js +7 -3
  64. package/src/core/mega-app.js +253 -505
  65. package/src/core/mega-cluster.js +4 -1
  66. package/src/core/mega-server.js +40 -9
  67. package/src/core/migration/dialect-registry.js +107 -0
  68. package/src/core/migration/dialects/README.md +62 -0
  69. package/src/core/migration/dialects/maria.js +496 -0
  70. package/src/core/migration/dialects/mongo.js +824 -0
  71. package/src/core/migration/dialects/postgres.js +563 -0
  72. package/src/core/migration/dialects/sqlite.js +476 -0
  73. package/src/core/migration/differ.js +456 -0
  74. package/src/core/migration/generate.js +508 -0
  75. package/src/core/migration/journal.js +167 -0
  76. package/src/core/migration/model-scan.js +84 -0
  77. package/src/core/migration/mongo-migration-db.js +97 -0
  78. package/src/core/migration/schema-builder.js +400 -0
  79. package/src/core/migration/schema-validator.js +315 -0
  80. package/src/core/migration-lock.js +205 -0
  81. package/src/core/migration-runner.js +166 -38
  82. package/src/core/multipart.js +28 -5
  83. package/src/core/pipeline.js +131 -0
  84. package/src/core/router.js +70 -65
  85. package/src/core/scope-registry.js +1 -0
  86. package/src/core/security.js +70 -12
  87. package/src/core/session-store.js +14 -1
  88. package/src/core/workers-manager.js +12 -1
  89. package/src/core/ws-cluster.js +10 -3
  90. package/src/core/ws-message.js +48 -4
  91. package/src/core/ws-presence.js +636 -0
  92. package/src/core/ws-roster.js +50 -8
  93. package/src/core/ws-upgrade.js +223 -12
  94. package/src/index.js +1 -1
  95. package/src/lib/hub-protocol.js +29 -0
  96. package/src/lib/mega-circuit-breaker.js +5 -3
  97. package/src/lib/mega-health.js +35 -4
  98. package/src/lib/mega-job-queue.js +151 -34
  99. package/src/lib/mega-job.js +37 -1
  100. package/src/lib/mega-metrics.js +31 -13
  101. package/src/lib/mega-plugin.js +34 -3
  102. package/src/lib/mega-schedule.js +40 -22
  103. package/src/lib/mega-shutdown.js +114 -39
  104. package/src/lib/mega-tracing.js +66 -19
  105. package/src/lib/mega-worker.js +33 -6
  106. package/src/lib/otel-resource.js +36 -0
  107. package/src/{cli → lib}/ws-hub.js +139 -15
  108. package/src/models/crud-sql-builder.js +133 -0
  109. package/src/models/mega-model.js +82 -2
  110. package/src/models/model-crud.js +483 -0
  111. package/src/models/mongo-crud.js +285 -0
  112. package/templates/adr/code.tpl +23 -0
  113. package/templates/model/code-mongo.tpl +35 -0
  114. package/templates/model/code.tpl +15 -1
  115. package/templates/model/test-mongo.tpl +38 -0
  116. package/templates/model/test.tpl +4 -0
  117. package/types/adapters/adapter-manager.d.ts +95 -0
  118. package/types/adapters/adapter-options.d.ts +93 -0
  119. package/types/adapters/file-adapter.d.ts +105 -0
  120. package/types/adapters/file-session-adapter.d.ts +103 -0
  121. package/types/adapters/index.d.ts +20 -0
  122. package/types/adapters/maria-adapter.d.ts +117 -0
  123. package/types/adapters/mega-adapter.d.ts +215 -0
  124. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  125. package/types/adapters/mega-cache-adapter.d.ts +73 -0
  126. package/types/adapters/mega-db-adapter.d.ts +50 -0
  127. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  128. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  129. package/types/adapters/mega-session-adapter.d.ts +32 -0
  130. package/types/adapters/mongo-adapter.d.ts +150 -0
  131. package/types/adapters/nats-adapter.d.ts +108 -0
  132. package/types/adapters/postgres-adapter.d.ts +141 -0
  133. package/types/adapters/redis-adapter.d.ts +78 -0
  134. package/types/adapters/redis-session-adapter.d.ts +82 -0
  135. package/types/adapters/redlock-adapter.d.ts +149 -0
  136. package/types/adapters/registry.d.ts +46 -0
  137. package/types/adapters/sqlite-adapter.d.ts +112 -0
  138. package/types/auth/index.d.ts +24 -0
  139. package/types/cli/commands/console-cmd.d.ts +37 -0
  140. package/types/cli/commands/new.d.ts +16 -0
  141. package/types/cli/commands/routes.d.ts +36 -0
  142. package/types/cli/commands/scaffold.d.ts +78 -0
  143. package/types/cli/commands/test-cmd.d.ts +14 -0
  144. package/types/cli/generators/index.d.ts +122 -0
  145. package/types/cli/index.d.ts +234 -0
  146. package/types/cli/template-engine.d.ts +40 -0
  147. package/types/cli/watch.d.ts +59 -0
  148. package/types/core/ajv-mapper.d.ts +27 -0
  149. package/types/core/boot.d.ts +233 -0
  150. package/types/core/cluster-metrics.d.ts +52 -0
  151. package/types/core/config-loader.d.ts +13 -0
  152. package/types/core/config-validator.d.ts +30 -0
  153. package/types/core/ctx-builder.d.ts +103 -0
  154. package/types/core/envelope.d.ts +79 -0
  155. package/types/core/error-mapper.d.ts +17 -0
  156. package/types/core/formbody.d.ts +41 -0
  157. package/types/core/hub-link.d.ts +266 -0
  158. package/types/core/i18n.d.ts +178 -0
  159. package/types/core/index.d.ts +28 -0
  160. package/types/core/mega-app.d.ts +529 -0
  161. package/types/core/mega-cluster.d.ts +104 -0
  162. package/types/core/mega-server.d.ts +91 -0
  163. package/types/core/mega-service.d.ts +31 -0
  164. package/types/core/migration/dialect-registry.d.ts +22 -0
  165. package/types/core/migration/dialects/maria.d.ts +99 -0
  166. package/types/core/migration/dialects/mongo.d.ts +89 -0
  167. package/types/core/migration/dialects/postgres.d.ts +117 -0
  168. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  169. package/types/core/migration/differ.d.ts +47 -0
  170. package/types/core/migration/generate.d.ts +56 -0
  171. package/types/core/migration/journal.d.ts +52 -0
  172. package/types/core/migration/model-scan.d.ts +19 -0
  173. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  174. package/types/core/migration/schema-builder.d.ts +197 -0
  175. package/types/core/migration/schema-validator.d.ts +20 -0
  176. package/types/core/migration-lock.d.ts +33 -0
  177. package/types/core/migration-runner.d.ts +101 -0
  178. package/types/core/multipart.d.ts +86 -0
  179. package/types/core/openapi.d.ts +62 -0
  180. package/types/core/pipeline.d.ts +93 -0
  181. package/types/core/router.d.ts +159 -0
  182. package/types/core/routes-loader.d.ts +21 -0
  183. package/types/core/scope-registry.d.ts +14 -0
  184. package/types/core/security.d.ts +77 -0
  185. package/types/core/services-loader.d.ts +27 -0
  186. package/types/core/session-cleanup-schedule.d.ts +19 -0
  187. package/types/core/session-store.d.ts +25 -0
  188. package/types/core/session.d.ts +77 -0
  189. package/types/core/static-assets.d.ts +73 -0
  190. package/types/core/template.d.ts +106 -0
  191. package/types/core/workers-manager.d.ts +79 -0
  192. package/types/core/ws-cluster.d.ts +208 -0
  193. package/types/core/ws-compression.d.ts +112 -0
  194. package/types/core/ws-controller.d.ts +65 -0
  195. package/types/core/ws-message.d.ts +106 -0
  196. package/types/core/ws-presence.d.ts +273 -0
  197. package/types/core/ws-roster.d.ts +108 -0
  198. package/types/core/ws-upgrade.d.ts +260 -0
  199. package/types/errors/config-error.d.ts +10 -0
  200. package/types/errors/http-errors.d.ts +120 -0
  201. package/types/errors/index.d.ts +3 -0
  202. package/types/errors/mega-error.d.ts +32 -0
  203. package/types/index.d.ts +39 -0
  204. package/types/lib/asp/config.d.ts +49 -0
  205. package/types/lib/asp/crypto.d.ts +43 -0
  206. package/types/lib/asp/errors.d.ts +30 -0
  207. package/types/lib/asp/nonce-cache.d.ts +52 -0
  208. package/types/lib/asp/plugin.d.ts +30 -0
  209. package/types/lib/asp/ws-terminator.d.ts +45 -0
  210. package/types/lib/env-mapper.d.ts +14 -0
  211. package/types/lib/hub-protocol.d.ts +106 -0
  212. package/types/lib/index.d.ts +22 -0
  213. package/types/lib/logger/telegram-core.d.ts +104 -0
  214. package/types/lib/logger/telegram-transport.d.ts +45 -0
  215. package/types/lib/mega-brute-force.d.ts +66 -0
  216. package/types/lib/mega-circuit-breaker.d.ts +243 -0
  217. package/types/lib/mega-cron.d.ts +66 -0
  218. package/types/lib/mega-hash.d.ts +32 -0
  219. package/types/lib/mega-health.d.ts +48 -0
  220. package/types/lib/mega-job-queue.d.ts +188 -0
  221. package/types/lib/mega-job-worker.d.ts +130 -0
  222. package/types/lib/mega-job.d.ts +145 -0
  223. package/types/lib/mega-logger.d.ts +45 -0
  224. package/types/lib/mega-metrics.d.ts +285 -0
  225. package/types/lib/mega-plugin.d.ts +245 -0
  226. package/types/lib/mega-retry.d.ts +85 -0
  227. package/types/lib/mega-schedule.d.ts +260 -0
  228. package/types/lib/mega-shutdown.d.ts +135 -0
  229. package/types/lib/mega-tracing.d.ts +224 -0
  230. package/types/lib/mega-worker.d.ts +129 -0
  231. package/types/lib/otel-resource.d.ts +16 -0
  232. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  233. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  234. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  235. package/types/lib/ws-hub.d.ts +259 -0
  236. package/types/models/crud-sql-builder.d.ts +48 -0
  237. package/types/models/index.d.ts +1 -0
  238. package/types/models/mega-model.d.ts +138 -0
  239. package/types/models/model-crud.d.ts +82 -0
  240. package/types/models/mongo-crud.d.ts +59 -0
  241. package/types/test/index.d.ts +84 -0
  242. package/.env +0 -127
  243. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  244. package/sample/crud/apps/main/models/note.js +0 -71
  245. package/sample/crud/apps/main/models/user.js +0 -86
  246. package/sample/crud/package-lock.json +0 -5665
  247. package/sample/crud/yarn.lock +0 -2142
  248. package/sample/simple/package-lock.json +0 -1851
@@ -73,9 +73,9 @@
73
73
  * @module adapters/maria-adapter
74
74
  */
75
75
  import { AsyncLocalStorage } from 'node:async_hooks'
76
- import { MegaValidationError } from '../errors/http-errors.js'
76
+ import { MegaInternalError, MegaValidationError } from '../errors/http-errors.js'
77
77
  import { MegaDbAdapter } from './mega-db-adapter.js'
78
- import { resolveConnection, normalizePool, assertPlainObject, MARIA_POOL_SPEC } from './adapter-options.js'
78
+ import { resolveConnection, normalizePool, assertPlainObject, resolveTxIsolation, MARIA_POOL_SPEC } from './adapter-options.js'
79
79
  import * as Registry from './registry.js'
80
80
 
81
81
  /**
@@ -165,6 +165,11 @@ function resolveBigIntStrategy(options) {
165
165
  }
166
166
 
167
167
  export class MegaMariaAdapter extends MegaDbAdapter {
168
+ /** driver 식별자 — dialect 디스패치(getDialect)·CRUD 의 단일 출처(ADR-212). @returns {'mariadb'} */
169
+ get driver() {
170
+ return 'mariadb'
171
+ }
172
+
168
173
  /** @type {import('mariadb').Pool | null} 연결된 Pool 인스턴스 (connect 후에만). */
169
174
  #pool = null
170
175
  /** @type {Record<string, unknown>} _connect 의 `createPool()` 인자 (url 도 discrete 로 파싱해 항상 객체, ADR-109). */
@@ -293,19 +298,22 @@ export class MegaMariaAdapter extends MegaDbAdapter {
293
298
  }
294
299
 
295
300
  /**
296
- * 누적 통계 + 풀 통계(total/idle/active/queue). 연결 전이면 통계는 0.
297
- * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, idle: number, active: number, queue: number } }}
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 } }}
298
304
  */
299
305
  getStats() {
300
306
  const pool = this.#pool
307
+ const waiting = pool?.taskQueueSize() ?? 0
301
308
  return {
302
309
  ...super.getStats(),
303
310
  driver: 'mariadb',
304
311
  pool: {
305
312
  total: pool?.totalConnections() ?? 0,
306
- idle: pool?.idleConnections() ?? 0,
307
313
  active: pool?.activeConnections() ?? 0,
308
- queue: pool?.taskQueueSize() ?? 0,
314
+ idle: pool?.idleConnections() ?? 0,
315
+ waiting,
316
+ queue: waiting,
309
317
  },
310
318
  }
311
319
  }
@@ -318,23 +326,41 @@ export class MegaMariaAdapter extends MegaDbAdapter {
318
326
  * RELEASE SAVEPOINT) 후 반환값을 그대로 돌려주고, throw 시 rollback(또는 ROLLBACK TO SAVEPOINT)
319
327
  * 후 원본 에러 re-throw.
320
328
  *
329
+ * `opts.isolation`(ADR-190) — top-level 이면 `beginTransaction()` **직전에** 같은 연결에서
330
+ * `SET TRANSACTION ISOLATION LEVEL` 을 실행한다(MariaDB 의 `SET TRANSACTION` 은 같은 세션의
331
+ * **다음** 트랜잭션 1회에만 적용 — 공식 문서). 격리수준은 트랜잭션 시작 시 고정되므로
332
+ * nested(SAVEPOINT) 호출에 지정하면 `adapter.nested_isolation_unsupported` 로 거부한다.
333
+ *
321
334
  * hook(`onCallStart/onCallEnd`) + 상태 검증 + stats 누적은 `_instrument` 가 처리한다(ADR-077).
322
335
  *
323
336
  * @template T
324
337
  * @param {(conn: import('mariadb').PoolConnection) => Promise<T> | T} fn
338
+ * @param {{ isolation?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable' }} [opts] - 트랜잭션 옵션(ADR-190).
325
339
  * @returns {Promise<T>}
340
+ * @throws {MegaInternalError} `adapter.nested_isolation_unsupported` - nested 호출에 isolation 지정.
326
341
  */
327
- async withTransaction(fn) {
342
+ async withTransaction(fn, opts) {
343
+ // 옵션 검증은 연결 상태와 무관한 설정 오류라 _instrument(상태 검증) 전에 fail-fast 한다.
344
+ const isolationSql = resolveTxIsolation(opts?.isolation, 'mariadb')
328
345
  return this._instrument('withTransaction', { table: undefined }, async () => {
329
346
  const existing = this.#txContext.getStore()
330
347
  // nested — 진행 중 트랜잭션이 있으면 같은 연결에 SAVEPOINT 를 건다(Sqlite 와 분기).
331
348
  if (existing !== undefined) {
349
+ if (isolationSql !== undefined) {
350
+ throw new MegaInternalError(
351
+ 'adapter.nested_isolation_unsupported',
352
+ `${this.constructor.name}.withTransaction() cannot set an isolation level on a nested (SAVEPOINT) transaction — isolation is fixed when the top-level transaction starts (ADR-190).`,
353
+ { details: { adapter: this.constructor.name, isolation: opts?.isolation } },
354
+ )
355
+ }
332
356
  return this.#runSavepoint(existing, fn)
333
357
  }
334
358
  // top-level — 풀에서 연결 1개 획득 후 beginTransaction/commit/rollback.
335
359
  const pool = /** @type {import('mariadb').Pool} */ (this.#pool)
336
360
  const conn = await pool.getConnection()
337
361
  try {
362
+ // SET TRANSACTION 은 다음 트랜잭션 1회에만 적용 — 반드시 beginTransaction 직전, 같은 연결에서.
363
+ if (isolationSql !== undefined) await conn.query(`SET TRANSACTION ISOLATION LEVEL ${isolationSql}`)
338
364
  await conn.beginTransaction()
339
365
  let result
340
366
  try {
@@ -5,10 +5,15 @@
5
5
  * `ctx.cache(key)` 가 본 베이스 인스턴스를 반환. 구체: `MegaRedisAdapter` /
6
6
  * `MegaFileAdapter` (ADR-082).
7
7
  *
8
- * ⚠️ **키 네임스페이스(ADR-064)**: ADR-064 `mega:cache:<appName>:<key>` 자동 prefix 를 *결정*했으나
9
- * **현재 코어에 미구현**이다(get/set/del raw `key` 그대로 사용). 멀티앱이 같은 redis 를 공유하면
10
- * 충돌 위험이 있으니, **현재는 사용자가 키에 앱별 네임스페이스를 직접 붙여야 한다**. 자동 prefix 구현은
11
- * 후속 과제(ADR-064 open). 단일앱( sample)에선 무관.
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 {object} [config]
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,11 +30,20 @@ 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 격리수준 옵트인. **미지정이면 driver 디폴트가 적용되며 그
34
+ * 디폴트는 driver 마다 다르다**(ADR-216 G2 M-2): postgres = READ COMMITTED,
35
+ * mariadb(InnoDB) = REPEATABLE READ — 같은 `withTransaction(fn)` 코드가 driver 에 따라 다른
36
+ * 동시성 의미를 가진다. 이식성 있는 동시성 가정이 필요하면 isolation 을 명시할 것. driver 별 지원:
37
+ * postgres/maria 는 top-level 트랜잭션에 `SET TRANSACTION ISOLATION LEVEL` 로 반영(nested 엔 불가 —
38
+ * `adapter.nested_isolation_unsupported`), sqlite 는 항상 SERIALIZABLE 동작이라 'serializable' 만 수용,
39
+ * mongodb 는 SQL 격리수준 개념이 없어 지정 시 `adapter.invalid_option`.
40
+ *
33
41
  * @template T
34
42
  * @param {(db: any) => Promise<T>} _fn - 트랜잭션 컨텍스트의 `db` 를 받는 콜백.
43
+ * @param {{ isolation?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable' }} [_opts] - 트랜잭션 옵션(ADR-190).
35
44
  * @returns {Promise<T>}
36
45
  */
37
- async withTransaction(_fn) {
46
+ async withTransaction(_fn, _opts) {
38
47
  return this._notImplemented('withTransaction')
39
48
  }
40
49
 
@@ -137,9 +137,15 @@ 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 {
144
+ /** driver 식별자 — dialect 디스패치·CRUD 의 단일 출처(ADR-212). mongo 는 SQL CRUD 미지원(P3). @returns {'mongodb'} */
145
+ get driver() {
146
+ return 'mongodb'
147
+ }
148
+
143
149
  /** @type {import('mongodb').MongoClient | null} 연결된 MongoClient (connect 후에만). */
144
150
  #client = null
145
151
  /** @type {import('mongodb').Db | null} 선택된 Db 인스턴스 (connect 후에만). */
@@ -157,7 +163,7 @@ export class MegaMongoAdapter extends MegaDbAdapter {
157
163
  */
158
164
  #txContext = new AsyncLocalStorage()
159
165
  /** @type {PoolCounters} CMAP 이벤트 누적 풀 카운터. */
160
- #pool = { created: 0, closed: 0, checkedOut: 0, checkedIn: 0 }
166
+ #pool = { created: 0, closed: 0, checkedOut: 0, checkedIn: 0, checkOutStarted: 0, checkOutFailed: 0 }
161
167
  /**
162
168
  * 등록한 CMAP 리스너 — disconnect 시 정확히 제거하기 위해 보관(누수·재연결 시 중복 방지).
163
169
  * @type {Array<[string, (...args: any[]) => void]>}
@@ -291,23 +297,33 @@ export class MegaMongoAdapter extends MegaDbAdapter {
291
297
  }
292
298
 
293
299
  /**
294
- * 누적 통계 + mongo 특화(driver/dbName + CMAP 풀 카운터). 연결 전이면 카운터는 0.
295
- * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, dbName: string, pool: { created: number, closed: number, checkedOut: number, checkedIn: number, open: number, inUse: number } }}
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 } }}
296
306
  */
297
307
  getStats() {
298
- 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
299
311
  return {
300
312
  ...super.getStats(),
301
313
  driver: 'mongodb',
302
314
  dbName: this.#dbName,
303
315
  pool: {
316
+ total,
317
+ active,
318
+ idle: total - active,
319
+ waiting: checkOutStarted - checkedOut - checkOutFailed,
304
320
  created,
305
321
  closed,
306
322
  checkedOut,
307
323
  checkedIn,
308
- // 파생값 — open=살아있는 연결 수, inUse=현재 체크아웃된(작업 중) 연결 수.
309
- open: created - closed,
310
- inUse: checkedOut - checkedIn,
324
+ // 파생값(기존 표면 유지) — open=살아있는 연결 수, inUse=현재 체크아웃된(작업 중) 연결 수.
325
+ open: total,
326
+ inUse: active,
311
327
  },
312
328
  }
313
329
  }
@@ -320,14 +336,27 @@ export class MegaMongoAdapter extends MegaDbAdapter {
320
336
  * throw 시 abort 후 원본 에러를 re-throw 한다. nested 호출은 ALS 로 감지해 거부(Sqlite 와 동일).
321
337
  * `session.endSession()` 은 `finally` 에서 반드시 호출(leak 방지).
322
338
  *
339
+ * `opts.isolation`(ADR-190) — MongoDB 트랜잭션에는 SQL 격리수준 개념이 없다(snapshot 의미는 driver/
340
+ * readConcern 관할). 지정 시 `adapter.invalid_option` 으로 명시 거부한다(조용히 무시 X — P4).
341
+ *
323
342
  * hook(`onCallStart/onCallEnd`) + 상태 검증 + stats 누적은 `_instrument` 가 처리한다(ADR-077).
324
343
  *
325
344
  * @template T
326
345
  * @param {(db: import('mongodb').Db, session: import('mongodb').ClientSession) => Promise<T> | T} fn
346
+ * @param {{ isolation?: never }} [opts] - 트랜잭션 옵션(ADR-190) — mongodb 는 isolation 미지원.
327
347
  * @returns {Promise<T>}
328
348
  * @throws {MegaInternalError} `adapter.nested_transaction_unsupported` - 이미 트랜잭션 진행 중.
349
+ * @throws {MegaValidationError} `adapter.invalid_option` - isolation 지정(미지원).
329
350
  */
330
- async withTransaction(fn) {
351
+ async withTransaction(fn, opts) {
352
+ // 옵션 검증은 연결 상태와 무관한 설정 오류라 _instrument(상태 검증) 전에 fail-fast 한다.
353
+ if (opts?.isolation !== undefined) {
354
+ throw new MegaValidationError(
355
+ 'adapter.invalid_option',
356
+ 'mongodb: SQL isolation levels do not apply to MongoDB transactions — remove the "isolation" option (snapshot semantics are managed by the driver/readConcern).',
357
+ { details: { driver: 'mongodb', option: 'isolation', value: opts.isolation } },
358
+ )
359
+ }
331
360
  return this._instrument('withTransaction', { table: undefined }, async () => {
332
361
  // nested 거부 — 진행 중 트랜잭션이 있으면 즉시 throw (Sqlite 와 동일 정책·에러 코드, ADR-108).
333
362
  // 검사를 driver 호출 전에 두어, standalone(트랜잭션 미지원)에서도 nested 거부는 동작한다.
@@ -371,6 +400,9 @@ export class MegaMongoAdapter extends MegaDbAdapter {
371
400
  ['connectionClosed', () => (this.#pool.closed += 1)],
372
401
  ['connectionCheckedOut', () => (this.#pool.checkedOut += 1)],
373
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)],
374
406
  ]
375
407
  for (const [event, handler] of listeners) {
376
408
  client.on(event, handler)
@@ -67,8 +67,9 @@
67
67
  * @module adapters/postgres-adapter
68
68
  */
69
69
  import { AsyncLocalStorage } from 'node:async_hooks'
70
+ import { MegaInternalError } from '../errors/http-errors.js'
70
71
  import { MegaDbAdapter } from './mega-db-adapter.js'
71
- import { resolveConnection, normalizePool, assertPlainObject, PG_POOL_SPEC } from './adapter-options.js'
72
+ import { resolveConnection, normalizePool, assertPlainObject, resolveTxIsolation, PG_POOL_SPEC } from './adapter-options.js'
72
73
  import * as Registry from './registry.js'
73
74
 
74
75
  /**
@@ -89,6 +90,11 @@ import * as Registry from './registry.js'
89
90
  */
90
91
 
91
92
  export class MegaPostgresAdapter extends MegaDbAdapter {
93
+ /** driver 식별자 — dialect 디스패치(getDialect)·CRUD 의 단일 출처(ADR-212). @returns {'postgres'} */
94
+ get driver() {
95
+ return 'postgres'
96
+ }
97
+
92
98
  /** @type {import('pg').Pool | null} 연결된 Pool 인스턴스 (connect 후에만). */
93
99
  #pool = null
94
100
  /** @type {import('pg').PoolConfig} _connect 에서 `new Pool()` 에 넘길 설정 (생성자에서 구성·고정). */
@@ -210,17 +216,21 @@ export class MegaPostgresAdapter extends MegaDbAdapter {
210
216
  }
211
217
 
212
218
  /**
213
- * 누적 통계 + 풀 통계(total/idle/waiting). 연결 전이면 통계는 0.
214
- * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, pool: { total: number, idle: number, waiting: number } }}
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 } }}
215
222
  */
216
223
  getStats() {
217
224
  const pool = this.#pool
225
+ const total = pool?.totalCount ?? 0
226
+ const idle = pool?.idleCount ?? 0
218
227
  return {
219
228
  ...super.getStats(),
220
229
  driver: 'postgres',
221
230
  pool: {
222
- total: pool?.totalCount ?? 0,
223
- idle: pool?.idleCount ?? 0,
231
+ total,
232
+ active: total - idle,
233
+ idle,
224
234
  waiting: pool?.waitingCount ?? 0,
225
235
  },
226
236
  }
@@ -234,17 +244,32 @@ export class MegaPostgresAdapter extends MegaDbAdapter {
234
244
  * RELEASE SAVEPOINT) 후 `fn` 반환값을 그대로 돌려주고, throw 시 ROLLBACK(또는 ROLLBACK TO
235
245
  * SAVEPOINT) 후 원본 에러를 re-throw 한다.
236
246
  *
247
+ * `opts.isolation`(ADR-190) — top-level 이면 `BEGIN` 직후 `SET TRANSACTION ISOLATION LEVEL` 로
248
+ * 반영한다(PostgreSQL 은 트랜잭션 첫 쿼리 전이라 유효). 격리수준은 트랜잭션 시작 시 고정되므로
249
+ * nested(SAVEPOINT) 호출에 지정하면 `adapter.nested_isolation_unsupported` 로 거부한다.
250
+ *
237
251
  * hook(`onCallStart/onCallEnd`) + 상태 검증 + stats 누적은 `_instrument` 가 처리한다(ADR-077).
238
252
  *
239
253
  * @template T
240
254
  * @param {(client: import('pg').PoolClient) => Promise<T> | T} fn
255
+ * @param {{ isolation?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable' }} [opts] - 트랜잭션 옵션(ADR-190).
241
256
  * @returns {Promise<T>}
257
+ * @throws {MegaInternalError} `adapter.nested_isolation_unsupported` - nested 호출에 isolation 지정.
242
258
  */
243
- async withTransaction(fn) {
259
+ async withTransaction(fn, opts) {
260
+ // 옵션 검증은 연결 상태와 무관한 설정 오류라 _instrument(상태 검증) 전에 fail-fast 한다.
261
+ const isolationSql = resolveTxIsolation(opts?.isolation, 'postgres')
244
262
  return this._instrument('withTransaction', { table: undefined }, async () => {
245
263
  const existing = this.#txContext.getStore()
246
264
  // nested — 진행 중 트랜잭션이 있으면 같은 클라이언트에 SAVEPOINT 를 건다(Sqlite 와 분기).
247
265
  if (existing !== undefined) {
266
+ if (isolationSql !== undefined) {
267
+ throw new MegaInternalError(
268
+ 'adapter.nested_isolation_unsupported',
269
+ `${this.constructor.name}.withTransaction() cannot set an isolation level on a nested (SAVEPOINT) transaction — isolation is fixed when the top-level transaction starts (ADR-190).`,
270
+ { details: { adapter: this.constructor.name, isolation: opts?.isolation } },
271
+ )
272
+ }
248
273
  return this.#runSavepoint(existing, fn)
249
274
  }
250
275
  // top-level — 풀에서 클라이언트 1개 획득 후 BEGIN/COMMIT/ROLLBACK.
@@ -252,6 +277,8 @@ export class MegaPostgresAdapter extends MegaDbAdapter {
252
277
  const client = await pool.connect()
253
278
  try {
254
279
  await client.query('BEGIN')
280
+ // 격리수준은 트랜잭션 첫 쿼리 전에만 설정 가능 — BEGIN 직후 반영(화이트리스트 SQL 조각이라 안전).
281
+ if (isolationSql !== undefined) await client.query(`SET TRANSACTION ISOLATION LEVEL ${isolationSql}`)
255
282
  let result
256
283
  try {
257
284
  result = await this.#txContext.run({ client, depth: 0 }, () => fn(client))
@@ -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
  }
@@ -65,6 +65,11 @@ import * as Registry from './registry.js'
65
65
  */
66
66
 
67
67
  export class MegaSqliteAdapter extends MegaDbAdapter {
68
+ /** driver 식별자 — dialect 디스패치(getDialect)·CRUD 의 단일 출처(ADR-212). @returns {'sqlite'} */
69
+ get driver() {
70
+ return 'sqlite'
71
+ }
72
+
68
73
  /** @type {import('better-sqlite3').Database | null} 연결된 Database 인스턴스 (connect 후에만). */
69
74
  #db = null
70
75
  /** @type {string} DB 파일 경로 또는 ':memory:'. */
@@ -214,10 +219,11 @@ export class MegaSqliteAdapter extends MegaDbAdapter {
214
219
  }
215
220
 
216
221
  /**
217
- * 누적 통계 + sqlite 특화 필드.
218
- * @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 } }}
219
224
  */
220
225
  getStats() {
226
+ const connected = this.state === 'connected'
221
227
  return {
222
228
  ...super.getStats(),
223
229
  driver: 'sqlite',
@@ -226,6 +232,9 @@ export class MegaSqliteAdapter extends MegaDbAdapter {
226
232
  readonly: this.#openOptions.readonly,
227
233
  // _connect() 에서 캐시한 불변값 사용 — 미연결이면 undefined(매 호출 PRAGMA 실행 제거, L-4).
228
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 },
229
238
  }
230
239
  }
231
240
 
@@ -237,14 +246,28 @@ export class MegaSqliteAdapter extends MegaDbAdapter {
237
246
  * 성공 시 COMMIT 후 `fn` 반환값을 그대로 돌려주고, `fn` 이 throw 하면 ROLLBACK 후 원본 에러를
238
247
  * re-throw 한다. nested / 동시 호출은 `db.inTransaction` 으로 감지해 거부(ADR-105).
239
248
  *
249
+ * `opts.isolation`(ADR-190) — SQLite 트랜잭션은 항상 SERIALIZABLE 동작(단일 writer, 공식 문서)이라
250
+ * `'serializable'` 만 수용(no-op)하고 다른 값은 `adapter.invalid_option` 으로 명시 거부한다
251
+ * (조용히 무시하면 사용자가 다른 격리수준이 적용된 줄 오인 — P4).
252
+ *
240
253
  * hook(`onCallStart/onCallEnd`) + 상태 검증 + stats 누적은 `_instrument` 가 처리한다(ADR-077).
241
254
  *
242
255
  * @template T
243
256
  * @param {(db: import('better-sqlite3').Database) => Promise<T> | T} fn
257
+ * @param {{ isolation?: 'serializable' }} [opts] - 트랜잭션 옵션(ADR-190).
244
258
  * @returns {Promise<T>}
245
259
  * @throws {MegaInternalError} `adapter.nested_transaction_unsupported` - 이미 트랜잭션 진행 중.
260
+ * @throws {MegaValidationError} `adapter.invalid_option` - 'serializable' 외 isolation 값.
246
261
  */
247
- async withTransaction(fn) {
262
+ async withTransaction(fn, opts) {
263
+ // 옵션 검증은 연결 상태와 무관한 설정 오류라 _instrument(상태 검증) 전에 fail-fast 한다.
264
+ if (opts?.isolation !== undefined && opts.isolation !== 'serializable') {
265
+ throw new MegaValidationError(
266
+ 'adapter.invalid_option',
267
+ `sqlite transactions are always SERIALIZABLE — withTransaction "isolation" accepts only 'serializable'. Got ${JSON.stringify(opts.isolation)}.`,
268
+ { details: { driver: 'sqlite', option: 'isolation', value: opts.isolation } },
269
+ )
270
+ }
248
271
  return this._instrument('withTransaction', { table: undefined }, async () => {
249
272
  const db = /** @type {import('better-sqlite3').Database} */ (this.#db)
250
273
  if (db.inTransaction) {
@@ -9,7 +9,6 @@
9
9
  * @module cli/commands/console-cmd
10
10
  */
11
11
  import repl from 'node:repl'
12
- import { prepareRuntime } from '../../core/boot.js'
13
12
  import { MegaShutdown } from '../../lib/mega-shutdown.js'
14
13
 
15
14
  /**
@@ -38,6 +37,9 @@ export async function startConsole(
38
37
  projectRoot,
39
38
  { logger, replFactory = defaultReplFactory, out = console.log, shutdown = () => MegaShutdown.now(), setupSignals = (opts) => MegaShutdown.setupSignals(opts) } = {},
40
39
  ) {
40
+ // boot 그래프(fastify·OTel·pino 등)는 콘솔 기동 시점에만 로드 — scaffold.js 가 본 모듈을 정적
41
+ // import 하므로, 여기서 정적 import 하면 `mega help`/`g` 까지 전체 그래프를 지불하게 된다.
42
+ const { prepareRuntime } = await import('../../core/boot.js')
41
43
  const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
42
44
  out('mega: console ready — globals: ctx, config, mega')
43
45
  // prepareRuntime 이 어댑터/워커/wsHub 를 connect 하고 각자 MegaShutdown hook 을 자기등록한다. 정리 없이
@@ -15,6 +15,7 @@
15
15
  * @module cli/commands/new
16
16
  */
17
17
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
18
+ import { randomBytes } from 'node:crypto'
18
19
  import { dirname, join, relative, resolve, sep } from 'node:path'
19
20
  import { fileURLToPath } from 'node:url'
20
21
  import { nameVariants } from '../template-engine.js'
@@ -76,10 +77,11 @@ function walk(dir, base, acc) {
76
77
  }
77
78
 
78
79
  /**
79
- * 토큰 치환 — `sample-crud` 를 프로젝트명으로, package.json 의 모노레포 dep(`file:../..`)을 실제 버전으로.
80
+ * 토큰 치환 — `sample-crud` 를 프로젝트명으로, package.json 의 모노레포 dep(`file:../..`)을 실제 버전으로,
81
+ * `.env` 의 SESSION_SECRET 을 프로젝트별 신규 시크릿으로.
80
82
  * @param {string} rel - 파일 상대경로.
81
83
  * @param {string} content - 원본 텍스트.
82
- * @param {{ name: string, version: string }} vars
84
+ * @param {{ name: string, version: string, sessionSecret: string }} vars
83
85
  * @returns {string}
84
86
  */
85
87
  function transformText(rel, content, vars) {
@@ -88,6 +90,12 @@ function transformText(rel, content, vars) {
88
90
  // 모노레포 로컬 링크 → 퍼블리시된 프레임워크 버전 핀. 스캐폴드된 프로젝트는 독립 패키지라 file: 가 안 풀린다.
89
91
  out = out.replace('"file:../.."', `"^${vars.version}"`)
90
92
  }
93
+ if (rel === '.env') {
94
+ // 세션 쿠키 HMAC 시크릿은 데모 값을 복제하면 모든 `mega new` 프로젝트가 같은 서명 키를 공유한다
95
+ // (세션 위조·cross-app 쿠키 수용 위험) — 프로젝트마다 신규 생성으로 치환한다. `.env.example` 은
96
+ // placeholder("change-me-...") 그대로 둔다(문서 역할).
97
+ out = out.replace(/^SESSION_SECRET=.*$/m, `SESSION_SECRET=${vars.sessionSecret}`)
98
+ }
91
99
  return out
92
100
  }
93
101
 
@@ -104,7 +112,9 @@ export function scaffoldProject(targetDir, { name, force = false } = {}) {
104
112
  const projectName = nameVariants(name ?? root.split(/[/\\]/).pop() ?? 'mega-app').kebab
105
113
  /** @type {string} */
106
114
  const version = JSON.parse(readFileSync(FRAMEWORK_PKG, 'utf8')).version
107
- const vars = { name: projectName, version }
115
+ // 프로젝트별 세션 시크릿 base64url(영숫자·-·_ 만)이라 .env 값·쉘 복붙에 안전(=/+// 회피).
116
+ const sessionSecret = randomBytes(32).toString('base64url')
117
+ const vars = { name: projectName, version, sessionSecret }
108
118
 
109
119
  /** @type {string[]} */ const written = []
110
120
  /** @type {string[]} */ const skipped = []