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.
Files changed (87) hide show
  1. package/README.md +14 -4
  2. package/package.json +23 -21
  3. package/sample/crud/.env +10 -2
  4. package/sample/crud/.env.example +8 -0
  5. package/sample/crud/apps/main/controllers/bus-controller.js +122 -0
  6. package/sample/crud/apps/main/controllers/lock-controller.js +117 -0
  7. package/sample/crud/apps/main/locales/server/en.json +31 -1
  8. package/sample/crud/apps/main/locales/server/ko.json +31 -1
  9. package/sample/crud/apps/main/public/js/bus-demo.js +131 -0
  10. package/sample/crud/apps/main/public/js/lock-demo.js +122 -0
  11. package/sample/crud/apps/main/routes/bus.js +43 -0
  12. package/sample/crud/apps/main/routes/lock.js +35 -0
  13. package/sample/crud/apps/main/services/jobs-demo-service.js +21 -14
  14. package/sample/crud/apps/main/views/bus/index.ejs +80 -0
  15. package/sample/crud/apps/main/views/layouts/main.ejs +2 -0
  16. package/sample/crud/apps/main/views/lock/index.ejs +99 -0
  17. package/sample/crud/docs/guide/03-service-model-db.md +48 -0
  18. package/sample/crud/docs/guide/05-scheduler-job-worker.md +3 -1
  19. package/sample/crud/docs/guide/09-distributed-lock-and-bus.md +224 -0
  20. package/sample/crud/docs/guide/10-multi-app.md +74 -0
  21. package/sample/crud/mega.config.js +32 -0
  22. package/sample/crud/package.json +3 -2
  23. package/sample/multi/.env +16 -0
  24. package/sample/multi/.env.example +17 -0
  25. package/sample/multi/README.md +54 -0
  26. package/sample/multi/apps/admin/app.config.js +24 -0
  27. package/sample/multi/apps/admin/controllers/admin-controller.js +42 -0
  28. package/sample/multi/apps/admin/public/js/admin.js +31 -0
  29. package/sample/multi/apps/admin/routes/pages.js +11 -0
  30. package/sample/multi/apps/admin/views/index.ejs +33 -0
  31. package/sample/multi/apps/web/app.config.js +30 -0
  32. package/sample/multi/apps/web/controllers/web-controller.js +45 -0
  33. package/sample/multi/apps/web/public/js/web.js +24 -0
  34. package/sample/multi/apps/web/routes/pages.js +13 -0
  35. package/sample/multi/apps/web/views/index.ejs +51 -0
  36. package/sample/multi/mega.config.js +42 -0
  37. package/sample/multi/package.json +20 -0
  38. package/sample/simple/package.json +2 -2
  39. package/src/adapters/nats-adapter.js +39 -44
  40. package/src/adapters/nats-codec.js +38 -0
  41. package/src/cli/commands/scaffold.js +1 -0
  42. package/src/cli/index.js +9 -1
  43. package/src/core/app-registry.js +69 -0
  44. package/src/core/boot.js +99 -0
  45. package/src/core/bus/cluster-bus.js +190 -0
  46. package/src/core/bus/contract.js +123 -0
  47. package/src/core/bus/index.js +285 -0
  48. package/src/core/bus/memory-bus.js +103 -0
  49. package/src/core/bus/nats-bus.js +203 -0
  50. package/src/core/config-validator.js +118 -1
  51. package/src/core/ctx-builder.js +14 -1
  52. package/src/core/index.js +2 -0
  53. package/src/core/lock/cluster-lock.js +174 -0
  54. package/src/core/lock/contract.js +123 -0
  55. package/src/core/lock/fifo-waitlist.js +93 -0
  56. package/src/core/lock/index.js +292 -0
  57. package/src/core/lock/memory-lock.js +162 -0
  58. package/src/core/lock/redis-lock.js +276 -0
  59. package/src/core/mega-app.js +29 -0
  60. package/src/core/migration/generate.js +1 -1
  61. package/src/core/migration/journal.js +1 -1
  62. package/src/core/scope-registry.js +9 -0
  63. package/src/eslint-plugin/no-direct-model-import.js +2 -2
  64. package/src/index.js +2 -0
  65. package/src/lib/mega-job-queue.js +71 -47
  66. package/types/adapters/mega-adapter.d.ts +1 -1
  67. package/types/adapters/nats-adapter.d.ts +4 -4
  68. package/types/adapters/nats-codec.d.ts +13 -0
  69. package/types/adapters/redlock-adapter.d.ts +1 -1
  70. package/types/core/app-registry.d.ts +22 -0
  71. package/types/core/bus/cluster-bus.d.ts +45 -0
  72. package/types/core/bus/contract.d.ts +164 -0
  73. package/types/core/bus/index.d.ts +100 -0
  74. package/types/core/bus/memory-bus.d.ts +45 -0
  75. package/types/core/bus/nats-bus.d.ts +41 -0
  76. package/types/core/index.d.ts +1 -0
  77. package/types/core/lock/cluster-lock.d.ts +44 -0
  78. package/types/core/lock/contract.d.ts +181 -0
  79. package/types/core/lock/fifo-waitlist.d.ts +38 -0
  80. package/types/core/lock/index.d.ts +96 -0
  81. package/types/core/lock/memory-lock.d.ts +58 -0
  82. package/types/core/lock/redis-lock.d.ts +43 -0
  83. package/types/core/mega-app.d.ts +10 -0
  84. package/types/core/scope-registry.d.ts +6 -0
  85. package/types/index.d.ts +1 -1
  86. package/types/lib/mega-job-queue.d.ts +27 -4
  87. package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -0,0 +1,100 @@
1
+ /** 활성 버스 manager 설정(boot bus 스테이지). null 로 해제(셧다운/테스트). @param {BusManager | null} m @returns {void} */
2
+ export function setBusManager(m: BusManager | null): void;
3
+ /** 현재 활성 버스 manager(없으면 null). @returns {BusManager | null} */
4
+ export function getBusManager(): BusManager | null;
5
+ /**
6
+ * 메시지 버스 manager 를 만든다 — driver 자동 폴백 포함. boot 가 1회 호출해 `ctx.bus` 에 부착한다.
7
+ * @param {{ driver?: string, nats?: string, prefix?: string, defaultPersist?: boolean, requestTimeoutMs?: number }} [busConfig]
8
+ * @param {{ logger?: any, nc?: any, isClusterWorker?: boolean }} [deps] - `nc` boot 가 bus 어댑터에서 빌린 NatsConnection(없으면 null).
9
+ * @returns {BusManager}
10
+ */
11
+ export function createBusManager(busConfig?: {
12
+ driver?: string;
13
+ nats?: string;
14
+ prefix?: string;
15
+ defaultPersist?: boolean;
16
+ requestTimeoutMs?: number;
17
+ }, deps?: {
18
+ logger?: any;
19
+ nc?: any;
20
+ isClusterWorker?: boolean;
21
+ }): BusManager;
22
+ /**
23
+ * `ctx.bus` 콜러블에 사용자 API 메서드를 얹는다 — `ctx.bus(alias)`(기존)와 `ctx.bus.emit(...)`(신규) 공존.
24
+ * @param {Function & Record<string, any>} busAccessor - `(alias) => MegaBusAdapter` 콜러블(ctx-builder 산출).
25
+ * @param {BusManager} manager @returns {Function & Record<string, any>} 같은 accessor(체이닝).
26
+ */
27
+ export function attachBusApi(busAccessor: Function & Record<string, any>, manager: BusManager): Function & Record<string, any>;
28
+ /**
29
+ * 사용자 메시지 버스 API. driver 한 개를 감싸 emit/on/off/request/with 를 제공한다.
30
+ */
31
+ export class BusManager {
32
+ /**
33
+ * @param {import('./contract.js').BusDriver} driver
34
+ * @param {{ defaults: { prefix: string, defaultPersist: boolean, requestTimeoutMs: number }, logger?: any }} opts
35
+ */
36
+ constructor(driver: import("./contract.js").BusDriver, { defaults, logger }: {
37
+ defaults: {
38
+ prefix: string;
39
+ defaultPersist: boolean;
40
+ requestTimeoutMs: number;
41
+ };
42
+ logger?: any;
43
+ });
44
+ /** @returns {string} 활성 driver 이름('nats'|'cluster'|'memory'). */
45
+ get driverName(): string;
46
+ /**
47
+ * 메시지 발행(fire-and-forget fan-out).
48
+ * @param {string} subject - 구체 subject(wildcard 불가).
49
+ * @param {any} payload
50
+ * @param {import('./contract.js').EmitOpts} [opts] - meta / persist.
51
+ * @returns {Promise<void>}
52
+ */
53
+ emit(subject: string, payload: any, opts?: import("./contract.js").EmitOpts): Promise<void>;
54
+ /**
55
+ * 구독. 핸들러는 `(payload, meta)`. request 대상이면 핸들러 **반환값이 reply**(undefined 면 응답 안 함).
56
+ * @param {string} subject - wildcard(`*`/`>`) 가능.
57
+ * @param {import('./contract.js').BusHandler} handler
58
+ * @param {import('./contract.js').OnOpts} [opts] - persist / ordered.
59
+ * @returns {Promise<import('./contract.js').Subscription>}
60
+ */
61
+ on(subject: string, handler: import("./contract.js").BusHandler, opts?: import("./contract.js").OnOpts): Promise<import("./contract.js").Subscription>;
62
+ /**
63
+ * 구독 해제 — 같은 (subject, handler) 로 등록된 구독을 푼다.
64
+ * @param {string} subject @param {import('./contract.js').BusHandler} handler @returns {Promise<void>}
65
+ */
66
+ off(subject: string, handler: import("./contract.js").BusHandler): Promise<void>;
67
+ /**
68
+ * req/reply — 첫 응답 payload 반환. 응답자 없으면 `bus.no_responders`, 시한 초과면 `bus.request_timeout`.
69
+ * @param {string} subject @param {any} payload
70
+ * @param {{ timeout?: number, meta?: Record<string, any> }} [opts]
71
+ * @returns {Promise<any>}
72
+ */
73
+ request(subject: string, payload: any, opts?: {
74
+ timeout?: number;
75
+ meta?: Record<string, any>;
76
+ }): Promise<any>;
77
+ /**
78
+ * meta 를 바인딩한 스코프 버스 — 이후 emit/request 가 이 meta 를 자동 병합한다(요청 단위 traceId 등).
79
+ * @param {Record<string, any>} meta @returns {{ emit: Function, on: Function, off: Function, request: Function, with: Function }}
80
+ */
81
+ with(meta: Record<string, any>): {
82
+ emit: Function;
83
+ on: Function;
84
+ off: Function;
85
+ request: Function;
86
+ with: Function;
87
+ };
88
+ /** @returns {Promise<{ driver: string, subscriptions: number }>} */
89
+ stats(): Promise<{
90
+ driver: string;
91
+ subscriptions: number;
92
+ }>;
93
+ /** 정리 — 등록 구독 해제 + driver 정리. @returns {Promise<void>} */
94
+ close(): Promise<void>;
95
+ #private;
96
+ }
97
+ import { MemoryBusDriver } from './memory-bus.js';
98
+ import { ClusterBusDriver } from './cluster-bus.js';
99
+ import { NatsBusDriver } from './nats-bus.js';
100
+ export { MemoryBusDriver, ClusterBusDriver, NatsBusDriver };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @typedef {Object} MemorySub - 구독 레코드.
3
+ * @property {string} pattern @property {(envelope: import('./contract.js').BusEnvelope, reply?: import('./contract.js').ReplyFn, subject?: string) => any} handler
4
+ * @property {boolean} ordered @property {Promise<void>} tail - ordered 직렬화용 꼬리 프라미스.
5
+ */
6
+ export class MemoryBusDriver {
7
+ /** @type {'memory'} */
8
+ get name(): "memory";
9
+ /** 매칭 구독에 fan-out. memory 는 persist 무시(opts 는 계약 정합용). @param {string} subject @param {import('./contract.js').BusEnvelope} envelope @param {{ persist?: boolean }} [_opts] @returns {Promise<void>} */
10
+ publish(subject: string, envelope: import("./contract.js").BusEnvelope, _opts?: {
11
+ persist?: boolean;
12
+ }): Promise<void>;
13
+ /** 구독 등록. @param {string} pattern @param {MemorySub['handler']} handler @param {{ ordered?: boolean }} opts @returns {Promise<import('./contract.js').Subscription>} */
14
+ subscribe(pattern: string, handler: MemorySub["handler"], { ordered }?: {
15
+ ordered?: boolean;
16
+ }): Promise<import("./contract.js").Subscription>;
17
+ /**
18
+ * req/reply — 매칭 핸들러에 replyFn 을 주고 첫 응답을 반환. 응답자 없으면 no_responders, 시한 초과면 timeout.
19
+ * @param {string} subject @param {import('./contract.js').BusEnvelope} envelope @param {{ timeout: number }} opts
20
+ * @returns {Promise<import('./contract.js').BusEnvelope>}
21
+ */
22
+ request(subject: string, envelope: import("./contract.js").BusEnvelope, { timeout }: {
23
+ timeout: number;
24
+ }): Promise<import("./contract.js").BusEnvelope>;
25
+ /** @returns {Promise<{ driver: string, subscriptions: number }>} */
26
+ stats(): Promise<{
27
+ driver: string;
28
+ subscriptions: number;
29
+ }>;
30
+ /** 정리 — 구독 비움. @returns {Promise<void>} */
31
+ close(): Promise<void>;
32
+ #private;
33
+ }
34
+ /**
35
+ * - 구독 레코드.
36
+ */
37
+ export type MemorySub = {
38
+ pattern: string;
39
+ handler: (envelope: import("./contract.js").BusEnvelope, reply?: import("./contract.js").ReplyFn, subject?: string) => any;
40
+ ordered: boolean;
41
+ /**
42
+ * - ordered 직렬화용 꼬리 프라미스.
43
+ */
44
+ tail: Promise<void>;
45
+ };
@@ -0,0 +1,41 @@
1
+ export class NatsBusDriver {
2
+ /** @param {{ nc: any }} opts - `nc` 빌린 NatsConnection(`busAdapter.native`). */
3
+ constructor({ nc }: {
4
+ nc: any;
5
+ });
6
+ /** @type {'nats'} */
7
+ get name(): "nats";
8
+ /**
9
+ * 발행 — persist 면 JetStream(영속), 아니면 core pub/sub(비영속).
10
+ * @param {string} subject @param {import('./contract.js').BusEnvelope} envelope @param {{ persist?: boolean }} opts
11
+ * @returns {Promise<void>}
12
+ */
13
+ publish(subject: string, envelope: import("./contract.js").BusEnvelope, { persist }?: {
14
+ persist?: boolean;
15
+ }): Promise<void>;
16
+ /**
17
+ * 구독 — persist 면 JetStream ephemeral consumer, 아니면 core 구독. core 는 wildcard·reply 를 native 지원.
18
+ * @param {string} pattern @param {(envelope: import('./contract.js').BusEnvelope, reply?: import('./contract.js').ReplyFn, subject?: string) => any} handler
19
+ * @param {{ persist?: boolean, ordered?: boolean }} opts @returns {Promise<import('./contract.js').Subscription>}
20
+ */
21
+ subscribe(pattern: string, handler: (envelope: import("./contract.js").BusEnvelope, reply?: import("./contract.js").ReplyFn, subject?: string) => any, { persist, ordered }?: {
22
+ persist?: boolean;
23
+ ordered?: boolean;
24
+ }): Promise<import("./contract.js").Subscription>;
25
+ /**
26
+ * req/reply — NATS native request. 첫 응답 envelope 반환. v3 에러 클래스로 timeout/no-responders 판별.
27
+ * @param {string} subject @param {import('./contract.js').BusEnvelope} envelope @param {{ timeout: number }} opts
28
+ * @returns {Promise<import('./contract.js').BusEnvelope>}
29
+ */
30
+ request(subject: string, envelope: import("./contract.js").BusEnvelope, { timeout }: {
31
+ timeout: number;
32
+ }): Promise<import("./contract.js").BusEnvelope>;
33
+ /** @returns {Promise<{ driver: string, subscriptions: number }>} 영속 구독 수(core 구독은 NATS 측 관리라 미집계). */
34
+ stats(): Promise<{
35
+ driver: string;
36
+ subscriptions: number;
37
+ }>;
38
+ /** 정리 — 영속 consumer 정지. nc 는 어댑터 소유라 닫지 않는다. @returns {Promise<void>} */
39
+ close(): Promise<void>;
40
+ #private;
41
+ }
@@ -11,6 +11,7 @@ export { MegaHubLink } from "./hub-link.js";
11
11
  export { createSessionCleanupSchedule } from "./session-cleanup-schedule.js";
12
12
  export { Router, MegaRouteError } from "./router.js";
13
13
  export { bootApp, buildBootContext } from "./boot.js";
14
+ export { getApp, hasApp } from "./app-registry.js";
14
15
  export { wrapEnvelope, errorEnvelope, synthesizeEnvelopeResponseSchema, ENVELOPE_MARK } from "./envelope.js";
15
16
  export { buildHttpPipeline, wrapPreHandler, composeTransform, composeAfter } from "./pipeline.js";
16
17
  export { ajvErrorToValidationError, MAX_VALIDATION_DETAILS } from "./ajv-mapper.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * master 프로세스에 락 responder 를 설치한다(멱등). 워커들의 IPC 요청을 한 MemoryLockDriver 로 직렬화한다.
3
+ * `mega start` 클러스터 분기에서 master 가 1회 호출한다.
4
+ *
5
+ * @param {import('node:cluster').Cluster} [cluster] - 테스트 주입용(기본 node:cluster).
6
+ * @returns {MemoryLockDriver | null} 설치된 master driver(테스트/관측용). 이미 설치됐으면 null.
7
+ */
8
+ export function installClusterLockMaster(cluster?: import("node:cluster").Cluster): MemoryLockDriver | null;
9
+ /** 테스트 격리용 — master responder 설치 플래그 초기화. @returns {void} */
10
+ export function _resetClusterLockMaster(): void;
11
+ /**
12
+ * 워커 측 cluster 락 driver — master 에 IPC 위임하는 얇은 RPC 클라이언트.
13
+ * driver 계약은 {@link import('./contract.js').LockDriver} 를 따른다(typedef — 런타임 implements 아님).
14
+ */
15
+ export class ClusterLockDriver {
16
+ /**
17
+ * @param {{ proc?: NodeJS.Process }} [opts] - `proc` 테스트 주입용(기본 전역 process). cluster 워커면
18
+ * `process.send` 로 master 에 닿는다.
19
+ * @throws {Error} proc.send 가 없으면(=cluster 워커가 아님) — factory 가 cluster 워커일 때만 생성한다.
20
+ */
21
+ constructor({ proc }?: {
22
+ proc?: NodeJS.Process;
23
+ });
24
+ /** @type {'cluster'} */
25
+ get name(): "cluster";
26
+ /** @param {string} key @param {import('./contract.js').NormalizedLockOpts} opts @returns {Promise<import('./contract.js').DriverLock | null>} */
27
+ acquire(key: string, opts: import("./contract.js").NormalizedLockOpts): Promise<import("./contract.js").DriverLock | null>;
28
+ /** @param {string} key @param {string} token @returns {Promise<boolean>} */
29
+ release(key: string, token: string): Promise<boolean>;
30
+ /** @param {string} key @param {string} token @param {number} ttl @returns {Promise<boolean>} */
31
+ extend(key: string, token: string, ttl: number): Promise<boolean>;
32
+ /** @param {string} key @returns {Promise<void>} */
33
+ forceRelease(key: string): Promise<void>;
34
+ /** @returns {Promise<{ driver: string, active: number, waiting: number }>} */
35
+ stats(): Promise<{
36
+ driver: string;
37
+ active: number;
38
+ waiting: number;
39
+ }>;
40
+ /** 정리 — IPC 리스너 제거 + 대기 요청에 null 응답(누수 방지). @returns {Promise<void>} */
41
+ close(): Promise<void>;
42
+ #private;
43
+ }
44
+ import { MemoryLockDriver } from './memory-lock.js';
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @typedef {Object} LockOpts - acquire/with/tryAcquire 옵션(정규화 전).
3
+ * @property {number} [ttl] - 락 보유 시간(ms, 양의 정수). 미지정 시 manager 디폴트.
4
+ * @property {number} [waitMs] - 획득 대기 상한(ms, 0 이상). 0 = 즉시(tryAcquire 와 동일). 미지정 시 디폴트.
5
+ * @property {boolean} [fifo] - true 면 대기자를 도착 순서대로 깨운다(기아 방지). driver 가 지원해야 함.
6
+ * @property {boolean} [fence] - true 면 단조 증가 fence 토큰을 함께 반환(외부 시스템의 stale-writer 차단용).
7
+ * @property {boolean} [extendable] - true 면 manager 가 watchdog 으로 ttl 절반마다 자동 연장(긴 임계구역).
8
+ */
9
+ /**
10
+ * @typedef {Object} NormalizedLockOpts - 검증·기본값 적용된 옵션.
11
+ * @property {number} ttl @property {number} waitMs @property {boolean} fifo @property {boolean} fence @property {boolean} extendable
12
+ */
13
+ /**
14
+ * @typedef {Object} DriverLock - driver.acquire 성공 결과(저수준 — manager 가 LockHandle 로 감싼다).
15
+ * @property {string} token - 이 보유를 식별하는 랜덤 토큰(release/extend 가 소유 검증에 쓴다).
16
+ * @property {number} [fence] - fence 토큰(단조 증가). `fence:true` 일 때만.
17
+ */
18
+ /**
19
+ * @typedef {Object} LockHandle - 사용자에게 반환되는 락 핸들(manager 구성).
20
+ * @property {string} key - 잠근 자원 키.
21
+ * @property {string} token - 보유 토큰.
22
+ * @property {number} [fence] - fence 토큰(`fence:true` 일 때만).
23
+ * @property {() => Promise<void>} release - 락 해제(idempotent — 이미 풀렸어도 안전). watchdog 도 멈춘다.
24
+ * @property {(ttl?: number) => Promise<boolean>} extend - TTL 연장. 아직 보유 중이면 true, 잃었으면 false.
25
+ */
26
+ /**
27
+ * @typedef {Object} LockDriver - driver 가 구현하는 저수준 인터페이스(manager 가 호출).
28
+ * @property {string} name - 'redis' | 'cluster' | 'memory'.
29
+ * @property {(key: string, opts: NormalizedLockOpts) => Promise<DriverLock | null>} acquire -
30
+ * waitMs 안에 획득하면 `{token, fence?}`, 못 잡으면 `null`(throw 아님 — 경합은 정상 결과).
31
+ * @property {(key: string, token: string) => Promise<boolean>} release - 토큰이 현재 보유자면 풀고 true,
32
+ * 아니면(이미 만료/타인 보유) false. 절대 throw 안 함.
33
+ * @property {(key: string, token: string, ttl: number) => Promise<boolean>} extend - 토큰이 보유자면 ttl
34
+ * 갱신 후 true, 아니면 false.
35
+ * @property {(key: string) => Promise<void>} forceRelease - 관리용 강제 해제(토큰 무관). 대기자 깨움 포함.
36
+ * @property {() => Promise<{ driver: string, active: number, waiting: number }>} stats - 현재 보유/대기 수.
37
+ * @property {() => Promise<void>} close - 타이머·구독·연결참조 정리(graceful shutdown).
38
+ */
39
+ /**
40
+ * 락 보유 토큰 생성 — 충돌 사실상 불가한 랜덤 24-hex. release/extend 가 소유권 검증에 쓴다.
41
+ * @returns {string}
42
+ */
43
+ export function generateToken(): string;
44
+ /**
45
+ * acquire/with/tryAcquire 옵션을 검증·정규화한다(fail-fast — 잘못된 입력은 락 시도 전에 throw).
46
+ * @param {{ ttl: number, waitMs: number, fifo: boolean }} defaults - manager 디폴트(config 유래).
47
+ * @param {LockOpts} [opts] - 사용자 옵션.
48
+ * @returns {NormalizedLockOpts}
49
+ * @throws {MegaValidationError} `lock.invalid_opts` - ttl/waitMs 가 정수 범위를 벗어나거나 boolean 아닌 플래그.
50
+ */
51
+ export function normalizeLockOpts(defaults: {
52
+ ttl: number;
53
+ waitMs: number;
54
+ fifo: boolean;
55
+ }, opts?: LockOpts): NormalizedLockOpts;
56
+ /**
57
+ * 락 key 검증 — 비어있지 않은 문자열. 저장소 키/redis subject 로 쓰이므로 제어문자·공백을 막는다.
58
+ * @param {string} key
59
+ * @returns {void}
60
+ * @throws {MegaValidationError} `lock.invalid_key`
61
+ */
62
+ export function assertLockKey(key: string): void;
63
+ /** 락 TTL 디폴트(ms) — 임계구역 보호 시간. config `lock.ttl` 로 조정. */
64
+ export const DEFAULT_TTL_MS: 5000;
65
+ /** 획득 대기 디폴트(ms) — 경합 시 이만큼 기다린다(0=즉시 실패). config `lock.waitMs` 로 조정. */
66
+ export const DEFAULT_WAIT_MS: 1000;
67
+ /** watchdog 자동 연장 주기 = ttl × 이 비율. 만료 전에 넉넉히 갱신(절반). */
68
+ export const WATCHDOG_RATIO: 0.5;
69
+ /**
70
+ * - acquire/with/tryAcquire 옵션(정규화 전).
71
+ */
72
+ export type LockOpts = {
73
+ /**
74
+ * - 락 보유 시간(ms, 양의 정수). 미지정 시 manager 디폴트.
75
+ */
76
+ ttl?: number;
77
+ /**
78
+ * - 획득 대기 상한(ms, 0 이상). 0 = 즉시(tryAcquire 와 동일). 미지정 시 디폴트.
79
+ */
80
+ waitMs?: number;
81
+ /**
82
+ * - true 면 대기자를 도착 순서대로 깨운다(기아 방지). driver 가 지원해야 함.
83
+ */
84
+ fifo?: boolean;
85
+ /**
86
+ * - true 면 단조 증가 fence 토큰을 함께 반환(외부 시스템의 stale-writer 차단용).
87
+ */
88
+ fence?: boolean;
89
+ /**
90
+ * - true 면 manager 가 watchdog 으로 ttl 절반마다 자동 연장(긴 임계구역).
91
+ */
92
+ extendable?: boolean;
93
+ };
94
+ /**
95
+ * - 검증·기본값 적용된 옵션.
96
+ */
97
+ export type NormalizedLockOpts = {
98
+ ttl: number;
99
+ waitMs: number;
100
+ fifo: boolean;
101
+ fence: boolean;
102
+ extendable: boolean;
103
+ };
104
+ /**
105
+ * - driver.acquire 성공 결과(저수준 — manager 가 LockHandle 로 감싼다).
106
+ */
107
+ export type DriverLock = {
108
+ /**
109
+ * - 이 보유를 식별하는 랜덤 토큰(release/extend 가 소유 검증에 쓴다).
110
+ */
111
+ token: string;
112
+ /**
113
+ * - fence 토큰(단조 증가). `fence:true` 일 때만.
114
+ */
115
+ fence?: number;
116
+ };
117
+ /**
118
+ * - 사용자에게 반환되는 락 핸들(manager 구성).
119
+ */
120
+ export type LockHandle = {
121
+ /**
122
+ * - 잠근 자원 키.
123
+ */
124
+ key: string;
125
+ /**
126
+ * - 보유 토큰.
127
+ */
128
+ token: string;
129
+ /**
130
+ * - fence 토큰(`fence:true` 일 때만).
131
+ */
132
+ fence?: number;
133
+ /**
134
+ * - 락 해제(idempotent — 이미 풀렸어도 안전). watchdog 도 멈춘다.
135
+ */
136
+ release: () => Promise<void>;
137
+ /**
138
+ * - TTL 연장. 아직 보유 중이면 true, 잃었으면 false.
139
+ */
140
+ extend: (ttl?: number) => Promise<boolean>;
141
+ };
142
+ /**
143
+ * - driver 가 구현하는 저수준 인터페이스(manager 가 호출).
144
+ */
145
+ export type LockDriver = {
146
+ /**
147
+ * - 'redis' | 'cluster' | 'memory'.
148
+ */
149
+ name: string;
150
+ /**
151
+ * -
152
+ * waitMs 안에 획득하면 `{token, fence?}`, 못 잡으면 `null`(throw 아님 — 경합은 정상 결과).
153
+ */
154
+ acquire: (key: string, opts: NormalizedLockOpts) => Promise<DriverLock | null>;
155
+ /**
156
+ * - 토큰이 현재 보유자면 풀고 true,
157
+ * 아니면(이미 만료/타인 보유) false. 절대 throw 안 함.
158
+ */
159
+ release: (key: string, token: string) => Promise<boolean>;
160
+ /**
161
+ * - 토큰이 보유자면 ttl
162
+ * 갱신 후 true, 아니면 false.
163
+ */
164
+ extend: (key: string, token: string, ttl: number) => Promise<boolean>;
165
+ /**
166
+ * - 관리용 강제 해제(토큰 무관). 대기자 깨움 포함.
167
+ */
168
+ forceRelease: (key: string) => Promise<void>;
169
+ /**
170
+ * - 현재 보유/대기 수.
171
+ */
172
+ stats: () => Promise<{
173
+ driver: string;
174
+ active: number;
175
+ waiting: number;
176
+ }>;
177
+ /**
178
+ * - 타이머·구독·연결참조 정리(graceful shutdown).
179
+ */
180
+ close: () => Promise<void>;
181
+ };
@@ -0,0 +1,38 @@
1
+ export class FifoWaitlist {
2
+ /**
3
+ * @param {RedisLike} client - ioredis 호환 클라이언트(명령용, redis-lock 과 공유).
4
+ * @param {{ zkey: (key: string) => string, seqKey: string }} opts -
5
+ * `zkey` 락 key → zset 키 매퍼, `seqKey` 전역 시퀀스 카운터 키(네임스페이스 일원화).
6
+ */
7
+ constructor(client: RedisLike, { zkey, seqKey }: {
8
+ zkey: (key: string) => string;
9
+ seqKey: string;
10
+ });
11
+ /**
12
+ * 대기열에 합류 — 전역 INCR 시퀀스를 점수로 부여해 도착 순서를 확정한다.
13
+ * @param {string} key @param {string} token @param {number} deadlineMs - 이 대기자의 시한(now+waitMs); reaping 판정용.
14
+ * @returns {Promise<string>} 등록된 member 문자열(leave 에 그대로 넘겨 정확히 제거).
15
+ */
16
+ join(key: string, token: string, deadlineMs: number): Promise<string>;
17
+ /**
18
+ * 내가 큐의 head 인지 — 시한 지난(버려진) head 를 청소한 뒤 맨 앞 token 과 비교.
19
+ * @param {string} key @param {string} token @param {number} nowMs - 현재 시각(Date.now()).
20
+ * @returns {Promise<boolean>}
21
+ */
22
+ isHead(key: string, token: string, nowMs: number): Promise<boolean>;
23
+ /** 대기열에서 이탈(획득 성공/타임아웃/취소 시). @param {string} key @param {string} member - join 이 돌려준 값. @returns {Promise<void>} */
24
+ leave(key: string, member: string): Promise<void>;
25
+ /** 현재 대기자 수. @param {string} key @returns {Promise<number>} */
26
+ size(key: string): Promise<number>;
27
+ #private;
28
+ }
29
+ /**
30
+ * - 이 헬퍼가 쓰는 ioredis 메서드 부분집합(테스트 fake 도 충족).
31
+ */
32
+ export type RedisLike = {
33
+ incr: (key: string) => Promise<number>;
34
+ zadd: (key: string, score: number, member: string) => Promise<any>;
35
+ zrem: (key: string, member: string) => Promise<any>;
36
+ zrange: (key: string, start: number, stop: number) => Promise<string[]>;
37
+ zcard: (key: string) => Promise<number>;
38
+ };
@@ -0,0 +1,96 @@
1
+ /** 활성 락 manager 설정(boot lock 스테이지). null 로 해제(셧다운/테스트). @param {LockManager | null} manager @returns {void} */
2
+ export function setLockManager(manager: LockManager | null): void;
3
+ /** 현재 활성 락 manager(없으면 null — `ctx.lock.with` 미부착). @returns {LockManager | null} */
4
+ export function getLockManager(): LockManager | null;
5
+ /**
6
+ * 분산 락 manager 를 만든다 — driver 자동 폴백 포함. boot 가 1회 호출해 `ctx.lock` 에 부착한다.
7
+ *
8
+ * @param {{ driver?: string, ttl?: number, waitMs?: number, fifo?: boolean, cache?: string }} [lockConfig] -
9
+ * config 의 `lock` 섹션. `cache` 는 redis driver 가 빌릴 cache 어댑터 alias(redis/auto 일 때 의미).
10
+ * @param {{ logger?: any, redisClient?: any, isClusterWorker?: boolean }} [deps] -
11
+ * - `redisClient` boot 가 cache 어댑터에서 빌려 넘긴 ioredis 클라이언트(없으면 null).
12
+ * - `isClusterWorker` 현재 프로세스가 cluster 워커인지(`cluster.isWorker`).
13
+ * @returns {LockManager}
14
+ */
15
+ export function createLockManager(lockConfig?: {
16
+ driver?: string;
17
+ ttl?: number;
18
+ waitMs?: number;
19
+ fifo?: boolean;
20
+ cache?: string;
21
+ }, deps?: {
22
+ logger?: any;
23
+ redisClient?: any;
24
+ isClusterWorker?: boolean;
25
+ }): LockManager;
26
+ /**
27
+ * `ctx.lock` 콜러블에 사용자 API 메서드를 얹는다 — `ctx.lock(alias)`(기존)와 `ctx.lock.with(...)`(신규) 공존.
28
+ * boot 가 manager 생성 후 각 앱의 lock accessor 에 1회 호출한다.
29
+ *
30
+ * @param {Function & Record<string, any>} lockAccessor - `(alias) => MegaLockAdapter` 콜러블(ctx-builder 산출).
31
+ * @param {LockManager} manager
32
+ * @returns {Function & Record<string, any>} 같은 accessor(체이닝용).
33
+ */
34
+ export function attachLockApi(lockAccessor: Function & Record<string, any>, manager: LockManager): Function & Record<string, any>;
35
+ /**
36
+ * 사용자 분산 락 API. driver 한 개를 감싸 `with/acquire/tryAcquire/forceRelease/stats` 를 제공한다.
37
+ */
38
+ export class LockManager {
39
+ /**
40
+ * @param {import('./contract.js').LockDriver} driver - 선택된 저수준 driver.
41
+ * @param {{ defaults: { ttl: number, waitMs: number, fifo: boolean }, logger?: any }} opts
42
+ */
43
+ constructor(driver: import("./contract.js").LockDriver, { defaults, logger }: {
44
+ defaults: {
45
+ ttl: number;
46
+ waitMs: number;
47
+ fifo: boolean;
48
+ };
49
+ logger?: any;
50
+ });
51
+ /** @returns {string} 활성 driver 이름('redis'|'cluster'|'memory'). */
52
+ get driverName(): string;
53
+ /**
54
+ * 락 획득(대기 후 실패 시 throw) — `try/finally` 로 직접 release 하는 용도.
55
+ * @param {string} key - 자원 키(비어있지 않은 공백 없는 문자열).
56
+ * @param {import('./contract.js').LockOpts} [opts] - ttl/waitMs/fifo/fence/extendable.
57
+ * @returns {Promise<import('./contract.js').LockHandle>}
58
+ * @throws {MegaConflictError} `lock.not_acquired` - waitMs 안에 못 잡음(자원이 잠겨있음 — 409).
59
+ * @throws {MegaValidationError} key/opts 부적합.
60
+ */
61
+ acquire(key: string, opts?: import("./contract.js").LockOpts): Promise<import("./contract.js").LockHandle>;
62
+ /**
63
+ * 락을 즉시 1회만 시도(대기 없음) — 못 잡으면 null(throw 아님).
64
+ * @param {string} key @param {import('./contract.js').LockOpts} [opts]
65
+ * @returns {Promise<import('./contract.js').LockHandle | null>}
66
+ */
67
+ tryAcquire(key: string, opts?: import("./contract.js").LockOpts): Promise<import("./contract.js").LockHandle | null>;
68
+ /**
69
+ * 락 보호 임계구역 실행 — 획득 → fn 실행 → **반드시 해제**(성공/예외 무관). 가장 권장되는 사용법.
70
+ * @template T
71
+ * @param {string} key
72
+ * @param {import('./contract.js').LockOpts | ((lock: import('./contract.js').LockHandle) => Promise<T> | T)} optsOrFn -
73
+ * 옵션, 또는 옵션을 생략하고 바로 fn(2-인자 형태 `with(key, fn)`).
74
+ * @param {(lock: import('./contract.js').LockHandle) => Promise<T> | T} [maybeFn] - 3-인자 형태의 fn.
75
+ * @returns {Promise<T>} fn 의 반환값.
76
+ */
77
+ with<T>(key: string, optsOrFn: import("./contract.js").LockOpts | ((lock: import("./contract.js").LockHandle) => Promise<T> | T), maybeFn?: (lock: import("./contract.js").LockHandle) => Promise<T> | T): Promise<T>;
78
+ /**
79
+ * 관리용 강제 해제(토큰 무관) — 운영 개입(고아 락 정리)용. 주의: 정상 보유자의 임계구역을 깰 수 있다.
80
+ * @param {string} key @returns {Promise<void>}
81
+ */
82
+ forceRelease(key: string): Promise<void>;
83
+ /** @returns {Promise<{ driver: string, active: number, waiting: number }>} 현재 보유/대기 수. */
84
+ stats(): Promise<{
85
+ driver: string;
86
+ active: number;
87
+ waiting: number;
88
+ }>;
89
+ /** 정리 — driver 자원(타이머·구독·IPC) 해제. graceful shutdown 에서 호출. @returns {Promise<void>} */
90
+ close(): Promise<void>;
91
+ #private;
92
+ }
93
+ import { MemoryLockDriver } from './memory-lock.js';
94
+ import { ClusterLockDriver } from './cluster-lock.js';
95
+ import { RedisLockDriver } from './redis-lock.js';
96
+ export { MemoryLockDriver, ClusterLockDriver, RedisLockDriver };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @typedef {Object} HeldLock - 보유 중 락 레코드.
3
+ * @property {string} token @property {number} expiresAt @property {ReturnType<typeof setTimeout>} timer @property {number} [fence]
4
+ */
5
+ /**
6
+ * @typedef {Object} Waiter - 대기 큐 항목.
7
+ * @property {number} ttl @property {boolean} fence @property {(lock: import('./contract.js').DriverLock | null) => void} resolve
8
+ * @property {ReturnType<typeof setTimeout> | null} timer
9
+ */
10
+ export class MemoryLockDriver {
11
+ /** @type {'memory'} */
12
+ get name(): "memory";
13
+ /**
14
+ * 락 획득 — 즉시 가능하면 grant, 아니면 waitMs 안에서 FIFO 큐 대기.
15
+ * @param {string} key @param {import('./contract.js').NormalizedLockOpts} opts
16
+ * @returns {Promise<import('./contract.js').DriverLock | null>}
17
+ */
18
+ acquire(key: string, { ttl, waitMs, fence }: import("./contract.js").NormalizedLockOpts): Promise<import("./contract.js").DriverLock | null>;
19
+ /**
20
+ * 락 해제 — 토큰이 현재 보유자일 때만. 해제 후 다음 대기자 승계.
21
+ * @param {string} key @param {string} token @returns {Promise<boolean>}
22
+ */
23
+ release(key: string, token: string): Promise<boolean>;
24
+ /**
25
+ * TTL 연장 — 토큰이 보유자일 때만 만료 타이머 재설정.
26
+ * @param {string} key @param {string} token @param {number} ttl @returns {Promise<boolean>}
27
+ */
28
+ extend(key: string, token: string, ttl: number): Promise<boolean>;
29
+ /** 강제 해제(관리용, 토큰 무관) — 보유 비우고 다음 대기자 승계. @param {string} key @returns {Promise<void>} */
30
+ forceRelease(key: string): Promise<void>;
31
+ /** @returns {Promise<{ driver: string, active: number, waiting: number }>} 보유/대기 수. */
32
+ stats(): Promise<{
33
+ driver: string;
34
+ active: number;
35
+ waiting: number;
36
+ }>;
37
+ /** 정리 — 모든 만료 타이머 해제 + 대기자에 null 응답(graceful shutdown). @returns {Promise<void>} */
38
+ close(): Promise<void>;
39
+ #private;
40
+ }
41
+ /**
42
+ * - 보유 중 락 레코드.
43
+ */
44
+ export type HeldLock = {
45
+ token: string;
46
+ expiresAt: number;
47
+ timer: ReturnType<typeof setTimeout>;
48
+ fence?: number;
49
+ };
50
+ /**
51
+ * - 대기 큐 항목.
52
+ */
53
+ export type Waiter = {
54
+ ttl: number;
55
+ fence: boolean;
56
+ resolve: (lock: import("./contract.js").DriverLock | null) => void;
57
+ timer: ReturnType<typeof setTimeout> | null;
58
+ };
@@ -0,0 +1,43 @@
1
+ export class RedisLockDriver {
2
+ /**
3
+ * @param {{ client: any, createSubscriber?: () => any }} opts
4
+ * - `client` ioredis 호환 클라이언트(`set`/`eval`/`incr`/`zadd`… 보유). cache 어댑터의 `.native`.
5
+ * - `createSubscriber` Pub/Sub 구독 연결 생성자(기본 `client.duplicate()`). 테스트 주입용.
6
+ */
7
+ constructor({ client, createSubscriber }: {
8
+ client: any;
9
+ createSubscriber?: () => any;
10
+ });
11
+ /** @type {'redis'} */
12
+ get name(): "redis";
13
+ /**
14
+ * 락 획득 — fifo 면 대기열 head 일 때만, 아니면 즉시 SET 후 경합 시 Pub/Sub 대기.
15
+ * @param {string} key @param {import('./contract.js').NormalizedLockOpts} opts
16
+ * @returns {Promise<import('./contract.js').DriverLock | null>}
17
+ */
18
+ acquire(key: string, { ttl, waitMs, fifo, fence }: import("./contract.js").NormalizedLockOpts): Promise<import("./contract.js").DriverLock | null>;
19
+ /**
20
+ * 해제 — 소유 토큰일 때만 DEL + 해제 알림(Lua 원자).
21
+ * @param {string} key @param {string} token @returns {Promise<boolean>}
22
+ */
23
+ release(key: string, token: string): Promise<boolean>;
24
+ /**
25
+ * 연장 — 소유 토큰일 때만 PEXPIRE(Lua 원자).
26
+ * @param {string} key @param {string} token @param {number} ttl @returns {Promise<boolean>}
27
+ */
28
+ extend(key: string, token: string, ttl: number): Promise<boolean>;
29
+ /** 강제 해제(관리용, 토큰 무관) — DEL + 해제 알림(대기자 깨움). @param {string} key @returns {Promise<void>} */
30
+ forceRelease(key: string): Promise<void>;
31
+ /**
32
+ * 통계(best-effort) — `…:v:*`/`…:w:*` 키를 SCAN 으로 센다. O(키 수) 라 운영 관측용(고빈도 호출 비권장).
33
+ * @returns {Promise<{ driver: string, active: number, waiting: number }>}
34
+ */
35
+ stats(): Promise<{
36
+ driver: string;
37
+ active: number;
38
+ waiting: number;
39
+ }>;
40
+ /** 정리 — 구독 연결만 quit(명령 클라이언트는 cache 소유라 건드리지 않음). @returns {Promise<void>} */
41
+ close(): Promise<void>;
42
+ #private;
43
+ }