@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,186 @@
1
+ import type { InternalProxySubscriberEntryMap } from "./internal.ts"
2
+ import type {
3
+ BaseEvents,
4
+ SubscriberEntry,
5
+ } from "./common.ts"
6
+
7
+ /**
8
+ * 表示 InstanceEventProxy 用于接入底层事件目标的适配器。
9
+ */
10
+ export interface InstanceEventProxyTargetAdapter<Events extends BaseEvents> {
11
+ emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): boolean
12
+ subscribe<K extends keyof Events>(event: K, subscriber: Events[K]): void
13
+ unsubscribe<K extends keyof Events>(event: K, subscriber: Events[K]): void
14
+ }
15
+
16
+ /**
17
+ * 表示 InstanceEventProxy 的构造选项。
18
+ */
19
+ export interface InstanceEventProxyOptions<Events extends BaseEvents> {
20
+ targetAdapter: InstanceEventProxyTargetAdapter<Events>
21
+ /**
22
+ * 在代理管理的订阅者执行出错时接收错误与对应订阅项。
23
+ */
24
+ onSubscriberError?: (subscriberEntry: SubscriberEntry<Events, keyof Events>, error: unknown) => void
25
+ }
26
+
27
+ /**
28
+ * 将单个事件目标适配为可管理下游订阅关系的代理。
29
+ */
30
+ export class InstanceEventProxy<Events extends BaseEvents> {
31
+ protected options: InstanceEventProxyOptions<Events>
32
+ protected subscribers: InternalProxySubscriberEntryMap<Events>
33
+
34
+ constructor(options: InstanceEventProxyOptions<Events>) {
35
+ this.options = options
36
+ this.subscribers = {}
37
+ }
38
+
39
+ /**
40
+ * 检查某个事件是否已经管理给定订阅者。
41
+ */
42
+ has<K extends keyof Events>(
43
+ event: K, subscriber: Events[K]
44
+ ): boolean {
45
+ const proxySubscriberEntry = this.subscribers[event]
46
+ if (proxySubscriberEntry === undefined) {
47
+ return false
48
+ }
49
+
50
+ const has = proxySubscriberEntry.managedSubscribers.has(subscriber)
51
+ return has
52
+ }
53
+
54
+ protected internalSubscribe<K extends keyof Events>(
55
+ event: K, subscriber: Events[K], once: boolean
56
+ ): SubscriberEntry<Events, K> {
57
+ let proxySubscriberEntry = this.subscribers[event]
58
+
59
+ if (proxySubscriberEntry === undefined) {
60
+ const managedSubscribers = new Map<Events[K], SubscriberEntry<Events, K>>()
61
+ // oxlint-disable-next-line no-unsafe-type-assertion
62
+ const proxySubscriber = ((...args: Parameters<Events[K]>) => {
63
+ const managedSubscriberEntries = [...managedSubscribers.values()]
64
+ for (const managedSubscriberEntry of managedSubscriberEntries) {
65
+ try {
66
+ managedSubscriberEntry.subscriber(...args)
67
+ } catch (exception) {
68
+ if (this.options.onSubscriberError !== undefined) {
69
+ this.options.onSubscriberError(managedSubscriberEntry, exception)
70
+ } else {
71
+ console.error(`Error occurred while emitting event "${String(event)}":`, exception)
72
+ }
73
+ } finally {
74
+ if (managedSubscriberEntry.once === true) {
75
+ managedSubscriberEntry.unsubscribe()
76
+ }
77
+ }
78
+ }
79
+ }) as Events[K]
80
+
81
+ proxySubscriberEntry = {
82
+ proxySubscriber,
83
+ managedSubscribers
84
+ }
85
+ this.subscribers[event] = proxySubscriberEntry
86
+ this.options.targetAdapter.subscribe(event, proxySubscriber)
87
+ }
88
+
89
+ const existingSubscriberEntry = proxySubscriberEntry.managedSubscribers.get(subscriber)
90
+ if (existingSubscriberEntry !== undefined) {
91
+ return existingSubscriberEntry
92
+ }
93
+
94
+ const unsubscribe = (): void => {
95
+ this.unsubscribe(event, subscriber)
96
+ }
97
+ const newSubscriberEntry: SubscriberEntry<Events, K> = {
98
+ event,
99
+ once,
100
+ subscriber,
101
+ unsubscribe,
102
+ }
103
+ proxySubscriberEntry.managedSubscribers.set(subscriber, newSubscriberEntry)
104
+
105
+ return newSubscriberEntry
106
+ }
107
+
108
+ /**
109
+ * 为指定事件添加常规订阅者,并在首次接入时创建底层代理订阅。
110
+ */
111
+ subscribe<K extends keyof Events>(
112
+ event: K, subscriber: Events[K]
113
+ ): SubscriberEntry<Events, K> {
114
+ return this.internalSubscribe(event, subscriber, false)
115
+ }
116
+
117
+ /**
118
+ * 为指定事件添加只触发一次的订阅者。
119
+ */
120
+ subscribeOnce<K extends keyof Events>(
121
+ event: K, listener: Events[K]
122
+ ): SubscriberEntry<Events, K> {
123
+ return this.internalSubscribe(event, listener, true)
124
+ }
125
+
126
+ /**
127
+ * 移除指定事件上的给定订阅者。
128
+ */
129
+ unsubscribe<K extends keyof Events>(
130
+ event: K, listener: Events[K]
131
+ ): this {
132
+ const proxySubscriberEntry = this.subscribers[event]
133
+ if (proxySubscriberEntry === undefined) {
134
+ return this
135
+ }
136
+
137
+ proxySubscriberEntry.managedSubscribers.delete(listener)
138
+ if (proxySubscriberEntry.managedSubscribers.size === 0) {
139
+ this.options.targetAdapter.unsubscribe(event, proxySubscriberEntry.proxySubscriber)
140
+ delete this.subscribers[event]
141
+ }
142
+
143
+ return this
144
+ }
145
+
146
+ /**
147
+ * 移除指定事件上的全部代理订阅者。
148
+ */
149
+ removeSubscribersOfEvent<K extends keyof Events>(
150
+ event: K
151
+ ): this {
152
+ const proxySubscriberEntry = this.subscribers[event]
153
+ if (proxySubscriberEntry === undefined) {
154
+ return this
155
+ }
156
+
157
+ proxySubscriberEntry.managedSubscribers.clear()
158
+ this.options.targetAdapter.unsubscribe(event, proxySubscriberEntry.proxySubscriber)
159
+ delete this.subscribers[event]
160
+
161
+ return this
162
+ }
163
+
164
+ /**
165
+ * 移除代理当前管理的全部订阅者。
166
+ */
167
+ removeAllSubscribers(): this {
168
+ const events = Object.keys(this.subscribers)
169
+
170
+ for (const event of events) {
171
+ this.removeSubscribersOfEvent(event)
172
+ }
173
+ this.subscribers = {}
174
+
175
+ return this
176
+ }
177
+
178
+ /**
179
+ * 通过底层适配器派发指定事件。
180
+ */
181
+ emit<K extends keyof Events>(
182
+ event: K, ...args: Parameters<Events[K]>
183
+ ): boolean {
184
+ return this.options.targetAdapter.emit(event, ...args)
185
+ }
186
+ }
@@ -0,0 +1,24 @@
1
+ import type { BaseEvents, SubscriberEntry } from "./common.ts"
2
+
3
+ /**
4
+ * 表示某一事件名下由订阅者函数到订阅项的内部映射。
5
+ */
6
+ export type InternalSubscriberEntryMap<Events extends BaseEvents, K extends keyof Events> = Map<
7
+ Events[K],
8
+ SubscriberEntry<Events, K>
9
+ >
10
+
11
+ /**
12
+ * 表示代理订阅项及其所管理的下游订阅集合的内部结构。
13
+ */
14
+ export interface InternalProxySubscriberEntry<Events extends BaseEvents, K extends keyof Events> {
15
+ managedSubscribers: InternalSubscriberEntryMap<Events, K>
16
+ proxySubscriber: Events[K]
17
+ }
18
+
19
+ /**
20
+ * 使用对象而非 Map 来保存事件到代理订阅项的内部映射。
21
+ */
22
+ export type InternalProxySubscriberEntryMap<Events extends BaseEvents> = {
23
+ [K in keyof Events]?: InternalProxySubscriberEntry<Events, K> | undefined
24
+ }
@@ -0,0 +1,96 @@
1
+ # Exception
2
+
3
+ ## Description
4
+
5
+ Exception 模块提供围绕宿主环境全局异常入口(global exception entry)的监听、标准化与组合能力,用于把浏览器和 Node.js 暴露出来的异常来源整理成稳定、可清理、可组合的公共异常模型。
6
+
7
+ 它关注的是“异常从哪里被捕获、如何被整理成稳定记录”,而不是“异常之后如何打印、上报、恢复、吞掉或终止进程”。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 Exception 模块时,应把它视为“异常来源层”,而不是“异常处理策略层”。这个模块的职责边界非常明确:
12
+
13
+ - 它负责封装宿主提供的全局异常入口。
14
+ - 它负责把宿主原始事件整理成更稳定的异常记录结构。
15
+ - 它负责提供可清理的监听语义与批量组合入口。
16
+ - 它不负责日志发送、监控上报、默认阻止、错误恢复或退出进程等策略决策。
17
+
18
+ 当前模块主要覆盖两类宿主:
19
+
20
+ - 浏览器侧异常入口,例如 `error` 事件、`window.onerror` 与 `unhandledrejection`。
21
+ - Node.js 侧异常入口,例如 `uncaughtExceptionMonitor`、`uncaughtException` 与 `unhandledRejection`。
22
+
23
+ 在建模上,这个模块采用“保留来源差异,同时提供标准化记录”的方式,而不是过早追求绝对统一。也就是说,浏览器与 Node.js 的来源种类、原始参数和补充字段可以不同,但它们都可以被整理成带有 runtime、source、exception、message、timestamp 等稳定字段的公共记录。这种分层有助于同时满足宿主特定处理与跨宿主消费。
24
+
25
+ ## For Using
26
+
27
+ 当应用需要在入口层、基础设施层、日志层或诊断层统一接入宿主全局异常时,可以使用这个模块,把分散在不同宿主 API 上的异常来源整理成更清楚的订阅接口。
28
+
29
+ 当前公共能力大致可以按以下几类理解:
30
+
31
+ - 单一入口监听:用于单独监听浏览器或 Node.js 的某一种异常来源。
32
+ - 批量入口监听:用于按宿主一次性安装一组常见异常入口,并返回统一清理函数。
33
+ - 异常记录标准化:用于把浏览器或 Node.js 的原始输入整理成稳定的异常记录结构。
34
+ - 类型与清理语义:用于表达异常来源种类、记录形状以及取消监听的统一返回类型。
35
+
36
+ 使用时应把它接在 logger、遥测、监控、恢复策略或业务错误处理的上游,而不是期待它直接替代这些能力。它更适合作为异常输入层,而不是最终处理层。
37
+
38
+ ## For Contributing
39
+
40
+ 贡献 Exception 模块时,应优先判断新增能力是否表达了稳定、可复用的宿主级异常来源,还是只是某个框架、某个应用内部错误流的便捷包装。只有前者才适合进入这个模块。
41
+
42
+ 在扩展时,应优先遵守以下边界:
43
+
44
+ - 公共能力应围绕宿主异常入口、标准化记录与监听清理语义展开。
45
+ - 不要在监听逻辑中默认执行日志输出、上报请求、`preventDefault`、`process.exit` 或其它强策略动作,除非 API 名称与文档已经明确承诺。
46
+ - 组合入口只负责批量安装监听器与汇总清理,不负责掩盖宿主差异或替调用方做策略决策。
47
+ - 与 Environment 的关系应保持清楚:Environment 可以帮助判断和获取宿主上下文,Exception 则负责安装异常监听器。
48
+
49
+ ### JSDoc 注释格式要求
50
+
51
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
52
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
53
+ - 如果描述后还有其他内容,应在描述后加一个空行。
54
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
55
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
56
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
57
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
58
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
59
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
60
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
61
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
62
+
63
+ ### 实现规范要求
64
+
65
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
66
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
67
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
68
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
69
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
70
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
71
+ - 辅助元素永远不要公开导出。
72
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/exception/internal.ts`;若共享只存在于浏览器或 Node.js 子问题域内部,也可以放在对应文件中。
73
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
74
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
75
+ - 子模块不需要有自己的 `README.md`。
76
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
77
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
78
+ - 监听注册逻辑、记录标准化逻辑和上层策略逻辑应保持分层,避免把一个模块做成过重入口。
79
+
80
+ ### 导出策略要求
81
+
82
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
83
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
84
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
85
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的异常入口语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
86
+
87
+ ### 测试要求
88
+
89
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
90
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
91
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
92
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
93
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
94
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
95
+ - 模块的单元测试文件目录是 `./tests/unit/exception`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/exception/<sub-module-name>`。
96
+ - 测试应重点覆盖监听注册、清理行为、默认包含的入口集合、记录标准化结果,以及宿主不可用时的失败或空操作语义是否稳定。
@@ -0,0 +1,219 @@
1
+ import { useBrowser } from "#Source/environment/index.ts"
2
+
3
+ import { normalizeBrowserExceptionRecord } from "./normalize.ts"
4
+ import type {
5
+ BrowserExceptionRecord,
6
+ ExceptionListenerCleanup,
7
+ ObserveExceptionsOptions,
8
+ } from "./types.ts"
9
+
10
+ /**
11
+ * 表示浏览器侧异常监听回调。
12
+ */
13
+ export type BrowserExceptionListener = (record: BrowserExceptionRecord) => void
14
+
15
+ /**
16
+ * 监听浏览器侧通过 `error` 事件暴露的全局异常。
17
+ */
18
+ export const onBrowserGlobalError = (
19
+ listener: BrowserExceptionListener,
20
+ options?: ObserveExceptionsOptions | undefined,
21
+ ): ExceptionListenerCleanup => {
22
+ return useBrowser(({ window }) => {
23
+ const internalListener = (event: ErrorEvent): void => {
24
+ listener(normalizeBrowserExceptionRecord({
25
+ source: "browser.global-error",
26
+ exception: event.error ?? event.message,
27
+ message: event.message,
28
+ timestamp: options?.captureTimestamp?.() ?? Date.now(),
29
+ originalEvent: event,
30
+ filename: event.filename,
31
+ lineno: event.lineno,
32
+ colno: event.colno,
33
+ }))
34
+ }
35
+
36
+ window.addEventListener("error", internalListener)
37
+
38
+ return () => {
39
+ window.removeEventListener("error", internalListener)
40
+ }
41
+ }, () => {
42
+ return (): void => {
43
+ return undefined
44
+ }
45
+ })
46
+ }
47
+
48
+ type InternalBrowserOnErrorListener = (
49
+ message: string | Event,
50
+ source?: string | undefined,
51
+ lineno?: number | undefined,
52
+ colno?: number | undefined,
53
+ error?: Error | undefined,
54
+ ) => boolean
55
+ interface InternalBrowserOnErrorState {
56
+ previousOnError: OnErrorEventHandler | null
57
+ currentOnError: OnErrorEventHandler
58
+ listeners: Set<InternalBrowserOnErrorListener>
59
+ }
60
+ const internalBrowserOnErrorStateMap = new WeakMap<Window, InternalBrowserOnErrorState>()
61
+ const internalGetBrowserOnErrorState = (window: Window): InternalBrowserOnErrorState => {
62
+ const existingState = internalBrowserOnErrorStateMap.get(window)
63
+
64
+ if (existingState !== undefined) {
65
+ return existingState
66
+ }
67
+
68
+ const internalListeners = new Set<InternalBrowserOnErrorListener>()
69
+ const internalPreviousOnError = window.onerror
70
+ const internalCurrentOnError: OnErrorEventHandler = (message, source, lineno, colno, error): boolean => {
71
+ let internalHandled = false
72
+
73
+ for (const internalListener of internalListeners) {
74
+ internalHandled = internalListener(message, source, lineno, colno, error) || internalHandled
75
+ }
76
+
77
+ if (typeof internalPreviousOnError === "function") {
78
+ internalHandled = internalPreviousOnError(message, source, lineno, colno, error) === true || internalHandled
79
+ }
80
+
81
+ return internalHandled
82
+ }
83
+
84
+ const internalState: InternalBrowserOnErrorState = {
85
+ previousOnError: internalPreviousOnError,
86
+ currentOnError: internalCurrentOnError,
87
+ listeners: internalListeners,
88
+ }
89
+
90
+ internalBrowserOnErrorStateMap.set(window, internalState)
91
+
92
+ // oxlint-disable-next-line prefer-add-event-listener
93
+ window.onerror = internalCurrentOnError
94
+
95
+ return internalState
96
+ }
97
+ /**
98
+ * 监听浏览器侧通过 `window.onerror` 暴露的全局异常。
99
+ */
100
+ export const onBrowserWindowOnError = (
101
+ listener: BrowserExceptionListener,
102
+ options?: ObserveExceptionsOptions | undefined,
103
+ ): ExceptionListenerCleanup => {
104
+ return useBrowser(({ window }) => {
105
+ const internalOnErrorState = internalGetBrowserOnErrorState(window)
106
+ const internalListener: InternalBrowserOnErrorListener = (message, source, lineno, colno, error): boolean => {
107
+ listener(normalizeBrowserExceptionRecord({
108
+ source: "browser.window-onerror",
109
+ exception: error ?? message,
110
+ message: typeof message === "string" ? message : undefined,
111
+ timestamp: options?.captureTimestamp?.() ?? Date.now(),
112
+ originalEvent: { message, source, lineno, colno, error },
113
+ filename: source,
114
+ lineno,
115
+ colno,
116
+ }))
117
+
118
+ return false
119
+ }
120
+
121
+ internalOnErrorState.listeners.add(internalListener)
122
+
123
+ return () => {
124
+ internalOnErrorState.listeners.delete(internalListener)
125
+
126
+ if (internalOnErrorState.listeners.size !== 0) {
127
+ return
128
+ }
129
+
130
+ internalBrowserOnErrorStateMap.delete(window)
131
+
132
+ if (window.onerror === internalOnErrorState.currentOnError) {
133
+ // oxlint-disable-next-line prefer-add-event-listener
134
+ window.onerror = internalOnErrorState.previousOnError
135
+ }
136
+ }
137
+ }, () => {
138
+ return (): void => {
139
+ return undefined
140
+ }
141
+ })
142
+ }
143
+
144
+ /**
145
+ * 监听浏览器侧未处理 Promise 拒绝。
146
+ */
147
+ export const onBrowserUnhandledRejection = (
148
+ listener: BrowserExceptionListener,
149
+ options?: ObserveExceptionsOptions | undefined,
150
+ ): ExceptionListenerCleanup => {
151
+ return useBrowser(({ window }) => {
152
+ const internalListener = (event: PromiseRejectionEvent): void => {
153
+ listener(normalizeBrowserExceptionRecord({
154
+ source: "browser.unhandled-rejection",
155
+ exception: event.reason,
156
+ timestamp: options?.captureTimestamp?.() ?? Date.now(),
157
+ originalEvent: event,
158
+ }))
159
+ }
160
+
161
+ window.addEventListener("unhandledrejection", internalListener)
162
+
163
+ return () => {
164
+ window.removeEventListener("unhandledrejection", internalListener)
165
+ }
166
+ }, () => {
167
+ return (): void => {
168
+ return undefined
169
+ }
170
+ })
171
+ }
172
+
173
+ /**
174
+ * 表示批量监听浏览器异常时可选的配置。
175
+ *
176
+ * 未显式传入时,三个浏览器入口默认都会启用;仅当对应选项为 `false` 时才跳过该入口。
177
+ */
178
+ export interface ObserveBrowserExceptionsOptions extends ObserveExceptionsOptions {
179
+ includeGlobalError?: boolean | undefined
180
+ includeWindowOnError?: boolean | undefined
181
+ includeUnhandledRejection?: boolean | undefined
182
+ }
183
+
184
+ /**
185
+ * 批量监听浏览器侧全局异常入口。
186
+ *
187
+ * 默认会同时监听 `error`、`window.onerror` 和 `unhandledrejection`。
188
+ * 如需关闭某个入口,请将对应的 `include*` 选项显式设为 `false`。
189
+ */
190
+ export const observeBrowserExceptions = (
191
+ listener: BrowserExceptionListener,
192
+ options?: ObserveBrowserExceptionsOptions | undefined,
193
+ ): ExceptionListenerCleanup => {
194
+ const {
195
+ includeGlobalError,
196
+ includeWindowOnError,
197
+ includeUnhandledRejection,
198
+ } = options ?? {}
199
+
200
+ const cleanups: ExceptionListenerCleanup[] = []
201
+
202
+ if (includeGlobalError !== false) {
203
+ cleanups.push(onBrowserGlobalError(listener, options))
204
+ }
205
+
206
+ if (includeWindowOnError !== false) {
207
+ cleanups.push(onBrowserWindowOnError(listener, options))
208
+ }
209
+
210
+ if (includeUnhandledRejection !== false) {
211
+ cleanups.push(onBrowserUnhandledRejection(listener, options))
212
+ }
213
+
214
+ return () => {
215
+ for (const cleanup of cleanups) {
216
+ cleanup()
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,4 @@
1
+ export type * from "./types.ts"
2
+ export * from "./normalize.ts"
3
+ export * from "./browser.ts"
4
+ export * from "./nodejs.ts"