@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,658 @@
1
+ import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
2
+ import { Logger } from "#Source/log/index.ts"
3
+ import type { BuildEvents } from "#Source/event/index.ts"
4
+ import { EventManager } from "#Source/event/index.ts"
5
+ import type {
6
+ SocketUnitBaseSnapshot,
7
+ SocketUnitLifecycleState,
8
+ SocketUnitHeartbeatOptions,
9
+ SocketUnitHeartbeatSnapshot,
10
+ } from "../common/index.ts"
11
+ import {
12
+ SocketUnitHeartbeat,
13
+ } from "../common/index.ts"
14
+
15
+ /**
16
+ * 表示客户端 SocketUnit 的传输状态。
17
+ */
18
+ export type SocketUnitStatus = "UNINSTANTIATED" | "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED"
19
+
20
+ /**
21
+ * 表示客户端 SocketUnit 对外派发的事件表。
22
+ */
23
+ export type SocketUnitEvents<Message> = BuildEvents<{
24
+ status: (status: SocketUnitStatus) => void
25
+ message: (message: Message) => void
26
+ }>
27
+
28
+ /**
29
+ * 表示客户端 SocketUnit 的诊断快照。
30
+ */
31
+ export interface SocketUnitSnapshot extends SocketUnitBaseSnapshot<
32
+ string | undefined,
33
+ SocketUnitStatus,
34
+ SocketUnitHeartbeatSnapshot
35
+ > {
36
+ adapterState: {
37
+ kind: "client"
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 表示客户端初始消息的构造器。
43
+ */
44
+ export type InitialMessageBuilder<Message> = (clientId: string) => Message
45
+
46
+ /**
47
+ * 表示客户端 SocketUnit 的构造参数。
48
+ */
49
+ export interface SocketUnitOptions<Message> extends LoggerFriendlyOptions {
50
+ url: string
51
+
52
+ /**
53
+ * 是否启用初始消息。
54
+ *
55
+ * 初始消息遵循与普通业务消息一致的 ready 语义:
56
+ * 只有在连接已打开且 clientId 已就绪后才会发送。
57
+ *
58
+ * @default false
59
+ */
60
+ enableInitialMessage?: boolean | undefined
61
+ initialMessageBuilder?: InitialMessageBuilder<Message> | undefined
62
+
63
+ heartbeat?: SocketUnitHeartbeatOptions<Message> | undefined
64
+ }
65
+
66
+ /**
67
+ * 验证客户端 SocketUnit 配置是否合法。
68
+ */
69
+ export const validateSocketUnitOptions = <Message>(options: SocketUnitOptions<Message>): void => {
70
+ if (
71
+ options.enableInitialMessage === true
72
+ && options.initialMessageBuilder === undefined
73
+ ) {
74
+ throw new Error("Initialize message builder is required when enable initial message.")
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 表示补齐默认值后的客户端 SocketUnit 配置。
80
+ */
81
+ export interface ResolvedSocketUnitOptions<Message> extends LoggerFriendlyOptions {
82
+ url: string
83
+ enableInitialMessage: boolean
84
+ initialMessageBuilder?: InitialMessageBuilder<Message> | undefined
85
+ heartbeat: SocketUnitHeartbeatOptions<Message>
86
+ }
87
+
88
+ /**
89
+ * 为客户端 SocketUnit 配置补齐默认值。
90
+ */
91
+ export const resolveSocketUnitOptions = <Message>(options: SocketUnitOptions<Message>): ResolvedSocketUnitOptions<Message> => {
92
+ return {
93
+ ...options,
94
+ url: options.url,
95
+ enableInitialMessage: options.enableInitialMessage ?? false,
96
+ initialMessageBuilder: options.initialMessageBuilder,
97
+ heartbeat: options.heartbeat ?? {},
98
+ }
99
+ }
100
+
101
+ interface Action {
102
+ action: () => void
103
+ }
104
+
105
+ /**
106
+ * 关于心跳:
107
+ * 心跳只会在正常连接时工作,目的是保持连接的活跃状态,避免被服务器断开。
108
+ * 连接断开之后,心跳会停止工作,直到连接重新建立。
109
+ */
110
+ export class SocketUnit<Message> implements LoggerFriendly {
111
+ protected readonly options: ResolvedSocketUnitOptions<Message>
112
+
113
+ readonly logger: Logger
114
+ protected heartbeat: SocketUnitHeartbeat<Message>
115
+ protected clientId: string | undefined
116
+ protected webSocket: WebSocket | null
117
+ protected status: SocketUnitStatus
118
+ protected actionQueue: Action[]
119
+ protected runtimeWebSocketListenerCleanup: (() => void) | undefined
120
+ protected openSettlementListenerCleanup: (() => void) | undefined
121
+
122
+ eventManager: EventManager<SocketUnitEvents<Message>>
123
+
124
+ constructor(options: SocketUnitOptions<Message>) {
125
+ validateSocketUnitOptions(options)
126
+
127
+ this.options = resolveSocketUnitOptions(options)
128
+
129
+ this.logger = Logger.fromOptions(options).setDefaultName("SocketUnit")
130
+ this.heartbeat = new SocketUnitHeartbeat<Message>({
131
+ ...this.options.heartbeat,
132
+ logger: Logger.derive(this.logger).setName("SocketUnitHeartbeat")
133
+ }, {
134
+ sendMessage: (message) => {
135
+ return this.safeSendMessage(message)
136
+ },
137
+ close: () => {
138
+ void this.close()
139
+ },
140
+ })
141
+ this.clientId = undefined
142
+ this.webSocket = null
143
+ this.status = "UNINSTANTIATED"
144
+ this.actionQueue = []
145
+ this.runtimeWebSocketListenerCleanup = undefined
146
+ this.openSettlementListenerCleanup = undefined
147
+
148
+ this.eventManager = new EventManager<SocketUnitEvents<Message>>()
149
+ }
150
+
151
+ /**
152
+ * 返回当前 SocketUnit 的状态。
153
+ */
154
+ getStatus(): SocketUnitStatus {
155
+ return this.status
156
+ }
157
+
158
+ /**
159
+ * 返回当前 SocketUnit 的快照。
160
+ */
161
+ getSnapshot(): SocketUnitSnapshot {
162
+ const heartbeatSnapshot: SocketUnitHeartbeatSnapshot = this.heartbeat.getSnapshot()
163
+
164
+ return {
165
+ status: this.status,
166
+ clientId: this.clientId,
167
+ lifecycleState: this.getLifecycleState(),
168
+ isReadyToWork: this.isReadyToWork(),
169
+ hasTransport: this.webSocket !== null,
170
+ pendingActionCount: this.actionQueue.length,
171
+ transportOwnership: "OWNED",
172
+ heartbeat: heartbeatSnapshot,
173
+ adapterState: {
174
+ kind: "client",
175
+ },
176
+ }
177
+ }
178
+
179
+ protected getLifecycleState(): SocketUnitLifecycleState {
180
+ if (this.status === "UNINSTANTIATED") {
181
+ return "IDLE"
182
+ }
183
+
184
+ if (this.status === "CLOSED") {
185
+ return "CLOSED"
186
+ }
187
+
188
+ return "RUNNING"
189
+ }
190
+
191
+ protected mutateRuntimeState(mutator: () => void): void {
192
+ const previousStatus = this.status
193
+ const wasReadyToWork = this.isReadyToWork()
194
+
195
+ mutator()
196
+
197
+ const nextStatus = this.status
198
+ const isReadyToWork = this.isReadyToWork()
199
+ this.handleRuntimeStateChange(previousStatus, nextStatus, wasReadyToWork, isReadyToWork)
200
+ }
201
+
202
+ protected handleRuntimeStateChange(
203
+ previousStatus: SocketUnitStatus,
204
+ nextStatus: SocketUnitStatus,
205
+ wasReadyToWork: boolean,
206
+ isReadyToWork: boolean,
207
+ ): void {
208
+ if (
209
+ (previousStatus !== nextStatus && nextStatus !== "OPEN")
210
+ || (wasReadyToWork === true && isReadyToWork === false)
211
+ ) {
212
+ this.heartbeat.stop()
213
+ }
214
+
215
+ if (previousStatus !== nextStatus) {
216
+ this.eventManager.emit("status", nextStatus)
217
+ }
218
+
219
+ if (wasReadyToWork === false && isReadyToWork === true) {
220
+ this.triggerReadyToWork()
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 从 WebSocket 实例计算状态。
226
+ */
227
+ protected calculateStatus(): SocketUnitStatus {
228
+ if (this.webSocket === null) {
229
+ return "UNINSTANTIATED"
230
+ }
231
+
232
+ const readyStateToStatusMap: Record<number, SocketUnitStatus> = {
233
+ [WebSocket.CONNECTING]: "CONNECTING",
234
+ [WebSocket.OPEN]: "OPEN",
235
+ [WebSocket.CLOSING]: "CLOSING",
236
+ [WebSocket.CLOSED]: "CLOSED",
237
+ }
238
+
239
+ const status = readyStateToStatusMap[this.webSocket.readyState]
240
+ if (status === undefined) {
241
+ throw new Error(`Unexpected readyState: ${this.webSocket.readyState}`)
242
+ }
243
+
244
+ return status
245
+ }
246
+
247
+ /**
248
+ * [sugar method] 自动执行 {@link calculateStatus} 并使用计算结果设置状态。
249
+ */
250
+ protected updateStatus(): SocketUnitStatus {
251
+ const status = this.calculateStatus()
252
+ this.mutateRuntimeState(() => {
253
+ this.status = status
254
+ })
255
+ return status
256
+ }
257
+
258
+ /**
259
+ * 设置当前 SocketUnit 绑定的 clientId。
260
+ */
261
+ setClientId(clientId: string): void {
262
+ this.mutateRuntimeState(() => {
263
+ this.clientId = clientId
264
+ })
265
+ }
266
+
267
+ /**
268
+ * [sugar method] 设置 WebSocket 实例并调用 {@link updateStatus} 更新状态。
269
+ */
270
+ protected setWebSocket(ws: WebSocket | null): void {
271
+ this.mutateRuntimeState(() => {
272
+ this.webSocket = ws
273
+ this.status = this.calculateStatus()
274
+ })
275
+ }
276
+
277
+ protected replaceRuntimeWebSocketListenerCleanup(cleanup: () => void): void {
278
+ this.removeRuntimeWebSocketListeners()
279
+ this.runtimeWebSocketListenerCleanup = cleanup
280
+ }
281
+
282
+ protected removeRuntimeWebSocketListeners(): void {
283
+ const cleanup = this.runtimeWebSocketListenerCleanup
284
+ this.runtimeWebSocketListenerCleanup = undefined
285
+ cleanup?.()
286
+ }
287
+
288
+ protected replaceOpenSettlementListenerCleanup(cleanup: () => void): void {
289
+ this.removeOpenSettlementListeners()
290
+ this.openSettlementListenerCleanup = cleanup
291
+ }
292
+
293
+ protected removeOpenSettlementListeners(): void {
294
+ const cleanup = this.openSettlementListenerCleanup
295
+ this.openSettlementListenerCleanup = undefined
296
+ cleanup?.()
297
+ }
298
+
299
+ protected removeWebSocketListeners(): void {
300
+ this.removeOpenSettlementListeners()
301
+ this.removeRuntimeWebSocketListeners()
302
+ }
303
+
304
+ protected clearPendingActions(): void {
305
+ this.actionQueue = []
306
+ }
307
+
308
+ protected pushToQueueOrExecute(action: Action): void {
309
+ if (this.isReadyToWork() === true) {
310
+ action.action()
311
+ }
312
+ else {
313
+ this.actionQueue.push(action)
314
+ }
315
+ }
316
+
317
+ protected executeActionQueue(): void {
318
+ [...this.actionQueue].forEach((action) => {
319
+ action.action()
320
+ })
321
+ this.actionQueue = []
322
+ }
323
+
324
+ /**
325
+ * NOTE: Ready 之后才可以发消息。
326
+ */
327
+ protected isReadyToWork(): boolean {
328
+ return this.status === "OPEN" && this.clientId !== undefined
329
+ }
330
+
331
+ protected triggerReadyToWork(): void {
332
+ if (this.isReadyToWork() === true) {
333
+ this.heartbeat.start(this.clientId!)
334
+ this.executeActionQueue()
335
+ }
336
+ }
337
+
338
+ protected cleanupConnectionResources(): void {
339
+ this.heartbeat.stop()
340
+ this.removeWebSocketListeners()
341
+
342
+ if (this.webSocket !== null) {
343
+ this.setWebSocket(null)
344
+ }
345
+ }
346
+
347
+ protected resetRuntimeState(): void {
348
+ this.cleanupConnectionResources()
349
+ this.clearPendingActions()
350
+
351
+ this.mutateRuntimeState(() => {
352
+ this.clientId = undefined
353
+ this.status = "UNINSTANTIATED"
354
+ })
355
+ }
356
+
357
+ /**
358
+ * 发送一条业务消息;若尚未 ready,则先进入待发送队列。
359
+ */
360
+ sendMessage(message: Message): void {
361
+ this.pushToQueueOrExecute({
362
+ action: (): void => {
363
+ this.safeSendMessage({
364
+ action: "Business message send",
365
+ messageString: JSON.stringify(message)
366
+ })
367
+ },
368
+ })
369
+ }
370
+
371
+ protected enqueueInitialMessageIfEnabled(): void {
372
+ if (this.options.enableInitialMessage === true) {
373
+ this.pushToQueueOrExecute({
374
+ action: () => {
375
+ const clientId = this.clientId!
376
+ const initialMessage = this.options.initialMessageBuilder!(clientId)
377
+ const initialMessageString = JSON.stringify(initialMessage)
378
+ const sendResult = this.safeSendMessage({
379
+ action: "Initial message send",
380
+ messageString: initialMessageString
381
+ })
382
+ if (sendResult === true) {
383
+ this.logger.log(`Initial message sent: ${clientId}, ${initialMessageString}`)
384
+ }
385
+ },
386
+ })
387
+ }
388
+ }
389
+
390
+ protected safeSendMessage(message: {
391
+ action: string,
392
+ messageString: string
393
+ }): boolean {
394
+ const webSocket = this.webSocket
395
+ if (webSocket === null) {
396
+ this.logger.warn(`${message.action} skipped because WebSocket is not initialized.`)
397
+ return false
398
+ }
399
+
400
+ if (webSocket.readyState !== WebSocket.OPEN) {
401
+ this.logger.warn(`${message.action} skipped because WebSocket is not open.`)
402
+ return false
403
+ }
404
+
405
+ try {
406
+ webSocket.send(message.messageString)
407
+ return true
408
+ }
409
+ catch (exception) {
410
+ this.logger.error(`${message.action} failed.`, exception)
411
+ void this.close()
412
+ return false
413
+ }
414
+ }
415
+
416
+ protected parseMessage(event: MessageEvent): Message | undefined {
417
+ try {
418
+ const messageString = event.data as string
419
+ const parsedMessage = JSON.parse(messageString) as Message
420
+ return parsedMessage
421
+ }
422
+ catch {
423
+ this.logger.error("Failed to parse message:", event.data)
424
+ return undefined
425
+ }
426
+ }
427
+
428
+ protected handleParsedMessage(parsedMessage: Message): void {
429
+ const clientId = this.clientId
430
+ if (clientId === undefined) {
431
+ this.logger.warn("Message skipped because clientId is not ready.")
432
+ return
433
+ }
434
+
435
+ const heartbeatHandleResult = this.heartbeat.handleMessage(
436
+ clientId, parsedMessage
437
+ )
438
+ if (heartbeatHandleResult === true) {
439
+ return
440
+ }
441
+
442
+ this.handleBusinessMessage(parsedMessage)
443
+ }
444
+
445
+ protected handleBusinessMessage(message: Message): void {
446
+ this.eventManager.emit("message", message)
447
+ }
448
+
449
+ protected handleMessageEvent(event: MessageEvent): void {
450
+ const parsedMessage = this.parseMessage(event)
451
+ if (parsedMessage === undefined) {
452
+ return
453
+ }
454
+
455
+ this.handleParsedMessage(parsedMessage)
456
+ }
457
+
458
+ protected readonly handleRuntimeOpen = (event: Event): void => {
459
+ this.logger.log("WebSocket open", event)
460
+ this.updateStatus()
461
+ }
462
+
463
+ protected readonly handleRuntimeMessage = (event: MessageEvent): void => {
464
+ this.updateStatus()
465
+ this.handleMessageEvent(event)
466
+ }
467
+
468
+ protected readonly handleRuntimeClose = (event: CloseEvent): void => {
469
+ this.logger.log("WebSocket close", event)
470
+ this.updateStatus()
471
+ }
472
+
473
+ protected readonly handleRuntimeError = (event: Event): void => {
474
+ this.logger.error("WebSocket error", event)
475
+ this.updateStatus()
476
+ }
477
+
478
+ protected attachRuntimeWebSocketListeners(webSocket: WebSocket): void {
479
+ webSocket.addEventListener("open", this.handleRuntimeOpen)
480
+ webSocket.addEventListener("message", this.handleRuntimeMessage)
481
+ webSocket.addEventListener("close", this.handleRuntimeClose)
482
+ webSocket.addEventListener("error", this.handleRuntimeError)
483
+
484
+ this.replaceRuntimeWebSocketListenerCleanup((): void => {
485
+ webSocket.removeEventListener("open", this.handleRuntimeOpen)
486
+ webSocket.removeEventListener("message", this.handleRuntimeMessage)
487
+ webSocket.removeEventListener("close", this.handleRuntimeClose)
488
+ webSocket.removeEventListener("error", this.handleRuntimeError)
489
+ })
490
+ }
491
+
492
+ protected attachOpenSettlementListeners(
493
+ webSocket: WebSocket,
494
+ resolve: () => void,
495
+ reject: (reason?: unknown) => void,
496
+ ): void {
497
+ let isOpenSettled = false
498
+
499
+ const settleOpen = (callback: () => void): void => {
500
+ if (isOpenSettled === true) {
501
+ return
502
+ }
503
+
504
+ isOpenSettled = true
505
+ this.removeOpenSettlementListeners()
506
+ callback()
507
+ }
508
+
509
+ const openListener = (): void => {
510
+ this.updateStatus()
511
+ settleOpen(resolve)
512
+ }
513
+
514
+ const errorListener = (): void => {
515
+ this.updateStatus()
516
+
517
+ if (webSocket.readyState !== WebSocket.CLOSED) {
518
+ return
519
+ }
520
+
521
+ settleOpen(() => {
522
+ this.cleanupConnectionResources()
523
+ reject(new Error(`Failed to open WebSocket connection: ${this.options.url}`))
524
+ })
525
+ }
526
+
527
+ const closeListener = (event: CloseEvent): void => {
528
+ this.updateStatus()
529
+
530
+ const closeReason = event.reason.length !== 0
531
+ ? ` (${event.code}: ${event.reason})`
532
+ : ` (${event.code})`
533
+
534
+ settleOpen(() => {
535
+ this.cleanupConnectionResources()
536
+ reject(new Error(`WebSocket closed before opening${closeReason}.`))
537
+ })
538
+ }
539
+
540
+ webSocket.addEventListener("open", openListener)
541
+ webSocket.addEventListener("error", errorListener)
542
+ webSocket.addEventListener("close", closeListener)
543
+
544
+ this.replaceOpenSettlementListenerCleanup((): void => {
545
+ webSocket.removeEventListener("open", openListener)
546
+ webSocket.removeEventListener("error", errorListener)
547
+ webSocket.removeEventListener("close", closeListener)
548
+ })
549
+ }
550
+
551
+ /**
552
+ * 如果没有创建 WebSocket 实例,则创建一个 WebSocket 实例,并注册事件监听器。
553
+ * 如果已经创建了 WebSocket 实例,则什么都不做。
554
+ * 如果你希望在已经创建了 WebSocket 实例的情况下报错,可以使用 {@link safeOpen} 方法。
555
+ */
556
+ async open(): Promise<void> {
557
+ const status = this.updateStatus()
558
+ if (status === "UNINSTANTIATED") {
559
+ return await new Promise((resolve, reject) => {
560
+ const webSocket = new WebSocket(this.options.url)
561
+ this.setWebSocket(webSocket)
562
+ this.attachRuntimeWebSocketListeners(webSocket)
563
+
564
+ this.enqueueInitialMessageIfEnabled()
565
+ this.attachOpenSettlementListeners(webSocket, resolve, reject)
566
+ })
567
+ }
568
+ }
569
+
570
+ /**
571
+ * {@link open}
572
+ */
573
+ async safeOpen(): Promise<void> {
574
+ const status = this.updateStatus()
575
+ if (status !== "UNINSTANTIATED") {
576
+ throw new Error("Socket is already open")
577
+ }
578
+ else {
579
+ await this.open()
580
+ }
581
+ }
582
+
583
+ /**
584
+ * 只负责关闭当前连接与释放连接级资源,保留 clientId 与待发送动作。
585
+ */
586
+ async close(): Promise<void> {
587
+ const closingWebSocket = new Promise<void>((resolve, _reject) => {
588
+ const status = this.calculateStatus()
589
+ switch (status) {
590
+ case "UNINSTANTIATED": {
591
+ resolve()
592
+ break
593
+ }
594
+ case "CONNECTING": {
595
+ // 如果 WebSocket 处于连接中,等待连接建立后再关闭,否则会有如下报错:
596
+ // WebSocket connection to 'ws://localhost:3001/' failed: WebSocket is closed before the connection is established.
597
+ const webSocket = this.webSocket!
598
+ const closeListener = (): void => {
599
+ resolve()
600
+ webSocket.removeEventListener("open", openListener)
601
+ webSocket.removeEventListener("close", closeListener)
602
+ }
603
+ webSocket.addEventListener("close", closeListener)
604
+ const openListener = (): void => {
605
+ webSocket.close()
606
+ }
607
+ webSocket.addEventListener("open", openListener)
608
+ break
609
+ }
610
+ case "OPEN": {
611
+ const webSocket = this.webSocket!
612
+ const closeListener = (): void => {
613
+ resolve()
614
+ webSocket.removeEventListener("close", closeListener)
615
+ }
616
+ webSocket.addEventListener("close", closeListener)
617
+ webSocket.close()
618
+ break
619
+ }
620
+ case "CLOSING": {
621
+ const webSocket = this.webSocket!
622
+ const closeListener = (): void => {
623
+ resolve()
624
+ webSocket.removeEventListener("close", closeListener)
625
+ }
626
+ webSocket.addEventListener("close", closeListener)
627
+ break
628
+ }
629
+ case "CLOSED": {
630
+ resolve()
631
+ break
632
+ }
633
+ default: {
634
+ throw new Error("Unexpected status")
635
+ }
636
+ }
637
+ })
638
+ await closingWebSocket
639
+ this.cleanupConnectionResources()
640
+ }
641
+
642
+ /**
643
+ * 关闭当前连接后重建连接,保留 clientId 与待发送动作。
644
+ */
645
+ async reopen(): Promise<void> {
646
+ await this.close()
647
+ await this.open()
648
+ }
649
+
650
+ /**
651
+ * 清空连接级资源与运行态,将实例恢复到接近构造后的状态。
652
+ */
653
+ async reset(onReset?: () => void): Promise<void> {
654
+ await this.close()
655
+ this.resetRuntimeState()
656
+ onReset?.()
657
+ }
658
+ }