@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.
- package/CHANGELOG.md +37 -0
- package/dist/index.js +706 -36
- package/dist/index.js.map +855 -59
- package/package.json +28 -16
- 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/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 +200 -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 +6 -5
- package/src/event/common.ts +13 -3
- package/src/event/event-manager.ts +3 -3
- package/src/event/instance-event-proxy.ts +6 -5
- package/src/event/internal.ts +4 -4
- package/src/form/README.md +25 -0
- package/src/form/index.ts +1 -0
- package/src/form/inputor-controller/base.ts +874 -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 +181 -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 +37 -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 +297 -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 +19 -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 +510 -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 +285 -0
- package/src/request/fetch/general.ts +20 -0
- package/src/request/fetch/index.ts +4 -0
- package/src/request/fetch/nodejs.ts +285 -0
- package/src/request/index.ts +2 -0
- package/src/request/request/base.ts +250 -0
- package/src/request/request/general.ts +64 -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 +54 -0
- package/src/result/either.ts +193 -0
- package/src/result/index.ts +2 -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 +1642 -0
- package/src/route/uri/hash.ts +308 -0
- package/src/route/uri/index.ts +7 -0
- package/src/route/uri/pathname.ts +376 -0
- package/src/route/uri/search.ts +413 -0
- package/src/socket/README.md +105 -0
- package/src/socket/client/index.ts +2 -0
- package/src/socket/client/socket-unit.ts +660 -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 +449 -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 +138 -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 +159 -0
- package/src/weixin/official-account/index.ts +2 -0
- package/src/weixin/official-account/js-api.ts +134 -0
- package/src/weixin/open/index.ts +1 -0
- package/src/weixin/open/oauth2.ts +133 -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 +147 -0
- package/tests/unit/aio/prompt.spec.ts +111 -0
- package/tests/unit/basic/error.spec.ts +16 -4
- 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 +37 -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 +41 -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/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 +191 -0
- 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 +385 -0
- package/tests/unit/request/request/general.spec.ts +161 -0
- package/tests/unit/route/router/route.spec.ts +431 -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 +147 -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 +135 -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/src/random/string.ts +0 -35
- package/tests/unit/random/string.spec.ts +0 -11
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断给定值是否为字符串。
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```
|
|
6
|
+
* // Expect: true
|
|
7
|
+
* const example1 = internalIsString("page=1")
|
|
8
|
+
*
|
|
9
|
+
* // Expect: false
|
|
10
|
+
* const example2 = internalIsString({ page: "1" })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
const internalIsString = (value: unknown): value is string => {
|
|
14
|
+
return typeof value === "string"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 判断给定值是否为普通对象。
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```
|
|
22
|
+
* // Expect: true
|
|
23
|
+
* const example1 = internalIsObject({ page: "1" })
|
|
24
|
+
*
|
|
25
|
+
* // Expect: false
|
|
26
|
+
* const example2 = internalIsObject(["page"])
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
const internalIsObject = (value: unknown): value is Record<string, string> => {
|
|
30
|
+
return typeof value === "object" && value !== null && Array.isArray(value) === false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 将单个 query 拆分为 key 和 value。
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```
|
|
38
|
+
* // Expect: ["page", "1"]
|
|
39
|
+
* const example1 = internalSplitQueryEntry("page=1")
|
|
40
|
+
*
|
|
41
|
+
* // Expect: ["formula", "a=b"]
|
|
42
|
+
* const example2 = internalSplitQueryEntry("formula=a=b")
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
const internalSplitQueryEntry = (query: string): [key: string, value: string] => {
|
|
46
|
+
const internalEqualSignIndex = query.indexOf("=")
|
|
47
|
+
|
|
48
|
+
if (internalEqualSignIndex === -1) {
|
|
49
|
+
const key = query
|
|
50
|
+
const value = ""
|
|
51
|
+
return [key, value]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const key = query.slice(0, internalEqualSignIndex)
|
|
55
|
+
const value = query.slice(internalEqualSignIndex + 1)
|
|
56
|
+
return [key, value]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 不带 `?` 前缀的 query 字符串。
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```
|
|
64
|
+
* // Expect: "page=1"
|
|
65
|
+
* const example1: QueryString = "page=1"
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export type QueryString = string
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 查询参数对象。
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```
|
|
75
|
+
* // Expect: { page: "1" }
|
|
76
|
+
* const example1: QueryObject = { page: "1" }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export type QueryObject = Record<string, string>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 查询参数支持的输入或输出形态。
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```
|
|
86
|
+
* // Expect: "page=1"
|
|
87
|
+
* const example1: QueryUnion = "page=1"
|
|
88
|
+
*
|
|
89
|
+
* // Expect: { page: "1" }
|
|
90
|
+
* const example2: QueryUnion = { page: "1" }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export type QueryUnion = QueryString | QueryObject
|
|
94
|
+
|
|
95
|
+
export type SearchStringLoose = string
|
|
96
|
+
/**
|
|
97
|
+
* 带 `?` 前缀的 search 字符串。
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```
|
|
101
|
+
* // Expect: "?page=1"
|
|
102
|
+
* const example1: SearchStringStrict = "?page=1"
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export type SearchStringStrict = `?${string}`
|
|
106
|
+
|
|
107
|
+
export type SearchUnion = QueryString | QueryObject | SearchStringLoose | SearchStringStrict
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 规范化 search 字符串,确保其带有 `?` 前缀。
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```
|
|
114
|
+
* // Expect: "?name=cigaret"
|
|
115
|
+
* const example1 = neatenSearch("?name=cigaret")
|
|
116
|
+
*
|
|
117
|
+
* // Expect: "?name=cigaret"
|
|
118
|
+
* const example2 = neatenSearch("name=cigaret")
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export const neatenSearch = (target: string): SearchStringStrict => {
|
|
122
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
123
|
+
return target.startsWith("?") ? (target as SearchStringStrict) : `?${target}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 规范化 query 字符串,确保其不带 `?` 前缀。
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```
|
|
131
|
+
* // Expect: "name=cigaret"
|
|
132
|
+
* const example1 = neatenQueryString("?name=cigaret")
|
|
133
|
+
*
|
|
134
|
+
* // Expect: "name=cigaret"
|
|
135
|
+
* const example2 = neatenQueryString("name=cigaret")
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export const neatenQueryString = (target: string): QueryString => {
|
|
139
|
+
return target.startsWith("?") ? target.slice(1) : target
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 将 query 字符串解析为查询参数对象。
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```
|
|
147
|
+
* // Expect: { name: "cigaret" }
|
|
148
|
+
* const example1 = queryStringToQueryObject("name=cigaret")
|
|
149
|
+
*
|
|
150
|
+
* // Expect: { formula: "a=b" }
|
|
151
|
+
* const example2 = queryStringToQueryObject("formula=a%3Db")
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export const queryStringToQueryObject = (target: QueryString): QueryObject => {
|
|
155
|
+
const queryString = neatenQueryString(target)
|
|
156
|
+
const querys = queryString.split("&")
|
|
157
|
+
const queryObject: QueryObject = {}
|
|
158
|
+
|
|
159
|
+
querys.forEach((query) => {
|
|
160
|
+
if (query !== "") {
|
|
161
|
+
const [rawKey, rawValue] = internalSplitQueryEntry(query)
|
|
162
|
+
queryObject[decodeURIComponent(rawKey)] = decodeURIComponent(rawValue)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return queryObject
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 将查询参数对象序列化为 query 字符串。
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```
|
|
174
|
+
* // Expect: "name=cigaret"
|
|
175
|
+
* const example1 = queryObjectToQueryString({ name: "cigaret" })
|
|
176
|
+
*
|
|
177
|
+
* // Expect: "formula=a%3Db"
|
|
178
|
+
* const example2 = queryObjectToQueryString({ formula: "a=b" })
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export const queryObjectToQueryString = (target: QueryObject): QueryString => {
|
|
182
|
+
const queryString = Object.entries(target)
|
|
183
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
184
|
+
.join("&")
|
|
185
|
+
|
|
186
|
+
return queryString
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 将 search 字符串转换为 query 字符串。
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```
|
|
194
|
+
* // Expect: "page=1"
|
|
195
|
+
* const example1 = searchToQueryString("?page=1")
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export const searchToQueryString = (target: SearchStringLoose): QueryString => {
|
|
199
|
+
return neatenQueryString(target)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 将 search 字符串解析为查询参数对象。
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```
|
|
207
|
+
* // Expect: { page: "1", size: "20" }
|
|
208
|
+
* const example1 = searchToQueryObject("?page=1&size=20")
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export const searchToQueryObject = (target: SearchStringLoose): QueryObject => {
|
|
212
|
+
return queryStringToQueryObject(searchToQueryString(target))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 将 query 字符串转换为 search 字符串。
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```
|
|
220
|
+
* // Expect: "?page=1"
|
|
221
|
+
* const example1 = queryStringToSearch("page=1")
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export const queryStringToSearch = (target: QueryString): SearchStringStrict => {
|
|
225
|
+
return neatenSearch(target)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 将查询参数对象转换为 search 字符串。
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```
|
|
233
|
+
* // Expect: "?page=1&size=20"
|
|
234
|
+
* const example1 = queryObjectToSearch({ page: "1", size: "20" })
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export const queryObjectToSearch = (target: QueryObject): SearchStringStrict => {
|
|
238
|
+
return queryStringToSearch(queryObjectToQueryString(target))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 将查询参数统一转换为 search 字符串。
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```
|
|
246
|
+
* // Expect: "?page=1"
|
|
247
|
+
* const example1 = toSearch("page=1")
|
|
248
|
+
*
|
|
249
|
+
* // Expect: "?page=1"
|
|
250
|
+
* const example2 = toSearch({ page: "1" })
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
export const toSearch = (target: SearchUnion): SearchStringStrict => {
|
|
254
|
+
if (internalIsString(target)) {
|
|
255
|
+
return neatenSearch(target)
|
|
256
|
+
}
|
|
257
|
+
else if (internalIsObject(target)) {
|
|
258
|
+
return queryObjectToSearch(target)
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
throw (new TypeError("\"target\" is expected to be type of \"string\" | \"object\"."))
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 将查询参数统一转换为 query 字符串。
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```
|
|
270
|
+
* // Expect: "page=1"
|
|
271
|
+
* const example1 = toQueryString("?page=1")
|
|
272
|
+
*
|
|
273
|
+
* // Expect: "page=1"
|
|
274
|
+
* const example2 = toQueryString({ page: "1" })
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export const toQueryString = (target: SearchUnion): QueryString => {
|
|
278
|
+
if (internalIsString(target)) {
|
|
279
|
+
return neatenQueryString(target)
|
|
280
|
+
}
|
|
281
|
+
else if (internalIsObject(target)) {
|
|
282
|
+
return queryObjectToQueryString(target)
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
throw (new TypeError("\"target\" is expected to be type of \"string\" | \"object\"."))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 将查询参数统一转换为查询参数对象。
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```
|
|
294
|
+
* // Expect: { page: "1" }
|
|
295
|
+
* const example1 = toQueryObject("?page=1")
|
|
296
|
+
*
|
|
297
|
+
* // Expect: { page: "1" }
|
|
298
|
+
* const example2 = toQueryObject({ page: "1" })
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
export const toQueryObject = (target: SearchUnion): QueryObject => {
|
|
302
|
+
if (internalIsString(target)) {
|
|
303
|
+
return queryStringToQueryObject(target)
|
|
304
|
+
}
|
|
305
|
+
else if (internalIsObject(target)) {
|
|
306
|
+
return target
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
throw (new TypeError("\"target\" is expected to be type of \"string\" | \"object\"."))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 宽松包含关系:查询参数的键集合是目标查询参数键集合的子集。
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```
|
|
318
|
+
* // Expect: true
|
|
319
|
+
* const example1 = isSearchLooseIncludes("?a=1&b=2", ["a"])
|
|
320
|
+
*
|
|
321
|
+
* // Expect: false
|
|
322
|
+
* const example2 = isSearchLooseIncludes("?a=1&b=2", ["a", "c"])
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
export const isSearchLooseIncludes = (search: SearchUnion, query: string | string[]): boolean => {
|
|
326
|
+
const existKeys = Object.keys(toQueryObject(search))
|
|
327
|
+
const queryKeys = internalIsString(query) ? [query] : query
|
|
328
|
+
return queryKeys.every(queryKey => existKeys.includes(queryKey))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 严格包含关系:查询参数的键集合与目标查询参数键集合完全相同。
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```
|
|
336
|
+
* // Expect: true
|
|
337
|
+
* const example1 = isSearchStrictIncludes("?a=1&b=2", ["a", "b"])
|
|
338
|
+
*
|
|
339
|
+
* // Expect: false
|
|
340
|
+
* const example2 = isSearchStrictIncludes("?a=1&b=2", ["a"])
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export const isSearchStrictIncludes = (search: SearchUnion, query: string | string[]): boolean => {
|
|
344
|
+
const existKeys = Object.keys(toQueryObject(search)).toSorted()
|
|
345
|
+
const queryKeys = (internalIsString(query) ? [query] : query).toSorted()
|
|
346
|
+
|
|
347
|
+
if (existKeys.length !== queryKeys.length) {
|
|
348
|
+
return false
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return existKeys.every((key, index) => key === queryKeys[index])
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 包含关系:查询参数的键集合必须包含目标查询参数键集合,并且不能包含除目标查询参数键集合之外的其他键。
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```
|
|
359
|
+
* // Expect: true
|
|
360
|
+
* const example1 = isSearchIncludes("?a=1&b=2", { a: "required", b: "optional" })
|
|
361
|
+
*
|
|
362
|
+
* // Expect: false
|
|
363
|
+
* const example2 = isSearchIncludes("?a=1&b=2", { a: "required" })
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
export const isSearchIncludes = (search: SearchUnion, query: Record<string, "required" | "optional">): boolean => {
|
|
367
|
+
const existKeys = Object.keys(toQueryObject(search))
|
|
368
|
+
const requiredKeys = Object.entries(query).filter(([, value]) => value === "required").map(([key]) => key)
|
|
369
|
+
const optionalKeys = Object.entries(query).filter(([, value]) => value === "optional").map(([key]) => key)
|
|
370
|
+
|
|
371
|
+
const requiredKeysExist = requiredKeys.every(requiredKey => existKeys.includes(requiredKey))
|
|
372
|
+
const extraKeysExist = existKeys.some(existKey => !requiredKeys.includes(existKey) && !optionalKeys.includes(existKey))
|
|
373
|
+
|
|
374
|
+
return requiredKeysExist && !extraKeysExist
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 宽松相等:查询参数的键集合是目标查询参数键集合的子集,并且对应的值相等。
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```
|
|
382
|
+
* // Expect: true
|
|
383
|
+
* const example1 = isSearchLooseEqual("?a=1&b=2", "?a=1")
|
|
384
|
+
*
|
|
385
|
+
* // Expect: false
|
|
386
|
+
* const example2 = isSearchLooseEqual("?a=1&b=2", "?a=1&c=3")
|
|
387
|
+
* ```
|
|
388
|
+
*/
|
|
389
|
+
export const isSearchLooseEqual = (searchA: SearchUnion, searchB: SearchUnion): boolean => {
|
|
390
|
+
const queryObjectA = toQueryObject(searchA)
|
|
391
|
+
const queryObjectB = toQueryObject(searchB)
|
|
392
|
+
return Object.entries(queryObjectB).every(([key, value]) => queryObjectA[key] === value)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 严格相等:查询参数的键集合与目标查询参数键集合完全相同,并且对应的值相等。
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```
|
|
400
|
+
* // Expect: true
|
|
401
|
+
* const example1 = isSearchStrictEqual("?a=1&b=2", "?b=2&a=1")
|
|
402
|
+
*
|
|
403
|
+
* // Expect: false
|
|
404
|
+
* const example2 = isSearchStrictEqual("?a=1&b=2", "?a=1")
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
export const isSearchStrictEqual = (searchA: SearchUnion, searchB: SearchUnion): boolean => {
|
|
408
|
+
const queryObjectA = toQueryObject(searchA)
|
|
409
|
+
const queryObjectB = toQueryObject(searchB)
|
|
410
|
+
const lengthEqual = Object.keys(queryObjectA).length === Object.keys(queryObjectB).length
|
|
411
|
+
const valueEqual = Object.entries(queryObjectB).every(([key, value]) => queryObjectA[key] === value)
|
|
412
|
+
return lengthEqual && valueEqual
|
|
413
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Socket
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Socket 模块提供围绕双向数据通道(bidirectional data channel)的基础建模能力,用于在客户端与服务端之间组织一类可持续存在、可观测、可发送、可关闭、并带有稳定生命周期语义的数据连接。
|
|
6
|
+
|
|
7
|
+
Socket 本质上首先是数据通道,而不是某个特定业务协议的实现容器。无论是业务消息,还是心跳、初始化消息等机制性消息,从这个模块的边界来看都属于“通道中流动的数据”;它们可以在行为上被区别对待,但不应在模块级类型边界里被人为拆成两套完全不同的体系。
|
|
8
|
+
|
|
9
|
+
## For Understanding
|
|
10
|
+
|
|
11
|
+
理解 Socket 模块时,最重要的前提不是“WebSocket 怎么用”,而是“一个稳定的数据通道模型该对外承诺什么”。这个模块试图表达的,并不是原生 API 的逐项映射,而是以下几类更稳定的公共语义:
|
|
12
|
+
|
|
13
|
+
- 连接生命周期应被建模为可以推断和观察的状态,而不是依赖零散事件时序去猜测。
|
|
14
|
+
- 通道 ready 语义应明确:只有在传输已打开且当前端点已具备可工作的身份条件后,业务消息和初始消息才应真正发送。
|
|
15
|
+
- 心跳属于通道运行机制的一部分,但仍然是消息流中的数据,不要求固定包结构,也不应把客户端与服务端的方向差异实现成两套长期分叉的运行时。
|
|
16
|
+
- 对外事件应尽量表达稳定状态转移,而不是传播实现过程中的偶然中间态或重复噪音。
|
|
17
|
+
|
|
18
|
+
因此,这个模块更适合被理解为“围绕数据通道生命周期、消息流转与连接保活机制的统一模型层”,而不是简单的 WebSocket 工具封装。这里有几个关键理解边界需要守住。
|
|
19
|
+
|
|
20
|
+
第一,客户端和服务端虽然在方向上不同,但它们共享的是同一种问题域:如何让一个通道稳定地进入可工作状态、如何让消息在这个状态下流转、以及如何在通道不再可靠时完成清理。因此,心跳运行时应尽量收束为一套共享实现,用配置表达主动与被动方向差异,而不是把方向差异扩大成重复类层次。
|
|
21
|
+
|
|
22
|
+
第二,心跳不应要求固定消息形状。这个模块允许使用者通过消息处理器定义“什么是 ping、什么是 pong、如何匹配响应”,统一运行时只负责调度、保留轮次状态、按规则匹配与在超时后做出关闭决策。也就是说,模块公共边界关注的是心跳行为模型,而不是某个预设协议字段。
|
|
23
|
+
|
|
24
|
+
第三,公共事件的语义必须稳定。比如服务端的 `connect` 应在对应 `SocketUnit` 已经进入可观察、可工作的 started 状态后再发出;客户端的状态事件也应以真实状态迁移为准,而不是在重复消息流量下不断重新广播同一个 `OPEN`。如果公共事件失去这类稳定性,接入者就会被迫去猜内部实现细节,从而破坏模块边界。
|
|
25
|
+
|
|
26
|
+
第四,这个模块关心的是“通道模型”,不是完整协议栈。它不负责定义认证协商、业务指令格式、重连策略编排、分布式会话一致性或消息持久化语义;这些都可以建立在 Socket 之上,但不应反过来改写 Socket 模块自身的父边界。
|
|
27
|
+
|
|
28
|
+
## For Using
|
|
29
|
+
|
|
30
|
+
从使用角度看,Socket 模块大致可以理解为四类能力。
|
|
31
|
+
|
|
32
|
+
- 通道生命周期能力:用 `SocketUnit` 与更高层的 `Socket` 表达连接的创建、关闭、重建、ready 判定与状态观察,让上层不必直接围绕原生事件时序写分散逻辑。
|
|
33
|
+
- 消息通道能力:统一处理业务消息、初始消息与机制性消息的发送边界。对于调用方来说,只需要关心“当前是否 ready 以及消息何时真正出站”,而不是在每处都手动判断底层传输状态。
|
|
34
|
+
- 心跳保活能力:通过统一心跳运行时支持主动 ping、被动 pong、超时统计、消息匹配与诊断快照。客户端与服务端在方向上可不同,但都应接入同一套共享的心跳建模方式。
|
|
35
|
+
- 服务端承载能力:通过服务端 `Socket` 与 `WebSocketServer` 包装类,管理多连接接入、稳定的连接事件时机,以及可直接用于本地开发和测试的服务地址发现结果。
|
|
36
|
+
|
|
37
|
+
实际接入时,更合理的思路通常不是先盯着某个方法名,而是先判断你的问题是否确实属于“数据通道模型”边界。例如,如果你需要:
|
|
38
|
+
|
|
39
|
+
- 在 ready 之前先缓存待发送动作,等连接真正具备工作条件后再统一发送;
|
|
40
|
+
- 把连接状态变化暴露成稳定事件,而不是围绕原生事件做重复判定;
|
|
41
|
+
- 用业务自定义消息结构实现心跳,而不是被迫接受某种固定心跳包格式;
|
|
42
|
+
- 在服务端统一管理多条连接,并对外暴露稳定的 `connect / close / message` 事件面;
|
|
43
|
+
|
|
44
|
+
那么这个模块就是合适的边界。
|
|
45
|
+
|
|
46
|
+
相反,如果你的目标主要是定义一套业务协议、管理复杂重连退避、实现会话恢复、做消息落盘或做跨节点广播,这些通常已经超出 Socket 模块本身,应该在更上层的协议、编排或基础设施边界中处理。
|
|
47
|
+
|
|
48
|
+
## For Contributing
|
|
49
|
+
|
|
50
|
+
为 Socket 模块继续贡献时,应优先维护这几个不能退让的设计精神,而不是只追求局部功能可用。
|
|
51
|
+
|
|
52
|
+
- Socket 首先是数据通道模型,文档与实现都不应把它收窄成某个具体业务协议的包装层。
|
|
53
|
+
- 机制性消息与业务消息都属于通道中的数据;心跳可以有自己的行为分支,但不应被提升为一套强绑定消息格式。
|
|
54
|
+
- 心跳运行时应保持单一共享实现,客户端与服务端的方向差异应通过配置、处理器或薄适配层表达,而不是重新复制一套长期分叉的运行时代码。
|
|
55
|
+
- 生命周期事件必须表达稳定状态转移。若某个公开事件只是内部实现顺序的偶然产物,或会在同一状态下重复噪音式触发,就应优先调整实现语义,而不是把不稳定性转嫁给调用方。
|
|
56
|
+
- 服务端连接与客户端连接在语义上应尽量对齐:错误必须被显式观测并进入确定性的清理路径,关闭路径必须保持幂等,ready 相关副作用应由明确的状态转移驱动。
|
|
57
|
+
|
|
58
|
+
### JSDoc 注释格式要求
|
|
59
|
+
|
|
60
|
+
- 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
|
|
61
|
+
- JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
|
|
62
|
+
- 如果描述后还有其他内容,应在描述后加一个空行。
|
|
63
|
+
- 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
|
|
64
|
+
- 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
|
|
65
|
+
- 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
|
|
66
|
+
- 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
|
|
67
|
+
- 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
|
|
68
|
+
- 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
|
|
69
|
+
- 如果函数返回结构化字符串,应展示其预期格式特征。
|
|
70
|
+
- 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
|
|
71
|
+
|
|
72
|
+
### 实现规范要求
|
|
73
|
+
|
|
74
|
+
- 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
|
|
75
|
+
- 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
|
|
76
|
+
- 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
|
|
77
|
+
- 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
|
|
78
|
+
- 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
|
|
79
|
+
- 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
|
|
80
|
+
- 辅助元素永远不要公开导出。
|
|
81
|
+
- 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/socket/internal.ts`。
|
|
82
|
+
- 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
|
|
83
|
+
- 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
|
|
84
|
+
- 子模块不需要有自己的 `README.md`。
|
|
85
|
+
- 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
|
|
86
|
+
- 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
|
|
87
|
+
- 与 socket 相关的实现应优先围绕生命周期状态、ready 语义、消息流转、连接级错误处理、心跳匹配与关闭策略组织,避免把协议专属细节或业务会话策略直接下沉为模块长期公共语义。
|
|
88
|
+
|
|
89
|
+
### 导出策略要求
|
|
90
|
+
|
|
91
|
+
- 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
|
|
92
|
+
- 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
|
|
93
|
+
- Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
|
|
94
|
+
- 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的通道语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
|
|
95
|
+
|
|
96
|
+
### 测试要求
|
|
97
|
+
|
|
98
|
+
- 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
|
|
99
|
+
- 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
|
|
100
|
+
- 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
|
|
101
|
+
- 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
|
|
102
|
+
- 测试顺序应与源文件中被测试目标的原始顺序保持一致。
|
|
103
|
+
- 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
|
|
104
|
+
- 模块的单元测试文件目录是 `./tests/unit/socket`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/socket/<sub-module-name>`。
|
|
105
|
+
- 对这个模块来说,测试应优先覆盖 ready 状态转移、消息排队与释放、服务端连接事件时机、错误与关闭路径的幂等性、心跳消息匹配顺序、超时关闭阈值,以及服务地址发现结果在本地环境中的可用性。
|