diff --git a/README.md b/README.md index 3a43bb2..10053a6 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ This event can now be emitted and subscribed to. ### Subscribing to an Event Listener functions are subscribed to an event using -`on(emitter: Emitter, E: event, function(E))`: +`on(emitter: Emitter, E: event, function(E), config?: ListenerConfig)`. ```teal emitter:on(WarningEvent, function(event: WarningEvent) @@ -94,6 +94,19 @@ emitter:emit(WarningEvent.new("This is a warning")) Emitting the event will print "This is a warning". +### Event listener configuration + +An optional configuration record can be passed when subscribing to an event +when using `on` or `once`: + +* `id`: string: An identifier for the listener (defaults to ""). An identifier + can be used to unsubscribe the listener by ID. No uniqueness checks are + performed on the ID; multiple listeners can use the same ID, allowing events + to be grouped. +* `position`: "first" | "last" (default): Controls whether the listener + is added as the last listener for the event using "last" (the default + behavior) or the first listener for the event "first". + ### Unsubscribing from an Event `off(event: E, listener: Listener | string)` is used to @@ -116,7 +129,7 @@ When subscribing to an event, an identifier can be given to the listener so that the listener can be unsubscribed by ID rather than the actual function. ```teal -emitter:on(WarningEvent, onWarning, "warning") +emitter:on(WarningEvent, onWarning, { id = "warning" }) emitter:off(WarningEvent, "warning") ``` @@ -124,8 +137,8 @@ IDs can be used for grouping event listeners. ```teal -- While adding listeners, use the same ID to group them. -emitter:on(WarningEvent, b, "print-group") -emitter:on(WarningEvent, b, "print-group") +emitter:on(WarningEvent, b, { id = "print-group" }) +emitter:on(WarningEvent, b, { id = "print-group" }) -- Remove both a and b listeners because they both have the id "print-group". emitter:off(WarningEvent, "print-group") diff --git a/emitter.lua b/emitter.lua index e3c463e..4779822 100644 --- a/emitter.lua +++ b/emitter.lua @@ -6,7 +6,21 @@ -local Emitter = {} +local Emitter = {ListenerConfig = {}, } + + + + + + + + + + + + + + @@ -77,7 +91,7 @@ local LinkedList = {} -function LinkedList.pushItem(list, node) +function LinkedList.append(list, node) if list.head == nil then list.head = node list.tail = node @@ -88,7 +102,18 @@ function LinkedList.pushItem(list, node) end end -function LinkedList.removeItem(list, node) +function LinkedList.prepend(list, node) + if list.head == nil then + list.head = node + list.tail = node + else + list.head.prev = node + node.next = list.head + list.head = node + end +end + +function LinkedList.remove(list, node) if node.prev then (node.prev).next = node.next else @@ -112,6 +137,8 @@ end +local DEFAULT_CONFIG = { id = "", position = "last" } + local emitter_mt = { __index = Emitter } function Emitter.new() @@ -126,14 +153,19 @@ function Emitter:reset() self._forwarding = {} end -function Emitter:on(event, listener, id) +function Emitter:on(event, listener, config) local list = self._listeners[event] if not list then list = {} self._listeners[event] = list end - local node = { id = id or "", listener = listener } - LinkedList.pushItem(list, node) + config = config or DEFAULT_CONFIG + local node = { id = config.id or "", listener = listener } + if config.position == "last" then + LinkedList.append(list, node) + else + LinkedList.prepend(list, node) + end end function Emitter:off(event, listener) @@ -144,7 +176,7 @@ function Emitter:off(event, listener) local node = listeners.head while node do if node.id == listener then - LinkedList.removeItem(listeners, node) + LinkedList.remove(listeners, node) end node = node.next end @@ -152,20 +184,20 @@ function Emitter:off(event, listener) local node = listeners.head while node do if node.listener == listener then - LinkedList.removeItem(listeners, node) + LinkedList.remove(listeners, node) end node = node.next end end end -function Emitter:once(event, listener, id) +function Emitter:once(event, listener, config) local wrappedFunction wrappedFunction = function(e) self:off(event, wrappedFunction) listener(e) end - self:on(event, wrappedFunction, id) + self:on(event, wrappedFunction, config) end function Emitter:emit(event) @@ -191,14 +223,14 @@ end function Emitter:startForwarding(emitter) local node = { emitter = emitter } - LinkedList.pushItem(self._forwarding, node) + LinkedList.append(self._forwarding, node) end function Emitter:stopForwarding(emitter) local node = self._forwarding.head while node do if node.emitter == emitter then - LinkedList.removeItem(self._forwarding, node) + LinkedList.remove(self._forwarding, node) return end node = node.next diff --git a/emitter.tl b/emitter.tl index 1b1d5d7..cbb5b2f 100644 --- a/emitter.tl +++ b/emitter.tl @@ -20,6 +20,20 @@ local record Emitter --- Receives a specific type of emitted events. type Listener = function(event: E) + --- Optional configuration used to configure a listener. + record ListenerConfig + id: string + position: ListenerPosition + end + + --- The position to insert a listener. + enum ListenerPosition + --- Insert the listener at the beginning of the list of listeners. + "first" + --- Insert the listener at the end of the list of listeners. + "last" + end + --- Create a new Emitter. -- @return the created Emitter new: function(): Emitter @@ -27,8 +41,8 @@ local record Emitter --- Add an event listener to the object. -- @param event The event type to subscribe to. -- @param listener Listener to invoke. - -- @param id? Optional name to assign the listener. - on: function(self: Emitter, event: E, listener: Listener, id?: string) + -- @param config? Optional listener configuration. + on: function(self: Emitter, event: E, listener: Listener, config?: ListenerConfig) --- Unsubscribes a listener from an event. -- @param event Event to unsubscribe from. @@ -38,8 +52,8 @@ local record Emitter --- Add an event listener that is unsubscribed after receiving an event. -- @param event The event type to subscribe to. -- @param listener Listener to invoke. - -- @param id Optional name to assign the listener. - once: function(self: Emitter, event: E, listener: Listener, id?: string) + -- @param config? Optional listener configuration. + once: function(self: Emitter, event: E, listener: Listener, config?: ListenerConfig) --- Emit an event to all listners. -- @param event Event to emit. @@ -77,7 +91,7 @@ local record LinkedList> tail: N end -function LinkedList.pushItem, L is LinkedList>(list: L, node: N) +function LinkedList.append, L is LinkedList>(list: L, node: N) if list.head == nil then list.head = node list.tail = node @@ -88,7 +102,18 @@ function LinkedList.pushItem, L is LinkedList>(list: L, no end end -function LinkedList.removeItem, L is LinkedList>(list: L, node: N) +function LinkedList.prepend, L is LinkedList>(list: L, node: N) + if list.head == nil then + list.head = node + list.tail = node + else + list.head.prev = node + node.next = list.head + list.head = node + end +end + +function LinkedList.remove, L is LinkedList>(list: L, node: N) if node.prev then (node.prev as N).next = node.next else @@ -112,6 +137,8 @@ end ----------------------------------------------------------------------------- +local DEFAULT_CONFIG: Emitter.ListenerConfig = { id = "", position = "last" } + local emitter_mt: metatable = { __index = Emitter } function Emitter.new(): Emitter @@ -126,14 +153,19 @@ function Emitter:reset() self._forwarding = {} end -function Emitter:on(event: E, listener: Emitter.Listener, id?: string) +function Emitter:on(event: E, listener: Emitter.Listener, config?: Emitter.ListenerConfig) local list = self._listeners[event] if not list then list = {} self._listeners[event] = list end - local node: ListenerNode = { id = id or "", listener = listener as Emitter.Listener } - LinkedList.pushItem(list, node as ListNode) + config = config or DEFAULT_CONFIG + local node: ListenerNode = { id = config.id or "", listener = listener as Emitter.Listener } + if config.position == "last" then + LinkedList.append(list, node as ListNode) + else + LinkedList.prepend(list, node as ListNode) + end end function Emitter:off(event: E, listener: Emitter.Listener | string) @@ -144,7 +176,7 @@ function Emitter:off(event: E, listener: Emitter.Listener local node = listeners.head while node do if node.id == listener then - LinkedList.removeItem(listeners, node as ListNode) + LinkedList.remove(listeners, node as ListNode) end node = node.next end @@ -152,20 +184,20 @@ function Emitter:off(event: E, listener: Emitter.Listener local node = listeners.head while node do if node.listener == listener as Emitter.Listener then - LinkedList.removeItem(listeners, node as ListNode) + LinkedList.remove(listeners, node as ListNode) end node = node.next end end end -function Emitter:once(event: E, listener: Emitter.Listener, id?: string) +function Emitter:once(event: E, listener: Emitter.Listener, config?: Emitter.ListenerConfig) local wrappedFunction: Emitter.Listener wrappedFunction = function(e: E) self:off(event, wrappedFunction) listener(e) end - self:on(event, wrappedFunction, id) + self:on(event, wrappedFunction, config) end function Emitter:emit(event: Emitter.Event) @@ -191,14 +223,14 @@ end function Emitter:startForwarding(emitter: Emitter) local node: EmitterNode = { emitter = emitter } - LinkedList.pushItem(self._forwarding, node as ListNode) + LinkedList.append(self._forwarding, node as ListNode) end function Emitter:stopForwarding(emitter: Emitter) local node = self._forwarding.head while node do if node.emitter == emitter then - LinkedList.removeItem(self._forwarding, node as ListNode) + LinkedList.remove(self._forwarding, node as ListNode) return end node = node.next diff --git a/spec/emitter_spec.lua b/spec/emitter_spec.lua index 653e69d..7e3f98d 100644 --- a/spec/emitter_spec.lua +++ b/spec/emitter_spec.lua @@ -39,6 +39,21 @@ describe("Emitter", function() assert.equals(3, #messages) assert.equals("Depart: Bye", messages[3]) end) + + it("can prepend events", function() + local messages = {} + local emitter = Emitter.new() + emitter:on(Greet, function(greet) + table.insert(messages, greet.name .. " 1") + end, { position = "first" }) + emitter:on(Greet, function(greet) + table.insert(messages, greet.name .. " 2") + end, { position = "first" }) + emitter:emit(Greet.new("Hi")) + assert.equals(2, #messages) + assert.equals("Hi 2", messages[1]) + assert.equals("Hi 1", messages[2]) + end) end) describe("unsubscribe", function() @@ -83,7 +98,7 @@ describe("Emitter", function() local listener = function(greet) table.insert(messages, greet.name) end - emitter:on(Greet, listener, "id") + emitter:on(Greet, listener, { id = "id" }) emitter:emit(Greet.new("Hi")) assert.equals(1, #messages) assert.equals("Hi", messages[1]) @@ -99,8 +114,8 @@ describe("Emitter", function() local listener = function(greet) table.insert(messages, greet.name) end - emitter:on(Greet, listener, "id") - emitter:on(Greet, listener, "id") + emitter:on(Greet, listener, { id = "id" }) + emitter:on(Greet, listener, { id = "id" }) emitter:emit(Greet.new("Hi")) assert.equals(2, #messages) diff --git a/spec/integ.tl b/spec/integ.tl index 34a78c0..408ae1b 100644 --- a/spec/integ.tl +++ b/spec/integ.tl @@ -41,7 +41,7 @@ end emitter:on(WarningEvent, onWarning) emitter:off(WarningEvent, onWarning) -emitter:on(WarningEvent, onWarning, "warning") +emitter:on(WarningEvent, onWarning, {id = "warning" }) emitter:off(WarningEvent, "warning") -- Receiving an event at most once