@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.
- package/CHANGELOG.md +32 -0
- package/README.md +134 -21
- package/dist/index.js +45 -4
- package/dist/index.js.map +186 -11
- package/oxlint.config.ts +6 -0
- package/package.json +16 -10
- package/src/abort/README.md +92 -0
- package/src/abort/abort-manager.ts +278 -0
- package/src/abort/abort-signal-listener-manager.ts +81 -0
- package/src/abort/index.ts +2 -0
- package/src/basic/README.md +69 -117
- package/src/basic/enhance.ts +10 -0
- package/src/basic/function.ts +81 -62
- package/src/basic/index.ts +2 -0
- package/src/basic/is.ts +152 -71
- package/src/basic/object.ts +82 -0
- package/src/basic/promise.ts +29 -8
- package/src/basic/string.ts +2 -33
- package/src/color/README.md +105 -0
- package/src/color/index.ts +3 -0
- package/src/color/internal.ts +42 -0
- package/src/color/rgb/analyze.ts +236 -0
- package/src/color/rgb/construct.ts +130 -0
- package/src/color/rgb/convert.ts +227 -0
- package/src/color/rgb/derive.ts +303 -0
- package/src/color/rgb/index.ts +6 -0
- package/src/color/rgb/internal.ts +208 -0
- package/src/color/rgb/parse.ts +302 -0
- package/src/color/rgb/serialize.ts +144 -0
- package/src/color/types.ts +57 -0
- package/src/color/xyz/analyze.ts +80 -0
- package/src/color/xyz/construct.ts +19 -0
- package/src/color/xyz/convert.ts +71 -0
- package/src/color/xyz/index.ts +3 -0
- package/src/color/xyz/internal.ts +23 -0
- package/src/css/README.md +93 -0
- package/src/css/class.ts +559 -0
- package/src/css/index.ts +1 -0
- package/src/encoding/README.md +92 -0
- package/src/encoding/base64.ts +107 -0
- package/src/encoding/index.ts +1 -0
- package/src/environment/README.md +97 -0
- package/src/environment/basic.ts +26 -0
- package/src/environment/device.ts +311 -0
- package/src/environment/feature.ts +285 -0
- package/src/environment/geo.ts +337 -0
- package/src/environment/index.ts +7 -0
- package/src/environment/runtime.ts +400 -0
- package/src/environment/snapshot.ts +60 -0
- package/src/environment/variable.ts +239 -0
- package/src/event/README.md +90 -0
- package/src/event/class-event-proxy.ts +228 -0
- package/src/event/common.ts +19 -0
- package/src/event/event-manager.ts +203 -0
- package/src/event/index.ts +4 -0
- package/src/event/instance-event-proxy.ts +186 -0
- package/src/event/internal.ts +24 -0
- package/src/exception/README.md +96 -0
- package/src/exception/browser.ts +219 -0
- package/src/exception/index.ts +4 -0
- package/src/exception/nodejs.ts +169 -0
- package/src/exception/normalize.ts +106 -0
- package/src/exception/types.ts +99 -0
- package/src/identifier/README.md +92 -0
- package/src/identifier/id.ts +119 -0
- package/src/identifier/index.ts +2 -0
- package/src/identifier/uuid.ts +187 -0
- package/src/index.ts +18 -1
- package/src/log/README.md +79 -0
- package/src/log/index.ts +5 -0
- package/src/log/log-emitter.ts +72 -0
- package/src/log/log-record.ts +10 -0
- package/src/log/log-scheduler.ts +74 -0
- package/src/log/log-type.ts +8 -0
- package/src/log/logger.ts +543 -0
- package/src/orchestration/README.md +89 -0
- package/src/orchestration/coordination/barrier.ts +214 -0
- package/src/orchestration/coordination/count-down-latch.ts +215 -0
- package/src/orchestration/coordination/errors.ts +98 -0
- package/src/orchestration/coordination/index.ts +16 -0
- package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
- package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
- package/src/orchestration/coordination/keyed-lock.ts +168 -0
- package/src/orchestration/coordination/mutex.ts +257 -0
- package/src/orchestration/coordination/permit.ts +127 -0
- package/src/orchestration/coordination/read-write-lock.ts +444 -0
- package/src/orchestration/coordination/semaphore.ts +280 -0
- package/src/orchestration/index.ts +1 -0
- package/src/random/README.md +78 -0
- package/src/random/index.ts +1 -0
- package/src/random/string.ts +35 -0
- package/src/reactor/README.md +4 -0
- package/src/reactor/reactor-core/primitive.ts +9 -9
- package/src/reactor/reactor-core/reactive-system.ts +5 -5
- package/src/singleton/README.md +79 -0
- package/src/singleton/factory.ts +55 -0
- package/src/singleton/index.ts +2 -0
- package/src/singleton/manager.ts +204 -0
- package/src/storage/README.md +107 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/table.ts +449 -0
- package/src/timer/README.md +86 -0
- package/src/timer/expiration/expiration-manager.ts +594 -0
- package/src/timer/expiration/index.ts +3 -0
- package/src/timer/expiration/min-heap.ts +208 -0
- package/src/timer/expiration/remaining-manager.ts +241 -0
- package/src/timer/index.ts +1 -0
- package/src/type/README.md +54 -307
- package/src/type/class.ts +2 -2
- package/src/type/index.ts +14 -14
- package/src/type/is.ts +265 -2
- package/src/type/object.ts +37 -0
- package/src/type/string.ts +7 -2
- package/src/type/tuple.ts +6 -6
- package/src/type/union.ts +16 -0
- package/src/web/README.md +77 -0
- package/src/web/capture.ts +35 -0
- package/src/web/clipboard.ts +97 -0
- package/src/web/dom.ts +117 -0
- package/src/web/download.ts +16 -0
- package/src/web/event.ts +46 -0
- package/src/web/index.ts +10 -0
- package/src/web/local-storage.ts +113 -0
- package/src/web/location.ts +28 -0
- package/src/web/permission.ts +172 -0
- package/src/web/script-loader.ts +432 -0
- package/tests/unit/abort/abort-manager.spec.ts +225 -0
- package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
- package/tests/unit/basic/array.spec.ts +1 -1
- package/tests/unit/basic/object.spec.ts +32 -1
- package/tests/unit/basic/stream.spec.ts +1 -1
- package/tests/unit/basic/string.spec.ts +0 -9
- package/tests/unit/color/rgb/analyze.spec.ts +110 -0
- package/tests/unit/color/rgb/construct.spec.ts +56 -0
- package/tests/unit/color/rgb/convert.spec.ts +60 -0
- package/tests/unit/color/rgb/derive.spec.ts +103 -0
- package/tests/unit/color/rgb/parse.spec.ts +66 -0
- package/tests/unit/color/rgb/serialize.spec.ts +46 -0
- package/tests/unit/color/xyz/analyze.spec.ts +33 -0
- package/tests/unit/color/xyz/construct.spec.ts +10 -0
- package/tests/unit/color/xyz/convert.spec.ts +18 -0
- package/tests/unit/css/class.spec.ts +157 -0
- package/tests/unit/encoding/base64.spec.ts +40 -0
- package/tests/unit/environment/basic.spec.ts +20 -0
- package/tests/unit/environment/device.spec.ts +146 -0
- package/tests/unit/environment/feature.spec.ts +388 -0
- package/tests/unit/environment/geo.spec.ts +111 -0
- package/tests/unit/environment/runtime.spec.ts +364 -0
- package/tests/unit/environment/snapshot.spec.ts +4 -0
- package/tests/unit/environment/variable.spec.ts +190 -0
- package/tests/unit/event/class-event-proxy.spec.ts +225 -0
- package/tests/unit/event/event-manager.spec.ts +246 -0
- package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
- package/tests/unit/exception/browser.spec.ts +213 -0
- package/tests/unit/exception/nodejs.spec.ts +144 -0
- package/tests/unit/exception/normalize.spec.ts +57 -0
- package/tests/unit/identifier/id.spec.ts +71 -0
- package/tests/unit/identifier/uuid.spec.ts +85 -0
- package/tests/unit/log/log-emitter.spec.ts +33 -0
- package/tests/unit/log/log-scheduler.spec.ts +40 -0
- package/tests/unit/log/log-type.spec.ts +7 -0
- package/tests/unit/log/logger.spec.ts +222 -0
- package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
- package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
- package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
- package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
- package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
- package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
- package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
- package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
- package/tests/unit/random/string.spec.ts +11 -0
- package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
- package/tests/unit/reactor/preact-signal.spec.ts +1 -2
- package/tests/unit/singleton/singleton.spec.ts +49 -0
- package/tests/unit/storage/table.spec.ts +620 -0
- package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
- package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
- package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
- 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
|
+
})
|