@planet-matrix/mobius-model 0.4.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 (179) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +134 -21
  3. package/dist/index.js +45 -4
  4. package/dist/index.js.map +186 -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 -117
  12. package/src/basic/enhance.ts +10 -0
  13. package/src/basic/function.ts +81 -62
  14. package/src/basic/index.ts +2 -0
  15. package/src/basic/is.ts +152 -71
  16. package/src/basic/object.ts +82 -0
  17. package/src/basic/promise.ts +29 -8
  18. package/src/basic/string.ts +2 -33
  19. package/src/color/README.md +105 -0
  20. package/src/color/index.ts +3 -0
  21. package/src/color/internal.ts +42 -0
  22. package/src/color/rgb/analyze.ts +236 -0
  23. package/src/color/rgb/construct.ts +130 -0
  24. package/src/color/rgb/convert.ts +227 -0
  25. package/src/color/rgb/derive.ts +303 -0
  26. package/src/color/rgb/index.ts +6 -0
  27. package/src/color/rgb/internal.ts +208 -0
  28. package/src/color/rgb/parse.ts +302 -0
  29. package/src/color/rgb/serialize.ts +144 -0
  30. package/src/color/types.ts +57 -0
  31. package/src/color/xyz/analyze.ts +80 -0
  32. package/src/color/xyz/construct.ts +19 -0
  33. package/src/color/xyz/convert.ts +71 -0
  34. package/src/color/xyz/index.ts +3 -0
  35. package/src/color/xyz/internal.ts +23 -0
  36. package/src/css/README.md +93 -0
  37. package/src/css/class.ts +559 -0
  38. package/src/css/index.ts +1 -0
  39. package/src/encoding/README.md +92 -0
  40. package/src/encoding/base64.ts +107 -0
  41. package/src/encoding/index.ts +1 -0
  42. package/src/environment/README.md +97 -0
  43. package/src/environment/basic.ts +26 -0
  44. package/src/environment/device.ts +311 -0
  45. package/src/environment/feature.ts +285 -0
  46. package/src/environment/geo.ts +337 -0
  47. package/src/environment/index.ts +7 -0
  48. package/src/environment/runtime.ts +400 -0
  49. package/src/environment/snapshot.ts +60 -0
  50. package/src/environment/variable.ts +239 -0
  51. package/src/event/README.md +90 -0
  52. package/src/event/class-event-proxy.ts +228 -0
  53. package/src/event/common.ts +19 -0
  54. package/src/event/event-manager.ts +203 -0
  55. package/src/event/index.ts +4 -0
  56. package/src/event/instance-event-proxy.ts +186 -0
  57. package/src/event/internal.ts +24 -0
  58. package/src/exception/README.md +96 -0
  59. package/src/exception/browser.ts +219 -0
  60. package/src/exception/index.ts +4 -0
  61. package/src/exception/nodejs.ts +169 -0
  62. package/src/exception/normalize.ts +106 -0
  63. package/src/exception/types.ts +99 -0
  64. package/src/identifier/README.md +92 -0
  65. package/src/identifier/id.ts +119 -0
  66. package/src/identifier/index.ts +2 -0
  67. package/src/identifier/uuid.ts +187 -0
  68. package/src/index.ts +18 -1
  69. package/src/log/README.md +79 -0
  70. package/src/log/index.ts +5 -0
  71. package/src/log/log-emitter.ts +72 -0
  72. package/src/log/log-record.ts +10 -0
  73. package/src/log/log-scheduler.ts +74 -0
  74. package/src/log/log-type.ts +8 -0
  75. package/src/log/logger.ts +543 -0
  76. package/src/orchestration/README.md +89 -0
  77. package/src/orchestration/coordination/barrier.ts +214 -0
  78. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  79. package/src/orchestration/coordination/errors.ts +98 -0
  80. package/src/orchestration/coordination/index.ts +16 -0
  81. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  82. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  83. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  84. package/src/orchestration/coordination/mutex.ts +257 -0
  85. package/src/orchestration/coordination/permit.ts +127 -0
  86. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  87. package/src/orchestration/coordination/semaphore.ts +280 -0
  88. package/src/orchestration/index.ts +1 -0
  89. package/src/random/README.md +78 -0
  90. package/src/random/index.ts +1 -0
  91. package/src/random/string.ts +35 -0
  92. package/src/reactor/README.md +4 -0
  93. package/src/reactor/reactor-core/primitive.ts +9 -9
  94. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  95. package/src/singleton/README.md +79 -0
  96. package/src/singleton/factory.ts +55 -0
  97. package/src/singleton/index.ts +2 -0
  98. package/src/singleton/manager.ts +204 -0
  99. package/src/storage/README.md +107 -0
  100. package/src/storage/index.ts +1 -0
  101. package/src/storage/table.ts +449 -0
  102. package/src/timer/README.md +86 -0
  103. package/src/timer/expiration/expiration-manager.ts +594 -0
  104. package/src/timer/expiration/index.ts +3 -0
  105. package/src/timer/expiration/min-heap.ts +208 -0
  106. package/src/timer/expiration/remaining-manager.ts +241 -0
  107. package/src/timer/index.ts +1 -0
  108. package/src/type/README.md +54 -307
  109. package/src/type/class.ts +2 -2
  110. package/src/type/index.ts +14 -14
  111. package/src/type/is.ts +265 -2
  112. package/src/type/object.ts +37 -0
  113. package/src/type/string.ts +7 -2
  114. package/src/type/tuple.ts +6 -6
  115. package/src/type/union.ts +16 -0
  116. package/src/web/README.md +77 -0
  117. package/src/web/capture.ts +35 -0
  118. package/src/web/clipboard.ts +97 -0
  119. package/src/web/dom.ts +117 -0
  120. package/src/web/download.ts +16 -0
  121. package/src/web/event.ts +46 -0
  122. package/src/web/index.ts +10 -0
  123. package/src/web/local-storage.ts +113 -0
  124. package/src/web/location.ts +28 -0
  125. package/src/web/permission.ts +172 -0
  126. package/src/web/script-loader.ts +432 -0
  127. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  128. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  129. package/tests/unit/basic/array.spec.ts +1 -1
  130. package/tests/unit/basic/object.spec.ts +32 -1
  131. package/tests/unit/basic/stream.spec.ts +1 -1
  132. package/tests/unit/basic/string.spec.ts +0 -9
  133. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  134. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  135. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  136. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  137. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  138. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  139. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  140. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  141. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  142. package/tests/unit/css/class.spec.ts +157 -0
  143. package/tests/unit/encoding/base64.spec.ts +40 -0
  144. package/tests/unit/environment/basic.spec.ts +20 -0
  145. package/tests/unit/environment/device.spec.ts +146 -0
  146. package/tests/unit/environment/feature.spec.ts +388 -0
  147. package/tests/unit/environment/geo.spec.ts +111 -0
  148. package/tests/unit/environment/runtime.spec.ts +364 -0
  149. package/tests/unit/environment/snapshot.spec.ts +4 -0
  150. package/tests/unit/environment/variable.spec.ts +190 -0
  151. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  152. package/tests/unit/event/event-manager.spec.ts +246 -0
  153. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  154. package/tests/unit/exception/browser.spec.ts +213 -0
  155. package/tests/unit/exception/nodejs.spec.ts +144 -0
  156. package/tests/unit/exception/normalize.spec.ts +57 -0
  157. package/tests/unit/identifier/id.spec.ts +71 -0
  158. package/tests/unit/identifier/uuid.spec.ts +85 -0
  159. package/tests/unit/log/log-emitter.spec.ts +33 -0
  160. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  161. package/tests/unit/log/log-type.spec.ts +7 -0
  162. package/tests/unit/log/logger.spec.ts +222 -0
  163. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  164. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  165. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  166. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  167. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  168. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  169. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  170. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  171. package/tests/unit/random/string.spec.ts +11 -0
  172. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  173. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  174. package/tests/unit/singleton/singleton.spec.ts +49 -0
  175. package/tests/unit/storage/table.spec.ts +620 -0
  176. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  177. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  178. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  179. package/.oxlintrc.json +0 -5
@@ -0,0 +1,107 @@
1
+ # Storage
2
+
3
+ ## Description
4
+
5
+ Storage 模块提供围绕运行时受控数据驻留(controlled runtime data residency)的通用建模能力,用于把一组需要在应用运行过程中被稳定放置、命名、查询、更新、删除与观察的数据,组织为清楚且可复用的公共语义。
6
+
7
+ 它关注的不是某个具体宿主提供的存储 API,也不是某个数据库产品、浏览器介质或网络协议本身,而是“数据在应用运行时如何以某种稳定结构驻留、如何成为多个上层消费者共同依赖的受控对象、以及这些结构本身该暴露什么样的长期语义”这一类更一般的问题。因此,这个模块更适合被理解为一组运行时存储模型,而不是浏览器存储工具箱、数据库客户端封装层或临时缓存函数集合。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 Storage 模块时,应先把它看作“运行时受控数据驻留”的建模层,而不是把任意能把值放起来的能力都塞进来的杂项容器。它要解决的问题,不只是某条数据该如何增删改查,也包括一组数据应如何在运行时被稳定地放置为某种结构、如何通过清楚边界对外暴露、以及如何成为 UI、业务逻辑或其它运行时协作方可以共同消费的对象。
12
+
13
+ 理解这个模块时,不适合把它简单归入“状态管理”一类过于宽泛的讨论。几乎所有应用都可以被描述为数据流动与状态变化,因此“状态管理”本身很难为模块提供足够稳定的立足点。相比之下,Storage 模块真正的立足点更具体:数据以什么结构驻留在运行时,这些结构如何被查询、更新、删除与观察,以及它们如何成为应用内部长期可依赖的消费面。
14
+
15
+ 在很多实际场景里,应用不会直接消费一次请求的返回结果,而是先把这些结果收束进运行时存储结构,再由界面层和业务层读取这份经过统一管理的数据驻留对象。这样做的价值,不在于单纯“缓存一下结果”,而在于把查询结果、本地可见状态、用户操作产生的临时变更以及后续可能发生的协调过程,统一纳入一个稳定的运行时存储模型。像乐观更新、局部回滚、统一订阅、跨视图共享、局部合并、以及与外部数据来源或持久化介质对接,也因此更容易被组织成清楚的公共语义。
16
+
17
+ 从这个角度看,当前已实现的 `table` 更适合被理解为 Storage 模块下的一个子形态:它表达的是“以唯一标识组织行数据集合”的表式运行时存储模型,而不是 Storage 模块全部问题域本身。未来若出现 `KeyValueStore`、命名空间存储或其它存储结构,它们也应与 `table` 一样,被理解为 Storage 父边界下的不同子模型,而不是彼此互不相干的零散工具。
18
+
19
+ 理解这个模块时,还应守住几条边界原则:
20
+
21
+ - Storage 模块表达的是运行时存储结构、驻留边界、变更管理与可观察语义,而不是某个宿主存储 API 的直接包装。
22
+ - 浏览器的 `localStorage`、`indexedDB`,或服务端数据库、远程接口、文件系统等具体介质与数据来源,通常属于宿主环境能力或外部系统;Storage 可以与它们对接,但不应把它们的临时接口细节直接上升为模块公共语义。
23
+ - 它适合表达“数据如何在运行时被组织”“结构内部如何查询、更新、删除、观察”“模型如何与外部系统形成接点”,但不应直接承担传输协议、连接管理、数据库迁移、分布式一致性、权限体系、查询规划器或业务专属缓存规则。
24
+ - 它可以天然承载运行时副本、乐观更新中介、多来源合流或本地持久化桥接等场景,但这些都应被理解为该问题域上的典型使用方式,而不是 Storage 模块得以成立的唯一理由。
25
+ - 它未来也可以自然吸收数据对齐、传播与副本协同之类的能力;像 `sync` 或 `replication` 这样的方向,可以成为 Storage 模块的一部分功能,但不应反过来成为当前模块定义本身的前提。
26
+ - 它关心的是“存储模型如何在运行时稳定存在”,而不是完整替代服务端数据库、ORM、状态管理框架或同步引擎的全部职责。
27
+
28
+ ## For Using
29
+
30
+ 当你希望在应用内部维护一组真正可被持续消费的受控数据驻留结构,而不是让 UI 和业务逻辑直接依赖零散变量、一次性请求结果或宿主介质的原始接口时,可以使用 Storage 模块。
31
+
32
+ 从使用角度看,这个模块大致可以分为三类能力:
33
+
34
+ - 运行时存储结构能力:用于表达数据在应用内如何被组织、命名、查询、更新、删除、批量处理以及如何保持结构本身的稳定边界。`table` 属于这一类,它适合承载具备唯一标识的一组记录。
35
+ - 变更管理能力:用于表达插入、更新、删除、覆盖、合并、乐观修改、回滚以及变更通知等语义,使运行时驻留数据能够成为应用内部真正可信的消费对象,而不是一次性响应结果的临时中转站。
36
+ - 集成接点能力:用于让运行时存储结构与外部系统形成清楚边界,例如吸收来自远端或其它来源的数据,或把内部数据继续写入本地持久化介质、离线缓存层或其它端侧环境中。
37
+
38
+ 更合适的接入方式,通常是先判断你的核心对象是不是“运行时中的一份受控数据驻留结构”。如果你需要的是:
39
+
40
+ - 请求结果先进入一份本地副本,再由多个界面和业务层共同消费;
41
+ - 某组数据需要在运行时被稳定放置为表、键值集合或其它结构,而不是散落在临时变量里;
42
+ - 用户操作要先体现在本地,再异步与远端对齐;
43
+ - 同一份数据需要同时连接上游远端与下游本地持久化;
44
+ - 你希望把查询、更新、订阅和外部集成入口都收束到一份统一模型中;
45
+
46
+ 那么这个模块就是合适的边界。
47
+
48
+ 相反,如果你的需求只是某个宿主环境里对具体介质进行直接读写,例如浏览器 `localStorage` 的原生操作、IndexedDB 事务细节、某个数据库驱动的连接与 SQL 交互,那么这些通常不应直接落在 Storage 模块本身,而应放在对应宿主模块、适配层或更外层系统中。
49
+
50
+ ## For Contributing
51
+
52
+ 为 Storage 模块贡献内容时,优先判断新增能力是否真的在澄清“运行时受控数据如何被建模为稳定存储结构”这一问题,而不是只是在补一个看起来方便的宿主 API 包装或某个业务缓存技巧。这个模块应长期服务于“数据如何在运行时稳定驻留并持续消费”“变更如何被统一管理”“不同存储结构如何表达各自稳定语义”“模型如何与外部系统形成清楚接点”这几类问题。
53
+
54
+ 扩展这个模块时,应特别警惕以下倾向:把 Storage 退化成浏览器存储工具集合;把某个数据库产品的接口直接平移成公共 API;把网络请求、同步协议、重试策略、权限判断或业务缓存失效规则无差别地下沉到存储模型内部;或者为了某个当前实现方便而暴露临时内部结构。文档应说明哪些运行时存储边界是长期成立的,以及为什么这些边界成立,而不是复述某个版本里的局部实现技巧。
55
+
56
+ 当前已经落地的 `table` 应被理解为 Storage 模块下的一个具体子模型,而不是整个模块的全部定义。后续若新增 `KeyValueStore`、命名空间存储、快照模型或其它存储结构,应优先判断它们是否都属于“运行时受控数据驻留结构”这一父问题域;如果只是某个特定宿主介质的读写封装,通常不应直接成为 Storage 模块的顶级公共能力。
57
+
58
+ 如果后续在这个模块中继续生长出数据对齐、传播、远端回灌、副本协同或持久化桥接能力,应优先把它们理解为“围绕运行时存储结构发生的后续语义”,而不是让这些能力反过来改写 Storage 的父边界。换句话说,`sync`、`replication` 或类似方向可以成为这个模块未来的组成部分,但它们应建立在清楚的存储结构语义之上,而不是取代这些结构语义本身。
59
+
60
+ ### JSDoc 注释格式要求
61
+
62
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
63
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
64
+ - 如果描述后还有其他内容,应在描述后加一个空行。
65
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
66
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
67
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
68
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
69
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
70
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
71
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
72
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
73
+
74
+ ### 实现规范要求
75
+
76
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
77
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
78
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
79
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
80
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
81
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
82
+ - 辅助元素永远不要公开导出。
83
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/storage/internal.ts`。
84
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
85
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
86
+ - 子模块不需要有自己的 `README.md`。
87
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
88
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
89
+ - 与 storage 相关的实现应优先围绕存储结构边界、唯一标识或命名语义、变更管理、查询与订阅、快照隔离、以及与外部系统的集成接点组织,避免把具体宿主介质细节或业务缓存策略混入模块边界。
90
+
91
+ ### 导出策略要求
92
+
93
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
94
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
95
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
96
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的运行时存储语义,而不是某段宿主实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
97
+
98
+ ### 测试要求
99
+
100
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
101
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
102
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
103
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
104
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
105
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
106
+ - 模块的单元测试文件目录是 `./tests/unit/storage`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/storage/<sub-module-name>`。
107
+ - 对这个模块来说,测试应优先覆盖唯一标识约束、查询与批量操作、变更事件通知、快照隔离、乐观更新或合并更新的语义边界、删除与回滚路径,以及运行时存储结构与外部系统接点之间的协同行为。
@@ -0,0 +1 @@
1
+ export * from "./table.ts"
@@ -0,0 +1,449 @@
1
+ import type { BaseEvents } from "#Source/event/index.ts"
2
+ import { EventManager } from "#Source/event/index.ts"
3
+ import { Mutex } from "#Source/orchestration/index.ts"
4
+
5
+ /**
6
+ * 表式运行时存储结构的事件集合。
7
+ */
8
+ export interface TableEvents<Row extends BaseRow = BaseRow> extends BaseEvents {
9
+ valueChanged: (rows: Row[]) => void
10
+ }
11
+
12
+ /**
13
+ * 表行数据的基础结构。
14
+ */
15
+ export type BaseRow = Record<string, unknown>
16
+
17
+ /**
18
+ * 表式运行时存储结构的构造选项。
19
+ */
20
+ export interface BaseTableOptions<Row extends BaseRow = BaseRow> {
21
+ /**
22
+ * 返回行的唯一标识。
23
+ *
24
+ * 不要在这个回调里修改行数据。
25
+ */
26
+ uniqueGetter: (row: Row) => string
27
+ }
28
+
29
+ /**
30
+ * 以唯一标识组织行数据集合的基础表模型。
31
+ */
32
+ export abstract class BaseTable<RowType extends BaseRow = BaseRow> {
33
+ protected options: BaseTableOptions<RowType>
34
+ protected map: Map<string, RowType>
35
+ protected mutex: Mutex
36
+
37
+ /**
38
+ * 表数据变更事件管理器。
39
+ */
40
+ event: EventManager<TableEvents<RowType>>
41
+
42
+ /**
43
+ * 创建一个基础表实例。
44
+ */
45
+ constructor(options: BaseTableOptions<RowType>) {
46
+ this.options = options
47
+ this.map = new Map()
48
+ this.mutex = new Mutex()
49
+ this.event = new EventManager()
50
+ }
51
+
52
+ protected getUnique(row: RowType): string {
53
+ return this.options.uniqueGetter(row)
54
+ }
55
+
56
+ private internalEmitValueChanged(): void {
57
+ this.event.emit("valueChanged", structuredClone(Array.from(this.map.values())))
58
+ }
59
+
60
+ private internalAssertBatchRowsHaveUniqueValues(rows: RowType[]): void {
61
+ const uniqueSet = new Set<string>()
62
+ for (const row of rows) {
63
+ const unique = this.getUnique(row)
64
+ if (uniqueSet.has(unique)) {
65
+ throw new Error(`Duplicate row unique "${unique}" found in batch, failed row: ${JSON.stringify(row)}`)
66
+ }
67
+ uniqueSet.add(unique)
68
+ }
69
+ }
70
+
71
+ private internalFindRows(getter: (row: RowType) => boolean): RowType[] {
72
+ const matchedRows: RowType[] = []
73
+ for (const row of this.map.values()) {
74
+ if (getter(structuredClone(row))) {
75
+ matchedRows.push(row)
76
+ }
77
+ }
78
+ return matchedRows
79
+ }
80
+
81
+ private internalSetRows(rows: RowType[]): void {
82
+ for (const row of structuredClone(rows)) {
83
+ const unique = this.getUnique(row)
84
+ this.map.set(unique, row)
85
+ }
86
+ this.internalEmitValueChanged()
87
+ }
88
+
89
+ private internalUnsetRows(rows: RowType[]): void {
90
+ for (const row of structuredClone(rows)) {
91
+ const unique = this.getUnique(row)
92
+ this.map.delete(unique)
93
+ }
94
+ this.internalEmitValueChanged()
95
+ }
96
+
97
+ /**
98
+ * 清空整张表。
99
+ */
100
+ async clearTable(): Promise<void> {
101
+ return await this.mutex.runExclusive(() => {
102
+ this.map.clear()
103
+ this.internalEmitValueChanged()
104
+ })
105
+ }
106
+
107
+ /**
108
+ * 按条件读取第一条匹配的行快照。
109
+ *
110
+ * @returns { Promise<RowType | undefined> } Row, or undefined if not found.
111
+ */
112
+ async getRow(getter: (row: RowType) => boolean): Promise<RowType | undefined> {
113
+ return await this.mutex.runExclusive(() => {
114
+ const row = this.internalFindRows(getter)[0]
115
+ return structuredClone(row)
116
+ })
117
+ }
118
+
119
+ /**
120
+ * 按条件读取全部匹配的行快照。
121
+ *
122
+ * @returns { Promise<RowType[]> } Rows.
123
+ */
124
+ async getRows(getter: (row: RowType) => boolean): Promise<RowType[]> {
125
+ return await this.mutex.runExclusive(() => {
126
+ const rows = this.internalFindRows(getter)
127
+ return structuredClone(rows)
128
+ })
129
+ }
130
+
131
+ /**
132
+ * 安全插入一条新行。
133
+ *
134
+ * @returns { Promise<RowType> } Inserted row.
135
+ * @throws { Error } If row with unique already exists.
136
+ */
137
+ async safeInsertRow(row: RowType): Promise<RowType> {
138
+ return await this.mutex.runExclusive(() => {
139
+ const unique = this.getUnique(row)
140
+ if (this.map.has(unique)) {
141
+ throw new Error(`Row with unique "${unique}" already exists, failed row: ${JSON.stringify(row)}`)
142
+ }
143
+ this.internalSetRows([row])
144
+ return structuredClone(row)
145
+ })
146
+ }
147
+
148
+ /**
149
+ * 插入一条新行;若唯一标识已存在则返回 undefined。
150
+ *
151
+ * @returns { Promise<RowType | undefined> } Inserted row, or undefined if row with unique already exists.
152
+ */
153
+ async insertRow(row: RowType): Promise<RowType | undefined> {
154
+ try {
155
+ return await this.safeInsertRow(row)
156
+ }
157
+ catch {
158
+ return undefined
159
+ }
160
+ }
161
+
162
+ /**
163
+ * 安全插入多条新行。
164
+ *
165
+ * @returns { Promise<RowType[]> } Inserted rows.
166
+ * @throws { Error } If row with unique already exists.
167
+ */
168
+ async safeInsertRows(rows: RowType[]): Promise<RowType[]> {
169
+ return await this.mutex.runExclusive(() => {
170
+ this.internalAssertBatchRowsHaveUniqueValues(rows)
171
+ const toInsertRows: RowType[] = []
172
+ for (const row of rows) {
173
+ const unique = this.getUnique(row)
174
+ if (this.map.has(unique)) {
175
+ throw new Error(`Row with unique "${unique}" already exists, failed row: ${JSON.stringify(row)}`)
176
+ }
177
+ toInsertRows.push(row)
178
+ }
179
+ this.internalSetRows(toInsertRows)
180
+ return structuredClone(rows)
181
+ })
182
+ }
183
+
184
+ /**
185
+ * 按唯一标识做浅合并更新,并返回更新后的行。
186
+ *
187
+ * @throws { Error } If row not found.
188
+ */
189
+ async safeSimpleUpdateRow(row: RowType): Promise<RowType> {
190
+ return await this.mutex.runExclusive(() => {
191
+ const unique = this.getUnique(row)
192
+ const existRow = this.map.get(unique)
193
+ if (existRow === undefined) {
194
+ throw new Error(`Row not found, target row: ${JSON.stringify(row)}`)
195
+ }
196
+ const updatedRow = { ...existRow, ...row }
197
+ this.internalSetRows([updatedRow])
198
+ return structuredClone(updatedRow)
199
+ })
200
+ }
201
+
202
+ /**
203
+ * 按唯一标识做浅合并更新;若行不存在则返回 undefined。
204
+ */
205
+ async simpleUpdateRow(row: RowType): Promise<RowType | undefined> {
206
+ try {
207
+ return await this.safeSimpleUpdateRow(row)
208
+ }
209
+ catch {
210
+ return undefined
211
+ }
212
+ }
213
+
214
+ /**
215
+ * 按唯一标识批量做浅合并更新,并返回更新后的行。
216
+ *
217
+ * @throws { Error } If any row is not found or the batch contains duplicate uniques.
218
+ */
219
+ async safeSimpleUpdateRows(rows: RowType[]): Promise<RowType[]> {
220
+ return await this.mutex.runExclusive(() => {
221
+ this.internalAssertBatchRowsHaveUniqueValues(rows)
222
+ const updatedRows: RowType[] = []
223
+ for (const row of rows) {
224
+ const unique = this.getUnique(row)
225
+ const existRow = this.map.get(unique)
226
+ if (existRow === undefined) {
227
+ throw new Error(`Row not found, target row: ${JSON.stringify(row)}`)
228
+ }
229
+ updatedRows.push({ ...existRow, ...row })
230
+ }
231
+ this.internalSetRows(updatedRows)
232
+ return structuredClone(updatedRows)
233
+ })
234
+ }
235
+
236
+ /**
237
+ * 按条件查找第一条行并用 updater 生成新值。
238
+ *
239
+ * @returns { Promise<RowType> } Updated row.
240
+ * @throws { Error } If row not found, or unique changed between before and after.
241
+ */
242
+ async safeUpdateRow(getter: (row: RowType) => boolean, updater: (row: RowType) => RowType): Promise<RowType> {
243
+ return await this.mutex.runExclusive(() => {
244
+ const row = this.internalFindRows(getter)[0]
245
+ if (row === undefined) {
246
+ throw new Error("Row not found.")
247
+ }
248
+ const oldUnique = this.getUnique(row)
249
+ const newRow = updater(structuredClone(row))
250
+ const newUnique = this.getUnique(newRow)
251
+ if (oldUnique !== newUnique) {
252
+ throw new Error(`Unique cannot be changed, before row: ${JSON.stringify(row)}, after row: ${JSON.stringify(newRow)}`)
253
+ }
254
+ this.internalSetRows([newRow])
255
+ return structuredClone(newRow)
256
+ })
257
+ }
258
+
259
+ /**
260
+ * 按条件查找第一条行并更新;若未找到或唯一标识被改变则返回 undefined。
261
+ *
262
+ * @returns { Promise<RowType | undefined> } Updated row, or undefined if row not found, or unique changed between before and after.
263
+ */
264
+ async updateRow(getter: (row: RowType) => boolean, updater: (row: RowType) => RowType): Promise<RowType | undefined> {
265
+ try {
266
+ return await this.safeUpdateRow(getter, updater)
267
+ }
268
+ catch {
269
+ return undefined
270
+ }
271
+ }
272
+
273
+ /**
274
+ * 按条件批量更新匹配到的全部行。
275
+ *
276
+ * @returns { Promise<RowType[]> } Updated rows.
277
+ * @throws { Error } If unique changed between before and after.
278
+ */
279
+ async updateRows(getter: (row: RowType) => boolean, updater: (row: RowType) => RowType): Promise<RowType[]> {
280
+ return await this.mutex.runExclusive(() => {
281
+ const rows = this.internalFindRows(getter)
282
+ const updatedRows: RowType[] = []
283
+ for (const row of rows) {
284
+ const oldUnique = this.getUnique(row)
285
+ const newRow = updater(structuredClone(row))
286
+ const newUnique = this.getUnique(newRow)
287
+ if (oldUnique !== newUnique) {
288
+ throw new Error(`Unique cannot be changed, before row: ${JSON.stringify(row)}, after row: ${JSON.stringify(newRow)}`)
289
+ }
290
+ updatedRows.push(newRow)
291
+ }
292
+ this.internalSetRows(updatedRows)
293
+ return structuredClone(updatedRows)
294
+ })
295
+ }
296
+
297
+ /**
298
+ * 按唯一标识插入或浅合并一条行。
299
+ *
300
+ * @returns { Promise<RowType> } Upserted row.
301
+ */
302
+ async upsertRow(newRow: RowType): Promise<RowType> {
303
+ const upserted = await this.mutex.runExclusive(() => {
304
+ const unique = this.getUnique(newRow)
305
+ const existRow = this.map.get(unique)
306
+ if (existRow === undefined) {
307
+ this.internalSetRows([newRow])
308
+ return structuredClone(newRow)
309
+ }
310
+ else {
311
+ const updatedRow = { ...existRow, ...newRow }
312
+ this.internalSetRows([updatedRow])
313
+ return structuredClone(updatedRow)
314
+ }
315
+ })
316
+ return upserted
317
+ }
318
+
319
+ /**
320
+ * 按唯一标识批量插入或浅合并多条行。
321
+ *
322
+ * @returns { Promise<RowType[]> } Upserted rows.
323
+ * @throws { Error } If the batch contains duplicate uniques.
324
+ */
325
+ async upsertRows(rows: RowType[]): Promise<RowType[]> {
326
+ return await this.mutex.runExclusive(() => {
327
+ this.internalAssertBatchRowsHaveUniqueValues(rows)
328
+ const upsertedRows: RowType[] = []
329
+ for (const row of rows) {
330
+ const unique = this.getUnique(row)
331
+ const existRow = this.map.get(unique)
332
+ if (existRow === undefined) {
333
+ upsertedRows.push(row)
334
+ }
335
+ else {
336
+ upsertedRows.push({ ...existRow, ...row })
337
+ }
338
+ }
339
+ this.internalSetRows(upsertedRows)
340
+ return structuredClone(upsertedRows)
341
+ })
342
+ }
343
+
344
+ /**
345
+ * 按唯一标识安全删除一条行,并返回被删除的存量行。
346
+ *
347
+ * @throws { Error } If row not found.
348
+ */
349
+ async safeSimpleDeleteRow(row: RowType): Promise<RowType> {
350
+ return await this.mutex.runExclusive(() => {
351
+ const unique = this.getUnique(row)
352
+ const existRow = this.map.get(unique)
353
+ if (existRow === undefined) {
354
+ throw new Error(`Row not found, target row: ${JSON.stringify(row)}`)
355
+ }
356
+ this.internalUnsetRows([existRow])
357
+ return structuredClone(existRow)
358
+ })
359
+ }
360
+
361
+ /**
362
+ * 按唯一标识删除一条行;若不存在则返回 undefined。
363
+ */
364
+ async simpleDeleteRow(row: RowType): Promise<RowType | undefined> {
365
+ try {
366
+ return await this.safeSimpleDeleteRow(row)
367
+ }
368
+ catch {
369
+ return undefined
370
+ }
371
+ }
372
+
373
+ /**
374
+ * 按唯一标识批量删除多条行,并返回被删除的存量行。
375
+ *
376
+ * @throws { Error } If any row is not found or the batch contains duplicate uniques.
377
+ */
378
+ async simpleDeleteRows(rows: RowType[]): Promise<RowType[]> {
379
+ return await this.mutex.runExclusive(() => {
380
+ this.internalAssertBatchRowsHaveUniqueValues(rows)
381
+ const toDeleteRows: RowType[] = []
382
+ for (const row of rows) {
383
+ const unique = this.getUnique(row)
384
+ const existRow = this.map.get(unique)
385
+ if (existRow === undefined) {
386
+ throw new Error(`Row not found, target row: ${JSON.stringify(row)}`)
387
+ }
388
+ toDeleteRows.push(existRow)
389
+ }
390
+ this.internalUnsetRows(toDeleteRows)
391
+ return structuredClone(toDeleteRows)
392
+ })
393
+ }
394
+
395
+ /**
396
+ * 按条件安全删除唯一一条匹配行。
397
+ *
398
+ * @returns { Promise<RowType> } Deleted row.
399
+ * @throws { Error } If multiple rows found, or row not found.
400
+ */
401
+ async safeDeleteRow(getter: (row: RowType) => boolean): Promise<RowType> {
402
+ return await this.mutex.runExclusive(() => {
403
+ const rows = this.internalFindRows(getter)
404
+ if (rows.length > 1) {
405
+ throw new Error(`Multiple rows found, target rows: ${JSON.stringify(rows)}`)
406
+ }
407
+ const row = rows[0]
408
+ if (row === undefined) {
409
+ throw new Error(`Row not found.`)
410
+ }
411
+ this.internalUnsetRows([row])
412
+ return structuredClone(row)
413
+ })
414
+ }
415
+
416
+ /**
417
+ * 按条件删除唯一一条匹配行;若不存在则返回 undefined。
418
+ *
419
+ * @returns { Promise<RowType | undefined> } Deleted row, or undefined if row not found.
420
+ * @throws { Error } If multiple rows found.
421
+ */
422
+ async deleteRow(getter: (row: RowType) => boolean): Promise<RowType | undefined> {
423
+ return await this.mutex.runExclusive(() => {
424
+ const rows = this.internalFindRows(getter)
425
+ if (rows.length > 1) {
426
+ throw new Error(`Multiple rows found, target rows: ${JSON.stringify(rows)}`)
427
+ }
428
+ const row = rows[0]
429
+ if (row === undefined) {
430
+ return undefined
431
+ }
432
+ this.internalUnsetRows([row])
433
+ return structuredClone(row)
434
+ })
435
+ }
436
+
437
+ /**
438
+ * 按条件删除全部匹配行。
439
+ *
440
+ * @returns { Promise<RowType[]> } Deleted rows.
441
+ */
442
+ async deleteRows(getter: (row: RowType) => boolean): Promise<RowType[]> {
443
+ return await this.mutex.runExclusive(() => {
444
+ const rows = this.internalFindRows(getter)
445
+ this.internalUnsetRows(rows)
446
+ return structuredClone(rows)
447
+ })
448
+ }
449
+ }
@@ -0,0 +1,86 @@
1
+ # Timer
2
+
3
+ ## Description
4
+
5
+ Timer 模块提供围绕时间、时点、持续时间以及定时触发相关语义的基础建模能力,用于在不同运行时中组织一类可以长期维护的时间边界。
6
+
7
+ 它关注的不是对 `setTimeout`、`setInterval` 之类宿主 API 做一层临时包装,而是“哪些与时间有关的能力值得被抽象成稳定模型”。当前这里包含 expiration 这一子模块,用于表达过期状态及其派生剩余时间;但 Timer 本身不应被理解为只等于“过期倒计时”,后续也可能继续承载 timer engine 等其它与时间调度相关、且边界清楚的子问题域。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 Timer 模块时,重点应放在“时间语义如何被建模”为稳定公共边界,而不是把它当成各种计时技巧的收纳箱。它适合表达的是这类问题:某个命名目标在什么时刻进入某种时间状态,哪些结果可以从这些时间状态稳定派生出来,以及时间推进过程中哪些变化值得被调度或被观察。
12
+
13
+ 因此,Timer 模块适合放在以下边界中:
14
+
15
+ - 你需要围绕绝对时点、剩余时间或时间推进过程定义一组可长期复用的基础语义,而不是在业务里散落零碎的计时逻辑。
16
+ - 你需要把“时间到了会发生什么”与“业务具体怎么处理”分离开,让上层只接入稳定的状态与事件模型。
17
+ - 你预计未来会出现多个彼此相关但职责不同的时间子问题域,例如过期管理、调度引擎、时钟适配或时间驱动状态派生,并希望它们在同一父边界下协作。
18
+
19
+ 同时也要守住几个边界:
20
+
21
+ - Timer 模块表达的是时间相关模型,不负责替代任务队列、工作流编排、分布式调度或动画系统。
22
+ - 某个能力若只是对单一宿主 API 的便捷封装,而没有形成稳定的时间语义,通常不应进入这个模块。
23
+ - 不是所有“和时间有关”的代码都适合放进来。只有当它能表达清楚的问题域边界,并能对外做出长期语义承诺时,才适合成为该模块或其子模块的一部分。
24
+
25
+ ## For Using
26
+
27
+ 当你需要复用一类已经被明确建模的时间能力,而不想在业务代码里反复拼接时间戳比较、到期检查、剩余时长换算和定时触发逻辑时,可以从 Timer 模块中选择合适的子模块接入。
28
+
29
+ 从当前使用角度看,Timer 模块主要承载两类方向:一类是围绕“某个命名目标何时到期”的过期状态管理能力;另一类是围绕这些基础时间状态继续派生出的观测、调度或驱动能力。当前仓库中已经落地的是 expiration 子模块,它更适合处理具名过期状态、剩余时间观察以及暂停恢复等问题;未来若加入其它子模块,也应继续围绕更广义的时间模型边界展开,而不是让父模块退化成只服务某一个现有子模块的命名容器。
30
+
31
+ 接入时更合理的方式,是先判断你的问题是否确实属于“时间模型”边界,再决定应使用哪个子模块。若问题本质上是过期状态及其派生值,应从 expiration 入手;若未来出现更偏向调度引擎或时钟抽象的能力,则应在相应子模块边界内理解和使用,而不是把所有时间问题都压到同一个现有实现上。
32
+
33
+ ## For Contributing
34
+
35
+ 贡献 Timer 模块时,应首先判断新增内容表达的是不是稳定的时间问题域,而不是某段实现中一时方便的计时技巧。尤其需要注意,当前存在 expiration 子模块,并不意味着 Timer 的整体边界已经被它完全定义;后续若出现 timer engine、时钟适配、时间驱动协调等其它子问题域,只要它们边界稳定且能被独立理解,就仍然适合继续放在 Timer 之下。
36
+
37
+ 扩展时应避免两类常见偏差。一类是把 Timer 缩窄成“只处理过期”的专用目录,导致其它合理的时间子模型无处安放;另一类是把各种与时间沾边的实现都塞进来,最终让模块失去清楚边界。更稳妥的做法,是优先明确新增能力到底是在表达时间状态、时间推进、时间派生,还是某种宿主专属调度技巧;只有前者形成了稳定问题域,才值得进入这个模块。
38
+
39
+ ### JSDoc 注释格式要求
40
+
41
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
42
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
43
+ - 如果描述后还有其他内容,应在描述后加一个空行。
44
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
45
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
46
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
47
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
48
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
49
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
50
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
51
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
52
+
53
+ ### 实现规范要求
54
+
55
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
56
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
57
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
58
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
59
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
60
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
61
+ - 辅助元素永远不要公开导出。
62
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/timer/internal.ts`。
63
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
64
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
65
+ - 子模块不需要有自己的 `README.md`。
66
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
67
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
68
+ - 与时间相关的实现应优先围绕时间状态、时间推进、时间派生与时间调度边界组织,避免把只服务于某个宿主的临时技巧直接提升为 Timer 的长期公共语义。
69
+
70
+ ### 导出策略要求
71
+
72
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
73
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
74
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
75
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的时间语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
76
+
77
+ ### 测试要求
78
+
79
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
80
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
81
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
82
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
83
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
84
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
85
+ - 模块的单元测试文件目录是 `./tests/unit/timer`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/timer/<sub-module-name>`。
86
+ - 对时间相关能力,应优先覆盖时间推进、暂停恢复、边界时刻、派生状态以及定时器生命周期管理等场景。