@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,107 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isSearchIncludes,
|
|
5
|
+
isSearchLooseEqual,
|
|
6
|
+
isSearchLooseIncludes,
|
|
7
|
+
isSearchStrictEqual,
|
|
8
|
+
isSearchStrictIncludes,
|
|
9
|
+
neatenQueryString,
|
|
10
|
+
neatenSearch,
|
|
11
|
+
queryObjectToQueryString,
|
|
12
|
+
queryObjectToSearch,
|
|
13
|
+
queryStringToQueryObject,
|
|
14
|
+
queryStringToSearch,
|
|
15
|
+
searchToQueryObject,
|
|
16
|
+
searchToQueryString,
|
|
17
|
+
toQueryObject,
|
|
18
|
+
toQueryString,
|
|
19
|
+
toSearch,
|
|
20
|
+
} from "#Source/route/index.ts"
|
|
21
|
+
|
|
22
|
+
test("neatenSearch ensures the search string starts with a question mark", () => {
|
|
23
|
+
expect(neatenSearch("a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
|
|
24
|
+
expect(neatenSearch("?a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("neatenQueryString removes a leading question mark when present", () => {
|
|
28
|
+
expect(neatenQueryString("?name=mobius")).toBe("name=mobius")
|
|
29
|
+
expect(neatenQueryString("name=mobius")).toBe("name=mobius")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("queryStringToQueryObject decodes keys and values and preserves later equals signs", () => {
|
|
33
|
+
expect(queryStringToQueryObject("?na%20me=mobius%20model&formula=a%3Db&empty="))
|
|
34
|
+
.toEqual({ "na me": "mobius model", formula: "a=b", empty: "" })
|
|
35
|
+
expect(queryStringToQueryObject("a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
|
|
36
|
+
expect(queryStringToQueryObject("?a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("queryObjectToQueryString encodes keys and values into a query string", () => {
|
|
40
|
+
expect(queryObjectToQueryString({ "na me": "mobius model", formula: "a=b" }))
|
|
41
|
+
.toBe("na%20me=mobius%20model&formula=a%3Db")
|
|
42
|
+
expect(queryObjectToQueryString({ a: "1", b: "2", c: "3" })).toBe("a=1&b=2&c=3")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("searchToQueryString aliases query-string normalization for search input", () => {
|
|
46
|
+
expect(searchToQueryString("?a=1&b=2&c=3")).toBe("a=1&b=2&c=3")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("searchToQueryObject parses search input into a query object", () => {
|
|
50
|
+
expect(searchToQueryObject("?name=mobius&mode=stable")).toEqual({ name: "mobius", mode: "stable" })
|
|
51
|
+
expect(searchToQueryObject("?a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("queryStringToSearch aliases search normalization for query-string input", () => {
|
|
55
|
+
expect(queryStringToSearch("a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("queryObjectToSearch serializes a query object into search format", () => {
|
|
59
|
+
expect(queryObjectToSearch({ name: "mobius model", mode: "a=b" })).toBe("?name=mobius%20model&mode=a%3Db")
|
|
60
|
+
expect(queryObjectToSearch({ a: "1", b: "2", c: "3" })).toBe("?a=1&b=2&c=3")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test("toSearch returns search format for both strings and objects", () => {
|
|
64
|
+
expect(toSearch("a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
|
|
65
|
+
expect(toSearch("?a=1&b=2&c=3")).toBe("?a=1&b=2&c=3")
|
|
66
|
+
expect(toSearch({ a: "1", b: "2", c: "3" })).toBe("?a=1&b=2&c=3")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("toQueryString returns query-string format for both strings and objects", () => {
|
|
70
|
+
expect(toQueryString("a=1&b=2&c=3")).toBe("a=1&b=2&c=3")
|
|
71
|
+
expect(toQueryString("?a=1&b=2&c=3")).toBe("a=1&b=2&c=3")
|
|
72
|
+
expect(toQueryString({ a: "1", b: "2", c: "3" })).toBe("a=1&b=2&c=3")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("queryObject returns parsed query objects for both strings and objects", () => {
|
|
76
|
+
const queryObject = { a: "1", b: "2", c: "3" }
|
|
77
|
+
|
|
78
|
+
expect(toQueryObject("a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
|
|
79
|
+
expect(toQueryObject("?a=1&b=2&c=3")).toEqual({ a: "1", b: "2", c: "3" })
|
|
80
|
+
expect(toQueryObject(queryObject)).toBe(queryObject)
|
|
81
|
+
expect(toQueryObject("")).toEqual({})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("isSearchLooseIncludes checks whether all requested query keys exist", () => {
|
|
85
|
+
expect(isSearchLooseIncludes("?a=1&b=2", ["a"])).toBe(true)
|
|
86
|
+
expect(isSearchLooseIncludes("?a=1&b=2", ["a", "c"])).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("isSearchStrictIncludes checks whether query key sets are identical", () => {
|
|
90
|
+
expect(isSearchStrictIncludes("?a=1&b=2", ["b", "a"])).toBe(true)
|
|
91
|
+
expect(isSearchStrictIncludes("?ab=1&c=2", ["a", "bc"])).toBe(false)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("isSearchIncludes supports required and optional query keys", () => {
|
|
95
|
+
expect(isSearchIncludes("?a=1&b=2", { a: "required", b: "optional" })).toBe(true)
|
|
96
|
+
expect(isSearchIncludes("?a=1&b=2", { a: "required" })).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("isSearchLooseEqual checks whether target query values are a matching subset", () => {
|
|
100
|
+
expect(isSearchLooseEqual("?a=1&b=2", "?a=1")).toBe(true)
|
|
101
|
+
expect(isSearchLooseEqual("?a=1&b=2", { a: "1", c: "3" })).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test("isSearchStrictEqual checks whether query key sets and values are identical", () => {
|
|
105
|
+
expect(isSearchStrictEqual("?a=1&b=2", "?b=2&a=1")).toBe(true)
|
|
106
|
+
expect(isSearchStrictEqual("?a=1&b=2", "?a=1")).toBe(false)
|
|
107
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { Client } from "#Source/socket/index.ts"
|
|
4
|
+
|
|
5
|
+
type FakeEventMap = {
|
|
6
|
+
close: { code: number; reason: string }
|
|
7
|
+
error: Event
|
|
8
|
+
message: { data: string }
|
|
9
|
+
open: Event
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class FakeWebSocket {
|
|
13
|
+
static readonly CONNECTING = 0
|
|
14
|
+
static readonly OPEN = 1
|
|
15
|
+
static readonly CLOSING = 2
|
|
16
|
+
static readonly CLOSED = 3
|
|
17
|
+
static instances: FakeWebSocket[] = []
|
|
18
|
+
|
|
19
|
+
readonly url: string
|
|
20
|
+
readonly sentMessages: string[]
|
|
21
|
+
readyState: number
|
|
22
|
+
|
|
23
|
+
private readonly listeners: {
|
|
24
|
+
[K in keyof FakeEventMap]: Set<(event: FakeEventMap[K]) => void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor(url: string) {
|
|
28
|
+
this.url = url
|
|
29
|
+
this.sentMessages = []
|
|
30
|
+
this.readyState = FakeWebSocket.CONNECTING
|
|
31
|
+
this.listeners = {
|
|
32
|
+
close: new Set(),
|
|
33
|
+
error: new Set(),
|
|
34
|
+
message: new Set(),
|
|
35
|
+
open: new Set(),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
FakeWebSocket.instances.push(this)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addEventListener<K extends keyof FakeEventMap>(type: K, listener: (event: FakeEventMap[K]) => void): void {
|
|
42
|
+
this.listeners[type].add(listener)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
removeEventListener<K extends keyof FakeEventMap>(type: K, listener: (event: FakeEventMap[K]) => void): void {
|
|
46
|
+
this.listeners[type].delete(listener)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
send(message: string): void {
|
|
50
|
+
if (this.readyState !== FakeWebSocket.OPEN) {
|
|
51
|
+
throw new Error("FakeWebSocket is not open.")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.sentMessages.push(message)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
close(): void {
|
|
58
|
+
if (this.readyState === FakeWebSocket.CLOSED) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.readyState = FakeWebSocket.CLOSED
|
|
63
|
+
this.emit("close", { code: 1_000, reason: "" })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
simulateOpen(): void {
|
|
67
|
+
this.readyState = FakeWebSocket.OPEN
|
|
68
|
+
this.emit("open", new Event("open"))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
simulateMessage(message: unknown): void {
|
|
72
|
+
this.emit("message", { data: JSON.stringify(message) })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
simulateError(): void {
|
|
76
|
+
this.emit("error", new Event("error"))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private emit<K extends keyof FakeEventMap>(type: K, event: FakeEventMap[K]): void {
|
|
80
|
+
for (const listener of this.listeners[type]) {
|
|
81
|
+
listener(event)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
FakeWebSocket.instances = []
|
|
88
|
+
vi.unstubAllGlobals()
|
|
89
|
+
vi.restoreAllMocks()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("Client.SocketUnit queues work until ready, deduplicates status events, and keeps runtime listeners after error", async () => {
|
|
93
|
+
vi.stubGlobal("WebSocket", FakeWebSocket)
|
|
94
|
+
|
|
95
|
+
const socketUnit = new Client.SocketUnit<{ type: string }>({
|
|
96
|
+
url: "ws://socket.test",
|
|
97
|
+
})
|
|
98
|
+
const statuses: string[] = []
|
|
99
|
+
const messages: Array<{ type: string }> = []
|
|
100
|
+
|
|
101
|
+
socketUnit.eventManager.subscribe("status", (status) => {
|
|
102
|
+
statuses.push(status)
|
|
103
|
+
})
|
|
104
|
+
socketUnit.eventManager.subscribe("message", (message) => {
|
|
105
|
+
messages.push(message)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
socketUnit.setClientId("client-1")
|
|
109
|
+
socketUnit.sendMessage({ type: "queued-before-open" })
|
|
110
|
+
|
|
111
|
+
expect(socketUnit.getSnapshot().pendingActionCount).toBe(1)
|
|
112
|
+
|
|
113
|
+
const openPromise = socketUnit.open()
|
|
114
|
+
const firstWebSocket = FakeWebSocket.instances[0]!
|
|
115
|
+
|
|
116
|
+
expect(socketUnit.getStatus()).toBe("CONNECTING")
|
|
117
|
+
|
|
118
|
+
firstWebSocket.simulateOpen()
|
|
119
|
+
await openPromise
|
|
120
|
+
|
|
121
|
+
expect(firstWebSocket.sentMessages).toEqual([
|
|
122
|
+
JSON.stringify({ type: "queued-before-open" }),
|
|
123
|
+
])
|
|
124
|
+
expect(statuses).toEqual(["CONNECTING", "OPEN"])
|
|
125
|
+
|
|
126
|
+
firstWebSocket.simulateMessage({ type: "message-1" })
|
|
127
|
+
firstWebSocket.simulateMessage({ type: "message-2" })
|
|
128
|
+
firstWebSocket.simulateError()
|
|
129
|
+
firstWebSocket.simulateMessage({ type: "message-3" })
|
|
130
|
+
|
|
131
|
+
expect(messages).toEqual([
|
|
132
|
+
{ type: "message-1" },
|
|
133
|
+
{ type: "message-2" },
|
|
134
|
+
{ type: "message-3" },
|
|
135
|
+
])
|
|
136
|
+
expect(statuses).toEqual(["CONNECTING", "OPEN"])
|
|
137
|
+
|
|
138
|
+
await expect(socketUnit.safeOpen()).rejects.toThrow("Socket is already open")
|
|
139
|
+
|
|
140
|
+
await socketUnit.close()
|
|
141
|
+
expect(socketUnit.getStatus()).toBe("UNINSTANTIATED")
|
|
142
|
+
|
|
143
|
+
let resetCount = 0
|
|
144
|
+
await socketUnit.reset(() => {
|
|
145
|
+
resetCount = resetCount + 1
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(resetCount).toBe(1)
|
|
149
|
+
expect(socketUnit.getSnapshot()).toMatchObject({
|
|
150
|
+
clientId: undefined,
|
|
151
|
+
pendingActionCount: 0,
|
|
152
|
+
status: "UNINSTANTIATED",
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test("Client.Socket emits connect close and message events from stable ready-to-work transitions", async () => {
|
|
157
|
+
vi.stubGlobal("WebSocket", FakeWebSocket)
|
|
158
|
+
|
|
159
|
+
const socket = new Client.Socket<{ type: string }>({
|
|
160
|
+
url: "ws://socket.test",
|
|
161
|
+
})
|
|
162
|
+
const connectClientIds: Array<string | undefined> = []
|
|
163
|
+
const closeClientIds: Array<string | undefined> = []
|
|
164
|
+
const forwardedMessages: Array<{ clientId: string; message: { type: string } }> = []
|
|
165
|
+
|
|
166
|
+
socket.eventManager.subscribe("connect", ({ clientId }) => {
|
|
167
|
+
connectClientIds.push(clientId)
|
|
168
|
+
})
|
|
169
|
+
socket.eventManager.subscribe("close", ({ clientId }) => {
|
|
170
|
+
closeClientIds.push(clientId)
|
|
171
|
+
})
|
|
172
|
+
socket.eventManager.subscribe("message", (payload) => {
|
|
173
|
+
forwardedMessages.push(payload)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
socket.setClientId("client-2")
|
|
177
|
+
socket.sendMessage({ type: "queued-before-open" })
|
|
178
|
+
|
|
179
|
+
const openPromise = socket.open()
|
|
180
|
+
const firstWebSocket = FakeWebSocket.instances[0]!
|
|
181
|
+
firstWebSocket.simulateOpen()
|
|
182
|
+
await openPromise
|
|
183
|
+
|
|
184
|
+
expect(connectClientIds).toEqual(["client-2"])
|
|
185
|
+
expect(firstWebSocket.sentMessages).toEqual([
|
|
186
|
+
JSON.stringify({ type: "queued-before-open" }),
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
firstWebSocket.simulateMessage({ type: "hello" })
|
|
190
|
+
firstWebSocket.simulateMessage({ type: "hello-again" })
|
|
191
|
+
|
|
192
|
+
expect(forwardedMessages).toEqual([
|
|
193
|
+
{ clientId: "client-2", message: { type: "hello" } },
|
|
194
|
+
{ clientId: "client-2", message: { type: "hello-again" } },
|
|
195
|
+
])
|
|
196
|
+
expect(connectClientIds).toEqual(["client-2"])
|
|
197
|
+
|
|
198
|
+
await socket.close()
|
|
199
|
+
expect(closeClientIds).toEqual(["client-2"])
|
|
200
|
+
|
|
201
|
+
socket.reset()
|
|
202
|
+
|
|
203
|
+
expect(socket.getSnapshot()).toMatchObject({
|
|
204
|
+
clientId: "client-2",
|
|
205
|
+
isReadyToWork: false,
|
|
206
|
+
status: "UNINSTANTIATED",
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { AddressInfo } from "node:net"
|
|
2
|
+
|
|
3
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
4
|
+
import { WebSocket as WsClient } from "ws"
|
|
5
|
+
|
|
6
|
+
import { Server } from "#Source/socket/index.ts"
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test("Server.createWebSocketServer returns usable local URLs and stable lifecycle helpers", async () => {
|
|
13
|
+
const webSocketServer = Server.createWebSocketServer({ port: 0 })
|
|
14
|
+
|
|
15
|
+
expect(webSocketServer.isRunning()).toBe(false)
|
|
16
|
+
|
|
17
|
+
const runningResult = await webSocketServer.run()
|
|
18
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
19
|
+
const address = webSocketServer.safeGetServer().address() as AddressInfo
|
|
20
|
+
|
|
21
|
+
expect(runningResult.server).toBe(webSocketServer.safeGetServer())
|
|
22
|
+
expect(runningResult.urlList).toContain(`ws://127.0.0.1:${address.port}`)
|
|
23
|
+
expect(runningResult.urlList).toContain(`ws://localhost:${address.port}`)
|
|
24
|
+
expect(runningResult.urlList).toContain(`ws://[::1]:${address.port}`)
|
|
25
|
+
expect(webSocketServer.isRunning()).toBe(true)
|
|
26
|
+
|
|
27
|
+
await webSocketServer.close()
|
|
28
|
+
|
|
29
|
+
expect(webSocketServer.isRunning()).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("Server.Socket attach does not take over an already running server lifecycle", async () => {
|
|
33
|
+
const webSocketServer = Server.createWebSocketServer({ port: 0 })
|
|
34
|
+
await webSocketServer.run()
|
|
35
|
+
|
|
36
|
+
const socket = new Server.Socket<{ type: string }>({
|
|
37
|
+
webSocketServer,
|
|
38
|
+
onMessage: () => {
|
|
39
|
+
// no op
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
socket.attach()
|
|
44
|
+
|
|
45
|
+
expect(socket.getSnapshot()).toMatchObject({
|
|
46
|
+
isRunning: true,
|
|
47
|
+
isStarted: true,
|
|
48
|
+
managesWebSocketServerLifecycle: false,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await socket.close()
|
|
52
|
+
|
|
53
|
+
expect(webSocketServer.isRunning()).toBe(true)
|
|
54
|
+
|
|
55
|
+
await webSocketServer.close()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("Server.Socket exposes stable connect timing and forwards messages for active connections", async () => {
|
|
59
|
+
const webSocketServer = Server.createWebSocketServer({ port: 0 })
|
|
60
|
+
const socket = new Server.Socket<{ type: string }>({
|
|
61
|
+
webSocketServer,
|
|
62
|
+
onMessage: () => {
|
|
63
|
+
// no-op
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const connectPromise = new Promise<string>((resolve) => {
|
|
68
|
+
socket.eventManager.subscribe("connect", ({ clientId }) => {
|
|
69
|
+
const socketUnitSnapshot = socket.getSocketUnitSnapshot(clientId)
|
|
70
|
+
|
|
71
|
+
expect(socketUnitSnapshot).toMatchObject({
|
|
72
|
+
clientId,
|
|
73
|
+
isReadyToWork: true,
|
|
74
|
+
status: "OPEN",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
resolve(clientId)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
const inboundMessagePromise = new Promise<{ clientId: string; message: { type: string } }>((resolve) => {
|
|
81
|
+
socket.eventManager.subscribe("message", (payload) => {
|
|
82
|
+
resolve(payload)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
const closePromise = new Promise<string>((resolve) => {
|
|
86
|
+
socket.eventManager.subscribe("close", ({ clientId }) => {
|
|
87
|
+
resolve(clientId)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
await socket.open()
|
|
92
|
+
|
|
93
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
94
|
+
const address = webSocketServer.safeGetServer().address() as AddressInfo
|
|
95
|
+
const client = new WsClient(`ws://127.0.0.1:${address.port}`)
|
|
96
|
+
|
|
97
|
+
await new Promise<void>((resolve, reject) => {
|
|
98
|
+
client.once("open", () => {
|
|
99
|
+
resolve()
|
|
100
|
+
})
|
|
101
|
+
client.once("error", reject)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const clientId = await connectPromise
|
|
105
|
+
|
|
106
|
+
client.send(JSON.stringify({ type: "hello-server" }))
|
|
107
|
+
|
|
108
|
+
await expect(inboundMessagePromise).resolves.toEqual({
|
|
109
|
+
clientId,
|
|
110
|
+
message: { type: "hello-server" },
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const outboundMessagePromise = new Promise<string>((resolve, reject) => {
|
|
114
|
+
client.once("message", (message) => {
|
|
115
|
+
resolve(message.toString())
|
|
116
|
+
})
|
|
117
|
+
client.once("error", reject)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
socket.sendMessage(clientId, { type: "hello-client" })
|
|
121
|
+
|
|
122
|
+
await expect(outboundMessagePromise).resolves.toBe(JSON.stringify({ type: "hello-client" }))
|
|
123
|
+
|
|
124
|
+
client.close()
|
|
125
|
+
|
|
126
|
+
await expect(closePromise).resolves.toBe(clientId)
|
|
127
|
+
|
|
128
|
+
await socket.close()
|
|
129
|
+
|
|
130
|
+
expect(socket.getSnapshot()).toMatchObject({
|
|
131
|
+
activeSocketUnitCount: 0,
|
|
132
|
+
isRunning: false,
|
|
133
|
+
isStarted: false,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SocketUnitHeartbeat,
|
|
5
|
+
resolveSocketUnitHeartbeatOptions,
|
|
6
|
+
validateSocketUnitHeartbeatOptions,
|
|
7
|
+
} from "#Source/socket/index.ts"
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.clearAllTimers()
|
|
11
|
+
vi.useRealTimers()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test("SocketUnitHeartbeat validates required handlers and resolves default values", () => {
|
|
15
|
+
expect(() => validateSocketUnitHeartbeatOptions({
|
|
16
|
+
enableActiveHeartbeat: true,
|
|
17
|
+
})).toThrow("Active heartbeat message handler is required")
|
|
18
|
+
|
|
19
|
+
expect(() => validateSocketUnitHeartbeatOptions({
|
|
20
|
+
enablePassiveHeartbeat: true,
|
|
21
|
+
})).toThrow("Passive heartbeat message handler is required")
|
|
22
|
+
|
|
23
|
+
expect(() => validateSocketUnitHeartbeatOptions({
|
|
24
|
+
heartbeatInterval: 0,
|
|
25
|
+
})).toThrow("Heartbeat interval must be a positive finite number.")
|
|
26
|
+
|
|
27
|
+
expect(() => validateSocketUnitHeartbeatOptions({
|
|
28
|
+
maxResponseTime: 0,
|
|
29
|
+
})).toThrow("Max response time must be a positive finite number.")
|
|
30
|
+
|
|
31
|
+
expect(() => validateSocketUnitHeartbeatOptions({
|
|
32
|
+
maxHeartbeatLostCount: 0,
|
|
33
|
+
})).toThrow("Max heartbeat lost count must be at least 1.")
|
|
34
|
+
|
|
35
|
+
const resolvedOptions = resolveSocketUnitHeartbeatOptions({})
|
|
36
|
+
|
|
37
|
+
expect(resolvedOptions).toMatchObject({
|
|
38
|
+
enableActiveHeartbeat: false,
|
|
39
|
+
enablePassiveHeartbeat: false,
|
|
40
|
+
heartbeatInterval: 30_000,
|
|
41
|
+
maxResponseTime: 10_000,
|
|
42
|
+
maxHeartbeatLostCount: 3,
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("SocketUnitHeartbeat unifies active and passive handling with newest-first matching", async () => {
|
|
47
|
+
vi.useFakeTimers()
|
|
48
|
+
|
|
49
|
+
const sentMessages: Array<{ action: string; messageString: string }> = []
|
|
50
|
+
const close = vi.fn(() => {
|
|
51
|
+
throw new Error("Heartbeat close should not be called in this test.")
|
|
52
|
+
})
|
|
53
|
+
const heartbeat = new SocketUnitHeartbeat<{ kind: string; round?: number }>({
|
|
54
|
+
enableActiveHeartbeat: true,
|
|
55
|
+
enablePassiveHeartbeat: true,
|
|
56
|
+
heartbeatInterval: 100,
|
|
57
|
+
maxResponseTime: 500,
|
|
58
|
+
maxHeartbeatLostCount: 2,
|
|
59
|
+
activeHeartbeatMessageHandlerGenerator: () => {
|
|
60
|
+
return {
|
|
61
|
+
buildPingMessage: () => {
|
|
62
|
+
return { kind: "ping" }
|
|
63
|
+
},
|
|
64
|
+
verifyPongMessage: (_clientId, message) => {
|
|
65
|
+
return message.kind === "pong"
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
passiveHeartbeatMessageHandlerGenerator: () => {
|
|
70
|
+
return {
|
|
71
|
+
verifyPingMessage: (_clientId, message) => {
|
|
72
|
+
return message.kind === "ping-from-peer"
|
|
73
|
+
},
|
|
74
|
+
buildPongMessage: () => {
|
|
75
|
+
return { kind: "pong-from-peer" }
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}, {
|
|
80
|
+
sendMessage: (message) => {
|
|
81
|
+
sentMessages.push(message)
|
|
82
|
+
return true
|
|
83
|
+
},
|
|
84
|
+
close,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(heartbeat.getSnapshot()).toMatchObject({
|
|
88
|
+
enabledActiveHeartbeat: true,
|
|
89
|
+
enabledPassiveHeartbeat: true,
|
|
90
|
+
failCount: 0,
|
|
91
|
+
isRunning: false,
|
|
92
|
+
recentStates: [],
|
|
93
|
+
successCount: 0,
|
|
94
|
+
waitingCount: 0,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
heartbeat.start("client-1")
|
|
98
|
+
|
|
99
|
+
expect(heartbeat.getSnapshot().isRunning).toBe(true)
|
|
100
|
+
|
|
101
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
102
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
103
|
+
|
|
104
|
+
const waitingSnapshot = heartbeat.getSnapshot()
|
|
105
|
+
|
|
106
|
+
expect(sentMessages).toHaveLength(2)
|
|
107
|
+
expect(waitingSnapshot.waitingCount).toBe(2)
|
|
108
|
+
expect(waitingSnapshot.recentStates).toHaveLength(2)
|
|
109
|
+
expect(waitingSnapshot.recentStates.every((state) => {
|
|
110
|
+
return state.status === "waiting" && state.hasPendingResponseTimer === true
|
|
111
|
+
})).toBe(true)
|
|
112
|
+
|
|
113
|
+
const activeHandled = heartbeat.handleMessage("client-1", { kind: "pong" })
|
|
114
|
+
|
|
115
|
+
expect(activeHandled).toBe(true)
|
|
116
|
+
expect(heartbeat.getSnapshot()).toMatchObject({
|
|
117
|
+
failCount: 0,
|
|
118
|
+
recentStates: [],
|
|
119
|
+
successCount: 0,
|
|
120
|
+
waitingCount: 0,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const passiveHandled = heartbeat.handleMessage("client-1", { kind: "ping-from-peer" })
|
|
124
|
+
|
|
125
|
+
expect(passiveHandled).toBe(true)
|
|
126
|
+
expect(sentMessages.at(-1)).toEqual({
|
|
127
|
+
action: "Passive heartbeat pong send",
|
|
128
|
+
messageString: JSON.stringify({ kind: "pong-from-peer" }),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
heartbeat.stop()
|
|
132
|
+
|
|
133
|
+
expect(close).not.toHaveBeenCalled()
|
|
134
|
+
expect(heartbeat.getSnapshot()).toMatchObject({
|
|
135
|
+
isRunning: false,
|
|
136
|
+
recentStates: [],
|
|
137
|
+
waitingCount: 0,
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test("SocketUnitHeartbeat closes transport after consecutive timeout limit is reached", async () => {
|
|
142
|
+
vi.useFakeTimers()
|
|
143
|
+
|
|
144
|
+
let closeCount = 0
|
|
145
|
+
const heartbeat = new SocketUnitHeartbeat<{ kind: string }>({
|
|
146
|
+
enableActiveHeartbeat: true,
|
|
147
|
+
heartbeatInterval: 100,
|
|
148
|
+
maxResponseTime: 10,
|
|
149
|
+
maxHeartbeatLostCount: 2,
|
|
150
|
+
activeHeartbeatMessageHandlerGenerator: () => {
|
|
151
|
+
return {
|
|
152
|
+
buildPingMessage: () => {
|
|
153
|
+
return { kind: "ping" }
|
|
154
|
+
},
|
|
155
|
+
verifyPongMessage: () => {
|
|
156
|
+
return false
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}, {
|
|
161
|
+
sendMessage: () => {
|
|
162
|
+
return true
|
|
163
|
+
},
|
|
164
|
+
close: () => {
|
|
165
|
+
closeCount = closeCount + 1
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
heartbeat.start("client-2")
|
|
170
|
+
|
|
171
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
172
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
173
|
+
expect(heartbeat.getSnapshot().failCount).toBe(1)
|
|
174
|
+
expect(closeCount).toBe(0)
|
|
175
|
+
|
|
176
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
177
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
178
|
+
expect(heartbeat.getSnapshot().failCount).toBeGreaterThanOrEqual(1)
|
|
179
|
+
expect(closeCount).toBe(1)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("SocketUnitHeartbeat stays idle when active heartbeat is disabled and ignores unmatched messages", () => {
|
|
183
|
+
const heartbeat = new SocketUnitHeartbeat<{ kind: string }>({
|
|
184
|
+
enablePassiveHeartbeat: true,
|
|
185
|
+
passiveHeartbeatMessageHandlerGenerator: () => {
|
|
186
|
+
return {
|
|
187
|
+
verifyPingMessage: (_clientId, message) => {
|
|
188
|
+
return message.kind === "ping"
|
|
189
|
+
},
|
|
190
|
+
buildPongMessage: () => {
|
|
191
|
+
return { kind: "pong" }
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
}, {
|
|
196
|
+
sendMessage: () => {
|
|
197
|
+
return true
|
|
198
|
+
},
|
|
199
|
+
close: () => {
|
|
200
|
+
throw new Error("Heartbeat close should not be called in this test.")
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
heartbeat.start("client-3")
|
|
205
|
+
|
|
206
|
+
expect(heartbeat.getSnapshot()).toMatchObject({
|
|
207
|
+
isRunning: false,
|
|
208
|
+
recentStates: [],
|
|
209
|
+
waitingCount: 0,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(heartbeat.handleMessage("client-3", { kind: "unmatched" })).toBe(false)
|
|
213
|
+
expect(heartbeat.handleMessage("client-3", { kind: "ping" })).toBe(true)
|
|
214
|
+
})
|