midiwire 0.1.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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Lightweight browser-compatible event emitter.
3
+ * Provides publish/subscribe functionality for MIDI events
4
+ * without external dependencies. Implements standard event
5
+ * emitter pattern: register listeners with on(), emit events
6
+ * with emit(), remove listeners with off(). Listener cleanup
7
+ * is automatic via returned unsubscribe functions.
8
+ */
9
+ export class EventEmitter {
10
+ constructor() {
11
+ this.events = new Map()
12
+ }
13
+
14
+ /**
15
+ * Register an event listener
16
+ * @param {string} event - Event name
17
+ * @param {Function} handler - Event handler function
18
+ * @returns {Function} Unsubscribe function
19
+ */
20
+ on(event, handler) {
21
+ if (!this.events.has(event)) {
22
+ this.events.set(event, [])
23
+ }
24
+ this.events.get(event).push(handler)
25
+
26
+ // Return unsubscribe function
27
+ return () => this.off(event, handler)
28
+ }
29
+
30
+ /**
31
+ * Register a one-time event listener
32
+ * @param {string} event - Event name
33
+ * @param {Function} handler - Event handler function
34
+ */
35
+ once(event, handler) {
36
+ const onceHandler = (...args) => {
37
+ handler(...args)
38
+ this.off(event, onceHandler)
39
+ }
40
+ this.on(event, onceHandler)
41
+ }
42
+
43
+ /**
44
+ * Remove an event listener
45
+ * @param {string} event - Event name
46
+ * @param {Function} handler - Event handler function
47
+ */
48
+ off(event, handler) {
49
+ if (!this.events.has(event)) return
50
+
51
+ const handlers = this.events.get(event)
52
+ const index = handlers.indexOf(handler)
53
+ if (index > -1) {
54
+ handlers.splice(index, 1)
55
+ }
56
+
57
+ if (handlers.length === 0) {
58
+ this.events.delete(event)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Emit an event
64
+ * @param {string} event - Event name
65
+ * @param {*} data - Event data
66
+ */
67
+ emit(event, data) {
68
+ if (!this.events.has(event)) return
69
+
70
+ // Create a copy to avoid modification during iteration
71
+ const handlers = [...this.events.get(event)]
72
+
73
+ handlers.forEach((handler) => {
74
+ try {
75
+ handler(data)
76
+ } catch (err) {
77
+ console.error(`Error in event handler for "${event}":`, err)
78
+ }
79
+ })
80
+ }
81
+
82
+ /**
83
+ * Remove all event listeners
84
+ * @param {string} [event] - Optional event name to clear specific event
85
+ */
86
+ removeAllListeners(event) {
87
+ if (event) {
88
+ this.events.delete(event)
89
+ } else {
90
+ this.events.clear()
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,357 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+ import { EventEmitter } from "./EventEmitter.js"
3
+
4
+ describe("EventEmitter", () => {
5
+ describe("on and emit", () => {
6
+ it("should register and trigger event handlers", () => {
7
+ const emitter = new EventEmitter()
8
+ const handler = vi.fn()
9
+
10
+ emitter.on("test", handler)
11
+ emitter.emit("test", "data")
12
+
13
+ expect(handler).toHaveBeenCalledTimes(1)
14
+ expect(handler).toHaveBeenCalledWith("data")
15
+ })
16
+
17
+ it("should support multiple handlers for same event", () => {
18
+ const emitter = new EventEmitter()
19
+ const handler1 = vi.fn()
20
+ const handler2 = vi.fn()
21
+
22
+ emitter.on("test", handler1)
23
+ emitter.on("test", handler2)
24
+ emitter.emit("test", "data")
25
+
26
+ expect(handler1).toHaveBeenCalledTimes(1)
27
+ expect(handler2).toHaveBeenCalledTimes(1)
28
+ })
29
+
30
+ it("should pass single data argument to handlers", () => {
31
+ const emitter = new EventEmitter()
32
+ const handler = vi.fn()
33
+
34
+ emitter.on("test", handler)
35
+ emitter.emit("test", "arg1")
36
+
37
+ expect(handler).toHaveBeenCalledWith("arg1")
38
+ })
39
+
40
+ it("should not call handlers for different events", () => {
41
+ const emitter = new EventEmitter()
42
+ const handler = vi.fn()
43
+
44
+ emitter.on("test1", handler)
45
+ emitter.emit("test2", "data")
46
+
47
+ expect(handler).not.toHaveBeenCalled()
48
+ })
49
+
50
+ it("should handle events with no handlers", () => {
51
+ const emitter = new EventEmitter()
52
+ expect(() => emitter.emit("test", "data")).not.toThrow()
53
+ })
54
+ })
55
+
56
+ describe("once", () => {
57
+ it("should only call handler once", () => {
58
+ const emitter = new EventEmitter()
59
+ const handler = vi.fn()
60
+
61
+ emitter.once("test", handler)
62
+ emitter.emit("test", "data1")
63
+ emitter.emit("test", "data2")
64
+
65
+ expect(handler).toHaveBeenCalledTimes(1)
66
+ expect(handler).toHaveBeenCalledWith("data1")
67
+ })
68
+
69
+ it("should handle multiple once handlers", () => {
70
+ const emitter = new EventEmitter()
71
+ const handler1 = vi.fn()
72
+ const handler2 = vi.fn()
73
+
74
+ emitter.once("test", handler1)
75
+ emitter.once("test", handler2)
76
+ emitter.emit("test", "data")
77
+
78
+ // Both handlers should be called once each
79
+ expect(handler1).toHaveBeenCalledTimes(1)
80
+ expect(handler2).toHaveBeenCalledTimes(1)
81
+ expect(handler1).toHaveBeenCalledWith("data")
82
+ expect(handler2).toHaveBeenCalledWith("data")
83
+
84
+ emitter.emit("test", "data2")
85
+
86
+ // Neither should be called again
87
+ expect(handler1).toHaveBeenCalledTimes(1)
88
+ expect(handler2).toHaveBeenCalledTimes(1)
89
+ })
90
+
91
+ it("should allow mixing once and regular handlers", () => {
92
+ const emitter = new EventEmitter()
93
+ const onceHandler = vi.fn()
94
+ const regularHandler = vi.fn()
95
+
96
+ emitter.once("test", onceHandler)
97
+ emitter.on("test", regularHandler)
98
+
99
+ emitter.emit("test", "data1")
100
+ emitter.emit("test", "data2")
101
+
102
+ // onceHandler should be called once, regularHandler twice
103
+ expect(onceHandler).toHaveBeenCalledTimes(1)
104
+ expect(onceHandler).toHaveBeenCalledWith("data1")
105
+ expect(regularHandler).toHaveBeenCalledTimes(2)
106
+ expect(regularHandler).toHaveBeenNthCalledWith(1, "data1")
107
+ expect(regularHandler).toHaveBeenNthCalledWith(2, "data2")
108
+ })
109
+
110
+ it("should handle once handler removing itself during emit", () => {
111
+ const emitter = new EventEmitter()
112
+ const handler1 = vi.fn()
113
+ const handler2 = vi.fn()
114
+ const handler3 = vi.fn()
115
+
116
+ emitter.once("test", handler1)
117
+ emitter.once("test", handler2)
118
+ emitter.once("test", handler3)
119
+
120
+ emitter.emit("test", "data")
121
+
122
+ // All three handlers should be called despite self-removal
123
+ expect(handler1).toHaveBeenCalledTimes(1)
124
+ expect(handler2).toHaveBeenCalledTimes(1)
125
+ expect(handler3).toHaveBeenCalledTimes(1)
126
+ })
127
+ })
128
+
129
+ describe("off", () => {
130
+ it("should remove a handler", () => {
131
+ const emitter = new EventEmitter()
132
+ const handler = vi.fn()
133
+
134
+ emitter.on("test", handler)
135
+ emitter.off("test", handler)
136
+ emitter.emit("test", "data")
137
+
138
+ expect(handler).not.toHaveBeenCalled()
139
+ })
140
+
141
+ it("should only remove specified handler", () => {
142
+ const emitter = new EventEmitter()
143
+ const handler1 = vi.fn()
144
+ const handler2 = vi.fn()
145
+
146
+ emitter.on("test", handler1)
147
+ emitter.on("test", handler2)
148
+ emitter.off("test", handler1)
149
+ emitter.emit("test", "data")
150
+
151
+ expect(handler1).not.toHaveBeenCalled()
152
+ expect(handler2).toHaveBeenCalledTimes(1)
153
+ })
154
+
155
+ it("should handle removing non-existent handler", () => {
156
+ const emitter = new EventEmitter()
157
+ const handler = vi.fn()
158
+
159
+ expect(() => emitter.off("test", handler)).not.toThrow()
160
+ })
161
+
162
+ it("should handle removing from non-existent event", () => {
163
+ const emitter = new EventEmitter()
164
+ const handler = vi.fn()
165
+
166
+ expect(() => emitter.off("nonexistent", handler)).not.toThrow()
167
+ })
168
+
169
+ it("should properly clean up empty event handlers", () => {
170
+ const emitter = new EventEmitter()
171
+ const handler = vi.fn()
172
+
173
+ emitter.on("test", handler)
174
+ emitter.off("test", handler)
175
+
176
+ // Emitting should not throw even though internal array was removed
177
+ expect(() => emitter.emit("test", "data")).not.toThrow()
178
+ })
179
+ })
180
+
181
+ describe("removeAllListeners", () => {
182
+ it("should remove all handlers from a specific event", () => {
183
+ const emitter = new EventEmitter()
184
+ const handler1 = vi.fn()
185
+ const handler2 = vi.fn()
186
+
187
+ emitter.on("test", handler1)
188
+ emitter.on("test", handler2)
189
+ emitter.on("other", handler1)
190
+
191
+ emitter.removeAllListeners("test")
192
+ emitter.emit("test", "data")
193
+ emitter.emit("other", "data")
194
+
195
+ expect(handler1).toHaveBeenCalledTimes(1)
196
+ expect(handler2).not.toHaveBeenCalled()
197
+ })
198
+
199
+ it("should remove all handlers from all events", () => {
200
+ const emitter = new EventEmitter()
201
+ const handler1 = vi.fn()
202
+ const handler2 = vi.fn()
203
+
204
+ emitter.on("test", handler1)
205
+ emitter.on("other", handler2)
206
+
207
+ emitter.removeAllListeners()
208
+ emitter.emit("test", "data")
209
+ emitter.emit("other", "data")
210
+
211
+ expect(handler1).not.toHaveBeenCalled()
212
+ expect(handler2).not.toHaveBeenCalled()
213
+ })
214
+
215
+ it("should handle removing from non-existent event", () => {
216
+ const emitter = new EventEmitter()
217
+ expect(() => emitter.removeAllListeners("nonexistent")).not.toThrow()
218
+ })
219
+ })
220
+
221
+ describe("on() return value", () => {
222
+ it("should return unsubscribe function", () => {
223
+ const emitter = new EventEmitter()
224
+ const handler = vi.fn()
225
+
226
+ const unsubscribe = emitter.on("test", handler)
227
+ emitter.emit("test", "data")
228
+ expect(handler).toHaveBeenCalledTimes(1)
229
+
230
+ unsubscribe()
231
+ emitter.emit("test", "data")
232
+ expect(handler).toHaveBeenCalledTimes(1)
233
+ })
234
+
235
+ it("should handle multiple unsubscribe calls", () => {
236
+ const emitter = new EventEmitter()
237
+ const handler = vi.fn()
238
+
239
+ const unsubscribe = emitter.on("test", handler)
240
+ unsubscribe()
241
+ unsubscribe() // Should not throw
242
+
243
+ emitter.emit("test", "data")
244
+ expect(handler).not.toHaveBeenCalled()
245
+ })
246
+ })
247
+
248
+ describe("error handling", () => {
249
+ it("should continue emitting when handler throws", () => {
250
+ const emitter = new EventEmitter()
251
+ const throwingHandler = vi.fn(() => {
252
+ throw new Error("Handler error")
253
+ })
254
+ const normalHandler = vi.fn()
255
+
256
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
257
+
258
+ emitter.on("test", throwingHandler)
259
+ emitter.on("test", normalHandler)
260
+
261
+ expect(() => emitter.emit("test", "data")).not.toThrow()
262
+
263
+ expect(throwingHandler).toHaveBeenCalled()
264
+ expect(normalHandler).toHaveBeenCalledTimes(1)
265
+
266
+ consoleError.mockRestore()
267
+ })
268
+
269
+ it("should emit all handlers even if one throws", () => {
270
+ const emitter = new EventEmitter()
271
+ const errorHandler = vi.fn(() => {
272
+ throw new Error("Error")
273
+ })
274
+ const handler1 = vi.fn()
275
+ const handler2 = vi.fn()
276
+
277
+ vi.spyOn(console, "error").mockImplementation(() => {})
278
+
279
+ emitter.on("test", handler1)
280
+ emitter.on("test", errorHandler)
281
+ emitter.on("test", handler2)
282
+
283
+ emitter.emit("test", "data")
284
+
285
+ expect(handler1).toHaveBeenCalledTimes(1)
286
+ expect(handler2).toHaveBeenCalledTimes(1)
287
+
288
+ vi.restoreAllMocks()
289
+ })
290
+ })
291
+
292
+ describe("multiple events", () => {
293
+ it("should handle multiple different events independently", () => {
294
+ const emitter = new EventEmitter()
295
+ const handler1 = vi.fn()
296
+ const handler2 = vi.fn()
297
+ const handler3 = vi.fn()
298
+
299
+ emitter.on("event1", handler1)
300
+ emitter.on("event2", handler2)
301
+ emitter.on("event1", handler3)
302
+
303
+ emitter.emit("event1", "data1")
304
+ emitter.emit("event2", "data2")
305
+
306
+ expect(handler1).toHaveBeenCalledTimes(1)
307
+ expect(handler1).toHaveBeenCalledWith("data1")
308
+ expect(handler2).toHaveBeenCalledTimes(1)
309
+ expect(handler2).toHaveBeenCalledWith("data2")
310
+ expect(handler3).toHaveBeenCalledTimes(1)
311
+ expect(handler3).toHaveBeenCalledWith("data1")
312
+ })
313
+
314
+ it("should handle large number of events", () => {
315
+ const emitter = new EventEmitter()
316
+ const handlers = []
317
+
318
+ for (let i = 0; i < 100; i++) {
319
+ handlers.push(function handler() {})
320
+ emitter.on(`event${i}`, handlers[i])
321
+ }
322
+
323
+ expect(() => emitter.emit("event50", "test")).not.toThrow()
324
+ })
325
+ })
326
+
327
+ describe("handler order", () => {
328
+ it("should call handlers in order of registration", () => {
329
+ const emitter = new EventEmitter()
330
+ const calls = []
331
+
332
+ emitter.on("test", () => calls.push(1))
333
+ emitter.on("test", () => calls.push(2))
334
+ emitter.on("test", () => calls.push(3))
335
+
336
+ emitter.emit("test")
337
+
338
+ expect(calls).toEqual([1, 2, 3])
339
+ })
340
+
341
+ it("should maintain order when removing middle handler", () => {
342
+ const emitter = new EventEmitter()
343
+ const calls = []
344
+
345
+ const handler2 = () => calls.push(2)
346
+
347
+ emitter.on("test", () => calls.push(1))
348
+ emitter.on("test", handler2)
349
+ emitter.on("test", () => calls.push(3))
350
+
351
+ emitter.off("test", handler2)
352
+ emitter.emit("test")
353
+
354
+ expect(calls).toEqual([1, 3])
355
+ })
356
+ })
357
+ })