@planet-matrix/mobius-model 0.6.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/oxlint.config.ts +1 -2
- package/package.json +29 -17
- package/scripts/build.ts +2 -52
- package/src/ai/README.md +1 -0
- package/src/ai/ai.ts +107 -0
- package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
- package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
- package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
- package/src/ai/chat-completion-ai/index.ts +7 -0
- package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
- package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
- package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
- package/src/ai/embedding-ai/embedding-ai.ts +63 -0
- package/src/ai/embedding-ai/embedding.ts +50 -0
- package/src/ai/embedding-ai/index.ts +4 -0
- package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
- package/src/ai/index.ts +4 -0
- package/src/aio/README.md +100 -0
- package/src/aio/content.ts +141 -0
- package/src/aio/index.ts +3 -0
- package/src/aio/json.ts +127 -0
- package/src/aio/prompt.ts +246 -0
- package/src/basic/README.md +20 -15
- package/src/basic/error.ts +19 -5
- package/src/basic/function.ts +2 -2
- package/src/basic/index.ts +1 -0
- package/src/basic/promise.ts +141 -71
- package/src/basic/schedule.ts +111 -0
- package/src/basic/stream.ts +135 -25
- package/src/credential/README.md +107 -0
- package/src/credential/api-key.ts +158 -0
- package/src/credential/bearer.ts +73 -0
- package/src/credential/index.ts +4 -0
- package/src/credential/json-web-token.ts +96 -0
- package/src/credential/password.ts +170 -0
- package/src/cron/README.md +86 -0
- package/src/cron/cron.ts +87 -0
- package/src/cron/index.ts +1 -0
- package/src/drizzle/README.md +1 -0
- package/src/drizzle/drizzle.ts +1 -0
- package/src/drizzle/helper.ts +47 -0
- package/src/drizzle/index.ts +5 -0
- package/src/drizzle/infer.ts +52 -0
- package/src/drizzle/kysely.ts +8 -0
- package/src/drizzle/pagination.ts +198 -0
- package/src/email/README.md +1 -0
- package/src/email/index.ts +1 -0
- package/src/email/resend.ts +25 -0
- package/src/event/class-event-proxy.ts +5 -6
- package/src/event/common.ts +13 -3
- package/src/event/event-manager.ts +3 -3
- package/src/event/instance-event-proxy.ts +5 -6
- package/src/event/internal.ts +4 -4
- package/src/exception/README.md +28 -19
- package/src/exception/error/error.ts +123 -0
- package/src/exception/error/index.ts +2 -0
- package/src/exception/error/match.ts +38 -0
- package/src/exception/error/must-fix.ts +17 -0
- package/src/exception/index.ts +2 -0
- package/src/file-system/find.ts +53 -0
- package/src/file-system/index.ts +2 -0
- package/src/file-system/path.ts +76 -0
- package/src/file-system/resolve.ts +22 -0
- package/src/form/README.md +25 -0
- package/src/form/index.ts +1 -0
- package/src/form/inputor-controller/base.ts +861 -0
- package/src/form/inputor-controller/boolean.ts +39 -0
- package/src/form/inputor-controller/file.ts +39 -0
- package/src/form/inputor-controller/form.ts +179 -0
- package/src/form/inputor-controller/helper.ts +117 -0
- package/src/form/inputor-controller/index.ts +17 -0
- package/src/form/inputor-controller/multi-select.ts +99 -0
- package/src/form/inputor-controller/number.ts +116 -0
- package/src/form/inputor-controller/select.ts +109 -0
- package/src/form/inputor-controller/text.ts +82 -0
- package/src/http/READMD.md +1 -0
- package/src/http/api/api-core.ts +84 -0
- package/src/http/api/api-handler.ts +79 -0
- package/src/http/api/api-host.ts +47 -0
- package/src/http/api/api-result.ts +56 -0
- package/src/http/api/api-schema.ts +154 -0
- package/src/http/api/api-server.ts +130 -0
- package/src/http/api/api-test.ts +142 -0
- package/src/http/api/api-type.ts +34 -0
- package/src/http/api/api.ts +81 -0
- package/src/http/api/index.ts +11 -0
- package/src/http/api-adapter/api-core-node-http.ts +260 -0
- package/src/http/api-adapter/api-host-node-http.ts +156 -0
- package/src/http/api-adapter/api-result-arktype.ts +294 -0
- package/src/http/api-adapter/api-result-zod.ts +286 -0
- package/src/http/api-adapter/index.ts +5 -0
- package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
- package/src/http/bin/gen-api-list/index.ts +1 -0
- package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
- package/src/http/bin/gen-api-test/index.ts +1 -0
- package/src/http/bin/gen-api-type/calc-code.ts +25 -0
- package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
- package/src/http/bin/gen-api-type/index.ts +2 -0
- package/src/http/bin/index.ts +2 -0
- package/src/http/index.ts +3 -0
- package/src/huawei/README.md +1 -0
- package/src/huawei/index.ts +2 -0
- package/src/huawei/moderation/index.ts +1 -0
- package/src/huawei/moderation/moderation.ts +355 -0
- package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
- package/src/huawei/obs/index.ts +1 -0
- package/src/huawei/obs/obs.ts +42 -0
- package/src/index.ts +21 -2
- package/src/json/README.md +92 -0
- package/src/json/index.ts +1 -0
- package/src/json/repair.ts +18 -0
- package/src/log/logger.ts +15 -4
- package/src/openai/README.md +1 -0
- package/src/openai/index.ts +1 -0
- package/src/openai/openai.ts +509 -0
- package/src/orchestration/README.md +9 -7
- package/src/orchestration/dispatching/dispatcher.ts +83 -0
- package/src/orchestration/dispatching/index.ts +2 -0
- package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
- package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
- package/src/orchestration/dispatching/selector/index.ts +2 -0
- package/src/orchestration/index.ts +2 -0
- package/src/orchestration/scheduling/index.ts +2 -0
- package/src/orchestration/scheduling/scheduler.ts +103 -0
- package/src/orchestration/scheduling/task.ts +32 -0
- package/src/random/README.md +8 -7
- package/src/random/base.ts +66 -0
- package/src/random/index.ts +5 -1
- package/src/random/random-boolean.ts +40 -0
- package/src/random/random-integer.ts +60 -0
- package/src/random/random-number.ts +72 -0
- package/src/random/random-string.ts +66 -0
- package/src/request/README.md +108 -0
- package/src/request/fetch/base.ts +108 -0
- package/src/request/fetch/browser.ts +280 -0
- package/src/request/fetch/general.ts +20 -0
- package/src/request/fetch/index.ts +4 -0
- package/src/request/fetch/nodejs.ts +280 -0
- package/src/request/index.ts +2 -0
- package/src/request/request/base.ts +246 -0
- package/src/request/request/general.ts +63 -0
- package/src/request/request/index.ts +3 -0
- package/src/request/request/resource.ts +68 -0
- package/src/result/README.md +4 -0
- package/src/result/controller.ts +58 -0
- package/src/result/either.ts +363 -0
- package/src/result/generator.ts +168 -0
- package/src/result/index.ts +3 -0
- package/src/route/README.md +105 -0
- package/src/route/adapter/browser.ts +122 -0
- package/src/route/adapter/driver.ts +56 -0
- package/src/route/adapter/index.ts +2 -0
- package/src/route/index.ts +3 -0
- package/src/route/router/index.ts +2 -0
- package/src/route/router/route.ts +630 -0
- package/src/route/router/router.ts +1641 -0
- package/src/route/uri/hash.ts +307 -0
- package/src/route/uri/index.ts +7 -0
- package/src/route/uri/pathname.ts +376 -0
- package/src/route/uri/search.ts +412 -0
- package/src/service/README.md +1 -0
- package/src/service/index.ts +1 -0
- package/src/service/service.ts +110 -0
- package/src/socket/README.md +105 -0
- package/src/socket/client/index.ts +2 -0
- package/src/socket/client/socket-unit.ts +658 -0
- package/src/socket/client/socket.ts +203 -0
- package/src/socket/common/index.ts +2 -0
- package/src/socket/common/socket-unit-common.ts +23 -0
- package/src/socket/common/socket-unit-heartbeat.ts +427 -0
- package/src/socket/index.ts +3 -0
- package/src/socket/server/index.ts +3 -0
- package/src/socket/server/server.ts +183 -0
- package/src/socket/server/socket-unit.ts +448 -0
- package/src/socket/server/socket.ts +264 -0
- package/src/storage/table.ts +3 -3
- package/src/timer/expiration/expiration-manager.ts +3 -3
- package/src/timer/expiration/remaining-manager.ts +3 -3
- package/src/tube/README.md +99 -0
- package/src/tube/helper.ts +137 -0
- package/src/tube/index.ts +2 -0
- package/src/tube/tube.ts +880 -0
- package/src/weixin/README.md +1 -0
- package/src/weixin/index.ts +2 -0
- package/src/weixin/official-account/authorization.ts +157 -0
- package/src/weixin/official-account/index.ts +2 -0
- package/src/weixin/official-account/js-api.ts +132 -0
- package/src/weixin/open/index.ts +1 -0
- package/src/weixin/open/oauth2.ts +131 -0
- package/tests/unit/ai/ai.spec.ts +85 -0
- package/tests/unit/aio/content.spec.ts +105 -0
- package/tests/unit/aio/json.spec.ts +146 -0
- package/tests/unit/aio/prompt.spec.ts +111 -0
- package/tests/unit/basic/error.spec.ts +16 -4
- package/tests/unit/basic/promise.spec.ts +158 -50
- package/tests/unit/basic/schedule.spec.ts +74 -0
- package/tests/unit/basic/stream.spec.ts +90 -37
- package/tests/unit/credential/api-key.spec.ts +36 -0
- package/tests/unit/credential/bearer.spec.ts +23 -0
- package/tests/unit/credential/json-web-token.spec.ts +23 -0
- package/tests/unit/credential/password.spec.ts +40 -0
- package/tests/unit/cron/cron.spec.ts +84 -0
- package/tests/unit/event/class-event-proxy.spec.ts +3 -3
- package/tests/unit/event/event-manager.spec.ts +3 -3
- package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
- package/tests/unit/exception/error/error.spec.ts +83 -0
- package/tests/unit/exception/error/match.spec.ts +81 -0
- package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
- package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
- package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
- package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
- package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
- package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
- package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
- package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
- package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
- package/tests/unit/http/api/api-core-host.spec.ts +207 -0
- package/tests/unit/http/api/api-schema.spec.ts +120 -0
- package/tests/unit/http/api/api-server.spec.ts +363 -0
- package/tests/unit/http/api/api-test.spec.ts +117 -0
- package/tests/unit/http/api/api.spec.ts +121 -0
- package/tests/unit/http/api-adapter/node-http.spec.ts +187 -0
- package/tests/unit/identifier/uuid.spec.ts +0 -1
- package/tests/unit/json/repair.spec.ts +11 -0
- package/tests/unit/log/logger.spec.ts +19 -4
- package/tests/unit/openai/openai.spec.ts +64 -0
- package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
- package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
- package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
- package/tests/unit/random/base.spec.ts +58 -0
- package/tests/unit/random/random-boolean.spec.ts +25 -0
- package/tests/unit/random/random-integer.spec.ts +32 -0
- package/tests/unit/random/random-number.spec.ts +33 -0
- package/tests/unit/random/random-string.spec.ts +22 -0
- package/tests/unit/request/fetch/browser.spec.ts +222 -0
- package/tests/unit/request/fetch/general.spec.ts +43 -0
- package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
- package/tests/unit/request/request/base.spec.ts +382 -0
- package/tests/unit/request/request/general.spec.ts +160 -0
- package/tests/unit/result/controller.spec.ts +82 -0
- package/tests/unit/result/either.spec.ts +377 -0
- package/tests/unit/result/generator.spec.ts +273 -0
- package/tests/unit/route/router/route.spec.ts +430 -0
- package/tests/unit/route/router/router.spec.ts +407 -0
- package/tests/unit/route/uri/hash.spec.ts +72 -0
- package/tests/unit/route/uri/pathname.spec.ts +146 -0
- package/tests/unit/route/uri/search.spec.ts +107 -0
- package/tests/unit/socket/client.spec.ts +208 -0
- package/tests/unit/socket/server.spec.ts +133 -0
- package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
- package/tests/unit/tube/helper.spec.ts +139 -0
- package/tests/unit/tube/tube.spec.ts +501 -0
- package/vite.config.ts +2 -1
- package/dist/index.js +0 -50
- package/dist/index.js.map +0 -209
- package/src/random/string.ts +0 -35
- package/tests/unit/random/string.spec.ts +0 -11
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Description
|
|
4
4
|
|
|
5
|
-
Orchestration 模块提供围绕执行单元编排(orchestration
|
|
5
|
+
Orchestration 模块提供围绕执行单元编排(orchestration)的基础建模能力,用于把多个执行单元之间的创建、组织、调度、分派、等待、放行、推进、收束以及整体协作关系表达为稳定且可复用的公共语义。
|
|
6
6
|
|
|
7
7
|
它关注的不是某个具体框架、某个具体队列系统或某个具体工作流引擎的宿主接口,而是“多个执行单元如何被组织为可理解、可组合、可控制、可持续演化的编排结构”这一更一般的问题。因此,这个模块不应被理解为仅仅承载互斥锁、信号量或栅栏的协调原语集合;这些内容只是当前实现中更基础的一层,而不是 Orchestration 这个词所指向的全部问题域。
|
|
8
8
|
|
|
@@ -10,13 +10,14 @@ Orchestration 模块提供围绕执行单元编排(orchestration)的基础
|
|
|
10
10
|
|
|
11
11
|
理解 Orchestration 模块时,应先把它看作“执行单元组织关系”的建模层,而不是一组彼此孤立的小工具。它要解决的问题,不只是多个异步流程怎样竞争资源、怎样等待同步点,也包括执行单元该如何被表达、如何被组合成更大的结构、流程阶段该如何被推进,以及这些结构之间应该暴露什么样的稳定公共语义。
|
|
12
12
|
|
|
13
|
-
因此,Orchestration 更适合被理解为一个分层的问题域:较底层是 coordination
|
|
13
|
+
因此,Orchestration 更适合被理解为一个分层的问题域:较底层是 coordination,用于表达进入控制、等待约束、同步点和资源占用;在其之上还可以有 dispatching,用于表达一组候选执行目标如何被选择、如何按上下文形成选择视图,以及反馈如何影响后续分派;再往上则可以逐步生长出更接近编排结构本身的能力,用于表达单个执行单元、执行单元之间的关系、阶段推进、依赖收敛、生命周期以及整体执行边界。未来这些能力的形态可能会落在 task、job、workflow 或其它更合适的抽象上,但 coordination 与 dispatching 都只是这些能力可复用的基础层,不等同于整个 orchestration 模块。
|
|
14
14
|
|
|
15
15
|
理解这个模块时,还应守住几条边界原则:
|
|
16
16
|
|
|
17
17
|
- Orchestration 模块表达的是编排语义与协调语义,但不应直接承担某个业务系统专属的流程定义语言、运营规则或领域状态机。
|
|
18
18
|
- 它可以吸收一切稳定且通用的编排概念,但这些概念应以模型形式出现,而不是某个具体平台配置格式、DSL 或产品术语的直接翻版。
|
|
19
19
|
- coordination 适合表达进程内、运行时内的基础协作约束;更高层的编排能力即使依赖这些约束,也不应把自己退化成底层锁语义的别名。
|
|
20
|
+
- dispatching 适合表达一组候选目标之间的运行时分派、反馈驱动选择与局部选择视图,但不应直接退化成只面向某类网络节点的负载均衡器别名。
|
|
20
21
|
- 它可以为重试、取消、阶段推进、依赖关系或执行计划等语义提供抽象空间,但不应把监控、持久化、分布式共识、租约续期或平台运维策略直接混入核心公共 API。
|
|
21
22
|
|
|
22
23
|
## For Using
|
|
@@ -26,18 +27,19 @@ Orchestration 模块提供围绕执行单元编排(orchestration)的基础
|
|
|
26
27
|
从使用角度看,当前模块中的能力大致可以分为三类:
|
|
27
28
|
|
|
28
29
|
- 底层协调能力:用于表达互斥进入、限量并发、阶段会合、闭锁等待、资源占用句柄以及超时和中止等等待约束。
|
|
30
|
+
- 分派能力:用于表达多个候选执行目标之间如何被选择、如何基于调用上下文形成可复用的 selector,以及失败或恢复反馈如何影响后续选择。
|
|
29
31
|
- 更高层编排能力:用于表达单个执行单元的边界、多个执行单元之间的关系、阶段推进、失败传播、取消语义、执行计划以及整体完成条件。这部分目前还不是主实现重心,但属于该模块未来合理的组成方向。
|
|
30
32
|
- 结构化执行能力:用于把一组原本零散的异步动作收束为可以理解、组合与扩展的执行结构。未来它可能体现为 task、job、workflow,也可能体现为其它更贴切的抽象,但都不应退化成业务代码里对底层 Promise 的随意组合。
|
|
31
33
|
|
|
32
34
|
更合适的接入方式,是先判断你的问题属于哪一层:如果你的核心诉求是某个时刻能否进入、是否需要排队、何时统一继续,那么你更接近 coordination;如果你的核心诉求已经开始涉及执行单元如何被声明、取消、组合、计划、收束或被纳入更大的执行结构,那么你已经进入更高层的 orchestration 语义。
|
|
33
35
|
|
|
34
|
-
当前对外已经落地的主要是 `coordination`
|
|
36
|
+
当前对外已经落地的主要是 `coordination` 与 `dispatching` 子模块,因此现阶段使用时既可以处理互斥、信号量、读写锁、闭锁与栅栏这类问题,也可以处理一组候选目标之间的 selector 构造、目标选择与反馈驱动分派;但理解这个模块时仍不应把它局限为“并发锁工具箱”或“负载均衡工具箱”。更合适的看法是:coordination 与 dispatching 先提供底层编排积木,而 orchestration 的整体职责会继续向更完整的执行组织与流程编排语义扩展。
|
|
35
37
|
|
|
36
38
|
## For Contributing
|
|
37
39
|
|
|
38
40
|
为 Orchestration 模块贡献内容时,优先判断新增能力是否真的在澄清某种稳定的编排语义,而不是只是在补一个局部业务流程的便捷封装。这个模块应长期服务于“执行单元如何被表达”“多个执行单元如何组成更大的结构”“流程如何推进与结束”“等待与放行如何成为可复用原语”这几类问题。如果一个能力只有在绑定特定平台、特定产品流程或特定部署环境时才成立,那么它通常不应直接成为该模块的公共组成部分。
|
|
39
41
|
|
|
40
|
-
扩展这个模块时,应特别警惕以下倾向:把 orchestration
|
|
42
|
+
扩展这个模块时,应特别警惕以下倾向:把 orchestration 退化成一堆零散锁工具;把 dispatching 退化成只对某一类 upstream 节点成立的网络负载均衡器包装;把某个具体工作流引擎的配置模型直接照搬进公共 API;把重试、补偿、优先级、监控或持久化策略无差别地下沉到所有层级;把调用方的一次性业务约定固化为模块语义;或者为了某个实现细节而暴露内部等待队列、内部状态节点、候选集快照与临时调度结构。文档应说明哪些编排边界是长期成立的,以及为什么这些边界成立,而不是复述当前版本的局部实现技巧。
|
|
41
43
|
|
|
42
44
|
### JSDoc 注释格式要求
|
|
43
45
|
|
|
@@ -68,14 +70,14 @@ Orchestration 模块提供围绕执行单元编排(orchestration)的基础
|
|
|
68
70
|
- 子模块不需要有自己的 `README.md`。
|
|
69
71
|
- 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
|
|
70
72
|
- 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
|
|
71
|
-
- 与 orchestration
|
|
73
|
+
- 与 orchestration 相关的实现应优先围绕执行单元边界、执行单元关系、等待约束、分派语义、生命周期推进、阶段语义与可组合性组织;其中 coordination 子模块再单独关注占用句柄、队列调度、公平性与同步阶段语义,dispatching 子模块再单独关注候选目标集合、selector 视图、选择策略与反馈驱动调整,避免把具体业务流程、网络拓扑假设或宿主运行时策略混入模块边界。
|
|
72
74
|
|
|
73
75
|
### 导出策略要求
|
|
74
76
|
|
|
75
77
|
- 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
|
|
76
78
|
- 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
|
|
77
79
|
- Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
|
|
78
|
-
-
|
|
80
|
+
- 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的编排语义、协调语义或分派语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
|
|
79
81
|
|
|
80
82
|
### 测试要求
|
|
81
83
|
|
|
@@ -86,4 +88,4 @@ Orchestration 模块提供围绕执行单元编排(orchestration)的基础
|
|
|
86
88
|
- 测试顺序应与源文件中被测试目标的原始顺序保持一致。
|
|
87
89
|
- 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
|
|
88
90
|
- 模块的单元测试文件目录是 `./tests/unit/orchestration`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/orchestration/<sub-module-name>`。
|
|
89
|
-
- 对这个模块来说,测试应按层覆盖:coordination 层优先覆盖资源获取与释放、等待队列的 FIFO 或公平性约束、超时与中止导致的失败语义、同步点完成或破坏时的整体行为,以及 permit
|
|
91
|
+
- 对这个模块来说,测试应按层覆盖:coordination 层优先覆盖资源获取与释放、等待队列的 FIFO 或公平性约束、超时与中止导致的失败语义、同步点完成或破坏时的整体行为,以及 permit 重复释放等误用路径;dispatching 层优先覆盖 selector 复用与销毁、候选目标过滤、策略驱动的选择顺序、无可选目标与无可用目标的区分,以及反馈回写后对后续选择的影响;未来若引入更高层编排能力,则应优先覆盖生命周期推进、依赖关系收敛、失败传播、取消与重试边界,以及整体编排结果的确定性。
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
|
|
2
|
+
import { generateUuidV4FromUrl } from "#Source/identifier/index.ts"
|
|
3
|
+
import { Logger } from "#Source/log/index.ts"
|
|
4
|
+
|
|
5
|
+
import type { BaseSelector } from "./selector/index.ts"
|
|
6
|
+
import { DownCountSelector } from "./selector/index.ts"
|
|
7
|
+
|
|
8
|
+
interface ItemStates<Target> {
|
|
9
|
+
item: Target
|
|
10
|
+
available: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GetSelectorOptions<Target> {
|
|
14
|
+
id?: string | undefined
|
|
15
|
+
filter?: ((item: Target) => boolean) | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GetSelectorResult<Target> {
|
|
19
|
+
selector: BaseSelector<Target>
|
|
20
|
+
destroy: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DispatcherOptions<Target> extends LoggerFriendlyOptions {
|
|
24
|
+
itemList: Target[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Dispatcher<Target> implements LoggerFriendly {
|
|
28
|
+
readonly logger: Logger
|
|
29
|
+
|
|
30
|
+
private itemStatesMap: Map<Target, ItemStates<Target>>
|
|
31
|
+
private selectorMap: Map<string, BaseSelector<Target>>
|
|
32
|
+
|
|
33
|
+
constructor(options: DispatcherOptions<Target>) {
|
|
34
|
+
this.logger = Logger.fromOptions(options).setDefaultName("Dispatcher")
|
|
35
|
+
|
|
36
|
+
this.itemStatesMap = new Map()
|
|
37
|
+
options.itemList.forEach((item) => {
|
|
38
|
+
this.itemStatesMap.set(item, {
|
|
39
|
+
item,
|
|
40
|
+
available: true,
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
this.selectorMap = new Map()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getSelector(options: GetSelectorOptions<Target>): GetSelectorResult<Target> {
|
|
47
|
+
const {
|
|
48
|
+
id = generateUuidV4FromUrl(),
|
|
49
|
+
filter = (): boolean => true,
|
|
50
|
+
} = options
|
|
51
|
+
|
|
52
|
+
const isInitialized = this.selectorMap.has(id)
|
|
53
|
+
if (isInitialized === false) {
|
|
54
|
+
const list = Array.from(this.itemStatesMap.values())
|
|
55
|
+
.filter((itemStates) => {
|
|
56
|
+
return itemStates.available === true && filter(itemStates.item) === true
|
|
57
|
+
})
|
|
58
|
+
.map((itemStates) => {
|
|
59
|
+
return itemStates.item
|
|
60
|
+
})
|
|
61
|
+
// TODO: 将 selector 的类型作为参数,从而实现不同的 dispatcher 策略
|
|
62
|
+
const selector = new DownCountSelector({
|
|
63
|
+
id,
|
|
64
|
+
itemList: list,
|
|
65
|
+
logger: Logger.derive(this.logger).setName(`Selector-${id}`),
|
|
66
|
+
})
|
|
67
|
+
this.selectorMap.set(id, selector)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const selector = this.selectorMap.get(id)!
|
|
71
|
+
const result = {
|
|
72
|
+
selector,
|
|
73
|
+
destroy: (): void => {
|
|
74
|
+
this.destroySelector(id)
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
return result
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
destroySelector(key: string): void {
|
|
81
|
+
this.selectorMap.delete(key)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
|
|
2
|
+
import type { Either } from "#Source/result/index.ts"
|
|
3
|
+
|
|
4
|
+
import { Logger } from "#Source/log/index.ts"
|
|
5
|
+
|
|
6
|
+
export interface GetItemLeft {
|
|
7
|
+
code: "NO_CHOICE" | "NO_AVAILABLE"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GetItemRight<Target> {
|
|
11
|
+
item: Target
|
|
12
|
+
markAvailable: () => void
|
|
13
|
+
markUnavailable: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type GetItemResult<Target> = Either<GetItemLeft, GetItemRight<Target>>
|
|
17
|
+
|
|
18
|
+
export interface BaseSelectorOptions<Target> extends LoggerFriendlyOptions {
|
|
19
|
+
id: string
|
|
20
|
+
itemList: Target[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export abstract class BaseSelector<Target> implements LoggerFriendly {
|
|
24
|
+
readonly logger: Logger
|
|
25
|
+
|
|
26
|
+
private id: string
|
|
27
|
+
|
|
28
|
+
constructor(options: BaseSelectorOptions<Target>) {
|
|
29
|
+
this.logger = Logger.fromOptions(options).setDefaultName("BaseSelector")
|
|
30
|
+
|
|
31
|
+
this.id = options.id
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getId(): string {
|
|
35
|
+
return this.id
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
abstract getItem(): Promise<GetItemResult<Target>>
|
|
39
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
|
|
2
|
+
import { controllerFromEitherType } from "#Source/result/index.ts"
|
|
3
|
+
|
|
4
|
+
import type { BaseSelectorOptions, GetItemResult } from "./base-selector.ts"
|
|
5
|
+
import { BaseSelector } from "./base-selector.ts"
|
|
6
|
+
|
|
7
|
+
interface ItemStates<Target> {
|
|
8
|
+
item: Target
|
|
9
|
+
downCount: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DownCountSelectorOptions<Target> extends BaseSelectorOptions<Target>, LoggerFriendlyOptions {
|
|
13
|
+
/**
|
|
14
|
+
* @default 1
|
|
15
|
+
*/
|
|
16
|
+
maxDownCount?: number | undefined
|
|
17
|
+
/**
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
resetAllWhenNoAvailable?: boolean | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DownCountSelector<Target> extends BaseSelector<Target> implements LoggerFriendly {
|
|
24
|
+
private maxDownCount: number
|
|
25
|
+
private resetAllWhenNoAvailable: boolean
|
|
26
|
+
|
|
27
|
+
private itemStatesMap: Map<Target, ItemStates<Target>>
|
|
28
|
+
|
|
29
|
+
constructor(options: DownCountSelectorOptions<Target>) {
|
|
30
|
+
super(options)
|
|
31
|
+
this.logger.setDefaultName("DownCountSelector")
|
|
32
|
+
|
|
33
|
+
this.maxDownCount = options.maxDownCount ?? 1
|
|
34
|
+
this.resetAllWhenNoAvailable = options.resetAllWhenNoAvailable ?? true
|
|
35
|
+
|
|
36
|
+
this.itemStatesMap = new Map()
|
|
37
|
+
options.itemList.forEach((item) => {
|
|
38
|
+
this.itemStatesMap.set(item, {
|
|
39
|
+
item,
|
|
40
|
+
downCount: 0,
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private markItemAvailable(item: Target): void {
|
|
46
|
+
const states = this.itemStatesMap.get(item)
|
|
47
|
+
if (states === undefined) {
|
|
48
|
+
throw new Error(`Item not found: ${String(item)}`)
|
|
49
|
+
}
|
|
50
|
+
states.downCount = 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private markItemUnavailable(item: Target): void {
|
|
54
|
+
const states = this.itemStatesMap.get(item)
|
|
55
|
+
if (states === undefined) {
|
|
56
|
+
throw new Error(`Item not found: ${String(item)}`)
|
|
57
|
+
}
|
|
58
|
+
states.downCount = states.downCount + 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private resetAllItem(): void {
|
|
62
|
+
this.itemStatesMap.forEach((states) => {
|
|
63
|
+
states.downCount = 0
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private getAvailableItemList(): Target[] {
|
|
68
|
+
const availableItemList: Target[] = []
|
|
69
|
+
this.itemStatesMap.forEach((states) => {
|
|
70
|
+
if (states.downCount < this.maxDownCount) {
|
|
71
|
+
availableItemList.push(states.item)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
return availableItemList
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getItem(): Promise<GetItemResult<Target>> {
|
|
78
|
+
const controller = controllerFromEitherType<GetItemResult<Target>>()
|
|
79
|
+
|
|
80
|
+
if (this.itemStatesMap.size === 0) {
|
|
81
|
+
return await controller.returnLeft({
|
|
82
|
+
code: "NO_CHOICE",
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const availableItemList = this.getAvailableItemList()
|
|
87
|
+
if (availableItemList.length === 0) {
|
|
88
|
+
if (this.resetAllWhenNoAvailable === false) {
|
|
89
|
+
return await controller.returnLeft({
|
|
90
|
+
code: "NO_AVAILABLE",
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.resetAllItem()
|
|
95
|
+
return await this.getItem()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
availableItemList.sort((a, b) => {
|
|
100
|
+
const aStates = this.itemStatesMap.get(a)!
|
|
101
|
+
const bStates = this.itemStatesMap.get(b)!
|
|
102
|
+
if (aStates.downCount === bStates.downCount) {
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
105
|
+
return aStates.downCount - bStates.downCount
|
|
106
|
+
})
|
|
107
|
+
const item = availableItemList[0]!
|
|
108
|
+
|
|
109
|
+
return await controller.returnRight({
|
|
110
|
+
item,
|
|
111
|
+
markAvailable: () => {
|
|
112
|
+
this.markItemAvailable(item)
|
|
113
|
+
},
|
|
114
|
+
markUnavailable: () => {
|
|
115
|
+
this.markItemUnavailable(item)
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Task } from "./task.ts"
|
|
2
|
+
|
|
3
|
+
export interface RunLogEntry {
|
|
4
|
+
name: string
|
|
5
|
+
time: Date
|
|
6
|
+
}
|
|
7
|
+
export interface ErrLogEntry {
|
|
8
|
+
name: string
|
|
9
|
+
time: Date
|
|
10
|
+
reason: string
|
|
11
|
+
}
|
|
12
|
+
export interface SchedulerOptions {
|
|
13
|
+
tasks: Task[]
|
|
14
|
+
maxLogNumber?: number | undefined
|
|
15
|
+
}
|
|
16
|
+
export class Scheduler {
|
|
17
|
+
protected options: SchedulerOptions
|
|
18
|
+
|
|
19
|
+
protected isRunning: boolean
|
|
20
|
+
protected maxLogNumber: number
|
|
21
|
+
protected timers: Set<ReturnType<typeof setTimeout>>
|
|
22
|
+
protected runLog: RunLogEntry[] = []
|
|
23
|
+
protected errLog: ErrLogEntry[] = []
|
|
24
|
+
|
|
25
|
+
constructor(options: SchedulerOptions) {
|
|
26
|
+
this.options = options
|
|
27
|
+
|
|
28
|
+
this.isRunning = false
|
|
29
|
+
this.maxLogNumber = options.maxLogNumber ?? 100
|
|
30
|
+
this.timers = new Set()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addTask(task: Task): void {
|
|
34
|
+
this.options.tasks.push(task)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addRunLog(runLog: RunLogEntry): void {
|
|
38
|
+
this.runLog.push(runLog)
|
|
39
|
+
if (this.runLog.length > this.maxLogNumber) {
|
|
40
|
+
this.runLog = this.runLog.slice(this.maxLogNumber * -1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
addErrLog(errLog: ErrLogEntry): void {
|
|
45
|
+
this.errLog.push(errLog)
|
|
46
|
+
if (this.errLog.length > this.maxLogNumber) {
|
|
47
|
+
this.errLog = this.errLog.slice(this.maxLogNumber * -1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getRunLog(): RunLogEntry[] {
|
|
52
|
+
return this.runLog
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getErrLog(): ErrLogEntry[] {
|
|
56
|
+
return this.errLog
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected scheduleTask(task: Task): void {
|
|
60
|
+
const nextRun = task.getCron().nextRun()
|
|
61
|
+
if (nextRun === undefined) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const delay = Math.max(nextRun.getTime() - Date.now(), 0)
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
this.timers.delete(timer)
|
|
68
|
+
if (this.isRunning !== true) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.scheduleTask(task)
|
|
73
|
+
void task.run()
|
|
74
|
+
.catch((reason: unknown) => {
|
|
75
|
+
this.addErrLog({
|
|
76
|
+
name: task.getName(),
|
|
77
|
+
time: new Date(),
|
|
78
|
+
reason: String(reason),
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
.finally(() => {
|
|
82
|
+
this.addRunLog({
|
|
83
|
+
name: task.getName(),
|
|
84
|
+
time: new Date(),
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}, delay)
|
|
88
|
+
|
|
89
|
+
this.timers.add(timer)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
run(): void {
|
|
93
|
+
if (this.isRunning === true) {
|
|
94
|
+
throw new Error("Scheduler is already running")
|
|
95
|
+
} else {
|
|
96
|
+
this.isRunning = true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const task of this.options.tasks) {
|
|
100
|
+
this.scheduleTask(task)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isCron, Cron } from "#Source/cron/index.ts"
|
|
2
|
+
|
|
3
|
+
export interface TaskOptions {
|
|
4
|
+
name: string
|
|
5
|
+
cron: Cron | string | Date
|
|
6
|
+
run: (() => void) | (() => Promise<void>)
|
|
7
|
+
}
|
|
8
|
+
export class Task {
|
|
9
|
+
protected options: TaskOptions
|
|
10
|
+
protected cron: Cron
|
|
11
|
+
|
|
12
|
+
constructor(options: TaskOptions) {
|
|
13
|
+
this.options = options
|
|
14
|
+
if (isCron(options.cron) === true) {
|
|
15
|
+
this.cron = options.cron
|
|
16
|
+
} else {
|
|
17
|
+
this.cron = new Cron({ pattern: options.cron })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getName(): string {
|
|
22
|
+
return this.options.name
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getCron(): Cron {
|
|
26
|
+
return this.cron
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async run(): Promise<void> {
|
|
30
|
+
await this.options.run()
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/random/README.md
CHANGED
|
@@ -2,31 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Description
|
|
4
4
|
|
|
5
|
-
Random 模块用于提供随机值(random value
|
|
5
|
+
Random 模块用于提供随机值(random value)相关的基础能力,当前主要承载随机布尔值、随机字符串、随机数字与随机整数生成语义。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
它关注的是“如何生成一类通用、可复用、语义清楚的随机值”,而不是各种标识格式校验、业务编码规则或安全承诺各异的随机机制集合。
|
|
8
8
|
|
|
9
9
|
## For Understanding
|
|
10
10
|
|
|
11
11
|
理解 Random 模块时,应先把它放在应用边界附近。随机值通常意味着不可复现、带有环境依赖,或者会影响测试与调试行为,因此更合理的做法是把它看作边界输入的生成层,而不是让随机行为无约束地扩散到核心业务逻辑内部。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
当前这个模块的核心边界比较克制:它主要处理“随机布尔值如何生成”“随机字符串如何生成”“通用随机数字如何生成”与“通用随机整数如何生成”这一类问题。也就是说,它更关心 true 概率、字符集、长度、数值区间、整数边界、结果格式和重复概率,而不再负责 UUID 这类已经具有明确标识语义的格式模型。
|
|
14
14
|
|
|
15
15
|
不适合放入本模块的能力,通常包括两类:一类是与密码学安全直接绑定、需要单独安全承诺的能力;另一类是已经形成稳定标识语义的格式模型,例如 UUID。前者应以更明确的安全语义单独建模,后者则应放入更贴近标识问题域的模块。
|
|
16
16
|
|
|
17
17
|
## For Using
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
当你需要在应用或库的边界层生成随机布尔值、临时标识、关联值、会话键或其它非业务主键用途的随机值时,可以使用这个模块。它更适合被理解为“随机值生成接口”,而不是一个什么都往里塞的杂项工具箱。
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
从使用角度看,当前公共能力主要围绕随机布尔值、随机字符串、随机数字与随机整数生成展开。更合适的接入方式,是在边界层尽早生成这些随机值,再把得到的结果作为普通输入传给后续模块,从而避免内部逻辑直接依赖随机过程本身。
|
|
22
22
|
|
|
23
23
|
需要特别注意的是,本模块中的“随机”默认应被理解为通用应用层随机,而不是自动带有密码学强度承诺的安全随机。只要调用场景对安全性、可预测性、重复碰撞风险或跨环境一致性有更严格要求,就应先在设计上明确这些前提,而不要仅凭模块名称推断安全等级。
|
|
24
24
|
|
|
25
25
|
## For Contributing
|
|
26
26
|
|
|
27
|
-
贡献 Random
|
|
27
|
+
贡献 Random 模块时,应先确认新增能力表达的是稳定、清楚且可长期承诺的随机语义,而不是某种实现手段的直接暴露。对外公开的重点应该是“调用方需要什么随机值能力”,而不是“内部恰好如何生成这些值”。只要一个能力过度依赖当前实现细节,或者只在某个很窄的业务上下文中成立,就不适合进入这个模块。
|
|
28
28
|
|
|
29
|
-
在扩展时,应特别警惕两个方向的边界漂移。其一,不要把安全领域的问题用模糊措辞塞进来,例如把需要明确密码学承诺的能力混入通用随机工具中。其二,不要把已经形成稳定标识语义的格式模型重新拉回 Random。Random
|
|
29
|
+
在扩展时,应特别警惕两个方向的边界漂移。其一,不要把安全领域的问题用模糊措辞塞进来,例如把需要明确密码学承诺的能力混入通用随机工具中。其二,不要把已经形成稳定标识语义的格式模型重新拉回 Random。Random 模块应尽量保持为面向广泛调用方的随机值生成边界,而不是业务专用规则仓库。
|
|
30
30
|
|
|
31
31
|
### JSDoc 注释格式要求
|
|
32
32
|
|
|
@@ -75,4 +75,5 @@ Random 模块用于提供随机值(random value)相关的基础能力,当
|
|
|
75
75
|
- 测试顺序应与源文件中被测试目标的原始顺序保持一致。
|
|
76
76
|
- 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
|
|
77
77
|
- 模块的单元测试文件目录是 `./tests/unit/random`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/random/<sub-module-name>`。
|
|
78
|
+
- 对随机布尔值相关能力,应优先测试概率边界、结果类型与极端输入语义,而不要把测试建立在脆弱的统计分布断言之上。
|
|
78
79
|
- 对随机字符串相关能力,应优先测试长度约束、字符集边界与结果特征,而不要把测试建立在脆弱的精确随机输出之上。
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Uint32 的取值空间大小为 2^32。
|
|
2
|
+
// Random 模块中的基础随机换算都会用到这一个范围常量。
|
|
3
|
+
const internalRandomUint32Range = 0x1_0000_0000
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 取得一个随机 Uint32 值。
|
|
7
|
+
*
|
|
8
|
+
* 当运行时提供 `crypto.getRandomValues` 时,使用该随机源生成结果;否则回退到 `Math.random`。
|
|
9
|
+
* 该函数适合作为 Random 模块内外共享的基础随机整数入口。
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```
|
|
13
|
+
* const value = getRandomUint32()
|
|
14
|
+
* // Expect: true
|
|
15
|
+
* const example1 = Number.isInteger(value) && value >= 0 && value < 0x1_0000_0000
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const getRandomUint32 = (): number => {
|
|
19
|
+
if (globalThis.crypto !== undefined && typeof globalThis.crypto.getRandomValues === "function") {
|
|
20
|
+
const buffer = new Uint32Array(1)
|
|
21
|
+
globalThis.crypto.getRandomValues(buffer)
|
|
22
|
+
return buffer[0]!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Math.floor(Math.random() * internalRandomUint32Range)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 取得一个 `0` 到 `1` 之间的随机小数,包含下界,不包含上界。
|
|
30
|
+
*
|
|
31
|
+
* 该函数基于统一的 Uint32 随机源换算得到,适合作为随机数字与随机整数逻辑的公共基础。
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```
|
|
35
|
+
* const value = getRandomUnitValue()
|
|
36
|
+
* // Expect: true
|
|
37
|
+
* const example1 = value >= 0 && value < 1
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const getRandomUnitValue = (): number => {
|
|
41
|
+
return getRandomUint32() / internalRandomUint32Range
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 将随机整数均匀映射为索引。
|
|
46
|
+
*
|
|
47
|
+
* 若直接对 `alphabetLength` 取模,在长度不能整除 `2^32` 时会产生轻微偏差,因此这里使用拒绝采样,丢弃落在尾部残余区间的随机值。
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```
|
|
51
|
+
* const index = getRandomIndex(3)
|
|
52
|
+
* // Expect: true
|
|
53
|
+
* const example1 = Number.isInteger(index) && index >= 0 && index < 3
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const getRandomIndex = (length: number): number => {
|
|
57
|
+
const limit = internalRandomUint32Range - (internalRandomUint32Range % length)
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
const value = getRandomUint32()
|
|
61
|
+
|
|
62
|
+
if (value < limit) {
|
|
63
|
+
return value % length
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/random/index.ts
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getRandomUnitValue } from "./base.ts"
|
|
2
|
+
|
|
3
|
+
// 默认 true 概率为 0.5。
|
|
4
|
+
// 当调用方未传入参数时,randomBoolean 会生成一个均匀分布的随机布尔值。
|
|
5
|
+
const internalDefaultRandomBooleanProbability = 0.5
|
|
6
|
+
|
|
7
|
+
// Step 1: 校验 true 概率。
|
|
8
|
+
//
|
|
9
|
+
// randomBoolean 只接受 0 到 1 之间的有限数值,避免把无效概率带入比较逻辑。
|
|
10
|
+
const internalAssertRandomBooleanProbability = (value: number): void => {
|
|
11
|
+
if (Number.isFinite(value) === false) {
|
|
12
|
+
throw new TypeError(`Expected probability to be a finite number, got: ${value}`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (value < 0 || value > 1) {
|
|
16
|
+
throw new RangeError(`Expected probability to be between 0 and 1 inclusive, got: ${value}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 生成随机布尔值,并可选指定返回 `true` 的概率。
|
|
22
|
+
*
|
|
23
|
+
* 当未传入参数时,`true` 与 `false` 的概率各为一半;当传入 `probability` 时,会将其解释为返回 `true` 的概率,其中 `0` 表示恒为 `false`,`1` 表示恒为 `true`。
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```
|
|
27
|
+
* const sample1 = randomBoolean()
|
|
28
|
+
* // Expect: true
|
|
29
|
+
* const example1 = typeof sample1 === "boolean"
|
|
30
|
+
* // Expect: false
|
|
31
|
+
* const example2 = randomBoolean(0)
|
|
32
|
+
* // Expect: true
|
|
33
|
+
* const example3 = randomBoolean(1)
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const randomBoolean = (probability: number = internalDefaultRandomBooleanProbability): boolean => {
|
|
37
|
+
internalAssertRandomBooleanProbability(probability)
|
|
38
|
+
|
|
39
|
+
return getRandomUnitValue() < probability
|
|
40
|
+
}
|