@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,225 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import type { BaseEvents, ClassEventProxyOptions } from "#Source/event/index.ts"
4
+ import {
5
+ ClassEventProxy,
6
+ EventManager,
7
+ } from "#Source/event/index.ts"
8
+
9
+ interface TestEvents extends BaseEvents {
10
+ change: (value: number) => void
11
+ close: (reason: string) => void
12
+ }
13
+
14
+ interface TestTarget {
15
+ events: EventManager<TestEvents>
16
+ }
17
+
18
+ const createTarget = (): TestTarget => {
19
+ return { events: new EventManager<TestEvents>() }
20
+ }
21
+
22
+ const createClassEventProxyHarness = (options?: {
23
+ onSubscriberError?: (error: unknown) => void
24
+ }): {
25
+ targetA: TestTarget
26
+ targetB: TestTarget
27
+ classEventProxy: ClassEventProxy<TestTarget, TestEvents>
28
+ } => {
29
+ const targetA = createTarget()
30
+ const targetB = createTarget()
31
+ const classEventProxyOptions: ClassEventProxyOptions<TestTarget, TestEvents> = {
32
+ targetAdapter: {
33
+ emit: (target, event, ...args): boolean => {
34
+ return target.events.emit(event, ...args)
35
+ },
36
+ subscribe: (target, event, subscriber): void => {
37
+ target.events.subscribe(event, subscriber)
38
+ },
39
+ unsubscribe: (target, event, subscriber): void => {
40
+ target.events.unsubscribe(event, subscriber)
41
+ },
42
+ },
43
+ ...(options?.onSubscriberError === undefined
44
+ ? {}
45
+ : {
46
+ onSubscriberError: (_subscriberEntry, error): void => {
47
+ options.onSubscriberError?.(error)
48
+ },
49
+ }),
50
+ }
51
+ const classEventProxy = new ClassEventProxy<TestTarget, TestEvents>(classEventProxyOptions)
52
+
53
+ return {
54
+ targetA,
55
+ targetB,
56
+ classEventProxy,
57
+ }
58
+ }
59
+
60
+ test("ClassEventProxy.has reports whether a managed subscriber exists on a target", () => {
61
+ const { targetA, classEventProxy } = createClassEventProxyHarness()
62
+ const subscriber = (value: number): void => {
63
+ void value
64
+ }
65
+
66
+ expect(classEventProxy.has(targetA, "change", subscriber)).toBe(false)
67
+
68
+ classEventProxy.subscribe(targetA, "change", subscriber)
69
+ expect(classEventProxy.has(targetA, "change", subscriber)).toBe(true)
70
+
71
+ classEventProxy.unsubscribe(targetA, "change", subscriber)
72
+ expect(classEventProxy.has(targetA, "change", subscriber)).toBe(false)
73
+ })
74
+
75
+ test("ClassEventProxy.subscribe creates one bridge per target event and returns a stable entry", () => {
76
+ const { targetA, targetB, classEventProxy } = createClassEventProxyHarness()
77
+ const subscribeSpy = vi.spyOn(targetA.events, "subscribe")
78
+ const valuesA: number[] = []
79
+ const valuesB: number[] = []
80
+ const subscriberA = (value: number): void => {
81
+ valuesA.push(value)
82
+ }
83
+ const subscriberB = (value: number): void => {
84
+ valuesB.push(value)
85
+ }
86
+
87
+ const entryA = classEventProxy.subscribe(targetA, "change", subscriberA)
88
+ const duplicateEntryA = classEventProxy.subscribe(targetA, "change", subscriberA)
89
+ classEventProxy.subscribe(targetB, "change", subscriberB)
90
+
91
+ expect(duplicateEntryA).toBe(entryA)
92
+ expect(entryA.once).toBe(false)
93
+ expect(subscribeSpy).toHaveBeenCalledTimes(1)
94
+
95
+ expect(classEventProxy.emit(targetA, "change", 1)).toBe(true)
96
+ expect(classEventProxy.emit(targetB, "change", 2)).toBe(true)
97
+ expect(valuesA).toEqual([1])
98
+ expect(valuesB).toEqual([2])
99
+ })
100
+
101
+ test("ClassEventProxy.subscribeOnce removes the managed subscriber after the first bridge emission", () => {
102
+ const handledErrors: unknown[] = []
103
+ const { targetA, classEventProxy } = createClassEventProxyHarness({
104
+ onSubscriberError: (error): void => {
105
+ handledErrors.push(error)
106
+ },
107
+ })
108
+ const onceSubscriber = (): void => {
109
+ throw new Error("class-proxy-once-failed")
110
+ }
111
+
112
+ const entry = classEventProxy.subscribeOnce(targetA, "change", onceSubscriber)
113
+
114
+ expect(entry.once).toBe(true)
115
+ expect(classEventProxy.emit(targetA, "change", 1)).toBe(true)
116
+ expect(classEventProxy.has(targetA, "change", onceSubscriber)).toBe(false)
117
+ expect(handledErrors).toHaveLength(1)
118
+ expect(targetA.events.emit("change", 2)).toBe(false)
119
+ })
120
+
121
+ test("ClassEventProxy.unsubscribe removes only the specified managed subscriber", () => {
122
+ const { targetA, classEventProxy } = createClassEventProxyHarness()
123
+ const unsubscribeSpy = vi.spyOn(targetA.events, "unsubscribe")
124
+ const values: number[] = []
125
+ const keptSubscriber = (value: number): void => {
126
+ values.push(value)
127
+ }
128
+ const removedSubscriber = (value: number): void => {
129
+ values.push(value * 10)
130
+ }
131
+
132
+ classEventProxy.subscribe(targetA, "change", keptSubscriber)
133
+ classEventProxy.subscribe(targetA, "change", removedSubscriber)
134
+
135
+ expect(classEventProxy.unsubscribe(targetA, "change", removedSubscriber)).toBe(classEventProxy)
136
+ expect(classEventProxy.emit(targetA, "change", 2)).toBe(true)
137
+ expect(values).toEqual([2])
138
+ expect(unsubscribeSpy).not.toHaveBeenCalled()
139
+
140
+ classEventProxy.unsubscribe(targetA, "change", keptSubscriber)
141
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1)
142
+ })
143
+
144
+ test("ClassEventProxy.removeSubscribersOfEvent clears one bridged event on one target", () => {
145
+ const { targetA, classEventProxy } = createClassEventProxyHarness()
146
+ const unsubscribeSpy = vi.spyOn(targetA.events, "unsubscribe")
147
+ const closeReasons: string[] = []
148
+
149
+ classEventProxy.subscribe(targetA, "change", () => undefined)
150
+ classEventProxy.subscribe(targetA, "close", (reason) => {
151
+ closeReasons.push(reason)
152
+ })
153
+
154
+ expect(classEventProxy.removeSubscribersOfEvent(targetA, "change")).toBe(classEventProxy)
155
+ expect(classEventProxy.emit(targetA, "change", 1)).toBe(false)
156
+ expect(classEventProxy.emit(targetA, "close", "kept")).toBe(true)
157
+ expect(closeReasons).toEqual(["kept"])
158
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1)
159
+ })
160
+
161
+ test("ClassEventProxy.removeSubscribersOfTarget clears every bridged event of one target", () => {
162
+ const { targetA, targetB, classEventProxy } = createClassEventProxyHarness()
163
+ const unsubscribeSpy = vi.spyOn(targetA.events, "unsubscribe")
164
+ const keptValues: number[] = []
165
+
166
+ classEventProxy.subscribe(targetA, "change", () => undefined)
167
+ classEventProxy.subscribe(targetA, "close", () => undefined)
168
+ classEventProxy.subscribe(targetB, "change", (value) => {
169
+ keptValues.push(value)
170
+ })
171
+
172
+ expect(classEventProxy.removeSubscribersOfTarget(targetA)).toBe(classEventProxy)
173
+ expect(classEventProxy.emit(targetA, "change", 1)).toBe(false)
174
+ expect(classEventProxy.emit(targetB, "change", 2)).toBe(true)
175
+ expect(keptValues).toEqual([2])
176
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(2)
177
+ })
178
+
179
+ test("ClassEventProxy.removeAllSubscribers clears every bridged target", () => {
180
+ const { targetA, targetB, classEventProxy } = createClassEventProxyHarness()
181
+ const subscriberA = (): void => undefined
182
+ const subscriberB = (): void => undefined
183
+
184
+ classEventProxy.subscribe(targetA, "change", subscriberA)
185
+ classEventProxy.subscribe(targetB, "close", subscriberB)
186
+
187
+ expect(classEventProxy.removeAllSubscribers()).toBe(classEventProxy)
188
+ expect(classEventProxy.has(targetA, "change", subscriberA)).toBe(false)
189
+ expect(classEventProxy.has(targetB, "close", subscriberB)).toBe(false)
190
+ expect(classEventProxy.emit(targetA, "change", 1)).toBe(false)
191
+ expect(classEventProxy.emit(targetB, "close", "done")).toBe(false)
192
+ })
193
+
194
+ test("ClassEventProxy.emit delegates to the target adapter and handles managed subscriber errors", () => {
195
+ const handledErrors: unknown[] = []
196
+ const { targetA, classEventProxy } = createClassEventProxyHarness({
197
+ onSubscriberError: (error): void => {
198
+ handledErrors.push(error)
199
+ },
200
+ })
201
+ const emitSpy = vi.spyOn(targetA.events, "emit")
202
+
203
+ classEventProxy.subscribe(targetA, "close", () => {
204
+ throw new Error("class-proxy-failed")
205
+ })
206
+
207
+ expect(classEventProxy.emit(targetA, "close", "boom")).toBe(true)
208
+ expect(emitSpy).toHaveBeenCalledTimes(1)
209
+ expect(handledErrors).toHaveLength(1)
210
+
211
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined)
212
+
213
+ try {
214
+ const { targetA: unmanagedTarget, classEventProxy: unmanagedClassEventProxy } = createClassEventProxyHarness()
215
+ unmanagedClassEventProxy.subscribe(unmanagedTarget, "close", () => {
216
+ throw new Error("class-proxy-unhandled")
217
+ })
218
+
219
+ expect(unmanagedClassEventProxy.emit(unmanagedTarget, "close", "boom")).toBe(true)
220
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
221
+ }
222
+ finally {
223
+ consoleErrorSpy.mockRestore()
224
+ }
225
+ })
@@ -0,0 +1,246 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import type { BaseEvents } from "#Source/event/index.ts"
4
+ import { EventManager } from "#Source/event/index.ts"
5
+
6
+ interface TestEvents extends BaseEvents {
7
+ ready: (value: number) => void | Promise<void>
8
+ done: (label: string) => void | Promise<void>
9
+ }
10
+ const createEventManager = (): EventManager<TestEvents> => {
11
+ return new EventManager<TestEvents>()
12
+ }
13
+
14
+ test("EventManager.has reports whether a subscriber is registered", () => {
15
+ const eventManager = createEventManager()
16
+ const subscriber = (value: number): void => {
17
+ void value
18
+ }
19
+
20
+ expect(eventManager.has("ready", subscriber)).toBe(false)
21
+
22
+ eventManager.subscribe("ready", subscriber)
23
+ expect(eventManager.has("ready", subscriber)).toBe(true)
24
+
25
+ eventManager.unsubscribe("ready", subscriber)
26
+ expect(eventManager.has("ready", subscriber)).toBe(false)
27
+ })
28
+
29
+ test("EventManager.subscribe returns a stable entry and keeps regular subscribers active", () => {
30
+ const eventManager = createEventManager()
31
+ const values: number[] = []
32
+ const subscriber = (value: number): void => {
33
+ values.push(value)
34
+ }
35
+
36
+ const firstEntry = eventManager.subscribe("ready", subscriber)
37
+ const duplicateEntry = eventManager.subscribe("ready", subscriber)
38
+
39
+ expect(duplicateEntry).toBe(firstEntry)
40
+ expect(firstEntry.once).toBe(false)
41
+
42
+ expect(eventManager.emit("ready", 1)).toBe(true)
43
+ expect(eventManager.emit("ready", 2)).toBe(true)
44
+ expect(values).toEqual([1, 2])
45
+ })
46
+
47
+ test("EventManager.subscribeOnce removes the subscriber after the first emission", () => {
48
+ const eventManager = createEventManager()
49
+ const values: number[] = []
50
+ const subscriber = (value: number): void => {
51
+ values.push(value)
52
+ }
53
+
54
+ const entry = eventManager.subscribeOnce("ready", subscriber)
55
+
56
+ expect(entry.once).toBe(true)
57
+ expect(eventManager.emit("ready", 1)).toBe(true)
58
+ expect(eventManager.emit("ready", 2)).toBe(false)
59
+ expect(values).toEqual([1])
60
+ expect(eventManager.has("ready", subscriber)).toBe(false)
61
+ })
62
+
63
+ test("EventManager.unsubscribe removes only the specified subscriber", () => {
64
+ const eventManager = createEventManager()
65
+ const values: number[] = []
66
+ const keptSubscriber = (value: number): void => {
67
+ values.push(value)
68
+ }
69
+ const removedSubscriber = (value: number): void => {
70
+ values.push(value * 10)
71
+ }
72
+
73
+ eventManager.subscribe("ready", keptSubscriber)
74
+ eventManager.subscribe("ready", removedSubscriber)
75
+
76
+ expect(eventManager.unsubscribe("ready", removedSubscriber)).toBe(eventManager)
77
+ expect(eventManager.emit("ready", 2)).toBe(true)
78
+ expect(values).toEqual([2])
79
+ })
80
+
81
+ test("EventManager.removeSubscribersOfEvent clears subscribers of only one event", () => {
82
+ const eventManager = createEventManager()
83
+ const doneValues: string[] = []
84
+
85
+ eventManager.subscribe("ready", () => {
86
+ throw new Error("should-not-run")
87
+ })
88
+ eventManager.subscribe("done", (label) => {
89
+ doneValues.push(label)
90
+ })
91
+
92
+ expect(eventManager.removeSubscribersOfEvent("ready")).toBe(eventManager)
93
+ expect(eventManager.emit("ready", 1)).toBe(false)
94
+ expect(eventManager.emit("done", "kept")).toBe(true)
95
+ expect(doneValues).toEqual(["kept"])
96
+ })
97
+
98
+ test("EventManager.removeAllSubscribers clears every event subscription", () => {
99
+ const eventManager = createEventManager()
100
+
101
+ eventManager.subscribe("ready", () => undefined)
102
+ eventManager.subscribe("done", () => undefined)
103
+
104
+ expect(eventManager.removeAllSubscribers()).toBe(eventManager)
105
+ expect(eventManager.emit("ready", 1)).toBe(false)
106
+ expect(eventManager.emit("done", "done")).toBe(false)
107
+ })
108
+
109
+ test("EventManager.emit invokes subscribers synchronously and handles sync errors", () => {
110
+ const handledErrors: unknown[] = []
111
+ const eventManager = new EventManager<TestEvents>({
112
+ onSubscriberError: (_subscriberEntry, error): void => {
113
+ handledErrors.push(error)
114
+ },
115
+ })
116
+ const callOrder: string[] = []
117
+ const onceSubscriber = (): void => {
118
+ callOrder.push("once")
119
+ throw new Error("once-failed")
120
+ }
121
+
122
+ eventManager.subscribe("ready", (value) => {
123
+ callOrder.push(`regular-${value}`)
124
+ })
125
+ eventManager.subscribeOnce("ready", onceSubscriber)
126
+ eventManager.subscribe("done", () => {
127
+ throw new Error("sync-failed")
128
+ })
129
+
130
+ expect(eventManager.emit("ready", 1)).toBe(true)
131
+ expect(eventManager.emit("ready", 2)).toBe(true)
132
+ expect(callOrder).toEqual(["regular-1", "once", "regular-2"])
133
+ expect(eventManager.has("ready", onceSubscriber)).toBe(false)
134
+
135
+ expect(eventManager.emit("done", "alpha")).toBe(true)
136
+ expect(handledErrors).toHaveLength(2)
137
+
138
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined)
139
+
140
+ try {
141
+ const unmanagedEventManager = createEventManager()
142
+ unmanagedEventManager.subscribe("done", () => {
143
+ throw new Error("unhandled")
144
+ })
145
+
146
+ expect(unmanagedEventManager.emit("done", "beta")).toBe(true)
147
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
148
+ }
149
+ finally {
150
+ consoleErrorSpy.mockRestore()
151
+ }
152
+ })
153
+
154
+ test("EventManager.emitAsync awaits subscribers in registration order and handles async errors", async () => {
155
+ const handledErrors: unknown[] = []
156
+ const eventManager = new EventManager<TestEvents>({
157
+ onSubscriberError: (_subscriberEntry, error): void => {
158
+ handledErrors.push(error)
159
+ },
160
+ })
161
+ const orderedSteps: string[] = []
162
+ // oxlint-disable-next-line require-await
163
+ const onceSubscriber = async (): Promise<void> => {
164
+ throw new Error("async-failed")
165
+ }
166
+
167
+ eventManager.subscribe("ready", async (value) => {
168
+ orderedSteps.push(`first-start-${value}`)
169
+ await Promise.resolve()
170
+ orderedSteps.push(`first-end-${value}`)
171
+ })
172
+ eventManager.subscribe("ready", async (value) => {
173
+ orderedSteps.push(`second-start-${value}`)
174
+ await Promise.resolve()
175
+ orderedSteps.push(`second-end-${value}`)
176
+ })
177
+ eventManager.subscribeOnce("ready", onceSubscriber)
178
+
179
+ expect(await eventManager.emitAsync("ready", 7)).toBe(true)
180
+ expect(orderedSteps).toEqual([
181
+ "first-start-7",
182
+ "first-end-7",
183
+ "second-start-7",
184
+ "second-end-7",
185
+ ])
186
+ expect(handledErrors).toHaveLength(1)
187
+ expect(eventManager.has("ready", onceSubscriber)).toBe(false)
188
+ })
189
+
190
+ test("EventManager.emitConcurrentAsync awaits subscribers concurrently and handles async errors", async () => {
191
+ const handledErrors: unknown[] = []
192
+ const eventManager = new EventManager<TestEvents>({
193
+ onSubscriberError: (_subscriberEntry, error): void => {
194
+ handledErrors.push(error)
195
+ },
196
+ })
197
+ const orderedSteps: string[] = []
198
+ let resolveFirst = (): void => undefined
199
+ let resolveSecond = (): void => undefined
200
+ const firstPromise = new Promise<void>((resolve) => {
201
+ resolveFirst = resolve
202
+ })
203
+ const secondPromise = new Promise<void>((resolve) => {
204
+ resolveSecond = resolve
205
+ })
206
+
207
+ eventManager.subscribe("ready", async () => {
208
+ orderedSteps.push("first-start")
209
+ await firstPromise
210
+ orderedSteps.push("first-end")
211
+ })
212
+ eventManager.subscribe("ready", async () => {
213
+ orderedSteps.push("second-start")
214
+ await secondPromise
215
+ orderedSteps.push("second-end")
216
+ })
217
+
218
+ const emitPromise = eventManager.emitConcurrentAsync("ready", 8)
219
+ await Promise.resolve()
220
+ expect(orderedSteps).toEqual(["first-start", "second-start"])
221
+
222
+ resolveSecond()
223
+ await Promise.resolve()
224
+ resolveFirst()
225
+
226
+ expect(await emitPromise).toBe(true)
227
+ expect(orderedSteps).toEqual(["first-start", "second-start", "second-end", "first-end"])
228
+
229
+ eventManager.removeAllSubscribers()
230
+ orderedSteps.length = 0
231
+
232
+ eventManager.subscribe("ready", async () => {
233
+ orderedSteps.push("third-start")
234
+ await Promise.resolve()
235
+ orderedSteps.push("third-end")
236
+ })
237
+ eventManager.subscribe("ready", async () => {
238
+ orderedSteps.push("fourth-start")
239
+ await Promise.resolve()
240
+ throw new Error("concurrent-failed")
241
+ })
242
+
243
+ expect(await eventManager.emitConcurrentAsync("ready", 9)).toBe(true)
244
+ expect(orderedSteps).toEqual(["third-start", "fourth-start", "third-end"])
245
+ expect(handledErrors).toHaveLength(1)
246
+ })
@@ -0,0 +1,187 @@
1
+ import { expect, test, vi } from "vitest"
2
+
3
+ import type { BaseEvents, InstanceEventProxyOptions } from "#Source/event/index.ts"
4
+ import {
5
+ EventManager,
6
+ InstanceEventProxy,
7
+ } from "#Source/event/index.ts"
8
+
9
+ interface TestEvents extends BaseEvents {
10
+ change: (value: number) => void
11
+ error: (message: string) => void
12
+ }
13
+
14
+ const createInstanceEventProxyHarness = (options?: {
15
+ onSubscriberError?: (error: unknown) => void
16
+ }): {
17
+ target: EventManager<TestEvents>
18
+ eventProxy: InstanceEventProxy<TestEvents>
19
+ } => {
20
+ const target = new EventManager<TestEvents>()
21
+ const eventProxyOptions: InstanceEventProxyOptions<TestEvents> = {
22
+ targetAdapter: {
23
+ emit: (event, ...args): boolean => {
24
+ return target.emit(event, ...args)
25
+ },
26
+ subscribe: (event, subscriber): void => {
27
+ target.subscribe(event, subscriber)
28
+ },
29
+ unsubscribe: (event, subscriber): void => {
30
+ target.unsubscribe(event, subscriber)
31
+ },
32
+ },
33
+ ...(options?.onSubscriberError === undefined
34
+ ? {}
35
+ : {
36
+ onSubscriberError: (_subscriberEntry, error): void => {
37
+ options.onSubscriberError?.(error)
38
+ },
39
+ }),
40
+ }
41
+ const eventProxy = new InstanceEventProxy<TestEvents>(eventProxyOptions)
42
+
43
+ return {
44
+ target,
45
+ eventProxy,
46
+ }
47
+ }
48
+
49
+ test("InstanceEventProxy.has reports whether a managed subscriber exists", () => {
50
+ const { eventProxy } = createInstanceEventProxyHarness()
51
+ const subscriber = (value: number): void => {
52
+ void value
53
+ }
54
+
55
+ expect(eventProxy.has("change", subscriber)).toBe(false)
56
+
57
+ eventProxy.subscribe("change", subscriber)
58
+ expect(eventProxy.has("change", subscriber)).toBe(true)
59
+
60
+ eventProxy.unsubscribe("change", subscriber)
61
+ expect(eventProxy.has("change", subscriber)).toBe(false)
62
+ })
63
+
64
+ test("InstanceEventProxy.subscribe creates one bridge subscriber and returns a stable entry", () => {
65
+ const { target, eventProxy } = createInstanceEventProxyHarness()
66
+ const subscribeSpy = vi.spyOn(target, "subscribe")
67
+ const values: number[] = []
68
+ const subscriber = (value: number): void => {
69
+ values.push(value)
70
+ }
71
+
72
+ const firstEntry = eventProxy.subscribe("change", subscriber)
73
+ const duplicateEntry = eventProxy.subscribe("change", subscriber)
74
+
75
+ expect(duplicateEntry).toBe(firstEntry)
76
+ expect(firstEntry.once).toBe(false)
77
+ expect(subscribeSpy).toHaveBeenCalledTimes(1)
78
+
79
+ expect(eventProxy.emit("change", 1)).toBe(true)
80
+ expect(values).toEqual([1])
81
+ })
82
+
83
+ test("InstanceEventProxy.subscribeOnce removes the managed subscriber after the first bridge emission", () => {
84
+ const handledErrors: unknown[] = []
85
+ const { eventProxy, target } = createInstanceEventProxyHarness({
86
+ onSubscriberError: (error): void => {
87
+ handledErrors.push(error)
88
+ },
89
+ })
90
+ const onceSubscriber = (): void => {
91
+ throw new Error("proxy-once-failed")
92
+ }
93
+
94
+ const entry = eventProxy.subscribeOnce("change", onceSubscriber)
95
+
96
+ expect(entry.once).toBe(true)
97
+ expect(eventProxy.emit("change", 1)).toBe(true)
98
+ expect(eventProxy.has("change", onceSubscriber)).toBe(false)
99
+ expect(handledErrors).toHaveLength(1)
100
+ expect(target.emit("change", 2)).toBe(false)
101
+ })
102
+
103
+ test("InstanceEventProxy.unsubscribe removes only the specified managed subscriber", () => {
104
+ const { target, eventProxy } = createInstanceEventProxyHarness()
105
+ const unsubscribeSpy = vi.spyOn(target, "unsubscribe")
106
+ const values: number[] = []
107
+ const keptSubscriber = (value: number): void => {
108
+ values.push(value)
109
+ }
110
+ const removedSubscriber = (value: number): void => {
111
+ values.push(value * 10)
112
+ }
113
+
114
+ eventProxy.subscribe("change", keptSubscriber)
115
+ eventProxy.subscribe("change", removedSubscriber)
116
+
117
+ expect(eventProxy.unsubscribe("change", removedSubscriber)).toBe(eventProxy)
118
+ expect(eventProxy.emit("change", 2)).toBe(true)
119
+ expect(values).toEqual([2])
120
+ expect(unsubscribeSpy).not.toHaveBeenCalled()
121
+
122
+ eventProxy.unsubscribe("change", keptSubscriber)
123
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1)
124
+ })
125
+
126
+ test("InstanceEventProxy.removeSubscribersOfEvent clears one bridged event", () => {
127
+ const { target, eventProxy } = createInstanceEventProxyHarness()
128
+ const unsubscribeSpy = vi.spyOn(target, "unsubscribe")
129
+ const errors: string[] = []
130
+
131
+ eventProxy.subscribe("change", () => undefined)
132
+ eventProxy.subscribe("error", (message) => {
133
+ errors.push(message)
134
+ })
135
+
136
+ expect(eventProxy.removeSubscribersOfEvent("change")).toBe(eventProxy)
137
+ expect(eventProxy.emit("change", 1)).toBe(false)
138
+ expect(eventProxy.emit("error", "kept")).toBe(true)
139
+ expect(errors).toEqual(["kept"])
140
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1)
141
+ })
142
+
143
+ test("InstanceEventProxy.removeAllSubscribers clears every bridged event", () => {
144
+ const { target, eventProxy } = createInstanceEventProxyHarness()
145
+ const unsubscribeSpy = vi.spyOn(target, "unsubscribe")
146
+
147
+ eventProxy.subscribe("change", () => undefined)
148
+ eventProxy.subscribe("error", () => undefined)
149
+
150
+ expect(eventProxy.removeAllSubscribers()).toBe(eventProxy)
151
+ expect(eventProxy.emit("change", 1)).toBe(false)
152
+ expect(eventProxy.emit("error", "boom")).toBe(false)
153
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(2)
154
+ })
155
+
156
+ test("InstanceEventProxy.emit delegates to the target adapter and handles managed subscriber errors", () => {
157
+ const handledErrors: unknown[] = []
158
+ const { target, eventProxy } = createInstanceEventProxyHarness({
159
+ onSubscriberError: (error): void => {
160
+ handledErrors.push(error)
161
+ },
162
+ })
163
+ const emitSpy = vi.spyOn(target, "emit")
164
+
165
+ eventProxy.subscribe("change", () => {
166
+ throw new Error("proxy-failed")
167
+ })
168
+
169
+ expect(eventProxy.emit("change", 1)).toBe(true)
170
+ expect(emitSpy).toHaveBeenCalledTimes(1)
171
+ expect(handledErrors).toHaveLength(1)
172
+
173
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined)
174
+
175
+ try {
176
+ const { eventProxy: unmanagedEventProxy } = createInstanceEventProxyHarness()
177
+ unmanagedEventProxy.subscribe("error", () => {
178
+ throw new Error("proxy-unhandled")
179
+ })
180
+
181
+ expect(unmanagedEventProxy.emit("error", "boom")).toBe(true)
182
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1)
183
+ }
184
+ finally {
185
+ consoleErrorSpy.mockRestore()
186
+ }
187
+ })