@planet-matrix/mobius-model 0.5.0 → 0.9.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 (379) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +123 -36
  3. package/dist/index.js +715 -4
  4. package/dist/index.js.map +981 -13
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +36 -18
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/ai/README.md +1 -0
  12. package/src/ai/ai.ts +107 -0
  13. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  14. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  15. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  16. package/src/ai/chat-completion-ai/index.ts +7 -0
  17. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  18. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  19. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  20. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  21. package/src/ai/embedding-ai/embedding.ts +50 -0
  22. package/src/ai/embedding-ai/index.ts +4 -0
  23. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  24. package/src/ai/index.ts +4 -0
  25. package/src/aio/README.md +100 -0
  26. package/src/aio/content.ts +141 -0
  27. package/src/aio/index.ts +3 -0
  28. package/src/aio/json.ts +127 -0
  29. package/src/aio/prompt.ts +246 -0
  30. package/src/basic/README.md +72 -116
  31. package/src/basic/error.ts +19 -5
  32. package/src/basic/function.ts +83 -64
  33. package/src/basic/index.ts +1 -0
  34. package/src/basic/is.ts +152 -71
  35. package/src/basic/promise.ts +29 -8
  36. package/src/basic/schedule.ts +111 -0
  37. package/src/basic/stream.ts +135 -25
  38. package/src/basic/string.ts +2 -33
  39. package/src/color/README.md +105 -0
  40. package/src/color/index.ts +3 -0
  41. package/src/color/internal.ts +42 -0
  42. package/src/color/rgb/analyze.ts +236 -0
  43. package/src/color/rgb/construct.ts +130 -0
  44. package/src/color/rgb/convert.ts +227 -0
  45. package/src/color/rgb/derive.ts +303 -0
  46. package/src/color/rgb/index.ts +6 -0
  47. package/src/color/rgb/internal.ts +208 -0
  48. package/src/color/rgb/parse.ts +302 -0
  49. package/src/color/rgb/serialize.ts +144 -0
  50. package/src/color/types.ts +57 -0
  51. package/src/color/xyz/analyze.ts +80 -0
  52. package/src/color/xyz/construct.ts +19 -0
  53. package/src/color/xyz/convert.ts +71 -0
  54. package/src/color/xyz/index.ts +3 -0
  55. package/src/color/xyz/internal.ts +23 -0
  56. package/src/credential/README.md +107 -0
  57. package/src/credential/api-key.ts +158 -0
  58. package/src/credential/bearer.ts +73 -0
  59. package/src/credential/index.ts +4 -0
  60. package/src/credential/json-web-token.ts +96 -0
  61. package/src/credential/password.ts +170 -0
  62. package/src/cron/README.md +86 -0
  63. package/src/cron/cron.ts +87 -0
  64. package/src/cron/index.ts +1 -0
  65. package/src/css/README.md +93 -0
  66. package/src/css/class.ts +559 -0
  67. package/src/css/index.ts +1 -0
  68. package/src/drizzle/README.md +1 -0
  69. package/src/drizzle/drizzle.ts +1 -0
  70. package/src/drizzle/helper.ts +47 -0
  71. package/src/drizzle/index.ts +5 -0
  72. package/src/drizzle/infer.ts +52 -0
  73. package/src/drizzle/kysely.ts +8 -0
  74. package/src/drizzle/pagination.ts +200 -0
  75. package/src/email/README.md +1 -0
  76. package/src/email/index.ts +1 -0
  77. package/src/email/resend.ts +25 -0
  78. package/src/encoding/README.md +66 -79
  79. package/src/encoding/base64.ts +13 -4
  80. package/src/environment/README.md +97 -0
  81. package/src/environment/basic.ts +26 -0
  82. package/src/environment/device.ts +311 -0
  83. package/src/environment/feature.ts +285 -0
  84. package/src/environment/geo.ts +337 -0
  85. package/src/environment/index.ts +7 -0
  86. package/src/environment/runtime.ts +400 -0
  87. package/src/environment/snapshot.ts +60 -0
  88. package/src/environment/variable.ts +239 -0
  89. package/src/event/README.md +90 -0
  90. package/src/event/class-event-proxy.ts +229 -0
  91. package/src/event/common.ts +29 -0
  92. package/src/event/event-manager.ts +203 -0
  93. package/src/event/index.ts +4 -0
  94. package/src/event/instance-event-proxy.ts +187 -0
  95. package/src/event/internal.ts +24 -0
  96. package/src/exception/README.md +96 -0
  97. package/src/exception/browser.ts +219 -0
  98. package/src/exception/index.ts +4 -0
  99. package/src/exception/nodejs.ts +169 -0
  100. package/src/exception/normalize.ts +106 -0
  101. package/src/exception/types.ts +99 -0
  102. package/src/form/README.md +25 -0
  103. package/src/form/index.ts +1 -0
  104. package/src/form/inputor-controller/base.ts +874 -0
  105. package/src/form/inputor-controller/boolean.ts +39 -0
  106. package/src/form/inputor-controller/file.ts +39 -0
  107. package/src/form/inputor-controller/form.ts +181 -0
  108. package/src/form/inputor-controller/helper.ts +117 -0
  109. package/src/form/inputor-controller/index.ts +17 -0
  110. package/src/form/inputor-controller/multi-select.ts +99 -0
  111. package/src/form/inputor-controller/number.ts +116 -0
  112. package/src/form/inputor-controller/select.ts +109 -0
  113. package/src/form/inputor-controller/text.ts +82 -0
  114. package/src/http/READMD.md +1 -0
  115. package/src/http/api/api-core.ts +84 -0
  116. package/src/http/api/api-handler.ts +79 -0
  117. package/src/http/api/api-host.ts +47 -0
  118. package/src/http/api/api-result.ts +56 -0
  119. package/src/http/api/api-schema.ts +154 -0
  120. package/src/http/api/api-server.ts +130 -0
  121. package/src/http/api/api-test.ts +142 -0
  122. package/src/http/api/api-type.ts +37 -0
  123. package/src/http/api/api.ts +81 -0
  124. package/src/http/api/index.ts +11 -0
  125. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  126. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  127. package/src/http/api-adapter/api-result-arktype.ts +297 -0
  128. package/src/http/api-adapter/api-result-zod.ts +286 -0
  129. package/src/http/api-adapter/index.ts +5 -0
  130. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  131. package/src/http/bin/gen-api-list/index.ts +1 -0
  132. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  133. package/src/http/bin/gen-api-test/index.ts +1 -0
  134. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  135. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  136. package/src/http/bin/gen-api-type/index.ts +2 -0
  137. package/src/http/bin/index.ts +2 -0
  138. package/src/http/index.ts +3 -0
  139. package/src/huawei/README.md +1 -0
  140. package/src/huawei/index.ts +2 -0
  141. package/src/huawei/moderation/index.ts +1 -0
  142. package/src/huawei/moderation/moderation.ts +355 -0
  143. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  144. package/src/huawei/obs/index.ts +1 -0
  145. package/src/huawei/obs/obs.ts +42 -0
  146. package/src/identifier/README.md +92 -0
  147. package/src/identifier/id.ts +119 -0
  148. package/src/identifier/index.ts +2 -0
  149. package/src/identifier/uuid.ts +187 -0
  150. package/src/index.ts +33 -1
  151. package/src/json/README.md +92 -0
  152. package/src/json/index.ts +1 -0
  153. package/src/json/repair.ts +18 -0
  154. package/src/log/README.md +79 -0
  155. package/src/log/index.ts +5 -0
  156. package/src/log/log-emitter.ts +72 -0
  157. package/src/log/log-record.ts +10 -0
  158. package/src/log/log-scheduler.ts +74 -0
  159. package/src/log/log-type.ts +8 -0
  160. package/src/log/logger.ts +554 -0
  161. package/src/openai/README.md +1 -0
  162. package/src/openai/index.ts +1 -0
  163. package/src/openai/openai.ts +510 -0
  164. package/src/orchestration/README.md +91 -0
  165. package/src/orchestration/coordination/barrier.ts +214 -0
  166. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  167. package/src/orchestration/coordination/errors.ts +98 -0
  168. package/src/orchestration/coordination/index.ts +16 -0
  169. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  170. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  171. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  172. package/src/orchestration/coordination/mutex.ts +257 -0
  173. package/src/orchestration/coordination/permit.ts +127 -0
  174. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  175. package/src/orchestration/coordination/semaphore.ts +280 -0
  176. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  177. package/src/orchestration/dispatching/index.ts +2 -0
  178. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  179. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  180. package/src/orchestration/dispatching/selector/index.ts +2 -0
  181. package/src/orchestration/index.ts +3 -0
  182. package/src/orchestration/scheduling/index.ts +2 -0
  183. package/src/orchestration/scheduling/scheduler.ts +103 -0
  184. package/src/orchestration/scheduling/task.ts +32 -0
  185. package/src/random/README.md +56 -86
  186. package/src/random/base.ts +66 -0
  187. package/src/random/index.ts +5 -1
  188. package/src/random/random-boolean.ts +40 -0
  189. package/src/random/random-integer.ts +60 -0
  190. package/src/random/random-number.ts +72 -0
  191. package/src/random/random-string.ts +66 -0
  192. package/src/reactor/README.md +4 -0
  193. package/src/reactor/reactor-core/primitive.ts +9 -9
  194. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  195. package/src/request/README.md +108 -0
  196. package/src/request/fetch/base.ts +108 -0
  197. package/src/request/fetch/browser.ts +285 -0
  198. package/src/request/fetch/general.ts +20 -0
  199. package/src/request/fetch/index.ts +4 -0
  200. package/src/request/fetch/nodejs.ts +285 -0
  201. package/src/request/index.ts +2 -0
  202. package/src/request/request/base.ts +250 -0
  203. package/src/request/request/general.ts +64 -0
  204. package/src/request/request/index.ts +3 -0
  205. package/src/request/request/resource.ts +68 -0
  206. package/src/result/README.md +4 -0
  207. package/src/result/controller.ts +54 -0
  208. package/src/result/either.ts +193 -0
  209. package/src/result/index.ts +2 -0
  210. package/src/route/README.md +105 -0
  211. package/src/route/adapter/browser.ts +122 -0
  212. package/src/route/adapter/driver.ts +56 -0
  213. package/src/route/adapter/index.ts +2 -0
  214. package/src/route/index.ts +3 -0
  215. package/src/route/router/index.ts +2 -0
  216. package/src/route/router/route.ts +630 -0
  217. package/src/route/router/router.ts +1642 -0
  218. package/src/route/uri/hash.ts +308 -0
  219. package/src/route/uri/index.ts +7 -0
  220. package/src/route/uri/pathname.ts +376 -0
  221. package/src/route/uri/search.ts +413 -0
  222. package/src/singleton/README.md +79 -0
  223. package/src/singleton/factory.ts +55 -0
  224. package/src/singleton/index.ts +2 -0
  225. package/src/singleton/manager.ts +204 -0
  226. package/src/socket/README.md +105 -0
  227. package/src/socket/client/index.ts +2 -0
  228. package/src/socket/client/socket-unit.ts +660 -0
  229. package/src/socket/client/socket.ts +203 -0
  230. package/src/socket/common/index.ts +2 -0
  231. package/src/socket/common/socket-unit-common.ts +23 -0
  232. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  233. package/src/socket/index.ts +3 -0
  234. package/src/socket/server/index.ts +3 -0
  235. package/src/socket/server/server.ts +183 -0
  236. package/src/socket/server/socket-unit.ts +449 -0
  237. package/src/socket/server/socket.ts +264 -0
  238. package/src/storage/README.md +107 -0
  239. package/src/storage/index.ts +1 -0
  240. package/src/storage/table.ts +449 -0
  241. package/src/timer/README.md +86 -0
  242. package/src/timer/expiration/expiration-manager.ts +594 -0
  243. package/src/timer/expiration/index.ts +3 -0
  244. package/src/timer/expiration/min-heap.ts +208 -0
  245. package/src/timer/expiration/remaining-manager.ts +241 -0
  246. package/src/timer/index.ts +1 -0
  247. package/src/tube/README.md +99 -0
  248. package/src/tube/helper.ts +138 -0
  249. package/src/tube/index.ts +2 -0
  250. package/src/tube/tube.ts +880 -0
  251. package/src/type/README.md +54 -307
  252. package/src/type/class.ts +2 -2
  253. package/src/type/index.ts +14 -14
  254. package/src/type/is.ts +265 -2
  255. package/src/type/object.ts +37 -0
  256. package/src/type/string.ts +7 -2
  257. package/src/type/tuple.ts +6 -6
  258. package/src/type/union.ts +16 -0
  259. package/src/web/README.md +77 -0
  260. package/src/web/capture.ts +35 -0
  261. package/src/web/clipboard.ts +97 -0
  262. package/src/web/dom.ts +117 -0
  263. package/src/web/download.ts +16 -0
  264. package/src/web/event.ts +46 -0
  265. package/src/web/index.ts +10 -0
  266. package/src/web/local-storage.ts +113 -0
  267. package/src/web/location.ts +28 -0
  268. package/src/web/permission.ts +172 -0
  269. package/src/web/script-loader.ts +432 -0
  270. package/src/weixin/README.md +1 -0
  271. package/src/weixin/index.ts +2 -0
  272. package/src/weixin/official-account/authorization.ts +159 -0
  273. package/src/weixin/official-account/index.ts +2 -0
  274. package/src/weixin/official-account/js-api.ts +134 -0
  275. package/src/weixin/open/index.ts +1 -0
  276. package/src/weixin/open/oauth2.ts +133 -0
  277. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  278. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  279. package/tests/unit/ai/ai.spec.ts +85 -0
  280. package/tests/unit/aio/content.spec.ts +105 -0
  281. package/tests/unit/aio/json.spec.ts +147 -0
  282. package/tests/unit/aio/prompt.spec.ts +111 -0
  283. package/tests/unit/basic/array.spec.ts +1 -1
  284. package/tests/unit/basic/error.spec.ts +16 -4
  285. package/tests/unit/basic/schedule.spec.ts +74 -0
  286. package/tests/unit/basic/stream.spec.ts +91 -38
  287. package/tests/unit/basic/string.spec.ts +0 -9
  288. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  289. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  290. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  291. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  292. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  293. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  294. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  295. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  296. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  297. package/tests/unit/credential/api-key.spec.ts +37 -0
  298. package/tests/unit/credential/bearer.spec.ts +23 -0
  299. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  300. package/tests/unit/credential/password.spec.ts +41 -0
  301. package/tests/unit/cron/cron.spec.ts +84 -0
  302. package/tests/unit/css/class.spec.ts +157 -0
  303. package/tests/unit/environment/basic.spec.ts +20 -0
  304. package/tests/unit/environment/device.spec.ts +146 -0
  305. package/tests/unit/environment/feature.spec.ts +388 -0
  306. package/tests/unit/environment/geo.spec.ts +111 -0
  307. package/tests/unit/environment/runtime.spec.ts +364 -0
  308. package/tests/unit/environment/snapshot.spec.ts +4 -0
  309. package/tests/unit/environment/variable.spec.ts +190 -0
  310. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  311. package/tests/unit/event/event-manager.spec.ts +246 -0
  312. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  313. package/tests/unit/exception/browser.spec.ts +213 -0
  314. package/tests/unit/exception/nodejs.spec.ts +144 -0
  315. package/tests/unit/exception/normalize.spec.ts +57 -0
  316. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  317. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  318. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  319. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  320. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  321. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  322. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  323. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  324. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  325. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  326. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  327. package/tests/unit/http/api/api-server.spec.ts +363 -0
  328. package/tests/unit/http/api/api-test.spec.ts +117 -0
  329. package/tests/unit/http/api/api.spec.ts +121 -0
  330. package/tests/unit/http/api-adapter/node-http.spec.ts +191 -0
  331. package/tests/unit/identifier/id.spec.ts +71 -0
  332. package/tests/unit/identifier/uuid.spec.ts +85 -0
  333. package/tests/unit/json/repair.spec.ts +11 -0
  334. package/tests/unit/log/log-emitter.spec.ts +33 -0
  335. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  336. package/tests/unit/log/log-type.spec.ts +7 -0
  337. package/tests/unit/log/logger.spec.ts +237 -0
  338. package/tests/unit/openai/openai.spec.ts +64 -0
  339. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  340. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  341. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  342. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  343. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  344. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  345. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  346. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  347. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  348. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  349. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  350. package/tests/unit/random/base.spec.ts +58 -0
  351. package/tests/unit/random/random-boolean.spec.ts +25 -0
  352. package/tests/unit/random/random-integer.spec.ts +32 -0
  353. package/tests/unit/random/random-number.spec.ts +33 -0
  354. package/tests/unit/random/random-string.spec.ts +22 -0
  355. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  356. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  357. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  358. package/tests/unit/request/fetch/general.spec.ts +43 -0
  359. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  360. package/tests/unit/request/request/base.spec.ts +385 -0
  361. package/tests/unit/request/request/general.spec.ts +161 -0
  362. package/tests/unit/route/router/route.spec.ts +431 -0
  363. package/tests/unit/route/router/router.spec.ts +407 -0
  364. package/tests/unit/route/uri/hash.spec.ts +72 -0
  365. package/tests/unit/route/uri/pathname.spec.ts +147 -0
  366. package/tests/unit/route/uri/search.spec.ts +107 -0
  367. package/tests/unit/singleton/singleton.spec.ts +49 -0
  368. package/tests/unit/socket/client.spec.ts +208 -0
  369. package/tests/unit/socket/server.spec.ts +135 -0
  370. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  371. package/tests/unit/storage/table.spec.ts +620 -0
  372. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  373. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  374. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  375. package/tests/unit/tube/helper.spec.ts +139 -0
  376. package/tests/unit/tube/tube.spec.ts +501 -0
  377. package/.oxlintrc.json +0 -5
  378. package/src/random/uuid.ts +0 -103
  379. package/tests/unit/random/uuid.spec.ts +0 -37
@@ -0,0 +1,183 @@
1
+ import type { AddressInfo } from "node:net"
2
+ import { networkInterfaces } from "node:os"
3
+
4
+ import { WebSocketServer as Server } from "ws"
5
+
6
+ const internalFormatWebSocketHost = (address: string): string => {
7
+ const normalizedAddress = address.replaceAll("%", "%25")
8
+
9
+ if (normalizedAddress.includes(":")) {
10
+ return `[${normalizedAddress}]`
11
+ }
12
+ return normalizedAddress
13
+ }
14
+
15
+ const internalBuildWebSocketUrlList = (port: number): string[] => {
16
+ const urlSet = new Set<string>([
17
+ `ws://127.0.0.1:${port}`,
18
+ `ws://localhost:${port}`,
19
+ `ws://[::1]:${port}`,
20
+ ])
21
+
22
+ for (const networkInterfaceInfo of Object.values(networkInterfaces()).flat()) {
23
+ if (
24
+ networkInterfaceInfo === undefined
25
+ || networkInterfaceInfo.address.length === 0
26
+ || networkInterfaceInfo.internal === true
27
+ || networkInterfaceInfo.address === "0.0.0.0"
28
+ || networkInterfaceInfo.address === "::"
29
+ ) {
30
+ continue
31
+ }
32
+
33
+ const address = internalFormatWebSocketHost(networkInterfaceInfo.address)
34
+ urlSet.add(`ws://${address}:${port}`)
35
+ }
36
+
37
+ return [...urlSet]
38
+ }
39
+
40
+ const internalGetListeningPort = (server: Server): number => {
41
+ const address = server.address()
42
+
43
+ if (address === null) {
44
+ throw new Error("Server is not running.")
45
+ }
46
+
47
+ if (typeof address === "string") {
48
+ throw new TypeError("WebSocket server is not listening on a TCP port.")
49
+ }
50
+
51
+ return (address as AddressInfo).port
52
+ }
53
+
54
+ /**
55
+ * 表示 WebSocketServer 启动后的结果。
56
+ */
57
+ export interface RunningResult {
58
+ urlList: string[]
59
+ server: Server
60
+ }
61
+
62
+ /**
63
+ * 表示 WebSocketServer 的构造参数。
64
+ */
65
+ export interface WebSocketServerOptions {
66
+ port: number
67
+ }
68
+
69
+ /**
70
+ * 对原生 WebSocketServer 进行包装,并补齐更稳定的运行时语义。
71
+ */
72
+ export class WebSocketServer {
73
+ private port: number
74
+ private server: Server | null
75
+
76
+ constructor(options: WebSocketServerOptions) {
77
+ const { port } = options
78
+
79
+ this.port = port
80
+ this.server = null
81
+ }
82
+
83
+ /**
84
+ * 返回当前运行中的原生 WebSocketServer 实例。
85
+ */
86
+ safeGetServer(): Server {
87
+ if (this.server === null) {
88
+ throw new Error("Server is not running.")
89
+ }
90
+
91
+ if (this.server.address() === null) {
92
+ this.server = null
93
+ throw new Error("Server is not running.")
94
+ }
95
+
96
+ return this.server
97
+ }
98
+
99
+ /**
100
+ * 返回服务当前是否处于可接受连接的运行状态。
101
+ */
102
+ isRunning(): boolean {
103
+ if (this.server === null) {
104
+ return false
105
+ }
106
+
107
+ if (this.server.address() === null) {
108
+ this.server = null
109
+ return false
110
+ }
111
+
112
+ return true
113
+ }
114
+
115
+ /**
116
+ * 启动服务并返回可直接使用的本地地址列表。
117
+ */
118
+ async run(): Promise<RunningResult> {
119
+ if (this.server !== null) {
120
+ throw new Error("Server is already running.")
121
+ }
122
+
123
+ const server = new Server({ port: this.port })
124
+
125
+ const result = await new Promise<RunningResult>((resolve, reject) => {
126
+ const internalHandleError = (error: Error): void => {
127
+ server.off("listening", internalHandleListening)
128
+ this.server = null
129
+ reject(error)
130
+ }
131
+
132
+ const internalHandleListening = (): void => {
133
+ server.off("error", internalHandleError)
134
+ this.server = server
135
+
136
+ server.once("close", () => {
137
+ if (this.server === server) {
138
+ this.server = null
139
+ }
140
+ })
141
+
142
+ const port = internalGetListeningPort(server)
143
+ const urlList = internalBuildWebSocketUrlList(port)
144
+
145
+ resolve({ urlList, server })
146
+ }
147
+
148
+ server.once("error", internalHandleError)
149
+ server.once("listening", internalHandleListening)
150
+ })
151
+
152
+ return result
153
+ }
154
+
155
+ /**
156
+ * 关闭当前服务实例。
157
+ */
158
+ async close(): Promise<void> {
159
+ if (this.isRunning() === false) {
160
+ return
161
+ }
162
+
163
+ const server = this.server!
164
+
165
+ await new Promise<void>((resolve, reject) => {
166
+ server.close((error) => {
167
+ if (error !== undefined) {
168
+ reject(error)
169
+ return
170
+ }
171
+
172
+ resolve()
173
+ })
174
+ })
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 创建一个 WebSocketServer 包装实例。
180
+ */
181
+ export const createWebSocketServer = (options: WebSocketServerOptions): WebSocketServer => {
182
+ return new WebSocketServer(options)
183
+ }
@@ -0,0 +1,449 @@
1
+ import type { IncomingMessage } from "node:http"
2
+ import type { RawData, WebSocket } from "ws"
3
+
4
+ import type { BuildEvents } from "#Source/event/index.ts"
5
+ import { EventManager } from "#Source/event/index.ts"
6
+ import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
7
+ import { Logger } from "#Source/log/index.ts"
8
+ import type {
9
+ SocketUnitBaseSnapshot,
10
+ SocketUnitLifecycleState,
11
+ SocketUnitHeartbeatOptions,
12
+ SocketUnitHeartbeatSnapshot,
13
+ } from "../common/index.ts"
14
+ import {
15
+ SocketUnitHeartbeat,
16
+ } from "../common/index.ts"
17
+
18
+ /**
19
+ * 表示服务端 SocketUnit 的传输状态。
20
+ */
21
+ export type SocketUnitStatus = "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED"
22
+
23
+ /**
24
+ * 表示服务端 SocketUnit 对外派发的事件表。
25
+ */
26
+ export type SocketUnitEvents<Message> = BuildEvents<{
27
+ status: (status: SocketUnitStatus) => void
28
+ message: (message: Message) => void
29
+ }>
30
+
31
+ /**
32
+ * 表示服务端 SocketUnit 的诊断快照。
33
+ */
34
+ export interface SocketUnitSnapshot extends SocketUnitBaseSnapshot<
35
+ string,
36
+ SocketUnitStatus,
37
+ SocketUnitHeartbeatSnapshot
38
+ > {
39
+ adapterState: {
40
+ kind: "server"
41
+ isStarted: boolean
42
+ isClosed: boolean
43
+ }
44
+ }
45
+
46
+ interface Action {
47
+ action: () => void
48
+ }
49
+
50
+ /**
51
+ * 表示服务端初始消息的构造器。
52
+ */
53
+ export type InitialMessageBuilder<Message> = (clientId: string) => Message
54
+
55
+ /**
56
+ * 表示服务端 SocketUnit 向外转发业务消息时的负载。
57
+ */
58
+ export interface SocketUnitMessage<Message> {
59
+ clientId: string
60
+ message: Message
61
+ }
62
+
63
+ /**
64
+ * 表示服务端 SocketUnit 的构造参数。
65
+ */
66
+ export interface SocketUnitOptions<Message> extends LoggerFriendlyOptions {
67
+ clientId: string
68
+ webSocket: WebSocket
69
+ incomingMessage: IncomingMessage
70
+
71
+ /**
72
+ * 是否启用初始消息。
73
+ *
74
+ * 初始消息遵循 ready 语义:
75
+ * 只有在 SocketUnit 已具备稳定发送条件后才会发送。
76
+ *
77
+ * @default false
78
+ */
79
+ enableInitialMessage?: boolean | undefined
80
+ initialMessageBuilder?: InitialMessageBuilder<Message> | undefined
81
+ heartbeat?: SocketUnitHeartbeatOptions<Message> | undefined
82
+
83
+ onMessage: (message: SocketUnitMessage<Message>) => void
84
+ onClose: (clientId: string) => void
85
+ }
86
+
87
+ /**
88
+ * 验证服务端 SocketUnit 配置是否合法。
89
+ */
90
+ export const validateSocketUnitOptions = <Message>(options: SocketUnitOptions<Message>): void => {
91
+ if (
92
+ options.enableInitialMessage === true
93
+ && options.initialMessageBuilder === undefined
94
+ ) {
95
+ throw new Error("Initialize message builder is required when enable initial message.")
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 表示补齐默认值后的服务端 SocketUnit 配置。
101
+ */
102
+ export interface ResolvedSocketUnitOptions<Message> extends LoggerFriendlyOptions {
103
+ clientId: string
104
+ webSocket: WebSocket
105
+ incomingMessage: IncomingMessage
106
+
107
+ enableInitialMessage: boolean
108
+ initialMessageBuilder?: InitialMessageBuilder<Message> | undefined
109
+ heartbeat: SocketUnitHeartbeatOptions<Message>
110
+
111
+ onMessage: (message: SocketUnitMessage<Message>) => void
112
+ onClose: (clientId: string) => void
113
+ }
114
+
115
+ /**
116
+ * 为服务端 SocketUnit 配置补齐默认值。
117
+ */
118
+ export const resolveSocketUnitOptions = <Message>(options: SocketUnitOptions<Message>): ResolvedSocketUnitOptions<Message> => {
119
+ return {
120
+ ...options,
121
+ clientId: options.clientId,
122
+ webSocket: options.webSocket,
123
+ incomingMessage: options.incomingMessage,
124
+ enableInitialMessage: options.enableInitialMessage ?? false,
125
+ initialMessageBuilder: options.initialMessageBuilder,
126
+ heartbeat: options.heartbeat ?? {},
127
+ onMessage: options.onMessage,
128
+ onClose: options.onClose,
129
+ }
130
+ }
131
+
132
+ export class SocketUnit<Message> implements LoggerFriendly {
133
+ protected readonly options: ResolvedSocketUnitOptions<Message>
134
+
135
+ readonly logger: Logger
136
+ protected readonly heartbeat: SocketUnitHeartbeat<Message>
137
+ protected isStarted: boolean
138
+ protected isClosed: boolean
139
+ protected actionQueue: Action[]
140
+
141
+ eventManager: EventManager<SocketUnitEvents<Message>>
142
+
143
+ constructor(options: SocketUnitOptions<Message>) {
144
+ validateSocketUnitOptions(options)
145
+
146
+ this.options = resolveSocketUnitOptions(options)
147
+
148
+ this.logger = Logger.fromOptions(options).setDefaultName("SocketUnit")
149
+ this.heartbeat = new SocketUnitHeartbeat<Message>({
150
+ ...this.options.heartbeat,
151
+ logger: Logger.derive(this.logger).setName("SocketUnitHeartbeat"),
152
+ }, {
153
+ sendMessage: (message) => {
154
+ return this.safeSendMessage(message)
155
+ },
156
+ close: () => {
157
+ this.options.webSocket.close()
158
+ },
159
+ })
160
+ this.isStarted = false
161
+ this.isClosed = false
162
+ this.actionQueue = []
163
+
164
+ this.eventManager = new EventManager<SocketUnitEvents<Message>>()
165
+ }
166
+
167
+ /**
168
+ * 启动当前服务端 SocketUnit 的运行时监听。
169
+ */
170
+ start(): void {
171
+ if (this.isStarted === true) {
172
+ throw new Error("SocketUnit has already started.")
173
+ }
174
+
175
+ this.options.webSocket.on("close", this.handleClose)
176
+ this.options.webSocket.on("error", this.handleError)
177
+ this.options.webSocket.on("message", this.handleMessage)
178
+ this.isStarted = true
179
+ this.eventManager.emit("status", this.getStatus())
180
+
181
+ this.triggerReadyToWork()
182
+ }
183
+
184
+ /**
185
+ * 关闭当前服务端 SocketUnit。
186
+ */
187
+ async close(): Promise<void> {
188
+ if (this.isClosed === true) {
189
+ return
190
+ }
191
+
192
+ const webSocket = this.options.webSocket
193
+ const closedState = webSocket.CLOSED
194
+ const closingState = webSocket.CLOSING
195
+
196
+ if (webSocket.readyState === closedState) {
197
+ this.finalizeClose(undefined, Buffer.alloc(0))
198
+ return
199
+ }
200
+
201
+ await new Promise<void>((resolve) => {
202
+ const handleClose = (): void => {
203
+ webSocket.off("close", handleClose)
204
+ resolve()
205
+ }
206
+
207
+ webSocket.on("close", handleClose)
208
+
209
+ if (webSocket.readyState !== closingState) {
210
+ webSocket.close()
211
+ }
212
+ })
213
+ }
214
+
215
+ /**
216
+ * 返回当前服务端 SocketUnit 的快照。
217
+ */
218
+ getSnapshot(): SocketUnitSnapshot {
219
+ const heartbeatSnapshot: SocketUnitHeartbeatSnapshot = this.heartbeat.getSnapshot()
220
+
221
+ return {
222
+ status: this.getStatus(),
223
+ clientId: this.options.clientId,
224
+ lifecycleState: this.getLifecycleState(),
225
+ isReadyToWork: this.isReadyToWork(),
226
+ hasTransport: true,
227
+ pendingActionCount: this.actionQueue.length,
228
+ transportOwnership: "INJECTED",
229
+ heartbeat: heartbeatSnapshot,
230
+ adapterState: {
231
+ kind: "server",
232
+ isStarted: this.isStarted,
233
+ isClosed: this.isClosed,
234
+ },
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 发送一条业务消息;若尚未 ready,则先进入待发送队列。
240
+ */
241
+ sendMessage(message: Message): void {
242
+ if (this.isClosed === true) {
243
+ this.logger.warn(`Business message send skipped because SocketUnit is closed: ${this.options.clientId}`)
244
+ return
245
+ }
246
+
247
+ this.pushToQueueOrExecute({
248
+ action: (): void => {
249
+ this.safeSendMessage({
250
+ action: "Business message send",
251
+ messageString: JSON.stringify(message)
252
+ })
253
+ },
254
+ })
255
+ }
256
+
257
+ protected getStatus(): SocketUnitStatus {
258
+ const readyStateToStatusMap: Record<number, SocketUnitStatus> = {
259
+ [this.options.webSocket.CONNECTING]: "CONNECTING",
260
+ [this.options.webSocket.OPEN]: "OPEN",
261
+ [this.options.webSocket.CLOSING]: "CLOSING",
262
+ [this.options.webSocket.CLOSED]: "CLOSED",
263
+ }
264
+
265
+ const status = readyStateToStatusMap[this.options.webSocket.readyState]
266
+ if (status === undefined) {
267
+ throw new Error(`Unexpected readyState: ${this.options.webSocket.readyState}`)
268
+ }
269
+
270
+ return status
271
+ }
272
+
273
+ protected getLifecycleState(): SocketUnitLifecycleState {
274
+ if (this.isClosed === true) {
275
+ return "CLOSED"
276
+ }
277
+
278
+ if (this.isStarted === true) {
279
+ return "RUNNING"
280
+ }
281
+
282
+ return "IDLE"
283
+ }
284
+
285
+ protected isReadyToWork(): boolean {
286
+ return this.isStarted === true && this.options.webSocket.readyState === this.options.webSocket.OPEN
287
+ }
288
+
289
+ protected pushToQueueOrExecute(action: Action): void {
290
+ if (this.isReadyToWork() === true) {
291
+ action.action()
292
+ return
293
+ }
294
+
295
+ this.actionQueue.push(action)
296
+ }
297
+
298
+ protected executeActionQueue(): void {
299
+ const pendingActions = [...this.actionQueue]
300
+ this.actionQueue = []
301
+
302
+ pendingActions.forEach((action) => {
303
+ action.action()
304
+ })
305
+ }
306
+
307
+ protected clearPendingActions(): void {
308
+ this.actionQueue = []
309
+ }
310
+
311
+ protected triggerReadyToWork(): void {
312
+ if (this.isReadyToWork() === false) {
313
+ return
314
+ }
315
+
316
+ this.heartbeat.start(this.options.clientId)
317
+ this.sendInitialMessageIfEnabled()
318
+ this.executeActionQueue()
319
+ }
320
+
321
+ protected sendInitialMessageIfEnabled(): void {
322
+ if (this.options.enableInitialMessage === false) {
323
+ return
324
+ }
325
+
326
+ const initialMessage = this.options.initialMessageBuilder!(this.options.clientId)
327
+ const initialMessageString = JSON.stringify(initialMessage)
328
+ const sendResult = this.safeSendMessage({
329
+ action: "Initial message send",
330
+ messageString: initialMessageString
331
+ })
332
+ if (sendResult === true) {
333
+ this.logger.log(`Initial message sent: ${this.options.clientId}, ${initialMessageString}`)
334
+ }
335
+ }
336
+
337
+ protected parseMessage(data: RawData): Message | undefined {
338
+ try {
339
+ const messageString = data.toString()
340
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
341
+ const parsedMessage = JSON.parse(messageString) as Message
342
+ return parsedMessage
343
+ }
344
+ catch (exception) {
345
+ this.logger.error("Error when handling message:", exception)
346
+ return undefined
347
+ }
348
+ }
349
+
350
+ protected handleParsedMessage(parsedMessage: Message): void {
351
+ const heartbeatHandleResult = this.heartbeat.handleMessage(
352
+ this.options.clientId,
353
+ parsedMessage,
354
+ )
355
+ if (heartbeatHandleResult === true) {
356
+ return
357
+ }
358
+
359
+ this.handleBusinessMessage(parsedMessage)
360
+ }
361
+
362
+ protected handleBusinessMessage(message: Message): void {
363
+ this.eventManager.emit("message", message)
364
+ this.options.onMessage({
365
+ clientId: this.options.clientId,
366
+ message,
367
+ })
368
+ }
369
+
370
+ protected readonly handleMessage = (message: RawData): void => {
371
+ const parsedMessage = this.parseMessage(message)
372
+ if (parsedMessage === undefined) {
373
+ return
374
+ }
375
+
376
+ this.handleParsedMessage(parsedMessage)
377
+ }
378
+
379
+ protected readonly handleClose = (code: number, reason: Buffer): void => {
380
+ this.finalizeClose(code, reason)
381
+ }
382
+
383
+ protected readonly handleError = (error: Error): void => {
384
+ this.logger.error(`WebSocket error: ${this.options.clientId}`, error)
385
+
386
+ const webSocket = this.options.webSocket
387
+ if (webSocket.readyState === webSocket.CLOSED) {
388
+ this.finalizeClose(undefined, Buffer.alloc(0))
389
+ return
390
+ }
391
+
392
+ if (webSocket.readyState !== webSocket.CLOSING) {
393
+ webSocket.close()
394
+ }
395
+ }
396
+
397
+ protected finalizeClose(code: number | undefined, reason: Buffer): void {
398
+ if (this.isClosed === true) {
399
+ return
400
+ }
401
+
402
+ this.isClosed = true
403
+ this.logger.log(`WebSocket closed: ${this.options.clientId} ${code} ${reason.toString("utf8")}`)
404
+ this.dispose()
405
+ this.eventManager.emit("status", this.getStatus())
406
+ this.options.onClose(this.options.clientId)
407
+ this.logger.log(`WebSocket closed, related resources collected: ${this.options.clientId}`)
408
+ }
409
+
410
+ protected dispose(): void {
411
+ this.clearPendingActions()
412
+
413
+ if (this.isStarted === false) {
414
+ return
415
+ }
416
+
417
+ this.options.webSocket.off("close", this.handleClose)
418
+ this.options.webSocket.off("error", this.handleError)
419
+ this.options.webSocket.off("message", this.handleMessage)
420
+ this.heartbeat.stop()
421
+ this.isStarted = false
422
+ }
423
+
424
+ protected safeSendMessage(message: {
425
+ action: string,
426
+ messageString: string
427
+ }): boolean {
428
+ if (this.options.webSocket.readyState !== this.options.webSocket.OPEN) {
429
+ this.logger.warn(`${message.action} skipped because WebSocket is not open: ${this.options.clientId}`)
430
+ return false
431
+ }
432
+
433
+ try {
434
+ this.options.webSocket.send(message.messageString, (error) => {
435
+ if (error !== undefined) {
436
+ this.logger.error(`${message.action} failed: ${this.options.clientId}`, error)
437
+ this.options.webSocket.close()
438
+ }
439
+ })
440
+
441
+ return true
442
+ }
443
+ catch (exception) {
444
+ this.logger.error(`${message.action} failed: ${this.options.clientId}`, exception)
445
+ this.options.webSocket.close()
446
+ return false
447
+ }
448
+ }
449
+ }