Skip to content

Commit 11873dc

Browse files
authored
Merge pull request #30 from sikanhe/add-event-emitter
Add EventEmitter module
2 parents 341bb8b + 75a3409 commit 11873dc

File tree

6 files changed

+334
-5
lines changed

6 files changed

+334
-5
lines changed

.gitignore

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,5 @@ bsb
88
*.bs.js
99
package-lock.json
1010

11-
src/ScratchPadJs.js
12-
src/ScratchRE.re
13-
src/ScratchML.ml
14-
src/Events.re
11+
src/ScratchRe.re
12+
src/ScratchMl.ml

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
"build": "bsb -make-world",
66
"start": "bsb -make-world -w",
77
"clean": "bsb -clean-world",
8-
"test": "jest"
8+
"test": "jest",
9+
"clean-build": "bsb -clean-world && bsb -make-world",
10+
"clean-start": "bsb -clean-world && bsb -make-world -w",
11+
"clean-test": "bsb -clean-world && jest"
912
},
1013
"keywords": [
1114
"BuckleScript"

src/Event.re

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* The `Event` type represents the strings/symbols used in Node to
3+
* identify event types for `EventEmitter` and its subclasses, including
4+
* streams, sockets, and servers.
5+
*
6+
* Given a type signature `Event.t('a => 'b, 'ty)`, the first type
7+
* variable, `'a => 'b`, denotes the type signature of the event listener
8+
* function, and `'ty` denotes the type of the associated `EventEmitter`.
9+
*
10+
* These abstract `Event.t` types must be passed to `EventEmitter`
11+
* functions to register event listeners or emit events. By encoding the
12+
* listener function type in a type variable, we can ensure that each
13+
* listener has the correct type. The type parameter for the emitter
14+
* prevents two different emitters from using each other's events.
15+
*
16+
* While this gives us some degree of type safety, it is still possible
17+
* to introduce runtime errors with this API. In particular, two or more
18+
* `Event.t` types can be defined from the same string/symbol, but with
19+
* different listener types. Therefore, we strongly recommend using
20+
* 100% unique strings/symbols to define events.
21+
*
22+
*/
23+
type t('listener, 'ty);
24+
external fromString: string => t('a => 'b, 'ty) = "%identity";
25+
external fromSymbol: Js.Types.symbol => t('a => 'b, 'ty) = "%identity";
26+
external unsafeToString: t('a => 'b, 'ty) => string = "%identity";
27+
external unsafeToSymbol: t('a => 'b, 'ty) => Js.Types.symbol = "%identity";
28+
type case =
29+
| String(string)
30+
| Symbol(Js.Types.symbol)
31+
| Unknown;
32+
let classify = evt => {
33+
switch (Js.typeof(evt)) {
34+
| "string" => String(unsafeToString(evt))
35+
| "symbol" => Symbol(unsafeToSymbol(evt))
36+
| _ => Unknown
37+
};
38+
};
39+
let eq = (event1, event2) => {
40+
switch (Js.typeof(event1), Js.typeof(event2)) {
41+
| ("string", "string") => Obj.magic(event1) === Obj.magic(event2)
42+
| ("symbol", "symbol") => Obj.magic(event1) === Obj.magic(event2)
43+
| _ => false
44+
};
45+
};

src/EventEmitter.re

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* `Impl` is a functor which generates FFI bindings to Node's `EventEmitter`
3+
* class for any type `t`. This is not inherently type-safe. Type-safety can
4+
* be achieved by implementing the known `Event.t('a => 'b, t)` types
5+
*/
6+
module Impl = (T: {type t;}) => {
7+
/**
8+
* `addListener(emitter, event, listener)`
9+
*
10+
* Adds a new event listener function to the event emitter.
11+
*/
12+
[@bs.send]
13+
external addListener: (T.t, Event.t('a => 'b, T.t), 'a => 'b) => T.t =
14+
"addListener";
15+
16+
[@bs.send]
17+
external emit: (T.t, Event.t('a => 'b, T.t), 'a) => bool = "emit";
18+
19+
[@bs.get] external errorMonitor: T.t => Js.Types.symbol = "errorMonitor";
20+
21+
[@bs.send]
22+
external eventNames:
23+
(T.t, Event.t('a => 'b, T.t)) => array(Event.t('a => 'b, T.t)) =
24+
"eventNames";
25+
26+
[@bs.send] external getMaxListeners: T.t => int = "getMaxListeners";
27+
28+
[@bs.send]
29+
external listenerCount: (T.t, Event.t('a => 'b, T.t)) => int =
30+
"listenerCount";
31+
[@bs.send]
32+
external listeners: (T.t, Event.t('a => 'b, T.t)) => array('a => 'b) =
33+
"listeners";
34+
35+
/**
36+
* `on(emitter, event, listener)`
37+
*
38+
* Adds a new event listener function to the event emitter.
39+
* Alias for `addListener`.
40+
*/
41+
[@bs.send]
42+
external on: (T.t, Event.t('a => 'b, T.t), 'a => 'b) => T.t = "on";
43+
44+
/**
45+
* `once(emitter, event, listener)`
46+
*
47+
* Adds a new **single-use** event listener function to the event
48+
* emitter. Then next time the given event is emitted, this listener
49+
* will fire exactly once, and then be removed from the emitter's
50+
* internal listener array.
51+
*/
52+
[@bs.send]
53+
external once: (T.t, Event.t('a => 'b, T.t), 'a => 'b) => T.t = "once";
54+
55+
/**
56+
* `off(emitter, event, listener)`
57+
*
58+
* Removes the listener function from the event emitter.
59+
*
60+
* The specified listener function is compared by **referential
61+
* equality** to each function in the emitter's internal listener
62+
* array.
63+
*
64+
* This means that, when the target listener is initially added, that
65+
* exact function reference must be maintained and provided here
66+
* in order to ensure removal.
67+
*
68+
* Alias for `removeListener`.
69+
*/
70+
[@bs.send]
71+
external off: (T.t, Event.t('a => 'b, T.t), 'a => 'b) => T.t = "off";
72+
73+
/**
74+
* `prependListener(emitter, event, listener)`
75+
*
76+
* Adds a new event listener function to the event emitter.
77+
*
78+
* Unlike `on` and `addListener`, `prependListener` adds the listener
79+
* function to the front of the internal listener array, ensuring
80+
* that this function is called before the rest of the listeners for
81+
* the given event.
82+
*/
83+
[@bs.send]
84+
external prependListener: (T.t, Event.t('a => 'b, T.t), 'a => 'b) => T.t =
85+
"prependListener";
86+
87+
/**
88+
* `prependListenerOnce(emitter, event, listener)`
89+
*
90+
* Adds a new **single-use** event listener function to the event
91+
* emitter. Then next time the given event is emitted, this listener
92+
* will fire exactly once, and then be removed from the emitter's
93+
* internal listener array.
94+
*
95+
* Unlike `once`, `prependListenerOnce` adds the listener function
96+
* to the front of the internal listener array, ensuring that this
97+
* function is called before the rest of the listeners for the
98+
* given event.
99+
*/
100+
[@bs.send]
101+
external prependOnceListener: (T.t, Event.t('a => 'b, T.t), 'a => 'b) => T.t =
102+
"prependOnceListener";
103+
104+
[@bs.send] external removeAllListeners: T.t => T.t = "removeAllListeners";
105+
106+
/**
107+
* `removeListener(emitter, event, listener)`
108+
*
109+
* Removes the listener function from the event emitter.
110+
*
111+
* The specified listener function is compared by **referential
112+
* equality** to each function in the emitter's internal listener
113+
* array.
114+
*
115+
* This means that, when the target listener is initially added, that
116+
* exact function reference must be maintained and provided here
117+
* in order to ensure removal.
118+
*/
119+
[@bs.send]
120+
external removeListener: (T.t, Event.t('a => 'b, T.t), 'a => 'b) => T.t =
121+
"removeListener";
122+
123+
/**
124+
* `setMaxListeners(emitter, numberOfListeners)`
125+
*
126+
* Sets the maximum number of event listeners that may be added to
127+
* an event emitter before Node begins emitting warnings.
128+
*
129+
* By default, each event emitter has this value set to 10. This is
130+
* intended to warn the user about possible memory leaks.
131+
* `setMaxListeners` will increase this threshold.
132+
*/
133+
[@bs.send]
134+
external setMaxListeners: (T.t, int) => T.t = "setMaxListeners";
135+
};
136+
137+
/**
138+
* A generative functor that creates a unique type `t` with the `EventEmitter`
139+
* interface bindings.
140+
*/
141+
module Make = (()) => {
142+
type t;
143+
include Impl({
144+
type nonrec t = t;
145+
});
146+
[@bs.module "events"] [@bs.new] external make: unit => t = "EventEmitter";
147+
};
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
open Jest;
2+
3+
describe("EventEmitter", () => {
4+
test(
5+
"'Emitter.make' should create a new emitter instance that is defined", () => {
6+
open! ExpectJs;
7+
open! EventEmitterTestLib;
8+
let emitter = Emitter1.make();
9+
expect(emitter->Js.Undefined.return) |> toBeDefined;
10+
});
11+
12+
test("'Emitter.addListener' should add a new event listener", () => {
13+
open! ExpectJs;
14+
open! EventEmitterTestLib;
15+
let listeners =
16+
Emitter1.(
17+
{
18+
make()->addListener(Events.text, _ => ())->listeners(Events.text);
19+
}
20+
);
21+
expect(Array.length(listeners)) |> toBe(1);
22+
});
23+
24+
test("'Emitter.on' should add a new event listener", () => {
25+
open! ExpectJs;
26+
open! EventEmitterTestLib;
27+
let listeners =
28+
Emitter1.(
29+
{
30+
make()->on(Events.text, _ => ())->listeners(Events.text);
31+
}
32+
);
33+
expect(Array.length(listeners)) |> toBe(1);
34+
});
35+
36+
test("'Emitter.on' should add a new event listener", () => {
37+
open! ExpectJs;
38+
open! EventEmitterTestLib;
39+
let listeners =
40+
Emitter1.(
41+
{
42+
make()->on(Events.text, _ => ())->listeners(Events.text);
43+
}
44+
);
45+
expect(Array.length(listeners)) |> toBe(1);
46+
});
47+
48+
test("'Emitter.removeListener' should remove the event listener", () => {
49+
open! ExpectJs;
50+
open! EventEmitterTestLib;
51+
let eventListener = (_) => ();
52+
let listeners =
53+
Emitter1.(
54+
{
55+
make()
56+
|> on(_, Events.text, eventListener)
57+
|> removeListener(_, Events.text, eventListener)
58+
|> listeners(_, Events.text);
59+
}
60+
);
61+
expect(Array.length(listeners)) |> toBe(0);
62+
});
63+
64+
test("'Emitter.off' should remove the event listener", () => {
65+
open! ExpectJs;
66+
open! EventEmitterTestLib;
67+
let eventListener = (_) => ();
68+
let listeners =
69+
Emitter1.(
70+
{
71+
make()
72+
|> on(_, Events.text, eventListener)
73+
|> off(_, Events.text, eventListener)
74+
|> listeners(_, Events.text);
75+
}
76+
);
77+
expect(Array.length(listeners)) |> toBe(0);
78+
});
79+
80+
test("'Emitter.emit' should execute each listener for the correct event", () => {
81+
open! ExpectJs;
82+
open! EventEmitterTestLib;
83+
let ref1 = ref(0);
84+
let ref2 = ref(0);
85+
let data1 = 1;
86+
let data2 = 2;
87+
let listener1 = (_) => { ref1 := data1; };
88+
let listener2 = (_) => { ref2 := data2; };
89+
Emitter1.({
90+
let emitter =
91+
make()
92+
|> on(_, Events.integer, listener1)
93+
|> on(_, Events.integer, listener2);
94+
emit(emitter, Events.integer, data1)->ignore;
95+
emit(emitter, Events.integer, data2)->ignore;
96+
});
97+
Assert.strictEqual(ref1^, 1);
98+
Assert.strictEqual(ref2^, 2);
99+
expect(ref1^ === 1 && ref2^ === 2) |> toBe(true);
100+
});
101+
102+
test("'Emitter.removeAllListeners' should remove all event listeners", () => {
103+
open! ExpectJs;
104+
open! EventEmitterTestLib;
105+
let eventListener = (_) => ();
106+
let emitter =
107+
Emitter1.(
108+
{
109+
make()
110+
|> on(_, Events.text, eventListener)
111+
|> on(_, Events.text, eventListener)
112+
|> on(_, Events.text, eventListener)
113+
}
114+
);
115+
// Make sure 3 listeners were indeed added:
116+
Assert.strictEqual(Emitter1.(listeners(emitter, Events.text))->Array.length, 3);
117+
// Remove all the listeners:
118+
Emitter1.removeAllListeners(emitter)->ignore;
119+
expect(Emitter1.(listeners(emitter, Events.text))->Array.length) |> toBe(0);
120+
});
121+
122+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Emitter1 = {
2+
include EventEmitter.Make();
3+
4+
let uniqueSymbol: Js.Types.symbol = [%raw {|Symbol("emitter1")|}];
5+
6+
module Events = {
7+
let symbol: Event.t(Js.Types.symbol => unit, t) = Event.fromSymbol(uniqueSymbol);
8+
let text: Event.t(string => unit, t) = Event.fromString("text");
9+
let integer: Event.t(int => unit, t) = Event.fromString("integer");
10+
let textAndInteger: Event.t((string, int) => unit, t) = Event.fromString("textAndInteger");
11+
};
12+
13+
};
14+

0 commit comments

Comments
 (0)