@gaonjs/config 0.17.2 → 0.17.4

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/testkit.js CHANGED
@@ -13,8 +13,8 @@ import { loadGaonConfig } from './load.js';
13
13
  import { configureNonDbBatteries } from './wire.js';
14
14
  import { resetStorage } from '@gaonjs/storage';
15
15
  import { resetI18n } from '@gaonjs/i18n';
16
- import { connectNats, configureJobs, configureEvents, configureBroadcast, resetJobs, resetEvents, resetBroadcast, outboxServiceTxWrapper, ensureOutboxTable, relayOutboxBatch, setOutboxDrainer, } from '@gaonjs/async';
17
- import { createDb, registerConnection, deriveTestDatabaseConfig, destroyAllConnections, setServiceTxWrapper, getConnection, hasConnection, } from '@gaonjs/data';
16
+ import { connectNats, configureJobs, configureEvents, configureBroadcast, resetJobs, resetEvents, resetBroadcast, outboxServiceTxWrapper, ensureOutboxTable, relayOutboxBatch, setOutboxDrainer, setLockBackend, memoryLockBackend, } from '@gaonjs/async';
17
+ import { createDb, registerConnection, deriveTestDatabaseConfig, destroyAllConnections, setServiceTxWrapper, setCacheBackend, memoryCacheBackend, getConnection, hasConnection, } from '@gaonjs/data';
18
18
  /**
19
19
  * 테스트용 DB 커넥션 + DB 외 배터리(mail·storage·i18n)를 배선한다(결정 111·136).
20
20
  * 각 커넥션을 `<db>_test` 로 파생해 poolMax:1 로 붙이고, 스토리지·메일을 테스트
@@ -29,6 +29,16 @@ export async function connectTestDatabase(cwd = process.cwd()) {
29
29
  registerConnection(key, createDb(testCfg), base.adapter);
30
30
  }
31
31
  }
32
+ // 결정 249(샘플 C): 락·캐시 백엔드를 serve·work 의 wireDomain(결정 202)과 **대칭**으로
33
+ // 배선한다. 이게 없으면 lock()·cache() 를 쓰는 도메인 서비스가 serve/work 에선 정상인데
34
+ // `gaon test` 에서만 LockBackendUnconfiguredError 로 깨졌다(결정 143·144 의 잡·이벤트 짝에
35
+ // 이어 락·캐시 짝의 사각). 테스트는 단일 프로세스라 분산 락이 무의미하고, 락 키에는 NATS
36
+ // 의 GAON_STREAM_PREFIX 같은 테스트 격리 prefix 가 없어 Redis 를 쓰면 병렬 워커 간 오염
37
+ // 위험이 있다 — 그래서 Redis 유무와 무관하게 in-memory 백엔드로 격리한다(The One Way).
38
+ // 캐시는 기본이 memory 라 이미 동작하지만, 매 테스트 스위트에 fresh 백엔드를 심어 파일 간
39
+ // 캐시 잔상을 끊는다(격리 대칭).
40
+ setLockBackend(memoryLockBackend());
41
+ setCacheBackend(memoryCacheBackend());
32
42
  // 결정 136: 스토리지·메일·i18n 을 serve·work 와 같은 경로로 배선(테스트 격리 값).
33
43
  await configureNonDbBatteries(config, cwd, {
34
44
  bucketFor: (b) => `${b}-test`,
@@ -62,6 +72,9 @@ export async function connectTestDatabase(cwd = process.cwd()) {
62
72
  await destroyAllConnections();
63
73
  resetStorage();
64
74
  resetI18n();
75
+ // 결정 249: 락·캐시 백엔드 해제(다음 테스트 파일로 상태 누출 방지 · wireDomain.closeDomain 대칭).
76
+ setLockBackend(undefined);
77
+ setCacheBackend(undefined);
65
78
  // 잡·이벤트·broadcast 배선 해제 후 NATS 종료(다음 테스트 파일로 상태 누출 방지).
66
79
  resetJobs();
67
80
  resetEvents();
package/dist/wire.js CHANGED
@@ -49,44 +49,58 @@ export async function wireDomain(config, cwd = process.cwd()) {
49
49
  const root = resolvePath(cwd);
50
50
  // 0) 타임존 — 어떤 배터리보다 먼저 적용해 이후 Date·로깅·스케줄러가 선언 타임존을 본다(결정 230).
51
51
  applyTimezone(config);
52
- // 1) DB 커넥션 — 키별로 등록(main + 명명 커넥션 · §4.5). 실 연결은 첫 쿼리에서(pool lazy).
53
- if (config.db) {
54
- for (const [key, cfg] of Object.entries(config.db)) {
55
- const db = createDb(cfg);
56
- registerConnection(key, db, cfg.adapter);
57
- }
58
- }
59
- // 2~4) mail · storage · i18n — DB 외 프로세스 전역 배터리. serve·work·테스트
60
- // 하네스가 공유한다(결정 136 · gaon test 도 이 경로를 태워야 스토리지·메일
61
- // 잡이 테스트에서 실제로 돈다).
62
- await configureNonDbBatteries(config, root);
63
- // 5) 락·캐시 백엔드(결정 202 · 결정 147·148) — **serve·work 공통**. 잡·서비스가 `lock()`·
64
- // 캐시를 쓰려면 work 프로세스에도 백엔드가 있어야 한다(이전엔 wireGaon[serve]에만 있어
65
- // 워커의 lock() 이 "백엔드 미설정" throw · kitchen-sink 중첩 방지 배터리 발견). redis 가
66
- // 있으면 진짜 분산 락(SET NX PX)·Redis 캐시, 없으면 dev/테스트 in-memory 폴백(락은 결정
67
- // 88① 로 prod 미설정 시 미배선 유지 → 사용 시 수리 안내 throw). 이 redis 는 wireGaon 이
68
- // 세션 스토어로도 재사용한다(프로세스당 커넥션 1개).
69
52
  let redis;
70
- if (config.redis) {
71
- redis = new Redis(config.redis.url, { lazyConnect: false, maxRetriesPerRequest: 3 });
72
- setLockBackend(redisLockBackend(redis));
73
- setCacheBackend(redisCacheBackend(redis));
53
+ // 배선 역순 정리 — 백엔드 해제(다음 인스턴스 누출 방지 · 결정 147·148) → redis 종료 → DB·기타.
54
+ // 성공 closeDomain 이, 실패 아래 catch 가 같은 경로로 호출한다. 결정 252(결정 250 확장):
55
+ // 부분 오픈된 핸들(Redis 소켓·DB 커넥션·전역 배터리)을 남긴 채 throw 하면 열린 소켓이 이벤트
56
+ // 루프를 붙잡아 상위(serve/work)가 exit 하지 못하고 매달린다 — 이 함수가 자기가 연 자원을
57
+ // 스스로 정리하고 rethrow 한다(opener 가 정리를 소유).
58
+ const teardown = async () => {
59
+ setLockBackend(undefined);
60
+ setCacheBackend(undefined);
61
+ if (redis)
62
+ redis.disconnect();
63
+ await destroyAllConnections();
64
+ resetStorage();
65
+ resetI18n();
66
+ };
67
+ try {
68
+ // 1) DB 커넥션 — 키별로 등록(main + 명명 커넥션 · §4.5). 실 연결은 첫 쿼리에서(pool lazy).
69
+ if (config.db) {
70
+ for (const [key, cfg] of Object.entries(config.db)) {
71
+ const db = createDb(cfg);
72
+ registerConnection(key, db, cfg.adapter);
73
+ }
74
+ }
75
+ // 2~4) mail · storage · i18n — DB 외 프로세스 전역 배터리. serve·work·테스트
76
+ // 하네스가 공유한다(결정 136 · gaon test 도 이 경로를 태워야 스토리지·메일
77
+ // 잡이 테스트에서 실제로 돈다).
78
+ await configureNonDbBatteries(config, root);
79
+ // 5) 락·캐시 백엔드(결정 202 · 결정 147·148) — **serve·work 공통**. 잡·서비스가 `lock()`·
80
+ // 캐시를 쓰려면 work 프로세스에도 백엔드가 있어야 한다(이전엔 wireGaon[serve]에만 있어
81
+ // 워커의 lock() 이 "백엔드 미설정" throw · kitchen-sink 중첩 방지 배터리 발견). redis 가
82
+ // 있으면 진짜 분산 락(SET NX PX)·Redis 캐시, 없으면 dev/테스트 in-memory 폴백(락은 결정
83
+ // 88① 로 prod 미설정 시 미배선 유지 → 사용 시 수리 안내 throw). 이 redis 는 wireGaon 이
84
+ // 세션 스토어로도 재사용한다(프로세스당 커넥션 1개).
85
+ if (config.redis) {
86
+ redis = new Redis(config.redis.url, { lazyConnect: false, maxRetriesPerRequest: 3 });
87
+ setLockBackend(redisLockBackend(redis));
88
+ setCacheBackend(redisCacheBackend(redis));
89
+ }
90
+ else if (process.env.NODE_ENV !== 'production') {
91
+ setLockBackend(memoryLockBackend());
92
+ }
74
93
  }
75
- else if (process.env.NODE_ENV !== 'production') {
76
- setLockBackend(memoryLockBackend());
94
+ catch (err) {
95
+ // 결정 252: mid-wire 부팅 실패 — 그때까지 연 부분 핸들을 모두 닫고 rethrow 한다.
96
+ await teardown().catch(() => { });
97
+ throw err;
77
98
  }
78
99
  return {
79
100
  config,
80
101
  redis,
81
102
  async closeDomain() {
82
- // 배선 역순 — 백엔드 해제(다음 인스턴스 누출 방지 · 결정 147·148) → redis 종료 → DB·기타.
83
- setLockBackend(undefined);
84
- setCacheBackend(undefined);
85
- if (redis)
86
- redis.disconnect();
87
- await destroyAllConnections();
88
- resetStorage();
89
- resetI18n();
103
+ await teardown();
90
104
  },
91
105
  };
92
106
  }
@@ -172,30 +186,54 @@ export async function configureNonDbBatteries(config, cwd = process.cwd(), opts
172
186
  export async function wireGaon(config, cwd = process.cwd()) {
173
187
  const root = resolvePath(cwd);
174
188
  // 1) 도메인 배터리(DB 커넥션·메일·스토리지·i18n) — serve·work 공통 경로(결정 129).
175
- // 잡 핸들러가 모델·메일을 쓰려면 work 도 같은 배선을 태워야 한다.
189
+ // 잡 핸들러가 모델·메일을 쓰려면 work 도 같은 배선을 태워야 한다. wireDomain 은
190
+ // 자기 mid-wire 실패를 스스로 정리하고 rethrow 한다(결정 252).
176
191
  const domain = await wireDomain(config, cwd);
177
192
  // 2) Redis — 세션 스토어. 결정 202: 락·캐시 백엔드 배선과 함께 wireDomain 이 소유하는
178
193
  // 커넥션을 재사용한다(serve·work 공통 경로 · 프로세스당 커넥션 1개). 세션은 웹 전용이라
179
194
  // 여기서 앱 스펙에 주입만 한다(백엔드 배선은 wireDomain 이 이미 완료).
180
195
  const redis = domain.redis;
181
- // 3) NATS 프로세스도 채널·잡·이벤트 publish 위해 연결한다.
182
- // 연결만으로는 부족하다 전송 런타임까지 배선해야 컨트롤러·서비스의
183
- // `.later()`·`emit()` 실제로 publish 된다(work runWork 이미 설정하지만
184
- // serve 여기서 해야 한다).
196
+ // 결정 252(결정 250 확장): wireDomain 이후 구간(NATS 접속 · ensureOutboxTable DB
197
+ // 접근[잘못된 자격증명이 여기서 throw] · createApp)에서 실패하면, 이미 연 NATS·Redis·DB
198
+ // 핸들이 이벤트 루프를 붙잡아 상위(serve) exit 하지 못하고 매달린다. 구간을 감싸 실패
199
+ // 부분 핸들(nats + domain[Redis·DB])을 모두 닫고 rethrow 한다 — 호출부 serve 는 exit.
185
200
  let nats;
186
- if (config.nats) {
187
- nats = await connectNats({ servers: config.nats.url, name: config.nats.name ?? 'gaon-serve' });
188
- configureJobs(nats);
189
- // 결정 144(F2a): 이벤트 전송 배선 이게 없으면 요청 emit() 이 "전송 미설정" 500.
190
- configureEvents(nats);
191
- // 결정 144(A): 서비스 트랜잭션 emit → 같은 트랜잭션으로 _gaon_outbox 적재(이중 쓰기
192
- // 방지). @gaonjs/async 아웃박스 래퍼를 data service() 트랜잭션에 주입하고,
193
- // 적재 대상 테이블을 main 커넥션에 보장한다(worker 의 릴레이가 그 행을 발행한다).
194
- setServiceTxWrapper(outboxServiceTxWrapper());
195
- if (hasConnection('main'))
196
- await ensureOutboxTable(getConnection('main'));
201
+ try {
202
+ // 3) NATS 프로세스도 채널·잡·이벤트 publish 위해 연결한다.
203
+ // 연결만으로는 부족하다 — 전송 런타임까지 배선해야 컨트롤러·서비스의
204
+ // `.later()`·`emit()` 실제로 publish 된다(work runWork 이미 설정하지만
205
+ // serve 는 여기서 해야 한다).
206
+ if (config.nats) {
207
+ nats = await connectNats({ servers: config.nats.url, name: config.nats.name ?? 'gaon-serve' });
208
+ configureJobs(nats);
209
+ // 결정 144(F2a): 이벤트 전송 배선 — 이게 없으면 웹 요청 emit() 이 "전송 미설정" 500.
210
+ configureEvents(nats);
211
+ // 결정 144(A): 서비스 트랜잭션 emit → 같은 트랜잭션으로 _gaon_outbox 적재(이중 쓰기
212
+ // 방지). @gaonjs/async 의 아웃박스 래퍼를 data 의 service() 트랜잭션에 주입하고,
213
+ // 적재 대상 테이블을 main 커넥션에 보장한다(worker 의 릴레이가 그 행을 발행한다).
214
+ setServiceTxWrapper(outboxServiceTxWrapper());
215
+ if (hasConnection('main'))
216
+ await ensureOutboxTable(getConnection('main'));
217
+ }
218
+ // 5) 앱 스캔 · createApp.
219
+ return await assembleApp(config, root, domain, redis, nats);
220
+ }
221
+ catch (err) {
222
+ // 결정 252: mid-wire 부팅 실패 — 그때까지 연 NATS·도메인(Redis·DB) 핸들을 닫고 rethrow.
223
+ // setServiceTxWrapper 는 이미 세팅됐을 수 있으니 해제한다(close() 대칭 · 다음 인스턴스 누출 방지).
224
+ setServiceTxWrapper(undefined);
225
+ if (nats)
226
+ await nats.close().catch(() => { });
227
+ await domain.closeDomain().catch(() => { });
228
+ throw err;
197
229
  }
198
- // 5) 앱 스캔 · createApp.
230
+ }
231
+ /**
232
+ * 결정 252: wireGaon 의 앱 조립 구간(resolveApps → toAppSpec → createApp)을 분리한다.
233
+ * mid-wire 실패 정리를 위해 wireGaon 이 NATS·DB 부팅 구간을 try 로 감싸는데, 앱 스펙 조립
234
+ * 로직이 길어 인라인하면 catch 범위가 흐려지므로 헬퍼로 뺀다(거동 동일 · 순수 조립).
235
+ */
236
+ async function assembleApp(config, root, domain, redis, nats) {
199
237
  const apps = await resolveApps(root);
200
238
  const specs = apps.map((a) => {
201
239
  let spec = toAppSpec(a);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/config",
3
- "version": "0.17.2",
3
+ "version": "0.17.4",
4
4
  "description": "Gaon 루트 설정 로더·자동 배선 (gaon.config.ts)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,11 +26,11 @@
26
26
  "dependencies": {
27
27
  "ioredis": "^5.4.1",
28
28
  "@gaonjs/async": "0.15.0",
29
- "@gaonjs/core": "0.2.3",
30
29
  "@gaonjs/data": "0.17.0",
30
+ "@gaonjs/i18n": "0.2.2",
31
31
  "@gaonjs/mail": "0.3.0",
32
+ "@gaonjs/core": "0.2.3",
32
33
  "@gaonjs/storage": "0.1.4",
33
- "@gaonjs/i18n": "0.2.2",
34
34
  "@gaonjs/web": "0.19.4"
35
35
  },
36
36
  "devDependencies": {