@gaonjs/config 0.14.1 → 0.15.2

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/dist/wire.d.ts CHANGED
@@ -16,7 +16,12 @@ export interface WiredGaon {
16
16
  /** 도메인 배터리 배선 결과 — serve·work 가 공유한다. closeDomain() 으로 뒤집는다. */
17
17
  export interface WiredDomain {
18
18
  readonly config: GaonConfig;
19
- /** DB 커넥션 파기 · storage/i18n 리셋(배선 역순). */
19
+ /**
20
+ * 락·캐시 백엔드용 Redis 커넥션(결정 202 · config.redis 있을 때). serve·work 공유 — wireGaon
21
+ * 은 이걸 세션 스토어로도 재사용한다(프로세스당 커넥션 1개). 없으면 undefined.
22
+ */
23
+ readonly redis?: Redis;
24
+ /** DB 커넥션 파기 · storage/i18n 리셋 · 락/캐시 백엔드 해제 · redis 종료(배선 역순). */
20
25
  closeDomain(): Promise<void>;
21
26
  }
22
27
  /**
package/dist/wire.js CHANGED
@@ -11,8 +11,8 @@ import { createDb, registerConnection, destroyAllConnections, getConnection, has
11
11
  import { connectNats, configureJobs, configureEvents, outboxServiceTxWrapper, ensureOutboxTable, setLockBackend, redisLockBackend, memoryLockBackend, } from '@gaonjs/async';
12
12
  import { configureMailer, smtpTransport } from '@gaonjs/mail';
13
13
  import { configureStorage, localDisk, s3Disk, resetStorage } from '@gaonjs/storage';
14
- import { configureI18n, loadLocales, resetI18n, detectLanguage, enterLanguage, languages, } from '@gaonjs/i18n';
15
- import { createApp, redisSessionStore } from '@gaonjs/web';
14
+ import { configureI18n, loadLocales, resetI18n, detectLanguage, enterLanguage, languages, t, } from '@gaonjs/i18n';
15
+ import { createApp, redisSessionStore, setValidationMessageResolver, } from '@gaonjs/web';
16
16
  import { resolveApps, toAppSpec } from './apps.js';
17
17
  import { existsSync } from 'node:fs';
18
18
  import { resolve as resolvePath, join } from 'node:path';
@@ -36,9 +36,30 @@ export async function wireDomain(config, cwd = process.cwd()) {
36
36
  // 하네스가 공유한다(결정 136 · gaon test 도 이 경로를 태워야 스토리지·메일
37
37
  // 잡이 테스트에서 실제로 돈다).
38
38
  await configureNonDbBatteries(config, root);
39
+ // 5) 락·캐시 백엔드(결정 202 · 결정 147·148) — **serve·work 공통**. 잡·서비스가 `lock()`·
40
+ // 캐시를 쓰려면 work 프로세스에도 백엔드가 있어야 한다(이전엔 wireGaon[serve]에만 있어
41
+ // 워커의 lock() 이 "백엔드 미설정" throw · kitchen-sink 중첩 방지 배터리 발견). redis 가
42
+ // 있으면 진짜 분산 락(SET NX PX)·Redis 캐시, 없으면 dev/테스트 in-memory 폴백(락은 결정
43
+ // 88① 로 prod 미설정 시 미배선 유지 → 사용 시 수리 안내 throw). 이 redis 는 wireGaon 이
44
+ // 세션 스토어로도 재사용한다(프로세스당 커넥션 1개).
45
+ let redis;
46
+ if (config.redis) {
47
+ redis = new Redis(config.redis.url, { lazyConnect: false, maxRetriesPerRequest: 3 });
48
+ setLockBackend(redisLockBackend(redis));
49
+ setCacheBackend(redisCacheBackend(redis));
50
+ }
51
+ else if (process.env.NODE_ENV !== 'production') {
52
+ setLockBackend(memoryLockBackend());
53
+ }
39
54
  return {
40
55
  config,
56
+ redis,
41
57
  async closeDomain() {
58
+ // 배선 역순 — 백엔드 해제(다음 인스턴스 누출 방지 · 결정 147·148) → redis 종료 → DB·기타.
59
+ setLockBackend(undefined);
60
+ setCacheBackend(undefined);
61
+ if (redis)
62
+ redis.disconnect();
42
63
  await destroyAllConnections();
43
64
  resetStorage();
44
65
  resetI18n();
@@ -102,6 +123,11 @@ export async function configureNonDbBatteries(config, cwd = process.cwd(), opts
102
123
  fallbackLng: config.i18n.fallbackLng,
103
124
  supportedLngs: config.i18n.supportedLngs,
104
125
  });
126
+ // 결정 183(옵션 C): 검증 사유를 요청 로케일로 로케일화한다. web 은 안정적 코드만
127
+ // 노출하고, 앱이 locales/ 의 예약 namespace `validation.<code>` 로 번역을 준다.
128
+ // 키가 없으면 defaultValue(web 내장 fallback)로 떨어져 거동이 보존된다. t() 는
129
+ // 요청 ALS 로케일을 따르므로 검증 시점(dispatch)에 요청 언어로 번역된다.
130
+ setValidationMessageResolver((code, params, fallback) => t(`validation.${code}`, { ...params, defaultValue: fallback }));
105
131
  }
106
132
  }
107
133
  /**
@@ -120,24 +146,10 @@ export async function wireGaon(config, cwd = process.cwd()) {
120
146
  // 1) 도메인 배터리(DB 커넥션·메일·스토리지·i18n) — serve·work 공통 경로(결정 129).
121
147
  // 잡 핸들러가 모델·메일을 쓰려면 work 도 같은 배선을 태워야 한다.
122
148
  const domain = await wireDomain(config, cwd);
123
- // 2) Redis — 세션 스토어. 스펙 조립 세션 옵션에 주입한다.
124
- let redis;
125
- if (config.redis) {
126
- redis = new Redis(config.redis.url, { lazyConnect: false, maxRetriesPerRequest: 3 });
127
- }
128
- // 결정 147: 분산 락 백엔드 배선. redis 가 있으면 진짜 분산 락(Redis SET NX PX),
129
- // 없으면 dev/테스트용 in-memory 폴백을 심는다. prod 에서 redis 미설정이면 여기서
130
- // 아무 것도 안 심어 lock() 은 미설정 백엔드로 남아 사용 시 수리 안내로 throw 한다
131
- // (로컬 뮤텍스로 조용히 떨어지지 않는다 · 결정 88 ①).
132
- if (redis)
133
- setLockBackend(redisLockBackend(redis));
134
- else if (process.env.NODE_ENV !== 'production')
135
- setLockBackend(memoryLockBackend());
136
- // 결정 148: 캐시 백엔드 배선. redis 가 있으면 Redis 캐시(§7.4), 없으면 data 의
137
- // 기본 in-memory 폴백을 그대로 둔다(캐시는 best-effort 라 미설정 폴백이 안전 ·
138
- // 락과 다른 지점 · 결정 147).
139
- if (redis)
140
- setCacheBackend(redisCacheBackend(redis));
149
+ // 2) Redis — 세션 스토어. 결정 202: 락·캐시 백엔드 배선과 함께 wireDomain 이 소유하는
150
+ // 커넥션을 재사용한다(serve·work 공통 경로 · 프로세스당 커넥션 1개). 세션은 웹 전용이라
151
+ // 여기서 앱 스펙에 주입만 한다(백엔드 배선은 wireDomain 이 이미 완료).
152
+ const redis = domain.redis;
141
153
  // 3) NATS — 웹 프로세스도 채널·잡·이벤트 publish 를 위해 연결한다.
142
154
  // 연결만으로는 부족하다 — 전송 런타임까지 배선해야 컨트롤러·서비스의
143
155
  // `.later()`·`emit()` 이 실제로 publish 된다(work 는 runWork 가 이미 설정하지만
@@ -232,21 +244,32 @@ export async function wireGaon(config, cwd = process.cwd()) {
232
244
  redis,
233
245
  async close() {
234
246
  try {
235
- await app.close();
247
+ // 결정 201(graceful drain): app.close 가 인플라이트 요청 완료를 기다리는 동안
248
+ // (createApp forceCloseConnections:false), idle keep-alive 커넥션은 스윕으로 즉시 닫아
249
+ // 종료가 keepAliveTimeout(기본 72s)까지 매달리지 않게 한다. closeIdleConnections 는
250
+ // 진행 중 요청 커넥션은 건드리지 않는다(실측) — 요청이 끝나 idle 가 되면 다음 스윕이 닫는다.
251
+ const closing = app.close();
252
+ const server = app.server;
253
+ const sweep = server && typeof server.closeIdleConnections === 'function'
254
+ ? setInterval(() => server.closeIdleConnections(), 200)
255
+ : undefined;
256
+ try {
257
+ await closing;
258
+ }
259
+ finally {
260
+ if (sweep)
261
+ clearInterval(sweep);
262
+ }
236
263
  }
237
264
  catch {
238
265
  /* 이미 닫혔을 수 있음 — graceful */
239
266
  }
240
267
  if (nats)
241
268
  await nats.close();
242
- if (redis)
243
- redis.disconnect();
244
- // 결정 147: 락 백엔드 배선 해제(다음 wireGaon 인스턴스로 누출 방지).
245
- setLockBackend(undefined);
246
- // 결정 148: 캐시 백엔드 배선 해제(메모리 폴백으로 되돌림 · 인스턴스 누출 방지).
247
- setCacheBackend(undefined);
248
269
  // 결정 144: 서비스 트랜잭션 래퍼 배선 해제(다음 wireGaon 인스턴스로 누출 방지).
249
270
  setServiceTxWrapper(undefined);
271
+ // 결정 202: 락·캐시 백엔드 해제 + redis 종료는 closeDomain 이 소유한다(serve·work 공통 ·
272
+ // wireDomain 이 배선했으므로 해제도 거기서). serve 는 여기서 중복 해제하지 않는다.
250
273
  await domain.closeDomain();
251
274
  },
252
275
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gaonjs/config",
3
- "version": "0.14.1",
4
- "description": "Gaon 루트 설정 로더·자동 배선 (gaon.config.ts · §3.3 · M9-A)",
3
+ "version": "0.15.2",
4
+ "description": "Gaon 루트 설정 로더·자동 배선 (gaon.config.ts)",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "homepage": "https://gaonjs.dev",
@@ -25,13 +25,13 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "ioredis": "^5.4.1",
28
- "@gaonjs/async": "0.11.0",
29
- "@gaonjs/core": "0.2.1",
30
- "@gaonjs/data": "0.16.0",
31
- "@gaonjs/storage": "0.1.2",
32
- "@gaonjs/mail": "0.2.0",
33
- "@gaonjs/i18n": "0.2.0",
34
- "@gaonjs/web": "0.17.0"
28
+ "@gaonjs/async": "0.12.0",
29
+ "@gaonjs/i18n": "0.2.1",
30
+ "@gaonjs/storage": "0.1.3",
31
+ "@gaonjs/core": "0.2.2",
32
+ "@gaonjs/data": "0.16.1",
33
+ "@gaonjs/mail": "0.2.1",
34
+ "@gaonjs/web": "0.18.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "fastify": "^5.0.0"