@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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表示最小堆中的一个节点。
|
|
3
|
+
*
|
|
4
|
+
* `name` 用于在 `indexMap` 中快速定位节点,
|
|
5
|
+
* `endAt` 用作最小堆的排序键,值越小表示越早到期,优先级越高。
|
|
6
|
+
*/
|
|
7
|
+
interface HeapNode<Name extends string> {
|
|
8
|
+
name: Name
|
|
9
|
+
endAt: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 提供一个按 `endAt` 升序排列的最小堆。
|
|
14
|
+
*
|
|
15
|
+
* 该实现同时维护:
|
|
16
|
+
* 1. `heap`:保存二叉堆结构本身,用于快速获取最小值。
|
|
17
|
+
* 2. `indexMap`:记录名称到数组索引的映射,用于按名称 $O(1)$ 定位节点,
|
|
18
|
+
* 从而支持高效的更新和删除。
|
|
19
|
+
*
|
|
20
|
+
* 这个堆特别适合“按时间先后触发”的场景,例如过期调度器。
|
|
21
|
+
*/
|
|
22
|
+
export class MinHeap<Name extends string> {
|
|
23
|
+
/**
|
|
24
|
+
* 使用数组存储完全二叉树结构。
|
|
25
|
+
* 父子节点关系遵循:
|
|
26
|
+
* - parent = Math.floor((index - 1) / 2)
|
|
27
|
+
* - left = index * 2 + 1
|
|
28
|
+
* - right = index * 2 + 2
|
|
29
|
+
*/
|
|
30
|
+
private heap: Array<HeapNode<Name>> = []
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 记录节点名称在 `heap` 数组中的当前位置。
|
|
34
|
+
*
|
|
35
|
+
* 这样删除指定名称的节点时,不需要线性扫描整个堆。
|
|
36
|
+
*/
|
|
37
|
+
private indexMap = new Map<Name, number>()
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 返回当前堆中的节点数量。
|
|
41
|
+
*/
|
|
42
|
+
size(): number {
|
|
43
|
+
return this.heap.length
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 查看堆顶节点但不移除。
|
|
48
|
+
*
|
|
49
|
+
* 堆顶始终是当前 `endAt` 最小的节点,也就是最早应被处理的节点。
|
|
50
|
+
*/
|
|
51
|
+
peek(): HeapNode<Name> | undefined {
|
|
52
|
+
return this.heap[0]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 向堆中插入一个新节点。
|
|
57
|
+
*
|
|
58
|
+
* 新节点会先追加到数组末尾,再通过上浮恢复最小堆性质。
|
|
59
|
+
*/
|
|
60
|
+
push(node: HeapNode<Name>): void {
|
|
61
|
+
const index = this.heap.length
|
|
62
|
+
this.heap.push(node)
|
|
63
|
+
this.indexMap.set(node.name, index)
|
|
64
|
+
this.bubbleUp(index)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 按名称移除一个节点。
|
|
69
|
+
*
|
|
70
|
+
* 过程如下:
|
|
71
|
+
* 1. 借助 `indexMap` 找到目标节点位置。
|
|
72
|
+
* 2. 用最后一个节点填补被删除的位置。
|
|
73
|
+
* 3. 根据替换节点与父/子节点的大小关系,分别尝试下沉和上浮,恢复堆结构。
|
|
74
|
+
*/
|
|
75
|
+
remove(name: Name): void {
|
|
76
|
+
const index = this.indexMap.get(name)
|
|
77
|
+
if (index === undefined) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const last = this.heap.pop()!
|
|
82
|
+
this.indexMap.delete(name)
|
|
83
|
+
|
|
84
|
+
// 被删除节点本来就在数组末尾时,弹出后无需再做任何调整。
|
|
85
|
+
if (index === this.heap.length) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.heap[index] = last
|
|
90
|
+
this.indexMap.set(last.name, index)
|
|
91
|
+
|
|
92
|
+
// 替换进来的节点可能比子节点大,也可能比父节点小,
|
|
93
|
+
// 因此两个方向都尝试一次,以覆盖所有位置变化情况。
|
|
94
|
+
this.bubbleDown(index)
|
|
95
|
+
this.bubbleUp(index)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 移除并返回堆顶节点。
|
|
100
|
+
*
|
|
101
|
+
* 当堆为空时返回 `undefined`。
|
|
102
|
+
*/
|
|
103
|
+
pop(): HeapNode<Name> | undefined {
|
|
104
|
+
if (this.heap.length === 0) {
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const top = this.heap[0]
|
|
109
|
+
if (top === undefined) {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.remove(top.name)
|
|
114
|
+
return top
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 交换两个索引位置上的节点,并同步更新它们在 `indexMap` 中的记录。
|
|
119
|
+
*/
|
|
120
|
+
private swap(i: number, j: number): void {
|
|
121
|
+
const a = this.heap[i]
|
|
122
|
+
const b = this.heap[j]
|
|
123
|
+
|
|
124
|
+
if (a === undefined || b === undefined) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.heap[i] = b
|
|
129
|
+
this.heap[j] = a
|
|
130
|
+
|
|
131
|
+
this.indexMap.set(a.name, j)
|
|
132
|
+
this.indexMap.set(b.name, i)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 让指定索引处的节点持续向上移动,直到满足最小堆性质。
|
|
137
|
+
*
|
|
138
|
+
* 适用于插入新节点,或某个节点的排序键变小之后的重排。
|
|
139
|
+
*/
|
|
140
|
+
private bubbleUp(index: number): void {
|
|
141
|
+
let currentIndex = index
|
|
142
|
+
|
|
143
|
+
while (currentIndex > 0) {
|
|
144
|
+
const parent = Math.floor((currentIndex - 1) / 2)
|
|
145
|
+
const parentNode = this.heap[parent]
|
|
146
|
+
const currentNode = this.heap[currentIndex]
|
|
147
|
+
|
|
148
|
+
if (parentNode === undefined || currentNode === undefined) {
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (parentNode.endAt <= currentNode.endAt) {
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.swap(parent, currentIndex)
|
|
157
|
+
currentIndex = parent
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 让指定索引处的节点持续向下移动,直到满足最小堆性质。
|
|
163
|
+
*
|
|
164
|
+
* 每次从当前节点与左右子节点中选出 `endAt` 最小者,
|
|
165
|
+
* 若最小者不是当前节点,则交换并继续向下检查。
|
|
166
|
+
*/
|
|
167
|
+
private bubbleDown(index: number): void {
|
|
168
|
+
const length = this.heap.length
|
|
169
|
+
let currentIndex = index
|
|
170
|
+
|
|
171
|
+
while (true) {
|
|
172
|
+
let smallest = currentIndex
|
|
173
|
+
const left = currentIndex * 2 + 1
|
|
174
|
+
const right = currentIndex * 2 + 2
|
|
175
|
+
const smallestNode = this.heap[smallest]
|
|
176
|
+
const leftNode = this.heap[left]
|
|
177
|
+
const rightNode = this.heap[right]
|
|
178
|
+
|
|
179
|
+
if (smallestNode === undefined) {
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 先用左子节点与当前最小值比较。
|
|
184
|
+
if (left < length && leftNode !== undefined && leftNode.endAt < smallestNode.endAt) {
|
|
185
|
+
smallest = left
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const nextSmallestNode = this.heap[smallest]
|
|
189
|
+
|
|
190
|
+
// 再基于最新的最小候选与右子节点比较。
|
|
191
|
+
if (
|
|
192
|
+
right < length &&
|
|
193
|
+
rightNode !== undefined &&
|
|
194
|
+
nextSmallestNode !== undefined &&
|
|
195
|
+
rightNode.endAt < nextSmallestNode.endAt
|
|
196
|
+
) {
|
|
197
|
+
smallest = right
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (smallest === currentIndex) {
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.swap(currentIndex, smallest)
|
|
205
|
+
currentIndex = smallest
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { EventManager } from "#Source/event/index.ts"
|
|
2
|
+
|
|
3
|
+
import type { BuildEvents, SubscriberEntry } from "#Source/event/index.ts"
|
|
4
|
+
import type { ExpirationManager, ExpirationManagerEvents } from "./expiration-manager.ts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 表示一个以秒为单位的剩余时长。
|
|
8
|
+
*/
|
|
9
|
+
export type RemainingTime = number
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 描述一组按名称索引的派生剩余时长字典。
|
|
13
|
+
*/
|
|
14
|
+
export type RemainingDict<ExpirationName extends string> = {
|
|
15
|
+
[K in ExpirationName]?: RemainingTime | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 描述 `RemainingManager` 对外发出的事件表。
|
|
20
|
+
*/
|
|
21
|
+
export type RemainingManagerEvents<ExpirationName extends string> = BuildEvents<{
|
|
22
|
+
/**
|
|
23
|
+
* 在剩余时间快照发生变化时发出最新结果。
|
|
24
|
+
*/
|
|
25
|
+
remaining: (remaining: RemainingDict<ExpirationName>) => void
|
|
26
|
+
}>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 描述创建 `RemainingManager` 时可用的配置项。
|
|
30
|
+
*/
|
|
31
|
+
export interface RemainingManagerOptions<ExpirationName extends string> {
|
|
32
|
+
/**
|
|
33
|
+
* 提供过期状态真相来源的过期管理器。
|
|
34
|
+
*/
|
|
35
|
+
expirationManager: ExpirationManager<ExpirationName>
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 控制是否在创建后立即启用周期性剩余时间检查。
|
|
39
|
+
*/
|
|
40
|
+
enabled?: boolean | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isRemainingDictEqual = <ExpirationName extends string>(
|
|
44
|
+
left: RemainingDict<ExpirationName>,
|
|
45
|
+
right: RemainingDict<ExpirationName>,
|
|
46
|
+
): boolean => {
|
|
47
|
+
const leftKeys = Object.keys(left)
|
|
48
|
+
const rightKeys = Object.keys(right)
|
|
49
|
+
|
|
50
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const key of leftKeys) {
|
|
55
|
+
// oxlint-disable-next-line no-unsafe-type-assertion
|
|
56
|
+
const expirationName = key as ExpirationName
|
|
57
|
+
|
|
58
|
+
if (left[expirationName] !== right[expirationName]) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 管理基于 `ExpirationManager` 状态派生出的剩余时间查询与周期性通知。
|
|
68
|
+
*
|
|
69
|
+
* `RemainingManager` 不拥有独立的过期真相来源,而是始终从 `ExpirationManager`
|
|
70
|
+
* 读取当前过期状态,并在需要时把这些状态换算为以秒为单位的剩余值。
|
|
71
|
+
*
|
|
72
|
+
* 这里的 `enabled` 只影响内部的周期性检查是否运行,不影响过期状态变化时
|
|
73
|
+
* 立即触发的派生结果更新。
|
|
74
|
+
*/
|
|
75
|
+
export class RemainingManager<ExpirationName extends string = string> {
|
|
76
|
+
readonly expirationManager: ExpirationManager<ExpirationName>
|
|
77
|
+
readonly event: EventManager<RemainingManagerEvents<ExpirationName>>
|
|
78
|
+
|
|
79
|
+
private enabled: boolean
|
|
80
|
+
private lastEmittedRemainingSnapshot: RemainingDict<ExpirationName>
|
|
81
|
+
private timer: ReturnType<typeof setInterval> | null
|
|
82
|
+
private terminated: boolean
|
|
83
|
+
private readonly expirationStateSubscriberEntry: SubscriberEntry<
|
|
84
|
+
ExpirationManagerEvents<ExpirationName>,
|
|
85
|
+
"expirationState"
|
|
86
|
+
>
|
|
87
|
+
|
|
88
|
+
constructor(options: RemainingManagerOptions<ExpirationName>) {
|
|
89
|
+
const { expirationManager, enabled } = options
|
|
90
|
+
|
|
91
|
+
this.expirationManager = expirationManager
|
|
92
|
+
this.event = new EventManager()
|
|
93
|
+
|
|
94
|
+
this.enabled = enabled === true
|
|
95
|
+
this.lastEmittedRemainingSnapshot = {}
|
|
96
|
+
this.timer = null
|
|
97
|
+
this.terminated = false
|
|
98
|
+
this.expirationStateSubscriberEntry = this.expirationManager.event.subscribe(
|
|
99
|
+
"expirationState",
|
|
100
|
+
(): void => {
|
|
101
|
+
this.emit()
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (this.enabled === true) {
|
|
106
|
+
this.resume()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 返回某个具名过期项当前的秒级剩余时长。
|
|
112
|
+
*
|
|
113
|
+
* 当目标不存在,或其状态不是 `active` 时,返回 `undefined`。
|
|
114
|
+
*/
|
|
115
|
+
getRemaining(name: ExpirationName): number | undefined {
|
|
116
|
+
const expirationState = this.expirationManager.getExpirationState(name)
|
|
117
|
+
if (expirationState === undefined) {
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
120
|
+
if (expirationState.state !== "active") {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Math.max(0, Math.ceil((expirationState.endAt - this.expirationManager.getNow()) / 1_000))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 返回当前所有活跃过期项的秒级剩余时长快照。
|
|
129
|
+
*/
|
|
130
|
+
getRemainingSnapshot(): RemainingDict<ExpirationName> {
|
|
131
|
+
const expirationDict = this.expirationManager.getExpirationSnapshot()
|
|
132
|
+
const result: RemainingDict<ExpirationName> = {}
|
|
133
|
+
|
|
134
|
+
for (const name in expirationDict) {
|
|
135
|
+
if (Object.hasOwn(expirationDict, name) === false) {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const expirationName = name as ExpirationName
|
|
140
|
+
const remaining = this.getRemaining(expirationName)
|
|
141
|
+
|
|
142
|
+
if (remaining === undefined) {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
result[expirationName] = remaining
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 在剩余时间快照发生变化时发出一次 `remaining` 事件。
|
|
154
|
+
*
|
|
155
|
+
* 若当前实例已经终止,或新旧快照完全一致,则不会重复发出事件。
|
|
156
|
+
*/
|
|
157
|
+
emit(): void {
|
|
158
|
+
if (this.terminated === true) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const remainingSnapshot = this.getRemainingSnapshot()
|
|
163
|
+
if (isRemainingDictEqual(this.lastEmittedRemainingSnapshot, remainingSnapshot) === true) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.lastEmittedRemainingSnapshot = remainingSnapshot
|
|
168
|
+
this.event.emit("remaining", remainingSnapshot)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 启用周期性剩余时间检查。
|
|
173
|
+
*
|
|
174
|
+
* 这不会跳过过期状态变更时本来就会触发的即时派生通知。
|
|
175
|
+
*/
|
|
176
|
+
start(): void {
|
|
177
|
+
if (this.terminated === true) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.enabled = true
|
|
182
|
+
this.resume()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 停止周期性剩余时间检查,并将启用标记设为关闭。
|
|
187
|
+
*/
|
|
188
|
+
stop(): void {
|
|
189
|
+
this.enabled = false
|
|
190
|
+
|
|
191
|
+
if (this.timer === null) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
clearInterval(this.timer)
|
|
196
|
+
this.timer = null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 暂停当前周期性定时器,但保留启用标记。
|
|
201
|
+
*
|
|
202
|
+
* 这通常用于由外层过期管理器在暂停期间临时停止派生检查,
|
|
203
|
+
* 以便之后通过 `resume()` 继续运行。
|
|
204
|
+
*/
|
|
205
|
+
pause(): void {
|
|
206
|
+
if (this.timer !== null) {
|
|
207
|
+
clearInterval(this.timer)
|
|
208
|
+
this.timer = null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 在实例已启用且尚未终止时恢复周期性剩余时间检查。
|
|
214
|
+
*/
|
|
215
|
+
resume(): void {
|
|
216
|
+
if (this.terminated === true || this.enabled !== true || this.timer !== null) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.timer = setInterval(() => {
|
|
221
|
+
if (Object.keys(this.expirationManager.getExpirationSnapshot()).length === 0) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.emit()
|
|
226
|
+
}, 1_000)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 终止实例并释放订阅与定时器资源。
|
|
231
|
+
*/
|
|
232
|
+
terminate(): void {
|
|
233
|
+
if (this.terminated === true) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.terminated = true
|
|
238
|
+
this.expirationStateSubscriberEntry.unsubscribe()
|
|
239
|
+
this.stop()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./expiration/index.ts"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Tube
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Tube 模块提供围绕运行时数据通道(runtime data conduit)的通用建模能力,用于把一段会经历开启、启动、接收数据、出现错误、结束与关闭等生命周期的数据传递过程,组织为可观察、可组合且可选择重放历史记录的稳定公共语义。
|
|
6
|
+
|
|
7
|
+
它关注的不是某个具体传输协议、宿主流实现或消息中间件接口本身,而是“当一段数据传递过程需要在运行时被明确建模为一个有生命周期、有事件面、可回放最近数据、并可与其它边界协作的通道对象时,应暴露什么样的长期语义”这一类更一般的问题。因此,这个模块更适合被理解为一组数据通道模型,而不是事件总线工具箱、Node.js Stream 包装层,或为了临时转发数据而拼接出来的便捷函数集合。
|
|
8
|
+
|
|
9
|
+
## For Understanding
|
|
10
|
+
|
|
11
|
+
理解 Tube 模块时,应先把它看作“有生命周期的数据通道”建模层,而不是把任何会发出事件或传递值的对象都简单归并到这里。它真正要表达的,不只是“某个值能不能被推送出去”,还包括一段通道是否已经打开、是否已经开始接收数据、是否已经变为非空、是否已经发生错误、是否已经结束,以及这些状态变化如何以清楚的事件面暴露给外部。
|
|
12
|
+
|
|
13
|
+
Tube 的核心价值,在于把一段原本容易散落在回调、标志位和局部变量里的通道过程,收束为一份稳定模型。这样做的重点不是单纯提供一个 `pushData` 方法,而是让“生命周期顺序”“数据到达语义”“错误传播语义”“历史回放语义”“与其它通道或宿主流互转的边界”都可以被统一理解和长期维护。
|
|
14
|
+
|
|
15
|
+
理解这个模块时,特别要守住几条边界:
|
|
16
|
+
|
|
17
|
+
- Tube 模块表达的是运行时数据通道及其生命周期,而不是传输协议、Socket 连接、HTTP 请求、文件读取或浏览器流 API 本身。
|
|
18
|
+
- 它适合描述通道何时开始可接收数据、何时真正进入流动状态、何时首次变为非空、何时结束以及如何对这些变化做出订阅。
|
|
19
|
+
- 它可以与 `ReadableStream` 等宿主流结构互相转换,也可以连接上下游通道,但这些都应被理解为围绕通道模型发生的集成接点,而不是让宿主 API 细节反过来定义 Tube 的公共语义。
|
|
20
|
+
- 它不应直接承担背压(backpressure)协商、传输重试、协议握手、序列化格式、连接保活、分布式消息确认或业务专属路由规则等职责。
|
|
21
|
+
- 数据历史回放的价值,在于帮助新订阅者接入一个已经运行中的通道,而不是把 Tube 退化成无限责任的缓存容器;因此扩展时应优先围绕“通道中的历史视图”思考,而不是围绕“持久存储”思考。
|
|
22
|
+
|
|
23
|
+
## For Using
|
|
24
|
+
|
|
25
|
+
当你希望在应用内部表达一段真正有生命周期的数据传递过程,而不是把数据流转逻辑拆散到多个事件回调、Promise 链或宿主流对象的临时包装里时,可以使用 Tube 模块。
|
|
26
|
+
|
|
27
|
+
从使用角度看,这个模块大致可以分为三类能力:
|
|
28
|
+
|
|
29
|
+
- 生命周期控制能力:用于表达通道的打开、启动、结束、关闭以及错误发生后的自动联动行为,适合把一段通道过程收束为稳定状态机。
|
|
30
|
+
- 事件与数据订阅能力:用于观察 `open`、`start`、`wet`、`error`、`end`、`close` 与数据到达本身,并根据需要为新订阅者重放最近的历史数据。
|
|
31
|
+
- 集成辅助能力:用于连接上下游 Tube、把 Tube 收束为 Promise,或与 `ReadableStream` 等宿主流结构互相转换。
|
|
32
|
+
|
|
33
|
+
更合适的接入方式,通常是先判断你的核心对象是不是“一段需要被长期观察和组合的数据通道过程”。如果你需要的是:
|
|
34
|
+
|
|
35
|
+
- 某个生产者持续把数据推给多个订阅方,并且这些订阅方还需要感知通道生命周期;
|
|
36
|
+
- 一个已经开始流动的通道要允许新接入者按需看到最近历史,而不是只看到未来数据;
|
|
37
|
+
- 你希望把宿主流结构转换为一份带稳定生命周期语义的对象,再与其它模型协作;
|
|
38
|
+
- 上下游两个处理阶段之间需要一个清楚的数据通道边界,而不是若干零散回调直接耦合;
|
|
39
|
+
|
|
40
|
+
那么这个模块就是合适的边界。
|
|
41
|
+
|
|
42
|
+
相反,如果你的需求只是宿主环境里某个具体流 API 的底层控制、传输协议细节、连接状态机、消息编码格式、流量控制或业务专属转发规则,那么这些通常不应直接落在 Tube 模块本身,而应放在对应宿主模块、适配层或更外层系统中。
|
|
43
|
+
|
|
44
|
+
## For Contributing
|
|
45
|
+
|
|
46
|
+
为 Tube 模块贡献内容时,优先判断新增能力是否真的在澄清“运行时数据通道如何被建模为稳定生命周期对象”这一问题,而不是只是在补一个临时好用的转发函数或宿主 API 包装。这个模块应长期服务于“通道状态如何表达”“数据与错误如何被送入并传播”“订阅者如何观察生命周期与数据”“历史如何被有边界地回放”“通道如何与其它模型形成集成接点”这几类问题。
|
|
47
|
+
|
|
48
|
+
扩展这个模块时,应特别警惕以下倾向:把 Tube 退化成普通事件总线;把某个宿主流实现的局部行为直接暴露为公共承诺;把连接、协议、背压、重试、路由或业务缓存逻辑无差别地下沉到 Tube 内部;或者为了当前实现方便而公开内部状态结构。文档应说明哪些通道语义是长期成立的,以及为什么这些语义成立,而不是复述某个版本里恰好采用的局部实现技巧。
|
|
49
|
+
|
|
50
|
+
当前已经落地的 `Tube` 应被理解为 Tube 模块下的核心通道模型,而 `connectTube`、`tubeToPromise`、`tubeToReadableStream` 与 `readableStreamToTube` 则是围绕这一模型形成的集成辅助函数。后续若增加子模块或更多辅助能力,应优先判断它们是否仍然服务于“清楚、稳定的数据通道语义”,而不是把其它更大的问题域误并到这个模块里。
|
|
51
|
+
|
|
52
|
+
### JSDoc 注释格式要求
|
|
53
|
+
|
|
54
|
+
- 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
|
|
55
|
+
- JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
|
|
56
|
+
- 如果描述后还有其他内容,应在描述后加一个空行。
|
|
57
|
+
- 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
|
|
58
|
+
- 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
|
|
59
|
+
- 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
|
|
60
|
+
- 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
|
|
61
|
+
- 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
|
|
62
|
+
- 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
|
|
63
|
+
- 如果函数返回结构化字符串,应展示其预期格式特征。
|
|
64
|
+
- 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
|
|
65
|
+
|
|
66
|
+
### 实现规范要求
|
|
67
|
+
|
|
68
|
+
- 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
|
|
69
|
+
- 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
|
|
70
|
+
- 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
|
|
71
|
+
- 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
|
|
72
|
+
- 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
|
|
73
|
+
- 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
|
|
74
|
+
- 辅助元素永远不要公开导出。
|
|
75
|
+
- 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/tube/internal.ts`。
|
|
76
|
+
- 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
|
|
77
|
+
- 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
|
|
78
|
+
- 子模块不需要有自己的 `README.md`。
|
|
79
|
+
- 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
|
|
80
|
+
- 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
|
|
81
|
+
- 与 tube 相关的实现应优先围绕生命周期、数据进入语义、错误传播、历史回放与集成接点组织,避免把协议细节、连接控制或宿主流底层策略直接混入模块公共边界。
|
|
82
|
+
|
|
83
|
+
### 导出策略要求
|
|
84
|
+
|
|
85
|
+
- 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
|
|
86
|
+
- 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
|
|
87
|
+
- Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
|
|
88
|
+
- 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的通道语义,而不是某段宿主实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
|
|
89
|
+
|
|
90
|
+
### 测试要求
|
|
91
|
+
|
|
92
|
+
- 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
|
|
93
|
+
- 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
|
|
94
|
+
- 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
|
|
95
|
+
- 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
|
|
96
|
+
- 测试顺序应与源文件中被测试目标的原始顺序保持一致。
|
|
97
|
+
- 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
|
|
98
|
+
- 模块的单元测试文件目录是 `./tests/unit/tube`。
|
|
99
|
+
- 对这个模块来说,测试应优先覆盖生命周期切换、自动联动配置、历史回放、错误传播、订阅与退订行为,以及 Tube 与其它 Tube 或宿主 `ReadableStream` 之间的集成协同行为。
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ReadableStream } from "node:stream/web"
|
|
2
|
+
|
|
3
|
+
import { streamConsumeInMacroTask, toError } from "#Source/basic/index.ts"
|
|
4
|
+
import { Tube } from "./tube.ts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 将上游 Tube 的生命周期、错误与数据转发到下游 Tube。
|
|
8
|
+
*/
|
|
9
|
+
export const connectTube = <D, E = Error>(
|
|
10
|
+
upstream: Tube<D, E>,
|
|
11
|
+
downstream: Tube<D, E>,
|
|
12
|
+
): (() => void) => {
|
|
13
|
+
const openEventSubscriber = async (): Promise<void> => {
|
|
14
|
+
await downstream.open()
|
|
15
|
+
}
|
|
16
|
+
upstream.subscribeOpenEvent({ subscriber: openEventSubscriber })
|
|
17
|
+
const closeEventSubscriber = async (): Promise<void> => {
|
|
18
|
+
await downstream.close()
|
|
19
|
+
}
|
|
20
|
+
upstream.subscribeCloseEvent({ subscriber: closeEventSubscriber })
|
|
21
|
+
const startEventSubscriber = async (): Promise<void> => {
|
|
22
|
+
await downstream.start()
|
|
23
|
+
}
|
|
24
|
+
upstream.subscribeStartEvent({ subscriber: startEventSubscriber })
|
|
25
|
+
const endEventSubscriber = async (): Promise<void> => {
|
|
26
|
+
await downstream.end()
|
|
27
|
+
}
|
|
28
|
+
upstream.subscribeEndEvent({ subscriber: endEventSubscriber })
|
|
29
|
+
const errorEventSubscriber = async (error: E): Promise<void> => {
|
|
30
|
+
await downstream.pushError(error)
|
|
31
|
+
}
|
|
32
|
+
upstream.subscribeErrorEvent({ subscriber: errorEventSubscriber })
|
|
33
|
+
const dataSubscriber = async (data: D): Promise<void> => {
|
|
34
|
+
await downstream.pushData(data)
|
|
35
|
+
}
|
|
36
|
+
upstream.subscribeData({ subscriber: dataSubscriber })
|
|
37
|
+
|
|
38
|
+
return (): void => {
|
|
39
|
+
upstream.unsubscribeOpenEvent(openEventSubscriber)
|
|
40
|
+
upstream.unsubscribeCloseEvent(closeEventSubscriber)
|
|
41
|
+
upstream.unsubscribeStartEvent(startEventSubscriber)
|
|
42
|
+
upstream.unsubscribeEndEvent(endEventSubscriber)
|
|
43
|
+
upstream.unsubscribeErrorEvent(errorEventSubscriber)
|
|
44
|
+
upstream.unsubscribeData(dataSubscriber)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 把 Tube 收束为一个 Promise,并在通道关闭时以最后一条数据或最新错误结算。
|
|
50
|
+
*/
|
|
51
|
+
export const tubeToPromise = async <D, E = Error>(
|
|
52
|
+
tube: Tube<D, E>,
|
|
53
|
+
): Promise<D> => {
|
|
54
|
+
const promise = new Promise<D>((resolve, reject) => {
|
|
55
|
+
const handle = (): void => {
|
|
56
|
+
if (tube.isError()) {
|
|
57
|
+
reject(toError(tube.safeGetLatestError()))
|
|
58
|
+
unsubscribe()
|
|
59
|
+
}
|
|
60
|
+
if (tube.hasClosed()) {
|
|
61
|
+
if (tube.isWet()) {
|
|
62
|
+
resolve(tube.safeGetLatestData())
|
|
63
|
+
unsubscribe()
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
reject(new Error("No data available"))
|
|
67
|
+
unsubscribe()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const unsubscribe = tube.subscribeCloseEvent({
|
|
72
|
+
subscriber: () => {
|
|
73
|
+
handle()
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
// invoke once
|
|
77
|
+
handle()
|
|
78
|
+
})
|
|
79
|
+
return await promise
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 把 Tube 暴露为一个 `ReadableStream`。
|
|
84
|
+
*/
|
|
85
|
+
export const tubeToReadableStream = <D, E = Error>(
|
|
86
|
+
tube: Tube<D, E>,
|
|
87
|
+
): ReadableStream<D> => {
|
|
88
|
+
const readableStream = new ReadableStream<D>({
|
|
89
|
+
start(controller): void {
|
|
90
|
+
const closeEventSubscriber = (): void => {
|
|
91
|
+
controller.close()
|
|
92
|
+
unsubscribeAll()
|
|
93
|
+
}
|
|
94
|
+
const unsubscribeCloseEvent = tube.subscribeCloseEvent({ subscriber: closeEventSubscriber })
|
|
95
|
+
const errorEventSubscriber = (error: E): void => {
|
|
96
|
+
controller.error(error)
|
|
97
|
+
}
|
|
98
|
+
const unsubscribeErrorEvent = tube.subscribeErrorEvent({ subscriber: errorEventSubscriber })
|
|
99
|
+
const dataSubscriber = (data: D): void => {
|
|
100
|
+
controller.enqueue(data)
|
|
101
|
+
}
|
|
102
|
+
const unsubscribeDataSubscriber = tube.subscribeData({ subscriber: dataSubscriber })
|
|
103
|
+
|
|
104
|
+
const unsubscribeAll = (): void => {
|
|
105
|
+
unsubscribeCloseEvent()
|
|
106
|
+
unsubscribeErrorEvent()
|
|
107
|
+
unsubscribeDataSubscriber()
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
return readableStream
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 把 `ReadableStream` 转换为一个立即开始消费上游数据的 Tube。
|
|
116
|
+
*
|
|
117
|
+
* 调用后会立刻开始消费 `ReadableStream`,并将数据推入返回的 Tube。
|
|
118
|
+
*/
|
|
119
|
+
export const readableStreamToTube = <D, E = Error>(
|
|
120
|
+
readableStream: ReadableStream<D>,
|
|
121
|
+
): Tube<D, E> => {
|
|
122
|
+
const tube = new Tube<D, E>({
|
|
123
|
+
historyCount: Infinity,
|
|
124
|
+
replayHistory: true,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// NOTE: 这里确保 chunk 的消费和 Tube 的回调穿插在宏任务队列中执行,
|
|
128
|
+
// 而不是所有 chunk 都获取完毕之后才开始处理 Tube 回调。
|
|
129
|
+
streamConsumeInMacroTask({
|
|
130
|
+
readableStream,
|
|
131
|
+
onValue: async chunk => await tube.pushData(chunk),
|
|
132
|
+
onDone: async () => await tube.close(),
|
|
133
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
134
|
+
onError: async error => await tube.pushError(error as E),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return tube
|
|
138
|
+
}
|