mega-framework 0.1.6 → 0.1.7
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/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +1 -1
- package/sample/crud/.env.example +1 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +3 -2
- package/sample/crud/package.json +1 -1
- package/sample/crud/scripts/start-ws-hub.sh +2 -2
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +30 -0
- package/src/adapters/maria-adapter.js +26 -3
- package/src/adapters/mega-db-adapter.js +7 -1
- package/src/adapters/mongo-adapter.js +19 -1
- package/src/adapters/postgres-adapter.js +25 -2
- package/src/adapters/sqlite-adapter.js +20 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +137 -33
- package/src/cli/generators/index.js +82 -2
- package/src/cli/index.js +353 -100
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +6 -2
- package/src/core/envelope.js +112 -12
- package/src/core/hub-link.js +65 -4
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +6 -2
- package/src/core/mega-app.js +201 -463
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +129 -0
- package/src/core/router.js +70 -65
- package/src/core/security.js +67 -9
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +624 -0
- package/src/core/ws-roster.js +4 -1
- package/src/core/ws-upgrade.js +118 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-health.js +25 -4
- package/src/lib/mega-job-queue.js +98 -21
- package/src/lib/mega-job.js +29 -0
- package/src/lib/mega-metrics.js +3 -12
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +5 -1
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +51 -8
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +91 -0
- package/types/adapters/file-adapter.d.ts +94 -0
- package/types/adapters/file-session-adapter.d.ts +101 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +115 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +47 -0
- package/types/adapters/mega-db-adapter.d.ts +47 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +139 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +139 -0
- package/types/adapters/redis-adapter.d.ts +70 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +106 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +112 -0
- package/types/cli/index.d.ts +249 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +80 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +264 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +92 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +18 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +96 -0
- package/types/core/ws-upgrade.d.ts +231 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +241 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +41 -0
- package/types/lib/mega-job-queue.d.ts +176 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +138 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +127 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +234 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ADR-094 의 ASP echo 데모 hub 를 본 12-타입 hub 로 교체(ADR-097). 클라↔bridge ASP
|
|
24
24
|
* round-trip 검증은 embedded 종단(`core/ws-upgrade.js`, ws-upgrade.integration)으로 이전됨.
|
|
25
25
|
*
|
|
26
|
-
* @module
|
|
26
|
+
* @module lib/ws-hub
|
|
27
27
|
*/
|
|
28
28
|
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
29
29
|
import { WebSocketServer } from 'ws'
|
|
@@ -32,8 +32,10 @@ import { buildPerMessageDeflate, COMPRESSION_DEFAULTS } from '../core/ws-compres
|
|
|
32
32
|
import {
|
|
33
33
|
HUB_MESSAGE_TYPES,
|
|
34
34
|
HUB_CLOSE_CODES,
|
|
35
|
+
HUB_PROTOCOL_VERSION,
|
|
35
36
|
createHubMessage,
|
|
36
37
|
validateHubMessage,
|
|
38
|
+
negotiateHubProtocolVersion,
|
|
37
39
|
} from '../lib/hub-protocol.js'
|
|
38
40
|
import { MegaShutdown } from '../lib/mega-shutdown.js'
|
|
39
41
|
|
|
@@ -45,6 +47,12 @@ export const DEFAULT_HEARTBEAT_MS = 25_000
|
|
|
45
47
|
/** 기본 최대 프레임 크기 (bytes, L3). 1 MiB — 정상 envelope 은 수 KB 이므로 넉넉하다. */
|
|
46
48
|
export const DEFAULT_MAX_PAYLOAD_BYTES = 1_048_576
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* bridge 별 송신 버퍼(bufferedAmount) 기본 상한(바이트) — 16 MiB. 초과 = 느린 bridge 가 fan-out 을 못
|
|
52
|
+
* 따라오는 것 → terminate(백프레셔). 클라↔bridge 종단(`ws-upgrade.js` DEFAULT_MAX_BUFFERED_BYTES)과 대칭.
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_MAX_BUFFERED_BYTES = 16 * 1024 * 1024
|
|
55
|
+
|
|
48
56
|
/**
|
|
49
57
|
* 토큰 timing-safe 비교 — sha256 으로 길이 정규화 후 `timingSafeEqual`. 후보 전체를 순회하여
|
|
50
58
|
* 조기 반환 timing 누출도 줄인다(early-return 안 함).
|
|
@@ -68,19 +76,23 @@ export class MegaWsHub {
|
|
|
68
76
|
* @param {string[]} [opts.acceptedTokens] - Bridge Bearer 토큰 화이트리스트 (비거나 누락 시 throw).
|
|
69
77
|
* @param {number} [opts.heartbeatMs=25000] - register_ok 로 알려줄 heartbeat 주기.
|
|
70
78
|
* @param {number} [opts.maxPayloadBytes=1048576] - WS 프레임 최대 크기(L3). 초과 시 ws 가 1009 close.
|
|
79
|
+
* @param {number} [opts.maxBufferedBytes=16777216] - bridge 별 송신 버퍼(bufferedAmount) 상한(바이트).
|
|
80
|
+
* 초과한 bridge 는 느린 소비자로 보고 terminate 한다 — fan-out 허브에서 bridge 1개가 느려도 모든
|
|
81
|
+
* 채널 메시지가 그 소켓 버퍼(힙)에 무한 적재돼 OOM 으로 가는 것을 막는다(백프레셔).
|
|
71
82
|
* @param {string} [opts.hubId] - hub 식별자. 기본 ULID 자동 생성.
|
|
72
83
|
* @param {import('../core/ws-compression.js').WsCompressionConfig} [opts.compression] - Bridge↔Hub
|
|
73
84
|
* link per-message deflate 압축(ADR-078 / wsHub.compression). 디폴트 OFF.
|
|
74
85
|
* bridge(MegaHubLink)와 양쪽이 협상해야 활성. 잘못된 threshold/windowBits 면 즉시 throw.
|
|
75
86
|
* @param {{ warn?: Function, debug?: Function, info?: Function, error?: Function }} [opts.logger]
|
|
76
87
|
*/
|
|
77
|
-
constructor({ acceptedTokens, heartbeatMs = DEFAULT_HEARTBEAT_MS, maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES, hubId, compression, logger } = {}) {
|
|
88
|
+
constructor({ acceptedTokens, heartbeatMs = DEFAULT_HEARTBEAT_MS, maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES, maxBufferedBytes = DEFAULT_MAX_BUFFERED_BYTES, hubId, compression, logger } = {}) {
|
|
78
89
|
if (!Array.isArray(acceptedTokens) || acceptedTokens.length === 0) {
|
|
79
90
|
throw new Error('MegaWsHub: acceptedTokens must be a non-empty array (ADR-059).')
|
|
80
91
|
}
|
|
81
92
|
this._acceptedTokens = [...acceptedTokens]
|
|
82
93
|
this._heartbeatMs = Number.isInteger(heartbeatMs) && heartbeatMs > 0 ? heartbeatMs : DEFAULT_HEARTBEAT_MS
|
|
83
94
|
this._maxPayloadBytes = Number.isInteger(maxPayloadBytes) && maxPayloadBytes > 0 ? maxPayloadBytes : DEFAULT_MAX_PAYLOAD_BYTES
|
|
95
|
+
this._maxBufferedBytes = Number.isInteger(maxBufferedBytes) && maxBufferedBytes > 0 ? maxBufferedBytes : DEFAULT_MAX_BUFFERED_BYTES
|
|
84
96
|
this._hubId = typeof hubId === 'string' && hubId.length > 0 ? hubId : `hub-${generateMessageId()}`
|
|
85
97
|
// Bridge↔Hub link 압축(ADR-078). enabled=false → false. 잘못된 값은 생성자에서 즉시 throw.
|
|
86
98
|
/** @type {false | Object} WebSocketServer perMessageDeflate 로 전달(start). */
|
|
@@ -91,7 +103,7 @@ export class MegaWsHub {
|
|
|
91
103
|
this._wss = null
|
|
92
104
|
/** heartbeat liveness 체크 interval (M3). @type {ReturnType<typeof setInterval> | null} */
|
|
93
105
|
this._livenessTimer = null
|
|
94
|
-
/** 등록된 bridge 연결. connId → { socket, lastSeen }. @type {Map<string, { socket: import('ws').WebSocket, lastSeen: number }>} */
|
|
106
|
+
/** 등록된 bridge 연결. connId → { socket, lastSeen, protocolVersion }. @type {Map<string, { socket: import('ws').WebSocket, lastSeen: number, protocolVersion?: number }>} */
|
|
95
107
|
this._bridges = new Map()
|
|
96
108
|
/** presence. sessionId → { bridgeConnId, userId, channels:Set, metadata }. @type {Map<string, { bridgeConnId: string, userId: string, channels: Set<string>, metadata: Object }>} */
|
|
97
109
|
this._sessions = new Map()
|
|
@@ -209,16 +221,30 @@ export class MegaWsHub {
|
|
|
209
221
|
* @private
|
|
210
222
|
*/
|
|
211
223
|
_handleRegister(connId, socket, msg) {
|
|
212
|
-
const payload = /** @type {{ instanceId: string, token: string, capabilities: string[] }} */ (/** @type {any} */ (msg).payload)
|
|
224
|
+
const payload = /** @type {{ instanceId: string, token: string, capabilities: string[], protocolVersion?: number }} */ (/** @type {any} */ (msg).payload)
|
|
213
225
|
if (!isTokenAccepted(payload.token, this._acceptedTokens)) {
|
|
214
226
|
this._log?.warn?.({ connId, instanceId: payload.instanceId }, 'ws-hub register denied — bad token (ADR-059)')
|
|
215
227
|
this._safeSend(socket, createHubMessage({ type: T.ERROR, error: { code: 'hub.unauthorized', message: 'invalid bridge token' }, ref: /** @type {any} */ (msg).id }))
|
|
216
228
|
socket.close(1008, 'unauthorized') // RFC 6455 1008 = policy violation
|
|
217
229
|
return false
|
|
218
230
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
// 프로토콜 버전 협상 — bridge 가 최대 지원 버전을 실으면 상호 최고 버전(min)을 채택해 회신한다.
|
|
232
|
+
// 부재 = 레거시 bridge → v1 고정 + register_ok 에 필드 미포함(strict 스키마 bridge 가 안 깨지게).
|
|
233
|
+
const hasVersionRequest = payload.protocolVersion !== undefined
|
|
234
|
+
const protocolVersion = hasVersionRequest
|
|
235
|
+
? negotiateHubProtocolVersion(payload.protocolVersion)
|
|
236
|
+
: HUB_PROTOCOL_VERSION
|
|
237
|
+
this._bridges.set(connId, { socket, lastSeen: Date.now(), protocolVersion })
|
|
238
|
+
this._log?.info?.({ connId, instanceId: payload.instanceId, hubId: this._hubId, protocolVersion }, 'ws-hub bridge registered')
|
|
239
|
+
this._safeSend(socket, createHubMessage({
|
|
240
|
+
type: T.REGISTER_OK,
|
|
241
|
+
payload: {
|
|
242
|
+
hubId: this._hubId,
|
|
243
|
+
acceptedAt: Date.now(),
|
|
244
|
+
heartbeatMs: this._heartbeatMs,
|
|
245
|
+
...(hasVersionRequest ? { protocolVersion } : {}),
|
|
246
|
+
},
|
|
247
|
+
}))
|
|
222
248
|
return true
|
|
223
249
|
}
|
|
224
250
|
|
|
@@ -478,6 +504,22 @@ export class MegaWsHub {
|
|
|
478
504
|
* @private
|
|
479
505
|
*/
|
|
480
506
|
_sendSerialized(socket, serialized) {
|
|
507
|
+
// 느린 bridge 백프레셔 가드: 송신 버퍼가 상한을 넘으면 더 쌓지 않고 연결을 끊는다 — fan-out 허브는
|
|
508
|
+
// bridge 1개만 느려도 모든 채널 메시지가 그 소켓 버퍼(힙)에 적재돼 OOM 으로 가는 최악 지점이다.
|
|
509
|
+
// terminate 후 'close' 이벤트가 `_handleBridgeGone` 을 불러 presence 정리 + BULK_LEAVE fan-out 이
|
|
510
|
+
// 따라온다(재연결은 bridge 의 retry 책임, ADR-098).
|
|
511
|
+
if (socket.bufferedAmount > this._maxBufferedBytes) {
|
|
512
|
+
this._log?.warn?.(
|
|
513
|
+
{ bufferedAmount: socket.bufferedAmount, max: this._maxBufferedBytes },
|
|
514
|
+
'ws-hub slow bridge — terminating (backpressure)',
|
|
515
|
+
)
|
|
516
|
+
try {
|
|
517
|
+
socket.terminate()
|
|
518
|
+
} catch (err) {
|
|
519
|
+
this._log?.debug?.({ err }, 'ws-hub backpressure terminate failed (already closing)')
|
|
520
|
+
}
|
|
521
|
+
return
|
|
522
|
+
}
|
|
481
523
|
try {
|
|
482
524
|
socket.send(serialized)
|
|
483
525
|
} catch (err) {
|
|
@@ -575,7 +617,8 @@ export async function runWsHubCli() {
|
|
|
575
617
|
const hub = new MegaWsHub({ acceptedTokens, heartbeatMs, maxPayloadBytes, compression, logger: console })
|
|
576
618
|
const addr = await hub.start({ port, host })
|
|
577
619
|
// 독립 hub 프로세스 graceful shutdown(L2 + drain) — SIGTERM/SIGINT → hub.stop({ drain: true }).
|
|
578
|
-
|
|
620
|
+
// 'server' stage — hub 도 수용 종단이라 가장 먼저 drain 종료한다.
|
|
621
|
+
MegaShutdown.register('mega-ws-hub', async () => hub.stop({ drain: true }), { stage: 'server' })
|
|
579
622
|
MegaShutdown.setupSignals()
|
|
580
623
|
console.log(`[mega:ws-hub] listening on ${addr.host}:${addr.port} (hubId=${hub.hubId})`)
|
|
581
624
|
return hub
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* CRUD SQL 조각 빌더 (ADR-212) — **순수 함수**(DB·어댑터 불요, 단위 테스트 용이).
|
|
4
|
+
*
|
|
5
|
+
* 모든 식별자(컬럼·테이블)는 `static schema` 화이트리스트에서만 도출하고 `dialect.quoteIdent` 로 인용한다.
|
|
6
|
+
* 값은 100% 파라미터 바인딩(`dialect.placeholder(i)` + params). **문자열 인터폴레이션 0**(ADR-009 안전 경계).
|
|
7
|
+
*
|
|
8
|
+
* @module models/crud-sql-builder
|
|
9
|
+
*/
|
|
10
|
+
import { MegaInternalError } from '../errors/http-errors.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} CrudDialect - dialect 모듈의 DML facet(ADR-212) 일부.
|
|
14
|
+
* @property {(name: string) => string} quoteIdent
|
|
15
|
+
* @property {(i: number) => string} placeholder
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 컬럼이 schema 화이트리스트에 있는지 — 없으면 throw. @param {string} col @param {Set<string>} cols @param {string} model @param {string} where */
|
|
19
|
+
export function assertColumn(col, cols, model, where) {
|
|
20
|
+
if (!cols.has(col)) {
|
|
21
|
+
throw new MegaInternalError(
|
|
22
|
+
'model.unknown_column',
|
|
23
|
+
`${model}: '${col}' 은 static schema 컬럼이 아닙니다(${where}). 선언된 컬럼만 CRUD 에 쓸 수 있습니다 — 임의 SQL 은 this.query 사용.`,
|
|
24
|
+
{ details: { model, column: col, where } },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** limit/offset 정수 검증 — 음수/비정수 throw, 통과하면 그 정수 반환. @param {unknown} v @param {string} name @param {string} model @returns {number} */
|
|
30
|
+
export function assertNonNegInt(v, name, model) {
|
|
31
|
+
if (!Number.isInteger(v) || /** @type {number} */ (v) < 0) {
|
|
32
|
+
throw new MegaInternalError(
|
|
33
|
+
'model.invalid_pagination',
|
|
34
|
+
`${model}: ${name} 은 0 이상의 정수여야 합니다(got ${JSON.stringify(v)}).`,
|
|
35
|
+
{
|
|
36
|
+
details: { model, [name]: v },
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return /** @type {number} */ (v)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* filter → WHERE 조각(접두 'WHERE' 없음) + params + 다음 placeholder 인덱스.
|
|
45
|
+
* 등호 AND + `IN`(배열) + `IS NULL`(null) 만(ADR-212 경계). `undefined` 값은 throw, 빈 배열은 `1=0`(매칭 0).
|
|
46
|
+
*
|
|
47
|
+
* @param {Record<string, any>} filter
|
|
48
|
+
* @param {Set<string>} cols - schema 컬럼 화이트리스트
|
|
49
|
+
* @param {CrudDialect} dialect
|
|
50
|
+
* @param {string} model - 에러 메시지용
|
|
51
|
+
* @param {number} [startIndex=1] - placeholder 시작 인덱스(SET 뒤 WHERE 처럼 이어붙일 때)
|
|
52
|
+
* @returns {{ clause: string, params: any[], nextIndex: number }}
|
|
53
|
+
*/
|
|
54
|
+
export function buildWhere(filter, cols, dialect, model, startIndex = 1) {
|
|
55
|
+
/** @type {string[]} */
|
|
56
|
+
const parts = []
|
|
57
|
+
/** @type {any[]} */
|
|
58
|
+
const params = []
|
|
59
|
+
let i = startIndex
|
|
60
|
+
for (const key of Object.keys(filter)) {
|
|
61
|
+
assertColumn(key, cols, model, 'filter')
|
|
62
|
+
const v = filter[key]
|
|
63
|
+
const id = dialect.quoteIdent(key)
|
|
64
|
+
if (v === undefined) {
|
|
65
|
+
throw new MegaInternalError(
|
|
66
|
+
'model.invalid_filter',
|
|
67
|
+
`${model}: filter['${key}'] 가 undefined 입니다(null 과 구분 — 의도 모호).`,
|
|
68
|
+
{
|
|
69
|
+
details: { model, column: key },
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
if (v === null) {
|
|
74
|
+
parts.push(`${id} IS NULL`)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(v)) {
|
|
78
|
+
if (v.length === 0) {
|
|
79
|
+
parts.push('1=0') // 빈 IN → 매칭 0(안전).
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const phs = v.map(() => dialect.placeholder(i++))
|
|
83
|
+
parts.push(`${id} IN (${phs.join(', ')})`)
|
|
84
|
+
params.push(...v)
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
parts.push(`${id} = ${dialect.placeholder(i++)}`)
|
|
88
|
+
params.push(v)
|
|
89
|
+
}
|
|
90
|
+
return { clause: parts.join(' AND '), params, nextIndex: i }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* orderBy 옵션 → `ORDER BY ...` 조각(없으면 ''). 컬럼 화이트리스트 + dir enum.
|
|
95
|
+
* @param {string | Array<{ column: string, dir?: 'asc'|'desc' }> | undefined} orderBy
|
|
96
|
+
* @param {Set<string>} cols @param {CrudDialect} dialect @param {string} model
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function buildOrderBy(orderBy, cols, dialect, model) {
|
|
100
|
+
if (orderBy === undefined) return ''
|
|
101
|
+
const list = typeof orderBy === 'string' ? [{ column: orderBy }] : orderBy
|
|
102
|
+
if (!Array.isArray(list) || list.length === 0) return ''
|
|
103
|
+
const items = list.map((o) => {
|
|
104
|
+
const col = typeof o === 'string' ? o : o.column
|
|
105
|
+
assertColumn(col, cols, model, 'orderBy')
|
|
106
|
+
const dir = typeof o === 'string' ? 'asc' : (o.dir ?? 'asc')
|
|
107
|
+
if (dir !== 'asc' && dir !== 'desc') {
|
|
108
|
+
throw new MegaInternalError(
|
|
109
|
+
'model.invalid_pagination',
|
|
110
|
+
`${model}: orderBy dir 은 'asc'|'desc' 만 — got '${dir}'.`,
|
|
111
|
+
{ details: { model, dir } },
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return `${dialect.quoteIdent(col)} ${dir.toUpperCase()}`
|
|
115
|
+
})
|
|
116
|
+
return ` ORDER BY ${items.join(', ')}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* select 옵션 → 컬럼 목록 조각(`*` 또는 인용 컬럼). 화이트리스트 검증.
|
|
121
|
+
* @param {string[] | undefined} select @param {Set<string>} cols @param {CrudDialect} dialect @param {string} model
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
export function buildSelectList(select, cols, dialect, model) {
|
|
125
|
+
if (select === undefined) return '*'
|
|
126
|
+
if (!Array.isArray(select) || select.length === 0) return '*'
|
|
127
|
+
return select
|
|
128
|
+
.map((c) => {
|
|
129
|
+
assertColumn(c, cols, model, 'select')
|
|
130
|
+
return dialect.quoteIdent(c)
|
|
131
|
+
})
|
|
132
|
+
.join(', ')
|
|
133
|
+
}
|
package/src/models/mega-model.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
*/
|
|
42
42
|
import { MegaInternalError } from '../errors/http-errors.js'
|
|
43
43
|
import * as MegaAdapterManager from '../adapters/adapter-manager.js'
|
|
44
|
+
import * as crud from './model-crud.js'
|
|
44
45
|
|
|
45
46
|
export class MegaModel {
|
|
46
47
|
/**
|
|
@@ -85,12 +86,16 @@ export class MegaModel {
|
|
|
85
86
|
* `fn` 인자 수는 어댑터별로 다르다 — SQL 어댑터는 `(client)` 1개, Mongo 어댑터는 `(db, session)`
|
|
86
87
|
* 2개를 넘긴다(ADR-108). 따라서 가변 인자로 타입을 둔다(어댑터가 인자 형태의 정본).
|
|
87
88
|
*
|
|
89
|
+
* `opts.isolation`(ADR-190) — SQL 격리수준 옵트인. driver 별 지원·제약은 어댑터가 정본
|
|
90
|
+
* (postgres/maria=top-level 만, sqlite='serializable' 만, mongodb=미지원 throw).
|
|
91
|
+
*
|
|
88
92
|
* @template T
|
|
89
93
|
* @param {(...args: any[]) => Promise<T>} fn - 트랜잭션 컨텍스트 native handle(들)을 받는 콜백.
|
|
94
|
+
* @param {{ isolation?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable' }} [opts] - 트랜잭션 옵션(ADR-190).
|
|
90
95
|
* @returns {Promise<T>}
|
|
91
96
|
*/
|
|
92
|
-
static async withTransaction(fn) {
|
|
93
|
-
return this._resolveAdapter().withTransaction(fn)
|
|
97
|
+
static async withTransaction(fn, opts) {
|
|
98
|
+
return this._resolveAdapter().withTransaction(fn, opts)
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
/**
|
|
@@ -109,6 +114,81 @@ export class MegaModel {
|
|
|
109
114
|
return this._resolveAdapter().query(sql, params)
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
// ── 공통 CRUD (ADR-212) ────────────────────────────────────────────────
|
|
118
|
+
//
|
|
119
|
+
// `static schema`(ADR-204)를 선언한 모델에 한해, 컬럼·PK 메타에서 도출된 **bounded** CRUD 를
|
|
120
|
+
// 제공한다. 모든 식별자는 schema 화이트리스트 + `quoteIdent`, 값은 100% 파라미터 바인딩이라
|
|
121
|
+
// SQL 인젝션 표면이 없다(crud-sql-builder). raw 트랙(`this.query`/`this.db`)은 그대로 살아있다 —
|
|
122
|
+
// CRUD 는 추가 표면일 뿐 ADR-009 native 형태를 바꾸지 않는다. driver→dialect(DML facet) 디스패치는
|
|
123
|
+
// 단일 base(MegaModel)에서 런타임에 결정한다(A안). mongo 는 미지원(P3) — 호출 시 명시 throw.
|
|
124
|
+
//
|
|
125
|
+
// 본문·정규화·에러는 model-crud 에 위임하고 여기서는 `this`(Model 클래스)만 넘긴다.
|
|
126
|
+
|
|
127
|
+
/** 단건 조회(없으면 null). filter 는 등호 AND + 배열 IN + null IS NULL. @param {Record<string, any>} filter @param {{ select?: string[] }} [opts] @returns {Promise<any|null>} */
|
|
128
|
+
static async findOne(filter, opts) {
|
|
129
|
+
return crud.findOne(this, filter, opts)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** PK 단건 조회(단일 PK 필요, 없으면 model.no_primary_key). @param {any} id @param {{ select?: string[] }} [opts] @returns {Promise<any|null>} */
|
|
133
|
+
static async findById(id, opts) {
|
|
134
|
+
return crud.findById(this, id, opts)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 다건 조회(기본 limit 없음 — 명시할 때만, ADR-212 #2). @param {Record<string, any>} [filter] @param {{ select?: string[], orderBy?: string | Array<{ column: string, dir?: 'asc'|'desc' }>, limit?: number, offset?: number }} [opts] @returns {Promise<any[]>} */
|
|
138
|
+
static async findMany(filter, opts) {
|
|
139
|
+
return crud.findMany(this, filter ?? {}, opts)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 조건 매칭 행 수. @param {Record<string, any>} [filter] @returns {Promise<number>} */
|
|
143
|
+
static async count(filter) {
|
|
144
|
+
return crud.count(this, filter)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 조건 매칭 행 존재 여부. @param {Record<string, any>} filter @returns {Promise<boolean>} */
|
|
148
|
+
static async exists(filter) {
|
|
149
|
+
return crud.exists(this, filter)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 페이지 조회. total 은 `{ withTotal: true }` 일 때만 추가 count(ADR-212 #4). @param {Record<string, any>} filter @param {{ select?: string[], orderBy?: any, limit: number, offset?: number, withTotal?: boolean }} opts @returns {Promise<{ rows: any[], limit: number, offset: number, total?: number }>} */
|
|
153
|
+
static async paginate(filter, opts) {
|
|
154
|
+
return crud.paginate(this, filter ?? {}, opts)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 단건 삽입. 기본 새 id 반환, `{ returning: true }` 면 레코드(ADR-212 #1). @param {Record<string, any>} data @param {{ returning?: boolean }} [opts] @returns {Promise<any>} */
|
|
158
|
+
static async insertOne(data, opts) {
|
|
159
|
+
return crud.insertOne(this, data, opts)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 다건 삽입. 기본 `{ count }`, `{ returning: true }` 면 레코드 배열(maria 미지원→throw). @param {Record<string, any>[]} rows @param {{ returning?: boolean }} [opts] @returns {Promise<{ count: number } | any[]>} */
|
|
163
|
+
static async insertMany(rows, opts) {
|
|
164
|
+
return crud.insertMany(this, rows, opts)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 정확히 한 행 갱신(>1 매칭→롤백 후 model.multiple_matches, ADR-212 #3). 변경 행 수 반환. @param {Record<string, any>} filter @param {Record<string, any>} patch @returns {Promise<number>} */
|
|
168
|
+
static async updateOne(filter, patch) {
|
|
169
|
+
return crud.updateOne(this, filter, patch)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 다건 갱신. 빈 filter 는 차단 — 전체 갱신은 `{ all: true }`. @param {Record<string, any>} filter @param {Record<string, any>} patch @param {{ all?: boolean }} [opts] @returns {Promise<number>} */
|
|
173
|
+
static async updateMany(filter, patch, opts) {
|
|
174
|
+
return crud.updateMany(this, filter, patch, opts)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 정확히 한 행 삭제(>1 매칭→롤백 후 model.multiple_matches). 삭제 행 수 반환. @param {Record<string, any>} filter @returns {Promise<number>} */
|
|
178
|
+
static async deleteOne(filter) {
|
|
179
|
+
return crud.deleteOne(this, filter)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 다건 삭제. 빈 filter 는 차단 — 전체 삭제는 `{ all: true }`. @param {Record<string, any>} filter @param {{ all?: boolean }} [opts] @returns {Promise<number>} */
|
|
183
|
+
static async deleteMany(filter, opts) {
|
|
184
|
+
return crud.deleteMany(this, filter, opts)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** upsert(INSERT … ON CONFLICT/DUPLICATE KEY UPDATE). `opts.conflict` 는 PK/unique 컬럼 필수. @param {Record<string, any>} data @param {{ conflict: string[], update?: string[], returning?: boolean }} opts @returns {Promise<any>} */
|
|
188
|
+
static async upsert(data, opts) {
|
|
189
|
+
return crud.upsert(this, data, opts)
|
|
190
|
+
}
|
|
191
|
+
|
|
112
192
|
/**
|
|
113
193
|
* `adapter`/`table` 정합성을 검증하고 글로벌 매니저에서 어댑터(MegaDbAdapter)를 잡아 반환한다.
|
|
114
194
|
*
|