mega-framework 0.1.0

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 (322) hide show
  1. package/.env +127 -0
  2. package/.env.example +186 -0
  3. package/.prettierrc.json +8 -0
  4. package/CHANGELOG.md +259 -0
  5. package/LICENSE +21 -0
  6. package/README.md +153 -0
  7. package/bin/mega-ws-hub.js +15 -0
  8. package/bin/mega.js +38 -0
  9. package/docker-compose.yml +201 -0
  10. package/eslint.config.js +57 -0
  11. package/infra/otel-collector-config.yaml +43 -0
  12. package/jsconfig.json +18 -0
  13. package/package.json +121 -0
  14. package/sample/crud/.env +18 -0
  15. package/sample/crud/.env.example +50 -0
  16. package/sample/crud/README.md +85 -0
  17. package/sample/crud/apps/main/app.config.js +114 -0
  18. package/sample/crud/apps/main/channels/chat-bus.js +115 -0
  19. package/sample/crud/apps/main/channels/chat-channel.js +145 -0
  20. package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
  21. package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
  22. package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
  23. package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
  24. package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
  25. package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
  26. package/sample/crud/apps/main/controllers/note-controller.js +116 -0
  27. package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
  28. package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
  29. package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
  30. package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
  31. package/sample/crud/apps/main/controllers/user-controller.js +34 -0
  32. package/sample/crud/apps/main/controllers/web-controller.js +137 -0
  33. package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
  34. package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
  35. package/sample/crud/apps/main/jobs/email-job.js +72 -0
  36. package/sample/crud/apps/main/locales/client/en.json +3 -0
  37. package/sample/crud/apps/main/locales/client/ko.json +3 -0
  38. package/sample/crud/apps/main/locales/server/en.json +316 -0
  39. package/sample/crud/apps/main/locales/server/ko.json +316 -0
  40. package/sample/crud/apps/main/middleware/web-auth.js +40 -0
  41. package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
  42. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
  43. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
  44. package/sample/crud/apps/main/models/note.js +71 -0
  45. package/sample/crud/apps/main/models/user.js +86 -0
  46. package/sample/crud/apps/main/public/css/app.css +101 -0
  47. package/sample/crud/apps/main/public/css/guide.css +137 -0
  48. package/sample/crud/apps/main/public/js/app.js +54 -0
  49. package/sample/crud/apps/main/public/js/perf.js +129 -0
  50. package/sample/crud/apps/main/public/js/theme-init.js +12 -0
  51. package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
  52. package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
  53. package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
  54. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  55. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  56. package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
  57. package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
  58. package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
  59. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
  60. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
  61. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
  62. package/sample/crud/apps/main/routes/auth.js +15 -0
  63. package/sample/crud/apps/main/routes/cron.js +14 -0
  64. package/sample/crud/apps/main/routes/guide.js +25 -0
  65. package/sample/crud/apps/main/routes/jobs.js +14 -0
  66. package/sample/crud/apps/main/routes/logs.js +28 -0
  67. package/sample/crud/apps/main/routes/metrics.js +13 -0
  68. package/sample/crud/apps/main/routes/notes.js +19 -0
  69. package/sample/crud/apps/main/routes/perf.js +47 -0
  70. package/sample/crud/apps/main/routes/redis.js +14 -0
  71. package/sample/crud/apps/main/routes/tracing.js +14 -0
  72. package/sample/crud/apps/main/routes/upload.js +16 -0
  73. package/sample/crud/apps/main/routes/users.js +54 -0
  74. package/sample/crud/apps/main/routes/web.js +23 -0
  75. package/sample/crud/apps/main/routes/worker.js +15 -0
  76. package/sample/crud/apps/main/routes/ws.js +30 -0
  77. package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
  78. package/sample/crud/apps/main/services/auth-service.js +74 -0
  79. package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
  80. package/sample/crud/apps/main/services/guide-service.js +145 -0
  81. package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
  82. package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
  83. package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
  84. package/sample/crud/apps/main/services/note-service.js +75 -0
  85. package/sample/crud/apps/main/services/perf-service.js +302 -0
  86. package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
  87. package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
  88. package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
  89. package/sample/crud/apps/main/services/user-service.js +65 -0
  90. package/sample/crud/apps/main/views/auth/login.ejs +57 -0
  91. package/sample/crud/apps/main/views/auth/register.ejs +71 -0
  92. package/sample/crud/apps/main/views/cron/index.ejs +92 -0
  93. package/sample/crud/apps/main/views/guide/index.ejs +24 -0
  94. package/sample/crud/apps/main/views/guide/page.ejs +64 -0
  95. package/sample/crud/apps/main/views/home.ejs +82 -0
  96. package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
  97. package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
  98. package/sample/crud/apps/main/views/logs/index.ejs +80 -0
  99. package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
  100. package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
  101. package/sample/crud/apps/main/views/notes/list.ejs +74 -0
  102. package/sample/crud/apps/main/views/notes/new.ejs +45 -0
  103. package/sample/crud/apps/main/views/perf/index.ejs +90 -0
  104. package/sample/crud/apps/main/views/redis/index.ejs +65 -0
  105. package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
  106. package/sample/crud/apps/main/views/upload/index.ejs +79 -0
  107. package/sample/crud/apps/main/views/users/edit.ejs +48 -0
  108. package/sample/crud/apps/main/views/users/list.ejs +81 -0
  109. package/sample/crud/apps/main/views/users/new.ejs +48 -0
  110. package/sample/crud/apps/main/views/worker/index.ejs +70 -0
  111. package/sample/crud/apps/main/views/ws/index.ejs +62 -0
  112. package/sample/crud/apps/main/workers/hash-worker.js +17 -0
  113. package/sample/crud/apps/main/workers/hash.task.js +22 -0
  114. package/sample/crud/ecosystem.config.cjs +9 -0
  115. package/sample/crud/mega.config.js +105 -0
  116. package/sample/crud/package-lock.json +5665 -0
  117. package/sample/crud/package.json +28 -0
  118. package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
  119. package/sample/crud/test/apps/main/auth-service.test.js +93 -0
  120. package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
  121. package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
  122. package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
  123. package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
  124. package/sample/crud/test/apps/main/email-job.test.js +76 -0
  125. package/sample/crud/test/apps/main/guide-service.test.js +68 -0
  126. package/sample/crud/test/apps/main/hash-task.test.js +30 -0
  127. package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
  128. package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
  129. package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
  130. package/sample/crud/test/apps/main/note-service.test.js +68 -0
  131. package/sample/crud/test/apps/main/perf-service.test.js +121 -0
  132. package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
  133. package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
  134. package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
  135. package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
  136. package/sample/crud/test/apps/main/user-service.test.js +65 -0
  137. package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
  138. package/sample/crud/vitest.config.js +8 -0
  139. package/sample/crud/yarn.lock +2142 -0
  140. package/sample/simple/.env.example +15 -0
  141. package/sample/simple/README.md +52 -0
  142. package/sample/simple/apps/main/app.config.js +35 -0
  143. package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
  144. package/sample/simple/apps/main/locales/client/en.json +3 -0
  145. package/sample/simple/apps/main/locales/client/ko.json +3 -0
  146. package/sample/simple/apps/main/locales/server/en.json +23 -0
  147. package/sample/simple/apps/main/locales/server/ko.json +23 -0
  148. package/sample/simple/apps/main/public/css/app.css +101 -0
  149. package/sample/simple/apps/main/public/hello.txt +1 -0
  150. package/sample/simple/apps/main/public/js/app.js +54 -0
  151. package/sample/simple/apps/main/public/js/theme-init.js +12 -0
  152. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  153. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  154. package/sample/simple/apps/main/routes/index.js +9 -0
  155. package/sample/simple/apps/main/routes/pages.js +12 -0
  156. package/sample/simple/apps/main/views/index.ejs +56 -0
  157. package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
  158. package/sample/simple/ecosystem.config.cjs +10 -0
  159. package/sample/simple/mega.config.js +27 -0
  160. package/sample/simple/package-lock.json +1851 -0
  161. package/sample/simple/package.json +25 -0
  162. package/sample/simple/test/apps/main/index.test.js +13 -0
  163. package/sample/simple/vitest.config.js +8 -0
  164. package/src/adapters/adapter-manager.js +305 -0
  165. package/src/adapters/adapter-options.js +208 -0
  166. package/src/adapters/file-adapter.js +350 -0
  167. package/src/adapters/file-session-adapter.js +363 -0
  168. package/src/adapters/index.js +38 -0
  169. package/src/adapters/maria-adapter.js +425 -0
  170. package/src/adapters/mega-adapter.js +511 -0
  171. package/src/adapters/mega-bus-adapter.js +81 -0
  172. package/src/adapters/mega-cache-adapter.js +94 -0
  173. package/src/adapters/mega-db-adapter.js +72 -0
  174. package/src/adapters/mega-lock-adapter.js +118 -0
  175. package/src/adapters/mega-log-sink-adapter.js +46 -0
  176. package/src/adapters/mega-session-adapter.js +72 -0
  177. package/src/adapters/mongo-adapter.js +396 -0
  178. package/src/adapters/nats-adapter.js +370 -0
  179. package/src/adapters/postgres-adapter.js +341 -0
  180. package/src/adapters/redis-adapter.js +331 -0
  181. package/src/adapters/redis-session-adapter.js +261 -0
  182. package/src/adapters/redlock-adapter.js +385 -0
  183. package/src/adapters/registry.js +157 -0
  184. package/src/adapters/sqlite-adapter.js +309 -0
  185. package/src/auth/index.js +103 -0
  186. package/src/cli/commands/console-cmd.js +56 -0
  187. package/src/cli/commands/new.js +101 -0
  188. package/src/cli/commands/routes.js +107 -0
  189. package/src/cli/commands/scaffold.js +120 -0
  190. package/src/cli/commands/test-cmd.js +45 -0
  191. package/src/cli/generators/index.js +368 -0
  192. package/src/cli/index.js +472 -0
  193. package/src/cli/template-engine.js +72 -0
  194. package/src/cli/ws-hub.js +582 -0
  195. package/src/core/ajv-mapper.js +80 -0
  196. package/src/core/boot.js +323 -0
  197. package/src/core/cluster-metrics.js +278 -0
  198. package/src/core/config-loader.js +115 -0
  199. package/src/core/config-validator.js +322 -0
  200. package/src/core/ctx-builder.js +253 -0
  201. package/src/core/envelope.js +88 -0
  202. package/src/core/error-mapper.js +116 -0
  203. package/src/core/formbody.js +69 -0
  204. package/src/core/hub-link.js +552 -0
  205. package/src/core/i18n.js +525 -0
  206. package/src/core/index.js +63 -0
  207. package/src/core/mega-app.js +1138 -0
  208. package/src/core/mega-cluster.js +232 -0
  209. package/src/core/mega-server.js +176 -0
  210. package/src/core/mega-service.js +41 -0
  211. package/src/core/migration-runner.js +196 -0
  212. package/src/core/multipart.js +282 -0
  213. package/src/core/openapi.js +114 -0
  214. package/src/core/router.js +388 -0
  215. package/src/core/routes-loader.js +57 -0
  216. package/src/core/scope-registry.js +53 -0
  217. package/src/core/security.js +275 -0
  218. package/src/core/services-loader.js +98 -0
  219. package/src/core/session-cleanup-schedule.js +57 -0
  220. package/src/core/session-store.js +55 -0
  221. package/src/core/session.js +414 -0
  222. package/src/core/static-assets.js +126 -0
  223. package/src/core/template.js +294 -0
  224. package/src/core/workers-manager.js +193 -0
  225. package/src/core/ws-compression.js +112 -0
  226. package/src/core/ws-controller.js +109 -0
  227. package/src/core/ws-message.js +176 -0
  228. package/src/core/ws-upgrade.js +445 -0
  229. package/src/errors/config-error.js +16 -0
  230. package/src/errors/http-errors.js +130 -0
  231. package/src/errors/index.js +19 -0
  232. package/src/errors/mega-error.js +34 -0
  233. package/src/eslint-plugin/index.js +15 -0
  234. package/src/eslint-plugin/no-direct-model-import.js +113 -0
  235. package/src/index.js +131 -0
  236. package/src/lib/asp/config.js +83 -0
  237. package/src/lib/asp/crypto.js +145 -0
  238. package/src/lib/asp/errors.js +49 -0
  239. package/src/lib/asp/nonce-cache.js +94 -0
  240. package/src/lib/asp/plugin.js +263 -0
  241. package/src/lib/asp/ws-terminator.js +101 -0
  242. package/src/lib/env-mapper.js +222 -0
  243. package/src/lib/hub-protocol.js +322 -0
  244. package/src/lib/index.js +42 -0
  245. package/src/lib/logger/telegram-core.js +150 -0
  246. package/src/lib/logger/telegram-transport.js +126 -0
  247. package/src/lib/mega-brute-force.js +225 -0
  248. package/src/lib/mega-circuit-breaker.js +412 -0
  249. package/src/lib/mega-cron.js +169 -0
  250. package/src/lib/mega-hash.js +179 -0
  251. package/src/lib/mega-health.js +91 -0
  252. package/src/lib/mega-job-queue.js +600 -0
  253. package/src/lib/mega-job-worker.js +295 -0
  254. package/src/lib/mega-job.js +140 -0
  255. package/src/lib/mega-logger.js +128 -0
  256. package/src/lib/mega-metrics.js +661 -0
  257. package/src/lib/mega-plugin.js +650 -0
  258. package/src/lib/mega-retry.js +95 -0
  259. package/src/lib/mega-schedule.js +507 -0
  260. package/src/lib/mega-shutdown.js +176 -0
  261. package/src/lib/mega-tracing.js +715 -0
  262. package/src/lib/mega-worker.js +653 -0
  263. package/src/lib/worker-runner/process-entry.js +30 -0
  264. package/src/lib/worker-runner/task-dispatch.js +72 -0
  265. package/src/lib/worker-runner/thread-entry.js +26 -0
  266. package/src/models/index.js +7 -0
  267. package/src/models/mega-model.js +151 -0
  268. package/src/test/index.js +288 -0
  269. package/templates/adapter/code.tpl +40 -0
  270. package/templates/adapter/test.tpl +13 -0
  271. package/templates/app/app.config.tpl +10 -0
  272. package/templates/app/route.tpl +10 -0
  273. package/templates/app/test.tpl +13 -0
  274. package/templates/channel/code.tpl +38 -0
  275. package/templates/channel/test.tpl +19 -0
  276. package/templates/controller/code.tpl +16 -0
  277. package/templates/controller/route.tpl +9 -0
  278. package/templates/controller/test.tpl +14 -0
  279. package/templates/job/code.tpl +23 -0
  280. package/templates/job/test.tpl +17 -0
  281. package/templates/locale/code.tpl +3 -0
  282. package/templates/locale/test.tpl +13 -0
  283. package/templates/middleware/code.tpl +13 -0
  284. package/templates/middleware/test.tpl +11 -0
  285. package/templates/migration/code.tpl +20 -0
  286. package/templates/migration/test.tpl +14 -0
  287. package/templates/model/code.tpl +21 -0
  288. package/templates/model/test.tpl +29 -0
  289. package/templates/project/app.config.tpl +8 -0
  290. package/templates/project/app.config.views.tpl +37 -0
  291. package/templates/project/ecosystem.config.tpl +10 -0
  292. package/templates/project/env.tpl +12 -0
  293. package/templates/project/gitignore.tpl +8 -0
  294. package/templates/project/locales/client/en.json.tpl +3 -0
  295. package/templates/project/locales/client/ko.json.tpl +3 -0
  296. package/templates/project/locales/server/en.json.tpl +17 -0
  297. package/templates/project/locales/server/ko.json.tpl +17 -0
  298. package/templates/project/mega.config.tpl +11 -0
  299. package/templates/project/package.tpl +25 -0
  300. package/templates/project/public/css/app.css +101 -0
  301. package/templates/project/public/js/app.js +54 -0
  302. package/templates/project/public/js/theme-init.js +12 -0
  303. package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  304. package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
  305. package/templates/project/readme.tpl +48 -0
  306. package/templates/project/route.test.tpl +13 -0
  307. package/templates/project/route.test.views.tpl +15 -0
  308. package/templates/project/route.tpl +10 -0
  309. package/templates/project/route.views.tpl +10 -0
  310. package/templates/project/views/index.ejs.tpl +58 -0
  311. package/templates/project/views/layout.ejs.tpl +73 -0
  312. package/templates/project/vitest.config.tpl +8 -0
  313. package/templates/route/code.tpl +11 -0
  314. package/templates/route/test.tpl +26 -0
  315. package/templates/schedule/code.tpl +19 -0
  316. package/templates/schedule/test.tpl +17 -0
  317. package/templates/service/code.tpl +18 -0
  318. package/templates/service/test.tpl +17 -0
  319. package/templates/worker/code.tpl +14 -0
  320. package/templates/worker/task.tpl +13 -0
  321. package/templates/worker/test.tpl +18 -0
  322. package/vitest.config.js +33 -0
@@ -0,0 +1,363 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaFileSessionAdapter — 로컬 파일시스템 세션 스토어 (Node `fs`/`path`/`crypto` 만, ADR-129).
4
+ *
5
+ * `MegaSessionAdapter`(ADR-046/027) 의 첫 구체 중 하나. dev / 단일 인스턴스 / Redis 없는 환경에서
6
+ * 세션 레코드를 로컬 파일로 보관한다 — driver 한 줄만 'redis' 로 바꾸면 분산 환경 전환(ADR-046).
7
+ * **신규 의존성 0** (`node:fs/promises`/`node:path`/`node:crypto` 표준만).
8
+ *
9
+ * # 표준 표면 (MegaSessionAdapter 상속)
10
+ * - `_connect()` — `fs.mkdir(basePath, { recursive: true })` 로 디렉토리 보장(+옵션 cleanup 타이머).
11
+ * - `_disconnect()` — cleanup 타이머 정지(파일시스템은 close 개념 없음).
12
+ * - `_native()` — `{ basePath }` (raw "디렉토리 핸들" 개념, ADR-009).
13
+ * - `healthCheck()` — basePath 에 probe write→read→unlink 로 실제 읽기/쓰기 가능 확인.
14
+ * - `load/save/destroy/touch/cleanup` — 단일 JSON envelope 파일 + TTL 메타데이터(아래).
15
+ *
16
+ * # 저장 포맷 — 단일 envelope 파일 (`MegaFileAdapter` 와 동일 전략, ADR-111)
17
+ * `{ v:1, sid, data, expiresAt }` 를 한 파일에 묶는다. `expiresAt` 은 epoch ms(무한이면 null —
18
+ * 세션은 항상 TTL 이 있어 실사용에선 null 이 거의 없다). 쓰기는 **temp write 후 `rename`** 으로
19
+ * atomic — 동시 write 가 반쯤 써진 파일을 읽는 일이 없다(POSIX rename 은 같은 파일시스템에서 원자적).
20
+ *
21
+ * # 파일명 — sid 의 SHA-256 hex (경로 안전)
22
+ * sid 는 ULID(Crockford base32, 경로 안전)지만, 일관성·방어를 위해 SHA-256 hex(64자)로 해싱해
23
+ * 파일명으로 쓴다(`MegaFileAdapter` 와 동일 — 경로 탈출 불가). 원본 sid 는 envelope 에 보관(디버깅용).
24
+ *
25
+ * # cleanup (ADR-046 — "file 모드 cleanup 은 mega scheduler 가 자동 등록")
26
+ * `cleanup()` 은 basePath 를 스캔해 만료 파일을 삭제하고 삭제 개수를 반환한다. 멀티 인스턴스는
27
+ * `SessionCleanupSchedule`(MegaSchedule, src/core/session-cleanup-schedule.js)을 `mega scheduler` 로
28
+ * 분산 등록한다. 단일 인스턴스 dev 편의로 `cleanupIntervalMs`(옵션) 가 있으면 내부 unref 타이머가
29
+ * 주기적으로 `cleanup()` 을 돌린다(프로세스를 살려두지 않음).
30
+ *
31
+ * # 설정
32
+ * ```js
33
+ * session: {
34
+ * store: { driver: 'file', basePath: '/var/lib/mega/sessions', cleanupIntervalMs: 3600000 },
35
+ * ttlMs: 86400000, // save() 가 적용할 기본 TTL (미들웨어가 store 생성 시 주입)
36
+ * }
37
+ * ```
38
+ *
39
+ * @module adapters/file-session-adapter
40
+ */
41
+ import { mkdir, writeFile, readFile, rename, unlink, readdir } from 'node:fs/promises'
42
+ import { join } from 'node:path'
43
+ import { createHash, randomUUID } from 'node:crypto'
44
+ import { MegaValidationError } from '../errors/http-errors.js'
45
+ import { MegaSessionAdapter } from './mega-session-adapter.js'
46
+
47
+ /** envelope 스키마 버전 — 포맷 변경 시 마이그레이션 분기용. */
48
+ const ENVELOPE_VERSION = 1
49
+
50
+ /** save() 가 store 기본 TTL 미설정 시 적용할 폴백 (24시간, ms). */
51
+ const DEFAULT_TTL_MS = 86_400_000
52
+
53
+ /**
54
+ * @typedef {object} FileSessionConfig
55
+ * @property {string} [driver] - 'file' (팩토리가 사용 — 어댑터는 무시).
56
+ * @property {string} [basePath] - 세션 파일 저장 디렉토리 (필수).
57
+ * @property {string} [dir] - `basePath` 의 별칭 (하위 호환).
58
+ * @property {number} [ttlMs] - save() 가 적용할 기본 TTL(ms). 미지정 시 24시간.
59
+ * @property {number} [cleanupIntervalMs] - 내부 cleanup 타이머 주기(ms). 미지정/0 이면 타이머 없음(단일 dev 편의).
60
+ * @property {string} [extension] - 파일 확장자 (기본 '.json').
61
+ */
62
+
63
+ /**
64
+ * @typedef {object} SessionEnvelope - 디스크에 저장되는 단일 파일 포맷.
65
+ * @property {number} v - 스키마 버전.
66
+ * @property {string} sid - 원본 세션 id(ULID — 파일명은 해시라 역추적 불가).
67
+ * @property {object} data - 세션 레코드.
68
+ * @property {number | null} expiresAt - 만료 epoch ms (무한이면 null).
69
+ */
70
+
71
+ export class MegaFileSessionAdapter extends MegaSessionAdapter {
72
+ /** @type {string} 세션 디렉토리 경로. */
73
+ #basePath
74
+ /** @type {number} save() 기본 TTL(ms). */
75
+ #ttlMs
76
+ /** @type {number} 내부 cleanup 타이머 주기(ms). 0=off. */
77
+ #cleanupIntervalMs
78
+ /** @type {string} 파일 확장자. */
79
+ #extension
80
+ /** @type {NodeJS.Timeout | null} 내부 cleanup 타이머(있으면). */
81
+ #timer = null
82
+
83
+ /**
84
+ * @param {FileSessionConfig} [config]
85
+ * @throws {MegaValidationError} `session.basepath_required` - basePath/dir 누락.
86
+ * @throws {MegaValidationError} `session.invalid_option` - 옵션 타입 오류.
87
+ */
88
+ constructor(config = /** @type {any} */ ({})) {
89
+ super(config)
90
+
91
+ const basePath = config.basePath ?? config.dir
92
+ if (typeof basePath !== 'string' || basePath.length === 0) {
93
+ throw new MegaValidationError(
94
+ 'session.basepath_required',
95
+ 'file session store: "basePath" (or alias "dir") is required — the directory to store session files (e.g. "/var/lib/mega/sessions").',
96
+ { details: { driver: 'file', basePath: basePath ?? null } },
97
+ )
98
+ }
99
+ this.#basePath = basePath
100
+
101
+ this.#ttlMs = this.#assertPositiveInt('ttlMs', config.ttlMs, DEFAULT_TTL_MS)
102
+ this.#cleanupIntervalMs = this.#assertNonNegativeInt('cleanupIntervalMs', config.cleanupIntervalMs, 0)
103
+
104
+ const extension = config.extension ?? '.json'
105
+ if (typeof extension !== 'string' || extension.length === 0) {
106
+ throw new MegaValidationError('session.invalid_option', 'file session "extension" must be a non-empty string (e.g. ".json").', {
107
+ details: { driver: 'file', option: 'extension', value: extension },
108
+ })
109
+ }
110
+ this.#extension = extension
111
+ }
112
+
113
+ /**
114
+ * 양의 정수 검증 헬퍼 (미지정 시 fallback). 잘못된 값은 fail-fast throw.
115
+ * @param {string} name @param {number | undefined} value @param {number} fallback @returns {number}
116
+ */
117
+ #assertPositiveInt(name, value, fallback) {
118
+ if (value === undefined) return fallback
119
+ if (!Number.isInteger(value) || value <= 0) {
120
+ throw new MegaValidationError('session.invalid_option', `file session "${name}" must be a positive integer (ms). Got: ${value}.`, {
121
+ details: { driver: 'file', option: name, value },
122
+ })
123
+ }
124
+ return value
125
+ }
126
+
127
+ /**
128
+ * 음이 아닌 정수 검증 헬퍼 (0 허용 — 타이머 off 의미). 잘못된 값은 fail-fast throw.
129
+ * @param {string} name @param {number | undefined} value @param {number} fallback @returns {number}
130
+ */
131
+ #assertNonNegativeInt(name, value, fallback) {
132
+ if (value === undefined) return fallback
133
+ if (!Number.isInteger(value) || value < 0) {
134
+ throw new MegaValidationError('session.invalid_option', `file session "${name}" must be a non-negative integer (ms, 0=off). Got: ${value}.`, {
135
+ details: { driver: 'file', option: name, value },
136
+ })
137
+ }
138
+ return value
139
+ }
140
+
141
+ /**
142
+ * 세션 디렉토리 보장 (`mkdir -p`) + (옵션) 내부 cleanup 타이머 시작.
143
+ * @protected
144
+ * @returns {Promise<void>}
145
+ */
146
+ async _connect() {
147
+ await mkdir(this.#basePath, { recursive: true })
148
+ if (this.#cleanupIntervalMs > 0 && this.#timer === null) {
149
+ // 단일 dev 편의용 주기 cleanup. unref() 로 이 타이머가 프로세스 종료를 막지 않게 한다.
150
+ this.#timer = setInterval(() => {
151
+ // 타이머 cleanup 실패는 비치명적 — 다음 주기에 재시도. console.warn 로 가시화.
152
+ this.cleanup().catch((err) => {
153
+ console.warn('[MegaFileSessionAdapter] periodic cleanup() failed (will retry next interval):', err)
154
+ })
155
+ }, this.#cleanupIntervalMs)
156
+ this.#timer.unref?.()
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 내부 cleanup 타이머 정지 (파일시스템은 연결/해제 개념 없음).
162
+ * @protected
163
+ * @returns {Promise<void>}
164
+ */
165
+ async _disconnect() {
166
+ if (this.#timer !== null) {
167
+ clearInterval(this.#timer)
168
+ this.#timer = null
169
+ }
170
+ }
171
+
172
+ /**
173
+ * raw "디렉토리 핸들" (ADR-009).
174
+ * @protected
175
+ * @returns {{ basePath: string }}
176
+ */
177
+ _native() {
178
+ return { basePath: this.#basePath }
179
+ }
180
+
181
+ /**
182
+ * 헬스 체크 — basePath 에 probe write→read→unlink 로 실제 읽기/쓰기 가능 확인.
183
+ * @returns {Promise<{ ok: boolean, driver: 'file', state: string, basePath?: string, error?: string }>}
184
+ */
185
+ async healthCheck() {
186
+ if (this.state !== 'connected') {
187
+ return { ok: false, driver: 'file', state: this.state }
188
+ }
189
+ const probe = join(this.#basePath, `.health-${randomUUID()}`)
190
+ try {
191
+ await writeFile(probe, 'ok', 'utf8')
192
+ const back = await readFile(probe, 'utf8')
193
+ await unlink(probe)
194
+ return { ok: back === 'ok', driver: 'file', state: this.state, basePath: this.#basePath }
195
+ } catch (err) {
196
+ await unlink(probe).catch(() => {})
197
+ return { ok: false, driver: 'file', state: this.state, basePath: this.#basePath, error: err instanceof Error ? err.message : String(err) }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * 누적 통계 + file 세션 특화.
203
+ * @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, basePath: string, ttlMs: number }}
204
+ */
205
+ getStats() {
206
+ return { ...super.getStats(), driver: 'file', basePath: this.#basePath, ttlMs: this.#ttlMs }
207
+ }
208
+
209
+ /**
210
+ * sid → 디스크 파일 경로. sid 를 SHA-256 hex 로 해싱해 경로 안전 파일명으로(모듈 docstring).
211
+ * @param {string} sid @returns {string}
212
+ */
213
+ #pathFor(sid) {
214
+ const hashed = createHash('sha256').update(sid, 'utf8').digest('hex')
215
+ return join(this.#basePath, hashed + this.#extension)
216
+ }
217
+
218
+ /**
219
+ * envelope 를 atomic 하게 기록 — temp write 후 `rename`(반쯤 써진 파일 노출 방지).
220
+ * @param {string} finalPath @param {SessionEnvelope} envelope @returns {Promise<void>}
221
+ */
222
+ async #atomicWrite(finalPath, envelope) {
223
+ const tmp = `${finalPath}.tmp-${randomUUID()}`
224
+ try {
225
+ await writeFile(tmp, JSON.stringify(envelope), 'utf8')
226
+ await rename(tmp, finalPath)
227
+ } catch (err) {
228
+ await unlink(tmp).catch(() => {})
229
+ throw err
230
+ }
231
+ }
232
+
233
+ /**
234
+ * 세션 조회. 없거나 만료면 `null`(만료 파일은 lazy 삭제).
235
+ * @param {string} sid
236
+ * @returns {Promise<object | null>}
237
+ */
238
+ async load(sid) {
239
+ return this._instrument('load', { sid }, async () => {
240
+ const path = this.#pathFor(sid)
241
+ let raw
242
+ try {
243
+ raw = await readFile(path, 'utf8')
244
+ } catch (err) {
245
+ // 파일 없음(ENOENT) = 세션 없음(정상). 그 외 I/O 에러는 전파(무차별 삼킴 X).
246
+ if (/** @type {any} */ (err)?.code === 'ENOENT') return null
247
+ throw err
248
+ }
249
+ /** @type {SessionEnvelope} */
250
+ const env = JSON.parse(raw)
251
+ if (env.expiresAt !== null && Date.now() > env.expiresAt) {
252
+ await unlink(path).catch(() => {}) // 만료 lazy 삭제(실패는 비치명적, 다음 cleanup 재시도).
253
+ return null
254
+ }
255
+ return env.data
256
+ })
257
+ }
258
+
259
+ /**
260
+ * 세션 저장 — store 기본 TTL(`ttlMs`)로 `expiresAt` 설정.
261
+ * @param {string} sid
262
+ * @param {object} record - 세션 레코드(JSON 직렬화 가능).
263
+ * @returns {Promise<void>}
264
+ */
265
+ async save(sid, record) {
266
+ return this._instrument('save', { sid }, async () => {
267
+ /** @type {SessionEnvelope} */
268
+ const envelope = { v: ENVELOPE_VERSION, sid, data: record ?? {}, expiresAt: Date.now() + this.#ttlMs }
269
+ await this.#atomicWrite(this.#pathFor(sid), envelope)
270
+ })
271
+ }
272
+
273
+ /**
274
+ * 세션 삭제. 없는 세션 삭제는 에러 아님(idempotent — ENOENT 흡수).
275
+ * @param {string} sid
276
+ * @returns {Promise<void>}
277
+ */
278
+ async destroy(sid) {
279
+ return this._instrument('destroy', { sid }, async () => {
280
+ try {
281
+ await unlink(this.#pathFor(sid))
282
+ } catch (err) {
283
+ if (/** @type {any} */ (err)?.code !== 'ENOENT') throw err // ENOENT=이미 없음(정상).
284
+ }
285
+ })
286
+ }
287
+
288
+ /**
289
+ * rolling TTL 갱신(ADR-046) — 기존 envelope 의 `expiresAt` 만 `now + ttlMs` 로 갱신. 세션이 없으면
290
+ * no-op(만료/삭제된 세션을 touch 가 되살리지 않음 — 데이터 없는 빈 세션 부활 방지).
291
+ * @param {string} sid
292
+ * @param {number} ttlMs - 갱신할 TTL(ms).
293
+ * @returns {Promise<void>}
294
+ */
295
+ async touch(sid, ttlMs) {
296
+ return this._instrument('touch', { sid, ttlMs }, async () => {
297
+ const path = this.#pathFor(sid)
298
+ let raw
299
+ try {
300
+ raw = await readFile(path, 'utf8')
301
+ } catch (err) {
302
+ if (/** @type {any} */ (err)?.code === 'ENOENT') return // 없는 세션 touch = no-op.
303
+ throw err
304
+ }
305
+ /** @type {SessionEnvelope} */
306
+ const env = JSON.parse(raw)
307
+ // 이미 만료된 세션은 touch 로 되살리지 않고 lazy 삭제(load 와 동일 의미).
308
+ if (env.expiresAt !== null && Date.now() > env.expiresAt) {
309
+ await unlink(path).catch(() => {})
310
+ return
311
+ }
312
+ env.expiresAt = Date.now() + ttlMs
313
+ await this.#atomicWrite(path, env)
314
+ })
315
+ }
316
+
317
+ /**
318
+ * 만료 세션 파일 일괄 정리(ADR-046). basePath 를 스캔해 `expiresAt` 이 지난 파일을 삭제한다.
319
+ * 삭제 개수를 반환한다. 읽기 불가/JSON 깨진 파일은 건너뛴다(개별 실패가 전체 cleanup 을 막지 않음).
320
+ *
321
+ * @returns {Promise<number>} 삭제된 세션 파일 수.
322
+ */
323
+ async cleanup() {
324
+ return this._instrument('cleanup', {}, async () => {
325
+ let names
326
+ try {
327
+ names = await readdir(this.#basePath)
328
+ } catch (err) {
329
+ if (/** @type {any} */ (err)?.code === 'ENOENT') return 0 // 디렉토리 없음 = 정리할 것 없음.
330
+ throw err
331
+ }
332
+ const now = Date.now()
333
+ let removed = 0
334
+ for (const name of names) {
335
+ if (!name.endsWith(this.#extension)) continue // tmp/probe/그 외 파일 건너뜀.
336
+ const path = join(this.#basePath, name)
337
+ let raw
338
+ try {
339
+ raw = await readFile(path, 'utf8')
340
+ } catch {
341
+ // 읽는 사이 사라졌거나 권한 문제 — 개별 파일 실패는 cleanup 전체를 막지 않는다(스캔 best-effort).
342
+ continue
343
+ }
344
+ let env
345
+ try {
346
+ env = /** @type {SessionEnvelope} */ (JSON.parse(raw))
347
+ } catch {
348
+ continue // 손상 파일은 건드리지 않음(수동 점검 대상).
349
+ }
350
+ if (env.expiresAt !== null && now > env.expiresAt) {
351
+ // unlink 실패(레이스)는 비치명적 — 다음 주기 재시도(삭제 카운트엔 미반영).
352
+ try {
353
+ await unlink(path)
354
+ removed += 1
355
+ } catch {
356
+ /* 레이스로 이미 삭제됨 등 — 다음 주기 재시도. */
357
+ }
358
+ }
359
+ }
360
+ return removed
361
+ })
362
+ }
363
+ }
@@ -0,0 +1,38 @@
1
+ // @ts-check
2
+ /**
3
+ * adapters 배럴 — 어댑터 베이스 트리 + 드라이버 레지스트리 (ADR-027/044/045).
4
+ *
5
+ * 빌트인 구체 어댑터(Postgres/Mongo/Maria/Sqlite/Redis/File/Nats/Redlock)는 import 시
6
+ * 추가되며, 각자 본 배럴의 레지스트리(`MegaAdapterRegistry.register`)에 자기 자신을 등록한다.
7
+ *
8
+ * @module adapters
9
+ */
10
+ export { MegaAdapter } from './mega-adapter.js'
11
+ export { MegaDbAdapter } from './mega-db-adapter.js'
12
+ export { MegaCacheAdapter } from './mega-cache-adapter.js'
13
+ export { MegaBusAdapter } from './mega-bus-adapter.js'
14
+ export { MegaLockAdapter } from './mega-lock-adapter.js'
15
+ export { MegaLogSinkAdapter } from './mega-log-sink-adapter.js'
16
+ export { MegaSessionAdapter } from './mega-session-adapter.js'
17
+
18
+ // 빌트인 구체 어댑터 — import 시 레지스트리에 자기등록(ADR-044).
19
+ // driver native binding/모듈 은 _connect() lazy import 까지 로드 안 됨 → 미사용 환경 안전.
20
+ export { MegaSqliteAdapter } from './sqlite-adapter.js'
21
+ export { MegaPostgresAdapter } from './postgres-adapter.js'
22
+ export { MegaMariaAdapter } from './maria-adapter.js'
23
+ export { MegaMongoAdapter } from './mongo-adapter.js'
24
+ export { MegaRedisAdapter } from './redis-adapter.js'
25
+ export { MegaFileAdapter } from './file-adapter.js'
26
+ export { MegaNatsAdapter } from './nats-adapter.js'
27
+ export { MegaRedlockAdapter } from './redlock-adapter.js'
28
+
29
+ // 세션 스토어 어댑터 (ADR-129) — 공유 driver 레지스트리 미사용(createSessionStore 전용 팩토리).
30
+ export { MegaFileSessionAdapter } from './file-session-adapter.js'
31
+ export { MegaRedisSessionAdapter } from './redis-session-adapter.js'
32
+
33
+ // 드라이버 레지스트리 (ADR-044) — 네임스페이스로 노출 (MegaHealth 패턴 정합).
34
+ export * as MegaAdapterRegistry from './registry.js'
35
+ export { BUILTIN_DRIVERS } from './registry.js'
36
+
37
+ // 전역 어댑터 인스턴스 매니저 (ADR-102) — 글로벌 공유 인스턴스 소유·connect/disconnect.
38
+ export * as MegaAdapterManager from './adapter-manager.js'