@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.
- package/CHANGELOG.md +50 -0
- package/oxlint.config.ts +1 -2
- package/package.json +29 -17
- package/scripts/build.ts +2 -52
- package/src/ai/README.md +1 -0
- package/src/ai/ai.ts +107 -0
- package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
- package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
- package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
- package/src/ai/chat-completion-ai/index.ts +7 -0
- package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
- package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
- package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
- package/src/ai/embedding-ai/embedding-ai.ts +63 -0
- package/src/ai/embedding-ai/embedding.ts +50 -0
- package/src/ai/embedding-ai/index.ts +4 -0
- package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
- package/src/ai/index.ts +4 -0
- package/src/aio/README.md +100 -0
- package/src/aio/content.ts +141 -0
- package/src/aio/index.ts +3 -0
- package/src/aio/json.ts +127 -0
- package/src/aio/prompt.ts +246 -0
- package/src/basic/README.md +20 -15
- package/src/basic/error.ts +19 -5
- package/src/basic/function.ts +2 -2
- package/src/basic/index.ts +1 -0
- package/src/basic/promise.ts +141 -71
- package/src/basic/schedule.ts +111 -0
- package/src/basic/stream.ts +135 -25
- package/src/credential/README.md +107 -0
- package/src/credential/api-key.ts +158 -0
- package/src/credential/bearer.ts +73 -0
- package/src/credential/index.ts +4 -0
- package/src/credential/json-web-token.ts +96 -0
- package/src/credential/password.ts +170 -0
- package/src/cron/README.md +86 -0
- package/src/cron/cron.ts +87 -0
- package/src/cron/index.ts +1 -0
- package/src/drizzle/README.md +1 -0
- package/src/drizzle/drizzle.ts +1 -0
- package/src/drizzle/helper.ts +47 -0
- package/src/drizzle/index.ts +5 -0
- package/src/drizzle/infer.ts +52 -0
- package/src/drizzle/kysely.ts +8 -0
- package/src/drizzle/pagination.ts +198 -0
- package/src/email/README.md +1 -0
- package/src/email/index.ts +1 -0
- package/src/email/resend.ts +25 -0
- package/src/event/class-event-proxy.ts +5 -6
- package/src/event/common.ts +13 -3
- package/src/event/event-manager.ts +3 -3
- package/src/event/instance-event-proxy.ts +5 -6
- package/src/event/internal.ts +4 -4
- package/src/exception/README.md +28 -19
- package/src/exception/error/error.ts +123 -0
- package/src/exception/error/index.ts +2 -0
- package/src/exception/error/match.ts +38 -0
- package/src/exception/error/must-fix.ts +17 -0
- package/src/exception/index.ts +2 -0
- package/src/file-system/find.ts +53 -0
- package/src/file-system/index.ts +2 -0
- package/src/file-system/path.ts +76 -0
- package/src/file-system/resolve.ts +22 -0
- package/src/form/README.md +25 -0
- package/src/form/index.ts +1 -0
- package/src/form/inputor-controller/base.ts +861 -0
- package/src/form/inputor-controller/boolean.ts +39 -0
- package/src/form/inputor-controller/file.ts +39 -0
- package/src/form/inputor-controller/form.ts +179 -0
- package/src/form/inputor-controller/helper.ts +117 -0
- package/src/form/inputor-controller/index.ts +17 -0
- package/src/form/inputor-controller/multi-select.ts +99 -0
- package/src/form/inputor-controller/number.ts +116 -0
- package/src/form/inputor-controller/select.ts +109 -0
- package/src/form/inputor-controller/text.ts +82 -0
- package/src/http/READMD.md +1 -0
- package/src/http/api/api-core.ts +84 -0
- package/src/http/api/api-handler.ts +79 -0
- package/src/http/api/api-host.ts +47 -0
- package/src/http/api/api-result.ts +56 -0
- package/src/http/api/api-schema.ts +154 -0
- package/src/http/api/api-server.ts +130 -0
- package/src/http/api/api-test.ts +142 -0
- package/src/http/api/api-type.ts +34 -0
- package/src/http/api/api.ts +81 -0
- package/src/http/api/index.ts +11 -0
- package/src/http/api-adapter/api-core-node-http.ts +260 -0
- package/src/http/api-adapter/api-host-node-http.ts +156 -0
- package/src/http/api-adapter/api-result-arktype.ts +294 -0
- package/src/http/api-adapter/api-result-zod.ts +286 -0
- package/src/http/api-adapter/index.ts +5 -0
- package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
- package/src/http/bin/gen-api-list/index.ts +1 -0
- package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
- package/src/http/bin/gen-api-test/index.ts +1 -0
- package/src/http/bin/gen-api-type/calc-code.ts +25 -0
- package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
- package/src/http/bin/gen-api-type/index.ts +2 -0
- package/src/http/bin/index.ts +2 -0
- package/src/http/index.ts +3 -0
- package/src/huawei/README.md +1 -0
- package/src/huawei/index.ts +2 -0
- package/src/huawei/moderation/index.ts +1 -0
- package/src/huawei/moderation/moderation.ts +355 -0
- package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
- package/src/huawei/obs/index.ts +1 -0
- package/src/huawei/obs/obs.ts +42 -0
- package/src/index.ts +21 -2
- package/src/json/README.md +92 -0
- package/src/json/index.ts +1 -0
- package/src/json/repair.ts +18 -0
- package/src/log/logger.ts +15 -4
- package/src/openai/README.md +1 -0
- package/src/openai/index.ts +1 -0
- package/src/openai/openai.ts +509 -0
- package/src/orchestration/README.md +9 -7
- package/src/orchestration/dispatching/dispatcher.ts +83 -0
- package/src/orchestration/dispatching/index.ts +2 -0
- package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
- package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
- package/src/orchestration/dispatching/selector/index.ts +2 -0
- package/src/orchestration/index.ts +2 -0
- package/src/orchestration/scheduling/index.ts +2 -0
- package/src/orchestration/scheduling/scheduler.ts +103 -0
- package/src/orchestration/scheduling/task.ts +32 -0
- package/src/random/README.md +8 -7
- package/src/random/base.ts +66 -0
- package/src/random/index.ts +5 -1
- package/src/random/random-boolean.ts +40 -0
- package/src/random/random-integer.ts +60 -0
- package/src/random/random-number.ts +72 -0
- package/src/random/random-string.ts +66 -0
- package/src/request/README.md +108 -0
- package/src/request/fetch/base.ts +108 -0
- package/src/request/fetch/browser.ts +280 -0
- package/src/request/fetch/general.ts +20 -0
- package/src/request/fetch/index.ts +4 -0
- package/src/request/fetch/nodejs.ts +280 -0
- package/src/request/index.ts +2 -0
- package/src/request/request/base.ts +246 -0
- package/src/request/request/general.ts +63 -0
- package/src/request/request/index.ts +3 -0
- package/src/request/request/resource.ts +68 -0
- package/src/result/README.md +4 -0
- package/src/result/controller.ts +58 -0
- package/src/result/either.ts +363 -0
- package/src/result/generator.ts +168 -0
- package/src/result/index.ts +3 -0
- package/src/route/README.md +105 -0
- package/src/route/adapter/browser.ts +122 -0
- package/src/route/adapter/driver.ts +56 -0
- package/src/route/adapter/index.ts +2 -0
- package/src/route/index.ts +3 -0
- package/src/route/router/index.ts +2 -0
- package/src/route/router/route.ts +630 -0
- package/src/route/router/router.ts +1641 -0
- package/src/route/uri/hash.ts +307 -0
- package/src/route/uri/index.ts +7 -0
- package/src/route/uri/pathname.ts +376 -0
- package/src/route/uri/search.ts +412 -0
- package/src/service/README.md +1 -0
- package/src/service/index.ts +1 -0
- package/src/service/service.ts +110 -0
- package/src/socket/README.md +105 -0
- package/src/socket/client/index.ts +2 -0
- package/src/socket/client/socket-unit.ts +658 -0
- package/src/socket/client/socket.ts +203 -0
- package/src/socket/common/index.ts +2 -0
- package/src/socket/common/socket-unit-common.ts +23 -0
- package/src/socket/common/socket-unit-heartbeat.ts +427 -0
- package/src/socket/index.ts +3 -0
- package/src/socket/server/index.ts +3 -0
- package/src/socket/server/server.ts +183 -0
- package/src/socket/server/socket-unit.ts +448 -0
- package/src/socket/server/socket.ts +264 -0
- package/src/storage/table.ts +3 -3
- package/src/timer/expiration/expiration-manager.ts +3 -3
- package/src/timer/expiration/remaining-manager.ts +3 -3
- package/src/tube/README.md +99 -0
- package/src/tube/helper.ts +137 -0
- package/src/tube/index.ts +2 -0
- package/src/tube/tube.ts +880 -0
- package/src/weixin/README.md +1 -0
- package/src/weixin/index.ts +2 -0
- package/src/weixin/official-account/authorization.ts +157 -0
- package/src/weixin/official-account/index.ts +2 -0
- package/src/weixin/official-account/js-api.ts +132 -0
- package/src/weixin/open/index.ts +1 -0
- package/src/weixin/open/oauth2.ts +131 -0
- package/tests/unit/ai/ai.spec.ts +85 -0
- package/tests/unit/aio/content.spec.ts +105 -0
- package/tests/unit/aio/json.spec.ts +146 -0
- package/tests/unit/aio/prompt.spec.ts +111 -0
- package/tests/unit/basic/error.spec.ts +16 -4
- package/tests/unit/basic/promise.spec.ts +158 -50
- package/tests/unit/basic/schedule.spec.ts +74 -0
- package/tests/unit/basic/stream.spec.ts +90 -37
- package/tests/unit/credential/api-key.spec.ts +36 -0
- package/tests/unit/credential/bearer.spec.ts +23 -0
- package/tests/unit/credential/json-web-token.spec.ts +23 -0
- package/tests/unit/credential/password.spec.ts +40 -0
- package/tests/unit/cron/cron.spec.ts +84 -0
- package/tests/unit/event/class-event-proxy.spec.ts +3 -3
- package/tests/unit/event/event-manager.spec.ts +3 -3
- package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
- package/tests/unit/exception/error/error.spec.ts +83 -0
- package/tests/unit/exception/error/match.spec.ts +81 -0
- package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
- package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
- package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
- package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
- package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
- package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
- package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
- package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
- package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
- package/tests/unit/http/api/api-core-host.spec.ts +207 -0
- package/tests/unit/http/api/api-schema.spec.ts +120 -0
- package/tests/unit/http/api/api-server.spec.ts +363 -0
- package/tests/unit/http/api/api-test.spec.ts +117 -0
- package/tests/unit/http/api/api.spec.ts +121 -0
- package/tests/unit/http/api-adapter/node-http.spec.ts +187 -0
- package/tests/unit/identifier/uuid.spec.ts +0 -1
- package/tests/unit/json/repair.spec.ts +11 -0
- package/tests/unit/log/logger.spec.ts +19 -4
- package/tests/unit/openai/openai.spec.ts +64 -0
- package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
- package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
- package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
- package/tests/unit/random/base.spec.ts +58 -0
- package/tests/unit/random/random-boolean.spec.ts +25 -0
- package/tests/unit/random/random-integer.spec.ts +32 -0
- package/tests/unit/random/random-number.spec.ts +33 -0
- package/tests/unit/random/random-string.spec.ts +22 -0
- package/tests/unit/request/fetch/browser.spec.ts +222 -0
- package/tests/unit/request/fetch/general.spec.ts +43 -0
- package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
- package/tests/unit/request/request/base.spec.ts +382 -0
- package/tests/unit/request/request/general.spec.ts +160 -0
- package/tests/unit/result/controller.spec.ts +82 -0
- package/tests/unit/result/either.spec.ts +377 -0
- package/tests/unit/result/generator.spec.ts +273 -0
- package/tests/unit/route/router/route.spec.ts +430 -0
- package/tests/unit/route/router/router.spec.ts +407 -0
- package/tests/unit/route/uri/hash.spec.ts +72 -0
- package/tests/unit/route/uri/pathname.spec.ts +146 -0
- package/tests/unit/route/uri/search.spec.ts +107 -0
- package/tests/unit/socket/client.spec.ts +208 -0
- package/tests/unit/socket/server.spec.ts +133 -0
- package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
- package/tests/unit/tube/helper.spec.ts +139 -0
- package/tests/unit/tube/tube.spec.ts +501 -0
- package/vite.config.ts +2 -1
- package/dist/index.js +0 -50
- package/dist/index.js.map +0 -209
- package/src/random/string.ts +0 -35
- package/tests/unit/random/string.spec.ts +0 -11
|
@@ -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,448 @@
|
|
|
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
|
+
const parsedMessage = JSON.parse(messageString) as Message
|
|
341
|
+
return parsedMessage
|
|
342
|
+
}
|
|
343
|
+
catch (exception) {
|
|
344
|
+
this.logger.error("Error when handling message:", exception)
|
|
345
|
+
return undefined
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
protected handleParsedMessage(parsedMessage: Message): void {
|
|
350
|
+
const heartbeatHandleResult = this.heartbeat.handleMessage(
|
|
351
|
+
this.options.clientId,
|
|
352
|
+
parsedMessage,
|
|
353
|
+
)
|
|
354
|
+
if (heartbeatHandleResult === true) {
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.handleBusinessMessage(parsedMessage)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
protected handleBusinessMessage(message: Message): void {
|
|
362
|
+
this.eventManager.emit("message", message)
|
|
363
|
+
this.options.onMessage({
|
|
364
|
+
clientId: this.options.clientId,
|
|
365
|
+
message,
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
protected readonly handleMessage = (message: RawData): void => {
|
|
370
|
+
const parsedMessage = this.parseMessage(message)
|
|
371
|
+
if (parsedMessage === undefined) {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.handleParsedMessage(parsedMessage)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
protected readonly handleClose = (code: number, reason: Buffer): void => {
|
|
379
|
+
this.finalizeClose(code, reason)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
protected readonly handleError = (error: Error): void => {
|
|
383
|
+
this.logger.error(`WebSocket error: ${this.options.clientId}`, error)
|
|
384
|
+
|
|
385
|
+
const webSocket = this.options.webSocket
|
|
386
|
+
if (webSocket.readyState === webSocket.CLOSED) {
|
|
387
|
+
this.finalizeClose(undefined, Buffer.alloc(0))
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (webSocket.readyState !== webSocket.CLOSING) {
|
|
392
|
+
webSocket.close()
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
protected finalizeClose(code: number | undefined, reason: Buffer): void {
|
|
397
|
+
if (this.isClosed === true) {
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.isClosed = true
|
|
402
|
+
this.logger.log(`WebSocket closed: ${this.options.clientId} ${code} ${reason.toString("utf8")}`)
|
|
403
|
+
this.dispose()
|
|
404
|
+
this.eventManager.emit("status", this.getStatus())
|
|
405
|
+
this.options.onClose(this.options.clientId)
|
|
406
|
+
this.logger.log(`WebSocket closed, related resources collected: ${this.options.clientId}`)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
protected dispose(): void {
|
|
410
|
+
this.clearPendingActions()
|
|
411
|
+
|
|
412
|
+
if (this.isStarted === false) {
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
this.options.webSocket.off("close", this.handleClose)
|
|
417
|
+
this.options.webSocket.off("error", this.handleError)
|
|
418
|
+
this.options.webSocket.off("message", this.handleMessage)
|
|
419
|
+
this.heartbeat.stop()
|
|
420
|
+
this.isStarted = false
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
protected safeSendMessage(message: {
|
|
424
|
+
action: string,
|
|
425
|
+
messageString: string
|
|
426
|
+
}): boolean {
|
|
427
|
+
if (this.options.webSocket.readyState !== this.options.webSocket.OPEN) {
|
|
428
|
+
this.logger.warn(`${message.action} skipped because WebSocket is not open: ${this.options.clientId}`)
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
this.options.webSocket.send(message.messageString, (error) => {
|
|
434
|
+
if (error !== undefined) {
|
|
435
|
+
this.logger.error(`${message.action} failed: ${this.options.clientId}`, error)
|
|
436
|
+
this.options.webSocket.close()
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
return true
|
|
441
|
+
}
|
|
442
|
+
catch (exception) {
|
|
443
|
+
this.logger.error(`${message.action} failed: ${this.options.clientId}`, exception)
|
|
444
|
+
this.options.webSocket.close()
|
|
445
|
+
return false
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|