mega-framework 0.1.10 → 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/lock-controller.js +117 -0
- package/sample/crud/apps/main/locales/server/en.json +31 -1
- package/sample/crud/apps/main/locales/server/ko.json +31 -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 +21 -14
- package/sample/crud/apps/main/views/bus/index.ejs +80 -0
- 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 +3 -1
- 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 +9 -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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* RedisLockDriver — redis 기반 **진짜 분산** 락 driver (ADR-226).
|
|
4
|
+
*
|
|
5
|
+
* 멀티 프로세스·멀티 노드에서 상호배제를 보장하는 유일한 driver. 단일 redis 노드에 대한 Redlock 단일-인스턴스
|
|
6
|
+
* 알고리즘(`SET key token NX PX ttl` + 소유 토큰 + Lua 원자 해제)을 raw ioredis 클라이언트로 직접 구현한다.
|
|
7
|
+
* 기존 {@link import('../../adapters/redlock-adapter.js')}(redlock 라이브러리, `ctx.lock(alias)`)와 별개 —
|
|
8
|
+
* 이쪽은 fence/FIFO/watchdog 를 위해 명령을 직접 제어해야 해서 raw 클라이언트를 쓴다(라이브러리 미경유).
|
|
9
|
+
* redis 클라이언트는 cache 어댑터가 소유한 것을 **빌려** 쓴다(연결 생애주기는 cache 가 관리 — close 에서
|
|
10
|
+
* quit 하지 않음). 단, Pub/Sub 구독은 별도 연결이 필요하므로 `duplicate()` 한 구독 연결만 이 driver 가 소유한다.
|
|
11
|
+
*
|
|
12
|
+
* # 키 네임스페이스(`mega:lock:` 접두)
|
|
13
|
+
* - `…:v:{key}` 락 값(member=token, PX ttl) · `…:f:{key}` fence 카운터(INCR) ·
|
|
14
|
+
* `…:w:{key}` FIFO 대기열 zset · `…:c:{key}` 해제 알림 Pub/Sub 채널.
|
|
15
|
+
*
|
|
16
|
+
* # 대기(waitMs) — Pub/Sub 깨우기
|
|
17
|
+
* 경합 시 폴링 대신 해제 채널을 구독해 깨어난다. 구독은 `PSUBSCRIBE …:c:*` 하나로 모든 키를 받고
|
|
18
|
+
* 채널에서 key 를 역산해 해당 대기자만 깨운다. 깨우기 누락(메시지 유실) 대비로 짧은 안전 폴링도 병행한다.
|
|
19
|
+
*
|
|
20
|
+
* # fence / FIFO / watchdog
|
|
21
|
+
* - fence: 획득 직후 `INCR …:f:{key}` — 단조 증가 토큰(외부 시스템의 stale-writer 차단). 카운터는 영속.
|
|
22
|
+
* - FIFO: {@link import('./fifo-waitlist.js').FifoWaitlist}(zset) — head 가 됐을 때만 SET 시도.
|
|
23
|
+
* - watchdog(extendable): manager 가 `extend` 를 주기 호출(Lua PEXPIRE) — driver 는 1회 연장만 책임.
|
|
24
|
+
*
|
|
25
|
+
* driver 계약은 {@link import('./contract.js').LockDriver} 를 따른다(typedef — 런타임 implements 아님).
|
|
26
|
+
* @module core/lock/redis-lock
|
|
27
|
+
*/
|
|
28
|
+
import { generateToken } from './contract.js'
|
|
29
|
+
import { FifoWaitlist } from './fifo-waitlist.js'
|
|
30
|
+
|
|
31
|
+
/** 키 접두 — 다른 redis 사용처(cache 등)와 충돌 방지. */
|
|
32
|
+
const PREFIX = 'mega:lock'
|
|
33
|
+
/** Pub/Sub 깨우기를 놓쳐도 진행하도록 하는 안전 폴링 간격(ms). */
|
|
34
|
+
const SAFETY_POLL_MS = 50
|
|
35
|
+
|
|
36
|
+
/** 소유 토큰일 때만 삭제 + 해제 알림(원자). KEYS=[lock, channel] ARGV=[token]. @type {string} */
|
|
37
|
+
const RELEASE_LUA = `
|
|
38
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
39
|
+
redis.call('del', KEYS[1])
|
|
40
|
+
redis.call('publish', KEYS[2], '1')
|
|
41
|
+
return 1
|
|
42
|
+
else
|
|
43
|
+
return 0
|
|
44
|
+
end`
|
|
45
|
+
|
|
46
|
+
/** 소유 토큰일 때만 PEXPIRE 연장(원자). KEYS=[lock] ARGV=[token, ttl]. @type {string} */
|
|
47
|
+
const EXTEND_LUA = `
|
|
48
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
49
|
+
return redis.call('pexpire', KEYS[1], ARGV[2])
|
|
50
|
+
else
|
|
51
|
+
return 0
|
|
52
|
+
end`
|
|
53
|
+
|
|
54
|
+
export class RedisLockDriver {
|
|
55
|
+
/** @type {any} ioredis 호환 명령 클라이언트(cache 어댑터 소유 — 빌림). */ #client
|
|
56
|
+
/** @type {any} duplicate() 한 구독 연결(이 driver 소유 — close 에서 quit). */ #sub = null
|
|
57
|
+
/** @type {(() => any) | null} 구독 연결 생성자(테스트 주입). */ #createSub
|
|
58
|
+
/** @type {FifoWaitlist} */ #waitlist
|
|
59
|
+
/** @type {Map<string, Set<() => void>>} key → 해제 알림 대기 콜백. */ #wakers = new Map()
|
|
60
|
+
/** @type {boolean} PSUBSCRIBE 완료 여부(최초 대기 때 1회). */ #subscribed = false
|
|
61
|
+
/** @type {Promise<void> | null} 구독 진행 중 락(중복 구독 방지). */ #subscribing = null
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {{ client: any, createSubscriber?: () => any }} opts
|
|
65
|
+
* - `client` ioredis 호환 클라이언트(`set`/`eval`/`incr`/`zadd`… 보유). cache 어댑터의 `.native`.
|
|
66
|
+
* - `createSubscriber` Pub/Sub 구독 연결 생성자(기본 `client.duplicate()`). 테스트 주입용.
|
|
67
|
+
*/
|
|
68
|
+
constructor({ client, createSubscriber }) {
|
|
69
|
+
this.#client = client
|
|
70
|
+
this.#createSub = createSubscriber ?? (() => client.duplicate())
|
|
71
|
+
this.#waitlist = new FifoWaitlist(client, { zkey: (key) => `${PREFIX}:w:${key}`, seqKey: `${PREFIX}:seq` })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @type {'redis'} */
|
|
75
|
+
get name() {
|
|
76
|
+
return 'redis'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {string} key @returns {string} 락 값 키. */
|
|
80
|
+
#lockKey(key) {
|
|
81
|
+
return `${PREFIX}:v:${key}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @param {string} key @returns {string} 해제 알림 채널. */
|
|
85
|
+
#chan(key) {
|
|
86
|
+
return `${PREFIX}:c:${key}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 락 획득 — fifo 면 대기열 head 일 때만, 아니면 즉시 SET 후 경합 시 Pub/Sub 대기.
|
|
91
|
+
* @param {string} key @param {import('./contract.js').NormalizedLockOpts} opts
|
|
92
|
+
* @returns {Promise<import('./contract.js').DriverLock | null>}
|
|
93
|
+
*/
|
|
94
|
+
async acquire(key, { ttl, waitMs, fifo, fence }) {
|
|
95
|
+
const token = generateToken()
|
|
96
|
+
const acquired = fifo
|
|
97
|
+
? await this.#acquireFifo(key, token, ttl, waitMs)
|
|
98
|
+
: await this.#acquirePlain(key, token, ttl, waitMs)
|
|
99
|
+
if (!acquired) return null
|
|
100
|
+
if (!fence) return { token }
|
|
101
|
+
// fence: 락을 쥔 상태에서만 INCR — 단조 증가 보장(외부 시스템 stale-writer 차단용).
|
|
102
|
+
const fenceVal = Number(await this.#client.incr(`${PREFIX}:f:${key}`))
|
|
103
|
+
return { token, fence: fenceVal }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 비-FIFO 획득 — SET NX 즉시 시도, 실패 시 waitMs 안에서 해제 알림 받으며 재시도.
|
|
108
|
+
* @param {string} key @param {string} token @param {number} ttl @param {number} waitMs @returns {Promise<boolean>}
|
|
109
|
+
*/
|
|
110
|
+
async #acquirePlain(key, token, ttl, waitMs) {
|
|
111
|
+
if (await this.#trySet(key, token, ttl)) return true
|
|
112
|
+
if (waitMs === 0) return false
|
|
113
|
+
const deadline = Date.now() + waitMs
|
|
114
|
+
while (Date.now() < deadline) {
|
|
115
|
+
await this.#waitForRelease(key, Math.min(SAFETY_POLL_MS, deadline - Date.now()))
|
|
116
|
+
if (await this.#trySet(key, token, ttl)) return true
|
|
117
|
+
}
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* FIFO 획득 — 대기열 합류 후, 내가 head 이고 락이 비었을 때만 SET. waitMs 초과 시 이탈하고 실패.
|
|
123
|
+
* @param {string} key @param {string} token @param {number} ttl @param {number} waitMs @returns {Promise<boolean>}
|
|
124
|
+
*/
|
|
125
|
+
async #acquireFifo(key, token, ttl, waitMs) {
|
|
126
|
+
const deadline = Date.now() + waitMs
|
|
127
|
+
// member 에 deadline 을 실어 합류 — 죽은 대기자를 head reaping 으로 청소할 수 있게(fifo-waitlist).
|
|
128
|
+
// waitMs=0(tryAcquire)이라도 head 검사 위해 잠깐 합류했다 finally 에서 바로 이탈.
|
|
129
|
+
const member = await this.#waitlist.join(key, token, deadline)
|
|
130
|
+
try {
|
|
131
|
+
for (;;) {
|
|
132
|
+
if (await this.#waitlist.isHead(key, token, Date.now())) {
|
|
133
|
+
if (await this.#trySet(key, token, ttl)) return true
|
|
134
|
+
}
|
|
135
|
+
if (Date.now() >= deadline) return false
|
|
136
|
+
await this.#waitForRelease(key, Math.min(SAFETY_POLL_MS, deadline - Date.now()))
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
await this.#waitlist.leave(key, member)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** SET NX PX 1회 시도. @param {string} key @param {string} token @param {number} ttl @returns {Promise<boolean>} */
|
|
144
|
+
async #trySet(key, token, ttl) {
|
|
145
|
+
const res = await this.#client.set(this.#lockKey(key), token, 'PX', ttl, 'NX')
|
|
146
|
+
return res === 'OK'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 해제 알림(Pub/Sub)을 timeoutMs 안에서 기다린다 — 알림 오면 즉시, 아니면 timeout 후 resolve(안전 폴링).
|
|
151
|
+
* @param {string} key @param {number} timeoutMs @returns {Promise<void>}
|
|
152
|
+
*/
|
|
153
|
+
async #waitForRelease(key, timeoutMs) {
|
|
154
|
+
await this.#ensureSubscribed()
|
|
155
|
+
await new Promise((resolve) => {
|
|
156
|
+
let done = false
|
|
157
|
+
const finish = () => {
|
|
158
|
+
if (done) return
|
|
159
|
+
done = true
|
|
160
|
+
clearTimeout(timer)
|
|
161
|
+
const set = this.#wakers.get(key)
|
|
162
|
+
if (set) {
|
|
163
|
+
set.delete(waker)
|
|
164
|
+
if (set.size === 0) this.#wakers.delete(key)
|
|
165
|
+
}
|
|
166
|
+
resolve(undefined)
|
|
167
|
+
}
|
|
168
|
+
const waker = () => finish()
|
|
169
|
+
const timer = setTimeout(finish, Math.max(1, timeoutMs))
|
|
170
|
+
timer.unref?.()
|
|
171
|
+
const set = this.#wakers.get(key) ?? new Set()
|
|
172
|
+
set.add(waker)
|
|
173
|
+
this.#wakers.set(key, set)
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 최초 대기 시 구독 연결 1회 PSUBSCRIBE. 동시 진입은 #subscribing 으로 직렬화. @returns {Promise<void>} */
|
|
178
|
+
async #ensureSubscribed() {
|
|
179
|
+
if (this.#subscribed) return
|
|
180
|
+
if (this.#subscribing) return this.#subscribing
|
|
181
|
+
this.#subscribing = (async () => {
|
|
182
|
+
const sub = this.#createSub()
|
|
183
|
+
sub.on('pmessage', (/** @type {string} */ _pattern, /** @type {string} */ channel) => {
|
|
184
|
+
// 채널 …:c:{key} 에서 key 역산 → 해당 대기자 전부 깨움(head 가 재시도).
|
|
185
|
+
const prefix = `${PREFIX}:c:`
|
|
186
|
+
if (!channel.startsWith(prefix)) return
|
|
187
|
+
const key = channel.slice(prefix.length)
|
|
188
|
+
const set = this.#wakers.get(key)
|
|
189
|
+
if (!set) return
|
|
190
|
+
for (const waker of [...set]) waker()
|
|
191
|
+
})
|
|
192
|
+
await sub.psubscribe(`${PREFIX}:c:*`)
|
|
193
|
+
this.#sub = sub
|
|
194
|
+
this.#subscribed = true
|
|
195
|
+
})()
|
|
196
|
+
try {
|
|
197
|
+
await this.#subscribing
|
|
198
|
+
} finally {
|
|
199
|
+
this.#subscribing = null
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 해제 — 소유 토큰일 때만 DEL + 해제 알림(Lua 원자).
|
|
205
|
+
* @param {string} key @param {string} token @returns {Promise<boolean>}
|
|
206
|
+
*/
|
|
207
|
+
async release(key, token) {
|
|
208
|
+
const res = await this.#client.eval(RELEASE_LUA, 2, this.#lockKey(key), this.#chan(key), token)
|
|
209
|
+
return Number(res) === 1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 연장 — 소유 토큰일 때만 PEXPIRE(Lua 원자).
|
|
214
|
+
* @param {string} key @param {string} token @param {number} ttl @returns {Promise<boolean>}
|
|
215
|
+
*/
|
|
216
|
+
async extend(key, token, ttl) {
|
|
217
|
+
const res = await this.#client.eval(EXTEND_LUA, 1, this.#lockKey(key), token, String(ttl))
|
|
218
|
+
return Number(res) === 1
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 강제 해제(관리용, 토큰 무관) — DEL + 해제 알림(대기자 깨움). @param {string} key @returns {Promise<void>} */
|
|
222
|
+
async forceRelease(key) {
|
|
223
|
+
await this.#client.del(this.#lockKey(key))
|
|
224
|
+
await this.#client.publish(this.#chan(key), '1')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 통계(best-effort) — `…:v:*`/`…:w:*` 키를 SCAN 으로 센다. O(키 수) 라 운영 관측용(고빈도 호출 비권장).
|
|
229
|
+
* @returns {Promise<{ driver: string, active: number, waiting: number }>}
|
|
230
|
+
*/
|
|
231
|
+
async stats() {
|
|
232
|
+
const active = await this.#scanCount(`${PREFIX}:v:*`)
|
|
233
|
+
let waiting = 0
|
|
234
|
+
for (const wkey of await this.#scanKeys(`${PREFIX}:w:*`)) {
|
|
235
|
+
waiting += Number(await this.#client.zcard(wkey))
|
|
236
|
+
}
|
|
237
|
+
return { driver: 'redis', active, waiting }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** MATCH 패턴 키 수를 SCAN 으로 센다. @param {string} match @returns {Promise<number>} */
|
|
241
|
+
async #scanCount(match) {
|
|
242
|
+
return (await this.#scanKeys(match)).length
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** MATCH 패턴 키를 SCAN 으로 모은다(커서 순회). @param {string} match @returns {Promise<string[]>} */
|
|
246
|
+
async #scanKeys(match) {
|
|
247
|
+
/** @type {string[]} */
|
|
248
|
+
const keys = []
|
|
249
|
+
let cursor = '0'
|
|
250
|
+
do {
|
|
251
|
+
const [next, batch] = await this.#client.scan(cursor, 'MATCH', match, 'COUNT', 100)
|
|
252
|
+
cursor = next
|
|
253
|
+
keys.push(...batch)
|
|
254
|
+
} while (cursor !== '0')
|
|
255
|
+
return keys
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 정리 — 구독 연결만 quit(명령 클라이언트는 cache 소유라 건드리지 않음). @returns {Promise<void>} */
|
|
259
|
+
async close() {
|
|
260
|
+
for (const set of this.#wakers.values()) for (const waker of [...set]) waker()
|
|
261
|
+
this.#wakers.clear()
|
|
262
|
+
if (this.#sub) {
|
|
263
|
+
try {
|
|
264
|
+
await this.#sub.punsubscribe(`${PREFIX}:c:*`)
|
|
265
|
+
await this.#sub.quit()
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// graceful quit 실패 → 강제 disconnect 로 소켓이라도 끊고, 에러는 wrap 해 상위로(매니저가 로깅).
|
|
268
|
+
if (typeof this.#sub.disconnect === 'function') this.#sub.disconnect()
|
|
269
|
+
throw new Error(`redis lock subscriber close failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e })
|
|
270
|
+
} finally {
|
|
271
|
+
this.#sub = null
|
|
272
|
+
this.#subscribed = false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/core/mega-app.js
CHANGED
|
@@ -620,6 +620,35 @@ export class MegaApp {
|
|
|
620
620
|
return this._adapterAccessors
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
// ── ctx 없는 영역(getApp())용 표면 (ADR-228) ─────────────────────────────────
|
|
624
|
+
// 요청 ctx 가 없는 곳(백그라운드 타이머·외부 콜백·테스트)에서 ctx.lock/ctx.bus 와 **동일 표면**으로 쓰도록
|
|
625
|
+
// process-level 자원만 노출한다. 요청-스코프(req/user/session/요청 locale t)는 ctx 전용이라 여기 없다.
|
|
626
|
+
|
|
627
|
+
/** 분산 락 사용자 API(ADR-226) — `ctx.lock` 과 같은 process manager(콜러블 + .with/.acquire/...). @returns {any} */
|
|
628
|
+
get lock() {
|
|
629
|
+
return this._adapterAccessors.lock
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** 메시지 버스 사용자 API(ADR-227) — `ctx.bus` 와 같은 process manager(콜러블 + .emit/.on/.request/...). @returns {any} */
|
|
633
|
+
get bus() {
|
|
634
|
+
return this._adapterAccessors.bus
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** 앱 별명 → 공유 DB 어댑터(ADR-102). 미선언 별명은 throw. @param {string} alias @returns {import('../adapters/mega-adapter.js').MegaAdapter} */
|
|
638
|
+
db(alias) {
|
|
639
|
+
return this._adapterAccessors.db(alias)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** 앱 별명 → 공유 캐시 어댑터(ADR-102). 미선언 별명은 throw. @param {string} alias @returns {import('../adapters/mega-adapter.js').MegaAdapter} */
|
|
643
|
+
cache(alias) {
|
|
644
|
+
return this._adapterAccessors.cache(alias)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** 앱 로거(pino, ADR-141) — `ctx.log` 의 요청 바인딩 없는 베이스 로거. @returns {any} */
|
|
648
|
+
get log() {
|
|
649
|
+
return this.fastify.log
|
|
650
|
+
}
|
|
651
|
+
|
|
623
652
|
/**
|
|
624
653
|
* 이 앱의 서비스 레지스트리(name → 클래스, ADR-148). 요청 ctx 빌더가 `ctx.services.<name>` lazy DI 의
|
|
625
654
|
* 클래스 lookup 출처로 읽는다. 부팅 orchestrator 가 `setServiceRegistry` 로 채운다.
|
|
@@ -330,7 +330,7 @@ async function generateInner(projectRoot, opts, lock) {
|
|
|
330
330
|
const allCandidates = diffs.flatMap((d) => d.candidates)
|
|
331
331
|
if (allCandidates.length > 0 && !check) {
|
|
332
332
|
/** @type {Record<string, Record<string, string>> | null} */
|
|
333
|
-
let mapping
|
|
333
|
+
let mapping
|
|
334
334
|
if (renames !== undefined) {
|
|
335
335
|
mapping = parseRenamesSpec(renames, allCandidates)
|
|
336
336
|
} else {
|
|
@@ -127,7 +127,7 @@ export function acquireGenerateLock(projectRoot) {
|
|
|
127
127
|
if (/** @type {any} */ (cleanupErr).code !== 'ENOENT') throw cleanupErr
|
|
128
128
|
}
|
|
129
129
|
if (/** @type {any} */ (err).code !== 'EEXIST') throw err
|
|
130
|
-
let holder
|
|
130
|
+
let holder
|
|
131
131
|
try {
|
|
132
132
|
holder = readFileSync(file, 'utf8').trim()
|
|
133
133
|
} catch {
|
|
@@ -12,6 +12,8 @@ export const GLOBAL_ONLY_KEYS = Object.freeze([
|
|
|
12
12
|
'server', // port, cluster, sessionSecret
|
|
13
13
|
'wsHub', // mega ws-hub 명령용 (ADR-068)
|
|
14
14
|
'wsCluster', // NATS 기반 WS 클러스터 broadcast/roster 자동배선 (ADR-176)
|
|
15
|
+
'lock', // 분산 락 driver 자동폴백 + 기본 옵션 — ctx.lock.with/.acquire (ADR-226)
|
|
16
|
+
'bus', // 메시지 버스 driver 자동폴백 + 기본 옵션 — ctx.bus.emit/.on/.request (ADR-227)
|
|
15
17
|
'logger', // 전역 로거 sinks
|
|
16
18
|
'apps', // 활성 앱 whitelist (ADR-066)
|
|
17
19
|
'asp', // masterSecret 등 시크릿
|
|
@@ -50,5 +52,12 @@ export const APP_ONLY_KEYS = Object.freeze([
|
|
|
50
52
|
/** Shared-Reference 키 — 전역 services 의 키만 참조 가능 */
|
|
51
53
|
export const SHARED_REFERENCE_KEYS = Object.freeze(['databases', 'caches', 'buses'])
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Dual-scope 키 — 글로벌(mega.config.js)과 앱(app.config.js) **양쪽**에 정의 가능 (ADR-229).
|
|
57
|
+
* 글로벌은 fallback, 앱별 정의가 있으면 그 앱은 자기 manager 를 쓴다(앱별 lock/bus backend 분리). GLOBAL_ONLY_KEYS
|
|
58
|
+
* 에도 그대로 남겨(글로벌 정의 허용) 두고, validateAppConfig 가 이 목록을 wrong_scope 예외로 둔다.
|
|
59
|
+
*/
|
|
60
|
+
export const DUAL_SCOPE_KEYS = Object.freeze(['lock', 'bus'])
|
|
61
|
+
|
|
53
62
|
export const ALL_GLOBAL_KEYS = Object.freeze(GLOBAL_ONLY_KEYS)
|
|
54
63
|
export const ALL_APP_KEYS = Object.freeze(APP_ONLY_KEYS)
|
|
@@ -65,7 +65,7 @@ const rule = {
|
|
|
65
65
|
},
|
|
66
66
|
|
|
67
67
|
create(context) {
|
|
68
|
-
const filename = context.filename ??
|
|
68
|
+
const filename = context.filename ?? ''
|
|
69
69
|
if (!isControllerOrRouteFile(filename)) return {}
|
|
70
70
|
|
|
71
71
|
/**
|
|
@@ -77,7 +77,7 @@ const rule = {
|
|
|
77
77
|
context.report({
|
|
78
78
|
node,
|
|
79
79
|
messageId: 'direct',
|
|
80
|
-
data: { source: sourceValue },
|
|
80
|
+
data: { source: String(sourceValue) },
|
|
81
81
|
})
|
|
82
82
|
}
|
|
83
83
|
}
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export { MegaApp, MegaServer, Router, loadAndValidateConfig, loadRoutes } from './core/index.js'
|
|
3
3
|
// 중앙 부팅 orchestrator + CLI (ADR-123)
|
|
4
4
|
export { bootApp, buildBootContext } from './core/index.js'
|
|
5
|
+
// ctx 없는 영역(백그라운드 타이머·외부 SDK 콜백·테스트)에서 booted 앱의 lock/bus/db/cache 접근 (ADR-228)
|
|
6
|
+
export { getApp, hasApp } from './core/index.js'
|
|
5
7
|
export { runCli, parseArgs, runWorkerHost, runSchedulerHost, dispatchPluginCommand, USAGE } from './cli/index.js'
|
|
6
8
|
export { MegaService } from './core/index.js'
|
|
7
9
|
export { MegaCluster } from './core/index.js'
|