@planet-matrix/mobius-model 0.6.0 → 0.10.1

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 (258) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/oxlint.config.ts +1 -2
  3. package/package.json +29 -17
  4. package/scripts/build.ts +2 -52
  5. package/src/ai/README.md +1 -0
  6. package/src/ai/ai.ts +107 -0
  7. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  8. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  9. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  10. package/src/ai/chat-completion-ai/index.ts +7 -0
  11. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  12. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  13. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  14. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  15. package/src/ai/embedding-ai/embedding.ts +50 -0
  16. package/src/ai/embedding-ai/index.ts +4 -0
  17. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  18. package/src/ai/index.ts +4 -0
  19. package/src/aio/README.md +100 -0
  20. package/src/aio/content.ts +141 -0
  21. package/src/aio/index.ts +3 -0
  22. package/src/aio/json.ts +127 -0
  23. package/src/aio/prompt.ts +246 -0
  24. package/src/basic/README.md +20 -15
  25. package/src/basic/error.ts +19 -5
  26. package/src/basic/function.ts +2 -2
  27. package/src/basic/index.ts +1 -0
  28. package/src/basic/promise.ts +141 -71
  29. package/src/basic/schedule.ts +111 -0
  30. package/src/basic/stream.ts +135 -25
  31. package/src/credential/README.md +107 -0
  32. package/src/credential/api-key.ts +158 -0
  33. package/src/credential/bearer.ts +73 -0
  34. package/src/credential/index.ts +4 -0
  35. package/src/credential/json-web-token.ts +96 -0
  36. package/src/credential/password.ts +170 -0
  37. package/src/cron/README.md +86 -0
  38. package/src/cron/cron.ts +87 -0
  39. package/src/cron/index.ts +1 -0
  40. package/src/drizzle/README.md +1 -0
  41. package/src/drizzle/drizzle.ts +1 -0
  42. package/src/drizzle/helper.ts +47 -0
  43. package/src/drizzle/index.ts +5 -0
  44. package/src/drizzle/infer.ts +52 -0
  45. package/src/drizzle/kysely.ts +8 -0
  46. package/src/drizzle/pagination.ts +198 -0
  47. package/src/email/README.md +1 -0
  48. package/src/email/index.ts +1 -0
  49. package/src/email/resend.ts +25 -0
  50. package/src/event/class-event-proxy.ts +5 -6
  51. package/src/event/common.ts +13 -3
  52. package/src/event/event-manager.ts +3 -3
  53. package/src/event/instance-event-proxy.ts +5 -6
  54. package/src/event/internal.ts +4 -4
  55. package/src/exception/README.md +28 -19
  56. package/src/exception/error/error.ts +123 -0
  57. package/src/exception/error/index.ts +2 -0
  58. package/src/exception/error/match.ts +38 -0
  59. package/src/exception/error/must-fix.ts +17 -0
  60. package/src/exception/index.ts +2 -0
  61. package/src/file-system/find.ts +53 -0
  62. package/src/file-system/index.ts +2 -0
  63. package/src/file-system/path.ts +76 -0
  64. package/src/file-system/resolve.ts +22 -0
  65. package/src/form/README.md +25 -0
  66. package/src/form/index.ts +1 -0
  67. package/src/form/inputor-controller/base.ts +861 -0
  68. package/src/form/inputor-controller/boolean.ts +39 -0
  69. package/src/form/inputor-controller/file.ts +39 -0
  70. package/src/form/inputor-controller/form.ts +179 -0
  71. package/src/form/inputor-controller/helper.ts +117 -0
  72. package/src/form/inputor-controller/index.ts +17 -0
  73. package/src/form/inputor-controller/multi-select.ts +99 -0
  74. package/src/form/inputor-controller/number.ts +116 -0
  75. package/src/form/inputor-controller/select.ts +109 -0
  76. package/src/form/inputor-controller/text.ts +82 -0
  77. package/src/http/READMD.md +1 -0
  78. package/src/http/api/api-core.ts +84 -0
  79. package/src/http/api/api-handler.ts +79 -0
  80. package/src/http/api/api-host.ts +47 -0
  81. package/src/http/api/api-result.ts +56 -0
  82. package/src/http/api/api-schema.ts +154 -0
  83. package/src/http/api/api-server.ts +130 -0
  84. package/src/http/api/api-test.ts +142 -0
  85. package/src/http/api/api-type.ts +34 -0
  86. package/src/http/api/api.ts +81 -0
  87. package/src/http/api/index.ts +11 -0
  88. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  89. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  90. package/src/http/api-adapter/api-result-arktype.ts +294 -0
  91. package/src/http/api-adapter/api-result-zod.ts +286 -0
  92. package/src/http/api-adapter/index.ts +5 -0
  93. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  94. package/src/http/bin/gen-api-list/index.ts +1 -0
  95. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  96. package/src/http/bin/gen-api-test/index.ts +1 -0
  97. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  98. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  99. package/src/http/bin/gen-api-type/index.ts +2 -0
  100. package/src/http/bin/index.ts +2 -0
  101. package/src/http/index.ts +3 -0
  102. package/src/huawei/README.md +1 -0
  103. package/src/huawei/index.ts +2 -0
  104. package/src/huawei/moderation/index.ts +1 -0
  105. package/src/huawei/moderation/moderation.ts +355 -0
  106. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  107. package/src/huawei/obs/index.ts +1 -0
  108. package/src/huawei/obs/obs.ts +42 -0
  109. package/src/index.ts +21 -2
  110. package/src/json/README.md +92 -0
  111. package/src/json/index.ts +1 -0
  112. package/src/json/repair.ts +18 -0
  113. package/src/log/logger.ts +15 -4
  114. package/src/openai/README.md +1 -0
  115. package/src/openai/index.ts +1 -0
  116. package/src/openai/openai.ts +509 -0
  117. package/src/orchestration/README.md +9 -7
  118. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  119. package/src/orchestration/dispatching/index.ts +2 -0
  120. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  121. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  122. package/src/orchestration/dispatching/selector/index.ts +2 -0
  123. package/src/orchestration/index.ts +2 -0
  124. package/src/orchestration/scheduling/index.ts +2 -0
  125. package/src/orchestration/scheduling/scheduler.ts +103 -0
  126. package/src/orchestration/scheduling/task.ts +32 -0
  127. package/src/random/README.md +8 -7
  128. package/src/random/base.ts +66 -0
  129. package/src/random/index.ts +5 -1
  130. package/src/random/random-boolean.ts +40 -0
  131. package/src/random/random-integer.ts +60 -0
  132. package/src/random/random-number.ts +72 -0
  133. package/src/random/random-string.ts +66 -0
  134. package/src/request/README.md +108 -0
  135. package/src/request/fetch/base.ts +108 -0
  136. package/src/request/fetch/browser.ts +280 -0
  137. package/src/request/fetch/general.ts +20 -0
  138. package/src/request/fetch/index.ts +4 -0
  139. package/src/request/fetch/nodejs.ts +280 -0
  140. package/src/request/index.ts +2 -0
  141. package/src/request/request/base.ts +246 -0
  142. package/src/request/request/general.ts +63 -0
  143. package/src/request/request/index.ts +3 -0
  144. package/src/request/request/resource.ts +68 -0
  145. package/src/result/README.md +4 -0
  146. package/src/result/controller.ts +58 -0
  147. package/src/result/either.ts +363 -0
  148. package/src/result/generator.ts +168 -0
  149. package/src/result/index.ts +3 -0
  150. package/src/route/README.md +105 -0
  151. package/src/route/adapter/browser.ts +122 -0
  152. package/src/route/adapter/driver.ts +56 -0
  153. package/src/route/adapter/index.ts +2 -0
  154. package/src/route/index.ts +3 -0
  155. package/src/route/router/index.ts +2 -0
  156. package/src/route/router/route.ts +630 -0
  157. package/src/route/router/router.ts +1641 -0
  158. package/src/route/uri/hash.ts +307 -0
  159. package/src/route/uri/index.ts +7 -0
  160. package/src/route/uri/pathname.ts +376 -0
  161. package/src/route/uri/search.ts +412 -0
  162. package/src/service/README.md +1 -0
  163. package/src/service/index.ts +1 -0
  164. package/src/service/service.ts +110 -0
  165. package/src/socket/README.md +105 -0
  166. package/src/socket/client/index.ts +2 -0
  167. package/src/socket/client/socket-unit.ts +658 -0
  168. package/src/socket/client/socket.ts +203 -0
  169. package/src/socket/common/index.ts +2 -0
  170. package/src/socket/common/socket-unit-common.ts +23 -0
  171. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  172. package/src/socket/index.ts +3 -0
  173. package/src/socket/server/index.ts +3 -0
  174. package/src/socket/server/server.ts +183 -0
  175. package/src/socket/server/socket-unit.ts +448 -0
  176. package/src/socket/server/socket.ts +264 -0
  177. package/src/storage/table.ts +3 -3
  178. package/src/timer/expiration/expiration-manager.ts +3 -3
  179. package/src/timer/expiration/remaining-manager.ts +3 -3
  180. package/src/tube/README.md +99 -0
  181. package/src/tube/helper.ts +137 -0
  182. package/src/tube/index.ts +2 -0
  183. package/src/tube/tube.ts +880 -0
  184. package/src/weixin/README.md +1 -0
  185. package/src/weixin/index.ts +2 -0
  186. package/src/weixin/official-account/authorization.ts +157 -0
  187. package/src/weixin/official-account/index.ts +2 -0
  188. package/src/weixin/official-account/js-api.ts +132 -0
  189. package/src/weixin/open/index.ts +1 -0
  190. package/src/weixin/open/oauth2.ts +131 -0
  191. package/tests/unit/ai/ai.spec.ts +85 -0
  192. package/tests/unit/aio/content.spec.ts +105 -0
  193. package/tests/unit/aio/json.spec.ts +146 -0
  194. package/tests/unit/aio/prompt.spec.ts +111 -0
  195. package/tests/unit/basic/error.spec.ts +16 -4
  196. package/tests/unit/basic/promise.spec.ts +158 -50
  197. package/tests/unit/basic/schedule.spec.ts +74 -0
  198. package/tests/unit/basic/stream.spec.ts +90 -37
  199. package/tests/unit/credential/api-key.spec.ts +36 -0
  200. package/tests/unit/credential/bearer.spec.ts +23 -0
  201. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  202. package/tests/unit/credential/password.spec.ts +40 -0
  203. package/tests/unit/cron/cron.spec.ts +84 -0
  204. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  205. package/tests/unit/event/event-manager.spec.ts +3 -3
  206. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  207. package/tests/unit/exception/error/error.spec.ts +83 -0
  208. package/tests/unit/exception/error/match.spec.ts +81 -0
  209. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  210. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  211. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  212. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  213. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  214. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  215. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  216. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  217. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  218. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  219. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  220. package/tests/unit/http/api/api-server.spec.ts +363 -0
  221. package/tests/unit/http/api/api-test.spec.ts +117 -0
  222. package/tests/unit/http/api/api.spec.ts +121 -0
  223. package/tests/unit/http/api-adapter/node-http.spec.ts +187 -0
  224. package/tests/unit/identifier/uuid.spec.ts +0 -1
  225. package/tests/unit/json/repair.spec.ts +11 -0
  226. package/tests/unit/log/logger.spec.ts +19 -4
  227. package/tests/unit/openai/openai.spec.ts +64 -0
  228. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  229. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  230. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  231. package/tests/unit/random/base.spec.ts +58 -0
  232. package/tests/unit/random/random-boolean.spec.ts +25 -0
  233. package/tests/unit/random/random-integer.spec.ts +32 -0
  234. package/tests/unit/random/random-number.spec.ts +33 -0
  235. package/tests/unit/random/random-string.spec.ts +22 -0
  236. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  237. package/tests/unit/request/fetch/general.spec.ts +43 -0
  238. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  239. package/tests/unit/request/request/base.spec.ts +382 -0
  240. package/tests/unit/request/request/general.spec.ts +160 -0
  241. package/tests/unit/result/controller.spec.ts +82 -0
  242. package/tests/unit/result/either.spec.ts +377 -0
  243. package/tests/unit/result/generator.spec.ts +273 -0
  244. package/tests/unit/route/router/route.spec.ts +430 -0
  245. package/tests/unit/route/router/router.spec.ts +407 -0
  246. package/tests/unit/route/uri/hash.spec.ts +72 -0
  247. package/tests/unit/route/uri/pathname.spec.ts +146 -0
  248. package/tests/unit/route/uri/search.spec.ts +107 -0
  249. package/tests/unit/socket/client.spec.ts +208 -0
  250. package/tests/unit/socket/server.spec.ts +133 -0
  251. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  252. package/tests/unit/tube/helper.spec.ts +139 -0
  253. package/tests/unit/tube/tube.spec.ts +501 -0
  254. package/vite.config.ts +2 -1
  255. package/dist/index.js +0 -50
  256. package/dist/index.js.map +0 -209
  257. package/src/random/string.ts +0 -35
  258. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,107 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ isSearchIncludes,
5
+ isSearchLooseEqual,
6
+ isSearchLooseIncludes,
7
+ isSearchStrictEqual,
8
+ isSearchStrictIncludes,
9
+ neatenQueryString,
10
+ neatenSearch,
11
+ queryObjectToQueryString,
12
+ queryObjectToSearch,
13
+ queryStringToQueryObject,
14
+ queryStringToSearch,
15
+ searchToQueryObject,
16
+ searchToQueryString,
17
+ toQueryObject,
18
+ toQueryString,
19
+ toSearch,
20
+ } from "#Source/route/index.ts"
21
+
22
+ test("neatenSearch ensures the search string starts with a question mark", () => {
23
+ expect(neatenSearch("a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
24
+ expect(neatenSearch("?a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
25
+ })
26
+
27
+ test("neatenQueryString removes a leading question mark when present", () => {
28
+ expect(neatenQueryString("?name=mobius")).toBe("name=mobius")
29
+ expect(neatenQueryString("name=mobius")).toBe("name=mobius")
30
+ })
31
+
32
+ test("queryStringToQueryObject decodes keys and values and preserves later equals signs", () => {
33
+ expect(queryStringToQueryObject("?na%20me=mobius%20model&formula=a%3Db&empty="))
34
+ .toEqual({ "na me": "mobius model", formula: "a=b", empty: "" })
35
+ expect(queryStringToQueryObject("a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
36
+ expect(queryStringToQueryObject("?a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
37
+ })
38
+
39
+ test("queryObjectToQueryString encodes keys and values into a query string", () => {
40
+ expect(queryObjectToQueryString({ "na me": "mobius model", formula: "a=b" }))
41
+ .toBe("na%20me=mobius%20model&formula=a%3Db")
42
+ expect(queryObjectToQueryString({ a: "1", b: "2", c: "3" })).toBe("a=1&b=2&c=3")
43
+ })
44
+
45
+ test("searchToQueryString aliases query-string normalization for search input", () => {
46
+ expect(searchToQueryString("?a=1&b=2&c=3")).toBe("a=1&b=2&c=3")
47
+ })
48
+
49
+ test("searchToQueryObject parses search input into a query object", () => {
50
+ expect(searchToQueryObject("?name=mobius&mode=stable")).toEqual({ name: "mobius", mode: "stable" })
51
+ expect(searchToQueryObject("?a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
52
+ })
53
+
54
+ test("queryStringToSearch aliases search normalization for query-string input", () => {
55
+ expect(queryStringToSearch("a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
56
+ })
57
+
58
+ test("queryObjectToSearch serializes a query object into search format", () => {
59
+ expect(queryObjectToSearch({ name: "mobius model", mode: "a=b" })).toBe("?name=mobius%20model&mode=a%3Db")
60
+ expect(queryObjectToSearch({ a: "1", b: "2", c: "3" })).toBe("?a=1&b=2&c=3")
61
+ })
62
+
63
+ test("toSearch returns search format for both strings and objects", () => {
64
+ expect(toSearch("a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
65
+ expect(toSearch("?a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
66
+ expect(toSearch({ a: "1", b: "2", c: "3" })).toBe("?a=1&b=2&c=3")
67
+ })
68
+
69
+ test("toQueryString returns query-string format for both strings and objects", () => {
70
+ expect(toQueryString("a=1&b=2&c=3")).toBe("a=1&b=2&c=3")
71
+ expect(toQueryString("?a=1&b=2&c=3")).toBe("a=1&b=2&c=3")
72
+ expect(toQueryString({ a: "1", b: "2", c: "3" })).toBe("a=1&b=2&c=3")
73
+ })
74
+
75
+ test("queryObject returns parsed query objects for both strings and objects", () => {
76
+ const queryObject = { a: "1", b: "2", c: "3" }
77
+
78
+ expect(toQueryObject("a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
79
+ expect(toQueryObject("?a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
80
+ expect(toQueryObject(queryObject)).toBe(queryObject)
81
+ expect(toQueryObject("")).toEqual({})
82
+ })
83
+
84
+ test("isSearchLooseIncludes checks whether all requested query keys exist", () => {
85
+ expect(isSearchLooseIncludes("?a=1&b=2", ["a"])).toBe(true)
86
+ expect(isSearchLooseIncludes("?a=1&b=2", ["a", "c"])).toBe(false)
87
+ })
88
+
89
+ test("isSearchStrictIncludes checks whether query key sets are identical", () => {
90
+ expect(isSearchStrictIncludes("?a=1&b=2", ["b", "a"])).toBe(true)
91
+ expect(isSearchStrictIncludes("?ab=1&c=2", ["a", "bc"])).toBe(false)
92
+ })
93
+
94
+ test("isSearchIncludes supports required and optional query keys", () => {
95
+ expect(isSearchIncludes("?a=1&b=2", { a: "required", b: "optional" })).toBe(true)
96
+ expect(isSearchIncludes("?a=1&b=2", { a: "required" })).toBe(false)
97
+ })
98
+
99
+ test("isSearchLooseEqual checks whether target query values are a matching subset", () => {
100
+ expect(isSearchLooseEqual("?a=1&b=2", "?a=1")).toBe(true)
101
+ expect(isSearchLooseEqual("?a=1&b=2", { a: "1", c: "3" })).toBe(false)
102
+ })
103
+
104
+ test("isSearchStrictEqual checks whether query key sets and values are identical", () => {
105
+ expect(isSearchStrictEqual("?a=1&b=2", "?b=2&a=1")).toBe(true)
106
+ expect(isSearchStrictEqual("?a=1&b=2", "?a=1")).toBe(false)
107
+ })
@@ -0,0 +1,208 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import { Client } from "#Source/socket/index.ts"
4
+
5
+ type FakeEventMap = {
6
+ close: { code: number; reason: string }
7
+ error: Event
8
+ message: { data: string }
9
+ open: Event
10
+ }
11
+
12
+ class FakeWebSocket {
13
+ static readonly CONNECTING = 0
14
+ static readonly OPEN = 1
15
+ static readonly CLOSING = 2
16
+ static readonly CLOSED = 3
17
+ static instances: FakeWebSocket[] = []
18
+
19
+ readonly url: string
20
+ readonly sentMessages: string[]
21
+ readyState: number
22
+
23
+ private readonly listeners: {
24
+ [K in keyof FakeEventMap]: Set<(event: FakeEventMap[K]) => void>
25
+ }
26
+
27
+ constructor(url: string) {
28
+ this.url = url
29
+ this.sentMessages = []
30
+ this.readyState = FakeWebSocket.CONNECTING
31
+ this.listeners = {
32
+ close: new Set(),
33
+ error: new Set(),
34
+ message: new Set(),
35
+ open: new Set(),
36
+ }
37
+
38
+ FakeWebSocket.instances.push(this)
39
+ }
40
+
41
+ addEventListener<K extends keyof FakeEventMap>(type: K, listener: (event: FakeEventMap[K]) => void): void {
42
+ this.listeners[type].add(listener)
43
+ }
44
+
45
+ removeEventListener<K extends keyof FakeEventMap>(type: K, listener: (event: FakeEventMap[K]) => void): void {
46
+ this.listeners[type].delete(listener)
47
+ }
48
+
49
+ send(message: string): void {
50
+ if (this.readyState !== FakeWebSocket.OPEN) {
51
+ throw new Error("FakeWebSocket is not open.")
52
+ }
53
+
54
+ this.sentMessages.push(message)
55
+ }
56
+
57
+ close(): void {
58
+ if (this.readyState === FakeWebSocket.CLOSED) {
59
+ return
60
+ }
61
+
62
+ this.readyState = FakeWebSocket.CLOSED
63
+ this.emit("close", { code: 1_000, reason: "" })
64
+ }
65
+
66
+ simulateOpen(): void {
67
+ this.readyState = FakeWebSocket.OPEN
68
+ this.emit("open", new Event("open"))
69
+ }
70
+
71
+ simulateMessage(message: unknown): void {
72
+ this.emit("message", { data: JSON.stringify(message) })
73
+ }
74
+
75
+ simulateError(): void {
76
+ this.emit("error", new Event("error"))
77
+ }
78
+
79
+ private emit<K extends keyof FakeEventMap>(type: K, event: FakeEventMap[K]): void {
80
+ for (const listener of this.listeners[type]) {
81
+ listener(event)
82
+ }
83
+ }
84
+ }
85
+
86
+ afterEach(() => {
87
+ FakeWebSocket.instances = []
88
+ vi.unstubAllGlobals()
89
+ vi.restoreAllMocks()
90
+ })
91
+
92
+ test("Client.SocketUnit queues work until ready, deduplicates status events, and keeps runtime listeners after error", async () => {
93
+ vi.stubGlobal("WebSocket", FakeWebSocket)
94
+
95
+ const socketUnit = new Client.SocketUnit<{ type: string }>({
96
+ url: "ws://socket.test",
97
+ })
98
+ const statuses: string[] = []
99
+ const messages: Array<{ type: string }> = []
100
+
101
+ socketUnit.eventManager.subscribe("status", (status) => {
102
+ statuses.push(status)
103
+ })
104
+ socketUnit.eventManager.subscribe("message", (message) => {
105
+ messages.push(message)
106
+ })
107
+
108
+ socketUnit.setClientId("client-1")
109
+ socketUnit.sendMessage({ type: "queued-before-open" })
110
+
111
+ expect(socketUnit.getSnapshot().pendingActionCount).toBe(1)
112
+
113
+ const openPromise = socketUnit.open()
114
+ const firstWebSocket = FakeWebSocket.instances[0]!
115
+
116
+ expect(socketUnit.getStatus()).toBe("CONNECTING")
117
+
118
+ firstWebSocket.simulateOpen()
119
+ await openPromise
120
+
121
+ expect(firstWebSocket.sentMessages).toEqual([
122
+ JSON.stringify({ type: "queued-before-open" }),
123
+ ])
124
+ expect(statuses).toEqual(["CONNECTING", "OPEN"])
125
+
126
+ firstWebSocket.simulateMessage({ type: "message-1" })
127
+ firstWebSocket.simulateMessage({ type: "message-2" })
128
+ firstWebSocket.simulateError()
129
+ firstWebSocket.simulateMessage({ type: "message-3" })
130
+
131
+ expect(messages).toEqual([
132
+ { type: "message-1" },
133
+ { type: "message-2" },
134
+ { type: "message-3" },
135
+ ])
136
+ expect(statuses).toEqual(["CONNECTING", "OPEN"])
137
+
138
+ await expect(socketUnit.safeOpen()).rejects.toThrow("Socket is already open")
139
+
140
+ await socketUnit.close()
141
+ expect(socketUnit.getStatus()).toBe("UNINSTANTIATED")
142
+
143
+ let resetCount = 0
144
+ await socketUnit.reset(() => {
145
+ resetCount = resetCount + 1
146
+ })
147
+
148
+ expect(resetCount).toBe(1)
149
+ expect(socketUnit.getSnapshot()).toMatchObject({
150
+ clientId: undefined,
151
+ pendingActionCount: 0,
152
+ status: "UNINSTANTIATED",
153
+ })
154
+ })
155
+
156
+ test("Client.Socket emits connect close and message events from stable ready-to-work transitions", async () => {
157
+ vi.stubGlobal("WebSocket", FakeWebSocket)
158
+
159
+ const socket = new Client.Socket<{ type: string }>({
160
+ url: "ws://socket.test",
161
+ })
162
+ const connectClientIds: Array<string | undefined> = []
163
+ const closeClientIds: Array<string | undefined> = []
164
+ const forwardedMessages: Array<{ clientId: string; message: { type: string } }> = []
165
+
166
+ socket.eventManager.subscribe("connect", ({ clientId }) => {
167
+ connectClientIds.push(clientId)
168
+ })
169
+ socket.eventManager.subscribe("close", ({ clientId }) => {
170
+ closeClientIds.push(clientId)
171
+ })
172
+ socket.eventManager.subscribe("message", (payload) => {
173
+ forwardedMessages.push(payload)
174
+ })
175
+
176
+ socket.setClientId("client-2")
177
+ socket.sendMessage({ type: "queued-before-open" })
178
+
179
+ const openPromise = socket.open()
180
+ const firstWebSocket = FakeWebSocket.instances[0]!
181
+ firstWebSocket.simulateOpen()
182
+ await openPromise
183
+
184
+ expect(connectClientIds).toEqual(["client-2"])
185
+ expect(firstWebSocket.sentMessages).toEqual([
186
+ JSON.stringify({ type: "queued-before-open" }),
187
+ ])
188
+
189
+ firstWebSocket.simulateMessage({ type: "hello" })
190
+ firstWebSocket.simulateMessage({ type: "hello-again" })
191
+
192
+ expect(forwardedMessages).toEqual([
193
+ { clientId: "client-2", message: { type: "hello" } },
194
+ { clientId: "client-2", message: { type: "hello-again" } },
195
+ ])
196
+ expect(connectClientIds).toEqual(["client-2"])
197
+
198
+ await socket.close()
199
+ expect(closeClientIds).toEqual(["client-2"])
200
+
201
+ socket.reset()
202
+
203
+ expect(socket.getSnapshot()).toMatchObject({
204
+ clientId: "client-2",
205
+ isReadyToWork: false,
206
+ status: "UNINSTANTIATED",
207
+ })
208
+ })
@@ -0,0 +1,133 @@
1
+ import type { AddressInfo } from "node:net"
2
+
3
+ import { afterEach, expect, test, vi } from "vitest"
4
+ import { WebSocket as WsClient } from "ws"
5
+
6
+ import { Server } from "#Source/socket/index.ts"
7
+
8
+ afterEach(() => {
9
+ vi.restoreAllMocks()
10
+ })
11
+
12
+ test("Server.createWebSocketServer returns usable local URLs and stable lifecycle helpers", async () => {
13
+ const webSocketServer = Server.createWebSocketServer({ port: 0 })
14
+
15
+ expect(webSocketServer.isRunning()).toBe(false)
16
+
17
+ const runningResult = await webSocketServer.run()
18
+ const address = webSocketServer.safeGetServer().address() as AddressInfo
19
+
20
+ expect(runningResult.server).toBe(webSocketServer.safeGetServer())
21
+ expect(runningResult.urlList).toContain(`ws://127.0.0.1:${address.port}`)
22
+ expect(runningResult.urlList).toContain(`ws://localhost:${address.port}`)
23
+ expect(runningResult.urlList).toContain(`ws://[::1]:${address.port}`)
24
+ expect(webSocketServer.isRunning()).toBe(true)
25
+
26
+ await webSocketServer.close()
27
+
28
+ expect(webSocketServer.isRunning()).toBe(false)
29
+ })
30
+
31
+ test("Server.Socket attach does not take over an already running server lifecycle", async () => {
32
+ const webSocketServer = Server.createWebSocketServer({ port: 0 })
33
+ await webSocketServer.run()
34
+
35
+ const socket = new Server.Socket<{ type: string }>({
36
+ webSocketServer,
37
+ onMessage: () => {
38
+ // no op
39
+ },
40
+ })
41
+
42
+ socket.attach()
43
+
44
+ expect(socket.getSnapshot()).toMatchObject({
45
+ isRunning: true,
46
+ isStarted: true,
47
+ managesWebSocketServerLifecycle: false,
48
+ })
49
+
50
+ await socket.close()
51
+
52
+ expect(webSocketServer.isRunning()).toBe(true)
53
+
54
+ await webSocketServer.close()
55
+ })
56
+
57
+ test("Server.Socket exposes stable connect timing and forwards messages for active connections", async () => {
58
+ const webSocketServer = Server.createWebSocketServer({ port: 0 })
59
+ const socket = new Server.Socket<{ type: string }>({
60
+ webSocketServer,
61
+ onMessage: () => {
62
+ // no-op
63
+ },
64
+ })
65
+
66
+ const connectPromise = new Promise<string>((resolve) => {
67
+ socket.eventManager.subscribe("connect", ({ clientId }) => {
68
+ const socketUnitSnapshot = socket.getSocketUnitSnapshot(clientId)
69
+
70
+ expect(socketUnitSnapshot).toMatchObject({
71
+ clientId,
72
+ isReadyToWork: true,
73
+ status: "OPEN",
74
+ })
75
+
76
+ resolve(clientId)
77
+ })
78
+ })
79
+ const inboundMessagePromise = new Promise<{ clientId: string; message: { type: string } }>((resolve) => {
80
+ socket.eventManager.subscribe("message", (payload) => {
81
+ resolve(payload)
82
+ })
83
+ })
84
+ const closePromise = new Promise<string>((resolve) => {
85
+ socket.eventManager.subscribe("close", ({ clientId }) => {
86
+ resolve(clientId)
87
+ })
88
+ })
89
+
90
+ await socket.open()
91
+
92
+ const address = webSocketServer.safeGetServer().address() as AddressInfo
93
+ const client = new WsClient(`ws://127.0.0.1:${address.port}`)
94
+
95
+ await new Promise<void>((resolve, reject) => {
96
+ client.once("open", () => {
97
+ resolve()
98
+ })
99
+ client.once("error", reject)
100
+ })
101
+
102
+ const clientId = await connectPromise
103
+
104
+ client.send(JSON.stringify({ type: "hello-server" }))
105
+
106
+ await expect(inboundMessagePromise).resolves.toEqual({
107
+ clientId,
108
+ message: { type: "hello-server" },
109
+ })
110
+
111
+ const outboundMessagePromise = new Promise<string>((resolve, reject) => {
112
+ client.once("message", (message) => {
113
+ resolve(message.toString())
114
+ })
115
+ client.once("error", reject)
116
+ })
117
+
118
+ socket.sendMessage(clientId, { type: "hello-client" })
119
+
120
+ await expect(outboundMessagePromise).resolves.toBe(JSON.stringify({ type: "hello-client" }))
121
+
122
+ client.close()
123
+
124
+ await expect(closePromise).resolves.toBe(clientId)
125
+
126
+ await socket.close()
127
+
128
+ expect(socket.getSnapshot()).toMatchObject({
129
+ activeSocketUnitCount: 0,
130
+ isRunning: false,
131
+ isStarted: false,
132
+ })
133
+ })
@@ -0,0 +1,214 @@
1
+ import { afterEach, expect, test, vi } from "vitest"
2
+
3
+ import {
4
+ SocketUnitHeartbeat,
5
+ resolveSocketUnitHeartbeatOptions,
6
+ validateSocketUnitHeartbeatOptions,
7
+ } from "#Source/socket/index.ts"
8
+
9
+ afterEach(() => {
10
+ vi.clearAllTimers()
11
+ vi.useRealTimers()
12
+ })
13
+
14
+ test("SocketUnitHeartbeat validates required handlers and resolves default values", () => {
15
+ expect(() => validateSocketUnitHeartbeatOptions({
16
+ enableActiveHeartbeat: true,
17
+ })).toThrow("Active heartbeat message handler is required")
18
+
19
+ expect(() => validateSocketUnitHeartbeatOptions({
20
+ enablePassiveHeartbeat: true,
21
+ })).toThrow("Passive heartbeat message handler is required")
22
+
23
+ expect(() => validateSocketUnitHeartbeatOptions({
24
+ heartbeatInterval: 0,
25
+ })).toThrow("Heartbeat interval must be a positive finite number.")
26
+
27
+ expect(() => validateSocketUnitHeartbeatOptions({
28
+ maxResponseTime: 0,
29
+ })).toThrow("Max response time must be a positive finite number.")
30
+
31
+ expect(() => validateSocketUnitHeartbeatOptions({
32
+ maxHeartbeatLostCount: 0,
33
+ })).toThrow("Max heartbeat lost count must be at least 1.")
34
+
35
+ const resolvedOptions = resolveSocketUnitHeartbeatOptions({})
36
+
37
+ expect(resolvedOptions).toMatchObject({
38
+ enableActiveHeartbeat: false,
39
+ enablePassiveHeartbeat: false,
40
+ heartbeatInterval: 30_000,
41
+ maxResponseTime: 10_000,
42
+ maxHeartbeatLostCount: 3,
43
+ })
44
+ })
45
+
46
+ test("SocketUnitHeartbeat unifies active and passive handling with newest-first matching", async () => {
47
+ vi.useFakeTimers()
48
+
49
+ const sentMessages: Array<{ action: string; messageString: string }> = []
50
+ const close = vi.fn(() => {
51
+ throw new Error("Heartbeat close should not be called in this test.")
52
+ })
53
+ const heartbeat = new SocketUnitHeartbeat<{ kind: string; round?: number }>({
54
+ enableActiveHeartbeat: true,
55
+ enablePassiveHeartbeat: true,
56
+ heartbeatInterval: 100,
57
+ maxResponseTime: 500,
58
+ maxHeartbeatLostCount: 2,
59
+ activeHeartbeatMessageHandlerGenerator: () => {
60
+ return {
61
+ buildPingMessage: () => {
62
+ return { kind: "ping" }
63
+ },
64
+ verifyPongMessage: (_clientId, message) => {
65
+ return message.kind === "pong"
66
+ },
67
+ }
68
+ },
69
+ passiveHeartbeatMessageHandlerGenerator: () => {
70
+ return {
71
+ verifyPingMessage: (_clientId, message) => {
72
+ return message.kind === "ping-from-peer"
73
+ },
74
+ buildPongMessage: () => {
75
+ return { kind: "pong-from-peer" }
76
+ },
77
+ }
78
+ },
79
+ }, {
80
+ sendMessage: (message) => {
81
+ sentMessages.push(message)
82
+ return true
83
+ },
84
+ close,
85
+ })
86
+
87
+ expect(heartbeat.getSnapshot()).toMatchObject({
88
+ enabledActiveHeartbeat: true,
89
+ enabledPassiveHeartbeat: true,
90
+ failCount: 0,
91
+ isRunning: false,
92
+ recentStates: [],
93
+ successCount: 0,
94
+ waitingCount: 0,
95
+ })
96
+
97
+ heartbeat.start("client-1")
98
+
99
+ expect(heartbeat.getSnapshot().isRunning).toBe(true)
100
+
101
+ await vi.advanceTimersByTimeAsync(100)
102
+ await vi.advanceTimersByTimeAsync(100)
103
+
104
+ const waitingSnapshot = heartbeat.getSnapshot()
105
+
106
+ expect(sentMessages).toHaveLength(2)
107
+ expect(waitingSnapshot.waitingCount).toBe(2)
108
+ expect(waitingSnapshot.recentStates).toHaveLength(2)
109
+ expect(waitingSnapshot.recentStates.every((state) => {
110
+ return state.status === "waiting" && state.hasPendingResponseTimer === true
111
+ })).toBe(true)
112
+
113
+ const activeHandled = heartbeat.handleMessage("client-1", { kind: "pong" })
114
+
115
+ expect(activeHandled).toBe(true)
116
+ expect(heartbeat.getSnapshot()).toMatchObject({
117
+ failCount: 0,
118
+ recentStates: [],
119
+ successCount: 0,
120
+ waitingCount: 0,
121
+ })
122
+
123
+ const passiveHandled = heartbeat.handleMessage("client-1", { kind: "ping-from-peer" })
124
+
125
+ expect(passiveHandled).toBe(true)
126
+ expect(sentMessages.at(-1)).toEqual({
127
+ action: "Passive heartbeat pong send",
128
+ messageString: JSON.stringify({ kind: "pong-from-peer" }),
129
+ })
130
+
131
+ heartbeat.stop()
132
+
133
+ expect(close).not.toHaveBeenCalled()
134
+ expect(heartbeat.getSnapshot()).toMatchObject({
135
+ isRunning: false,
136
+ recentStates: [],
137
+ waitingCount: 0,
138
+ })
139
+ })
140
+
141
+ test("SocketUnitHeartbeat closes transport after consecutive timeout limit is reached", async () => {
142
+ vi.useFakeTimers()
143
+
144
+ let closeCount = 0
145
+ const heartbeat = new SocketUnitHeartbeat<{ kind: string }>({
146
+ enableActiveHeartbeat: true,
147
+ heartbeatInterval: 100,
148
+ maxResponseTime: 10,
149
+ maxHeartbeatLostCount: 2,
150
+ activeHeartbeatMessageHandlerGenerator: () => {
151
+ return {
152
+ buildPingMessage: () => {
153
+ return { kind: "ping" }
154
+ },
155
+ verifyPongMessage: () => {
156
+ return false
157
+ },
158
+ }
159
+ },
160
+ }, {
161
+ sendMessage: () => {
162
+ return true
163
+ },
164
+ close: () => {
165
+ closeCount = closeCount + 1
166
+ },
167
+ })
168
+
169
+ heartbeat.start("client-2")
170
+
171
+ await vi.advanceTimersByTimeAsync(100)
172
+ await vi.advanceTimersByTimeAsync(10)
173
+ expect(heartbeat.getSnapshot().failCount).toBe(1)
174
+ expect(closeCount).toBe(0)
175
+
176
+ await vi.advanceTimersByTimeAsync(100)
177
+ await vi.advanceTimersByTimeAsync(10)
178
+ expect(heartbeat.getSnapshot().failCount).toBeGreaterThanOrEqual(1)
179
+ expect(closeCount).toBe(1)
180
+ })
181
+
182
+ test("SocketUnitHeartbeat stays idle when active heartbeat is disabled and ignores unmatched messages", () => {
183
+ const heartbeat = new SocketUnitHeartbeat<{ kind: string }>({
184
+ enablePassiveHeartbeat: true,
185
+ passiveHeartbeatMessageHandlerGenerator: () => {
186
+ return {
187
+ verifyPingMessage: (_clientId, message) => {
188
+ return message.kind === "ping"
189
+ },
190
+ buildPongMessage: () => {
191
+ return { kind: "pong" }
192
+ },
193
+ }
194
+ },
195
+ }, {
196
+ sendMessage: () => {
197
+ return true
198
+ },
199
+ close: () => {
200
+ throw new Error("Heartbeat close should not be called in this test.")
201
+ },
202
+ })
203
+
204
+ heartbeat.start("client-3")
205
+
206
+ expect(heartbeat.getSnapshot()).toMatchObject({
207
+ isRunning: false,
208
+ recentStates: [],
209
+ waitingCount: 0,
210
+ })
211
+
212
+ expect(heartbeat.handleMessage("client-3", { kind: "unmatched" })).toBe(false)
213
+ expect(heartbeat.handleMessage("client-3", { kind: "ping" })).toBe(true)
214
+ })