@planet-matrix/mobius-model 0.6.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 (233) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/index.js +706 -36
  3. package/dist/index.js.map +855 -59
  4. package/package.json +28 -16
  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/schedule.ts +111 -0
  29. package/src/basic/stream.ts +135 -25
  30. package/src/credential/README.md +107 -0
  31. package/src/credential/api-key.ts +158 -0
  32. package/src/credential/bearer.ts +73 -0
  33. package/src/credential/index.ts +4 -0
  34. package/src/credential/json-web-token.ts +96 -0
  35. package/src/credential/password.ts +170 -0
  36. package/src/cron/README.md +86 -0
  37. package/src/cron/cron.ts +87 -0
  38. package/src/cron/index.ts +1 -0
  39. package/src/drizzle/README.md +1 -0
  40. package/src/drizzle/drizzle.ts +1 -0
  41. package/src/drizzle/helper.ts +47 -0
  42. package/src/drizzle/index.ts +5 -0
  43. package/src/drizzle/infer.ts +52 -0
  44. package/src/drizzle/kysely.ts +8 -0
  45. package/src/drizzle/pagination.ts +200 -0
  46. package/src/email/README.md +1 -0
  47. package/src/email/index.ts +1 -0
  48. package/src/email/resend.ts +25 -0
  49. package/src/event/class-event-proxy.ts +6 -5
  50. package/src/event/common.ts +13 -3
  51. package/src/event/event-manager.ts +3 -3
  52. package/src/event/instance-event-proxy.ts +6 -5
  53. package/src/event/internal.ts +4 -4
  54. package/src/form/README.md +25 -0
  55. package/src/form/index.ts +1 -0
  56. package/src/form/inputor-controller/base.ts +874 -0
  57. package/src/form/inputor-controller/boolean.ts +39 -0
  58. package/src/form/inputor-controller/file.ts +39 -0
  59. package/src/form/inputor-controller/form.ts +181 -0
  60. package/src/form/inputor-controller/helper.ts +117 -0
  61. package/src/form/inputor-controller/index.ts +17 -0
  62. package/src/form/inputor-controller/multi-select.ts +99 -0
  63. package/src/form/inputor-controller/number.ts +116 -0
  64. package/src/form/inputor-controller/select.ts +109 -0
  65. package/src/form/inputor-controller/text.ts +82 -0
  66. package/src/http/READMD.md +1 -0
  67. package/src/http/api/api-core.ts +84 -0
  68. package/src/http/api/api-handler.ts +79 -0
  69. package/src/http/api/api-host.ts +47 -0
  70. package/src/http/api/api-result.ts +56 -0
  71. package/src/http/api/api-schema.ts +154 -0
  72. package/src/http/api/api-server.ts +130 -0
  73. package/src/http/api/api-test.ts +142 -0
  74. package/src/http/api/api-type.ts +37 -0
  75. package/src/http/api/api.ts +81 -0
  76. package/src/http/api/index.ts +11 -0
  77. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  78. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  79. package/src/http/api-adapter/api-result-arktype.ts +297 -0
  80. package/src/http/api-adapter/api-result-zod.ts +286 -0
  81. package/src/http/api-adapter/index.ts +5 -0
  82. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  83. package/src/http/bin/gen-api-list/index.ts +1 -0
  84. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  85. package/src/http/bin/gen-api-test/index.ts +1 -0
  86. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  87. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  88. package/src/http/bin/gen-api-type/index.ts +2 -0
  89. package/src/http/bin/index.ts +2 -0
  90. package/src/http/index.ts +3 -0
  91. package/src/huawei/README.md +1 -0
  92. package/src/huawei/index.ts +2 -0
  93. package/src/huawei/moderation/index.ts +1 -0
  94. package/src/huawei/moderation/moderation.ts +355 -0
  95. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  96. package/src/huawei/obs/index.ts +1 -0
  97. package/src/huawei/obs/obs.ts +42 -0
  98. package/src/index.ts +19 -2
  99. package/src/json/README.md +92 -0
  100. package/src/json/index.ts +1 -0
  101. package/src/json/repair.ts +18 -0
  102. package/src/log/logger.ts +15 -4
  103. package/src/openai/README.md +1 -0
  104. package/src/openai/index.ts +1 -0
  105. package/src/openai/openai.ts +510 -0
  106. package/src/orchestration/README.md +9 -7
  107. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  108. package/src/orchestration/dispatching/index.ts +2 -0
  109. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  110. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  111. package/src/orchestration/dispatching/selector/index.ts +2 -0
  112. package/src/orchestration/index.ts +2 -0
  113. package/src/orchestration/scheduling/index.ts +2 -0
  114. package/src/orchestration/scheduling/scheduler.ts +103 -0
  115. package/src/orchestration/scheduling/task.ts +32 -0
  116. package/src/random/README.md +8 -7
  117. package/src/random/base.ts +66 -0
  118. package/src/random/index.ts +5 -1
  119. package/src/random/random-boolean.ts +40 -0
  120. package/src/random/random-integer.ts +60 -0
  121. package/src/random/random-number.ts +72 -0
  122. package/src/random/random-string.ts +66 -0
  123. package/src/request/README.md +108 -0
  124. package/src/request/fetch/base.ts +108 -0
  125. package/src/request/fetch/browser.ts +285 -0
  126. package/src/request/fetch/general.ts +20 -0
  127. package/src/request/fetch/index.ts +4 -0
  128. package/src/request/fetch/nodejs.ts +285 -0
  129. package/src/request/index.ts +2 -0
  130. package/src/request/request/base.ts +250 -0
  131. package/src/request/request/general.ts +64 -0
  132. package/src/request/request/index.ts +3 -0
  133. package/src/request/request/resource.ts +68 -0
  134. package/src/result/README.md +4 -0
  135. package/src/result/controller.ts +54 -0
  136. package/src/result/either.ts +193 -0
  137. package/src/result/index.ts +2 -0
  138. package/src/route/README.md +105 -0
  139. package/src/route/adapter/browser.ts +122 -0
  140. package/src/route/adapter/driver.ts +56 -0
  141. package/src/route/adapter/index.ts +2 -0
  142. package/src/route/index.ts +3 -0
  143. package/src/route/router/index.ts +2 -0
  144. package/src/route/router/route.ts +630 -0
  145. package/src/route/router/router.ts +1642 -0
  146. package/src/route/uri/hash.ts +308 -0
  147. package/src/route/uri/index.ts +7 -0
  148. package/src/route/uri/pathname.ts +376 -0
  149. package/src/route/uri/search.ts +413 -0
  150. package/src/socket/README.md +105 -0
  151. package/src/socket/client/index.ts +2 -0
  152. package/src/socket/client/socket-unit.ts +660 -0
  153. package/src/socket/client/socket.ts +203 -0
  154. package/src/socket/common/index.ts +2 -0
  155. package/src/socket/common/socket-unit-common.ts +23 -0
  156. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  157. package/src/socket/index.ts +3 -0
  158. package/src/socket/server/index.ts +3 -0
  159. package/src/socket/server/server.ts +183 -0
  160. package/src/socket/server/socket-unit.ts +449 -0
  161. package/src/socket/server/socket.ts +264 -0
  162. package/src/storage/table.ts +3 -3
  163. package/src/timer/expiration/expiration-manager.ts +3 -3
  164. package/src/timer/expiration/remaining-manager.ts +3 -3
  165. package/src/tube/README.md +99 -0
  166. package/src/tube/helper.ts +138 -0
  167. package/src/tube/index.ts +2 -0
  168. package/src/tube/tube.ts +880 -0
  169. package/src/weixin/README.md +1 -0
  170. package/src/weixin/index.ts +2 -0
  171. package/src/weixin/official-account/authorization.ts +159 -0
  172. package/src/weixin/official-account/index.ts +2 -0
  173. package/src/weixin/official-account/js-api.ts +134 -0
  174. package/src/weixin/open/index.ts +1 -0
  175. package/src/weixin/open/oauth2.ts +133 -0
  176. package/tests/unit/ai/ai.spec.ts +85 -0
  177. package/tests/unit/aio/content.spec.ts +105 -0
  178. package/tests/unit/aio/json.spec.ts +147 -0
  179. package/tests/unit/aio/prompt.spec.ts +111 -0
  180. package/tests/unit/basic/error.spec.ts +16 -4
  181. package/tests/unit/basic/schedule.spec.ts +74 -0
  182. package/tests/unit/basic/stream.spec.ts +90 -37
  183. package/tests/unit/credential/api-key.spec.ts +37 -0
  184. package/tests/unit/credential/bearer.spec.ts +23 -0
  185. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  186. package/tests/unit/credential/password.spec.ts +41 -0
  187. package/tests/unit/cron/cron.spec.ts +84 -0
  188. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  189. package/tests/unit/event/event-manager.spec.ts +3 -3
  190. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  191. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  192. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  193. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  194. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  195. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  196. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  197. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  198. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  199. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  200. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  201. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  202. package/tests/unit/http/api/api-server.spec.ts +363 -0
  203. package/tests/unit/http/api/api-test.spec.ts +117 -0
  204. package/tests/unit/http/api/api.spec.ts +121 -0
  205. package/tests/unit/http/api-adapter/node-http.spec.ts +191 -0
  206. package/tests/unit/json/repair.spec.ts +11 -0
  207. package/tests/unit/log/logger.spec.ts +19 -4
  208. package/tests/unit/openai/openai.spec.ts +64 -0
  209. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  210. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  211. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  212. package/tests/unit/random/base.spec.ts +58 -0
  213. package/tests/unit/random/random-boolean.spec.ts +25 -0
  214. package/tests/unit/random/random-integer.spec.ts +32 -0
  215. package/tests/unit/random/random-number.spec.ts +33 -0
  216. package/tests/unit/random/random-string.spec.ts +22 -0
  217. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  218. package/tests/unit/request/fetch/general.spec.ts +43 -0
  219. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  220. package/tests/unit/request/request/base.spec.ts +385 -0
  221. package/tests/unit/request/request/general.spec.ts +161 -0
  222. package/tests/unit/route/router/route.spec.ts +431 -0
  223. package/tests/unit/route/router/router.spec.ts +407 -0
  224. package/tests/unit/route/uri/hash.spec.ts +72 -0
  225. package/tests/unit/route/uri/pathname.spec.ts +147 -0
  226. package/tests/unit/route/uri/search.spec.ts +107 -0
  227. package/tests/unit/socket/client.spec.ts +208 -0
  228. package/tests/unit/socket/server.spec.ts +135 -0
  229. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  230. package/tests/unit/tube/helper.spec.ts +139 -0
  231. package/tests/unit/tube/tube.spec.ts +501 -0
  232. package/src/random/string.ts +0 -35
  233. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,203 @@
1
+ import type { LoggerFriendly } from "#Project/src/log/logger.ts"
2
+ import { Logger } from "#Project/src/log/logger.ts"
3
+ import type { BuildEvents, SubscriberEntry } from "#Source/event/index.ts"
4
+ import { EventManager } from "#Source/event/index.ts"
5
+
6
+ import type { SocketUnitEvents, SocketUnitOptions, SocketUnitSnapshot, SocketUnitStatus } from "./socket-unit.ts"
7
+ import { SocketUnit } from "./socket-unit.ts"
8
+
9
+ /**
10
+ * 表示客户端 Socket 进入 ready 态时的事件负载。
11
+ */
12
+ export interface SocketConnectedEvent {
13
+ clientId: string | undefined
14
+ }
15
+
16
+ /**
17
+ * 表示客户端 Socket 离开 ready 态时的事件负载。
18
+ */
19
+ export interface SocketClosedEvent {
20
+ clientId: string | undefined
21
+ }
22
+
23
+ /**
24
+ * 表示客户端 Socket 派发业务消息时的事件负载。
25
+ */
26
+ export interface SocketMessageEvent<Message> {
27
+ clientId: string
28
+ message: Message
29
+ }
30
+
31
+ /**
32
+ * 表示客户端 Socket 对外派发的事件表。
33
+ */
34
+ export type SocketEvents<Message> = BuildEvents<{
35
+ connect: (payload: SocketConnectedEvent) => void
36
+ close: (payload: SocketClosedEvent) => void
37
+ message: (payload: SocketMessageEvent<Message>) => void
38
+ }>
39
+
40
+ /**
41
+ * 表示客户端 Socket 的公开状态类型。
42
+ */
43
+ export type SocketStatus = SocketUnitStatus
44
+
45
+ /**
46
+ * 表示客户端 Socket 的构造参数。
47
+ */
48
+ export interface SocketOptions<Message> extends SocketUnitOptions<Message> {
49
+ }
50
+ /**
51
+ * 此 Socket 类并不直接创建和管理 WebSocket 实例,而是管理很多 SocketUnit,
52
+ * SocketUnit 是 WebSocket 实例的实际管理者。这样可以做到当 WebSocket 实例
53
+ * 出现连接不稳定或断线重连等情况时,Socket 的功能对使用者来说始终是稳定的。
54
+ */
55
+ export class Socket<Message> implements LoggerFriendly {
56
+ protected readonly options: SocketUnitOptions<Message>
57
+
58
+ readonly logger: Logger
59
+
60
+ protected clientId: string | undefined
61
+ protected legacyUnitSet: Set<SocketUnit<Message>>
62
+ protected unit: SocketUnit<Message>
63
+
64
+ eventManager: EventManager<SocketEvents<Message>>
65
+ protected messageSubscriberEntry!: SubscriberEntry<SocketUnitEvents<Message>, 'message'>
66
+ protected statusSubscriberEntry!: SubscriberEntry<SocketUnitEvents<Message>, 'status'>
67
+ protected lastUnitReadyToWork: boolean
68
+
69
+ constructor(options: SocketUnitOptions<Message>) {
70
+ this.options = options
71
+
72
+ this.logger = Logger.fromOptions(options).setDefaultName("Socket")
73
+ this.clientId = undefined
74
+ this.legacyUnitSet = new Set()
75
+ this.unit = this.createUnit()
76
+ this.lastUnitReadyToWork = this.unit.getSnapshot().isReadyToWork
77
+
78
+ this.eventManager = new EventManager<SocketEvents<Message>>()
79
+ this.prepareEventManager()
80
+ }
81
+
82
+ protected createUnit(): SocketUnit<Message> {
83
+ return new SocketUnit<Message>({
84
+ ...this.options,
85
+ logger: Logger.derive(this.logger).setName("SocketUnit")
86
+ })
87
+ }
88
+
89
+ protected replaceCurrentUnit(unit: SocketUnit<Message>): void {
90
+ this.unit = unit
91
+ this.lastUnitReadyToWork = unit.getSnapshot().isReadyToWork
92
+ this.prepareEventManager()
93
+
94
+ if (this.clientId !== undefined) {
95
+ this.unit.setClientId(this.clientId)
96
+ }
97
+ }
98
+
99
+ protected prepareEventManager(): void {
100
+ if (this.messageSubscriberEntry !== undefined) {
101
+ this.messageSubscriberEntry.unsubscribe()
102
+ }
103
+ if (this.statusSubscriberEntry !== undefined) {
104
+ this.statusSubscriberEntry.unsubscribe()
105
+ }
106
+
107
+ this.messageSubscriberEntry = this.unit.eventManager.subscribe('message', (message) => {
108
+ const clientId = this.unit.getSnapshot().clientId
109
+ if (clientId === undefined) {
110
+ this.logger.warn('Message event skipped because clientId is not ready.')
111
+ return
112
+ }
113
+
114
+ this.eventManager.emit('message', { clientId, message })
115
+ })
116
+
117
+ this.statusSubscriberEntry = this.unit.eventManager.subscribe('status', (status) => {
118
+ this.handleUnitStatusChange(status)
119
+ })
120
+ }
121
+
122
+ protected handleUnitStatusChange(_status: SocketUnitStatus): void {
123
+ const previousReadyToWork = this.lastUnitReadyToWork
124
+ const snapshot = this.unit.getSnapshot()
125
+ const nextReadyToWork = snapshot.isReadyToWork
126
+
127
+ this.lastUnitReadyToWork = nextReadyToWork
128
+
129
+ if (previousReadyToWork === false && nextReadyToWork === true) {
130
+ this.eventManager.emit('connect', { clientId: snapshot.clientId })
131
+ }
132
+
133
+ if (
134
+ previousReadyToWork === true
135
+ && nextReadyToWork === false
136
+ ) {
137
+ this.eventManager.emit('close', { clientId: snapshot.clientId })
138
+ }
139
+ }
140
+
141
+ /**
142
+ * 设置当前 Socket 对外暴露的 clientId。
143
+ */
144
+ setClientId(clientId: string): void {
145
+ this.clientId = clientId
146
+ this.unit.setClientId(clientId)
147
+ this.handleUnitStatusChange(this.unit.getStatus())
148
+ }
149
+
150
+ /**
151
+ * 返回当前活跃 SocketUnit 的状态。
152
+ */
153
+ getStatus(): SocketStatus {
154
+ return this.unit.getStatus()
155
+ }
156
+
157
+ /**
158
+ * 返回当前活跃 SocketUnit 的快照。
159
+ */
160
+ getSnapshot(): SocketUnitSnapshot {
161
+ return this.unit.getSnapshot()
162
+ }
163
+
164
+ /**
165
+ * 打开当前活跃的 SocketUnit。
166
+ */
167
+ async open(): Promise<void> {
168
+ await this.unit.open()
169
+ }
170
+
171
+ /**
172
+ * 重建当前活跃的 SocketUnit 连接。
173
+ */
174
+ async reopen(): Promise<void> {
175
+ await this.unit.reopen()
176
+ }
177
+
178
+ /**
179
+ * 关闭当前活跃的 SocketUnit 连接。
180
+ */
181
+ async close(): Promise<void> {
182
+ await this.unit.close()
183
+ }
184
+
185
+ /**
186
+ * 异步回收当前 SocketUnit,并切换到新的运行单元。
187
+ */
188
+ reset(): void {
189
+ const legacyUnit = this.unit
190
+ this.legacyUnitSet.add(legacyUnit)
191
+ void legacyUnit.reset(() => {
192
+ this.legacyUnitSet.delete(legacyUnit)
193
+ })
194
+ this.replaceCurrentUnit(this.createUnit())
195
+ }
196
+
197
+ /**
198
+ * 向当前活跃的 SocketUnit 发送业务消息。
199
+ */
200
+ sendMessage(message: Message): void {
201
+ this.unit.sendMessage(message)
202
+ }
203
+ }
@@ -0,0 +1,2 @@
1
+ export type * from "./socket-unit-common.ts"
2
+ export * from "./socket-unit-heartbeat.ts"
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 表示 SocketUnit 当前所处的生命周期阶段。
3
+ */
4
+ export type SocketUnitLifecycleState = "IDLE" | "RUNNING" | "CLOSED"
5
+
6
+ /**
7
+ * 表示当前传输对象由谁负责创建和销毁。
8
+ */
9
+ export type SocketUnitTransportOwnership = "OWNED" | "INJECTED"
10
+
11
+ /**
12
+ * 表示客户端与服务端 SocketUnit 共享的基础快照结构。
13
+ */
14
+ export interface SocketUnitBaseSnapshot<ClientId, Status, HeartbeatSnapshot> {
15
+ status: Status
16
+ clientId: ClientId
17
+ lifecycleState: SocketUnitLifecycleState
18
+ isReadyToWork: boolean
19
+ hasTransport: boolean
20
+ pendingActionCount: number
21
+ transportOwnership: SocketUnitTransportOwnership
22
+ heartbeat: HeartbeatSnapshot
23
+ }
@@ -0,0 +1,427 @@
1
+ import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
2
+ import { Logger } from "#Source/log/index.ts"
3
+
4
+ /**
5
+ * 表示主动心跳轮次使用的消息处理器。
6
+ */
7
+ export interface ActiveHeartbeatMessageHandler<Message> {
8
+ buildPingMessage: (clientId: string) => Message
9
+ verifyPongMessage: (clientId: string, message: Message) => boolean
10
+ }
11
+
12
+ /**
13
+ * 表示被动响应心跳时使用的消息处理器。
14
+ */
15
+ export interface PassiveHeartbeatMessageHandler<Message> {
16
+ verifyPingMessage: (clientId: string, message: Message) => boolean
17
+ buildPongMessage: (clientId: string, message: Message) => Message
18
+ }
19
+
20
+ /**
21
+ * 表示主动心跳消息处理器的构造器。
22
+ */
23
+ export type ActiveHeartbeatMessageHandlerGenerator<Message> = () => ActiveHeartbeatMessageHandler<Message>
24
+
25
+ /**
26
+ * 表示被动心跳消息处理器的构造器。
27
+ */
28
+ export type PassiveHeartbeatMessageHandlerGenerator<Message> = () => PassiveHeartbeatMessageHandler<Message>
29
+
30
+ /**
31
+ * 表示 SocketUnitHeartbeat 的配置项。
32
+ */
33
+ export interface SocketUnitHeartbeatOptions<Message> extends LoggerFriendlyOptions {
34
+ enableActiveHeartbeat?: boolean | undefined
35
+ enablePassiveHeartbeat?: boolean | undefined
36
+ heartbeatInterval?: number | undefined
37
+ maxResponseTime?: number | undefined
38
+ maxHeartbeatLostCount?: number | undefined
39
+ activeHeartbeatMessageHandlerGenerator?: ActiveHeartbeatMessageHandlerGenerator<Message> | undefined
40
+ passiveHeartbeatMessageHandlerGenerator?: PassiveHeartbeatMessageHandlerGenerator<Message> | undefined
41
+ }
42
+
43
+ /**
44
+ * 验证心跳配置是否满足统一运行时的基本约束。
45
+ */
46
+ export const validateSocketUnitHeartbeatOptions = <Message>(options: SocketUnitHeartbeatOptions<Message>): void => {
47
+ if (
48
+ options.enableActiveHeartbeat === true
49
+ && options.activeHeartbeatMessageHandlerGenerator === undefined
50
+ ) {
51
+ throw new Error("Active heartbeat message handler is required when active heartbeat is enabled.")
52
+ }
53
+
54
+ if (
55
+ options.enablePassiveHeartbeat === true
56
+ && options.passiveHeartbeatMessageHandlerGenerator === undefined
57
+ ) {
58
+ throw new Error("Passive heartbeat message handler is required when passive heartbeat is enabled.")
59
+ }
60
+
61
+ if (
62
+ options.heartbeatInterval !== undefined
63
+ && (!Number.isFinite(options.heartbeatInterval) || options.heartbeatInterval <= 0)
64
+ ) {
65
+ throw new Error("Heartbeat interval must be a positive finite number.")
66
+ }
67
+
68
+ if (
69
+ options.maxResponseTime !== undefined
70
+ && (!Number.isFinite(options.maxResponseTime) || options.maxResponseTime <= 0)
71
+ ) {
72
+ throw new Error("Max response time must be a positive finite number.")
73
+ }
74
+
75
+ if (
76
+ options.maxHeartbeatLostCount !== undefined
77
+ && (!Number.isFinite(options.maxHeartbeatLostCount) || options.maxHeartbeatLostCount < 1)
78
+ ) {
79
+ throw new Error("Max heartbeat lost count must be at least 1.")
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 表示补齐默认值后的心跳配置。
85
+ */
86
+ export interface ResolvedSocketUnitHeartbeatOptions<Message> extends LoggerFriendlyOptions {
87
+ enableActiveHeartbeat: boolean
88
+ enablePassiveHeartbeat: boolean
89
+ heartbeatInterval: number
90
+ maxResponseTime: number
91
+ maxHeartbeatLostCount: number
92
+ activeHeartbeatMessageHandlerGenerator?: ActiveHeartbeatMessageHandlerGenerator<Message> | undefined
93
+ passiveHeartbeatMessageHandlerGenerator?: PassiveHeartbeatMessageHandlerGenerator<Message> | undefined
94
+ }
95
+
96
+ /**
97
+ * 为心跳配置补齐统一运行时所需的默认值。
98
+ */
99
+ export const resolveSocketUnitHeartbeatOptions = <Message>(options: SocketUnitHeartbeatOptions<Message>): ResolvedSocketUnitHeartbeatOptions<Message> => {
100
+ return {
101
+ ...options,
102
+ enableActiveHeartbeat: options.enableActiveHeartbeat ?? false,
103
+ enablePassiveHeartbeat: options.enablePassiveHeartbeat ?? false,
104
+ heartbeatInterval: options.heartbeatInterval ?? 30 * 1_000,
105
+ maxResponseTime: options.maxResponseTime ?? 10 * 1_000,
106
+ maxHeartbeatLostCount: options.maxHeartbeatLostCount ?? 3,
107
+ activeHeartbeatMessageHandlerGenerator: options.activeHeartbeatMessageHandlerGenerator,
108
+ passiveHeartbeatMessageHandlerGenerator: options.passiveHeartbeatMessageHandlerGenerator,
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 表示心跳运行时与外部 SocketUnit 之间的回调边界。
114
+ */
115
+ export interface SocketUnitHeartbeatCallbacks {
116
+ sendMessage: (message: { action: string, messageString: string }) => boolean
117
+ close: () => void
118
+ }
119
+
120
+ /**
121
+ * 表示单个心跳轮次的状态。
122
+ */
123
+ export type SocketUnitHeartbeatStateStatus = "waiting" | "success" | "fail"
124
+
125
+ /**
126
+ * 表示统一心跳运行时内部保留的一条轮次记录。
127
+ */
128
+ export interface SocketUnitHeartbeatState<Message> {
129
+ activeHeartbeatMessageHandler: ActiveHeartbeatMessageHandler<Message>
130
+ responseTimer: ReturnType<typeof setTimeout> | undefined
131
+ status: SocketUnitHeartbeatStateStatus
132
+ pingMessageString: string | undefined
133
+ pongMessageString: string | undefined
134
+ }
135
+
136
+ /**
137
+ * 表示单个心跳轮次对外暴露的只读快照。
138
+ */
139
+ export interface SocketUnitHeartbeatStateSnapshot {
140
+ status: SocketUnitHeartbeatStateStatus
141
+ hasPendingResponseTimer: boolean
142
+ pingMessageString: string | undefined
143
+ pongMessageString: string | undefined
144
+ }
145
+
146
+ /**
147
+ * 表示统一心跳运行时的聚合快照。
148
+ */
149
+ export interface SocketUnitHeartbeatSnapshot {
150
+ enabledActiveHeartbeat: boolean
151
+ enabledPassiveHeartbeat: boolean
152
+ isRunning: boolean
153
+ waitingCount: number
154
+ successCount: number
155
+ failCount: number
156
+ recentStates: SocketUnitHeartbeatStateSnapshot[]
157
+ }
158
+
159
+ /**
160
+ * 管理 SocketUnit 的统一心跳运行时。
161
+ */
162
+ export class SocketUnitHeartbeat<Message> implements LoggerFriendly {
163
+ protected readonly options: ResolvedSocketUnitHeartbeatOptions<Message>
164
+ protected readonly callbacks: SocketUnitHeartbeatCallbacks
165
+
166
+ readonly logger: Logger
167
+
168
+ protected heartbeatTimer: ReturnType<typeof setInterval> | undefined
169
+ protected heartbeatStates: Array<SocketUnitHeartbeatState<Message>>
170
+
171
+ constructor(
172
+ options: SocketUnitHeartbeatOptions<Message>,
173
+ callbacks: SocketUnitHeartbeatCallbacks,
174
+ ) {
175
+ validateSocketUnitHeartbeatOptions(options)
176
+
177
+ this.options = resolveSocketUnitHeartbeatOptions(options)
178
+ this.callbacks = callbacks
179
+
180
+ this.logger = Logger.fromOptions(options).setDefaultName("SocketUnitHeartbeat")
181
+
182
+ this.heartbeatTimer = undefined
183
+ this.heartbeatStates = []
184
+ }
185
+
186
+
187
+ /**
188
+ * 返回统一心跳运行时的诊断快照。
189
+ */
190
+ getSnapshot(): SocketUnitHeartbeatSnapshot {
191
+ let waitingCount = 0
192
+ let successCount = 0
193
+ let failCount = 0
194
+
195
+ const recentStates = this.heartbeatStates.map((heartbeatState) => {
196
+ switch (heartbeatState.status) {
197
+ case "waiting": {
198
+ waitingCount = waitingCount + 1
199
+ break
200
+ }
201
+ case "success": {
202
+ successCount = successCount + 1
203
+ break
204
+ }
205
+ case "fail": {
206
+ failCount = failCount + 1
207
+ break
208
+ }
209
+ default: {
210
+ throw new Error("Unexpected heartbeat state status.")
211
+ }
212
+ }
213
+
214
+ return {
215
+ status: heartbeatState.status,
216
+ hasPendingResponseTimer: heartbeatState.responseTimer !== undefined,
217
+ pingMessageString: heartbeatState.pingMessageString,
218
+ pongMessageString: heartbeatState.pongMessageString,
219
+ }
220
+ })
221
+
222
+ return {
223
+ enabledActiveHeartbeat: this.options.enableActiveHeartbeat,
224
+ enabledPassiveHeartbeat: this.options.enablePassiveHeartbeat,
225
+ isRunning: this.heartbeatTimer !== undefined,
226
+ waitingCount,
227
+ successCount,
228
+ failCount,
229
+ recentStates,
230
+ }
231
+ }
232
+
233
+ /**
234
+ * 在启用主动心跳时启动轮询调度。
235
+ */
236
+ start(clientId: string): void {
237
+ if (this.options.enableActiveHeartbeat === false) {
238
+ return
239
+ }
240
+
241
+ if (this.heartbeatTimer !== undefined) {
242
+ return
243
+ }
244
+
245
+ this.heartbeatTimer = setInterval(() => {
246
+ this.runActiveHeartbeat(clientId)
247
+ }, this.options.heartbeatInterval)
248
+ }
249
+
250
+ /**
251
+ * 停止心跳调度并清理全部轮次状态。
252
+ */
253
+ stop(): void {
254
+ if (this.heartbeatTimer !== undefined) {
255
+ clearInterval(this.heartbeatTimer)
256
+ this.heartbeatTimer = undefined
257
+ }
258
+
259
+ this.heartbeatStates.forEach((heartbeatState) => {
260
+ if (heartbeatState.responseTimer !== undefined) {
261
+ clearTimeout(heartbeatState.responseTimer)
262
+ }
263
+ })
264
+ this.heartbeatStates = []
265
+ }
266
+
267
+ protected runActiveHeartbeat(clientId: string): void {
268
+ const activeHeartbeatMessageHandler = this.options.activeHeartbeatMessageHandlerGenerator!()
269
+ const heartbeatState: SocketUnitHeartbeatState<Message> = {
270
+ activeHeartbeatMessageHandler,
271
+ responseTimer: undefined,
272
+ status: "waiting",
273
+ pingMessageString: undefined,
274
+ pongMessageString: undefined,
275
+ }
276
+ this.heartbeatStates.push(heartbeatState)
277
+
278
+ const pingMessage = activeHeartbeatMessageHandler.buildPingMessage(clientId)
279
+ const pingMessageString = JSON.stringify(pingMessage)
280
+ heartbeatState.pingMessageString = pingMessageString
281
+
282
+ const sendResult = this.callbacks.sendMessage({
283
+ action: "Active heartbeat ping send",
284
+ messageString: pingMessageString,
285
+ })
286
+ if (sendResult === true) {
287
+ this.logger.info(`Active heartbeat ping message sent: ${clientId}, ${pingMessageString}`)
288
+ }
289
+ else {
290
+ heartbeatState.status = "fail"
291
+ this.callbacks.close()
292
+ return
293
+ }
294
+
295
+ heartbeatState.responseTimer = setTimeout(() => {
296
+ if (heartbeatState.status !== "waiting") {
297
+ return
298
+ }
299
+
300
+ heartbeatState.responseTimer = undefined
301
+ heartbeatState.status = "fail"
302
+ const timeoutCount = this.countConsecutiveHeartbeatTimeouts(this.heartbeatStates)
303
+ if (timeoutCount >= this.options.maxHeartbeatLostCount) {
304
+ this.logger.error("Heartbeat timeout limit reached. Closing WebSocket connection.")
305
+ this.callbacks.close()
306
+ }
307
+ }, this.options.maxResponseTime)
308
+ }
309
+
310
+ handleMessage(
311
+ clientId: string,
312
+ message: Message,
313
+ ): boolean {
314
+ const activeHandleResult = this.handleActiveHeartbeatMessage(
315
+ clientId, message
316
+ )
317
+ if (activeHandleResult === true) {
318
+ return true
319
+ }
320
+
321
+ const passiveHandleResult = this.handlePassiveHeartbeatMessage(
322
+ clientId, message
323
+ )
324
+ return passiveHandleResult
325
+ }
326
+
327
+ protected handleActiveHeartbeatMessage(
328
+ clientId: string,
329
+ message: Message,
330
+ ): boolean {
331
+ if (this.options.enableActiveHeartbeat === false) {
332
+ return false
333
+ }
334
+
335
+ const heartbeatState = this.findMatchingWaitingHeartbeatState(clientId, message)
336
+ if (heartbeatState === undefined) {
337
+ return false
338
+ }
339
+
340
+ const messageString = JSON.stringify(message)
341
+ this.logger.info(`Active heartbeat pong message received: ${clientId}, ${messageString}`)
342
+ if (heartbeatState.responseTimer !== undefined) {
343
+ clearTimeout(heartbeatState.responseTimer)
344
+ heartbeatState.responseTimer = undefined
345
+ }
346
+ heartbeatState.pongMessageString = messageString
347
+ heartbeatState.status = "success"
348
+ this.cleanupHeartbeatStatesAfterSuccess(heartbeatState)
349
+ return true
350
+ }
351
+
352
+ protected handlePassiveHeartbeatMessage(
353
+ clientId: string,
354
+ message: Message,
355
+ ): boolean {
356
+ if (this.options.enablePassiveHeartbeat === false) {
357
+ return false
358
+ }
359
+
360
+ const passiveHeartbeatMessageHandler = this.options.passiveHeartbeatMessageHandlerGenerator!()
361
+ if (passiveHeartbeatMessageHandler.verifyPingMessage(clientId, message) === false) {
362
+ return false
363
+ }
364
+
365
+ const messageString = JSON.stringify(message)
366
+ this.logger.info(`Passive heartbeat ping message received: ${clientId}, ${messageString}`)
367
+ const pongMessage = passiveHeartbeatMessageHandler.buildPongMessage(clientId, message)
368
+ const pongMessageString = JSON.stringify(pongMessage)
369
+ const sendResult = this.callbacks.sendMessage({
370
+ action: "Passive heartbeat pong send",
371
+ messageString: pongMessageString,
372
+ })
373
+ if (sendResult === true) {
374
+ this.logger.info(`Passive heartbeat pong message sent: ${clientId}, ${pongMessageString}`)
375
+ }
376
+ return true
377
+ }
378
+
379
+ protected findMatchingWaitingHeartbeatState(
380
+ clientId: string,
381
+ message: Message,
382
+ ): SocketUnitHeartbeatState<Message> | undefined {
383
+ for (let index = this.heartbeatStates.length - 1; index >= 0; index = index - 1) {
384
+ const heartbeatState = this.heartbeatStates[index]!
385
+
386
+ if (heartbeatState.status !== "waiting") {
387
+ continue
388
+ }
389
+
390
+ if (heartbeatState.activeHeartbeatMessageHandler.verifyPongMessage(clientId, message) === true) {
391
+ return heartbeatState
392
+ }
393
+ }
394
+
395
+ return undefined
396
+ }
397
+
398
+ protected countConsecutiveHeartbeatTimeouts(
399
+ heartbeatStates: Array<SocketUnitHeartbeatState<Message>>,
400
+ ): number {
401
+ let timeoutCount = 0
402
+
403
+ for (let index = heartbeatStates.length - 1; index >= 0; index = index - 1) {
404
+ const heartbeatState = heartbeatStates[index]!
405
+
406
+ if (heartbeatState.status === "fail") {
407
+ timeoutCount = timeoutCount + 1
408
+ continue
409
+ }
410
+
411
+ break
412
+ }
413
+
414
+ return timeoutCount
415
+ }
416
+
417
+ protected cleanupHeartbeatStatesAfterSuccess(
418
+ successState: SocketUnitHeartbeatState<Message>,
419
+ ): void {
420
+ const successStateIndex = this.heartbeatStates.lastIndexOf(successState)
421
+ if (successStateIndex === -1) {
422
+ return
423
+ }
424
+
425
+ this.heartbeatStates.splice(0, successStateIndex + 1)
426
+ }
427
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./common/index.ts"
2
+ export * as Server from "./server/index.ts"
3
+ export * as Client from "./client/index.ts"
@@ -0,0 +1,3 @@
1
+ export * from "./socket.ts"
2
+ export * from "./socket-unit.ts"
3
+ export * from "./server.ts"