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.
Files changed (322) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +1 -2
  2. package/dist/ai/agents/agent.js +2 -3
  3. package/dist/ai/agents/index.js +1 -2
  4. package/dist/ai/index.js +1 -2
  5. package/dist/ai/providers/rtzr/api.js +1 -2
  6. package/dist/ai/providers/rtzr/error.js +2 -3
  7. package/dist/ai/providers/rtzr/model.js +1 -2
  8. package/dist/ai/providers/rtzr/options.js +1 -2
  9. package/dist/ai/providers/rtzr/provider.js +1 -2
  10. package/dist/ai/providers/rtzr/utils.js +1 -2
  11. package/dist/api/base-frame.js +4 -4
  12. package/dist/api/caster.d.ts +1 -1
  13. package/dist/api/caster.d.ts.map +1 -1
  14. package/dist/api/caster.js +9 -9
  15. package/dist/api/code-converters.js +6 -5
  16. package/dist/api/config.d.ts +1 -1
  17. package/dist/api/config.d.ts.map +1 -1
  18. package/dist/api/config.js +2 -3
  19. package/dist/api/context.d.ts +2 -3
  20. package/dist/api/context.d.ts.map +1 -1
  21. package/dist/api/context.js +2 -3
  22. package/dist/api/decorators.js +2 -3
  23. package/dist/api/http-validator.js +2 -3
  24. package/dist/api/index.js +8 -3
  25. package/dist/api/secret.js +1 -2
  26. package/dist/api/sonamu.d.ts +3 -5
  27. package/dist/api/sonamu.d.ts.map +1 -1
  28. package/dist/api/sonamu.js +30 -24
  29. package/dist/api/validator.js +1 -2
  30. package/dist/api/websocket-helpers.js +2 -3
  31. package/dist/auth/audit-log/builders.js +1 -2
  32. package/dist/auth/audit-log/client-ip.js +2 -3
  33. package/dist/auth/audit-log/events.js +1 -2
  34. package/dist/auth/audit-log/plugin.d.ts.map +1 -1
  35. package/dist/auth/audit-log/plugin.js +2 -3
  36. package/dist/auth/audit-log-ingestor.js +2 -3
  37. package/dist/auth/auth-generator.js +2 -3
  38. package/dist/auth/better-auth-entities.js +1 -2
  39. package/dist/auth/better-auth-request.js +2 -3
  40. package/dist/auth/index.js +2 -3
  41. package/dist/auth/knex-adapter.js +2 -3
  42. package/dist/auth/plugins/entity-definitions/admin.js +1 -2
  43. package/dist/auth/plugins/entity-definitions/anonymous.js +1 -2
  44. package/dist/auth/plugins/entity-definitions/api-key.js +1 -2
  45. package/dist/auth/plugins/entity-definitions/audit-log.js +1 -2
  46. package/dist/auth/plugins/entity-definitions/index.js +1 -2
  47. package/dist/auth/plugins/entity-definitions/jwt.js +1 -2
  48. package/dist/auth/plugins/entity-definitions/organization.js +1 -2
  49. package/dist/auth/plugins/entity-definitions/passkey.js +1 -2
  50. package/dist/auth/plugins/entity-definitions/phone-number.js +1 -2
  51. package/dist/auth/plugins/entity-definitions/sso.js +1 -2
  52. package/dist/auth/plugins/entity-definitions/two-factor.js +1 -2
  53. package/dist/auth/plugins/entity-definitions/username.js +1 -2
  54. package/dist/auth/plugins/wrappers/admin.d.ts +33 -32
  55. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  56. package/dist/auth/plugins/wrappers/admin.js +2 -3
  57. package/dist/auth/plugins/wrappers/anonymous.js +2 -3
  58. package/dist/auth/plugins/wrappers/api-key.js +2 -3
  59. package/dist/auth/plugins/wrappers/jwt.js +2 -3
  60. package/dist/auth/plugins/wrappers/organization.js +2 -3
  61. package/dist/auth/plugins/wrappers/passkey.js +2 -3
  62. package/dist/auth/plugins/wrappers/phone-number.d.ts +43 -1
  63. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  64. package/dist/auth/plugins/wrappers/phone-number.js +2 -3
  65. package/dist/auth/plugins/wrappers/sso.d.ts +138 -187
  66. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -1
  67. package/dist/auth/plugins/wrappers/sso.js +1 -2
  68. package/dist/auth/plugins/wrappers/two-factor.d.ts +51 -10
  69. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  70. package/dist/auth/plugins/wrappers/two-factor.js +2 -3
  71. package/dist/auth/plugins/wrappers/username.d.ts +39 -5
  72. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  73. package/dist/auth/plugins/wrappers/username.js +2 -3
  74. package/dist/bin/build-config.js +5 -5
  75. package/dist/bin/compiler-policy.js +2 -3
  76. package/dist/bin/fixture.js +7 -5
  77. package/dist/bin/hmr-hook-register.js +1 -2
  78. package/dist/bin/migrate-targets.js +2 -3
  79. package/dist/bin/test-command.js +2 -3
  80. package/dist/bin/ts-loader-register.js +2 -3
  81. package/dist/bin/ts-loader-registration.js +2 -3
  82. package/dist/bin/typecheck-web.js +2 -3
  83. package/dist/cache/cache-manager.js +1 -2
  84. package/dist/cache/decorator.js +2 -3
  85. package/dist/cache/drivers.js +1 -2
  86. package/dist/cache/index.js +2 -3
  87. package/dist/cache-control/cache-control.js +1 -2
  88. package/dist/code-formatting/index.js +2 -3
  89. package/dist/code-formatting/oxfmt.js +2 -3
  90. package/dist/code-formatting/oxlint.js +2 -3
  91. package/dist/compress/compress.js +1 -2
  92. package/dist/compress/index.js +2 -2
  93. package/dist/compress/types.d.ts +1 -1
  94. package/dist/compress/types.d.ts.map +1 -1
  95. package/dist/cone/cone-generator.js +2 -3
  96. package/dist/database/_batch_update.js +1 -2
  97. package/dist/database/base-model.js +4 -4
  98. package/dist/database/db.js +4 -4
  99. package/dist/database/knex.js +2 -3
  100. package/dist/database/puri-wrapper.js +2 -3
  101. package/dist/database/puri.js +23 -22
  102. package/dist/database/puri.types.js +1 -2
  103. package/dist/database/transaction-context.js +1 -2
  104. package/dist/database/upsert-builder.js +2 -3
  105. package/dist/dict/en.js +2 -3
  106. package/dist/dict/index.js +3 -3
  107. package/dist/dict/ko.js +2 -3
  108. package/dist/dict/rc-keys.js +1 -2
  109. package/dist/dict/sd.js +2 -3
  110. package/dist/dict/sonamu-dictionary.js +7 -6
  111. package/dist/dict/utils.js +2 -3
  112. package/dist/entity/entity-manager.d.ts +66 -66
  113. package/dist/entity/entity-manager.js +4 -4
  114. package/dist/entity/entity-template-cone.js +2 -3
  115. package/dist/entity/entity.js +21 -18
  116. package/dist/env.js +1 -2
  117. package/dist/exceptions/error-handler.d.ts.map +1 -1
  118. package/dist/exceptions/error-handler.js +2 -3
  119. package/dist/exceptions/so-exceptions.js +2 -3
  120. package/dist/filter/index.js +3 -2
  121. package/dist/filter/types.js +1 -2
  122. package/dist/filter/utils.js +7 -6
  123. package/dist/index.d.ts +1 -0
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +42 -6
  126. package/dist/logger/category.js +2 -3
  127. package/dist/logger/configure.js +5 -5
  128. package/dist/migration/code-generation.js +23 -16
  129. package/dist/migration/index-where-predicate.js +2 -3
  130. package/dist/migration/migration-set.js +2 -3
  131. package/dist/migration/migrator.js +2 -3
  132. package/dist/migration/postgresql-schema-reader.js +6 -9
  133. package/dist/migration/slack-confirm.js +2 -3
  134. package/dist/naite/naite-reporter.js +2 -3
  135. package/dist/naite/naite.js +10 -8
  136. package/dist/runtime-type.js +2 -3
  137. package/dist/ssr/hmr-opt-out.js +1 -2
  138. package/dist/ssr/index.js +7 -4
  139. package/dist/ssr/registry.js +1 -2
  140. package/dist/ssr/renderer.js +2 -3
  141. package/dist/storage/base-file.js +1 -2
  142. package/dist/storage/buffered-file.js +2 -3
  143. package/dist/storage/drivers.js +1 -2
  144. package/dist/storage/index.js +2 -3
  145. package/dist/storage/s3-driver.js +1 -2
  146. package/dist/storage/storage-manager.js +2 -3
  147. package/dist/storage/uploaded-file.js +2 -3
  148. package/dist/stream/__tests__/ws-test-helpers.d.ts +108 -0
  149. package/dist/stream/__tests__/ws-test-helpers.d.ts.map +1 -0
  150. package/dist/stream/__tests__/ws-test-helpers.js +259 -0
  151. package/dist/stream/index.d.ts +1 -0
  152. package/dist/stream/index.d.ts.map +1 -1
  153. package/dist/stream/index.js +15 -4
  154. package/dist/stream/sse.js +1 -2
  155. package/dist/stream/ws-audience-resolver.d.ts +8 -5
  156. package/dist/stream/ws-audience-resolver.d.ts.map +1 -1
  157. package/dist/stream/ws-audience-resolver.js +17 -15
  158. package/dist/stream/ws-audience.js +1 -2
  159. package/dist/stream/ws-cluster-bus.d.ts +9 -4
  160. package/dist/stream/ws-cluster-bus.d.ts.map +1 -1
  161. package/dist/stream/ws-cluster-bus.js +2 -3
  162. package/dist/stream/ws-core.d.ts +10 -0
  163. package/dist/stream/ws-core.d.ts.map +1 -1
  164. package/dist/stream/ws-delivery.d.ts +30 -4
  165. package/dist/stream/ws-delivery.d.ts.map +1 -1
  166. package/dist/stream/ws-delivery.js +200 -54
  167. package/dist/stream/ws-frame.d.ts +15 -0
  168. package/dist/stream/ws-frame.d.ts.map +1 -0
  169. package/dist/stream/ws-frame.js +28 -0
  170. package/dist/stream/ws-local-connection-store.d.ts +4 -4
  171. package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
  172. package/dist/stream/ws-local-connection-store.js +3 -4
  173. package/dist/stream/ws-presence-index.d.ts +36 -0
  174. package/dist/stream/ws-presence-index.d.ts.map +1 -0
  175. package/dist/stream/ws-presence-index.js +176 -0
  176. package/dist/stream/ws-presence-store.d.ts +39 -39
  177. package/dist/stream/ws-presence-store.d.ts.map +1 -1
  178. package/dist/stream/ws-presence-store.js +23 -154
  179. package/dist/stream/ws-registry.d.ts +45 -17
  180. package/dist/stream/ws-registry.d.ts.map +1 -1
  181. package/dist/stream/ws-registry.js +170 -62
  182. package/dist/stream/ws-session-id.d.ts +14 -0
  183. package/dist/stream/ws-session-id.d.ts.map +1 -0
  184. package/dist/stream/ws-session-id.js +36 -0
  185. package/dist/stream/ws-telemetry-memory.js +3 -4
  186. package/dist/stream/ws-telemetry-trace.js +1 -2
  187. package/dist/stream/ws-telemetry.js +2 -3
  188. package/dist/stream/ws.d.ts +20 -10
  189. package/dist/stream/ws.d.ts.map +1 -1
  190. package/dist/stream/ws.js +60 -38
  191. package/dist/syncer/api-parser.js +2 -3
  192. package/dist/syncer/checksum.js +4 -4
  193. package/dist/syncer/code-generator.js +4 -4
  194. package/dist/syncer/entity-operations.js +2 -3
  195. package/dist/syncer/event-batcher.js +1 -2
  196. package/dist/syncer/file-copy.js +2 -3
  197. package/dist/syncer/file-patterns.js +2 -3
  198. package/dist/syncer/file-tracking.js +2 -3
  199. package/dist/syncer/filesystem-dependencies.js +1 -2
  200. package/dist/syncer/index.js +8 -2
  201. package/dist/syncer/module-loader.js +8 -6
  202. package/dist/syncer/syncer-actions.js +8 -7
  203. package/dist/syncer/syncer.js +13 -8
  204. package/dist/syncer/watcher.js +2 -3
  205. package/dist/tasks/decorator.js +1 -2
  206. package/dist/tasks/step-wrapper.js +2 -3
  207. package/dist/tasks/workflow-manager.js +2 -3
  208. package/dist/template/entity-converter.js +17 -16
  209. package/dist/template/helpers.js +2 -3
  210. package/dist/template/implementations/entity.template.js +2 -3
  211. package/dist/template/implementations/entry-server.template.js +2 -3
  212. package/dist/template/implementations/generated.template.js +2 -3
  213. package/dist/template/implementations/generated_http.template.js +6 -6
  214. package/dist/template/implementations/generated_sso.template.js +2 -3
  215. package/dist/template/implementations/http_validators.template.js +2 -3
  216. package/dist/template/implementations/init_types.template.js +2 -3
  217. package/dist/template/implementations/model.template.js +4 -4
  218. package/dist/template/implementations/model_test.template.js +2 -3
  219. package/dist/template/implementations/queries.template.js +2 -3
  220. package/dist/template/implementations/sd.template.js +2 -3
  221. package/dist/template/implementations/services.template.js +2 -3
  222. package/dist/template/implementations/view_form.template.js +12 -9
  223. package/dist/template/implementations/view_id_all_select.template.js +2 -3
  224. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  225. package/dist/template/implementations/view_list.template.js +19 -17
  226. package/dist/template/implementations/view_search_input.template.js +2 -3
  227. package/dist/template/index.js +9 -3
  228. package/dist/template/template-manager.js +2 -3
  229. package/dist/template/template-types.js +1 -2
  230. package/dist/template/template.js +2 -3
  231. package/dist/template/zod-converter.js +54 -47
  232. package/dist/testing/_relation-graph.js +2 -3
  233. package/dist/testing/bootstrap.js +2 -3
  234. package/dist/testing/data-explorer.js +2 -3
  235. package/dist/testing/dev-test-routes.js +2 -3
  236. package/dist/testing/dev-vitest-manager.js +15 -12
  237. package/dist/testing/faker-mappings.js +1 -2
  238. package/dist/testing/filesystem.js +2 -3
  239. package/dist/testing/fixture-generator.js +2 -3
  240. package/dist/testing/fixture-loader.js +1 -2
  241. package/dist/testing/fixture-manager.js +12 -9
  242. package/dist/testing/global-setup.js +5 -5
  243. package/dist/testing/index.js +6 -3
  244. package/dist/testing/naite-vitest-reporter.js +1 -2
  245. package/dist/testing/parallel-db-manager.js +2 -3
  246. package/dist/testing/vitest-helpers.js +2 -3
  247. package/dist/tooling/__tests__/helpers/sonamu-test-root.js +2 -3
  248. package/dist/tooling/cdd-service.js +1 -2
  249. package/dist/tooling/cli-tooling.js +1 -2
  250. package/dist/tooling/fixture-service.js +1 -2
  251. package/dist/tooling/index.js +1 -2
  252. package/dist/tooling/test-run-client.js +2 -3
  253. package/dist/tsdown.api.config.js +1 -2
  254. package/dist/types/types.js +2 -3
  255. package/dist/ui/ai-api.d.ts.map +1 -1
  256. package/dist/ui/ai-api.js +4 -5
  257. package/dist/ui/ai-client.d.ts +2 -2
  258. package/dist/ui/ai-client.d.ts.map +1 -1
  259. package/dist/ui/ai-client.js +9 -8
  260. package/dist/ui/api.d.ts.map +1 -1
  261. package/dist/ui/api.js +11 -8
  262. package/dist/ui/async-route.js +1 -2
  263. package/dist/ui/cdd-service.js +22 -21
  264. package/dist/ui/migrations-api.js +6 -5
  265. package/dist/ui-web/assets/index-QDMyy31K.js +180 -0
  266. package/dist/ui-web/assets/index-dV7akMFB.css +1 -0
  267. package/dist/ui-web/index.html +2 -2
  268. package/dist/utils/async-utils.js +1 -2
  269. package/dist/utils/class-name.js +1 -2
  270. package/dist/utils/console-util.js +1 -2
  271. package/dist/utils/controller.js +2 -3
  272. package/dist/utils/esm-utils.js +1 -2
  273. package/dist/utils/fs-utils.js +2 -3
  274. package/dist/utils/lodash-able.js +1 -2
  275. package/dist/utils/model.js +1 -2
  276. package/dist/utils/object-utils.js +2 -3
  277. package/dist/utils/path-utils.js +2 -3
  278. package/dist/utils/process-utils.js +2 -3
  279. package/dist/utils/runtime-value.js +1 -2
  280. package/dist/utils/sql-parser.js +2 -3
  281. package/dist/utils/type-utils.js +8 -8
  282. package/dist/utils/utils.js +2 -3
  283. package/dist/utils/zod-error.js +2 -3
  284. package/dist/vector/chunking.js +1 -2
  285. package/dist/vector/config.js +1 -2
  286. package/dist/vector/embedding.js +4 -4
  287. package/package.json +63 -63
  288. package/src/ai/agents/agent.ts +1 -1
  289. package/src/api/__tests__/sonamu.plugins.test.ts +11 -1
  290. package/src/api/__tests__/sonamu.websocket-routes.test.ts +234 -0
  291. package/src/api/__tests__/sonamu.websocket.test.ts +241 -24
  292. package/src/api/config.ts +1 -1
  293. package/src/api/context.ts +2 -8
  294. package/src/api/sonamu.ts +46 -28
  295. package/src/auth/audit-log/plugin.ts +2 -0
  296. package/src/compress/types.ts +1 -1
  297. package/src/exceptions/error-handler.ts +3 -2
  298. package/src/index.ts +1 -0
  299. package/src/stream/__tests__/ws-contracts.test.ts +483 -265
  300. package/src/stream/__tests__/ws-multinode.test.ts +326 -0
  301. package/src/stream/__tests__/ws-registry-async.test.ts +855 -0
  302. package/src/stream/__tests__/ws-test-helpers.ts +360 -0
  303. package/src/stream/__tests__/ws.test.ts +342 -145
  304. package/src/stream/index.ts +1 -0
  305. package/src/stream/ws-audience-resolver.ts +21 -19
  306. package/src/stream/ws-cluster-bus.ts +12 -4
  307. package/src/stream/ws-core.ts +14 -0
  308. package/src/stream/ws-delivery.ts +321 -97
  309. package/src/stream/ws-frame.ts +39 -0
  310. package/src/stream/ws-local-connection-store.ts +10 -9
  311. package/src/stream/ws-presence-index.ts +273 -0
  312. package/src/stream/ws-presence-store.ts +64 -284
  313. package/src/stream/ws-registry.ts +272 -85
  314. package/src/stream/ws-session-id.ts +39 -0
  315. package/src/stream/ws.ts +91 -50
  316. package/src/template/implementations/model.template.ts +3 -3
  317. package/src/template/implementations/view_list.template.ts +12 -13
  318. package/src/ui/ai-api.ts +4 -2
  319. package/src/ui/ai-client.ts +2 -2
  320. package/src/ui/api.ts +58 -62
  321. package/dist/ui-web/assets/index-BMdIyKXS.css +0 -1
  322. package/dist/ui-web/assets/index-LpE8u19O.js +0 -178
@@ -0,0 +1,360 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ import { type WebSocketTransport } from "../ws";
4
+ import { type WebSocketAudience } from "../ws-audience";
5
+ import {
6
+ type WebSocketClusterBus,
7
+ type WebSocketClusterEnvelope,
8
+ type WebSocketClusterEnvelopeHandler,
9
+ } from "../ws-cluster-bus";
10
+ import { type WebSocketRoomId, type WebSocketUserId } from "../ws-core";
11
+ import { InMemoryWebSocketPresenceStore } from "../ws-presence-store";
12
+
13
+ // setImmediate 단위로 큐를 비워 fan-out/outbound flush/cluster 전달이 끝나기를 기다린다
14
+ export async function flushAsync(rounds: number = 5): Promise<void> {
15
+ for (let index = 0; index < rounds; index += 1) {
16
+ await new Promise<void>((resolve) => {
17
+ setImmediate(resolve);
18
+ });
19
+ }
20
+ }
21
+
22
+ // 조건이 만족될 때까지 setImmediate 단위로 기다린다. 끝내 만족하지 않으면 그대로 반환해 assertion에서 실패하게 한다
23
+ export async function flushUntil(predicate: () => boolean, maxRounds: number = 50): Promise<void> {
24
+ for (let index = 0; index < maxRounds && !predicate(); index += 1) {
25
+ await flushAsync(1);
26
+ }
27
+ }
28
+
29
+ export type Deferred<T> = {
30
+ promise: Promise<T>;
31
+ resolve: (value: T) => void;
32
+ reject: (error: Error) => void;
33
+ };
34
+
35
+ export function createDeferred<T = void>(): Deferred<T> {
36
+ let resolve!: (value: T) => void;
37
+ let reject!: (error: Error) => void;
38
+ const promise = new Promise<T>((innerResolve, innerReject) => {
39
+ resolve = innerResolve;
40
+ reject = innerReject;
41
+ });
42
+ return { promise, resolve, reject };
43
+ }
44
+
45
+ // promise가 아직 settle되지 않았는지 확인한다
46
+ export async function isSettled<T>(promise: Promise<T>): Promise<boolean> {
47
+ let settled = false;
48
+ promise.then(
49
+ () => {
50
+ settled = true;
51
+ },
52
+ () => {
53
+ settled = true;
54
+ },
55
+ );
56
+ await flushAsync(2);
57
+ return settled;
58
+ }
59
+
60
+ export class FakeWebSocket extends EventEmitter {
61
+ readyState: 0 | 1 | 2 | 3 = 1;
62
+ bufferedAmount = 0;
63
+ sentMessages: string[] = [];
64
+ pingCount = 0;
65
+ closedWith: { code?: number; reason?: string } | null = null;
66
+
67
+ send(data: string): void {
68
+ this.sentMessages.push(data);
69
+ }
70
+
71
+ close(code?: number, reason?: string): void {
72
+ this.closedWith = { code, reason };
73
+ this.readyState = 3;
74
+ this.emit("close", code ?? 1000, Buffer.from(reason ?? "", "utf-8"));
75
+ }
76
+
77
+ ping(): void {
78
+ this.pingCount += 1;
79
+ }
80
+
81
+ terminate(): void {
82
+ this.readyState = 3;
83
+ this.emit("close", 1006, Buffer.alloc(0));
84
+ }
85
+ }
86
+
87
+ export function asWebSocket(socket: FakeWebSocket): WebSocketTransport {
88
+ return {
89
+ get bufferedAmount() {
90
+ return socket.bufferedAmount;
91
+ },
92
+ get readyState() {
93
+ return socket.readyState;
94
+ },
95
+ close(code, reason) {
96
+ socket.close(code, reason?.toString());
97
+ },
98
+ off(...args) {
99
+ socket.off(...args);
100
+ },
101
+ on(...args) {
102
+ socket.on(...args);
103
+ },
104
+ ping() {
105
+ socket.ping();
106
+ },
107
+ send(data) {
108
+ socket.send(data.toString());
109
+ },
110
+ terminate() {
111
+ socket.terminate();
112
+ },
113
+ };
114
+ }
115
+
116
+ // 테스트 데이터 직렬화 결과를 되돌린다. 깨진 JSON이면 원문을 포함한 에러로 테스트를 실패시킨다
117
+ function parseTestJson<T>(raw: string): T {
118
+ try {
119
+ return /* SAFETY: 테스트가 직접 직렬화했거나 runtime이 직렬화한 값만 넘긴다. */ JSON.parse(
120
+ raw,
121
+ ) as T;
122
+ } catch (error) {
123
+ throw new Error(`테스트 JSON 파싱 실패: ${raw}`, { cause: error });
124
+ }
125
+ }
126
+
127
+ export function parseFrames(socket: FakeWebSocket): Array<{ event: string; data: object }> {
128
+ return socket.sentMessages.map((message) =>
129
+ parseTestJson<{ event: string; data: object }>(message),
130
+ );
131
+ }
132
+
133
+ type ScriptedMethod =
134
+ | "register"
135
+ | "activate"
136
+ | "unregister"
137
+ | "setUserId"
138
+ | "clearUserId"
139
+ | "join"
140
+ | "leave"
141
+ | "resolveRoute"
142
+ | "getRoomMemberCount"
143
+ | "shutdown";
144
+
145
+ // 다음 호출 직전에 끼워넣을 동작. 동기 throw, reject, 지연된 promise를 그대로 흉내낸다
146
+ type ScriptedInterceptor = () => void | Promise<void>;
147
+
148
+ /**
149
+ * InMemoryWebSocketPresenceStore를 감싸 호출 순서를 기록하고, 메서드별로 다음 호출의 동작을 스크립트할 수 있는 store.
150
+ * 호출 기록은 store가 호출을 "받은" 시점에 남긴다.
151
+ */
152
+ export class ScriptedPresenceStore {
153
+ readonly calls: string[] = [];
154
+ readonly inner: InMemoryWebSocketPresenceStore;
155
+ nodeId?: string;
156
+ private readonly interceptors = new Map<ScriptedMethod, ScriptedInterceptor[]>();
157
+
158
+ constructor(inner: InMemoryWebSocketPresenceStore = new InMemoryWebSocketPresenceStore()) {
159
+ this.inner = inner;
160
+ }
161
+
162
+ next(method: ScriptedMethod, interceptor: ScriptedInterceptor): this {
163
+ const queue = this.interceptors.get(method) ?? [];
164
+ queue.push(interceptor);
165
+ this.interceptors.set(method, queue);
166
+ return this;
167
+ }
168
+
169
+ // 이후 모든 호출에 같은 동작을 적용한다
170
+ always(method: ScriptedMethod, interceptor: ScriptedInterceptor): this {
171
+ for (let index = 0; index < 1_000; index += 1) {
172
+ this.next(method, interceptor);
173
+ }
174
+ return this;
175
+ }
176
+
177
+ register(input: {
178
+ sessionId: string;
179
+ nodeId: string;
180
+ namespace: string;
181
+ active: boolean;
182
+ connectedAt: Date;
183
+ }): void | Promise<void> {
184
+ this.calls.push(`register:${input.sessionId}`);
185
+ return this.run("register", () => this.inner.register(input));
186
+ }
187
+
188
+ activate(sessionId: string): void | Promise<void> {
189
+ this.calls.push(`activate:${sessionId}`);
190
+ return this.run("activate", () => this.inner.activate(sessionId));
191
+ }
192
+
193
+ unregister(sessionId: string): void | Promise<void> {
194
+ this.calls.push(`unregister:${sessionId}`);
195
+ return this.run("unregister", () => this.inner.unregister(sessionId));
196
+ }
197
+
198
+ setUserId(sessionId: string, userId: WebSocketUserId): void | Promise<void> {
199
+ this.calls.push(`setUserId:${sessionId}:${JSON.stringify(userId)}`);
200
+ return this.run("setUserId", () => this.inner.setUserId(sessionId, userId));
201
+ }
202
+
203
+ clearUserId(sessionId: string): void | Promise<void> {
204
+ this.calls.push(`clearUserId:${sessionId}`);
205
+ return this.run("clearUserId", () => this.inner.clearUserId(sessionId));
206
+ }
207
+
208
+ join(sessionId: string, roomId: WebSocketRoomId): void | Promise<void> {
209
+ this.calls.push(`join:${sessionId}:${roomId}`);
210
+ return this.run("join", () => this.inner.join(sessionId, roomId));
211
+ }
212
+
213
+ leave(sessionId: string, roomId: WebSocketRoomId): void | Promise<void> {
214
+ this.calls.push(`leave:${sessionId}:${roomId}`);
215
+ return this.run("leave", () => this.inner.leave(sessionId, roomId));
216
+ }
217
+
218
+ resolveRoute(
219
+ audience: WebSocketAudience,
220
+ nodeId: string,
221
+ ):
222
+ | { localSessionIds: string[]; remoteNodeIds: string[] }
223
+ | Promise<{ localSessionIds: string[]; remoteNodeIds: string[] }> {
224
+ this.calls.push(`resolveRoute:${nodeId}:${audience.type}`);
225
+ return this.run("resolveRoute", () => this.inner.resolveRoute(audience, nodeId));
226
+ }
227
+
228
+ getRoomMemberCount(roomId: WebSocketRoomId, namespace?: string): number | Promise<number> {
229
+ this.calls.push(`getRoomMemberCount:${roomId}`);
230
+ return this.run("getRoomMemberCount", () => this.inner.getRoomMemberCount(roomId, namespace));
231
+ }
232
+
233
+ shutdown(): void | Promise<void> {
234
+ this.calls.push("shutdown");
235
+ return this.run("shutdown", () => undefined);
236
+ }
237
+
238
+ callsOf(prefix: string): string[] {
239
+ return this.calls.filter((call) => call.startsWith(prefix));
240
+ }
241
+
242
+ private run<T>(method: ScriptedMethod, delegate: () => T | Promise<T>): T | Promise<T> {
243
+ const interceptor = this.interceptors.get(method)?.shift();
244
+ if (!interceptor) {
245
+ return delegate();
246
+ }
247
+
248
+ const result = interceptor();
249
+ if (result instanceof Promise) {
250
+ return result.then(() => delegate());
251
+ }
252
+ return delegate();
253
+ }
254
+ }
255
+
256
+ /**
257
+ * 한 프로세스 안에서 targetNodeIds 기준으로 envelope을 라우팅하는 fake cluster 네트워크.
258
+ * 전송 구간을 흉내내기 위해 envelope을 JSON 직렬화/역직렬화한 뒤 다음 tick에 전달한다.
259
+ */
260
+ export class InProcessClusterNetwork {
261
+ readonly published: WebSocketClusterEnvelope[] = [];
262
+ private readonly handlers = new Map<string, Set<WebSocketClusterEnvelopeHandler>>();
263
+
264
+ bus(nodeId: string): InProcessClusterBus {
265
+ return new InProcessClusterBus(nodeId, this);
266
+ }
267
+
268
+ subscribe(nodeId: string, handler: WebSocketClusterEnvelopeHandler): () => void {
269
+ const handlers = this.handlers.get(nodeId) ?? new Set();
270
+ handlers.add(handler);
271
+ this.handlers.set(nodeId, handlers);
272
+ return () => {
273
+ handlers.delete(handler);
274
+ };
275
+ }
276
+
277
+ deliver(envelope: WebSocketClusterEnvelope): void {
278
+ const wire = JSON.stringify(envelope);
279
+ this.published.push(parseTestJson<WebSocketClusterEnvelope>(wire));
280
+ setImmediate(() => {
281
+ for (const targetNodeId of envelope.targetNodeIds ?? []) {
282
+ for (const handler of this.handlers.get(targetNodeId) ?? []) {
283
+ const received = parseTestJson<WebSocketClusterEnvelope>(wire);
284
+ void Promise.resolve(handler(received)).catch(() => {});
285
+ }
286
+ }
287
+ });
288
+ }
289
+ }
290
+
291
+ export class InProcessClusterBus implements WebSocketClusterBus {
292
+ shutdownCalls = 0;
293
+ unsubscribeCalls = 0;
294
+
295
+ constructor(
296
+ readonly nodeId: string,
297
+ private readonly network: InProcessClusterNetwork,
298
+ ) {}
299
+
300
+ publish(envelope: WebSocketClusterEnvelope): void {
301
+ this.network.deliver(envelope);
302
+ }
303
+
304
+ // 실제 adapter처럼 구독 완료를 promise로 알린다
305
+ async subscribe(handler: WebSocketClusterEnvelopeHandler): Promise<() => void> {
306
+ const unsubscribe = this.network.subscribe(this.nodeId, handler);
307
+ return () => {
308
+ this.unsubscribeCalls += 1;
309
+ unsubscribe();
310
+ };
311
+ }
312
+
313
+ shutdown(): void {
314
+ this.shutdownCalls += 1;
315
+ }
316
+ }
317
+
318
+ // 발행된 envelope을 기록하고 테스트가 직접 수신 envelope을 주입할 수 있는 bus
319
+ export class RecordingClusterBus implements WebSocketClusterBus {
320
+ readonly published: WebSocketClusterEnvelope[] = [];
321
+ readonly handlers = new Set<WebSocketClusterEnvelopeHandler>();
322
+ shutdownCalls = 0;
323
+ unsubscribeCalls = 0;
324
+ nodeId?: string;
325
+
326
+ publish(envelope: WebSocketClusterEnvelope): void | Promise<void> {
327
+ this.published.push(envelope);
328
+ }
329
+
330
+ subscribe(handler: WebSocketClusterEnvelopeHandler): (() => void) | Promise<() => void> {
331
+ this.handlers.add(handler);
332
+ return () => {
333
+ this.unsubscribeCalls += 1;
334
+ this.handlers.delete(handler);
335
+ };
336
+ }
337
+
338
+ async emit(envelope: WebSocketClusterEnvelope): Promise<void> {
339
+ for (const handler of this.handlers) {
340
+ await handler(envelope);
341
+ }
342
+ }
343
+
344
+ shutdown(): void | Promise<void> {
345
+ this.shutdownCalls += 1;
346
+ }
347
+ }
348
+
349
+ // 전역 unhandledRejection을 수집한다. 반환한 함수로 해제하고 수집 결과를 받는다
350
+ export function collectUnhandledRejections(): () => Error[] {
351
+ const collected: Error[] = [];
352
+ const listener = (reason: Error) => {
353
+ collected.push(reason);
354
+ };
355
+ process.on("unhandledRejection", listener);
356
+ return () => {
357
+ process.off("unhandledRejection", listener);
358
+ return collected;
359
+ };
360
+ }