sonamu 0.11.16 → 0.13.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/dist/_virtual/_rolldown/runtime.js +1 -2
- package/dist/ai/agents/agent.js +2 -3
- package/dist/ai/agents/index.js +1 -2
- package/dist/ai/index.js +1 -2
- package/dist/ai/providers/rtzr/api.js +1 -2
- package/dist/ai/providers/rtzr/error.js +2 -3
- package/dist/ai/providers/rtzr/model.js +1 -2
- package/dist/ai/providers/rtzr/options.js +1 -2
- package/dist/ai/providers/rtzr/provider.js +1 -2
- package/dist/ai/providers/rtzr/utils.js +1 -2
- package/dist/api/base-frame.js +4 -4
- package/dist/api/caster.d.ts +1 -1
- package/dist/api/caster.d.ts.map +1 -1
- package/dist/api/caster.js +9 -9
- package/dist/api/code-converters.js +6 -5
- package/dist/api/config.d.ts +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +2 -3
- package/dist/api/context.d.ts +2 -3
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/context.js +2 -3
- package/dist/api/decorators.js +2 -3
- package/dist/api/http-validator.js +2 -3
- package/dist/api/index.js +8 -3
- package/dist/api/secret.js +1 -2
- package/dist/api/sonamu.d.ts +3 -5
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +30 -24
- package/dist/api/validator.js +1 -2
- package/dist/api/websocket-helpers.js +2 -3
- package/dist/auth/audit-log/builders.js +1 -2
- package/dist/auth/audit-log/client-ip.js +2 -3
- package/dist/auth/audit-log/events.js +1 -2
- package/dist/auth/audit-log/plugin.d.ts.map +1 -1
- package/dist/auth/audit-log/plugin.js +2 -3
- package/dist/auth/audit-log-ingestor.js +2 -3
- package/dist/auth/auth-generator.js +2 -3
- package/dist/auth/better-auth-entities.js +1 -2
- package/dist/auth/better-auth-request.js +2 -3
- package/dist/auth/index.js +2 -3
- package/dist/auth/knex-adapter.js +2 -3
- package/dist/auth/plugins/entity-definitions/admin.js +1 -2
- package/dist/auth/plugins/entity-definitions/anonymous.js +1 -2
- package/dist/auth/plugins/entity-definitions/api-key.js +1 -2
- package/dist/auth/plugins/entity-definitions/audit-log.js +1 -2
- package/dist/auth/plugins/entity-definitions/index.js +1 -2
- package/dist/auth/plugins/entity-definitions/jwt.js +1 -2
- package/dist/auth/plugins/entity-definitions/organization.js +1 -2
- package/dist/auth/plugins/entity-definitions/passkey.js +1 -2
- package/dist/auth/plugins/entity-definitions/phone-number.js +1 -2
- package/dist/auth/plugins/entity-definitions/sso.js +1 -2
- package/dist/auth/plugins/entity-definitions/two-factor.js +1 -2
- package/dist/auth/plugins/entity-definitions/username.js +1 -2
- package/dist/auth/plugins/wrappers/admin.d.ts +33 -32
- package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/admin.js +2 -3
- package/dist/auth/plugins/wrappers/anonymous.js +2 -3
- package/dist/auth/plugins/wrappers/api-key.js +2 -3
- package/dist/auth/plugins/wrappers/jwt.js +2 -3
- package/dist/auth/plugins/wrappers/organization.js +2 -3
- package/dist/auth/plugins/wrappers/passkey.js +2 -3
- package/dist/auth/plugins/wrappers/phone-number.d.ts +43 -1
- package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/phone-number.js +2 -3
- package/dist/auth/plugins/wrappers/sso.d.ts +138 -187
- package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/sso.js +1 -2
- package/dist/auth/plugins/wrappers/two-factor.d.ts +51 -10
- package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/two-factor.js +2 -3
- package/dist/auth/plugins/wrappers/username.d.ts +39 -5
- package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/username.js +2 -3
- package/dist/bin/build-config.js +5 -5
- package/dist/bin/compiler-policy.js +2 -3
- package/dist/bin/fixture.js +7 -5
- package/dist/bin/hmr-hook-register.js +1 -2
- package/dist/bin/migrate-targets.js +2 -3
- package/dist/bin/test-command.js +2 -3
- package/dist/bin/ts-loader-register.js +2 -3
- package/dist/bin/ts-loader-registration.js +2 -3
- package/dist/bin/typecheck-web.js +2 -3
- package/dist/cache/cache-manager.js +1 -2
- package/dist/cache/decorator.js +2 -3
- package/dist/cache/drivers.js +1 -2
- package/dist/cache/index.js +2 -3
- package/dist/cache-control/cache-control.js +1 -2
- package/dist/code-formatting/index.js +2 -3
- package/dist/code-formatting/oxfmt.js +2 -3
- package/dist/code-formatting/oxlint.js +2 -3
- package/dist/compress/compress.js +1 -2
- package/dist/compress/index.js +2 -2
- package/dist/compress/types.d.ts +1 -1
- package/dist/compress/types.d.ts.map +1 -1
- package/dist/cone/cone-generator.js +2 -3
- package/dist/database/_batch_update.js +1 -2
- package/dist/database/base-model.js +4 -4
- package/dist/database/db.js +4 -4
- package/dist/database/knex.js +2 -3
- package/dist/database/puri-wrapper.js +2 -3
- package/dist/database/puri.js +23 -22
- package/dist/database/puri.types.js +1 -2
- package/dist/database/transaction-context.js +1 -2
- package/dist/database/upsert-builder.js +2 -3
- package/dist/dict/en.js +2 -3
- package/dist/dict/index.js +3 -3
- package/dist/dict/ko.js +2 -3
- package/dist/dict/rc-keys.js +1 -2
- package/dist/dict/sd.js +2 -3
- package/dist/dict/sonamu-dictionary.js +7 -6
- package/dist/dict/utils.js +2 -3
- package/dist/entity/entity-manager.d.ts +66 -66
- package/dist/entity/entity-manager.js +4 -4
- package/dist/entity/entity-template-cone.js +2 -3
- package/dist/entity/entity.js +21 -18
- package/dist/env.js +1 -2
- package/dist/exceptions/error-handler.d.ts.map +1 -1
- package/dist/exceptions/error-handler.js +2 -3
- package/dist/exceptions/so-exceptions.js +2 -3
- package/dist/filter/index.js +3 -2
- package/dist/filter/types.js +1 -2
- package/dist/filter/utils.js +7 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -6
- package/dist/logger/category.js +2 -3
- package/dist/logger/configure.js +5 -5
- package/dist/migration/code-generation.js +23 -16
- package/dist/migration/index-where-predicate.js +2 -3
- package/dist/migration/migration-set.js +2 -3
- package/dist/migration/migrator.js +2 -3
- package/dist/migration/postgresql-schema-reader.js +6 -9
- package/dist/migration/slack-confirm.js +2 -3
- package/dist/naite/naite-reporter.js +2 -3
- package/dist/naite/naite.js +10 -8
- package/dist/runtime-type.js +2 -3
- package/dist/ssr/hmr-opt-out.js +1 -2
- package/dist/ssr/index.js +7 -4
- package/dist/ssr/registry.js +1 -2
- package/dist/ssr/renderer.js +2 -3
- package/dist/storage/base-file.js +1 -2
- package/dist/storage/buffered-file.js +2 -3
- package/dist/storage/drivers.js +1 -2
- package/dist/storage/index.js +2 -3
- package/dist/storage/s3-driver.js +1 -2
- package/dist/storage/storage-manager.js +2 -3
- package/dist/storage/uploaded-file.js +2 -3
- package/dist/stream/__tests__/ws-test-helpers.d.ts +108 -0
- package/dist/stream/__tests__/ws-test-helpers.d.ts.map +1 -0
- package/dist/stream/__tests__/ws-test-helpers.js +259 -0
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.d.ts.map +1 -1
- package/dist/stream/index.js +15 -4
- package/dist/stream/sse.js +1 -2
- package/dist/stream/ws-audience-resolver.d.ts +8 -5
- package/dist/stream/ws-audience-resolver.d.ts.map +1 -1
- package/dist/stream/ws-audience-resolver.js +17 -15
- package/dist/stream/ws-audience.js +1 -2
- package/dist/stream/ws-cluster-bus.d.ts +9 -4
- package/dist/stream/ws-cluster-bus.d.ts.map +1 -1
- package/dist/stream/ws-cluster-bus.js +2 -3
- package/dist/stream/ws-core.d.ts +10 -0
- package/dist/stream/ws-core.d.ts.map +1 -1
- package/dist/stream/ws-delivery.d.ts +30 -4
- package/dist/stream/ws-delivery.d.ts.map +1 -1
- package/dist/stream/ws-delivery.js +200 -54
- package/dist/stream/ws-frame.d.ts +15 -0
- package/dist/stream/ws-frame.d.ts.map +1 -0
- package/dist/stream/ws-frame.js +28 -0
- package/dist/stream/ws-local-connection-store.d.ts +4 -4
- package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
- package/dist/stream/ws-local-connection-store.js +3 -4
- package/dist/stream/ws-presence-index.d.ts +36 -0
- package/dist/stream/ws-presence-index.d.ts.map +1 -0
- package/dist/stream/ws-presence-index.js +176 -0
- package/dist/stream/ws-presence-store.d.ts +39 -39
- package/dist/stream/ws-presence-store.d.ts.map +1 -1
- package/dist/stream/ws-presence-store.js +23 -154
- package/dist/stream/ws-registry.d.ts +45 -17
- package/dist/stream/ws-registry.d.ts.map +1 -1
- package/dist/stream/ws-registry.js +170 -62
- package/dist/stream/ws-session-id.d.ts +14 -0
- package/dist/stream/ws-session-id.d.ts.map +1 -0
- package/dist/stream/ws-session-id.js +36 -0
- package/dist/stream/ws-telemetry-memory.js +3 -4
- package/dist/stream/ws-telemetry-trace.js +1 -2
- package/dist/stream/ws-telemetry.js +2 -3
- package/dist/stream/ws.d.ts +20 -10
- package/dist/stream/ws.d.ts.map +1 -1
- package/dist/stream/ws.js +60 -38
- package/dist/syncer/api-parser.js +2 -3
- package/dist/syncer/checksum.js +4 -4
- package/dist/syncer/code-generator.js +4 -4
- package/dist/syncer/entity-operations.js +2 -3
- package/dist/syncer/event-batcher.js +1 -2
- package/dist/syncer/file-copy.js +2 -3
- package/dist/syncer/file-patterns.js +2 -3
- package/dist/syncer/file-tracking.js +2 -3
- package/dist/syncer/filesystem-dependencies.js +1 -2
- package/dist/syncer/index.js +8 -2
- package/dist/syncer/module-loader.js +8 -6
- package/dist/syncer/syncer-actions.js +8 -7
- package/dist/syncer/syncer.js +13 -8
- package/dist/syncer/watcher.js +2 -3
- package/dist/tasks/decorator.js +1 -2
- package/dist/tasks/step-wrapper.js +2 -3
- package/dist/tasks/workflow-manager.js +2 -3
- package/dist/template/entity-converter.js +17 -16
- package/dist/template/helpers.js +2 -3
- package/dist/template/implementations/entity.template.js +2 -3
- package/dist/template/implementations/entry-server.template.js +2 -3
- package/dist/template/implementations/generated.template.js +2 -3
- package/dist/template/implementations/generated_http.template.js +6 -6
- package/dist/template/implementations/generated_sso.template.js +2 -3
- package/dist/template/implementations/http_validators.template.js +2 -3
- package/dist/template/implementations/init_types.template.js +2 -3
- package/dist/template/implementations/model.template.js +4 -4
- package/dist/template/implementations/model_test.template.js +2 -3
- package/dist/template/implementations/queries.template.js +2 -3
- package/dist/template/implementations/sd.template.js +2 -3
- package/dist/template/implementations/services.template.js +2 -3
- package/dist/template/implementations/view_form.template.js +12 -9
- package/dist/template/implementations/view_id_all_select.template.js +2 -3
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +19 -17
- package/dist/template/implementations/view_search_input.template.js +2 -3
- package/dist/template/index.js +9 -3
- package/dist/template/template-manager.js +2 -3
- package/dist/template/template-types.js +1 -2
- package/dist/template/template.js +2 -3
- package/dist/template/zod-converter.js +54 -47
- package/dist/testing/_relation-graph.js +2 -3
- package/dist/testing/bootstrap.js +2 -3
- package/dist/testing/data-explorer.js +2 -3
- package/dist/testing/dev-test-routes.js +2 -3
- package/dist/testing/dev-vitest-manager.js +15 -12
- package/dist/testing/faker-mappings.js +1 -2
- package/dist/testing/filesystem.js +2 -3
- package/dist/testing/fixture-generator.js +2 -3
- package/dist/testing/fixture-loader.js +1 -2
- package/dist/testing/fixture-manager.js +12 -9
- package/dist/testing/global-setup.js +5 -5
- package/dist/testing/index.js +6 -3
- package/dist/testing/naite-vitest-reporter.js +1 -2
- package/dist/testing/parallel-db-manager.js +2 -3
- package/dist/testing/vitest-helpers.js +2 -3
- package/dist/tooling/__tests__/helpers/sonamu-test-root.js +2 -3
- package/dist/tooling/cdd-service.js +1 -2
- package/dist/tooling/cli-tooling.js +1 -2
- package/dist/tooling/fixture-service.js +1 -2
- package/dist/tooling/index.js +1 -2
- package/dist/tooling/test-run-client.js +2 -3
- package/dist/tsdown.api.config.js +1 -2
- package/dist/types/types.js +2 -3
- package/dist/ui/ai-api.d.ts.map +1 -1
- package/dist/ui/ai-api.js +4 -5
- package/dist/ui/ai-client.d.ts +2 -2
- package/dist/ui/ai-client.d.ts.map +1 -1
- package/dist/ui/ai-client.js +9 -8
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +11 -8
- package/dist/ui/async-route.js +1 -2
- package/dist/ui/cdd-service.js +22 -21
- package/dist/ui/migrations-api.js +6 -5
- package/dist/ui-web/assets/index-QDMyy31K.js +180 -0
- package/dist/ui-web/assets/index-dV7akMFB.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/dist/utils/async-utils.js +1 -2
- package/dist/utils/class-name.js +1 -2
- package/dist/utils/console-util.js +1 -2
- package/dist/utils/controller.js +2 -3
- package/dist/utils/esm-utils.js +1 -2
- package/dist/utils/fs-utils.js +2 -3
- package/dist/utils/lodash-able.js +1 -2
- package/dist/utils/model.js +1 -2
- package/dist/utils/object-utils.js +2 -3
- package/dist/utils/path-utils.js +2 -3
- package/dist/utils/process-utils.js +2 -3
- package/dist/utils/runtime-value.js +1 -2
- package/dist/utils/sql-parser.js +2 -3
- package/dist/utils/type-utils.js +8 -8
- package/dist/utils/utils.js +2 -3
- package/dist/utils/zod-error.js +2 -3
- package/dist/vector/chunking.js +1 -2
- package/dist/vector/config.js +1 -2
- package/dist/vector/embedding.js +4 -4
- package/package.json +63 -63
- package/src/ai/agents/agent.ts +1 -1
- package/src/api/__tests__/sonamu.plugins.test.ts +11 -1
- package/src/api/__tests__/sonamu.websocket-routes.test.ts +234 -0
- package/src/api/__tests__/sonamu.websocket.test.ts +241 -24
- package/src/api/config.ts +1 -1
- package/src/api/context.ts +2 -8
- package/src/api/sonamu.ts +46 -28
- package/src/auth/audit-log/plugin.ts +2 -0
- package/src/compress/types.ts +1 -1
- package/src/exceptions/error-handler.ts +3 -2
- package/src/index.ts +1 -0
- package/src/stream/__tests__/ws-contracts.test.ts +483 -265
- package/src/stream/__tests__/ws-multinode.test.ts +326 -0
- package/src/stream/__tests__/ws-registry-async.test.ts +855 -0
- package/src/stream/__tests__/ws-test-helpers.ts +360 -0
- package/src/stream/__tests__/ws.test.ts +342 -145
- package/src/stream/index.ts +1 -0
- package/src/stream/ws-audience-resolver.ts +21 -19
- package/src/stream/ws-cluster-bus.ts +12 -4
- package/src/stream/ws-core.ts +14 -0
- package/src/stream/ws-delivery.ts +321 -97
- package/src/stream/ws-frame.ts +39 -0
- package/src/stream/ws-local-connection-store.ts +10 -9
- package/src/stream/ws-presence-index.ts +273 -0
- package/src/stream/ws-presence-store.ts +64 -284
- package/src/stream/ws-registry.ts +272 -85
- package/src/stream/ws-session-id.ts +39 -0
- package/src/stream/ws.ts +91 -50
- package/src/template/implementations/model.template.ts +3 -3
- package/src/template/implementations/view_list.template.ts +12 -13
- package/src/ui/ai-api.ts +4 -2
- package/src/ui/ai-client.ts +2 -2
- package/src/ui/api.ts +58 -62
- package/dist/ui-web/assets/index-BMdIyKXS.css +0 -1
- package/dist/ui-web/assets/index-LpE8u19O.js +0 -178
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { type z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { runtimeTypeOf } from "../runtime-type";
|
|
1
4
|
import { isFunctionValue } from "../utils/runtime-value";
|
|
2
5
|
import { WebSocketAudience, type WebSocketAudience as WebSocketAudienceSpec } from "./ws-audience";
|
|
3
6
|
import { WebSocketAudienceResolver } from "./ws-audience-resolver";
|
|
4
7
|
import { NoopWebSocketClusterBus, type WebSocketClusterBus } from "./ws-cluster-bus";
|
|
5
8
|
import {
|
|
6
9
|
type ManagedWebSocketConnection,
|
|
10
|
+
type MaybePromise,
|
|
11
|
+
type WebSocketFrameConnection,
|
|
7
12
|
type WebSocketRegistryStats,
|
|
8
13
|
type WebSocketRoomId,
|
|
9
14
|
type WebSocketUserId,
|
|
10
15
|
} from "./ws-core";
|
|
11
|
-
import { WebSocketDeliveryEngine } from "./ws-delivery";
|
|
16
|
+
import { WebSocketDeliveryEngine, type WebSocketRouteDefinition } from "./ws-delivery";
|
|
12
17
|
import { WebSocketLocalConnectionStore } from "./ws-local-connection-store";
|
|
18
|
+
import { WebSocketPresenceIndex } from "./ws-presence-index";
|
|
13
19
|
import {
|
|
14
20
|
InMemoryWebSocketPresenceStore,
|
|
15
21
|
type WebSocketPresenceStore,
|
|
@@ -29,6 +35,22 @@ export type {
|
|
|
29
35
|
WebSocketUserId,
|
|
30
36
|
};
|
|
31
37
|
export type WebSocketConnectionMeta = WebSocketSessionPresence;
|
|
38
|
+
export type WebSocketRoomMemberCount = {
|
|
39
|
+
count: number;
|
|
40
|
+
// store 조회에 실패해 노드 로컬 수로 대체했으면 true
|
|
41
|
+
partial: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 5000;
|
|
45
|
+
|
|
46
|
+
type StoreMutationOperation =
|
|
47
|
+
| "register"
|
|
48
|
+
| "activate"
|
|
49
|
+
| "unregister"
|
|
50
|
+
| "setUserId"
|
|
51
|
+
| "clearUserId"
|
|
52
|
+
| "join"
|
|
53
|
+
| "leave";
|
|
32
54
|
|
|
33
55
|
export type WebSocketRegistryOptions = {
|
|
34
56
|
// 분산 환경에서 노드 간 라우팅 식별자로 쓰이므로 반드시 호출 측(WebSocketRuntime)에서 결정해 넘긴다
|
|
@@ -36,38 +58,52 @@ export type WebSocketRegistryOptions = {
|
|
|
36
58
|
presenceStore?: WebSocketPresenceStore;
|
|
37
59
|
clusterBus?: WebSocketClusterBus;
|
|
38
60
|
telemetryController?: WebSocketTelemetryController;
|
|
61
|
+
// shutdown 시 진행 중인 store mutation과 publish를 기다리는 상한
|
|
62
|
+
drainTimeoutMs?: number;
|
|
39
63
|
};
|
|
40
64
|
|
|
65
|
+
/**
|
|
66
|
+
* 노드 로컬 presence를 메모리에 소유하고, 설정된 store에는 세션별 순서대로 replica를 반영한다.
|
|
67
|
+
* mutation과 publish는 호출 시점에 메모리를 동기로 갱신하고, store 실패는 telemetry로만 기록하며 reject하지 않는다.
|
|
68
|
+
*/
|
|
41
69
|
export class WebSocketRegistry {
|
|
42
70
|
readonly nodeId: string;
|
|
43
71
|
readonly localConnections = new WebSocketLocalConnectionStore();
|
|
44
72
|
readonly presenceStore: WebSocketPresenceStore;
|
|
45
73
|
readonly clusterBus: WebSocketClusterBus;
|
|
46
|
-
readonly audienceResolver: WebSocketAudienceResolver;
|
|
47
|
-
readonly deliveryEngine: WebSocketDeliveryEngine;
|
|
48
74
|
readonly telemetryController: WebSocketTelemetryController;
|
|
75
|
+
private readonly memory = new WebSocketPresenceIndex();
|
|
76
|
+
private readonly deliveryEngine: WebSocketDeliveryEngine;
|
|
77
|
+
private readonly drainTimeoutMs: number;
|
|
78
|
+
// 세션별 store mutation 큐의 꼬리. 같은 세션의 호출은 이 promise 뒤에 이어 붙어 호출 순서대로 store에 도달한다
|
|
79
|
+
private readonly mutationQueues = new Map<string, Promise<void>>();
|
|
49
80
|
|
|
50
81
|
constructor(options: WebSocketRegistryOptions) {
|
|
51
82
|
this.nodeId = options.nodeId;
|
|
52
83
|
this.presenceStore = options.presenceStore ?? new InMemoryWebSocketPresenceStore();
|
|
53
84
|
this.clusterBus = options.clusterBus ?? new NoopWebSocketClusterBus();
|
|
85
|
+
this.drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
86
|
+
assertMatchingNodeId("presenceStore", this.presenceStore.nodeId, this.nodeId);
|
|
87
|
+
assertMatchingNodeId("clusterBus", this.clusterBus.nodeId, this.nodeId);
|
|
54
88
|
this.telemetryController =
|
|
55
89
|
options.telemetryController ?? new NoopWebSocketTelemetryController();
|
|
56
|
-
this.audienceResolver = new WebSocketAudienceResolver({
|
|
57
|
-
nodeId: this.nodeId,
|
|
58
|
-
presenceStore: this.presenceStore,
|
|
59
|
-
});
|
|
60
90
|
this.deliveryEngine = new WebSocketDeliveryEngine({
|
|
61
91
|
nodeId: this.nodeId,
|
|
62
92
|
localConnections: this.localConnections,
|
|
63
|
-
|
|
93
|
+
resolveLocalSessionIds: (audience) =>
|
|
94
|
+
this.memory.queryAudience(audience).map((session) => session.sessionId),
|
|
95
|
+
audienceResolver: new WebSocketAudienceResolver({
|
|
96
|
+
nodeId: this.nodeId,
|
|
97
|
+
presenceStore: this.presenceStore,
|
|
98
|
+
}),
|
|
64
99
|
clusterBus: this.clusterBus,
|
|
65
100
|
telemetryController: this.telemetryController,
|
|
66
101
|
});
|
|
67
102
|
|
|
103
|
+
// telemetry는 노드 로컬 메모리만 집계한다. 클러스터 합계를 모든 노드가 보고하면 집계 시 중복되기 때문이다
|
|
68
104
|
this.telemetryController.registerMetricSource({
|
|
69
105
|
collect: (_now) => {
|
|
70
|
-
const stats = this.
|
|
106
|
+
const stats = this.memory.getStats();
|
|
71
107
|
const localSnapshot = this.localConnections.getTelemetrySnapshot();
|
|
72
108
|
return {
|
|
73
109
|
activeConnections: stats.totalConnections,
|
|
@@ -81,17 +117,31 @@ export class WebSocketRegistry {
|
|
|
81
117
|
});
|
|
82
118
|
}
|
|
83
119
|
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
registerRoute(routeKey: string, outEvents: z.ZodObject): void {
|
|
121
|
+
this.deliveryEngine.registerRoute(routeKey, outEvents);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
replaceRoutes(routes: WebSocketRouteDefinition[]): void {
|
|
125
|
+
this.deliveryEngine.replaceRoutes(routes);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 연결을 메모리에 즉시 등록하고 store.register가 끝나면 resolve한다.
|
|
130
|
+
* store가 실패해도 메모리 전용 등록으로 resolve한다.
|
|
131
|
+
*/
|
|
132
|
+
async register(
|
|
133
|
+
connection: WebSocketFrameConnection,
|
|
86
134
|
active: boolean = true,
|
|
87
|
-
): WebSocketConnectionMeta {
|
|
88
|
-
|
|
89
|
-
const meta = this.presenceStore.register({
|
|
135
|
+
): Promise<WebSocketConnectionMeta> {
|
|
136
|
+
const registration = {
|
|
90
137
|
sessionId: connection.id,
|
|
91
138
|
nodeId: this.nodeId,
|
|
92
139
|
namespace: connection.namespace,
|
|
93
140
|
active,
|
|
94
|
-
|
|
141
|
+
connectedAt: new Date(),
|
|
142
|
+
};
|
|
143
|
+
this.localConnections.register(connection);
|
|
144
|
+
this.memory.register(registration);
|
|
95
145
|
this.telemetryController.emit({
|
|
96
146
|
name: "ws.connection.registered",
|
|
97
147
|
level: "info",
|
|
@@ -108,16 +158,27 @@ export class WebSocketRegistry {
|
|
|
108
158
|
tags: { outcome: "registered", namespace: connection.namespace },
|
|
109
159
|
userId: connection.userId,
|
|
110
160
|
});
|
|
111
|
-
|
|
161
|
+
|
|
162
|
+
await this.enqueueStoreMutation(connection.id, "register", () =>
|
|
163
|
+
this.presenceStore.register(registration),
|
|
164
|
+
);
|
|
165
|
+
// 등록 도중 닫혔으면 메모리에서 이미 빠졌으므로 등록 시점 값을 돌려준다
|
|
166
|
+
return (
|
|
167
|
+
this.memory.getConnection(connection.id) ?? {
|
|
168
|
+
...registration,
|
|
169
|
+
rooms: new Set(),
|
|
170
|
+
lastSeenAt: registration.connectedAt,
|
|
171
|
+
}
|
|
172
|
+
);
|
|
112
173
|
}
|
|
113
174
|
|
|
114
|
-
activate(connectionId: string): void {
|
|
115
|
-
if (!this.
|
|
175
|
+
activate(connectionId: string): Promise<void> {
|
|
176
|
+
if (!this.memory.has(connectionId)) {
|
|
116
177
|
this.emitMutationSkipped(connectionId, { operation: "activate" });
|
|
117
|
-
return;
|
|
178
|
+
return Promise.resolve();
|
|
118
179
|
}
|
|
119
180
|
|
|
120
|
-
this.
|
|
181
|
+
this.memory.activate(connectionId);
|
|
121
182
|
const connection = this.localConnections.getConnection(connectionId);
|
|
122
183
|
this.telemetryController.emit({
|
|
123
184
|
name: "ws.connection.activated",
|
|
@@ -126,14 +187,16 @@ export class WebSocketRegistry {
|
|
|
126
187
|
userId: connection?.userId,
|
|
127
188
|
...getConnectionTelemetryContext(connection),
|
|
128
189
|
});
|
|
190
|
+
return this.enqueueStoreMutation(connectionId, "activate", () =>
|
|
191
|
+
this.presenceStore.activate(connectionId),
|
|
192
|
+
);
|
|
129
193
|
}
|
|
130
194
|
|
|
131
|
-
unregister(connectionId: string): void {
|
|
195
|
+
unregister(connectionId: string): Promise<void> {
|
|
132
196
|
const connection = this.localConnections.getConnection(connectionId);
|
|
133
197
|
const telemetryContext = getConnectionTelemetryContext(connection);
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const meta = this.presenceStore.unregister(connectionId);
|
|
198
|
+
const meta = this.memory.unregister(connectionId);
|
|
199
|
+
const userId = connection?.userId ?? formatUserId(meta?.userId);
|
|
137
200
|
this.localConnections.unregister(connectionId);
|
|
138
201
|
this.telemetryController.emit({
|
|
139
202
|
name: "ws.connection.unregistered",
|
|
@@ -143,50 +206,60 @@ export class WebSocketRegistry {
|
|
|
143
206
|
userId,
|
|
144
207
|
...telemetryContext,
|
|
145
208
|
});
|
|
146
|
-
if (meta) {
|
|
147
|
-
|
|
148
|
-
name: "sonamu.ws.connections",
|
|
149
|
-
kind: "counter",
|
|
150
|
-
value: 1,
|
|
151
|
-
unit: "1",
|
|
152
|
-
tags: { outcome: "unregistered", namespace: meta.namespace },
|
|
153
|
-
userId,
|
|
154
|
-
});
|
|
209
|
+
if (!meta) {
|
|
210
|
+
return Promise.resolve();
|
|
155
211
|
}
|
|
212
|
+
|
|
213
|
+
this.telemetryController.recordMetric({
|
|
214
|
+
name: "sonamu.ws.connections",
|
|
215
|
+
kind: "counter",
|
|
216
|
+
value: 1,
|
|
217
|
+
unit: "1",
|
|
218
|
+
tags: { outcome: "unregistered", namespace: meta.namespace },
|
|
219
|
+
userId,
|
|
220
|
+
});
|
|
221
|
+
// 같은 세션 큐에 이어 붙으므로 진행 중인 register보다 먼저 store에 도달하지 않는다
|
|
222
|
+
return this.enqueueStoreMutation(connectionId, "unregister", () =>
|
|
223
|
+
this.presenceStore.unregister(connectionId),
|
|
224
|
+
);
|
|
156
225
|
}
|
|
157
226
|
|
|
227
|
+
// lastSeenAt은 메모리만 갱신한다. store에는 마지막 mutation 시점 값이 남는다
|
|
158
228
|
touch(connectionId: string): void {
|
|
159
|
-
this.
|
|
229
|
+
this.memory.touch(connectionId);
|
|
160
230
|
}
|
|
161
231
|
|
|
162
|
-
setUserId(connectionId: string, userId: WebSocketUserId): void {
|
|
163
|
-
|
|
232
|
+
setUserId(connectionId: string, userId: WebSocketUserId): Promise<void> {
|
|
233
|
+
const meta = this.memory.getConnection(connectionId);
|
|
234
|
+
if (!meta) {
|
|
164
235
|
this.emitMutationSkipped(connectionId, { operation: "setUserId", userId });
|
|
165
|
-
return;
|
|
236
|
+
return Promise.resolve();
|
|
166
237
|
}
|
|
167
238
|
|
|
168
|
-
this.
|
|
169
|
-
const meta = this.presenceStore.getConnection(connectionId);
|
|
239
|
+
this.memory.setUserId(connectionId, userId);
|
|
170
240
|
this.telemetryController.emit({
|
|
171
241
|
name: "ws.user.bound",
|
|
172
242
|
level: "debug",
|
|
173
243
|
connectionId,
|
|
174
|
-
namespace: meta
|
|
244
|
+
namespace: meta.namespace,
|
|
175
245
|
userId: String(userId),
|
|
176
246
|
detail: { userId },
|
|
177
247
|
...getConnectionTelemetryContext(this.localConnections.getConnection(connectionId)),
|
|
178
248
|
});
|
|
249
|
+
return this.enqueueStoreMutation(connectionId, "setUserId", () =>
|
|
250
|
+
this.presenceStore.setUserId(connectionId, userId),
|
|
251
|
+
);
|
|
179
252
|
}
|
|
180
253
|
|
|
181
|
-
clearUserId(connectionId: string): void {
|
|
182
|
-
const meta = this.
|
|
254
|
+
clearUserId(connectionId: string): Promise<void> {
|
|
255
|
+
const meta = this.memory.getConnection(connectionId);
|
|
183
256
|
if (!meta) {
|
|
184
257
|
this.emitMutationSkipped(connectionId, { operation: "clearUserId" });
|
|
185
|
-
return;
|
|
258
|
+
return Promise.resolve();
|
|
186
259
|
}
|
|
187
260
|
|
|
188
261
|
const userId = meta.userId;
|
|
189
|
-
this.
|
|
262
|
+
this.memory.clearUserId(connectionId);
|
|
190
263
|
this.telemetryController.emit({
|
|
191
264
|
name: "ws.user.cleared",
|
|
192
265
|
level: "debug",
|
|
@@ -196,36 +269,42 @@ export class WebSocketRegistry {
|
|
|
196
269
|
detail: userId !== undefined ? { userId } : undefined,
|
|
197
270
|
...getConnectionTelemetryContext(this.localConnections.getConnection(connectionId)),
|
|
198
271
|
});
|
|
272
|
+
return this.enqueueStoreMutation(connectionId, "clearUserId", () =>
|
|
273
|
+
this.presenceStore.clearUserId(connectionId),
|
|
274
|
+
);
|
|
199
275
|
}
|
|
200
276
|
|
|
201
|
-
join(connectionId: string, roomId: WebSocketRoomId): void {
|
|
202
|
-
|
|
277
|
+
join(connectionId: string, roomId: WebSocketRoomId): Promise<void> {
|
|
278
|
+
const meta = this.memory.getConnection(connectionId);
|
|
279
|
+
if (!meta) {
|
|
203
280
|
this.emitMutationSkipped(connectionId, { operation: "join", roomId });
|
|
204
|
-
return;
|
|
281
|
+
return Promise.resolve();
|
|
205
282
|
}
|
|
206
283
|
|
|
207
|
-
this.
|
|
208
|
-
const meta = this.presenceStore.getConnection(connectionId);
|
|
284
|
+
this.memory.join(connectionId, roomId);
|
|
209
285
|
const connection = this.localConnections.getConnection(connectionId);
|
|
210
286
|
this.telemetryController.emit({
|
|
211
287
|
name: "ws.room.joined",
|
|
212
288
|
level: "debug",
|
|
213
289
|
connectionId,
|
|
214
|
-
namespace: meta
|
|
215
|
-
userId: connection?.userId ?? formatUserId(meta
|
|
290
|
+
namespace: meta.namespace,
|
|
291
|
+
userId: connection?.userId ?? formatUserId(meta.userId),
|
|
216
292
|
detail: { roomId },
|
|
217
293
|
...getConnectionTelemetryContext(connection),
|
|
218
294
|
});
|
|
295
|
+
return this.enqueueStoreMutation(connectionId, "join", () =>
|
|
296
|
+
this.presenceStore.join(connectionId, roomId),
|
|
297
|
+
);
|
|
219
298
|
}
|
|
220
299
|
|
|
221
|
-
leave(connectionId: string, roomId: WebSocketRoomId): void {
|
|
222
|
-
const meta = this.
|
|
300
|
+
leave(connectionId: string, roomId: WebSocketRoomId): Promise<void> {
|
|
301
|
+
const meta = this.memory.getConnection(connectionId);
|
|
223
302
|
if (!meta) {
|
|
224
303
|
this.emitMutationSkipped(connectionId, { operation: "leave", roomId });
|
|
225
|
-
return;
|
|
304
|
+
return Promise.resolve();
|
|
226
305
|
}
|
|
227
306
|
|
|
228
|
-
this.
|
|
307
|
+
this.memory.leave(connectionId, roomId);
|
|
229
308
|
const connection = this.localConnections.getConnection(connectionId);
|
|
230
309
|
this.telemetryController.emit({
|
|
231
310
|
name: "ws.room.left",
|
|
@@ -236,26 +315,13 @@ export class WebSocketRegistry {
|
|
|
236
315
|
detail: { roomId },
|
|
237
316
|
...getConnectionTelemetryContext(connection),
|
|
238
317
|
});
|
|
318
|
+
return this.enqueueStoreMutation(connectionId, "leave", () =>
|
|
319
|
+
this.presenceStore.leave(connectionId, roomId),
|
|
320
|
+
);
|
|
239
321
|
}
|
|
240
322
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
detail: {
|
|
244
|
-
operation: "activate" | "setUserId" | "clearUserId" | "join" | "leave";
|
|
245
|
-
roomId?: WebSocketRoomId;
|
|
246
|
-
userId?: WebSocketUserId;
|
|
247
|
-
},
|
|
248
|
-
): void {
|
|
249
|
-
this.telemetryController.emit({
|
|
250
|
-
name: "ws.registry.mutation.skipped",
|
|
251
|
-
level: "debug",
|
|
252
|
-
connectionId,
|
|
253
|
-
detail: { ...detail, reason: "connectionMissing" },
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
broadcast<Data>(event: string, data: Data, namespace?: string): void {
|
|
258
|
-
this.publishToAudience(WebSocketAudience.all(namespace), event, data);
|
|
323
|
+
broadcast<Data>(event: string, data: Data, namespace?: string): Promise<void> {
|
|
324
|
+
return this.publishToAudience(WebSocketAudience.all(namespace), event, data);
|
|
259
325
|
}
|
|
260
326
|
|
|
261
327
|
publishToRoom<Data>(
|
|
@@ -263,8 +329,8 @@ export class WebSocketRegistry {
|
|
|
263
329
|
event: string,
|
|
264
330
|
data: Data,
|
|
265
331
|
namespace?: string,
|
|
266
|
-
): void {
|
|
267
|
-
this.publishToAudience(WebSocketAudience.room(roomId, namespace), event, data);
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
return this.publishToAudience(WebSocketAudience.room(roomId, namespace), event, data);
|
|
268
334
|
}
|
|
269
335
|
|
|
270
336
|
publishToUser<Data>(
|
|
@@ -272,40 +338,161 @@ export class WebSocketRegistry {
|
|
|
272
338
|
event: string,
|
|
273
339
|
data: Data,
|
|
274
340
|
namespace?: string,
|
|
275
|
-
): void {
|
|
276
|
-
this.publishToAudience(WebSocketAudience.user(userId, namespace), event, data);
|
|
341
|
+
): Promise<void> {
|
|
342
|
+
return this.publishToAudience(WebSocketAudience.user(userId, namespace), event, data);
|
|
277
343
|
}
|
|
278
344
|
|
|
279
|
-
publishToAudience<Data>(
|
|
280
|
-
|
|
345
|
+
publishToAudience<Data>(
|
|
346
|
+
audience: WebSocketAudienceSpec,
|
|
347
|
+
event: string,
|
|
348
|
+
data: Data,
|
|
349
|
+
): Promise<void> {
|
|
350
|
+
return this.deliveryEngine.publishToAudience(audience, event, data);
|
|
281
351
|
}
|
|
282
352
|
|
|
353
|
+
// 아래 읽기 API는 노드 로컬 snapshot(복사본)이다
|
|
283
354
|
getConnection(connectionId: string): WebSocketConnectionMeta | undefined {
|
|
284
|
-
return this.
|
|
355
|
+
return this.memory.getConnection(connectionId);
|
|
285
356
|
}
|
|
286
357
|
|
|
287
358
|
getConnectionCount(namespace?: string): number {
|
|
288
|
-
return this.
|
|
359
|
+
return this.memory.getConnectionCount(namespace);
|
|
289
360
|
}
|
|
290
361
|
|
|
291
362
|
getRoomMembers(roomId: WebSocketRoomId, namespace?: string): WebSocketConnectionMeta[] {
|
|
292
|
-
return this.
|
|
363
|
+
return this.memory.getRoomMembers(roomId, namespace);
|
|
293
364
|
}
|
|
294
365
|
|
|
295
366
|
getStats(): WebSocketRegistryStats {
|
|
296
|
-
return this.
|
|
367
|
+
return this.memory.getStats();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 클러스터 전체의 활성 방 인원수. store가 실패하면 노드 로컬 수에 partial: true를 붙여 반환하며 reject하지 않는다.
|
|
372
|
+
*/
|
|
373
|
+
async getRoomMemberCount(
|
|
374
|
+
roomId: WebSocketRoomId,
|
|
375
|
+
namespace?: string,
|
|
376
|
+
): Promise<WebSocketRoomMemberCount> {
|
|
377
|
+
try {
|
|
378
|
+
const count = await callStore(() => this.presenceStore.getRoomMemberCount(roomId, namespace));
|
|
379
|
+
return { count, partial: false };
|
|
380
|
+
} catch (cause) {
|
|
381
|
+
this.telemetryController.emit({
|
|
382
|
+
name: "ws.registry.read.failed",
|
|
383
|
+
level: "warn",
|
|
384
|
+
detail: { operation: "getRoomMemberCount", roomId, errorType: describeError(cause) },
|
|
385
|
+
});
|
|
386
|
+
return { count: this.memory.getRoomMemberCount(roomId, namespace), partial: true };
|
|
387
|
+
}
|
|
297
388
|
}
|
|
298
389
|
|
|
299
390
|
closeAll(code?: number, reason?: string): void {
|
|
300
391
|
this.localConnections.closeAll(code, reason);
|
|
301
392
|
}
|
|
302
393
|
|
|
394
|
+
/**
|
|
395
|
+
* 로컬 연결을 닫고, 진행 중인 store mutation과 publish를 drainTimeoutMs까지 기다린 뒤 store와 bus를 종료한다.
|
|
396
|
+
*/
|
|
303
397
|
async shutdown(code?: number, reason?: string): Promise<void> {
|
|
304
398
|
this.closeAll(code, reason);
|
|
305
|
-
|
|
399
|
+
|
|
400
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
401
|
+
const deadline = new Promise<void>((resolve) => {
|
|
402
|
+
timer = setTimeout(resolve, this.drainTimeoutMs);
|
|
403
|
+
});
|
|
404
|
+
try {
|
|
405
|
+
await Promise.race([
|
|
406
|
+
Promise.all([...this.mutationQueues.values(), this.deliveryEngine.drainPublishes()]),
|
|
407
|
+
deadline,
|
|
408
|
+
]);
|
|
409
|
+
} finally {
|
|
410
|
+
clearTimeout(timer);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await callStore(() => this.presenceStore.shutdown?.());
|
|
415
|
+
} catch (cause) {
|
|
416
|
+
this.telemetryController.emit({
|
|
417
|
+
name: "ws.registry.shutdown.failed",
|
|
418
|
+
level: "error",
|
|
419
|
+
detail: { errorType: describeError(cause) },
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
// 구독 해제는 drain이 끝났으므로 즉시 포기할 수 있게 이미 끝난 deadline을 넘긴다
|
|
423
|
+
await this.deliveryEngine.shutdown(Promise.resolve());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 세션별 큐 뒤에 store 호출을 이어 붙인다. 실패는 telemetry로만 기록하고, 반환한 promise는 reject하지 않는다.
|
|
428
|
+
*/
|
|
429
|
+
private enqueueStoreMutation(
|
|
430
|
+
sessionId: string,
|
|
431
|
+
operation: StoreMutationOperation,
|
|
432
|
+
call: () => MaybePromise<void>,
|
|
433
|
+
): Promise<void> {
|
|
434
|
+
const previous = this.mutationQueues.get(sessionId) ?? Promise.resolve();
|
|
435
|
+
const next: Promise<void> = previous
|
|
436
|
+
.then(call)
|
|
437
|
+
.then(
|
|
438
|
+
() => undefined,
|
|
439
|
+
(cause: unknown) => {
|
|
440
|
+
this.telemetryController.emit({
|
|
441
|
+
name: "ws.registry.mutation.failed",
|
|
442
|
+
level: "warn",
|
|
443
|
+
connectionId: sessionId,
|
|
444
|
+
detail: { operation, errorType: describeError(cause) },
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
)
|
|
448
|
+
.then(() => {
|
|
449
|
+
// 마지막 mutation이 끝나면 큐를 비워 세션 수만큼 promise가 쌓이지 않게 한다
|
|
450
|
+
if (this.mutationQueues.get(sessionId) === next) {
|
|
451
|
+
this.mutationQueues.delete(sessionId);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
this.mutationQueues.set(sessionId, next);
|
|
455
|
+
return next;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private emitMutationSkipped(
|
|
459
|
+
connectionId: string,
|
|
460
|
+
detail: {
|
|
461
|
+
operation: "activate" | "setUserId" | "clearUserId" | "join" | "leave";
|
|
462
|
+
roomId?: WebSocketRoomId;
|
|
463
|
+
userId?: WebSocketUserId;
|
|
464
|
+
},
|
|
465
|
+
): void {
|
|
466
|
+
this.telemetryController.emit({
|
|
467
|
+
name: "ws.registry.mutation.skipped",
|
|
468
|
+
level: "debug",
|
|
469
|
+
connectionId,
|
|
470
|
+
detail: { ...detail, reason: "connectionMissing" },
|
|
471
|
+
});
|
|
306
472
|
}
|
|
307
473
|
}
|
|
308
474
|
|
|
475
|
+
// store 호출의 동기 throw를 rejection으로 바꾼다
|
|
476
|
+
function callStore<T>(call: () => MaybePromise<T>): Promise<T> {
|
|
477
|
+
return Promise.resolve().then(call);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function assertMatchingNodeId(
|
|
481
|
+
source: "presenceStore" | "clusterBus",
|
|
482
|
+
sourceNodeId: string | undefined,
|
|
483
|
+
nodeId: string,
|
|
484
|
+
): void {
|
|
485
|
+
if (sourceNodeId !== undefined && sourceNodeId !== nodeId) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`WebSocket ${source}.nodeId (${sourceNodeId}) must match the runtime nodeId (${nodeId}).`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function describeError(cause: unknown): string {
|
|
493
|
+
return cause instanceof Error ? cause.name : runtimeTypeOf(cause);
|
|
494
|
+
}
|
|
495
|
+
|
|
309
496
|
function getConnectionTelemetryContext(
|
|
310
497
|
connection: ManagedWebSocketConnection | undefined,
|
|
311
498
|
): WebSocketTelemetryConnectionContext {
|
|
@@ -316,7 +503,7 @@ function getConnectionTelemetryContext(
|
|
|
316
503
|
return connection.getTelemetryContext();
|
|
317
504
|
}
|
|
318
505
|
|
|
319
|
-
// telemetry record의 userId는 string 정규화 — presence
|
|
506
|
+
// telemetry record의 userId는 string 정규화 — presence는 number | string 그대로 보관함
|
|
320
507
|
function formatUserId(userId: WebSocketUserId | undefined): string | undefined {
|
|
321
508
|
return userId === undefined ? undefined : String(userId);
|
|
322
509
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
|
|
4
|
+
// 세션 ID는 `${nodeId}:${uuid}` 형식이다. uuid에는 ':'가 없으므로 마지막 ':'로 나누면
|
|
5
|
+
// 노드 ID에 ':' 같은 구분자가 들어 있어도 그대로 복원된다
|
|
6
|
+
const SESSION_ID_SEPARATOR = ":";
|
|
7
|
+
const SESSION_SUFFIX_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 기본 노드 ID를 만든다. 형식은 `${hostname()}-${process.pid}-${짧은 랜덤}`이다.
|
|
11
|
+
* 컨테이너 재시작 뒤 hostname과 pid가 같아도 이전 노드의 stale 상태를 물려받지 않도록 랜덤 접미사를 붙인다.
|
|
12
|
+
*/
|
|
13
|
+
export function createWebSocketNodeId(): string {
|
|
14
|
+
return `${hostname()}-${process.pid}-${randomBytes(6).toString("base64url")}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 노드 ID를 포함한 세션 ID를 만든다. `connections` audience는 이 ID만으로 대상 노드를 찾으므로 store 조회가 필요 없다.
|
|
19
|
+
*/
|
|
20
|
+
export function createWebSocketSessionId(nodeId: string): string {
|
|
21
|
+
return `${nodeId}${SESSION_ID_SEPARATOR}${randomUUID()}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 세션 ID에서 노드 ID를 복원한다. Sonamu가 만든 형식이 아니면 undefined를 반환한다.
|
|
26
|
+
*/
|
|
27
|
+
export function parseWebSocketSessionNodeId(sessionId: string): string | undefined {
|
|
28
|
+
const separatorIndex = sessionId.lastIndexOf(SESSION_ID_SEPARATOR);
|
|
29
|
+
if (separatorIndex < 0) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const suffix = sessionId.slice(separatorIndex + SESSION_ID_SEPARATOR.length);
|
|
34
|
+
if (!SESSION_SUFFIX_PATTERN.test(suffix)) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return sessionId.slice(0, separatorIndex);
|
|
39
|
+
}
|