mega-framework 0.1.0
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/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* MegaHubLink — bridge(=mega server) 측 hub 연결 클라이언트 (ADR-033/059/097/098).
|
|
4
|
+
*
|
|
5
|
+
* bridge 가 `mega ws-hub`(Hub) 에 **plaintext + Bearer** WebSocket 으로 붙어 12-타입 프로토콜
|
|
6
|
+
* (lib/hub-protocol.js)을 주고받는다. ASP 는 클라↔bridge 구간에서만 종단하므로(ADR-059) 이
|
|
7
|
+
* 링크엔 암호화가 없다 — 인증은 Bearer 토큰(핸드셰이크 헤더 + REGISTER payload).
|
|
8
|
+
*
|
|
9
|
+
* # 라이프사이클
|
|
10
|
+
* connect() → ws open → `hub.register` 송신 → hub 가 `hub.register_ok` 응답 → resolve.
|
|
11
|
+
* register 이후 heartbeat 루프 시작(`heartbeatMs` 주기, application-level, ADR-033). 수신
|
|
12
|
+
* 메시지는 type 별 핸들러로 디스패치({@link MegaHubLink#on}).
|
|
13
|
+
*
|
|
14
|
+
* # 재연결 (ADR-098)
|
|
15
|
+
* `retry` 옵션을 주면 (1) 최초 connect() 실패 시 지수 백오프 재시도, (2) **등록 후 연결이
|
|
16
|
+
* 끊기면**(hub 재시작·drain 4503·네트워크 단절) 자동으로 재연결을 시도한다({@link MegaRetry}).
|
|
17
|
+
* 재연결 성공 시 {@link MegaHubLink.EVENTS}.RECONNECTED 이벤트를 emit 하므로, 상위(MegaApp)가
|
|
18
|
+
* presence(JOIN)를 재동기화할 수 있다 — hub 는 절단 시점 presence 를 잃기 때문(at-most-once).
|
|
19
|
+
* `retry` 미지정 시 본래 동작 — 끊기면 통지만 하고 재연결하지 않는다.
|
|
20
|
+
*
|
|
21
|
+
* # fail-closed (ADR-084)
|
|
22
|
+
* register 전 hub.error / close → connect() reject. 수신 프레임이 hub 프로토콜 위반이면
|
|
23
|
+
* silent 무시 금지 — warn 로그 후 드롭(연결은 유지, 비치명적).
|
|
24
|
+
*
|
|
25
|
+
* @module core/hub-link
|
|
26
|
+
*/
|
|
27
|
+
import { WebSocket } from 'ws'
|
|
28
|
+
import {
|
|
29
|
+
HUB_MESSAGE_TYPES,
|
|
30
|
+
CLOSE_CODE_DRAIN,
|
|
31
|
+
createHubMessage,
|
|
32
|
+
parseHubMessage,
|
|
33
|
+
} from '../lib/hub-protocol.js'
|
|
34
|
+
import { withRetry } from '../lib/mega-retry.js'
|
|
35
|
+
import { buildPerMessageDeflate } from './ws-compression.js'
|
|
36
|
+
|
|
37
|
+
const T = HUB_MESSAGE_TYPES
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 재시도해도 결과가 같은 치명적 등록 실패(인증 거부 등) — `fatal` 마킹으로 백오프를 즉시 멈춘다.
|
|
41
|
+
* 일시적 실패(네트워크·타임아웃)는 일반 Error 라 재시도된다.
|
|
42
|
+
*/
|
|
43
|
+
class HubRegistrationError extends Error {
|
|
44
|
+
/** @param {string} message */
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message)
|
|
47
|
+
this.name = 'HubRegistrationError'
|
|
48
|
+
/** @type {boolean} shouldRetry 가 false 를 반환하게 하는 마커. */
|
|
49
|
+
this.fatal = true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** withRetry 공통 shouldRetry — fatal(인증 거부 등) 이면 재시도 중단. @param {{error: any}} ctx */
|
|
54
|
+
const retryUnlessFatal = (ctx) => !ctx?.error?.fatal
|
|
55
|
+
|
|
56
|
+
export class MegaHubLink {
|
|
57
|
+
/**
|
|
58
|
+
* 링크 라이프사이클 이벤트 (wire 타입과 충돌하지 않도록 `link.*` reserved). {@link MegaHubLink#on}
|
|
59
|
+
* 으로 구독한다. **핸들러 인자는 이벤트마다 다르다**(L-2):
|
|
60
|
+
* - `RECONNECTED` → `{ hubId: string }` — 재연결 성공(새 등록 완료). hubId 는 새 hub 의 식별자.
|
|
61
|
+
* - `DISCONNECTED` → `{ code: number, reason: string, isDrain: boolean }` — 등록 후 절단.
|
|
62
|
+
* `isDrain` 은 close code 가 4503(DRAIN)인지(다른 hub 로 회전 신호, ADR-098).
|
|
63
|
+
* - `RECONNECT_FAILED`→ `{ error: Error }` — 재연결 재시도 소진(더 이상 자동 재시도 없음).
|
|
64
|
+
* @type {Readonly<{ RECONNECTED: string, DISCONNECTED: string, RECONNECT_FAILED: string }>}
|
|
65
|
+
*/
|
|
66
|
+
static EVENTS = Object.freeze({
|
|
67
|
+
RECONNECTED: 'link.reconnected',
|
|
68
|
+
DISCONNECTED: 'link.disconnected',
|
|
69
|
+
RECONNECT_FAILED: 'link.reconnect_failed',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {Object} [opts]
|
|
74
|
+
* @param {string} [opts.url] - hub URL (예: 'ws://hub1.internal:19991/_hub'). 누락 시 throw.
|
|
75
|
+
* @param {string} [opts.token] - Bearer 토큰 (wsHub.acceptedTokens 중 하나). 누락 시 throw.
|
|
76
|
+
* @param {string} [opts.bridgeId] - 운영 식별자 (예: 'main-1'). 누락 시 throw.
|
|
77
|
+
* @param {string} [opts.instanceId] - REGISTER instanceId. 기본 bridgeId.
|
|
78
|
+
* @param {string[]} [opts.capabilities] - 선언 capability 목록. 기본 [].
|
|
79
|
+
* @param {import('../lib/mega-retry.js').MegaRetryOptions} [opts.retry] - 지정 시 재연결 활성(ADR-098).
|
|
80
|
+
* `{ retries, minTimeout, maxTimeout, factor, jitter }`. 미지정 → 재연결 끔(기본 동작).
|
|
81
|
+
* @param {number} [opts.connectTimeoutMs=10000] - register_ok 미수신 시 attempt reject 까지(M4).
|
|
82
|
+
* @param {import('./ws-compression.js').WsCompressionConfig} [opts.compression] - Bridge↔Hub link
|
|
83
|
+
* per-message deflate 압축(ADR-078). Global `wsHub.compression` 블록과 동일
|
|
84
|
+
* 스키마 — hub 서버와 양쪽이 협상해야 활성(RFC 7692). 디폴트 OFF. 잘못된 값은 즉시 throw.
|
|
85
|
+
* @param {{ warn?: Function, debug?: Function, info?: Function, error?: Function }} [opts.logger]
|
|
86
|
+
* @param {typeof import('ws').WebSocket} [opts.WebSocketCtor] - 테스트 주입용 ws 생성자.
|
|
87
|
+
*/
|
|
88
|
+
constructor({ url, token, bridgeId, instanceId, capabilities, retry, connectTimeoutMs, compression, logger, WebSocketCtor } = {}) {
|
|
89
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
90
|
+
throw new Error('MegaHubLink: url is required (MegaBridgeHubConfig.url).')
|
|
91
|
+
}
|
|
92
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
93
|
+
throw new Error('MegaHubLink: token is required (Bearer, ADR-059).')
|
|
94
|
+
}
|
|
95
|
+
if (typeof bridgeId !== 'string' || bridgeId.length === 0) {
|
|
96
|
+
throw new Error('MegaHubLink: bridgeId is required.')
|
|
97
|
+
}
|
|
98
|
+
this._url = url
|
|
99
|
+
this._token = token
|
|
100
|
+
this._bridgeId = bridgeId
|
|
101
|
+
this._instanceId = typeof instanceId === 'string' && instanceId.length > 0 ? instanceId : bridgeId
|
|
102
|
+
this._capabilities = Array.isArray(capabilities) ? [...capabilities] : []
|
|
103
|
+
this._log = logger ?? null
|
|
104
|
+
this._WS = WebSocketCtor ?? WebSocket
|
|
105
|
+
/** 재연결 옵션 (없으면 재연결 비활성). @type {import('../lib/mega-retry.js').MegaRetryOptions | null} */
|
|
106
|
+
this._retry = retry && typeof retry === 'object' ? retry : null
|
|
107
|
+
/** 한 attempt 의 register_ok 대기 한도(M4). reconnect 에서도 동일 적용. @type {number} */
|
|
108
|
+
this._connectTimeoutMs = Number.isFinite(connectTimeoutMs) ? Number(connectTimeoutMs) : 10_000
|
|
109
|
+
// Bridge↔Hub link 압축(ADR-078). enabled=false → false. 잘못된 값은 생성자에서 즉시 throw.
|
|
110
|
+
/** @type {false | Object} ws 클라이언트 perMessageDeflate 로 전달(_connectOnce). */
|
|
111
|
+
this._perMessageDeflate = buildPerMessageDeflate(compression, 'wsHub.compression')
|
|
112
|
+
|
|
113
|
+
/** @type {import('ws').WebSocket | null} */
|
|
114
|
+
this._ws = null
|
|
115
|
+
/** @type {boolean} register_ok 수신 여부. */
|
|
116
|
+
this._isRegistered = false
|
|
117
|
+
/** @type {string | null} hub 가 부여한 hubId. */
|
|
118
|
+
this._hubId = null
|
|
119
|
+
/** @type {number} register_ok 의 heartbeatMs(0 이면 heartbeat 끔). */
|
|
120
|
+
this._heartbeatMs = 0
|
|
121
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
122
|
+
this._hbTimer = null
|
|
123
|
+
/** type(wire)/lifecycle event → 핸들러 집합. @type {Map<string, Set<Function>>} */
|
|
124
|
+
this._handlers = new Map()
|
|
125
|
+
/** close() 로 의도적으로 닫는 중인지 — true 면 자동 재연결하지 않는다. @type {boolean} */
|
|
126
|
+
this._isClosing = false
|
|
127
|
+
/** 재연결 진행 중 가드(중복 reconnect 방지). @type {boolean} */
|
|
128
|
+
this._isReconnecting = false
|
|
129
|
+
/** close() 가 진행 중인 재시도(백오프 대기)를 취소하기 위한 컨트롤러. @type {AbortController | null} */
|
|
130
|
+
this._abort = null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** hub 가 부여한 hubId (register 전엔 null). */
|
|
134
|
+
get hubId() {
|
|
135
|
+
return this._hubId
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** register_ok 를 받아 사용 가능한 상태인지. */
|
|
139
|
+
get isRegistered() {
|
|
140
|
+
return this._isRegistered
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 소켓이 OPEN(=1) 인지. */
|
|
144
|
+
get isOpen() {
|
|
145
|
+
return this._ws?.readyState === 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 재연결이 활성(retry 옵션 지정)인지. */
|
|
149
|
+
get canReconnect() {
|
|
150
|
+
return this._retry !== null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 수신 메시지/라이프사이클 핸들러 등록. 같은 type 에 여러 개 등록 가능(모두 호출).
|
|
155
|
+
* @param {string} wireType - {@link HUB_MESSAGE_TYPES} 의 값(`hub.broadcast` 등) 또는
|
|
156
|
+
* {@link MegaHubLink.EVENTS} 의 라이프사이클 이벤트(`link.*`).
|
|
157
|
+
* @param {(msg: Object) => void} handler
|
|
158
|
+
* @returns {this}
|
|
159
|
+
*/
|
|
160
|
+
on(wireType, handler) {
|
|
161
|
+
let set = this._handlers.get(wireType)
|
|
162
|
+
if (!set) {
|
|
163
|
+
set = new Set()
|
|
164
|
+
this._handlers.set(wireType, set)
|
|
165
|
+
}
|
|
166
|
+
set.add(handler)
|
|
167
|
+
return this
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* hub 연결 + REGISTER 핸드셰이크. register_ok 수신 시 resolve.
|
|
172
|
+
*
|
|
173
|
+
* `retry` 옵션이 있으면 최초 연결도 지수 백오프로 재시도한다(ADR-098). 없으면 단발 시도.
|
|
174
|
+
*
|
|
175
|
+
* @param {Object} [opts]
|
|
176
|
+
* @param {number} [opts.connectTimeoutMs] - register_ok 대기 한도 override(M4). **이 값은 인스턴스에
|
|
177
|
+
* 저장되어(`this._connectTimeoutMs`) 이후 자동 재연결 attempt 에도 동일하게 적용된다** — 이 한 번의
|
|
178
|
+
* connect() 에만 적용되는 것이 아니다(L-1). 재연결마다 같은 타임아웃을 쓰는 동작은 의도된 것이다.
|
|
179
|
+
* @returns {Promise<{ hubId: string, heartbeatMs: number }>}
|
|
180
|
+
* @throws register 전 에러/종료/타임아웃(재시도 소진 포함) 시 reject.
|
|
181
|
+
*/
|
|
182
|
+
async connect({ connectTimeoutMs } = {}) {
|
|
183
|
+
this._isClosing = false
|
|
184
|
+
const timeout = Number.isFinite(connectTimeoutMs) ? Number(connectTimeoutMs) : this._connectTimeoutMs
|
|
185
|
+
this._connectTimeoutMs = timeout
|
|
186
|
+
if (!this._retry) {
|
|
187
|
+
return this._connectOnce({ connectTimeoutMs: timeout })
|
|
188
|
+
}
|
|
189
|
+
// retry 활성 — 최초 연결도 백오프 재시도. AbortError(인증 실패 등) 는 재시도하지 않는다.
|
|
190
|
+
this._abort = new AbortController()
|
|
191
|
+
return withRetry(() => this._connectOnce({ connectTimeoutMs: timeout }), {
|
|
192
|
+
...this._retry,
|
|
193
|
+
signal: this._abort.signal,
|
|
194
|
+
shouldRetry: retryUnlessFatal,
|
|
195
|
+
onFailedAttempt: (ctx) => {
|
|
196
|
+
this._log?.warn?.(
|
|
197
|
+
{ bridgeId: this._bridgeId, attempt: ctx.attemptNumber, retriesLeft: ctx.retriesLeft },
|
|
198
|
+
'hub-link connect attempt failed — backing off',
|
|
199
|
+
)
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 단일 연결 시도 — ws open → REGISTER → register_ok. 한 attempt 의 Promise 를 돌려준다.
|
|
206
|
+
* 재시도/재연결은 호출부({@link MegaHubLink#connect}/{@link MegaHubLink#_reconnect})가 감싼다.
|
|
207
|
+
*
|
|
208
|
+
* @param {{ connectTimeoutMs: number }} args
|
|
209
|
+
* @returns {Promise<{ hubId: string, heartbeatMs: number }>}
|
|
210
|
+
* @private
|
|
211
|
+
*/
|
|
212
|
+
_connectOnce({ connectTimeoutMs }) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
// Bearer 는 핸드셰이크 Authorization 헤더 + REGISTER payload 양쪽에 싣는다(이중 안전).
|
|
215
|
+
// perMessageDeflate 는 ws 클라 기본이 true 이므로, 압축 OFF 면 명시적으로 false 를 넘겨
|
|
216
|
+
// 협상 자체를 끈다(ADR-078). enabled 면 build 한 옵션 객체를 그대로 전달.
|
|
217
|
+
const ws = new this._WS(this._url, /** @type {any} */ ({
|
|
218
|
+
headers: { authorization: `Bearer ${this._token}` },
|
|
219
|
+
perMessageDeflate: this._perMessageDeflate,
|
|
220
|
+
}))
|
|
221
|
+
this._ws = ws
|
|
222
|
+
let settled = false
|
|
223
|
+
/** 이 attempt 가 register_ok 로 resolve 됐는지 — 이후 close 가 "등록 후 절단"인지 판별. */
|
|
224
|
+
let didRegister = false
|
|
225
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
226
|
+
let timer = null
|
|
227
|
+
|
|
228
|
+
/** register 결과 1회 확정 — settle 시 타임아웃 타이머도 정리(M4). @param {(v:any)=>void} done @param {any} val */
|
|
229
|
+
const finish = (done, val) => {
|
|
230
|
+
if (settled) return
|
|
231
|
+
settled = true
|
|
232
|
+
if (timer) clearTimeout(timer)
|
|
233
|
+
done(val)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 등록 타임아웃 — hub 가 무응답이면 소켓 닫고 reject(M4). 프로세스 종료 막지 않게 unref.
|
|
237
|
+
if (connectTimeoutMs > 0) {
|
|
238
|
+
timer = setTimeout(() => {
|
|
239
|
+
this._log?.warn?.({ bridgeId: this._bridgeId, connectTimeoutMs }, 'hub-link register timeout')
|
|
240
|
+
// reject 먼저 확정(settled=true) — 이어지는 ws.close 의 동기 'close' 이벤트가 다른 사유로
|
|
241
|
+
// settle 하지 않게 한다. close 는 소켓 정리용(이미 settled 라 close 핸들러는 no-op).
|
|
242
|
+
finish(reject, new Error('hub.register_timeout'))
|
|
243
|
+
if (ws.readyState <= 1) ws.close(1000, 'register timeout')
|
|
244
|
+
}, connectTimeoutMs)
|
|
245
|
+
timer.unref?.()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
ws.on('open', () => {
|
|
249
|
+
this._log?.debug?.({ bridgeId: this._bridgeId, url: this._url }, 'hub-link open → REGISTER')
|
|
250
|
+
try {
|
|
251
|
+
this._sendEnvelope(
|
|
252
|
+
createHubMessage({
|
|
253
|
+
type: T.REGISTER,
|
|
254
|
+
payload: {
|
|
255
|
+
instanceId: this._instanceId,
|
|
256
|
+
token: this._token,
|
|
257
|
+
capabilities: this._capabilities,
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
)
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// REGISTER 송신 실패 = 연결 불가. fail-closed reject.
|
|
263
|
+
finish(reject, err instanceof Error ? err : new Error(String(err)))
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
ws.on('message', (data) => {
|
|
268
|
+
const frame = data.toString('utf8')
|
|
269
|
+
let msg
|
|
270
|
+
try {
|
|
271
|
+
msg = parseHubMessage(frame)
|
|
272
|
+
} catch (err) {
|
|
273
|
+
// hub 프로토콜 위반 프레임 — silent 금지. 드롭하되 연결은 유지(비치명적).
|
|
274
|
+
this._log?.warn?.({ err, bridgeId: this._bridgeId }, 'hub-link invalid frame dropped')
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
// register 핸드셰이크 단계: register_ok → resolve, error → reject.
|
|
278
|
+
if (!this._isRegistered) {
|
|
279
|
+
if (msg.type === T.REGISTER_OK) {
|
|
280
|
+
this._isRegistered = true
|
|
281
|
+
didRegister = true
|
|
282
|
+
this._hubId = msg.payload.hubId
|
|
283
|
+
this._heartbeatMs = msg.payload.heartbeatMs
|
|
284
|
+
this._startHeartbeat()
|
|
285
|
+
this._log?.info?.({ bridgeId: this._bridgeId, hubId: this._hubId }, 'hub-link registered')
|
|
286
|
+
finish(resolve, { hubId: this._hubId, heartbeatMs: this._heartbeatMs })
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
if (msg.type === T.ERROR) {
|
|
290
|
+
const code = msg.error?.code ?? 'hub.error'
|
|
291
|
+
this._log?.warn?.({ bridgeId: this._bridgeId, code }, 'hub-link registration rejected')
|
|
292
|
+
// 인증 실패 등 등록 거부는 재시도해도 같은 결과 — fatal 마킹으로 백오프 중단(ADR-098).
|
|
293
|
+
finish(reject, new HubRegistrationError(`hub registration failed: ${code}`))
|
|
294
|
+
ws.close()
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
// register 전 다른 타입은 규약 위반 — 드롭(연결 유지). resolve/reject 는 register_ok/error 만.
|
|
298
|
+
this._log?.warn?.({ bridgeId: this._bridgeId, type: msg.type }, 'hub-link pre-register message ignored')
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
this._dispatch(msg)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
ws.on('error', (err) => {
|
|
305
|
+
if (!settled) {
|
|
306
|
+
finish(reject, err instanceof Error ? err : new Error(String(err)))
|
|
307
|
+
} else {
|
|
308
|
+
// 등록 후 소켓 에러 — 비치명적, close 가 뒤따른다. 로그만.
|
|
309
|
+
this._log?.warn?.({ err, bridgeId: this._bridgeId }, 'hub-link socket error')
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
ws.on('close', (code, reasonBuf) => {
|
|
314
|
+
this._stopHeartbeat()
|
|
315
|
+
const reason = Buffer.isBuffer(reasonBuf) ? reasonBuf.toString('utf8') : String(reasonBuf ?? '')
|
|
316
|
+
this._log?.debug?.({ bridgeId: this._bridgeId, code, reason }, 'hub-link closed')
|
|
317
|
+
if (!settled) {
|
|
318
|
+
// register 전 절단 — 이 attempt 실패. 재시도는 connect()/_reconnect 의 withRetry 가 처리.
|
|
319
|
+
finish(reject, new Error(`hub link closed before register (code ${code})`))
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
// 이미 settled. register_ok 로 resolve 된 뒤의 close = "등록 후 연결 절단".
|
|
323
|
+
if (didRegister) {
|
|
324
|
+
this._isRegistered = false
|
|
325
|
+
this._handleEstablishedClose(code, reason)
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 등록 후 연결이 끊겼을 때 처리 — DISCONNECTED emit + (retry 활성·의도적 close 아님) 자동 재연결.
|
|
333
|
+
* @param {number} code - WS close code (4503=drain 등).
|
|
334
|
+
* @param {string} reason
|
|
335
|
+
* @private
|
|
336
|
+
*/
|
|
337
|
+
_handleEstablishedClose(code, reason) {
|
|
338
|
+
const isDrain = code === CLOSE_CODE_DRAIN
|
|
339
|
+
this._log?.debug?.({ bridgeId: this._bridgeId, code, reason, isDrain }, 'hub-link disconnected')
|
|
340
|
+
this._emit(MegaHubLink.EVENTS.DISCONNECTED, { code, reason, isDrain })
|
|
341
|
+
if (this._retry && !this._isClosing) {
|
|
342
|
+
// drain(4503) 은 "다른 hub 로 재연결" 신호 — 현재는 같은 url 재시도(fallbackUrls 회전은 OQ).
|
|
343
|
+
void this._reconnect()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 자동 재연결 — 백오프로 _connectOnce 반복. 성공 시 RECONNECTED, 소진 시 RECONNECT_FAILED emit.
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
* @private
|
|
351
|
+
*/
|
|
352
|
+
async _reconnect() {
|
|
353
|
+
if (this._isReconnecting) return
|
|
354
|
+
this._isReconnecting = true
|
|
355
|
+
this._abort = new AbortController()
|
|
356
|
+
try {
|
|
357
|
+
await withRetry(() => this._connectOnce({ connectTimeoutMs: this._connectTimeoutMs }), {
|
|
358
|
+
.../** @type {any} */ (this._retry),
|
|
359
|
+
signal: this._abort.signal,
|
|
360
|
+
shouldRetry: retryUnlessFatal,
|
|
361
|
+
onFailedAttempt: (ctx) => {
|
|
362
|
+
this._log?.warn?.(
|
|
363
|
+
{ bridgeId: this._bridgeId, attempt: ctx.attemptNumber, retriesLeft: ctx.retriesLeft },
|
|
364
|
+
'hub-link reconnect attempt failed — backing off',
|
|
365
|
+
)
|
|
366
|
+
},
|
|
367
|
+
})
|
|
368
|
+
this._log?.info?.({ bridgeId: this._bridgeId, hubId: this._hubId }, 'hub-link reconnected')
|
|
369
|
+
this._emit(MegaHubLink.EVENTS.RECONNECTED, { hubId: this._hubId })
|
|
370
|
+
} catch (err) {
|
|
371
|
+
// 의도적 close 로 인한 abort 는 실패가 아님(조용히 종료). 그 외엔 재시도 소진 — 통지.
|
|
372
|
+
if (this._isClosing) {
|
|
373
|
+
this._log?.debug?.({ bridgeId: this._bridgeId }, 'hub-link reconnect aborted (closing)')
|
|
374
|
+
} else {
|
|
375
|
+
this._log?.error?.({ err, bridgeId: this._bridgeId }, 'hub-link reconnect exhausted')
|
|
376
|
+
this._emit(MegaHubLink.EVENTS.RECONNECT_FAILED, { error: err })
|
|
377
|
+
}
|
|
378
|
+
} finally {
|
|
379
|
+
this._isReconnecting = false
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 사용자 접속 알림 (JOIN). presence 등록.
|
|
385
|
+
* @param {{ userId: string, sessionId: string, channels: string[], metadata?: Object }} entry
|
|
386
|
+
* @returns {void}
|
|
387
|
+
*/
|
|
388
|
+
join(entry) {
|
|
389
|
+
this._send(T.JOIN, entry)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 단일 세션 종료 (LEAVE).
|
|
394
|
+
* @param {string} sessionId
|
|
395
|
+
* @returns {void}
|
|
396
|
+
*/
|
|
397
|
+
leave(sessionId) {
|
|
398
|
+
this._send(T.LEAVE, { sessionId })
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 다수 세션 일괄 종료 (BULK_LEAVE) — 여러 세션의 presence LEAVE 를 한 프레임으로 보낸다.
|
|
403
|
+
*
|
|
404
|
+
* 공개 API 다(프레임워크 내부는 현재 hub 의 bridge-gone 자동 감지에 의존해 직접 호출하지 않음).
|
|
405
|
+
* 다음 같은 **대량 정리** 상황에서 애플리케이션이 직접 호출한다:
|
|
406
|
+
* - 대량 logout(한 사용자의 여러 세션을 한 번에 종료),
|
|
407
|
+
* - 클라이언트 풀/탭 정리(연결 다수를 묶어 종료),
|
|
408
|
+
* - bridge 가 자체 drain 으로 보유 세션을 능동적으로 비울 때(소켓 close 를 기다리지 않고 선통지).
|
|
409
|
+
* 세션 1건이면 {@link MegaHubLink#leave} 를 쓰는 편이 단순하다.
|
|
410
|
+
*
|
|
411
|
+
* @param {string[]} sessionIds
|
|
412
|
+
* @returns {void}
|
|
413
|
+
*/
|
|
414
|
+
bulkLeave(sessionIds) {
|
|
415
|
+
this._send(T.BULK_LEAVE, { sessionIds })
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 채널 전체 푸시 (BROADCAST). hub 가 가입 bridge 들에 fan-out.
|
|
420
|
+
* @param {{ ns: string, channel: string, message: Object, exceptSessionIds?: string[] }} payload
|
|
421
|
+
* @returns {void}
|
|
422
|
+
*/
|
|
423
|
+
broadcast(payload) {
|
|
424
|
+
this._send(T.BROADCAST, payload)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 특정 사용자에게 직접 (DIRECT). hub 가 userId→sessionIds[] fan-out (ADR-035).
|
|
429
|
+
* @param {{ userId: string, message: Object }} payload
|
|
430
|
+
* @returns {void}
|
|
431
|
+
*/
|
|
432
|
+
direct(payload) {
|
|
433
|
+
this._send(T.DIRECT, payload)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 세션 메타데이터 갱신 (METADATA) — 명시 필드만(ADR-059).
|
|
438
|
+
* @param {{ sessionId: string, metadata: Object }} payload
|
|
439
|
+
* @returns {void}
|
|
440
|
+
*/
|
|
441
|
+
updateMetadata(payload) {
|
|
442
|
+
this._send(T.METADATA, payload)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 바이너리 메타 프레임 (BINARY). raw bytes 후속 프레임 전송은 본 Step 미포함(envelope 한정).
|
|
447
|
+
* @param {{ ref: string, mimeType: string, bytes: number }} payload
|
|
448
|
+
* @returns {void}
|
|
449
|
+
*/
|
|
450
|
+
binary(payload) {
|
|
451
|
+
this._send(T.BINARY, payload)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** 연결 종료 — 의도적 close 표시 + 진행 중 재연결 취소 + heartbeat 정리 후 소켓 close. */
|
|
455
|
+
close() {
|
|
456
|
+
this._isClosing = true
|
|
457
|
+
// 진행 중인 재시도(백오프 대기/최초 connect)를 취소한다(ADR-098). signal 은 다음 시도를 막는다.
|
|
458
|
+
this._abort?.abort(new Error('hub-link closing'))
|
|
459
|
+
this._stopHeartbeat()
|
|
460
|
+
if (this._ws && this._ws.readyState <= 1) this._ws.close(1000, 'bridge closing')
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 타입별 송신 — register_ok 이후에만 허용. register 전 호출은 fail-closed throw.
|
|
465
|
+
* @param {string} type - wire type(`hub.*`).
|
|
466
|
+
* @param {Object} payload
|
|
467
|
+
* @returns {void}
|
|
468
|
+
* @private
|
|
469
|
+
*/
|
|
470
|
+
_send(type, payload) {
|
|
471
|
+
if (!this._isRegistered) {
|
|
472
|
+
throw new Error(`MegaHubLink: cannot send '${type}' before register_ok.`)
|
|
473
|
+
}
|
|
474
|
+
this._sendEnvelope(createHubMessage({ type, payload }))
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* envelope 직렬화 후 소켓 송신.
|
|
479
|
+
* @param {Object} envelope
|
|
480
|
+
* @returns {void}
|
|
481
|
+
* @private
|
|
482
|
+
*/
|
|
483
|
+
_sendEnvelope(envelope) {
|
|
484
|
+
if (this._ws?.readyState !== 1) {
|
|
485
|
+
throw new Error('MegaHubLink: socket is not open.')
|
|
486
|
+
}
|
|
487
|
+
this._ws.send(JSON.stringify(envelope))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 수신 메시지를 등록된 핸들러에 디스패치. 핸들러 throw 는 격리(다른 핸들러 보호).
|
|
492
|
+
* @param {import('../lib/hub-protocol.js').HubEnvelope} msg
|
|
493
|
+
* @returns {void}
|
|
494
|
+
* @private
|
|
495
|
+
*/
|
|
496
|
+
_dispatch(msg) {
|
|
497
|
+
const set = this._handlers.get(msg.type)
|
|
498
|
+
if (!set || set.size === 0) {
|
|
499
|
+
this._log?.debug?.({ type: msg.type }, 'hub-link no handler for type')
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
for (const fn of set) {
|
|
503
|
+
try {
|
|
504
|
+
fn(msg)
|
|
505
|
+
} catch (err) {
|
|
506
|
+
// 한 핸들러 throw 가 나머지 디스패치를 막지 않도록 격리 + 로그.
|
|
507
|
+
this._log?.warn?.({ err, type: msg.type }, 'hub-link handler threw')
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 라이프사이클 이벤트 emit (RECONNECTED 등). 디스패치와 같은 핸들러 맵을 쓰되 인자는 이벤트 데이터.
|
|
514
|
+
* @param {string} event - {@link MegaHubLink.EVENTS} 값.
|
|
515
|
+
* @param {Object} data
|
|
516
|
+
* @returns {void}
|
|
517
|
+
* @private
|
|
518
|
+
*/
|
|
519
|
+
_emit(event, data) {
|
|
520
|
+
const set = this._handlers.get(event)
|
|
521
|
+
if (!set) return
|
|
522
|
+
for (const fn of set) {
|
|
523
|
+
try {
|
|
524
|
+
fn(data)
|
|
525
|
+
} catch (err) {
|
|
526
|
+
this._log?.warn?.({ err, event }, 'hub-link lifecycle handler threw')
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** heartbeat 루프 시작 (heartbeatMs>0 일 때만). 프로세스 종료 막지 않게 unref. @private */
|
|
532
|
+
_startHeartbeat() {
|
|
533
|
+
if (this._heartbeatMs <= 0) return
|
|
534
|
+
this._hbTimer = setInterval(() => {
|
|
535
|
+
try {
|
|
536
|
+
this._sendEnvelope(createHubMessage({ type: T.HEARTBEAT, payload: { at: Date.now() } }))
|
|
537
|
+
} catch (err) {
|
|
538
|
+
// 송신 실패 = 소켓 닫히는 중. 비치명적 — close 핸들러가 정리(이유+로그).
|
|
539
|
+
this._log?.debug?.({ err, bridgeId: this._bridgeId }, 'hub-link heartbeat send failed')
|
|
540
|
+
}
|
|
541
|
+
}, this._heartbeatMs)
|
|
542
|
+
this._hbTimer.unref?.()
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** heartbeat 루프 정지. @private */
|
|
546
|
+
_stopHeartbeat() {
|
|
547
|
+
if (this._hbTimer) {
|
|
548
|
+
clearInterval(this._hbTimer)
|
|
549
|
+
this._hbTimer = null
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|