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,383 +1,601 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
|
|
4
|
+
import { createWebSocketRuntime } from "../ws";
|
|
3
5
|
import { WebSocketAudience } from "../ws-audience";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
NoopWebSocketClusterBus,
|
|
7
|
-
type WebSocketClusterBus,
|
|
8
|
-
type WebSocketClusterEnvelope,
|
|
9
|
-
type WebSocketClusterEnvelopeHandler,
|
|
10
|
-
} from "../ws-cluster-bus";
|
|
11
|
-
import { type ManagedWebSocketConnection } from "../ws-core";
|
|
12
|
-
import { WebSocketDeliveryEngine } from "../ws-delivery";
|
|
13
|
-
import { WebSocketLocalConnectionStore } from "../ws-local-connection-store";
|
|
6
|
+
import { NoopWebSocketClusterBus, type WebSocketClusterEnvelope } from "../ws-cluster-bus";
|
|
14
7
|
import { InMemoryWebSocketPresenceStore } from "../ws-presence-store";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for (const handler of this.handlers) {
|
|
43
|
-
handler(envelope);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
shutdown(): void {
|
|
48
|
-
this.handlers.clear();
|
|
49
|
-
}
|
|
8
|
+
import {
|
|
9
|
+
FakeWebSocket,
|
|
10
|
+
RecordingClusterBus,
|
|
11
|
+
ScriptedPresenceStore,
|
|
12
|
+
asWebSocket,
|
|
13
|
+
flushAsync,
|
|
14
|
+
flushUntil,
|
|
15
|
+
} from "./ws-test-helpers";
|
|
16
|
+
|
|
17
|
+
const OutEvents = z.object({
|
|
18
|
+
onReady: z.object({
|
|
19
|
+
ok: z.boolean(),
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
const InEvents = z.object({});
|
|
23
|
+
|
|
24
|
+
async function registerSession(
|
|
25
|
+
store: InMemoryWebSocketPresenceStore,
|
|
26
|
+
input: { sessionId: string; nodeId: string; namespace: string; active?: boolean },
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
await store.register({
|
|
29
|
+
sessionId: input.sessionId,
|
|
30
|
+
nodeId: input.nodeId,
|
|
31
|
+
namespace: input.namespace,
|
|
32
|
+
active: input.active ?? true,
|
|
33
|
+
connectedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
34
|
+
});
|
|
50
35
|
}
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
37
|
+
function createEnvelope(input: {
|
|
38
|
+
id: string;
|
|
39
|
+
sourceNodeId?: string;
|
|
40
|
+
targetNodeIds?: string[];
|
|
41
|
+
audience?: WebSocketAudience;
|
|
42
|
+
event?: string;
|
|
43
|
+
frames: Record<string, string>;
|
|
44
|
+
}): WebSocketClusterEnvelope {
|
|
45
|
+
return {
|
|
46
|
+
id: input.id,
|
|
47
|
+
sourceNodeId: input.sourceNodeId ?? "node-b",
|
|
48
|
+
targetNodeIds: input.targetNodeIds ?? ["node-a"],
|
|
49
|
+
audience: input.audience ?? WebSocketAudience.all("chat"),
|
|
50
|
+
event: input.event ?? "onReady",
|
|
51
|
+
frames: input.frames,
|
|
52
|
+
emittedAt: Date.now(),
|
|
53
|
+
};
|
|
68
54
|
}
|
|
69
55
|
|
|
70
56
|
describe("WebSocket contract tests", () => {
|
|
71
|
-
describe("PresenceStore", () => {
|
|
72
|
-
it("
|
|
57
|
+
describe("PresenceStore (InMemoryWebSocketPresenceStore)", () => {
|
|
58
|
+
it("활성화 전의 세션은 라우팅과 방 인원수에서 제외한다", async () => {
|
|
73
59
|
const store = new InMemoryWebSocketPresenceStore();
|
|
74
|
-
store
|
|
60
|
+
await registerSession(store, {
|
|
75
61
|
sessionId: "s1",
|
|
76
62
|
nodeId: "node-a",
|
|
77
63
|
namespace: "chat",
|
|
78
64
|
active: false,
|
|
79
65
|
});
|
|
66
|
+
await store.join("s1", "room-1");
|
|
80
67
|
|
|
81
|
-
expect(store.
|
|
82
|
-
|
|
68
|
+
expect(await store.resolveRoute(WebSocketAudience.all("chat"), "node-a")).toEqual({
|
|
69
|
+
localSessionIds: [],
|
|
70
|
+
remoteNodeIds: [],
|
|
71
|
+
});
|
|
72
|
+
expect(await store.getRoomMemberCount("room-1", "chat")).toBe(0);
|
|
83
73
|
|
|
84
|
-
store.activate("s1");
|
|
74
|
+
await store.activate("s1");
|
|
85
75
|
|
|
86
|
-
expect(store.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
76
|
+
expect(await store.resolveRoute(WebSocketAudience.all("chat"), "node-a")).toEqual({
|
|
77
|
+
localSessionIds: ["s1"],
|
|
78
|
+
remoteNodeIds: [],
|
|
79
|
+
});
|
|
80
|
+
expect(await store.getRoomMemberCount("room-1", "chat")).toBe(1);
|
|
90
81
|
});
|
|
91
82
|
|
|
92
|
-
it("
|
|
83
|
+
it("방과 사용자 바인딩을 namespace별로 격리한다", async () => {
|
|
93
84
|
const store = new InMemoryWebSocketPresenceStore();
|
|
94
|
-
store
|
|
95
|
-
|
|
96
|
-
nodeId: "node-a",
|
|
97
|
-
namespace: "chat",
|
|
98
|
-
});
|
|
99
|
-
store.register({
|
|
100
|
-
sessionId: "admin-1",
|
|
101
|
-
nodeId: "node-a",
|
|
102
|
-
namespace: "admin",
|
|
103
|
-
});
|
|
85
|
+
await registerSession(store, { sessionId: "chat-1", nodeId: "node-a", namespace: "chat" });
|
|
86
|
+
await registerSession(store, { sessionId: "admin-1", nodeId: "node-a", namespace: "admin" });
|
|
104
87
|
|
|
105
|
-
store.join("chat-1", "room-1");
|
|
106
|
-
store.join("admin-1", "room-1");
|
|
107
|
-
store.setUserId("chat-1", "user-1");
|
|
108
|
-
store.setUserId("admin-1", "user-1");
|
|
88
|
+
await store.join("chat-1", "room-1");
|
|
89
|
+
await store.join("admin-1", "room-1");
|
|
90
|
+
await store.setUserId("chat-1", "user-1");
|
|
91
|
+
await store.setUserId("admin-1", "user-1");
|
|
109
92
|
|
|
110
93
|
expect(
|
|
111
|
-
store.
|
|
94
|
+
(await store.resolveRoute(WebSocketAudience.room("room-1", "chat"), "node-a"))
|
|
95
|
+
.localSessionIds,
|
|
112
96
|
).toEqual(["chat-1"]);
|
|
113
97
|
expect(
|
|
114
|
-
store
|
|
115
|
-
.
|
|
116
|
-
.map((meta) => meta.sessionId),
|
|
98
|
+
(await store.resolveRoute(WebSocketAudience.user("user-1", "admin"), "node-a"))
|
|
99
|
+
.localSessionIds,
|
|
117
100
|
).toEqual(["admin-1"]);
|
|
118
101
|
});
|
|
119
102
|
|
|
120
|
-
it("
|
|
103
|
+
it("세션을 해제하면 방과 사용자 바인딩도 정리한다", async () => {
|
|
121
104
|
const store = new InMemoryWebSocketPresenceStore();
|
|
122
|
-
store
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
namespace: "chat",
|
|
126
|
-
});
|
|
127
|
-
store.join("s1", "room-1");
|
|
128
|
-
store.setUserId("s1", "user-1");
|
|
105
|
+
await registerSession(store, { sessionId: "s1", nodeId: "node-a", namespace: "chat" });
|
|
106
|
+
await store.join("s1", "room-1");
|
|
107
|
+
await store.setUserId("s1", "user-1");
|
|
129
108
|
|
|
130
|
-
|
|
109
|
+
await store.unregister("s1");
|
|
131
110
|
|
|
132
111
|
expect(store.getConnection("s1")).toBeUndefined();
|
|
133
|
-
expect(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
112
|
+
expect(
|
|
113
|
+
(await store.resolveRoute(WebSocketAudience.room("room-1", "chat"), "node-a"))
|
|
114
|
+
.localSessionIds,
|
|
115
|
+
).toHaveLength(0);
|
|
116
|
+
expect(
|
|
117
|
+
(await store.resolveRoute(WebSocketAudience.user("user-1", "chat"), "node-a"))
|
|
118
|
+
.localSessionIds,
|
|
119
|
+
).toHaveLength(0);
|
|
120
|
+
expect(await store.getRoomMemberCount("room-1")).toBe(0);
|
|
139
121
|
});
|
|
140
122
|
|
|
141
|
-
it("
|
|
123
|
+
it("union audience의 중복 세션과 중복 노드를 제거한다", async () => {
|
|
142
124
|
const store = new InMemoryWebSocketPresenceStore();
|
|
143
|
-
store
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
store.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const metas = store.queryAudience(
|
|
125
|
+
await registerSession(store, { sessionId: "s1", nodeId: "node-a", namespace: "chat" });
|
|
126
|
+
await registerSession(store, { sessionId: "r1", nodeId: "node-b", namespace: "chat" });
|
|
127
|
+
await store.join("s1", "room-1");
|
|
128
|
+
await store.setUserId("s1", "user-1");
|
|
129
|
+
await store.join("r1", "room-1");
|
|
130
|
+
await store.setUserId("r1", "user-1");
|
|
131
|
+
|
|
132
|
+
const plan = await store.resolveRoute(
|
|
152
133
|
WebSocketAudience.union(
|
|
153
134
|
WebSocketAudience.room("room-1", "chat"),
|
|
154
135
|
WebSocketAudience.user("user-1", "chat"),
|
|
155
136
|
),
|
|
137
|
+
"node-a",
|
|
156
138
|
);
|
|
157
139
|
|
|
158
|
-
expect(
|
|
140
|
+
expect(plan).toEqual({ localSessionIds: ["s1"], remoteNodeIds: ["node-b"] });
|
|
159
141
|
});
|
|
160
|
-
});
|
|
161
142
|
|
|
162
|
-
|
|
163
|
-
it("splits local session ids from unique remote node ids", () => {
|
|
143
|
+
it("로컬 세션 ID와 중복 없는 원격 노드 ID를 나눠 반환하고 비활성 원격 세션은 제외한다", async () => {
|
|
164
144
|
const store = new InMemoryWebSocketPresenceStore();
|
|
165
|
-
store
|
|
166
|
-
|
|
167
|
-
|
|
145
|
+
await registerSession(store, { sessionId: "local-1", nodeId: "node-a", namespace: "chat" });
|
|
146
|
+
await registerSession(store, { sessionId: "remote-1", nodeId: "node-b", namespace: "chat" });
|
|
147
|
+
await registerSession(store, { sessionId: "remote-2", nodeId: "node-b", namespace: "chat" });
|
|
148
|
+
await registerSession(store, {
|
|
149
|
+
sessionId: "inactive-1",
|
|
150
|
+
nodeId: "node-c",
|
|
168
151
|
namespace: "chat",
|
|
152
|
+
active: false,
|
|
169
153
|
});
|
|
170
|
-
|
|
171
|
-
sessionId
|
|
172
|
-
|
|
173
|
-
|
|
154
|
+
for (const sessionId of ["local-1", "remote-1", "remote-2", "inactive-1"]) {
|
|
155
|
+
await store.join(sessionId, "room-1");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
expect(await store.resolveRoute(WebSocketAudience.room("room-1", "chat"), "node-a")).toEqual({
|
|
159
|
+
localSessionIds: ["local-1"],
|
|
160
|
+
remoteNodeIds: ["node-b"],
|
|
174
161
|
});
|
|
175
|
-
store
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
162
|
+
// 같은 store를 다른 노드 관점에서 조회하면 로컬/원격이 뒤바뀐다
|
|
163
|
+
const fromNodeB = await store.resolveRoute(
|
|
164
|
+
WebSocketAudience.room("room-1", "chat"),
|
|
165
|
+
"node-b",
|
|
166
|
+
);
|
|
167
|
+
expect([...fromNodeB.localSessionIds].sort()).toEqual(["remote-1", "remote-2"]);
|
|
168
|
+
expect(fromNodeB.remoteNodeIds).toEqual(["node-a"]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("namespace 없는 audience는 모든 namespace의 세션을 라우팅한다", async () => {
|
|
172
|
+
const store = new InMemoryWebSocketPresenceStore();
|
|
173
|
+
await registerSession(store, { sessionId: "chat-1", nodeId: "node-a", namespace: "chat" });
|
|
174
|
+
await registerSession(store, { sessionId: "admin-1", nodeId: "node-b", namespace: "admin" });
|
|
175
|
+
await store.join("chat-1", "room-1");
|
|
176
|
+
await store.join("admin-1", "room-1");
|
|
177
|
+
|
|
178
|
+
expect(await store.resolveRoute(WebSocketAudience.room("room-1"), "node-a")).toEqual({
|
|
179
|
+
localSessionIds: ["chat-1"],
|
|
180
|
+
remoteNodeIds: ["node-b"],
|
|
179
181
|
});
|
|
180
|
-
store.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const resolver = new WebSocketAudienceResolver({
|
|
184
|
-
nodeId: "node-a",
|
|
185
|
-
presenceStore: store,
|
|
182
|
+
expect(await store.resolveRoute(WebSocketAudience.all(), "node-a")).toEqual({
|
|
183
|
+
localSessionIds: ["chat-1"],
|
|
184
|
+
remoteNodeIds: ["node-b"],
|
|
186
185
|
});
|
|
186
|
+
});
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
it("getRoomMemberCount는 활성 세션만 namespace 유무에 따라 클러스터 전체에서 센다", async () => {
|
|
189
|
+
const store = new InMemoryWebSocketPresenceStore();
|
|
190
|
+
await registerSession(store, { sessionId: "a1", nodeId: "node-a", namespace: "chat" });
|
|
191
|
+
await registerSession(store, { sessionId: "b1", nodeId: "node-b", namespace: "chat" });
|
|
192
|
+
await registerSession(store, {
|
|
193
|
+
sessionId: "b2",
|
|
194
|
+
nodeId: "node-b",
|
|
195
|
+
namespace: "chat",
|
|
196
|
+
active: false,
|
|
191
197
|
});
|
|
198
|
+
await registerSession(store, { sessionId: "b3", nodeId: "node-b", namespace: "admin" });
|
|
199
|
+
for (const sessionId of ["a1", "b1", "b2", "b3"]) {
|
|
200
|
+
await store.join(sessionId, "room-1");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
expect(await store.getRoomMemberCount("room-1", "chat")).toBe(2);
|
|
204
|
+
expect(await store.getRoomMemberCount("room-1", "admin")).toBe(1);
|
|
205
|
+
expect(await store.getRoomMemberCount("room-1")).toBe(3);
|
|
206
|
+
expect(await store.getRoomMemberCount("room-404")).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('userId는 타입까지 일치해야 매칭된다 (1과 "1"은 다르다)', async () => {
|
|
210
|
+
const store = new InMemoryWebSocketPresenceStore();
|
|
211
|
+
await registerSession(store, { sessionId: "numeric", nodeId: "node-a", namespace: "chat" });
|
|
212
|
+
await registerSession(store, { sessionId: "string", nodeId: "node-a", namespace: "chat" });
|
|
213
|
+
await store.setUserId("numeric", 1);
|
|
214
|
+
await store.setUserId("string", "1");
|
|
215
|
+
|
|
216
|
+
expect(
|
|
217
|
+
(await store.resolveRoute(WebSocketAudience.user(1, "chat"), "node-a")).localSessionIds,
|
|
218
|
+
).toEqual(["numeric"]);
|
|
219
|
+
expect(
|
|
220
|
+
(await store.resolveRoute(WebSocketAudience.user("1", "chat"), "node-a")).localSessionIds,
|
|
221
|
+
).toEqual(["string"]);
|
|
222
|
+
expect(store.getConnection("numeric")?.userId).toBe(1);
|
|
223
|
+
expect(store.getConnection("string")?.userId).toBe("1");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("presence는 snapshot 복사본이라 반환값을 바꿔도 store 상태는 그대로다", async () => {
|
|
227
|
+
const store = new InMemoryWebSocketPresenceStore();
|
|
228
|
+
await registerSession(store, { sessionId: "s1", nodeId: "node-a", namespace: "chat" });
|
|
229
|
+
await store.join("s1", "room-1");
|
|
230
|
+
|
|
231
|
+
const snapshot = store.getConnection("s1");
|
|
232
|
+
expect(snapshot).toBeDefined();
|
|
233
|
+
snapshot!.rooms.add("room-hacked");
|
|
234
|
+
snapshot!.rooms.delete("room-1");
|
|
235
|
+
snapshot!.active = false;
|
|
236
|
+
snapshot!.userId = "intruder";
|
|
237
|
+
|
|
238
|
+
const fresh = store.getConnection("s1");
|
|
239
|
+
expect(fresh).not.toBe(snapshot);
|
|
240
|
+
expect([...fresh!.rooms]).toEqual(["room-1"]);
|
|
241
|
+
expect(fresh!.active).toBe(true);
|
|
242
|
+
expect(fresh!.userId).toBeUndefined();
|
|
243
|
+
expect(await store.getRoomMemberCount("room-hacked")).toBe(0);
|
|
244
|
+
expect(
|
|
245
|
+
(await store.resolveRoute(WebSocketAudience.user("intruder", "chat"), "node-a"))
|
|
246
|
+
.localSessionIds,
|
|
247
|
+
).toEqual([]);
|
|
248
|
+
expect(
|
|
249
|
+
(await store.resolveRoute(WebSocketAudience.room("room-1", "chat"), "node-a"))
|
|
250
|
+
.localSessionIds,
|
|
251
|
+
).toEqual(["s1"]);
|
|
192
252
|
});
|
|
193
253
|
});
|
|
194
254
|
|
|
195
255
|
describe("ClusterBus", () => {
|
|
196
|
-
it("
|
|
197
|
-
const bus = new
|
|
256
|
+
it("구독을 해제하면 이후 envelope을 받지 않는다", async () => {
|
|
257
|
+
const bus = new RecordingClusterBus();
|
|
198
258
|
const received: string[] = [];
|
|
199
|
-
const unsubscribe = bus.subscribe((envelope) => {
|
|
259
|
+
const unsubscribe = await bus.subscribe((envelope) => {
|
|
200
260
|
received.push(envelope.id);
|
|
201
261
|
});
|
|
202
262
|
|
|
203
|
-
bus.emit(createEnvelope("e1"));
|
|
263
|
+
await bus.emit(createEnvelope({ id: "e1", frames: {} }));
|
|
204
264
|
unsubscribe();
|
|
205
|
-
bus.emit(createEnvelope("e2"));
|
|
265
|
+
await bus.emit(createEnvelope({ id: "e2", frames: {} }));
|
|
206
266
|
|
|
207
267
|
expect(received).toEqual(["e1"]);
|
|
208
268
|
});
|
|
209
269
|
|
|
210
|
-
it("
|
|
270
|
+
it("NoopClusterBus의 publish, subscribe, shutdown은 안전하다", async () => {
|
|
211
271
|
const bus = new NoopWebSocketClusterBus();
|
|
212
|
-
const unsubscribe = bus.subscribe(() => {
|
|
272
|
+
const unsubscribe = await bus.subscribe(() => {
|
|
213
273
|
throw new Error("noop bus must not retain handlers");
|
|
214
274
|
});
|
|
215
275
|
|
|
216
|
-
await Promise.resolve(bus.publish(createEnvelope("noop")));
|
|
276
|
+
await Promise.resolve(bus.publish(createEnvelope({ id: "noop", frames: {} })));
|
|
217
277
|
expect(unsubscribe()).toBeUndefined();
|
|
218
278
|
await Promise.resolve(bus.shutdown());
|
|
219
279
|
});
|
|
220
|
-
});
|
|
221
280
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
281
|
+
it("subscribe가 Promise<unsubscribe>를 반환해도 runtime이 envelope을 받고 shutdown에서 해제한다", async () => {
|
|
282
|
+
const bus = new RecordingClusterBus();
|
|
283
|
+
let unsubscribeCalls = 0;
|
|
284
|
+
bus.subscribe = async (handler) => {
|
|
285
|
+
bus.handlers.add(handler);
|
|
286
|
+
return () => {
|
|
287
|
+
unsubscribeCalls += 1;
|
|
288
|
+
bus.handlers.delete(handler);
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
const runtime = createWebSocketRuntime({ nodeId: "node-a", clusterBus: bus });
|
|
292
|
+
runtime.registerRoute("/chat", { outEvents: OutEvents });
|
|
293
|
+
const socket = new FakeWebSocket();
|
|
294
|
+
await runtime.registerConnection(asWebSocket(socket), {
|
|
295
|
+
routeKey: "/chat",
|
|
296
|
+
outEvents: OutEvents,
|
|
297
|
+
inEvents: InEvents,
|
|
232
298
|
namespace: "chat",
|
|
299
|
+
heartbeat: 0,
|
|
233
300
|
});
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
301
|
+
await flushUntil(() => bus.handlers.size > 0);
|
|
302
|
+
|
|
303
|
+
const frame = JSON.stringify({ event: "onReady", data: { ok: true } });
|
|
304
|
+
await bus.emit(createEnvelope({ id: "remote", frames: { "/chat": frame } }));
|
|
305
|
+
await flushUntil(() => socket.sentMessages.length > 0);
|
|
306
|
+
|
|
307
|
+
expect(socket.sentMessages).toEqual([frame]);
|
|
240
308
|
|
|
241
|
-
|
|
242
|
-
|
|
309
|
+
await runtime.shutdown();
|
|
310
|
+
|
|
311
|
+
expect(unsubscribeCalls).toBe(1);
|
|
312
|
+
expect(bus.shutdownCalls).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("Delivery", () => {
|
|
317
|
+
it("로컬 전달은 큐를 거쳐 비동기로 전송된다", async () => {
|
|
318
|
+
const runtime = createWebSocketRuntime({ nodeId: "node-a" });
|
|
319
|
+
const socket = new FakeWebSocket();
|
|
320
|
+
await runtime.registerConnection(asWebSocket(socket), {
|
|
321
|
+
outEvents: OutEvents,
|
|
322
|
+
inEvents: InEvents,
|
|
323
|
+
namespace: "chat",
|
|
324
|
+
heartbeat: 0,
|
|
243
325
|
});
|
|
244
326
|
|
|
245
|
-
|
|
327
|
+
const publishing = runtime.broadcast("onReady", { ok: true }, "chat");
|
|
246
328
|
|
|
247
|
-
|
|
329
|
+
expect(socket.sentMessages).toHaveLength(0);
|
|
248
330
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
},
|
|
331
|
+
await publishing;
|
|
332
|
+
await flushAsync();
|
|
333
|
+
|
|
334
|
+
expect(socket.sentMessages.map((message) => JSON.parse(message))).toEqual([
|
|
335
|
+
{ event: "onReady", data: { ok: true } },
|
|
256
336
|
]);
|
|
257
337
|
});
|
|
258
338
|
|
|
259
|
-
it("
|
|
339
|
+
it("원격 노드가 여러 개여도 publish 한 번에 envelope 하나를 보내고, envelope은 원본 data 대신 route별 frame을 싣는다", async () => {
|
|
260
340
|
const presenceStore = new InMemoryWebSocketPresenceStore();
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
presenceStore.register({
|
|
341
|
+
const clusterBus = new RecordingClusterBus();
|
|
342
|
+
await registerSession(presenceStore, {
|
|
264
343
|
sessionId: "remote-1",
|
|
265
344
|
nodeId: "node-b",
|
|
266
345
|
namespace: "chat",
|
|
267
346
|
});
|
|
268
|
-
presenceStore
|
|
269
|
-
|
|
270
|
-
nodeId: "node-
|
|
271
|
-
|
|
272
|
-
localConnections,
|
|
273
|
-
clusterBus,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
engine.publishToAudience(WebSocketAudience.room("room-1", "chat"), "evt", {
|
|
277
|
-
ok: true,
|
|
347
|
+
await registerSession(presenceStore, {
|
|
348
|
+
sessionId: "remote-2",
|
|
349
|
+
nodeId: "node-c",
|
|
350
|
+
namespace: "chat",
|
|
278
351
|
});
|
|
352
|
+
await presenceStore.join("remote-1", "room-1");
|
|
353
|
+
await presenceStore.join("remote-2", "room-1");
|
|
354
|
+
const runtime = createWebSocketRuntime({ nodeId: "node-a", presenceStore, clusterBus });
|
|
355
|
+
runtime.registerRoute("/chat", { outEvents: OutEvents });
|
|
279
356
|
|
|
280
|
-
await
|
|
357
|
+
await runtime.publishToRoom("room-1", "onReady", { ok: true }, "chat");
|
|
358
|
+
await flushAsync();
|
|
281
359
|
|
|
282
360
|
expect(clusterBus.published).toHaveLength(1);
|
|
283
|
-
|
|
361
|
+
const envelope = clusterBus.published[0];
|
|
362
|
+
expect(envelope).toMatchObject({
|
|
284
363
|
sourceNodeId: "node-a",
|
|
285
|
-
|
|
286
|
-
|
|
364
|
+
event: "onReady",
|
|
365
|
+
audience: WebSocketAudience.room("room-1", "chat"),
|
|
366
|
+
});
|
|
367
|
+
expect([...envelope.targetNodeIds].sort()).toEqual(["node-b", "node-c"]);
|
|
368
|
+
expect(Object.keys(envelope.frames)).toEqual(["/chat"]);
|
|
369
|
+
expect(JSON.parse(envelope.frames["/chat"])).toEqual({
|
|
370
|
+
event: "onReady",
|
|
371
|
+
data: { ok: true },
|
|
287
372
|
});
|
|
373
|
+
expect(envelope).not.toHaveProperty("data");
|
|
288
374
|
});
|
|
289
375
|
|
|
290
|
-
it("
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
376
|
+
it("자기 자신이 보냈거나 다른 노드를 대상으로 한 envelope은 무시한다", async () => {
|
|
377
|
+
const clusterBus = new RecordingClusterBus();
|
|
378
|
+
const runtime = createWebSocketRuntime({ nodeId: "node-a", clusterBus });
|
|
379
|
+
runtime.registerRoute("/chat", { outEvents: OutEvents });
|
|
380
|
+
const socket = new FakeWebSocket();
|
|
381
|
+
await runtime.registerConnection(asWebSocket(socket), {
|
|
382
|
+
routeKey: "/chat",
|
|
383
|
+
outEvents: OutEvents,
|
|
384
|
+
inEvents: InEvents,
|
|
299
385
|
namespace: "chat",
|
|
386
|
+
heartbeat: 0,
|
|
300
387
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
388
|
+
await flushUntil(() => clusterBus.handlers.size > 0);
|
|
389
|
+
const frame = JSON.stringify({ event: "onReady", data: { ok: true } });
|
|
390
|
+
|
|
391
|
+
await clusterBus.emit(
|
|
392
|
+
createEnvelope({
|
|
393
|
+
id: "self",
|
|
394
|
+
sourceNodeId: "node-a",
|
|
395
|
+
targetNodeIds: ["node-a"],
|
|
396
|
+
frames: { "/chat": frame },
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
await clusterBus.emit(
|
|
400
|
+
createEnvelope({
|
|
401
|
+
id: "other-target",
|
|
402
|
+
sourceNodeId: "node-b",
|
|
403
|
+
targetNodeIds: ["node-c"],
|
|
404
|
+
frames: { "/chat": frame },
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
await flushAsync();
|
|
408
|
+
|
|
409
|
+
expect(socket.sentMessages).toHaveLength(0);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("수신한 frame은 재검증·재인코딩 없이 그대로 로컬 대상에게만 보낸다", async () => {
|
|
413
|
+
const presenceStore = new ScriptedPresenceStore();
|
|
414
|
+
const clusterBus = new RecordingClusterBus();
|
|
415
|
+
const runtime = createWebSocketRuntime({ nodeId: "node-a", presenceStore, clusterBus });
|
|
416
|
+
runtime.registerRoute("/chat", { outEvents: OutEvents });
|
|
417
|
+
const roomSocket = new FakeWebSocket();
|
|
418
|
+
const outsiderSocket = new FakeWebSocket();
|
|
419
|
+
const roomConnection = await runtime.registerConnection(asWebSocket(roomSocket), {
|
|
420
|
+
routeKey: "/chat",
|
|
421
|
+
outEvents: OutEvents,
|
|
422
|
+
inEvents: InEvents,
|
|
423
|
+
namespace: "chat",
|
|
424
|
+
heartbeat: 0,
|
|
425
|
+
});
|
|
426
|
+
await runtime.registerConnection(asWebSocket(outsiderSocket), {
|
|
427
|
+
routeKey: "/chat",
|
|
428
|
+
outEvents: OutEvents,
|
|
429
|
+
inEvents: InEvents,
|
|
430
|
+
namespace: "chat",
|
|
431
|
+
heartbeat: 0,
|
|
432
|
+
});
|
|
433
|
+
await roomConnection.join("room-1");
|
|
434
|
+
await flushUntil(() => clusterBus.handlers.size > 0);
|
|
435
|
+
const callsBefore = presenceStore.calls.length;
|
|
436
|
+
// 수신 노드의 스키마로는 통과할 수 없는 frame이다. 재검증하면 1011로 닫히고, 재인코딩하면 바이트가 달라진다
|
|
437
|
+
const frame = '{"data":{"ok":"not-a-boolean"}, "event":"onReady"}';
|
|
438
|
+
|
|
439
|
+
await clusterBus.emit(
|
|
440
|
+
createEnvelope({
|
|
441
|
+
id: "remote",
|
|
442
|
+
audience: WebSocketAudience.room("room-1", "chat"),
|
|
443
|
+
frames: { "/chat": frame },
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
await flushUntil(() => roomSocket.sentMessages.length > 0);
|
|
447
|
+
await flushAsync();
|
|
448
|
+
|
|
449
|
+
expect(roomSocket.sentMessages).toEqual([frame]);
|
|
450
|
+
expect(roomSocket.closedWith).toBeNull();
|
|
451
|
+
expect(outsiderSocket.sentMessages).toHaveLength(0);
|
|
452
|
+
// 수신 노드는 store가 아닌 자기 메모리로 audience를 해석한다
|
|
453
|
+
expect(presenceStore.calls.slice(callsBefore)).toEqual([]);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("수신 노드는 연결마다 자기 route의 frame을 보내고, frame이 없는 route의 연결은 건너뛴다", async () => {
|
|
457
|
+
const clusterBus = new RecordingClusterBus();
|
|
458
|
+
const runtime = createWebSocketRuntime({ nodeId: "node-a", clusterBus });
|
|
459
|
+
const NoReadyEvents = z.object({ onOther: z.object({ value: z.number() }) });
|
|
460
|
+
runtime.registerRoute("/a", { outEvents: OutEvents });
|
|
461
|
+
runtime.registerRoute("/b", { outEvents: OutEvents });
|
|
462
|
+
runtime.registerRoute("/c", { outEvents: NoReadyEvents });
|
|
463
|
+
const sockets = { a: new FakeWebSocket(), b: new FakeWebSocket(), c: new FakeWebSocket() };
|
|
464
|
+
await runtime.registerConnection(asWebSocket(sockets.a), {
|
|
465
|
+
routeKey: "/a",
|
|
466
|
+
outEvents: OutEvents,
|
|
467
|
+
inEvents: InEvents,
|
|
468
|
+
namespace: "chat",
|
|
469
|
+
heartbeat: 0,
|
|
470
|
+
});
|
|
471
|
+
await runtime.registerConnection(asWebSocket(sockets.b), {
|
|
472
|
+
routeKey: "/b",
|
|
473
|
+
outEvents: OutEvents,
|
|
474
|
+
inEvents: InEvents,
|
|
475
|
+
namespace: "chat",
|
|
476
|
+
heartbeat: 0,
|
|
306
477
|
});
|
|
478
|
+
await runtime.registerConnection(asWebSocket(sockets.c), {
|
|
479
|
+
routeKey: "/c",
|
|
480
|
+
outEvents: NoReadyEvents,
|
|
481
|
+
inEvents: InEvents,
|
|
482
|
+
namespace: "chat",
|
|
483
|
+
heartbeat: 0,
|
|
484
|
+
});
|
|
485
|
+
await flushUntil(() => clusterBus.handlers.size > 0);
|
|
486
|
+
const frameA = JSON.stringify({ event: "onReady", data: { ok: true, route: "a" } });
|
|
487
|
+
const frameB = JSON.stringify({ event: "onReady", data: { ok: true, route: "b" } });
|
|
307
488
|
|
|
308
|
-
clusterBus.emit(
|
|
309
|
-
|
|
489
|
+
await clusterBus.emit(
|
|
490
|
+
createEnvelope({ id: "remote", frames: { "/a": frameA, "/b": frameB } }),
|
|
491
|
+
);
|
|
492
|
+
await flushUntil(
|
|
493
|
+
() => sockets.a.sentMessages.length > 0 && sockets.b.sentMessages.length > 0,
|
|
494
|
+
);
|
|
495
|
+
await flushAsync();
|
|
310
496
|
|
|
311
|
-
|
|
497
|
+
expect(sockets.a.sentMessages).toEqual([frameA]);
|
|
498
|
+
expect(sockets.b.sentMessages).toEqual([frameB]);
|
|
499
|
+
expect(sockets.c.sentMessages).toEqual([]);
|
|
500
|
+
expect(sockets.c.closedWith).toBeNull();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
312
503
|
|
|
313
|
-
|
|
504
|
+
describe("Route table", () => {
|
|
505
|
+
const NoticeEventsV1 = z.object({ onNotice: z.object({ level: z.number() }) });
|
|
506
|
+
const NoticeEventsV2 = z.object({
|
|
507
|
+
onNotice: z.object({ level: z.number(), source: z.string().default("v2") }),
|
|
314
508
|
});
|
|
509
|
+
const AlertEvents = z.object({ onAlert: z.object({ message: z.string() }) });
|
|
315
510
|
|
|
316
|
-
|
|
511
|
+
// 로컬 연결 없이 원격 세션만 room-1에 있는 노드를 만든다
|
|
512
|
+
async function createRemoteOnlyRuntime() {
|
|
317
513
|
const presenceStore = new InMemoryWebSocketPresenceStore();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
localConnections.register(connection);
|
|
322
|
-
presenceStore.register({
|
|
323
|
-
sessionId: "s1",
|
|
324
|
-
nodeId: "node-a",
|
|
514
|
+
await registerSession(presenceStore, {
|
|
515
|
+
sessionId: "remote-1",
|
|
516
|
+
nodeId: "node-b",
|
|
325
517
|
namespace: "chat",
|
|
326
518
|
});
|
|
327
|
-
|
|
519
|
+
await presenceStore.join("remote-1", "room-1");
|
|
520
|
+
const clusterBus = new RecordingClusterBus();
|
|
521
|
+
const runtime = createWebSocketRuntime({
|
|
328
522
|
nodeId: "node-a",
|
|
329
523
|
presenceStore,
|
|
330
|
-
localConnections,
|
|
331
524
|
clusterBus,
|
|
525
|
+
telemetry: true,
|
|
526
|
+
});
|
|
527
|
+
return { runtime, clusterBus };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
it("로컬 연결이 없어도 등록된 route의 frame을 담은 envelope을 원격 노드로 보낸다", async () => {
|
|
531
|
+
const { runtime, clusterBus } = await createRemoteOnlyRuntime();
|
|
532
|
+
runtime.registerRoute("/x", { outEvents: NoticeEventsV1 });
|
|
533
|
+
|
|
534
|
+
await runtime.publishToRoom("room-1", "onNotice", { level: 1 }, "chat");
|
|
535
|
+
await flushAsync();
|
|
536
|
+
|
|
537
|
+
expect(clusterBus.published).toHaveLength(1);
|
|
538
|
+
expect(clusterBus.published[0].targetNodeIds).toEqual(["node-b"]);
|
|
539
|
+
expect(Object.keys(clusterBus.published[0].frames)).toEqual(["/x"]);
|
|
540
|
+
expect(JSON.parse(clusterBus.published[0].frames["/x"])).toEqual({
|
|
541
|
+
event: "onNotice",
|
|
542
|
+
data: { level: 1 },
|
|
332
543
|
});
|
|
544
|
+
});
|
|
333
545
|
|
|
334
|
-
|
|
546
|
+
it("같은 routeKey로 registerRoute를 다시 호출하면 스키마가 교체된다", async () => {
|
|
547
|
+
const { runtime, clusterBus } = await createRemoteOnlyRuntime();
|
|
548
|
+
runtime.registerRoute("/x", { outEvents: NoticeEventsV1 });
|
|
549
|
+
runtime.registerRoute("/x", { outEvents: NoticeEventsV2 });
|
|
335
550
|
|
|
336
|
-
await
|
|
551
|
+
await runtime.publishToRoom("room-1", "onNotice", { level: 1 }, "chat");
|
|
552
|
+
await flushAsync();
|
|
337
553
|
|
|
338
|
-
expect(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
554
|
+
expect(Object.keys(clusterBus.published[0].frames)).toEqual(["/x"]);
|
|
555
|
+
expect(JSON.parse(clusterBus.published[0].frames["/x"])).toEqual({
|
|
556
|
+
event: "onNotice",
|
|
557
|
+
data: { level: 1, source: "v2" },
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("replaceRoutes는 route 집합 전체를 교체해 빠진 route는 제거하고 새 route와 새 스키마를 쓴다", async () => {
|
|
562
|
+
const { runtime, clusterBus } = await createRemoteOnlyRuntime();
|
|
563
|
+
runtime.registerRoute("/x", { outEvents: NoticeEventsV1 });
|
|
564
|
+
runtime.registerRoute("/y", { outEvents: NoticeEventsV1 });
|
|
565
|
+
|
|
566
|
+
runtime.replaceRoutes([
|
|
567
|
+
{ routeKey: "/y", outEvents: NoticeEventsV2 },
|
|
568
|
+
{ routeKey: "/z", outEvents: AlertEvents },
|
|
345
569
|
]);
|
|
570
|
+
|
|
571
|
+
await runtime.publishToRoom("room-1", "onNotice", { level: 3 }, "chat");
|
|
572
|
+
await runtime.publishToRoom("room-1", "onAlert", { message: "fire" }, "chat");
|
|
573
|
+
await flushAsync();
|
|
574
|
+
|
|
575
|
+
expect(clusterBus.published).toHaveLength(2);
|
|
576
|
+
expect(Object.keys(clusterBus.published[0].frames)).toEqual(["/y"]);
|
|
577
|
+
expect(JSON.parse(clusterBus.published[0].frames["/y"])).toEqual({
|
|
578
|
+
event: "onNotice",
|
|
579
|
+
data: { level: 3, source: "v2" },
|
|
580
|
+
});
|
|
581
|
+
expect(Object.keys(clusterBus.published[1].frames)).toEqual(["/z"]);
|
|
346
582
|
});
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
583
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
localConnections: WebSocketLocalConnectionStore;
|
|
354
|
-
clusterBus: WebSocketClusterBus;
|
|
355
|
-
}): WebSocketDeliveryEngine {
|
|
356
|
-
return new WebSocketDeliveryEngine({
|
|
357
|
-
nodeId: input.nodeId,
|
|
358
|
-
localConnections: input.localConnections,
|
|
359
|
-
audienceResolver: new WebSocketAudienceResolver({
|
|
360
|
-
nodeId: input.nodeId,
|
|
361
|
-
presenceStore: input.presenceStore,
|
|
362
|
-
}),
|
|
363
|
-
clusterBus: input.clusterBus,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
584
|
+
it("어떤 route도 선언하지 않은 이벤트는 envelope 없이 resolve하고 ws.publish.rejected를 남긴다", async () => {
|
|
585
|
+
const { runtime, clusterBus } = await createRemoteOnlyRuntime();
|
|
586
|
+
runtime.replaceRoutes([{ routeKey: "/z", outEvents: AlertEvents }]);
|
|
366
587
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
emittedAt: Date.now(),
|
|
382
|
-
};
|
|
383
|
-
}
|
|
588
|
+
await expect(
|
|
589
|
+
runtime.publishToRoom("room-1", "onNotice", { level: 1 }, "chat"),
|
|
590
|
+
).resolves.toBeUndefined();
|
|
591
|
+
await flushAsync();
|
|
592
|
+
|
|
593
|
+
expect(clusterBus.published).toEqual([]);
|
|
594
|
+
expect(
|
|
595
|
+
(
|
|
596
|
+
runtime.telemetryController.getEventStore()?.query({ name: "ws.publish.rejected" }) ?? []
|
|
597
|
+
).map((record) => record.detail),
|
|
598
|
+
).toEqual([expect.objectContaining({ event: "onNotice", reason: "unknownEvent" })]);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
});
|