mega-framework 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/package.json +23 -21
- package/sample/crud/.env +10 -2
- package/sample/crud/.env.example +8 -0
- package/sample/crud/apps/main/controllers/bus-controller.js +122 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +22 -2
- package/sample/crud/apps/main/controllers/lock-controller.js +117 -0
- package/sample/crud/apps/main/jobs/email-job.js +37 -2
- package/sample/crud/apps/main/locales/server/en.json +36 -1
- package/sample/crud/apps/main/locales/server/ko.json +36 -1
- package/sample/crud/apps/main/public/js/bus-demo.js +131 -0
- package/sample/crud/apps/main/public/js/lock-demo.js +122 -0
- package/sample/crud/apps/main/routes/bus.js +43 -0
- package/sample/crud/apps/main/routes/lock.js +35 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +22 -15
- package/sample/crud/apps/main/views/bus/index.ejs +80 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +9 -3
- package/sample/crud/apps/main/views/layouts/main.ejs +2 -0
- package/sample/crud/apps/main/views/lock/index.ejs +99 -0
- package/sample/crud/docs/guide/03-service-model-db.md +48 -0
- package/sample/crud/docs/guide/05-scheduler-job-worker.md +29 -2
- package/sample/crud/docs/guide/09-distributed-lock-and-bus.md +224 -0
- package/sample/crud/docs/guide/10-multi-app.md +74 -0
- package/sample/crud/mega.config.js +32 -0
- package/sample/crud/package.json +3 -2
- package/sample/multi/.env +16 -0
- package/sample/multi/.env.example +17 -0
- package/sample/multi/README.md +54 -0
- package/sample/multi/apps/admin/app.config.js +24 -0
- package/sample/multi/apps/admin/controllers/admin-controller.js +42 -0
- package/sample/multi/apps/admin/public/js/admin.js +31 -0
- package/sample/multi/apps/admin/routes/pages.js +11 -0
- package/sample/multi/apps/admin/views/index.ejs +33 -0
- package/sample/multi/apps/web/app.config.js +30 -0
- package/sample/multi/apps/web/controllers/web-controller.js +45 -0
- package/sample/multi/apps/web/public/js/web.js +24 -0
- package/sample/multi/apps/web/routes/pages.js +13 -0
- package/sample/multi/apps/web/views/index.ejs +51 -0
- package/sample/multi/mega.config.js +42 -0
- package/sample/multi/package.json +20 -0
- package/sample/simple/package.json +2 -2
- package/src/adapters/nats-adapter.js +39 -44
- package/src/adapters/nats-codec.js +38 -0
- package/src/cli/commands/scaffold.js +1 -0
- package/src/cli/index.js +50 -1
- package/src/core/app-registry.js +69 -0
- package/src/core/boot.js +99 -0
- package/src/core/bus/cluster-bus.js +190 -0
- package/src/core/bus/contract.js +123 -0
- package/src/core/bus/index.js +285 -0
- package/src/core/bus/memory-bus.js +103 -0
- package/src/core/bus/nats-bus.js +203 -0
- package/src/core/config-validator.js +118 -1
- package/src/core/ctx-builder.js +14 -1
- package/src/core/index.js +2 -0
- package/src/core/lock/cluster-lock.js +174 -0
- package/src/core/lock/contract.js +123 -0
- package/src/core/lock/fifo-waitlist.js +93 -0
- package/src/core/lock/index.js +292 -0
- package/src/core/lock/memory-lock.js +162 -0
- package/src/core/lock/redis-lock.js +276 -0
- package/src/core/mega-app.js +29 -0
- package/src/core/migration/generate.js +1 -1
- package/src/core/migration/journal.js +1 -1
- package/src/core/scope-registry.js +9 -0
- package/src/eslint-plugin/no-direct-model-import.js +2 -2
- package/src/index.js +2 -0
- package/src/lib/mega-job-queue.js +71 -47
- package/types/adapters/mega-adapter.d.ts +1 -1
- package/types/adapters/nats-adapter.d.ts +4 -4
- package/types/adapters/nats-codec.d.ts +13 -0
- package/types/adapters/redlock-adapter.d.ts +1 -1
- package/types/core/app-registry.d.ts +22 -0
- package/types/core/bus/cluster-bus.d.ts +45 -0
- package/types/core/bus/contract.d.ts +164 -0
- package/types/core/bus/index.d.ts +100 -0
- package/types/core/bus/memory-bus.d.ts +45 -0
- package/types/core/bus/nats-bus.d.ts +41 -0
- package/types/core/index.d.ts +1 -0
- package/types/core/lock/cluster-lock.d.ts +44 -0
- package/types/core/lock/contract.d.ts +181 -0
- package/types/core/lock/fifo-waitlist.d.ts +38 -0
- package/types/core/lock/index.d.ts +96 -0
- package/types/core/lock/memory-lock.d.ts +58 -0
- package/types/core/lock/redis-lock.d.ts +43 -0
- package/types/core/mega-app.d.ts +10 -0
- package/types/core/scope-registry.d.ts +6 -0
- package/types/index.d.ts +1 -1
- package/types/lib/mega-job-queue.d.ts +27 -4
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* MegaNatsAdapter — NATS 메시지 버스 어댑터 (
|
|
3
|
+
* MegaNatsAdapter — NATS 메시지 버스 어댑터 (`@nats-io/*` v3 driver 래퍼, ADR-112/225).
|
|
4
4
|
*
|
|
5
5
|
* **첫 bus 도메인 어댑터**(`MegaBusAdapter` 첫 구체). DB/cache 와 달리 pub/sub·req/reply·
|
|
6
6
|
* queue group 메시징 인터페이스를 구현한다.
|
|
7
7
|
*
|
|
8
|
+
* # nats v3 (`@nats-io/*`) — v2 `nats` 단일 패키지에서 분리됨 (ADR-225)
|
|
9
|
+
* core connect 는 `@nats-io/transport-node`, 타입은 `@nats-io/nats-core`. `JSONCodec()` 팩토리가
|
|
10
|
+
* 제거돼 본 어댑터는 {@link import('./nats-codec.js')} 의 공유 JSON 코덱을 쓴다.
|
|
11
|
+
*
|
|
8
12
|
* # 표준 표면 (MegaBusAdapter 상속)
|
|
9
13
|
* - `_connect()` — `connect({ servers, ...auth, ...options })` (driver 는 connect 시점 lazy import).
|
|
10
14
|
* connect() 자체가 서버 응답까지 기다리므로 별도 ping 불필요(실패 시 throw=검증).
|
|
@@ -15,10 +19,10 @@
|
|
|
15
19
|
* - `getStats()` — 베이스 stats + nats 특화(server/연결 stats: inMsgs/outMsgs/inBytes/outBytes).
|
|
16
20
|
* - `publish/subscribe/request` + `enqueue/process` — 아래 인터페이스.
|
|
17
21
|
*
|
|
18
|
-
* # 직렬화 = JSONCodec
|
|
19
|
-
* payload 는 NATS wire 에서 `Uint8Array` 다. 본 어댑터는
|
|
20
|
-
* 사용자는 JS 값을 그대로 publish/subscribe
|
|
21
|
-
* 는 `null` 로 정규화(
|
|
22
|
+
* # 직렬화 = 공유 JSON 코덱 (ADR-225 — v3 JSONCodec 제거)
|
|
23
|
+
* payload 는 NATS wire 에서 `Uint8Array` 다. 본 어댑터는 {@link import('./nats-codec.js')} 의
|
|
24
|
+
* `encodeJson`/`decodeJson` 로 인코드/디코드를 표준화해 사용자는 JS 값을 그대로 publish/subscribe
|
|
25
|
+
* 한다(수신 측에서 같은 값으로 복원). `undefined` payload 는 `null` 로 정규화(JSON 이 undefined 미표현).
|
|
22
26
|
*
|
|
23
27
|
* # queue (job) 인터페이스 — 단순 publish + queue group (jetstream 미사용, ADR-112)
|
|
24
28
|
* `enqueue(job, msg)` = 해당 subject 로 **단순 publish**. `process(job, handler)` = **queue group**
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
import { MegaValidationError, MegaInternalError } from '../errors/http-errors.js'
|
|
60
64
|
import { MegaBusAdapter } from './mega-bus-adapter.js'
|
|
61
65
|
import { resolveConnection, assertPlainObject } from './adapter-options.js'
|
|
66
|
+
import { encodeJson, decodeJson } from './nats-codec.js'
|
|
62
67
|
import * as Registry from './registry.js'
|
|
63
68
|
|
|
64
69
|
/** NATS 기본 클라이언트 포트(discrete 모드에서 port 미지정 시). */
|
|
@@ -78,11 +83,9 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30000
|
|
|
78
83
|
*/
|
|
79
84
|
|
|
80
85
|
export class MegaNatsAdapter extends MegaBusAdapter {
|
|
81
|
-
/** @type {import('nats').NatsConnection | null} 연결된 NatsConnection (connect 후에만). */
|
|
86
|
+
/** @type {import('@nats-io/nats-core').NatsConnection | null} 연결된 NatsConnection (connect 후에만). */
|
|
82
87
|
#nc = null
|
|
83
|
-
/** @type {import('nats').
|
|
84
|
-
#codec = null
|
|
85
|
-
/** @type {import('nats').ConnectionOptions} _connect 에서 `connect()` 에 넘길 옵션(생성자에서 고정). */
|
|
88
|
+
/** @type {import('@nats-io/nats-core').ConnectionOptions} _connect 에서 `connect()` 에 넘길 옵션(생성자에서 고정). */
|
|
86
89
|
#connectOptions
|
|
87
90
|
|
|
88
91
|
/**
|
|
@@ -107,7 +110,7 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
107
110
|
const conn = resolveConnection(config, { driver: 'nats', dbConflictsWithUrl: false })
|
|
108
111
|
assertPlainObject('options', config.options, { driver: 'nats' })
|
|
109
112
|
|
|
110
|
-
/** @type {import('nats').ConnectionOptions} */
|
|
113
|
+
/** @type {import('@nats-io/nats-core').ConnectionOptions} */
|
|
111
114
|
const connectOptions = {}
|
|
112
115
|
// options(passthrough) 먼저 — 아래 servers/auth 가 항상 이긴다(연결 필수값 보호).
|
|
113
116
|
if (config.options !== undefined) Object.assign(connectOptions, config.options)
|
|
@@ -135,10 +138,9 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
135
138
|
* @returns {Promise<void>}
|
|
136
139
|
*/
|
|
137
140
|
async _connect() {
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
this.#nc =
|
|
141
|
-
this.#codec = JSONCodec()
|
|
141
|
+
// v3: core connect 는 `@nats-io/transport-node`(노드 전송) — v2 `nats` 단일 패키지 대체(ADR-225).
|
|
142
|
+
const { connect } = await import('@nats-io/transport-node')
|
|
143
|
+
this.#nc = await connect(this.#connectOptions)
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
/**
|
|
@@ -151,7 +153,6 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
151
153
|
if (this.#nc !== null) {
|
|
152
154
|
const nc = this.#nc
|
|
153
155
|
this.#nc = null
|
|
154
|
-
this.#codec = null
|
|
155
156
|
// 이미 닫혀 있으면(서버측 절단 등) drain 이 throw 할 수 있어 가드.
|
|
156
157
|
if (!nc.isClosed()) await nc.drain()
|
|
157
158
|
}
|
|
@@ -160,7 +161,7 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
160
161
|
/**
|
|
161
162
|
* raw NatsConnection handle (ADR-009). `connect()` 후에만 베이스 `native` getter 가 호출한다.
|
|
162
163
|
* @protected
|
|
163
|
-
* @returns {import('nats').NatsConnection}
|
|
164
|
+
* @returns {import('@nats-io/nats-core').NatsConnection}
|
|
164
165
|
*/
|
|
165
166
|
_native() {
|
|
166
167
|
if (this.#nc === null) {
|
|
@@ -193,7 +194,7 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
193
194
|
|
|
194
195
|
/**
|
|
195
196
|
* 누적 통계 + nats 특화(server + 연결 stats). 연결 전이면 server/stats 는 undefined.
|
|
196
|
-
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, server: string | undefined, nats: import('nats').Stats | undefined }}
|
|
197
|
+
* @returns {ReturnType<import('./mega-adapter.js').MegaAdapter['getStats']> & { driver: string, server: string | undefined, nats: import('@nats-io/nats-core').Stats | undefined }}
|
|
197
198
|
*/
|
|
198
199
|
getStats() {
|
|
199
200
|
const nc = this.#nc
|
|
@@ -211,15 +212,15 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
211
212
|
// ──────────────────────────────────────────────────────────────────────
|
|
212
213
|
|
|
213
214
|
/**
|
|
214
|
-
* fire-and-forget 발행 (ack X). payload 는
|
|
215
|
+
* fire-and-forget 발행 (ack X). payload 는 공유 JSON 코덱(encodeJson)으로 인코드.
|
|
215
216
|
* @param {string} subject
|
|
216
217
|
* @param {any} payload
|
|
217
218
|
* @returns {Promise<void>}
|
|
218
219
|
*/
|
|
219
220
|
async publish(subject, payload) {
|
|
220
221
|
return this._instrument('publish', { subject }, async () => {
|
|
221
|
-
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
222
|
-
nc.publish(subject,
|
|
222
|
+
const nc = /** @type {import('@nats-io/nats-core').NatsConnection} */ (this.#nc)
|
|
223
|
+
nc.publish(subject, encodeJson(payload))
|
|
223
224
|
})
|
|
224
225
|
}
|
|
225
226
|
|
|
@@ -233,7 +234,7 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
233
234
|
*/
|
|
234
235
|
async subscribe(subject, handler) {
|
|
235
236
|
return this._instrument('subscribe', { subject }, async () => {
|
|
236
|
-
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
237
|
+
const nc = /** @type {import('@nats-io/nats-core').NatsConnection} */ (this.#nc)
|
|
237
238
|
const sub = nc.subscribe(subject, {
|
|
238
239
|
callback: (err, msg) => this.#dispatch('subscribe', subject, handler, err, msg),
|
|
239
240
|
})
|
|
@@ -254,20 +255,22 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
254
255
|
*/
|
|
255
256
|
async request(subject, payload, { timeout = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
|
|
256
257
|
return this._instrument('request', { subject, timeout }, async () => {
|
|
257
|
-
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
258
|
+
const nc = /** @type {import('@nats-io/nats-core').NatsConnection} */ (this.#nc)
|
|
258
259
|
try {
|
|
259
|
-
const reply = await nc.request(subject,
|
|
260
|
-
return
|
|
260
|
+
const reply = await nc.request(subject, encodeJson(payload), { timeout })
|
|
261
|
+
return decodeJson(reply.data)
|
|
261
262
|
} catch (err) {
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
// v3(ADR-225): 타임아웃/무응답이 문자열 code('TIMEOUT'/'503')에서 **에러 클래스**로 바뀌었다.
|
|
264
|
+
// 클래스는 cold path 라 catch 에서 lazy import(모듈은 _connect 가 이미 로드 — 비용 0). 명시
|
|
265
|
+
// 에러 코드로 변환(silent 무시 X). RequestError 는 no-responders 를 isNoResponders() 로 알린다.
|
|
266
|
+
const { TimeoutError, NoRespondersError, RequestError } = await import('@nats-io/nats-core')
|
|
267
|
+
if (err instanceof TimeoutError) {
|
|
265
268
|
throw new MegaInternalError('bus.request_timeout', `nats request("${subject}") timed out after ${timeout}ms.`, {
|
|
266
269
|
details: { subject, timeout },
|
|
267
270
|
cause: err,
|
|
268
271
|
})
|
|
269
272
|
}
|
|
270
|
-
if (
|
|
273
|
+
if (err instanceof NoRespondersError || (err instanceof RequestError && err.isNoResponders())) {
|
|
271
274
|
throw new MegaInternalError('bus.no_responders', `nats request("${subject}"): no responders subscribed to the subject.`, {
|
|
272
275
|
details: { subject },
|
|
273
276
|
cause: err,
|
|
@@ -286,8 +289,8 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
286
289
|
*/
|
|
287
290
|
async enqueue(jobName, payload) {
|
|
288
291
|
return this._instrument('enqueue', { jobName }, async () => {
|
|
289
|
-
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
290
|
-
nc.publish(jobName,
|
|
292
|
+
const nc = /** @type {import('@nats-io/nats-core').NatsConnection} */ (this.#nc)
|
|
293
|
+
nc.publish(jobName, encodeJson(payload))
|
|
291
294
|
})
|
|
292
295
|
}
|
|
293
296
|
|
|
@@ -313,7 +316,7 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
313
316
|
*/
|
|
314
317
|
async process(jobName, handler, { queue = jobName } = {}) {
|
|
315
318
|
return this._instrument('process', { jobName, queue }, async () => {
|
|
316
|
-
const nc = /** @type {import('nats').NatsConnection} */ (this.#nc)
|
|
319
|
+
const nc = /** @type {import('@nats-io/nats-core').NatsConnection} */ (this.#nc)
|
|
317
320
|
nc.subscribe(jobName, {
|
|
318
321
|
queue,
|
|
319
322
|
callback: (err, msg) => this.#dispatch('process', jobName, (m) => handler(m), err, msg),
|
|
@@ -321,23 +324,15 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
321
324
|
})
|
|
322
325
|
}
|
|
323
326
|
|
|
324
|
-
/**
|
|
325
|
-
* payload → Uint8Array (JSONCodec). undefined 는 null 로 정규화(JSONCodec 가 undefined 미지원).
|
|
326
|
-
* @param {any} payload @returns {Uint8Array}
|
|
327
|
-
*/
|
|
328
|
-
#encode(payload) {
|
|
329
|
-
return /** @type {import('nats').Codec<any>} */ (this.#codec).encode(payload === undefined ? null : payload)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
327
|
/**
|
|
333
328
|
* 구독/잡 콜백 공통 디스패처 — 에러·디코드·handler 호출을 표면화한다(silent 금지).
|
|
334
|
-
* 구독
|
|
329
|
+
* 구독 에러·디코드 실패·handler throw 를 모두 `console.error` 로 드러낸다.
|
|
335
330
|
*
|
|
336
331
|
* @param {'subscribe' | 'process'} kind
|
|
337
332
|
* @param {string} subject
|
|
338
333
|
* @param {(msg: any, replyFn?: (payload: any) => void) => any} handler
|
|
339
|
-
* @param {
|
|
340
|
-
* @param {import('nats').Msg} msg
|
|
334
|
+
* @param {Error | null} err
|
|
335
|
+
* @param {import('@nats-io/nats-core').Msg} msg
|
|
341
336
|
* @returns {void}
|
|
342
337
|
*/
|
|
343
338
|
#dispatch(kind, subject, handler, err, msg) {
|
|
@@ -348,14 +343,14 @@ export class MegaNatsAdapter extends MegaBusAdapter {
|
|
|
348
343
|
}
|
|
349
344
|
let decoded
|
|
350
345
|
try {
|
|
351
|
-
decoded =
|
|
346
|
+
decoded = decodeJson(msg.data)
|
|
352
347
|
} catch (decodeErr) {
|
|
353
348
|
console.error(`[MegaNatsAdapter] ${kind}("${subject}") payload decode failed:`, decodeErr)
|
|
354
349
|
return
|
|
355
350
|
}
|
|
356
351
|
// reply subject 가 있으면 replyFn 제공(request 응답용). subscribe 만 해당 — process 는 단방향.
|
|
357
352
|
const replyFn =
|
|
358
|
-
kind === 'subscribe' && msg.reply ? /** @param {any} p */ (p) => msg.respond(
|
|
353
|
+
kind === 'subscribe' && msg.reply ? /** @param {any} p */ (p) => msg.respond(encodeJson(p)) : undefined
|
|
359
354
|
try {
|
|
360
355
|
const out = handler(decoded, replyFn)
|
|
361
356
|
// handler 가 async 면 reject 도 표면화(떠다니는 promise 가 silent 실패되지 않게).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* NATS JSON wire 코덱 — `@nats-io/*` v3 에서 제거된 `JSONCodec()` 대체 (ADR-225).
|
|
4
|
+
*
|
|
5
|
+
* v2 `nats` 패키지는 `JSONCodec()` 팩토리로 JS 값 ↔ `Uint8Array` 변환을 제공했으나, v3
|
|
6
|
+
* (`@nats-io/nats-core`)에서 코덱 팩토리가 제거되고 메시지에 `.json()`/`.string()` 편의 메서드만
|
|
7
|
+
* 남았다. 본 모듈은 어댑터·잡 큐가 wire 에 싣는 payload 의 JSON 직렬화를 **한 곳에서** 정의해
|
|
8
|
+
* (`MegaNatsAdapter` publish ↔ 소비자 decode 라운드트립 일관성), v2 `JSONCodec` 의 의미를 보존한다:
|
|
9
|
+
* - encode: `undefined` 는 `null` 로 정규화(JSON 은 `undefined` 를 표현 못 함) 후 `JSON.stringify`.
|
|
10
|
+
* - decode: 빈 payload(길이 0)는 `null` 로(빈 발행을 graceful 처리). 그 외는 `JSON.parse`.
|
|
11
|
+
*
|
|
12
|
+
* TextEncoder/TextDecoder 는 Node 전역(웹 표준)이라 신규 의존성 0.
|
|
13
|
+
*
|
|
14
|
+
* @module adapters/nats-codec
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TE = new TextEncoder()
|
|
18
|
+
const TD = new TextDecoder()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* JS 값 → NATS wire 바이트(JSON). `undefined` 는 `null` 로 정규화한다.
|
|
22
|
+
* @param {any} value
|
|
23
|
+
* @returns {Uint8Array}
|
|
24
|
+
*/
|
|
25
|
+
export function encodeJson(value) {
|
|
26
|
+
return TE.encode(JSON.stringify(value === undefined ? null : value))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* NATS wire 바이트(JSON) → JS 값. 빈 payload(길이 0)는 `null`. 파싱 실패는 throw(호출부가 처리).
|
|
31
|
+
* @param {Uint8Array} data
|
|
32
|
+
* @returns {any}
|
|
33
|
+
* @throws {SyntaxError} JSON 파싱 실패 시(silent 금지 — poison 메시지 감지에 사용).
|
|
34
|
+
*/
|
|
35
|
+
export function decodeJson(data) {
|
|
36
|
+
if (!data || data.byteLength === 0) return null
|
|
37
|
+
return JSON.parse(TD.decode(data))
|
|
38
|
+
}
|
|
@@ -162,6 +162,7 @@ export function registerScaffoldCommands(program, { out, projectRoot, logger, re
|
|
|
162
162
|
throw new Error(
|
|
163
163
|
`Unknown generator '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')}. ` +
|
|
164
164
|
`(플러그인 generator 확인 실패: ${/** @type {any} */ (e).message ?? e})`,
|
|
165
|
+
{ cause: e },
|
|
165
166
|
)
|
|
166
167
|
}
|
|
167
168
|
if (def === undefined) {
|
package/src/cli/index.js
CHANGED
|
@@ -396,11 +396,13 @@ async function runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath
|
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
// 런타임 그래프 lazy 로드 — 부팅 명령에서만 프레임워크 전체를 지불한다(모듈 상단 주석).
|
|
399
|
-
const [{ bootApp }, { MegaCluster }, { installPrimaryAggregator, installWorkerResponder }, { buildLogger }] =
|
|
399
|
+
const [{ bootApp }, { MegaCluster }, { installPrimaryAggregator, installWorkerResponder }, { installClusterLockMaster }, { installClusterBusMaster }, { buildLogger }] =
|
|
400
400
|
await Promise.all([
|
|
401
401
|
import('../core/boot.js'),
|
|
402
402
|
import('../core/mega-cluster.js'),
|
|
403
403
|
import('../core/cluster-metrics.js'),
|
|
404
|
+
import('../core/lock/cluster-lock.js'),
|
|
405
|
+
import('../core/bus/cluster-bus.js'),
|
|
404
406
|
import('../lib/mega-logger.js'),
|
|
405
407
|
])
|
|
406
408
|
|
|
@@ -444,6 +446,12 @@ async function runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath
|
|
|
444
446
|
if (mega.isPrimary()) {
|
|
445
447
|
// 마스터에 메트릭 집계기 설치(ADR-163) — 워커의 /metrics 요청을 받아 전 워커 합산해 회신.
|
|
446
448
|
installPrimaryAggregator()
|
|
449
|
+
// 마스터에 분산 락 responder 설치(ADR-226) — cluster lock driver 워커들의 IPC 요청을 한 곳에서 직렬화.
|
|
450
|
+
// redis 없이 클러스터로만 돌 때 워커 간 상호배제를 마스터의 in-memory 락으로 제공한다(단일 노드 한정).
|
|
451
|
+
installClusterLockMaster()
|
|
452
|
+
// 마스터에 메시지 버스 라우터 설치(ADR-227) — cluster bus driver 워커들의 pub/sub IPC 를 fan-out.
|
|
453
|
+
// NATS 없이 클러스터로만 돌 때 워커 간 메시지 전달을 마스터 라우터로 제공한다(단일 노드 한정).
|
|
454
|
+
installClusterBusMaster()
|
|
447
455
|
announce(masterLogger, `cluster master ${process.pid} forked ${workers} worker(s)`)
|
|
448
456
|
}
|
|
449
457
|
return 0
|
|
@@ -691,6 +699,39 @@ async function wireHostLogger(global, injectedLogger) {
|
|
|
691
699
|
return appLogger
|
|
692
700
|
}
|
|
693
701
|
|
|
702
|
+
/**
|
|
703
|
+
* 잡 워커의 큐 길목 이벤트를 호스트 로거로 흘린다(ADR-223). `MegaJobWorker` 는 순수 런타임이라 logger 를
|
|
704
|
+
* 모르고 이벤트만 재방출하므로, 호스트가 구독하지 않으면 production 에서 잡 실패·DLQ 격리가 로그에 안 남는다.
|
|
705
|
+
* 레벨: `fail`=error(최종 실패), `dlq`=warn(격리됨), `retry`=warn(재시도), `start`/`done`=debug(prod silent).
|
|
706
|
+
* `dispatch`(enqueue)는 producer(웹) 측 이벤트라 워커 호스트에선 발화하지 않아 구독하지 않는다. 핵심 식별
|
|
707
|
+
* 필드만 박는다(전체 페이로드·결과 금지, P5). `log` 가 없으면(로거 미구성) 옵셔널 체이닝으로 no-op.
|
|
708
|
+
* @param {import('../lib/mega-job-worker.js').MegaJobWorker} worker
|
|
709
|
+
* @param {{ debug?: Function, warn?: Function, error?: Function }|null|undefined} log
|
|
710
|
+
* @returns {void}
|
|
711
|
+
*/
|
|
712
|
+
function subscribeJobLogging(worker, log) {
|
|
713
|
+
worker.on('fail', (e) => log?.error?.({ err: e?.error, subject: e?.subject, seq: e?.seq, phase: e?.phase }, 'job failed'))
|
|
714
|
+
worker.on('dlq', (e) => log?.warn?.({ subject: e?.subject, dlqSubject: e?.dlqSubject, seq: e?.seq }, 'job routed to DLQ'))
|
|
715
|
+
worker.on('retry', (e) => log?.warn?.({ err: e?.error, subject: e?.subject, seq: e?.seq, attempt: e?.attempt, retriesLeft: e?.retriesLeft }, 'job retry'))
|
|
716
|
+
worker.on('start', (e) => log?.debug?.({ subject: e?.subject, seq: e?.seq }, 'job start'))
|
|
717
|
+
worker.on('done', (e) => log?.debug?.({ subject: e?.subject, seq: e?.seq }, 'job done'))
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* 스케줄러의 길목 이벤트를 호스트 로거로 흘린다(ADR-223 — 잡 워커와 같은 갭). `MegaScheduler` 도 이벤트
|
|
722
|
+
* (run/skip/done/fail)만 노출하므로 호스트가 구독해 로그로 남긴다. 레벨: `fail`=error, 나머지는 debug
|
|
723
|
+
* (특히 `skip`=락 미획득은 클러스터 정상 동작이라 debug). `key`(락 키)는 클러스터 leader 추적에 유용.
|
|
724
|
+
* @param {import('../lib/mega-schedule.js').MegaScheduler} scheduler
|
|
725
|
+
* @param {{ debug?: Function, warn?: Function, error?: Function }|null|undefined} log
|
|
726
|
+
* @returns {void}
|
|
727
|
+
*/
|
|
728
|
+
function subscribeScheduleLogging(scheduler, log) {
|
|
729
|
+
scheduler.on('fail', (e) => log?.error?.({ err: e?.error, name: e?.name, key: e?.key, phase: e?.phase }, 'schedule failed'))
|
|
730
|
+
scheduler.on('skip', (e) => log?.debug?.({ name: e?.name, key: e?.key, reason: e?.reason }, 'schedule skipped'))
|
|
731
|
+
scheduler.on('run', (e) => log?.debug?.({ name: e?.name, key: e?.key }, 'schedule run'))
|
|
732
|
+
scheduler.on('done', (e) => log?.debug?.({ name: e?.name, key: e?.key }, 'schedule done'))
|
|
733
|
+
}
|
|
734
|
+
|
|
694
735
|
/**
|
|
695
736
|
* `mega worker` 호스트 골격 — config + 어댑터 connect + ctx + `MegaJobWorker` 인스턴스 + graceful.
|
|
696
737
|
* 등록 소스 = `config.jobs` + 플러그인 `mega.jobs.register` 분(`collectRegistrations`, ADR-123).
|
|
@@ -712,6 +753,11 @@ export async function runWorkerHost(projectRoot, logger) {
|
|
|
712
753
|
// 옵트인 OFF 면 subscribeJobs 는 no-op() 을 반환한다. start 전에 구독해 enqueue(dispatch)부터 잡는다. 구독은
|
|
713
754
|
// MegaMetrics.shutdown(boot 가 'mega-metrics' MegaShutdown hook 으로 등록)에서 일괄 해제되므로 별도 등록 불필요.
|
|
714
755
|
MegaMetrics.subscribeJobs(worker)
|
|
756
|
+
// 잡 길목 로깅 (ADR-223) — MegaJobWorker 는 큐 이벤트를 재방출만 하고 logger 를 모른다(순수 런타임). 호스트가
|
|
757
|
+
// 구독해 로그로 남기지 않으면 production 에서 잡 영구 실패·DLQ 격리가 로그에 0 줄로 남아(메트릭/데모 페이지로만
|
|
758
|
+
// 보임) 운영자가 실패 신호를 못 받는다(mega-job-queue 설계: "fail/dlq 구독이 사실상 필수"). P5(에러 핸들러·
|
|
759
|
+
// async 경계 로그). 메트릭(subscribeJobs)과 독립적이라 둘 다 둔다. 페이로드 전체가 아닌 핵심 필드만 박는다(P5).
|
|
760
|
+
subscribeJobLogging(worker, log)
|
|
715
761
|
// 등록 소스(ADR-123) = config.jobs(정적) + 플러그인 host.listJobs()(동적). register 가 subject/bus
|
|
716
762
|
// 미선언·중복을 부팅 시 fail-fast.
|
|
717
763
|
const jobs = collectRegistrations(global, host, 'jobs')
|
|
@@ -740,6 +786,9 @@ export async function runSchedulerHost(projectRoot, logger) {
|
|
|
740
786
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
741
787
|
const log = await wireHostLogger(global, logger)
|
|
742
788
|
const scheduler = new MegaScheduler({ ctx })
|
|
789
|
+
// 스케줄 길목 로깅 (ADR-223) — 워커와 같은 갭: MegaScheduler 도 이벤트(run/skip/done/fail)를 노출만 하고
|
|
790
|
+
// logger 를 모른다. 호스트가 구독하지 않으면 production 에서 스케줄 실패가 로그에 안 남는다. P5.
|
|
791
|
+
subscribeScheduleLogging(scheduler, log)
|
|
743
792
|
// 등록 소스(ADR-123) = config.schedules(정적) + 플러그인 host.listSchedules()(동적). register 가
|
|
744
793
|
// cron 미선언·중복을 부팅 시 fail-fast.
|
|
745
794
|
const schedules = collectRegistrations(global, host, 'schedules')
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 부팅된 MegaApp 프로세스 레지스트리 — **ctx 없는 영역**(백그라운드 setInterval·외부 SDK 콜백·테스트 헬퍼)에서
|
|
4
|
+
* `getApp()` 으로 booted 앱에 접근해 `app.lock` / `app.bus` 등 process-level 표면을 쓴다 (ADR-228).
|
|
5
|
+
*
|
|
6
|
+
* # 왜 필요한가
|
|
7
|
+
* `ctx.lock`/`ctx.bus` 는 요청 ctx 에만 있어, 요청 흐름 밖(타이머·외부 라이브러리 콜백)에선 잡을 수 없었다.
|
|
8
|
+
* lock/bus manager 는 **process 싱글톤**(`setLockManager`/`setBusManager`)이라 요청과 무관하게 같은 인스턴스다.
|
|
9
|
+
* `getApp()` 은 그 싱글톤을 들고 있는 MegaApp 을 돌려줘, ctx 가 있을 때와 **동일 표면**(app.lock/app.bus)을 제공한다.
|
|
10
|
+
*
|
|
11
|
+
* # ctx 와의 차이
|
|
12
|
+
* `app.*` 는 **요청 무관** 자원만 — `lock`/`bus`(process manager) · `db(alias)`/`cache(alias)`(앱 별명 해석) ·
|
|
13
|
+
* `log`/`name`. 요청-스코프(`req`/`user`/`session`/`reply`/요청 locale `t`)는 **없다** — 그건 ctx 전용이다.
|
|
14
|
+
*
|
|
15
|
+
* @module core/app-registry
|
|
16
|
+
*/
|
|
17
|
+
import { MegaError } from '../errors/mega-error.js'
|
|
18
|
+
|
|
19
|
+
/** @type {Map<string, import('./mega-app.js').MegaApp>} name → booted MegaApp(등록 순서). */
|
|
20
|
+
const apps = new Map()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* booted MegaApp 을 레지스트리에 등록한다(boot 의 apps 스테이지가 앱마다 1회 호출).
|
|
24
|
+
* @param {import('./mega-app.js').MegaApp} app
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
export function setApp(app) {
|
|
28
|
+
apps.set(app.name, app)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* booted MegaApp 을 가져온다 — ctx 없는 영역의 표준 접근점.
|
|
33
|
+
* - `name` 지정: 그 이름의 앱(없으면 `app.not_found`).
|
|
34
|
+
* - `name` 생략 + 앱 1개: 그 앱.
|
|
35
|
+
* - `name` 생략 + 앱 0개: `app.not_initialized`(부팅 전 호출 — fail-fast).
|
|
36
|
+
* - `name` 생략 + 앱 2개 이상: `app.ambiguous`(이름 필수).
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [name] - 앱 이름(멀티앱 프로세스에서 지정).
|
|
39
|
+
* @returns {import('./mega-app.js').MegaApp}
|
|
40
|
+
* @throws {MegaError} `app.not_initialized` | `app.ambiguous` | `app.not_found`
|
|
41
|
+
*/
|
|
42
|
+
export function getApp(name) {
|
|
43
|
+
if (name !== undefined) {
|
|
44
|
+
const app = apps.get(name)
|
|
45
|
+
if (!app) {
|
|
46
|
+
throw new MegaError('app.not_found', `getApp('${name}') — no booted app named '${name}'. Booted: [${[...apps.keys()].join(', ') || '(none)'}].`, {
|
|
47
|
+
details: { name, booted: [...apps.keys()] },
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
return app
|
|
51
|
+
}
|
|
52
|
+
if (apps.size === 0) {
|
|
53
|
+
throw new MegaError('app.not_initialized', 'getApp() called before boot — no app is initialized. Call after bootApp() completes (e.g. afterBoot hook or post-listen background tasks).', { details: {} })
|
|
54
|
+
}
|
|
55
|
+
if (apps.size > 1) {
|
|
56
|
+
throw new MegaError('app.ambiguous', `getApp() is ambiguous — ${apps.size} apps booted ([${[...apps.keys()].join(', ')}]). Pass a name: getApp('<name>').`, { details: { booted: [...apps.keys()] } })
|
|
57
|
+
}
|
|
58
|
+
return /** @type {import('./mega-app.js').MegaApp} */ ([...apps.values()][0])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 등록된 앱이 있나(부팅 여부 가드 — `getApp` throw 없이 확인). @returns {boolean} */
|
|
62
|
+
export function hasApp() {
|
|
63
|
+
return apps.size > 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 테스트 격리/재부팅용 — 레지스트리 비움(shutdown 에서도 호출). @returns {void} */
|
|
67
|
+
export function _resetApps() {
|
|
68
|
+
apps.clear()
|
|
69
|
+
}
|
package/src/core/boot.js
CHANGED
|
@@ -46,6 +46,9 @@ import { MegaShutdown } from '../lib/mega-shutdown.js'
|
|
|
46
46
|
import { buildLogger } from '../lib/mega-logger.js'
|
|
47
47
|
import { buildFromGlobalConfig, connectAll, get as getAdapter, entries as adapterEntries } from '../adapters/adapter-manager.js'
|
|
48
48
|
import { buildWorkers, startAll as startWorkers, contextProxy as workersContext } from './workers-manager.js'
|
|
49
|
+
import { createLockManager, setLockManager, attachLockApi } from './lock/index.js'
|
|
50
|
+
import { createBusManager, setBusManager, attachBusApi } from './bus/index.js'
|
|
51
|
+
import { setApp, _resetApps } from './app-registry.js'
|
|
49
52
|
import { MegaWsCluster } from './ws-cluster.js'
|
|
50
53
|
import { MegaWsRedisRoster } from './ws-roster.js'
|
|
51
54
|
import * as MegaMetrics from '../lib/mega-metrics.js'
|
|
@@ -95,6 +98,33 @@ function composeAspConfig(globalAsp, appAsp) {
|
|
|
95
98
|
return { ...appAsp, ...(masterSecret ? { masterSecret } : {}) }
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/**
|
|
102
|
+
* lock redis driver 가 빌릴 raw ioredis 를 cache 어댑터에서 빌린다(eval/duplicate 보유 검증). 글로벌·앱별 lock
|
|
103
|
+
* 스테이지가 공유한다. 없거나 raw redis 가 아니면 null(auto 면 폴백, 명시 redis 면 factory 가 fail-fast).
|
|
104
|
+
* @param {string | undefined} cacheKey - lock.cache(글로벌 services.caches 키). @param {any} [logger]
|
|
105
|
+
* @returns {any | null}
|
|
106
|
+
*/
|
|
107
|
+
function borrowLockRedis(cacheKey, logger) {
|
|
108
|
+
if (!cacheKey) return null
|
|
109
|
+
const native = /** @type {any} */ (getAdapter('cache', cacheKey))?.native
|
|
110
|
+
if (native && typeof native.eval === 'function' && typeof native.duplicate === 'function') return native
|
|
111
|
+
logger?.warn?.({ cache: cacheKey }, 'boot.lock: configured cache adapter has no raw ioredis (eval/duplicate) — redis lock driver unavailable')
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* bus nats driver 가 빌릴 raw NatsConnection 을 bus 어댑터에서 빌린다(publish/subscribe/request 보유 검증).
|
|
117
|
+
* @param {string | undefined} natsKey - bus.nats(글로벌 services.buses 키). @param {any} [logger]
|
|
118
|
+
* @returns {any | null}
|
|
119
|
+
*/
|
|
120
|
+
function borrowBusNc(natsKey, logger) {
|
|
121
|
+
if (!natsKey) return null
|
|
122
|
+
const native = /** @type {any} */ (getAdapter('bus', natsKey))?.native
|
|
123
|
+
if (native && typeof native.publish === 'function' && typeof native.subscribe === 'function' && typeof native.request === 'function') return native
|
|
124
|
+
logger?.warn?.({ nats: natsKey }, 'boot.bus: configured bus adapter has no NATS native (publish/subscribe/request) — nats bus driver unavailable')
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
/**
|
|
99
129
|
* boot/CLI 컨텍스트 — `db/cache/bus/lock` 을 글로벌 키로 직접 조회하고, `workers` 는 `static name` 으로
|
|
100
130
|
* lookup 한다(앱 별명 변환 없음 — 둘 다 글로벌 자원). worker/scheduler CLI 도 같은 형태를 재사용한다
|
|
@@ -222,6 +252,50 @@ const PREPARE_STEPS = [
|
|
|
222
252
|
st.logger?.debug?.('boot.adapters connected')
|
|
223
253
|
},
|
|
224
254
|
},
|
|
255
|
+
{
|
|
256
|
+
name: 'lock',
|
|
257
|
+
needs: ['global', 'host'],
|
|
258
|
+
run: (st) => {
|
|
259
|
+
// 분산 락 manager 자동배선 (ADR-226) — `ctx.lock.with/.acquire/...` 사용자 API. driver 자동 폴백:
|
|
260
|
+
// redis cache 어댑터(config `lock.cache`) → redis(진짜 분산) / cluster 워커 → cluster(단일 노드) /
|
|
261
|
+
// 둘 다 없음 → memory(분산 미보장, 경고). 명시 driver 의 전제 부재는 factory 가 fail-fast(P7).
|
|
262
|
+
// adapters 스테이지 이후라 cache 의 raw ioredis(`.native`)가 connect 된 상태다. setLockManager 싱글톤을
|
|
263
|
+
// ctx-builder 가 읽어 모든 ctx(HTTP·worker·scheduler)의 lock accessor 에 API 를 얹는다.
|
|
264
|
+
// 글로벌 lock manager — 앱별 lock 설정이 없는 앱의 fallback + worker/scheduler boot ctx(MegaApp 없음)용.
|
|
265
|
+
// 앱별 분리(ADR-229)는 apps 스테이지가 app.config.lock 이 있을 때만 별도 manager 를 만들어 덮어쓴다.
|
|
266
|
+
const lockCfg = /** @type {any} */ (st.global).lock ?? {}
|
|
267
|
+
const manager = createLockManager(lockCfg, { logger: st.logger, redisClient: borrowLockRedis(lockCfg.cache, st.logger), isClusterWorker: nodeCluster.isWorker })
|
|
268
|
+
setLockManager(manager)
|
|
269
|
+
// 'app' stage — 어댑터 disconnect 보다 먼저 정리(redis 구독 연결 quit·타이머 해제가 cache 끊기기 전에).
|
|
270
|
+
MegaShutdown.register('mega-lock', async () => {
|
|
271
|
+
await manager.close()
|
|
272
|
+
setLockManager(null)
|
|
273
|
+
})
|
|
274
|
+
st.logger?.debug?.({ driver: manager.driverName, configured: lockCfg.driver ?? 'auto' }, 'boot.lock manager ready (ADR-226)')
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'bus',
|
|
279
|
+
needs: ['global', 'host'],
|
|
280
|
+
run: (st) => {
|
|
281
|
+
// 메시지 버스 manager 자동배선 (ADR-227) — `ctx.bus.emit/.on/.request/...` 사용자 API. driver 자동 폴백:
|
|
282
|
+
// NATS bus 어댑터(config `bus.nats`) → nats(진짜 분산) / cluster 워커 → cluster(단일 노드) /
|
|
283
|
+
// 둘 다 없음 → memory(분산 미보장, 경고). 명시 driver 의 전제 부재는 factory 가 fail-fast(P7).
|
|
284
|
+
// adapters 스테이지 이후라 NATS 어댑터의 raw NatsConnection(`.native`)이 connect 된 상태다. 새 연결을 만들지
|
|
285
|
+
// 않고 빌려 쓰므로 클러스터 broadcast(ADR-176)·잡 큐(ADR-119)와 같은 nc 를 공유한다.
|
|
286
|
+
// 글로벌 bus manager — 앱별 bus 설정이 없는 앱의 fallback + worker/scheduler boot ctx 용. 앱별 분리(ADR-229)는
|
|
287
|
+
// apps 스테이지가 app.config.bus 가 있을 때만 별도 manager 로 덮어쓴다.
|
|
288
|
+
const busCfg = /** @type {any} */ (st.global).bus ?? {}
|
|
289
|
+
const manager = createBusManager(busCfg, { logger: st.logger, nc: borrowBusNc(busCfg.nats, st.logger), isClusterWorker: nodeCluster.isWorker })
|
|
290
|
+
setBusManager(manager)
|
|
291
|
+
// 'app' stage — 어댑터 disconnect 보다 먼저 정리(영속 consumer·구독이 nc 끊기기 전에).
|
|
292
|
+
MegaShutdown.register('mega-bus', async () => {
|
|
293
|
+
await manager.close()
|
|
294
|
+
setBusManager(null)
|
|
295
|
+
})
|
|
296
|
+
st.logger?.debug?.({ driver: manager.driverName, configured: busCfg.driver ?? 'auto' }, 'boot.bus manager ready (ADR-227)')
|
|
297
|
+
},
|
|
298
|
+
},
|
|
225
299
|
{
|
|
226
300
|
name: 'health-auto-checks',
|
|
227
301
|
needs: ['global'],
|
|
@@ -492,8 +566,33 @@ const BOOT_STEPS = [
|
|
|
492
566
|
st.logger?.debug?.({ app: name, filesLoaded }, 'boot.routes loaded')
|
|
493
567
|
st.server.mount(app)
|
|
494
568
|
megaApps.push(app)
|
|
569
|
+
// ctx 없는 영역의 getApp() 접근점에 등록(ADR-228).
|
|
570
|
+
setApp(app)
|
|
571
|
+
|
|
572
|
+
// 앱별 lock/bus 분리(ADR-229) — app.config.lock/bus 가 있으면 그 앱 전용 manager 를 만들어 이 앱의
|
|
573
|
+
// adapterAccessors 에 덮어쓴다(ctx.<app>·getApp(name) 둘 다 앱 manager 를 본다). 미지정 앱은 생성자가
|
|
574
|
+
// 이미 붙인 글로벌 manager 를 그대로 쓴다(중복 연결 없음). 앱 설정은 글로벌과 shallow merge(앱 키 우선).
|
|
575
|
+
const isWorker = nodeCluster.isWorker
|
|
576
|
+
const appLockCfg = /** @type {any} */ (config).lock
|
|
577
|
+
if (appLockCfg) {
|
|
578
|
+
const merged = { .../** @type {any} */ (st.global).lock, ...appLockCfg }
|
|
579
|
+
const lockMgr = createLockManager(merged, { logger: st.logger, redisClient: borrowLockRedis(merged.cache, st.logger), isClusterWorker: isWorker })
|
|
580
|
+
attachLockApi(/** @type {any} */ (app.adapterAccessors.lock), lockMgr)
|
|
581
|
+
MegaShutdown.register(`mega-lock:${name}`, async () => lockMgr.close())
|
|
582
|
+
st.logger?.debug?.({ app: name, driver: lockMgr.driverName }, 'boot.lock app-scoped manager (ADR-229)')
|
|
583
|
+
}
|
|
584
|
+
const appBusCfg = /** @type {any} */ (config).bus
|
|
585
|
+
if (appBusCfg) {
|
|
586
|
+
const merged = { .../** @type {any} */ (st.global).bus, ...appBusCfg }
|
|
587
|
+
const busMgr = createBusManager(merged, { logger: st.logger, nc: borrowBusNc(merged.nats, st.logger), isClusterWorker: isWorker })
|
|
588
|
+
attachBusApi(/** @type {any} */ (app.adapterAccessors.bus), busMgr)
|
|
589
|
+
MegaShutdown.register(`mega-bus:${name}`, async () => busMgr.close())
|
|
590
|
+
st.logger?.debug?.({ app: name, driver: busMgr.driverName }, 'boot.bus app-scoped manager (ADR-229)')
|
|
591
|
+
}
|
|
495
592
|
}
|
|
496
593
|
st.megaApps = megaApps
|
|
594
|
+
// 재부팅/테스트 격리 — 'app' stage 에서 레지스트리 비움(어댑터 disconnect 와 같은 시점, getApp 가 stale 앱 X).
|
|
595
|
+
MegaShutdown.register('mega-app-registry', () => _resetApps())
|
|
497
596
|
},
|
|
498
597
|
},
|
|
499
598
|
{
|