@planet-matrix/mobius-model 0.5.0 → 0.6.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.
Files changed (175) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +123 -36
  3. package/dist/index.js +45 -4
  4. package/dist/index.js.map +183 -11
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +16 -10
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/basic/README.md +69 -118
  12. package/src/basic/function.ts +81 -62
  13. package/src/basic/is.ts +152 -71
  14. package/src/basic/promise.ts +29 -8
  15. package/src/basic/string.ts +2 -33
  16. package/src/color/README.md +105 -0
  17. package/src/color/index.ts +3 -0
  18. package/src/color/internal.ts +42 -0
  19. package/src/color/rgb/analyze.ts +236 -0
  20. package/src/color/rgb/construct.ts +130 -0
  21. package/src/color/rgb/convert.ts +227 -0
  22. package/src/color/rgb/derive.ts +303 -0
  23. package/src/color/rgb/index.ts +6 -0
  24. package/src/color/rgb/internal.ts +208 -0
  25. package/src/color/rgb/parse.ts +302 -0
  26. package/src/color/rgb/serialize.ts +144 -0
  27. package/src/color/types.ts +57 -0
  28. package/src/color/xyz/analyze.ts +80 -0
  29. package/src/color/xyz/construct.ts +19 -0
  30. package/src/color/xyz/convert.ts +71 -0
  31. package/src/color/xyz/index.ts +3 -0
  32. package/src/color/xyz/internal.ts +23 -0
  33. package/src/css/README.md +93 -0
  34. package/src/css/class.ts +559 -0
  35. package/src/css/index.ts +1 -0
  36. package/src/encoding/README.md +66 -79
  37. package/src/encoding/base64.ts +13 -4
  38. package/src/environment/README.md +97 -0
  39. package/src/environment/basic.ts +26 -0
  40. package/src/environment/device.ts +311 -0
  41. package/src/environment/feature.ts +285 -0
  42. package/src/environment/geo.ts +337 -0
  43. package/src/environment/index.ts +7 -0
  44. package/src/environment/runtime.ts +400 -0
  45. package/src/environment/snapshot.ts +60 -0
  46. package/src/environment/variable.ts +239 -0
  47. package/src/event/README.md +90 -0
  48. package/src/event/class-event-proxy.ts +228 -0
  49. package/src/event/common.ts +19 -0
  50. package/src/event/event-manager.ts +203 -0
  51. package/src/event/index.ts +4 -0
  52. package/src/event/instance-event-proxy.ts +186 -0
  53. package/src/event/internal.ts +24 -0
  54. package/src/exception/README.md +96 -0
  55. package/src/exception/browser.ts +219 -0
  56. package/src/exception/index.ts +4 -0
  57. package/src/exception/nodejs.ts +169 -0
  58. package/src/exception/normalize.ts +106 -0
  59. package/src/exception/types.ts +99 -0
  60. package/src/identifier/README.md +92 -0
  61. package/src/identifier/id.ts +119 -0
  62. package/src/identifier/index.ts +2 -0
  63. package/src/identifier/uuid.ts +187 -0
  64. package/src/index.ts +16 -1
  65. package/src/log/README.md +79 -0
  66. package/src/log/index.ts +5 -0
  67. package/src/log/log-emitter.ts +72 -0
  68. package/src/log/log-record.ts +10 -0
  69. package/src/log/log-scheduler.ts +74 -0
  70. package/src/log/log-type.ts +8 -0
  71. package/src/log/logger.ts +543 -0
  72. package/src/orchestration/README.md +89 -0
  73. package/src/orchestration/coordination/barrier.ts +214 -0
  74. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  75. package/src/orchestration/coordination/errors.ts +98 -0
  76. package/src/orchestration/coordination/index.ts +16 -0
  77. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  78. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  79. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  80. package/src/orchestration/coordination/mutex.ts +257 -0
  81. package/src/orchestration/coordination/permit.ts +127 -0
  82. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  83. package/src/orchestration/coordination/semaphore.ts +280 -0
  84. package/src/orchestration/index.ts +1 -0
  85. package/src/random/README.md +55 -86
  86. package/src/random/index.ts +1 -1
  87. package/src/random/string.ts +35 -0
  88. package/src/reactor/README.md +4 -0
  89. package/src/reactor/reactor-core/primitive.ts +9 -9
  90. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  91. package/src/singleton/README.md +79 -0
  92. package/src/singleton/factory.ts +55 -0
  93. package/src/singleton/index.ts +2 -0
  94. package/src/singleton/manager.ts +204 -0
  95. package/src/storage/README.md +107 -0
  96. package/src/storage/index.ts +1 -0
  97. package/src/storage/table.ts +449 -0
  98. package/src/timer/README.md +86 -0
  99. package/src/timer/expiration/expiration-manager.ts +594 -0
  100. package/src/timer/expiration/index.ts +3 -0
  101. package/src/timer/expiration/min-heap.ts +208 -0
  102. package/src/timer/expiration/remaining-manager.ts +241 -0
  103. package/src/timer/index.ts +1 -0
  104. package/src/type/README.md +54 -307
  105. package/src/type/class.ts +2 -2
  106. package/src/type/index.ts +14 -14
  107. package/src/type/is.ts +265 -2
  108. package/src/type/object.ts +37 -0
  109. package/src/type/string.ts +7 -2
  110. package/src/type/tuple.ts +6 -6
  111. package/src/type/union.ts +16 -0
  112. package/src/web/README.md +77 -0
  113. package/src/web/capture.ts +35 -0
  114. package/src/web/clipboard.ts +97 -0
  115. package/src/web/dom.ts +117 -0
  116. package/src/web/download.ts +16 -0
  117. package/src/web/event.ts +46 -0
  118. package/src/web/index.ts +10 -0
  119. package/src/web/local-storage.ts +113 -0
  120. package/src/web/location.ts +28 -0
  121. package/src/web/permission.ts +172 -0
  122. package/src/web/script-loader.ts +432 -0
  123. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  124. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  125. package/tests/unit/basic/array.spec.ts +1 -1
  126. package/tests/unit/basic/stream.spec.ts +1 -1
  127. package/tests/unit/basic/string.spec.ts +0 -9
  128. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  129. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  130. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  131. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  132. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  133. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  134. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  135. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  136. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  137. package/tests/unit/css/class.spec.ts +157 -0
  138. package/tests/unit/environment/basic.spec.ts +20 -0
  139. package/tests/unit/environment/device.spec.ts +146 -0
  140. package/tests/unit/environment/feature.spec.ts +388 -0
  141. package/tests/unit/environment/geo.spec.ts +111 -0
  142. package/tests/unit/environment/runtime.spec.ts +364 -0
  143. package/tests/unit/environment/snapshot.spec.ts +4 -0
  144. package/tests/unit/environment/variable.spec.ts +190 -0
  145. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  146. package/tests/unit/event/event-manager.spec.ts +246 -0
  147. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  148. package/tests/unit/exception/browser.spec.ts +213 -0
  149. package/tests/unit/exception/nodejs.spec.ts +144 -0
  150. package/tests/unit/exception/normalize.spec.ts +57 -0
  151. package/tests/unit/identifier/id.spec.ts +71 -0
  152. package/tests/unit/identifier/uuid.spec.ts +85 -0
  153. package/tests/unit/log/log-emitter.spec.ts +33 -0
  154. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  155. package/tests/unit/log/log-type.spec.ts +7 -0
  156. package/tests/unit/log/logger.spec.ts +222 -0
  157. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  158. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  159. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  160. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  161. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  162. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  163. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  164. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  165. package/tests/unit/random/string.spec.ts +11 -0
  166. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  167. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  168. package/tests/unit/singleton/singleton.spec.ts +49 -0
  169. package/tests/unit/storage/table.spec.ts +620 -0
  170. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  171. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  172. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  173. package/.oxlintrc.json +0 -5
  174. package/src/random/uuid.ts +0 -103
  175. package/tests/unit/random/uuid.spec.ts +0 -37
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "oxlint"
2
+ import { Lint } from "@planet-matrix/mobius-mono"
3
+
4
+ const config: Lint.OxlintConfig = defineConfig(Lint.defineOxlintConfig({}))
5
+
6
+ export default config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planet-matrix/mobius-model",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Mobius model.",
5
5
  "keywords": [
6
6
  "mobius",
@@ -53,21 +53,27 @@
53
53
  "directory": "packages/model"
54
54
  },
55
55
  "scripts": {
56
- "lint": "bun oxlint",
56
+ "lint": "bun oxlint --type-aware --type-check",
57
57
  "test": "vitest watch",
58
58
  "build": "bun ./scripts/build.ts",
59
59
  "prepublishOnly": "bun run build"
60
60
  },
61
- "dependencies": {},
61
+ "dependencies": {
62
+ "ua-parser-js": "^2.0.9",
63
+ "html-to-image": "^1.11.13",
64
+ "@standard-schema/spec": "^v1.1.0",
65
+ "arktype": "^2.2.0",
66
+ "@dotenvx/dotenvx": "^1.54.1"
67
+ },
62
68
  "devDependencies": {
63
- "@planet-matrix/mobius-mono": "0.7.0",
64
- "@types/bun": "^1.3.8",
65
- "oxlint": "^1.42.0",
66
- "oxlint-tsgolint": "^0.13.0",
69
+ "@planet-matrix/mobius-mono": "0.8.0",
70
+ "@types/bun": "^1.3.10",
71
+ "oxlint": "^1.55.0",
72
+ "oxlint-tsgolint": "^0.16.0",
67
73
  "typescript": "^5.9.3",
68
- "@typescript/native-preview": "^7.0.0-dev.20260130.1",
69
- "vite": "^7.3.1",
70
- "vitest": "^4.0.18"
74
+ "@typescript/native-preview": "^7.0.0-dev.20260311.1",
75
+ "vite": "^8.0.0",
76
+ "vitest": "^4.1.0"
71
77
  },
72
78
  "peerDependencies": {},
73
79
  "peerDependenciesMeta": {},
@@ -0,0 +1,92 @@
1
+ # Abort
2
+
3
+ ## Description
4
+
5
+ Abort 模块提供围绕中止信号(Abort Signal)的统一建模能力,用于表达、传递、组合与观察取消语义。
6
+
7
+ 它关注的不是某一个具体异步 API 的取消写法,而是“中止应如何被稳定表示、如何沿调用链传递、以及多个中止来源应如何组合”这一类基础问题。模块中的能力因此更偏向语义整理与边界对齐,而不是面向业务流程的临时封装。
8
+
9
+ ## For Understanding
10
+
11
+ Abort 模块适合放在异步任务、资源加载、请求封装、响应式调度器或其它需要“可取消执行”语义的边界层之间。它试图回答的核心问题不是“如何立即停止某段代码”,而是“当系统中的多个参与方都可能发出中止意图时,应该如何用一套稳定、可组合、可维护的模型来表达它”。
12
+
13
+ 理解这个模块时,应该先把握几个前提:
14
+
15
+ - 中止是一种显式控制语义,而不是异常兜底。它表达的是“后续不应继续推进”,而不是“当前实现发生了错误”。
16
+ - 这个模块关心的是中止状态的表达、传播与组合,而不是任务调度、重试策略、超时策略或资源生命周期本身。后者如果要接入中止,应建立在本模块提供的稳定语义之上,而不是反过来让 Abort 模块吸收那些更高层的业务含义。
17
+ - 这里的“组合”主要指多个上游中止来源之间的关系建模,例如“任一来源中止即可停止”或“全部来源都中止后才停止”,而不是泛化为任意事件系统。
18
+ - 这里的“适配”主要指把不同形态的中止感知输入对齐到共同边界,例如从控制器(AbortController)、信号(AbortSignal)或已具备中止语义的管理对象中解析出一致的中止表示。
19
+ - 这里的“监听管理”只服务于稳定地感知中止状态变化,避免重复注册、难以拆除或让外部调用方直接依赖脆弱的监听细节。它不应该演变成通用事件总线或宿主环境监听框架。
20
+
21
+ 如果某个能力的核心价值已经不再是“表达中止语义”,而是“安排任务什么时候开始”“实现某种副作用策略”“驱动特定运行时资源”,那么它通常就不应继续放在 Abort 模块中,即使它表面上也会接触 `AbortSignal`。
22
+
23
+ ## For Using
24
+
25
+ 当你希望把“取消”视为一项独立、可组合的基础能力,而不是在每一层业务代码里临时传几个布尔值、状态位或回调函数时,可以使用 Abort 模块。
26
+
27
+ 这个模块通常适合以下几类使用场景:
28
+
29
+ - 统一中止输入边界:当调用方传入的可能是 `AbortSignal`、`AbortController`,或某个已经承载中止语义的对象时,可以先把这些输入统一到一致的中止模型,再让后续逻辑围绕统一边界工作。
30
+ - 在分层调用链中传递取消语义:当一个底层能力需要接收来自上层的中止意图,并继续向更下游转交时,可以通过本模块保持中止边界显式、稳定且易于组合,而不是在不同层里各自定义非兼容约定。
31
+ - 组合多个上游来源:当某个任务是否停止取决于多个来源,例如调用方取消、父任务结束、宿主环境销毁或上游流程提前终止时,可以把这些来源组合成一个可继续传递的中止模型,而不是在业务逻辑里散落多个条件分支。
32
+ - 管理与中止信号相关的监听关系:当你需要围绕 `AbortSignal` 建立可重复添加、可显式移除、并且不会把监听管理细节泄漏给外部调用方的能力时,可以把这部分职责收束到本模块中。
33
+ - 桥接选项对象与中止能力:当一个函数、类或工厂通过选项对象接收中止相关配置时,可以借助本模块把“是否携带中止信号”与“如何继续向下游传播”整理为一致约定。
34
+
35
+ 使用这个模块时,推荐把它放在“业务逻辑之前、宿主接口之后”的边界层。也就是说,先用它把中止语义整理干净,再由更高层能力决定收到中止后应该怎样停止请求、释放资源或结束流程。这样可以避免把业务细节反向耦合进 Abort 模块本身。
36
+
37
+ ## For Contributing
38
+
39
+ Abort 模块服务的不是某一种具体业务,而是“中止能力如何被表达、传递、组合与观察”这一类基础问题。为这个模块做贡献时,优先考虑新增能力是否真的在澄清中止语义边界,还是只是在为某个当前实现补一个便捷入口。如果一个能力离开当前业务背景后就失去意义,或者必须依赖特定任务模型、宿主环境细节、框架生命周期才能成立,它通常就不应成为 Abort 模块的公共部分。
40
+
41
+ 这个模块更关注中止约定的清晰度,而不是功能堆叠。新增内容时,优先回答以下问题:它是否真的属于中止模型的一部分;它是在统一调用方的取消语义,还是只是在暴露某段实现细节;它是否能让上游、下游和组合关系更清楚;它是否能在不同运行时和调用层次中保持稳定语义。若这些问题没有明确答案,应谨慎扩展公共 API。
42
+
43
+ 同时,说明文件应聚焦于模块目标、边界、原则与长期约束,而不是复述当前实现细节。实现如何达成,应主要交由源码表达;文档应说明为什么值得这样设计,以及哪些边界不能被破坏。
44
+
45
+ ### JSDoc 注释格式要求
46
+
47
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让调用方在不跳转实现的情况下就能理解用途。
48
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
49
+ - 如果描述后还有其他内容,应在描述后加一个空行。
50
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
51
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
52
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
53
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
54
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
55
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
56
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
57
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
58
+
59
+ ### 实现规范要求
60
+
61
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
62
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
63
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
64
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
65
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
66
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
67
+ - 辅助元素永远不要公开导出。
68
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/abort/internal.ts`。
69
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
70
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
71
+ - 子模块不需要有自己的 `README.md`。
72
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
73
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
74
+ - 与中止相关的实现应优先围绕“状态判定”“输入适配”“上游关系组合”“监听关系管理”这类清晰职责组织,避免把重试、调度、资源释放策略等更高层语义混入本模块。
75
+
76
+ ### 导出策略要求
77
+
78
+ - 保持内部辅助项和内部符号为私有,不要让调用方依赖临时性的内部结构。
79
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
80
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
81
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的中止语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
82
+
83
+ ### 测试要求
84
+
85
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
86
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
87
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
88
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
89
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
90
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
91
+ - 模块的单元测试文件目录是 `./tests/unit/abort`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/abort/<sub-module-name>`。
92
+ - 对这个模块来说,测试应优先覆盖不同中止来源之间的组合关系、状态传播、监听注册与移除行为,以及选项对象桥接时的典型边界场景。
@@ -0,0 +1,278 @@
1
+ import { getGlobalAbortSignalListenerManager } from "./abort-signal-listener-manager.ts"
2
+
3
+ /**
4
+ * 表示支持的中止感知输入。
5
+ */
6
+ export type Abortable = AbortSignal | AbortController | AbortManager
7
+
8
+ /**
9
+ * 表示可能包含 `abortSignal` 的选项对象。
10
+ */
11
+ export interface WithAbortSignal {
12
+ abortSignal?: AbortSignal | undefined
13
+ }
14
+ /**
15
+ * 从选项类型中移除 `abortSignal` 字段。
16
+ */
17
+ export type WithoutAbortSignal<T> = Omit<T, "abortSignal">
18
+
19
+ /**
20
+ * 从中止感知输入中解析出 `AbortSignal`。
21
+ */
22
+ export const getAbortSignal = (abortable: Abortable): AbortSignal => {
23
+ if (abortable instanceof AbortSignal) {
24
+ return abortable
25
+ }
26
+ else if (abortable instanceof AbortController) {
27
+ return abortable.signal
28
+ }
29
+ else if (abortable instanceof AbortManager) {
30
+ return abortable.abortSignal
31
+ }
32
+ else {
33
+ throw new TypeError("Unsupported abortable type.")
34
+ }
35
+ }
36
+ /**
37
+ * 检查中止感知输入是否已经中止。
38
+ */
39
+ export const isAborted = (abortable: Abortable): boolean => {
40
+ const signal = getAbortSignal(abortable)
41
+ return signal.aborted
42
+ }
43
+
44
+ /**
45
+ * 表示暴露 `AbortManager` 与 `abort` 操作的对象。
46
+ */
47
+ export interface AbortFriendly {
48
+ readonly abortManager: AbortManager
49
+
50
+ /**
51
+ * 此方法中应该将 {@link abortManager} 中止。
52
+ */
53
+ abort: (reason: string) => void
54
+ }
55
+ /**
56
+ * 表示可提供上游 `AbortManager` 的选项。
57
+ */
58
+ export interface AbortFriendlyOptions {
59
+ abortManager?: AbortManager | undefined
60
+ }
61
+
62
+ /**
63
+ * 定义支持的中止判定模式。
64
+ */
65
+ export const ABORT_MANAGER_MODE = {
66
+ ALL: "all",
67
+ ANY: "any",
68
+ } as const
69
+ /**
70
+ * 表示 `AbortManager` 使用的中止模式。
71
+ */
72
+ export type AbortManagerMode = typeof ABORT_MANAGER_MODE[keyof typeof ABORT_MANAGER_MODE]
73
+ interface Upstream {
74
+ abortable: Abortable
75
+ listener: () => void
76
+ connect: () => void
77
+ disconnect: () => void
78
+ remove: () => void
79
+ }
80
+ /**
81
+ * 表示 `AbortManager` 的构造选项。
82
+ */
83
+ export interface AbortManagerOptions extends AbortFriendlyOptions {
84
+ /**
85
+ * 如果为 true,则在中止后移除全部上游。
86
+ *
87
+ * @default true
88
+ */
89
+ autoRemoveUpstreamsAfterAbort?: boolean | undefined
90
+ }
91
+ /**
92
+ * 协调一个或多个上游中止源的下游中止状态。
93
+ */
94
+ export class AbortManager implements AbortFriendly {
95
+ readonly abortManager: AbortManager
96
+ readonly abortController: AbortController
97
+ readonly abortSignal: AbortSignal
98
+ protected upstreamMap: Map<Abortable, Upstream>
99
+ protected mode: AbortManagerMode
100
+ protected autoRemoveUpstreamsAfterAbort: boolean
101
+
102
+ constructor(options: AbortManagerOptions) {
103
+ this.abortManager = this
104
+ this.abortController = new AbortController()
105
+ this.abortSignal = this.abortController.signal
106
+ this.upstreamMap = new Map()
107
+ this.mode = ABORT_MANAGER_MODE.ALL
108
+ this.autoRemoveUpstreamsAfterAbort = options.autoRemoveUpstreamsAfterAbort ?? true
109
+
110
+ const abortManagerFromOptions = options.abortManager
111
+ if (abortManagerFromOptions !== undefined) {
112
+ this.addUpstreams([abortManagerFromOptions])
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 检查当前管理器是否已经中止。
118
+ */
119
+ isAborted(): boolean {
120
+ return this.abortSignal.aborted
121
+ }
122
+
123
+ /**
124
+ * 设置中止判定模式。
125
+ */
126
+ setMode(mode: AbortManagerMode): this {
127
+ this.mode = mode
128
+ return this
129
+ }
130
+
131
+ /**
132
+ * 添加需要观察的上游中止源。
133
+ */
134
+ addUpstreams(abortables: Abortable[]): this {
135
+ const globalAbortSignalListenerManager = getGlobalAbortSignalListenerManager()
136
+
137
+ abortables
138
+ .forEach((abortable) => {
139
+ const abortSignal = getAbortSignal(abortable)
140
+ const listener = (): void => {
141
+ this.tryToAbort() // 任一上游中止后都应重新尝试判定是否中止
142
+ }
143
+ const connect = (): void => {
144
+ globalAbortSignalListenerManager.addEventListener(abortSignal, listener)
145
+ }
146
+ const disconnect = (): void => {
147
+ globalAbortSignalListenerManager.removeEventListener(abortSignal, listener)
148
+ }
149
+ const remove = (): void => {
150
+ disconnect()
151
+ this.upstreamMap.delete(abortable)
152
+ }
153
+ this.upstreamMap.set(abortable, {
154
+ abortable,
155
+ listener,
156
+ connect,
157
+ disconnect,
158
+ remove,
159
+ })
160
+ connect()
161
+ })
162
+
163
+ // 添加上游后,立即重新判定是否需要中止
164
+ this.tryToAbort()
165
+
166
+ return this
167
+ }
168
+
169
+ /**
170
+ * 移除指定的上游中止源。
171
+ */
172
+ removeUpstreams(abortables: Abortable[]): this {
173
+ abortables
174
+ .forEach((abortable) => {
175
+ const upstream = this.upstreamMap.get(abortable)
176
+ if (upstream !== undefined) {
177
+ upstream.remove()
178
+ }
179
+ })
180
+
181
+ // 移除上游后,立即重新判定是否需要中止
182
+ this.tryToAbort()
183
+
184
+ return this
185
+ }
186
+
187
+ /**
188
+ * 移除全部已注册的上游中止源。
189
+ */
190
+ removeAllUpstreams(): this {
191
+ const upstreamAbortables = Array.from(this.upstreamMap.keys())
192
+ this.removeUpstreams(upstreamAbortables)
193
+
194
+ return this
195
+ }
196
+
197
+ /**
198
+ * 根据当前模式评估上游状态,并在满足条件时中止。
199
+ */
200
+ tryToAbort(): void {
201
+ const upstreams = Array.from(this.upstreamMap.values())
202
+
203
+ let shouldAbort: boolean
204
+ if (this.mode === ABORT_MANAGER_MODE.ALL) {
205
+ shouldAbort = upstreams.every(upstream => isAborted(upstream.abortable))
206
+ }
207
+ else if (this.mode === ABORT_MANAGER_MODE.ANY) {
208
+ shouldAbort = upstreams.some(upstream => isAborted(upstream.abortable))
209
+ }
210
+ else {
211
+ throw new TypeError("Unsupported mode.")
212
+ }
213
+
214
+ // 满足中止条件且当前尚未中止时,执行中止
215
+ if (shouldAbort === true && this.abortSignal.aborted === false) {
216
+ this.abort(`Abort by upstream, mode: ${this.mode}.`)
217
+ }
218
+ }
219
+
220
+ /**
221
+ * 使用给定原因中止当前管理器。
222
+ */
223
+ abort(reason: string): void {
224
+ this.abortController.abort(reason)
225
+ if (this.autoRemoveUpstreamsAfterAbort === true) {
226
+ this.removeAllUpstreams()
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * 基于单个上游中止源创建 `AbortManager`。
233
+ */
234
+ export const fromAbortable = (abortable: Abortable): AbortManager => {
235
+ const abortManager = new AbortManager({})
236
+ abortManager.addUpstreams([abortable])
237
+ return abortManager
238
+ }
239
+
240
+ /**
241
+ * 创建在任一上游中止时中止的 `AbortManager`。
242
+ */
243
+ export const any = (abortables: Array<Abortable | undefined | null>): AbortManager => {
244
+ const abortManager = new AbortManager({})
245
+ abortManager.setMode(ABORT_MANAGER_MODE.ANY)
246
+ abortManager.addUpstreams(abortables.filter(item => item !== undefined && item !== null))
247
+ return abortManager
248
+ }
249
+
250
+ /**
251
+ * 创建在全部上游中止时中止的 `AbortManager`。
252
+ */
253
+ export const all = (abortables: Array<Abortable | undefined | null>): AbortManager => {
254
+ const abortManager = new AbortManager({})
255
+ abortManager.setMode(ABORT_MANAGER_MODE.ALL)
256
+ abortManager.addUpstreams(abortables.filter(item => item !== undefined && item !== null))
257
+ return abortManager
258
+ }
259
+
260
+ /**
261
+ * 从可能携带 `abortSignal` 的选项对象创建 `AbortManager`。
262
+ */
263
+ export const fromWithAbortSignal = <T extends WithAbortSignal>(options: T): AbortManager => {
264
+ const { abortSignal } = options
265
+ const abortManager = new AbortManager({})
266
+ if (abortSignal !== undefined) {
267
+ abortManager.addUpstreams([abortSignal])
268
+ }
269
+ return abortManager
270
+ }
271
+
272
+ /**
273
+ * 从中止友好的选项对象创建 `AbortManager`。
274
+ */
275
+ export const fromAbortFriendlyOptions = <T extends AbortFriendlyOptions>(options: T): AbortManager => {
276
+ const abortManager = new AbortManager({ ...options })
277
+ return abortManager
278
+ }
@@ -0,0 +1,81 @@
1
+
2
+ type Listener = () => void
3
+ /**
4
+ * 管理绑定到 `AbortSignal` 实例上的监听器。
5
+ */
6
+ export class AbortSignalListenerManager {
7
+ readonly abortSignalMap: WeakMap<AbortSignal, {
8
+ syntheticListener: Listener
9
+ managedListenerSet: Set<Listener>
10
+ }>
11
+
12
+ constructor() {
13
+ this.abortSignalMap = new WeakMap()
14
+ }
15
+
16
+ /**
17
+ * 检查某个监听器当前是否由指定 `AbortSignal` 管理。
18
+ */
19
+ hasEventListener(abortSignal: AbortSignal, listener: Listener): boolean {
20
+ const listeners = this.abortSignalMap.get(abortSignal)
21
+ if (listeners === undefined) {
22
+ return false
23
+ }
24
+ return listeners.managedListenerSet.has(listener)
25
+ }
26
+
27
+ /**
28
+ * 为 `AbortSignal` 添加一个受管监听器。
29
+ */
30
+ addEventListener(abortSignal: AbortSignal, listener: Listener): this {
31
+ let listeners = this.abortSignalMap.get(abortSignal)
32
+ if (listeners === undefined) {
33
+ listeners = {
34
+ syntheticListener: (): void => {
35
+ // 在调用受管监听器之前,先将它们复制到一个新的数组中,以避免在调用过程中被修改。
36
+ const managedListenerSet = [...listeners?.managedListenerSet ?? []]
37
+ for (const managedListener of managedListenerSet) {
38
+ managedListener()
39
+ }
40
+ },
41
+ managedListenerSet: new Set(),
42
+ }
43
+ this.abortSignalMap.set(abortSignal, listeners)
44
+ abortSignal.addEventListener("abort", listeners.syntheticListener)
45
+ }
46
+ if (listeners.managedListenerSet.has(listener) === false) {
47
+ listeners.managedListenerSet.add(listener)
48
+ }
49
+
50
+ return this
51
+ }
52
+
53
+ /**
54
+ * 从 `AbortSignal` 移除一个受管监听器。
55
+ */
56
+ removeEventListener(abortSignal: AbortSignal, listener: Listener): this {
57
+ const listeners = this.abortSignalMap.get(abortSignal)
58
+ if (listeners === undefined) {
59
+ return this
60
+ }
61
+ listeners.managedListenerSet.delete(listener)
62
+ if (listeners.managedListenerSet.size === 0) {
63
+ abortSignal.removeEventListener("abort", listeners.syntheticListener)
64
+ this.abortSignalMap.delete(abortSignal)
65
+ }
66
+ return this
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 获取全局单例的 `AbortSignalListenerManager` 实例。
72
+ */
73
+ export const getGlobalAbortSignalListenerManager: () => AbortSignalListenerManager = (
74
+ () => {
75
+ let instance: AbortSignalListenerManager | undefined = undefined
76
+ return (): AbortSignalListenerManager => {
77
+ instance = instance ?? new AbortSignalListenerManager()
78
+ return instance
79
+ }
80
+ }
81
+ )()
@@ -0,0 +1,2 @@
1
+ export * from "./abort-signal-listener-manager.ts"
2
+ export * from './abort-manager.ts'