@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,90 @@
1
+ # Event
2
+
3
+ ## Description
4
+
5
+ Event 模块提供围绕事件订阅、事件派发与事件代理的通用建模能力,用于在不绑定具体宿主事件系统的前提下组织稳定的事件协作边界。
6
+
7
+ 它关注的不是某个框架、某个 DOM API 或某个类库各自的监听接口差异,而是“事件如何被声明为一组稳定语义、订阅关系如何被管理、以及现有事件源如何被适配进一致边界”这一类基础问题。因此,这个模块更像是一组事件模型能力,而不是某个特定运行时的监听工具集合。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 Event 模块时,首先应把它看作“事件交互边界”的建模层,而不是任意通知机制的杂糅容器。它所要解决的问题,是如何用一套清楚、可维护、可组合的方式表达事件名、订阅者、订阅生命周期以及代理关系,而不是简单地把 `on`、`off`、`emit` 这类常见方法重新包装一遍。
12
+
13
+ 这个模块适合放在以下边界中:
14
+
15
+ - 你需要在应用内部定义一组清楚的事件表(event map),并希望订阅与派发行为都围绕这组语义稳定演进。
16
+ - 你已经有某个现成事件源,但希望对外暴露更可控的订阅关系管理能力,而不是让外部直接依赖底层事件系统细节。
17
+ - 你需要把“单个对象的事件能力”或“多个目标实例的事件能力”适配到统一模型中,避免上层代码到处感知宿主接口差异。
18
+
19
+ 理解这个模块时,还应守住几条边界原则:
20
+
21
+ - Event 模块表达的是订阅与派发语义,不负责任务调度、消息持久化、跨进程传输或状态同步。
22
+ - 它可以代理既有事件系统,但不应因为适配某个宿主接口而把该宿主的临时细节直接上升为模块公共语义。
23
+ - 它关注的是“谁可订阅、何时移除、如何派发、如何桥接现有事件源”,而不是发布订阅架构的全部问题。像消息重放、优先级、背压或分布式广播等更高层语义,通常不应直接落入这个模块。
24
+
25
+ ## For Using
26
+
27
+ 当你希望把事件交互整理成一套明确模型,而不是在业务代码里反复拼接监听器注册、移除与调用逻辑时,可以使用 Event 模块。
28
+
29
+ 从使用角度看,这个模块大致可以分为三类能力:
30
+
31
+ - 事件管理能力:用于在一组明确定义的事件表上维护订阅关系、支持常规订阅和单次订阅,并按同步或异步方式派发事件。
32
+ - 实例级事件代理能力:用于把单个已有事件目标适配为受控代理,在不暴露底层订阅细节的前提下管理多个下游订阅者。
33
+ - 类型级事件代理能力:用于把多个目标实例统一纳入同一代理模型,使上层可以用一致方式管理不同目标上的事件订阅关系。
34
+
35
+ 更合适的接入方式,是先明确事件名及其参数语义,再选择直接使用事件管理器,或通过代理把既有事件源收束到相同边界里。这样可以让事件系统的长期演进围绕模块语义展开,而不是围绕某个宿主 API 的偶然形态展开。
36
+
37
+ ## For Contributing
38
+
39
+ 为 Event 模块贡献内容时,优先判断新增能力是否真的在澄清事件边界,而不是只是在补一个看起来方便的监听包装。这个模块应长期服务于“事件表定义”“订阅生命周期管理”“派发语义”“现有事件源适配”这几类稳定问题。如果一个能力的成立必须依赖某个框架、某个宿主环境或某个业务流转约定,那么它通常不应直接成为该模块的公共组成部分。
40
+
41
+ 扩展这个模块时,应特别警惕以下倾向:把临时监听技巧直接暴露给调用方;把目标事件源的实现细节泄漏进公共 API;把事件代理扩展成通用消息总线;或者把本来应该在上层处理的并发、缓存、持久化、重放等语义塞回到事件模块内部。文档应说明为什么这些边界成立,以及后续演进时哪些原则不能被破坏,而不是复述当前实现的局部技巧。
42
+
43
+ ### JSDoc 注释格式要求
44
+
45
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
46
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
47
+ - 如果描述后还有其他内容,应在描述后加一个空行。
48
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
49
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
50
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
51
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
52
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
53
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
54
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
55
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
56
+
57
+ ### 实现规范要求
58
+
59
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
60
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
61
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
62
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
63
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
64
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
65
+ - 辅助元素永远不要公开导出。
66
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/event/internal.ts`。
67
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
68
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
69
+ - 子模块不需要有自己的 `README.md`。
70
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
71
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
72
+ - 与事件相关的实现应优先围绕事件表、订阅关系、代理桥接与生命周期管理组织,避免把不属于事件语义本身的上层策略混入模块边界。
73
+
74
+ ### 导出策略要求
75
+
76
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
77
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
78
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
79
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的事件语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
80
+
81
+ ### 测试要求
82
+
83
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
84
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
85
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
86
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
87
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
88
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
89
+ - 模块的单元测试文件目录是 `./tests/unit/event`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/event/<sub-module-name>`。
90
+ - 对这个模块来说,测试应优先覆盖订阅注册与移除、单次订阅、错误处理、同步与异步派发顺序,以及代理对象与底层目标之间的桥接行为。
@@ -0,0 +1,228 @@
1
+ import type { InternalProxySubscriberEntryMap } from "./internal.ts"
2
+ import type {
3
+ BaseEvents,
4
+ SubscriberEntry,
5
+ } from "./common.ts"
6
+
7
+ /**
8
+ * 表示 ClassEventProxy 用于接入目标实例及其事件系统的适配器。
9
+ */
10
+ export interface ClassEventProxyTargetAdapter<Target, Events extends BaseEvents> {
11
+ emit<K extends keyof Events>(target: Target, event: K, ...args: Parameters<Events[K]>): boolean
12
+ subscribe<K extends keyof Events>(target: Target, event: K, subscriber: Events[K]): void
13
+ unsubscribe<K extends keyof Events>(target: Target, event: K, subscriber: Events[K]): void
14
+ }
15
+
16
+ /**
17
+ * 表示 ClassEventProxy 的构造选项。
18
+ */
19
+ export interface ClassEventProxyOptions<Target, Events extends BaseEvents> {
20
+ targetAdapter: ClassEventProxyTargetAdapter<Target, Events>
21
+ /**
22
+ * 在代理管理的订阅者执行出错时接收错误与对应订阅项。
23
+ */
24
+ onSubscriberError?: (subscriberEntry: SubscriberEntry<Events, keyof Events>, error: unknown) => void
25
+ }
26
+
27
+ /**
28
+ * 将一组目标实例的事件系统适配为统一的代理订阅模型。
29
+ */
30
+ export class ClassEventProxy<Target, Events extends BaseEvents> {
31
+ protected options: ClassEventProxyOptions<Target, Events>
32
+ protected subscribers: Map<Target, InternalProxySubscriberEntryMap<Events>>
33
+
34
+ constructor(options: ClassEventProxyOptions<Target, Events>) {
35
+ this.options = options
36
+ this.subscribers = new Map()
37
+ }
38
+
39
+ /**
40
+ * 检查给定目标和事件上是否已经管理指定订阅者。
41
+ */
42
+ has<K extends keyof Events>(
43
+ target: Target, event: K, subscriber: Events[K]
44
+ ): boolean {
45
+ const proxySubscriberEntryMap = this.subscribers.get(target)
46
+ if (proxySubscriberEntryMap === undefined) {
47
+ return false
48
+ }
49
+
50
+ const proxySubscriberEntry = proxySubscriberEntryMap[event]
51
+ if (proxySubscriberEntry === undefined) {
52
+ return false
53
+ }
54
+
55
+ const has = proxySubscriberEntry.managedSubscribers.has(subscriber)
56
+ return has
57
+ }
58
+
59
+ protected internalSubscribe<K extends keyof Events>(
60
+ target: Target, event: K, subscriber: Events[K], once: boolean
61
+ ): SubscriberEntry<Events, K> {
62
+ const proxySubscriberEntryMap = this.subscribers.get(target) ?? ({} as InternalProxySubscriberEntryMap<Events>)
63
+ this.subscribers.set(target, proxySubscriberEntryMap)
64
+
65
+ let proxySubscriberEntry = proxySubscriberEntryMap[event]
66
+
67
+ if (proxySubscriberEntry === undefined) {
68
+ const managedSubscribers = new Map<Events[K], SubscriberEntry<Events, K>>()
69
+ // oxlint-disable-next-line no-unsafe-type-assertion
70
+ const proxySubscriber = ((...args: Parameters<Events[K]>) => {
71
+ const managedSubscriberEntries = [...managedSubscribers.values()]
72
+ for (const managedSubscriberEntry of managedSubscriberEntries) {
73
+ try {
74
+ managedSubscriberEntry.subscriber(...args)
75
+ } catch (exception) {
76
+ if (this.options.onSubscriberError !== undefined) {
77
+ this.options.onSubscriberError(managedSubscriberEntry, exception)
78
+ } else {
79
+ console.error(`Error occurred while emitting event "${String(event)}" of target:`, exception)
80
+ }
81
+ } finally {
82
+ if (managedSubscriberEntry.once === true) {
83
+ managedSubscriberEntry.unsubscribe()
84
+ }
85
+ }
86
+ }
87
+ }) as Events[K]
88
+
89
+ proxySubscriberEntry = {
90
+ proxySubscriber,
91
+ managedSubscribers
92
+ }
93
+ proxySubscriberEntryMap[event] = proxySubscriberEntry
94
+ this.options.targetAdapter.subscribe(target, event, proxySubscriber)
95
+ }
96
+
97
+ const existingSubscriberEntry = proxySubscriberEntry.managedSubscribers.get(subscriber)
98
+ if (existingSubscriberEntry !== undefined) {
99
+ return existingSubscriberEntry
100
+ }
101
+
102
+ const unsubscribe = (): void => {
103
+ this.unsubscribe(target, event, subscriber)
104
+ }
105
+ const newSubscriberEntry: SubscriberEntry<Events, K> = {
106
+ event,
107
+ once,
108
+ subscriber,
109
+ unsubscribe,
110
+ }
111
+ proxySubscriberEntry.managedSubscribers.set(subscriber, newSubscriberEntry)
112
+
113
+ return newSubscriberEntry
114
+ }
115
+
116
+ /**
117
+ * 为指定目标事件添加常规订阅者,并在首次接入时创建底层代理订阅。
118
+ */
119
+ subscribe<K extends keyof Events>(
120
+ target: Target, event: K, subscriber: Events[K]
121
+ ): SubscriberEntry<Events, K> {
122
+ return this.internalSubscribe(target, event, subscriber, false)
123
+ }
124
+
125
+ /**
126
+ * 为指定目标事件添加只触发一次的订阅者。
127
+ */
128
+ subscribeOnce<K extends keyof Events>(
129
+ target: Target, event: K, listener: Events[K]
130
+ ): SubscriberEntry<Events, K> {
131
+ return this.internalSubscribe(target, event, listener, true)
132
+ }
133
+
134
+ /**
135
+ * 移除指定目标事件上的给定订阅者。
136
+ */
137
+ unsubscribe<K extends keyof Events>(
138
+ target: Target, event: K, listener: Events[K]
139
+ ): this {
140
+ const proxySubscriberEntryMap = this.subscribers.get(target)
141
+ if (proxySubscriberEntryMap === undefined) {
142
+ return this
143
+ }
144
+
145
+ const proxySubscriberEntry = proxySubscriberEntryMap[event]
146
+ if (proxySubscriberEntry === undefined) {
147
+ return this
148
+ }
149
+
150
+ proxySubscriberEntry.managedSubscribers.delete(listener)
151
+ if (proxySubscriberEntry.managedSubscribers.size === 0) {
152
+ this.options.targetAdapter.unsubscribe(target, event, proxySubscriberEntry.proxySubscriber)
153
+ delete proxySubscriberEntryMap[event]
154
+ if (Object.keys(proxySubscriberEntryMap).length === 0) {
155
+ this.subscribers.delete(target)
156
+ }
157
+ }
158
+
159
+ return this
160
+ }
161
+
162
+ /**
163
+ * 移除指定目标某个事件上的全部代理订阅者。
164
+ */
165
+ removeSubscribersOfEvent<K extends keyof Events>(
166
+ target: Target, event: K
167
+ ): this {
168
+ const proxySubscriberEntryMap = this.subscribers.get(target)
169
+ if (proxySubscriberEntryMap === undefined) {
170
+ return this
171
+ }
172
+
173
+ const proxySubscriberEntry = proxySubscriberEntryMap[event]
174
+ if (proxySubscriberEntry === undefined) {
175
+ return this
176
+ }
177
+
178
+ proxySubscriberEntry.managedSubscribers.clear()
179
+ this.options.targetAdapter.unsubscribe(target, event, proxySubscriberEntry.proxySubscriber)
180
+ delete proxySubscriberEntryMap[event]
181
+ if (Object.keys(proxySubscriberEntryMap).length === 0) {
182
+ this.subscribers.delete(target)
183
+ }
184
+
185
+ return this
186
+ }
187
+
188
+ /**
189
+ * 移除指定目标上的全部代理订阅者。
190
+ */
191
+ removeSubscribersOfTarget(target: Target): this {
192
+ const proxySubscriberEntryMap = this.subscribers.get(target)
193
+ if (proxySubscriberEntryMap === undefined) {
194
+ return this
195
+ }
196
+
197
+ const events = Object.keys(proxySubscriberEntryMap)
198
+ for (const event of events) {
199
+ this.removeSubscribersOfEvent(target, event)
200
+ }
201
+ this.subscribers.delete(target)
202
+
203
+ return this
204
+ }
205
+
206
+ /**
207
+ * 移除当前代理管理的全部目标订阅关系。
208
+ */
209
+ removeAllSubscribers(): this {
210
+ const targets = Array.from(this.subscribers.keys())
211
+
212
+ for (const target of targets) {
213
+ this.removeSubscribersOfTarget(target)
214
+ }
215
+ this.subscribers.clear()
216
+
217
+ return this
218
+ }
219
+
220
+ /**
221
+ * 通过底层适配器向指定目标派发事件。
222
+ */
223
+ emit<K extends keyof Events>(
224
+ target: Target, event: K, ...args: Parameters<Events[K]>
225
+ ): boolean {
226
+ return this.options.targetAdapter.emit(target, event, ...args)
227
+ }
228
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 表示事件表中允许的基础订阅者函数类型。
3
+ */
4
+ // oxlint-disable-next-line no-explicit-any
5
+ export type BaseSubscriber = (...args: any[]) => void
6
+ /**
7
+ * 表示由事件名到订阅者函数类型构成的事件表。
8
+ */
9
+ export type BaseEvents = Record<string, BaseSubscriber>
10
+
11
+ /**
12
+ * 表示一次订阅关系的标准化描述。
13
+ */
14
+ export interface SubscriberEntry<Events extends BaseEvents, K extends keyof Events> {
15
+ event: K
16
+ once: boolean
17
+ subscriber: Events[K]
18
+ unsubscribe: () => void
19
+ }
@@ -0,0 +1,203 @@
1
+ import type { InternalSubscriberEntryMap } from "./internal.ts"
2
+ import type { BaseEvents, SubscriberEntry } from "./common.ts"
3
+
4
+ /**
5
+ * 表示 EventManager 的构造选项。
6
+ */
7
+ export interface EventManagerOptions<Events extends BaseEvents> {
8
+ /**
9
+ * 在订阅者执行出错时接收错误与对应订阅项。
10
+ */
11
+ onSubscriberError?: (subscriberEntry: SubscriberEntry<Events, keyof Events>, error: unknown) => void
12
+ }
13
+
14
+ /**
15
+ * 管理同一事件表下的订阅、取消订阅与事件派发。
16
+ */
17
+ export class EventManager<Events extends BaseEvents> {
18
+ protected options: EventManagerOptions<Events>
19
+ /**
20
+ * 每一类事件对应一个内部订阅映射,映射中的 key 是订阅者函数,value 是 SubscriberEntry 对象。
21
+ */
22
+ protected subscribers: {
23
+ [K in keyof Events]?: InternalSubscriberEntryMap<Events, K> | undefined
24
+ }
25
+
26
+ constructor(options?: EventManagerOptions<Events>) {
27
+ this.options = options ?? {}
28
+ this.subscribers = {}
29
+ }
30
+
31
+ protected internalHandleSubscriberError(
32
+ subscriberEntry: SubscriberEntry<Events, keyof Events>,
33
+ error: unknown,
34
+ ): void {
35
+ if (this.options.onSubscriberError !== undefined) {
36
+ this.options.onSubscriberError(subscriberEntry, error)
37
+ } else {
38
+ console.error(`Error occurred while emitting event "${String(subscriberEntry.event)}":`, error)
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 检查某个事件是否已经注册给定订阅者。
44
+ */
45
+ has<K extends keyof Events>(event: K, subscriber: Events[K]): boolean {
46
+ const subscriberEntryMap = this.subscribers[event]
47
+ if (subscriberEntryMap === undefined) {
48
+ return false
49
+ }
50
+
51
+ const has = subscriberEntryMap.has(subscriber)
52
+ return has
53
+ }
54
+
55
+ protected internalSubscribe<K extends keyof Events>(event: K, subscriber: Events[K], once: boolean): SubscriberEntry<Events, K> {
56
+ const subscriberEntryMap = this.subscribers[event] ?? new Map<Events[K], SubscriberEntry<Events, K>>()
57
+ this.subscribers[event] = subscriberEntryMap
58
+
59
+ const existingSubscriberEntry = subscriberEntryMap.get(subscriber)
60
+ if (existingSubscriberEntry !== undefined) {
61
+ return existingSubscriberEntry
62
+ }
63
+
64
+ const unsubscribe = (): void => {
65
+ this.unsubscribe(event, subscriber)
66
+ }
67
+ const newSubscriberEntry: SubscriberEntry<Events, K> = {
68
+ event,
69
+ once,
70
+ subscriber,
71
+ unsubscribe,
72
+ }
73
+ subscriberEntryMap.set(subscriber, newSubscriberEntry)
74
+
75
+ return newSubscriberEntry
76
+ }
77
+
78
+ /**
79
+ * 为指定事件添加常规订阅者。
80
+ */
81
+ subscribe<K extends keyof Events>(event: K, subscriber: Events[K]): SubscriberEntry<Events, K> {
82
+ return this.internalSubscribe(event, subscriber, false)
83
+ }
84
+
85
+ /**
86
+ * 为指定事件添加只触发一次的订阅者。
87
+ */
88
+ subscribeOnce<K extends keyof Events>(event: K, subscriber: Events[K]): SubscriberEntry<Events, K> {
89
+ return this.internalSubscribe(event, subscriber, true)
90
+ }
91
+
92
+ /**
93
+ * 移除指定事件上的给定订阅者。
94
+ */
95
+ unsubscribe<K extends keyof Events>(event: K, subscriber: Events[K]): this {
96
+ const subscriberEntryMap = this.subscribers[event]
97
+ if (subscriberEntryMap === undefined) {
98
+ return this
99
+ }
100
+
101
+ subscriberEntryMap.delete(subscriber)
102
+ if (subscriberEntryMap.size === 0) {
103
+ delete this.subscribers[event]
104
+ }
105
+
106
+ return this
107
+ }
108
+
109
+ /**
110
+ * 移除指定事件上的全部订阅者。
111
+ */
112
+ removeSubscribersOfEvent<K extends keyof Events>(event: K): this {
113
+ delete this.subscribers[event]
114
+ return this
115
+ }
116
+
117
+ /**
118
+ * 移除当前管理器上的全部订阅者。
119
+ */
120
+ removeAllSubscribers(): this {
121
+ this.subscribers = {}
122
+ return this
123
+ }
124
+
125
+ /**
126
+ * 同步派发指定事件。
127
+ */
128
+ emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): boolean {
129
+ const subscriberEntryMap = this.subscribers[event]
130
+ if (subscriberEntryMap === undefined) {
131
+ return false
132
+ }
133
+
134
+ // Create a copy of the subscriber entries to avoid issues when
135
+ // subscribers are modified during event emission
136
+ const subscriberEntries = [...subscriberEntryMap.values()]
137
+ for (const subscriberEntry of subscriberEntries) {
138
+ try {
139
+ subscriberEntry.subscriber(...args)
140
+ } catch (exception) {
141
+ this.internalHandleSubscriberError(subscriberEntry, exception)
142
+ } finally {
143
+ if (subscriberEntry.once === true) {
144
+ subscriberEntry.unsubscribe()
145
+ }
146
+ }
147
+ }
148
+
149
+ return true
150
+ }
151
+
152
+ /**
153
+ * 按订阅顺序异步派发指定事件。
154
+ */
155
+ async emitAsync<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): Promise<boolean> {
156
+ const subscriberEntryMap = this.subscribers[event]
157
+ if (subscriberEntryMap === undefined) {
158
+ return false
159
+ }
160
+
161
+ const subscriberEntries = [...subscriberEntryMap.values()]
162
+ for (const subscriberEntry of subscriberEntries) {
163
+ try {
164
+ const result = subscriberEntry.subscriber(...args)
165
+ await Promise.resolve(result)
166
+ } catch (exception) {
167
+ this.internalHandleSubscriberError(subscriberEntry, exception)
168
+ } finally {
169
+ if (subscriberEntry.once === true) {
170
+ subscriberEntry.unsubscribe()
171
+ }
172
+ }
173
+ }
174
+
175
+ return true
176
+ }
177
+
178
+ /**
179
+ * 并发等待所有订阅者完成后结束异步派发。
180
+ */
181
+ async emitConcurrentAsync<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): Promise<boolean> {
182
+ const subscriberEntryMap = this.subscribers[event]
183
+ if (subscriberEntryMap === undefined) {
184
+ return false
185
+ }
186
+
187
+ const promises = [...subscriberEntryMap.values()].map(async subscriberEntry => {
188
+ try {
189
+ const result = subscriberEntry.subscriber(...args)
190
+ await Promise.resolve(result)
191
+ } catch (exception) {
192
+ this.internalHandleSubscriberError(subscriberEntry, exception)
193
+ } finally {
194
+ if (subscriberEntry.once === true) {
195
+ subscriberEntry.unsubscribe()
196
+ }
197
+ }
198
+ })
199
+ await Promise.all(promises)
200
+
201
+ return true
202
+ }
203
+ }
@@ -0,0 +1,4 @@
1
+ export type * from "./common.ts"
2
+ export * from "./event-manager.ts"
3
+ export * from "./instance-event-proxy.ts"
4
+ export * from "./class-event-proxy.ts"