@planet-matrix/mobius-model 0.5.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 +61 -0
- package/README.md +123 -36
- package/dist/index.js +715 -4
- package/dist/index.js.map +981 -13
- package/oxlint.config.ts +6 -0
- package/package.json +36 -18
- package/src/abort/README.md +92 -0
- package/src/abort/abort-manager.ts +278 -0
- package/src/abort/abort-signal-listener-manager.ts +81 -0
- package/src/abort/index.ts +2 -0
- 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 +72 -116
- package/src/basic/error.ts +19 -5
- package/src/basic/function.ts +83 -64
- package/src/basic/index.ts +1 -0
- package/src/basic/is.ts +152 -71
- package/src/basic/promise.ts +29 -8
- package/src/basic/schedule.ts +111 -0
- package/src/basic/stream.ts +135 -25
- package/src/basic/string.ts +2 -33
- package/src/color/README.md +105 -0
- package/src/color/index.ts +3 -0
- package/src/color/internal.ts +42 -0
- package/src/color/rgb/analyze.ts +236 -0
- package/src/color/rgb/construct.ts +130 -0
- package/src/color/rgb/convert.ts +227 -0
- package/src/color/rgb/derive.ts +303 -0
- package/src/color/rgb/index.ts +6 -0
- package/src/color/rgb/internal.ts +208 -0
- package/src/color/rgb/parse.ts +302 -0
- package/src/color/rgb/serialize.ts +144 -0
- package/src/color/types.ts +57 -0
- package/src/color/xyz/analyze.ts +80 -0
- package/src/color/xyz/construct.ts +19 -0
- package/src/color/xyz/convert.ts +71 -0
- package/src/color/xyz/index.ts +3 -0
- package/src/color/xyz/internal.ts +23 -0
- 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/css/README.md +93 -0
- package/src/css/class.ts +559 -0
- package/src/css/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/encoding/README.md +66 -79
- package/src/encoding/base64.ts +13 -4
- package/src/environment/README.md +97 -0
- package/src/environment/basic.ts +26 -0
- package/src/environment/device.ts +311 -0
- package/src/environment/feature.ts +285 -0
- package/src/environment/geo.ts +337 -0
- package/src/environment/index.ts +7 -0
- package/src/environment/runtime.ts +400 -0
- package/src/environment/snapshot.ts +60 -0
- package/src/environment/variable.ts +239 -0
- package/src/event/README.md +90 -0
- package/src/event/class-event-proxy.ts +229 -0
- package/src/event/common.ts +29 -0
- package/src/event/event-manager.ts +203 -0
- package/src/event/index.ts +4 -0
- package/src/event/instance-event-proxy.ts +187 -0
- package/src/event/internal.ts +24 -0
- package/src/exception/README.md +96 -0
- package/src/exception/browser.ts +219 -0
- package/src/exception/index.ts +4 -0
- package/src/exception/nodejs.ts +169 -0
- package/src/exception/normalize.ts +106 -0
- package/src/exception/types.ts +99 -0
- 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/identifier/README.md +92 -0
- package/src/identifier/id.ts +119 -0
- package/src/identifier/index.ts +2 -0
- package/src/identifier/uuid.ts +187 -0
- package/src/index.ts +33 -1
- package/src/json/README.md +92 -0
- package/src/json/index.ts +1 -0
- package/src/json/repair.ts +18 -0
- package/src/log/README.md +79 -0
- package/src/log/index.ts +5 -0
- package/src/log/log-emitter.ts +72 -0
- package/src/log/log-record.ts +10 -0
- package/src/log/log-scheduler.ts +74 -0
- package/src/log/log-type.ts +8 -0
- package/src/log/logger.ts +554 -0
- 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 +91 -0
- package/src/orchestration/coordination/barrier.ts +214 -0
- package/src/orchestration/coordination/count-down-latch.ts +215 -0
- package/src/orchestration/coordination/errors.ts +98 -0
- package/src/orchestration/coordination/index.ts +16 -0
- package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
- package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
- package/src/orchestration/coordination/keyed-lock.ts +168 -0
- package/src/orchestration/coordination/mutex.ts +257 -0
- package/src/orchestration/coordination/permit.ts +127 -0
- package/src/orchestration/coordination/read-write-lock.ts +444 -0
- package/src/orchestration/coordination/semaphore.ts +280 -0
- 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 +3 -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 +56 -86
- 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/reactor/README.md +4 -0
- package/src/reactor/reactor-core/primitive.ts +9 -9
- package/src/reactor/reactor-core/reactive-system.ts +5 -5
- 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/singleton/README.md +79 -0
- package/src/singleton/factory.ts +55 -0
- package/src/singleton/index.ts +2 -0
- package/src/singleton/manager.ts +204 -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/README.md +107 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/table.ts +449 -0
- package/src/timer/README.md +86 -0
- package/src/timer/expiration/expiration-manager.ts +594 -0
- package/src/timer/expiration/index.ts +3 -0
- package/src/timer/expiration/min-heap.ts +208 -0
- package/src/timer/expiration/remaining-manager.ts +241 -0
- package/src/timer/index.ts +1 -0
- 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/type/README.md +54 -307
- package/src/type/class.ts +2 -2
- package/src/type/index.ts +14 -14
- package/src/type/is.ts +265 -2
- package/src/type/object.ts +37 -0
- package/src/type/string.ts +7 -2
- package/src/type/tuple.ts +6 -6
- package/src/type/union.ts +16 -0
- package/src/web/README.md +77 -0
- package/src/web/capture.ts +35 -0
- package/src/web/clipboard.ts +97 -0
- package/src/web/dom.ts +117 -0
- package/src/web/download.ts +16 -0
- package/src/web/event.ts +46 -0
- package/src/web/index.ts +10 -0
- package/src/web/local-storage.ts +113 -0
- package/src/web/location.ts +28 -0
- package/src/web/permission.ts +172 -0
- package/src/web/script-loader.ts +432 -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/abort/abort-manager.spec.ts +225 -0
- package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -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/array.spec.ts +1 -1
- 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 +91 -38
- package/tests/unit/basic/string.spec.ts +0 -9
- package/tests/unit/color/rgb/analyze.spec.ts +110 -0
- package/tests/unit/color/rgb/construct.spec.ts +56 -0
- package/tests/unit/color/rgb/convert.spec.ts +60 -0
- package/tests/unit/color/rgb/derive.spec.ts +103 -0
- package/tests/unit/color/rgb/parse.spec.ts +66 -0
- package/tests/unit/color/rgb/serialize.spec.ts +46 -0
- package/tests/unit/color/xyz/analyze.spec.ts +33 -0
- package/tests/unit/color/xyz/construct.spec.ts +10 -0
- package/tests/unit/color/xyz/convert.spec.ts +18 -0
- 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/css/class.spec.ts +157 -0
- package/tests/unit/environment/basic.spec.ts +20 -0
- package/tests/unit/environment/device.spec.ts +146 -0
- package/tests/unit/environment/feature.spec.ts +388 -0
- package/tests/unit/environment/geo.spec.ts +111 -0
- package/tests/unit/environment/runtime.spec.ts +364 -0
- package/tests/unit/environment/snapshot.spec.ts +4 -0
- package/tests/unit/environment/variable.spec.ts +190 -0
- package/tests/unit/event/class-event-proxy.spec.ts +225 -0
- package/tests/unit/event/event-manager.spec.ts +246 -0
- package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
- package/tests/unit/exception/browser.spec.ts +213 -0
- package/tests/unit/exception/nodejs.spec.ts +144 -0
- package/tests/unit/exception/normalize.spec.ts +57 -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 +191 -0
- package/tests/unit/identifier/id.spec.ts +71 -0
- package/tests/unit/identifier/uuid.spec.ts +85 -0
- package/tests/unit/json/repair.spec.ts +11 -0
- package/tests/unit/log/log-emitter.spec.ts +33 -0
- package/tests/unit/log/log-scheduler.spec.ts +40 -0
- package/tests/unit/log/log-type.spec.ts +7 -0
- package/tests/unit/log/logger.spec.ts +237 -0
- package/tests/unit/openai/openai.spec.ts +64 -0
- package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
- package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
- package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
- package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
- package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
- package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
- package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
- package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -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/reactor/alien-signals-effect.spec.ts +11 -10
- package/tests/unit/reactor/preact-signal.spec.ts +1 -2
- 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/singleton/singleton.spec.ts +49 -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/storage/table.spec.ts +620 -0
- package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
- package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
- package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
- package/tests/unit/tube/helper.spec.ts +139 -0
- package/tests/unit/tube/tube.spec.ts +501 -0
- package/.oxlintrc.json +0 -5
- package/src/random/uuid.ts +0 -103
- package/tests/unit/random/uuid.spec.ts +0 -37
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { streamConsumeInMacroTask } from "#Source/basic/index.ts"
|
|
4
|
+
import { eitherToTuple } from "#Source/result/index.ts"
|
|
5
|
+
import { tubeToReadableStream } from "#Source/tube/index.ts"
|
|
6
|
+
import { Openai } from "#Source/openai/index.ts"
|
|
7
|
+
|
|
8
|
+
const testApiKey = "sk-DLj8aHvKjvHfZ7IHA2F07c968c3d4aE491C1525e40E24932"
|
|
9
|
+
const testBaseUrl = "https://cn.api.openai-next.com/v1/"
|
|
10
|
+
const openai = new Openai({
|
|
11
|
+
apiKey: testApiKey,
|
|
12
|
+
baseUrl: testBaseUrl,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("customEmbedding ", { timeout: 60_000 }, async () => {
|
|
16
|
+
const embedding = await openai.customEmbedding({
|
|
17
|
+
input: "床前明月光,疑是地上霜。举头望明月,低头思故乡。",
|
|
18
|
+
})
|
|
19
|
+
const [left, right] = eitherToTuple(embedding)
|
|
20
|
+
if (left !== undefined) {
|
|
21
|
+
throw new Error(`请求失败: ${JSON.stringify(left)}`)
|
|
22
|
+
}
|
|
23
|
+
const resultLength = right.embedding.length
|
|
24
|
+
expect(resultLength).toBe(1_536)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("customChatCompletion ", { timeout: 60_000 }, async () => {
|
|
28
|
+
const nonStreamingResult = await openai.customChatCompletionNonStreaming({
|
|
29
|
+
model: "gpt-4o-mini",
|
|
30
|
+
messages: [
|
|
31
|
+
{ role: "system", content: "你是讲故事大王。" },
|
|
32
|
+
{ role: "user", content: "讲一个小红帽吃掉大灰狼的故事。" },
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
const [left, right] = eitherToTuple(nonStreamingResult)
|
|
36
|
+
if (left !== undefined) {
|
|
37
|
+
throw new Error(`请求失败: ${JSON.stringify(left)}`)
|
|
38
|
+
}
|
|
39
|
+
const contentLength = right.content.length
|
|
40
|
+
expect(contentLength).toBeGreaterThan(0)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("customChatCompletionStreaming ", { timeout: 60_000 }, async () => {
|
|
44
|
+
const [left, right] = eitherToTuple(await openai.customChatCompletionStreaming({
|
|
45
|
+
model: "gpt-4o-mini",
|
|
46
|
+
messages: [
|
|
47
|
+
{ role: "system", content: "你是讲故事大王。" },
|
|
48
|
+
{ role: "user", content: "讲一个小红帽吃掉大灰狼的故事。" },
|
|
49
|
+
],
|
|
50
|
+
}))
|
|
51
|
+
if (left !== undefined) {
|
|
52
|
+
throw new Error(`请求失败: ${JSON.stringify(left)}`)
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const stream = tubeToReadableStream(right.completionTube)
|
|
56
|
+
streamConsumeInMacroTask({
|
|
57
|
+
readableStream: stream,
|
|
58
|
+
onValue: (chunk) => {
|
|
59
|
+
console.log(JSON.stringify(chunk.content.total, null, 2))
|
|
60
|
+
// process.stdout.write()
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { Barrier, BrokenBarrierError, CoordinationAbortError, CoordinationTimeoutError } from "#Source/orchestration/index.ts"
|
|
4
|
+
|
|
5
|
+
const getRejectedReason = async (promise: Promise<unknown>): Promise<unknown> => {
|
|
6
|
+
try {
|
|
7
|
+
await promise
|
|
8
|
+
}
|
|
9
|
+
catch (error: unknown) {
|
|
10
|
+
return error
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
throw new Error("Expected promise to reject.")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.clearAllTimers()
|
|
18
|
+
vi.useRealTimers()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("Barrier completes generations and can be reused", async () => {
|
|
22
|
+
const barrier = new Barrier(2)
|
|
23
|
+
const firstGenerationWait = barrier.signalAndWait()
|
|
24
|
+
|
|
25
|
+
expect(barrier.getParticipantCount()).toBe(2)
|
|
26
|
+
expect(barrier.getGeneration()).toBe(0)
|
|
27
|
+
expect(barrier.getPendingCount()).toBe(1)
|
|
28
|
+
expect(barrier.getRemainingCount()).toBe(1)
|
|
29
|
+
|
|
30
|
+
const secondArrivalGeneration = await barrier.signalAndWait()
|
|
31
|
+
const firstArrivalGeneration = await firstGenerationWait
|
|
32
|
+
|
|
33
|
+
expect(firstArrivalGeneration).toBe(0)
|
|
34
|
+
expect(secondArrivalGeneration).toBe(0)
|
|
35
|
+
expect(barrier.getGeneration()).toBe(1)
|
|
36
|
+
expect(barrier.getPendingCount()).toBe(0)
|
|
37
|
+
expect(barrier.getRemainingCount()).toBe(2)
|
|
38
|
+
|
|
39
|
+
const thirdArrivalGenerationPromise = barrier.signalAndWait()
|
|
40
|
+
const fourthArrivalGeneration = await barrier.signalAndWait()
|
|
41
|
+
|
|
42
|
+
expect(await thirdArrivalGenerationPromise).toBe(1)
|
|
43
|
+
expect(fourthArrivalGeneration).toBe(1)
|
|
44
|
+
expect(barrier.getGeneration()).toBe(2)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("Barrier timeout breaks the current generation for all waiting participants", async () => {
|
|
48
|
+
vi.useFakeTimers()
|
|
49
|
+
|
|
50
|
+
const barrier = new Barrier(3)
|
|
51
|
+
const firstWait = barrier.signalAndWait()
|
|
52
|
+
const secondWait = barrier.signalAndWait({ timeout: 25 })
|
|
53
|
+
|
|
54
|
+
expect(barrier.getPendingCount()).toBe(2)
|
|
55
|
+
expect(barrier.getRemainingCount()).toBe(1)
|
|
56
|
+
|
|
57
|
+
await vi.advanceTimersByTimeAsync(25)
|
|
58
|
+
|
|
59
|
+
const timeoutError = await getRejectedReason(secondWait)
|
|
60
|
+
const brokenError = await getRejectedReason(firstWait)
|
|
61
|
+
|
|
62
|
+
expect(timeoutError).toBeInstanceOf(CoordinationTimeoutError)
|
|
63
|
+
expect(brokenError).toBeInstanceOf(BrokenBarrierError)
|
|
64
|
+
|
|
65
|
+
if (!(brokenError instanceof BrokenBarrierError)) {
|
|
66
|
+
throw new Error("Expected brokenError to be an instance of BrokenBarrierError.")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(brokenError.reason).toBe(timeoutError)
|
|
70
|
+
expect(barrier.getGeneration()).toBe(1)
|
|
71
|
+
expect(barrier.getPendingCount()).toBe(0)
|
|
72
|
+
expect(barrier.getRemainingCount()).toBe(3)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("Barrier abort rejects the failing participant and breaks peers", async () => {
|
|
76
|
+
const barrier = new Barrier(3)
|
|
77
|
+
const abortController = new AbortController()
|
|
78
|
+
const firstWait = barrier.signalAndWait()
|
|
79
|
+
const secondWait = barrier.signalAndWait({ abortSignal: abortController.signal })
|
|
80
|
+
|
|
81
|
+
abortController.abort("cancelled")
|
|
82
|
+
|
|
83
|
+
const abortError = await getRejectedReason(secondWait)
|
|
84
|
+
const brokenError = await getRejectedReason(firstWait)
|
|
85
|
+
|
|
86
|
+
expect(abortError).toBeInstanceOf(CoordinationAbortError)
|
|
87
|
+
expect(brokenError).toBeInstanceOf(BrokenBarrierError)
|
|
88
|
+
|
|
89
|
+
if (!(brokenError instanceof BrokenBarrierError)) {
|
|
90
|
+
throw new Error("Expected brokenError to be an instance of BrokenBarrierError.")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
expect(brokenError.reason).toBe(abortError)
|
|
94
|
+
expect(barrier.getGeneration()).toBe(1)
|
|
95
|
+
expect(barrier.getPendingCount()).toBe(0)
|
|
96
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { CoordinationAbortError, CoordinationTimeoutError, CountDownLatch } from "#Source/orchestration/index.ts"
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.clearAllTimers()
|
|
7
|
+
vi.useRealTimers()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test("CountDownLatch opens when the count reaches zero and resolves waiters", async () => {
|
|
11
|
+
const countDownLatch = new CountDownLatch(2)
|
|
12
|
+
const firstWait = countDownLatch.wait()
|
|
13
|
+
const secondWait = countDownLatch.wait()
|
|
14
|
+
|
|
15
|
+
expect(countDownLatch.isOpen()).toBe(false)
|
|
16
|
+
expect(countDownLatch.tryWait()).toBe(false)
|
|
17
|
+
expect(countDownLatch.getRemainingCount()).toBe(2)
|
|
18
|
+
expect(countDownLatch.getPendingCount()).toBe(2)
|
|
19
|
+
|
|
20
|
+
expect(countDownLatch.countDown()).toBe(1)
|
|
21
|
+
expect(countDownLatch.arrive()).toBe(0)
|
|
22
|
+
|
|
23
|
+
await Promise.all([firstWait, secondWait])
|
|
24
|
+
|
|
25
|
+
expect(countDownLatch.isOpen()).toBe(true)
|
|
26
|
+
expect(countDownLatch.tryWait()).toBe(true)
|
|
27
|
+
expect(countDownLatch.getRemainingCount()).toBe(0)
|
|
28
|
+
expect(countDownLatch.getPendingCount()).toBe(0)
|
|
29
|
+
await expect(countDownLatch.wait()).resolves.toBeUndefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("CountDownLatch wait rejects on timeout and abort without leaking pending entries", async () => {
|
|
33
|
+
vi.useFakeTimers()
|
|
34
|
+
|
|
35
|
+
const countDownLatch = new CountDownLatch(1)
|
|
36
|
+
const timeoutPromise = countDownLatch.wait({ timeout: 30 })
|
|
37
|
+
|
|
38
|
+
expect(countDownLatch.getPendingCount()).toBe(1)
|
|
39
|
+
|
|
40
|
+
await vi.advanceTimersByTimeAsync(30)
|
|
41
|
+
|
|
42
|
+
await expect(timeoutPromise).rejects.toBeInstanceOf(CoordinationTimeoutError)
|
|
43
|
+
expect(countDownLatch.getPendingCount()).toBe(0)
|
|
44
|
+
|
|
45
|
+
const abortController = new AbortController()
|
|
46
|
+
const abortPromise = countDownLatch.wait({ abortSignal: abortController.signal })
|
|
47
|
+
|
|
48
|
+
expect(countDownLatch.getPendingCount()).toBe(1)
|
|
49
|
+
|
|
50
|
+
abortController.abort("cancelled")
|
|
51
|
+
|
|
52
|
+
await expect(abortPromise).rejects.toBeInstanceOf(CoordinationAbortError)
|
|
53
|
+
expect(countDownLatch.getPendingCount()).toBe(0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("CountDownLatch ignores further countdowns after opening", () => {
|
|
57
|
+
const countDownLatch = new CountDownLatch(1)
|
|
58
|
+
|
|
59
|
+
expect(countDownLatch.countDown()).toBe(0)
|
|
60
|
+
expect(countDownLatch.countDown()).toBe(0)
|
|
61
|
+
expect(countDownLatch.arrive()).toBe(0)
|
|
62
|
+
expect(countDownLatch.isOpen()).toBe(true)
|
|
63
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { BrokenBarrierError, CoordinationAbortError, CoordinationTimeoutError } from "#Source/orchestration/index.ts"
|
|
4
|
+
|
|
5
|
+
test("CoordinationAbortError stores operation, reason and formatted message", () => {
|
|
6
|
+
const error = new CoordinationAbortError("Mutex acquire", "cancelled")
|
|
7
|
+
|
|
8
|
+
expect(error.name).toBe("CoordinationAbortError")
|
|
9
|
+
expect(error.operation).toBe("Mutex acquire")
|
|
10
|
+
expect(error.reason).toBe("cancelled")
|
|
11
|
+
expect(error.message).toBe("Mutex acquire aborted. cancelled")
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test("CoordinationTimeoutError stores operation, timeout and formatted message", () => {
|
|
15
|
+
const error = new CoordinationTimeoutError("Semaphore acquire", 25)
|
|
16
|
+
|
|
17
|
+
expect(error.name).toBe("CoordinationTimeoutError")
|
|
18
|
+
expect(error.operation).toBe("Semaphore acquire")
|
|
19
|
+
expect(error.timeout).toBe(25)
|
|
20
|
+
expect(error.message).toBe("Semaphore acquire timeout after 25ms.")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("BrokenBarrierError stores reason and formatted message", () => {
|
|
24
|
+
const error = new BrokenBarrierError("peer aborted")
|
|
25
|
+
|
|
26
|
+
expect(error.name).toBe("BrokenBarrierError")
|
|
27
|
+
expect(error.reason).toBe("peer aborted")
|
|
28
|
+
expect(error.message).toBe("Barrier generation was broken by another participant. peer aborted")
|
|
29
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { CoordinationAbortError, KeyedLock } from "#Source/orchestration/index.ts"
|
|
4
|
+
|
|
5
|
+
test("KeyedLock isolates different keys and cleans state after release", () => {
|
|
6
|
+
const keyedLock = new KeyedLock<string>()
|
|
7
|
+
|
|
8
|
+
expect(keyedLock.getKeyCount()).toBe(0)
|
|
9
|
+
expect(keyedLock.hasKey("alpha")).toBe(false)
|
|
10
|
+
|
|
11
|
+
const alphaPermit = keyedLock.tryAcquire("alpha")
|
|
12
|
+
const betaPermit = keyedLock.tryAcquire("beta")
|
|
13
|
+
|
|
14
|
+
expect(alphaPermit?.details).toEqual({ coordination: "keyed-lock", key: "alpha" })
|
|
15
|
+
expect(betaPermit?.details).toEqual({ coordination: "keyed-lock", key: "beta" })
|
|
16
|
+
expect(keyedLock.tryAcquire("alpha")).toBeUndefined()
|
|
17
|
+
expect(keyedLock.hasKey("alpha")).toBe(true)
|
|
18
|
+
expect(keyedLock.hasKey("beta")).toBe(true)
|
|
19
|
+
expect(keyedLock.getKeyCount()).toBe(2)
|
|
20
|
+
|
|
21
|
+
alphaPermit?.release()
|
|
22
|
+
betaPermit?.release()
|
|
23
|
+
|
|
24
|
+
expect(keyedLock.hasKey("alpha")).toBe(false)
|
|
25
|
+
expect(keyedLock.hasKey("beta")).toBe(false)
|
|
26
|
+
expect(keyedLock.getKeyCount()).toBe(0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("KeyedLock acquire cleans aborted waiters and releases state when idle", async () => {
|
|
30
|
+
const keyedLock = new KeyedLock<string>()
|
|
31
|
+
const blockingPermit = keyedLock.tryAcquire("alpha")
|
|
32
|
+
const abortController = new AbortController()
|
|
33
|
+
|
|
34
|
+
expect(blockingPermit).toBeDefined()
|
|
35
|
+
|
|
36
|
+
const waitingPermitPromise = keyedLock.acquire("alpha", { abortSignal: abortController.signal })
|
|
37
|
+
|
|
38
|
+
expect(keyedLock.hasKey("alpha")).toBe(true)
|
|
39
|
+
expect(keyedLock.getKeyCount()).toBe(1)
|
|
40
|
+
|
|
41
|
+
abortController.abort("cancelled")
|
|
42
|
+
|
|
43
|
+
await expect(waitingPermitPromise).rejects.toBeInstanceOf(CoordinationAbortError)
|
|
44
|
+
|
|
45
|
+
expect(keyedLock.hasKey("alpha")).toBe(true)
|
|
46
|
+
|
|
47
|
+
blockingPermit?.release()
|
|
48
|
+
|
|
49
|
+
expect(keyedLock.hasKey("alpha")).toBe(false)
|
|
50
|
+
expect(keyedLock.getKeyCount()).toBe(0)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("KeyedLock runExclusive serializes callbacks for the same key", async () => {
|
|
54
|
+
const keyedLock = new KeyedLock<string>()
|
|
55
|
+
let activeCount = 0
|
|
56
|
+
let peakCount = 0
|
|
57
|
+
|
|
58
|
+
await Promise.all([
|
|
59
|
+
keyedLock.runExclusive("alpha", async () => {
|
|
60
|
+
activeCount = activeCount + 1
|
|
61
|
+
peakCount = Math.max(peakCount, activeCount)
|
|
62
|
+
await Promise.resolve()
|
|
63
|
+
activeCount = activeCount - 1
|
|
64
|
+
}),
|
|
65
|
+
keyedLock.runExclusive("alpha", async () => {
|
|
66
|
+
activeCount = activeCount + 1
|
|
67
|
+
peakCount = Math.max(peakCount, activeCount)
|
|
68
|
+
await Promise.resolve()
|
|
69
|
+
activeCount = activeCount - 1
|
|
70
|
+
}),
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
expect(peakCount).toBe(1)
|
|
74
|
+
expect(keyedLock.hasKey("alpha")).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("KeyedLock duplicate release stays silent by default and uses custom handler when provided", () => {
|
|
78
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
|
|
79
|
+
// no-op to silence warnings during test
|
|
80
|
+
})
|
|
81
|
+
const silentKeyedLock = new KeyedLock<string>()
|
|
82
|
+
const silentPermit = silentKeyedLock.tryAcquire("alpha")
|
|
83
|
+
|
|
84
|
+
expect(silentPermit).toBeDefined()
|
|
85
|
+
|
|
86
|
+
silentPermit?.release()
|
|
87
|
+
silentPermit?.release()
|
|
88
|
+
silentPermit?.release()
|
|
89
|
+
|
|
90
|
+
expect(silentKeyedLock.hasKey("alpha")).toBe(false)
|
|
91
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
92
|
+
|
|
93
|
+
const onDuplicateRelease = vi.fn()
|
|
94
|
+
const customKeyedLock = new KeyedLock<string>({ onDuplicateRelease })
|
|
95
|
+
const customPermit = customKeyedLock.tryAcquire("alpha")
|
|
96
|
+
|
|
97
|
+
expect(customPermit).toBeDefined()
|
|
98
|
+
|
|
99
|
+
customPermit?.release()
|
|
100
|
+
customPermit?.release()
|
|
101
|
+
customPermit?.release()
|
|
102
|
+
|
|
103
|
+
expect(customKeyedLock.hasKey("alpha")).toBe(false)
|
|
104
|
+
expect(onDuplicateRelease).toHaveBeenCalledTimes(2)
|
|
105
|
+
expect(onDuplicateRelease).toHaveBeenCalledWith("KeyedLock permit release was called more than once.")
|
|
106
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
107
|
+
|
|
108
|
+
warnSpy.mockRestore()
|
|
109
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { CoordinationAbortError, CoordinationTimeoutError, Mutex } from "#Source/orchestration/index.ts"
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.clearAllTimers()
|
|
7
|
+
vi.useRealTimers()
|
|
8
|
+
vi.restoreAllMocks()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("Mutex reports lock state and wakes queued acquirers in FIFO order", async () => {
|
|
12
|
+
const mutex = new Mutex()
|
|
13
|
+
|
|
14
|
+
expect(mutex.isLocked()).toBe(false)
|
|
15
|
+
expect(mutex.getPendingCount()).toBe(0)
|
|
16
|
+
|
|
17
|
+
const firstPermit = mutex.tryAcquire()
|
|
18
|
+
|
|
19
|
+
expect(firstPermit).toBeDefined()
|
|
20
|
+
expect(mutex.isLocked()).toBe(true)
|
|
21
|
+
|
|
22
|
+
const secondPermitPromise = mutex.acquire()
|
|
23
|
+
const thirdPermitPromise = mutex.acquire()
|
|
24
|
+
|
|
25
|
+
expect(mutex.getPendingCount()).toBe(2)
|
|
26
|
+
expect(mutex.tryAcquire()).toBeUndefined()
|
|
27
|
+
|
|
28
|
+
firstPermit?.release()
|
|
29
|
+
|
|
30
|
+
const secondPermit = await secondPermitPromise
|
|
31
|
+
let thirdResolved = false
|
|
32
|
+
void thirdPermitPromise.then(() => {
|
|
33
|
+
thirdResolved = true
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await Promise.resolve()
|
|
37
|
+
|
|
38
|
+
expect(mutex.isLocked()).toBe(true)
|
|
39
|
+
expect(mutex.getPendingCount()).toBe(1)
|
|
40
|
+
expect(thirdResolved).toBe(false)
|
|
41
|
+
|
|
42
|
+
secondPermit.release()
|
|
43
|
+
|
|
44
|
+
const thirdPermit = await thirdPermitPromise
|
|
45
|
+
|
|
46
|
+
expect(mutex.getPendingCount()).toBe(0)
|
|
47
|
+
expect(mutex.isLocked()).toBe(true)
|
|
48
|
+
|
|
49
|
+
thirdPermit.release()
|
|
50
|
+
|
|
51
|
+
expect(mutex.isLocked()).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("Mutex acquire rejects on timeout and abort while cleaning pending state", async () => {
|
|
55
|
+
vi.useFakeTimers()
|
|
56
|
+
|
|
57
|
+
const mutex = new Mutex()
|
|
58
|
+
const blockingPermit = mutex.tryAcquire()
|
|
59
|
+
|
|
60
|
+
expect(blockingPermit).toBeDefined()
|
|
61
|
+
|
|
62
|
+
const timeoutPromise = mutex.acquire({ timeout: 10 })
|
|
63
|
+
|
|
64
|
+
expect(mutex.getPendingCount()).toBe(1)
|
|
65
|
+
|
|
66
|
+
await vi.advanceTimersByTimeAsync(10)
|
|
67
|
+
|
|
68
|
+
await expect(timeoutPromise).rejects.toBeInstanceOf(CoordinationTimeoutError)
|
|
69
|
+
expect(mutex.getPendingCount()).toBe(0)
|
|
70
|
+
|
|
71
|
+
const abortController = new AbortController()
|
|
72
|
+
const abortPromise = mutex.acquire({ abortSignal: abortController.signal })
|
|
73
|
+
|
|
74
|
+
expect(mutex.getPendingCount()).toBe(1)
|
|
75
|
+
|
|
76
|
+
abortController.abort("cancelled")
|
|
77
|
+
|
|
78
|
+
await expect(abortPromise).rejects.toBeInstanceOf(CoordinationAbortError)
|
|
79
|
+
expect(mutex.getPendingCount()).toBe(0)
|
|
80
|
+
|
|
81
|
+
blockingPermit?.release()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("Mutex runExclusive releases the lock after callback success or failure", async () => {
|
|
85
|
+
const mutex = new Mutex()
|
|
86
|
+
|
|
87
|
+
await expect(mutex.runExclusive(() => "done")).resolves.toBe("done")
|
|
88
|
+
expect(mutex.isLocked()).toBe(false)
|
|
89
|
+
|
|
90
|
+
const error = new Error("boom")
|
|
91
|
+
|
|
92
|
+
await expect(mutex.runExclusive(() => {
|
|
93
|
+
throw error
|
|
94
|
+
})).rejects.toThrow(error)
|
|
95
|
+
|
|
96
|
+
expect(mutex.isLocked()).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("Mutex is silent by default when release is called more than once", () => {
|
|
100
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
|
|
101
|
+
// no-op to silence warnings during test
|
|
102
|
+
})
|
|
103
|
+
const mutex = new Mutex()
|
|
104
|
+
|
|
105
|
+
const permit = mutex.tryAcquire()
|
|
106
|
+
expect(permit).toBeDefined()
|
|
107
|
+
|
|
108
|
+
permit?.release()
|
|
109
|
+
permit?.release()
|
|
110
|
+
permit?.release()
|
|
111
|
+
|
|
112
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test("Mutex uses custom duplicate-release handler when provided", () => {
|
|
116
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
|
|
117
|
+
// no-op to silence warnings during test
|
|
118
|
+
})
|
|
119
|
+
const onDuplicateRelease = vi.fn()
|
|
120
|
+
const mutex = new Mutex({ onDuplicateRelease })
|
|
121
|
+
|
|
122
|
+
const permit = mutex.tryAcquire()
|
|
123
|
+
expect(permit).toBeDefined()
|
|
124
|
+
|
|
125
|
+
permit?.release()
|
|
126
|
+
permit?.release()
|
|
127
|
+
permit?.release()
|
|
128
|
+
|
|
129
|
+
expect(onDuplicateRelease).toHaveBeenCalledTimes(2)
|
|
130
|
+
expect(onDuplicateRelease).toHaveBeenCalledWith("Mutex permit release was called more than once.")
|
|
131
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
132
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { Permit } from "#Source/orchestration/index.ts"
|
|
4
|
+
|
|
5
|
+
test("Permit release updates lifecycle state and only calls handlers once", () => {
|
|
6
|
+
const onRelease = vi.fn()
|
|
7
|
+
const onDuplicateRelease = vi.fn()
|
|
8
|
+
const permit = new Permit({
|
|
9
|
+
details: { coordination: "custom" },
|
|
10
|
+
duplicateReleaseMessage: "duplicate release",
|
|
11
|
+
onDuplicateRelease,
|
|
12
|
+
onRelease,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(permit.details).toEqual({ coordination: "custom" })
|
|
16
|
+
expect(permit.isActive()).toBe(true)
|
|
17
|
+
expect(permit.isReleased()).toBe(false)
|
|
18
|
+
|
|
19
|
+
permit.release()
|
|
20
|
+
|
|
21
|
+
expect(onRelease).toHaveBeenCalledTimes(1)
|
|
22
|
+
expect(permit.isActive()).toBe(false)
|
|
23
|
+
expect(permit.isReleased()).toBe(true)
|
|
24
|
+
|
|
25
|
+
permit.release()
|
|
26
|
+
|
|
27
|
+
expect(onDuplicateRelease).toHaveBeenCalledTimes(1)
|
|
28
|
+
expect(onDuplicateRelease).toHaveBeenCalledWith("duplicate release")
|
|
29
|
+
expect(onRelease).toHaveBeenCalledTimes(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("Permit dispose and Symbol.dispose delegate to release", () => {
|
|
33
|
+
const onRelease = vi.fn()
|
|
34
|
+
const onDuplicateRelease = vi.fn()
|
|
35
|
+
const permit = new Permit({ onDuplicateRelease, onRelease })
|
|
36
|
+
|
|
37
|
+
permit.dispose()
|
|
38
|
+
permit[Symbol.dispose]()
|
|
39
|
+
|
|
40
|
+
expect(onRelease).toHaveBeenCalledTimes(1)
|
|
41
|
+
expect(onDuplicateRelease).toHaveBeenCalledTimes(1)
|
|
42
|
+
expect(permit.isReleased()).toBe(true)
|
|
43
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { CoordinationAbortError, CoordinationTimeoutError, ReadWriteLock } from "#Source/orchestration/index.ts"
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.clearAllTimers()
|
|
7
|
+
vi.useRealTimers()
|
|
8
|
+
vi.restoreAllMocks()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("ReadWriteLock tracks reader and writer state across immediate acquisitions", () => {
|
|
12
|
+
const readWriteLock = new ReadWriteLock()
|
|
13
|
+
|
|
14
|
+
expect(readWriteLock.isLocked()).toBe(false)
|
|
15
|
+
expect(readWriteLock.isReadLocked()).toBe(false)
|
|
16
|
+
expect(readWriteLock.isWriteLocked()).toBe(false)
|
|
17
|
+
|
|
18
|
+
const firstReader = readWriteLock.tryAcquireRead()
|
|
19
|
+
const secondReader = readWriteLock.tryAcquireRead()
|
|
20
|
+
|
|
21
|
+
expect(firstReader?.details).toEqual({ coordination: "read-write-lock", mode: "read" })
|
|
22
|
+
expect(secondReader?.details).toEqual({ coordination: "read-write-lock", mode: "read" })
|
|
23
|
+
expect(readWriteLock.getActiveReaderCount()).toBe(2)
|
|
24
|
+
expect(readWriteLock.isReadLocked()).toBe(true)
|
|
25
|
+
expect(readWriteLock.tryAcquireWrite()).toBeUndefined()
|
|
26
|
+
|
|
27
|
+
firstReader?.release()
|
|
28
|
+
secondReader?.release()
|
|
29
|
+
|
|
30
|
+
const writer = readWriteLock.tryAcquireWrite()
|
|
31
|
+
|
|
32
|
+
expect(writer?.details).toEqual({ coordination: "read-write-lock", mode: "write" })
|
|
33
|
+
expect(readWriteLock.isWriteLocked()).toBe(true)
|
|
34
|
+
expect(readWriteLock.isLocked()).toBe(true)
|
|
35
|
+
|
|
36
|
+
writer?.release()
|
|
37
|
+
|
|
38
|
+
expect(readWriteLock.isLocked()).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("ReadWriteLock keeps queued writers ahead of later readers", async () => {
|
|
42
|
+
const readWriteLock = new ReadWriteLock()
|
|
43
|
+
const activeReader = readWriteLock.tryAcquireRead()
|
|
44
|
+
|
|
45
|
+
expect(activeReader).toBeDefined()
|
|
46
|
+
|
|
47
|
+
const writerPromise = readWriteLock.acquireWrite()
|
|
48
|
+
const lateReaderPromise = readWriteLock.acquireRead()
|
|
49
|
+
|
|
50
|
+
expect(readWriteLock.getPendingWriterCount()).toBe(1)
|
|
51
|
+
expect(readWriteLock.getPendingReaderCount()).toBe(1)
|
|
52
|
+
expect(readWriteLock.getPendingCount()).toBe(2)
|
|
53
|
+
|
|
54
|
+
activeReader?.release()
|
|
55
|
+
|
|
56
|
+
const writer = await writerPromise
|
|
57
|
+
let lateReaderResolved = false
|
|
58
|
+
void lateReaderPromise.then(() => {
|
|
59
|
+
lateReaderResolved = true
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await Promise.resolve()
|
|
63
|
+
|
|
64
|
+
expect(readWriteLock.isWriteLocked()).toBe(true)
|
|
65
|
+
expect(lateReaderResolved).toBe(false)
|
|
66
|
+
|
|
67
|
+
writer.release()
|
|
68
|
+
|
|
69
|
+
const lateReader = await lateReaderPromise
|
|
70
|
+
|
|
71
|
+
expect(readWriteLock.getPendingCount()).toBe(0)
|
|
72
|
+
expect(readWriteLock.getActiveReaderCount()).toBe(1)
|
|
73
|
+
|
|
74
|
+
lateReader.release()
|
|
75
|
+
|
|
76
|
+
expect(readWriteLock.isLocked()).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("ReadWriteLock acquire methods reject on timeout and abort without leaking counts", async () => {
|
|
80
|
+
vi.useFakeTimers()
|
|
81
|
+
|
|
82
|
+
const readWriteLock = new ReadWriteLock()
|
|
83
|
+
const writer = readWriteLock.tryAcquireWrite()
|
|
84
|
+
|
|
85
|
+
expect(writer).toBeDefined()
|
|
86
|
+
|
|
87
|
+
const timeoutPromise = readWriteLock.acquireRead({ timeout: 20 })
|
|
88
|
+
|
|
89
|
+
expect(readWriteLock.getPendingReaderCount()).toBe(1)
|
|
90
|
+
|
|
91
|
+
await vi.advanceTimersByTimeAsync(20)
|
|
92
|
+
|
|
93
|
+
await expect(timeoutPromise).rejects.toBeInstanceOf(CoordinationTimeoutError)
|
|
94
|
+
expect(readWriteLock.getPendingReaderCount()).toBe(0)
|
|
95
|
+
|
|
96
|
+
const abortController = new AbortController()
|
|
97
|
+
const abortPromise = readWriteLock.acquireWrite({ abortSignal: abortController.signal })
|
|
98
|
+
|
|
99
|
+
expect(readWriteLock.getPendingWriterCount()).toBe(1)
|
|
100
|
+
|
|
101
|
+
abortController.abort("cancelled")
|
|
102
|
+
|
|
103
|
+
await expect(abortPromise).rejects.toBeInstanceOf(CoordinationAbortError)
|
|
104
|
+
expect(readWriteLock.getPendingWriterCount()).toBe(0)
|
|
105
|
+
|
|
106
|
+
writer?.release()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test("ReadWriteLock runExclusive methods release permits after callback completion", async () => {
|
|
110
|
+
const readWriteLock = new ReadWriteLock()
|
|
111
|
+
|
|
112
|
+
await expect(readWriteLock.runExclusiveRead(() => "read")).resolves.toBe("read")
|
|
113
|
+
expect(readWriteLock.isLocked()).toBe(false)
|
|
114
|
+
|
|
115
|
+
const error = new Error("write failed")
|
|
116
|
+
|
|
117
|
+
await expect(readWriteLock.runExclusiveWrite(() => {
|
|
118
|
+
throw error
|
|
119
|
+
})).rejects.toThrow(error)
|
|
120
|
+
|
|
121
|
+
expect(readWriteLock.isLocked()).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("ReadWriteLock duplicate release stays silent by default and uses custom handler when provided", () => {
|
|
125
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
|
|
126
|
+
// no-op to silence warnings during test
|
|
127
|
+
})
|
|
128
|
+
const silentReadWriteLock = new ReadWriteLock()
|
|
129
|
+
const silentPermit = silentReadWriteLock.tryAcquireWrite()
|
|
130
|
+
|
|
131
|
+
expect(silentPermit).toBeDefined()
|
|
132
|
+
|
|
133
|
+
silentPermit?.release()
|
|
134
|
+
silentPermit?.release()
|
|
135
|
+
silentPermit?.release()
|
|
136
|
+
|
|
137
|
+
expect(silentReadWriteLock.isLocked()).toBe(false)
|
|
138
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
139
|
+
|
|
140
|
+
const onDuplicateRelease = vi.fn()
|
|
141
|
+
const customReadWriteLock = new ReadWriteLock({ onDuplicateRelease })
|
|
142
|
+
const customPermit = customReadWriteLock.tryAcquireRead()
|
|
143
|
+
|
|
144
|
+
expect(customPermit).toBeDefined()
|
|
145
|
+
|
|
146
|
+
customPermit?.release()
|
|
147
|
+
customPermit?.release()
|
|
148
|
+
customPermit?.release()
|
|
149
|
+
|
|
150
|
+
expect(customReadWriteLock.isLocked()).toBe(false)
|
|
151
|
+
expect(onDuplicateRelease).toHaveBeenCalledTimes(2)
|
|
152
|
+
expect(onDuplicateRelease).toHaveBeenCalledWith("ReadWriteLock permit release was called more than once.")
|
|
153
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
154
|
+
})
|