Skip to content

Commit

Permalink
Add ability to add listener first or last
Browse files Browse the repository at this point in the history
  • Loading branch information
mtdowling committed Dec 10, 2024
1 parent 82eadd0 commit e1321ee
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 35 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<E is Event>(emitter: Emitter, E: event, function(E))`:
`on<E is Event>(emitter: Emitter, E: event, function(E), config?: ListenerConfig)`.

```teal
emitter:on(WarningEvent, function(event: WarningEvent)
Expand All @@ -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"`: Controls whether the listener is added as the
last listener for the event using "last" (the default) or the first listener
for the event using "first".

### Unsubscribing from an Event

`off<E is Event>(event: E, listener: Listener<E> | string)` is used to
Expand All @@ -116,16 +129,16 @@ 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")
```

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")
Expand Down
56 changes: 44 additions & 12 deletions emitter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,21 @@



local Emitter = {}
local Emitter = {ListenerConfig = {}, }

















Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -112,6 +137,8 @@ end



local DEFAULT_CONFIG = { id = "", position = "last" }

local emitter_mt = { __index = Emitter }

function Emitter.new()
Expand All @@ -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)
Expand All @@ -144,28 +176,28 @@ 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
else
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)
Expand All @@ -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
Expand Down
62 changes: 47 additions & 15 deletions emitter.tl
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,29 @@ local record Emitter
--- Receives a specific type of emitted events.
type Listener = function<E is Event>(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

--- 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<E is Event>(self: Emitter, event: E, listener: Listener<E>, id?: string)
-- @param config? Optional listener configuration.
on: function<E is Event>(self: Emitter, event: E, listener: Listener<E>, config?: ListenerConfig)

--- Unsubscribes a listener from an event.
-- @param event Event to unsubscribe from.
Expand All @@ -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<E is Event>(self: Emitter, event: E, listener: Listener<E>, id?: string)
-- @param config? Optional listener configuration.
once: function<E is Event>(self: Emitter, event: E, listener: Listener<E>, config?: ListenerConfig)

--- Emit an event to all listners.
-- @param event Event to emit.
Expand Down Expand Up @@ -77,7 +91,7 @@ local record LinkedList<N is ListNode<any>>
tail: N
end

function LinkedList.pushItem<N is ListNode<any>, L is LinkedList<N>>(list: L, node: N)
function LinkedList.append<N is ListNode<any>, L is LinkedList<N>>(list: L, node: N)
if list.head == nil then
list.head = node
list.tail = node
Expand All @@ -88,7 +102,18 @@ function LinkedList.pushItem<N is ListNode<any>, L is LinkedList<N>>(list: L, no
end
end

function LinkedList.removeItem<N is ListNode<any>, L is LinkedList<N>>(list: L, node: N)
function LinkedList.prepend<N is ListNode<any>, L is LinkedList<N>>(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<N is ListNode<any>, L is LinkedList<N>>(list: L, node: N)
if node.prev then
(node.prev as N).next = node.next
else
Expand All @@ -112,6 +137,8 @@ end

-----------------------------------------------------------------------------

local DEFAULT_CONFIG: Emitter.ListenerConfig = { id = "", position = "last" }

local emitter_mt: metatable<Emitter> = { __index = Emitter }

function Emitter.new(): Emitter
Expand All @@ -126,14 +153,19 @@ function Emitter:reset()
self._forwarding = {}
end

function Emitter:on<E is Emitter.Event>(event: E, listener: Emitter.Listener<E>, id?: string)
function Emitter:on<E is Emitter.Event>(event: E, listener: Emitter.Listener<E>, 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<any> }
LinkedList.pushItem(list, node as ListNode<any>)
config = config or DEFAULT_CONFIG
local node: ListenerNode = { id = config.id or "", listener = listener as Emitter.Listener<any> }
if config.position == "last" then
LinkedList.append(list, node as ListNode<any>)
else
LinkedList.prepend(list, node as ListNode<any>)
end
end

function Emitter:off<E is Emitter.Event>(event: E, listener: Emitter.Listener<E> | string)
Expand All @@ -144,28 +176,28 @@ function Emitter:off<E is Emitter.Event>(event: E, listener: Emitter.Listener<E>
local node = listeners.head
while node do
if node.id == listener then
LinkedList.removeItem(listeners, node as ListNode<any>)
LinkedList.remove(listeners, node as ListNode<any>)
end
node = node.next
end
else
local node = listeners.head
while node do
if node.listener == listener as Emitter.Listener<any> then
LinkedList.removeItem(listeners, node as ListNode<any>)
LinkedList.remove(listeners, node as ListNode<any>)
end
node = node.next
end
end
end

function Emitter:once<E is Emitter.Event>(event: E, listener: Emitter.Listener<E>, id?: string)
function Emitter:once<E is Emitter.Event>(event: E, listener: Emitter.Listener<E>, config?: Emitter.ListenerConfig)
local wrappedFunction: Emitter.Listener<E>
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)
Expand All @@ -191,14 +223,14 @@ end

function Emitter:startForwarding(emitter: Emitter)
local node: EmitterNode = { emitter = emitter }
LinkedList.pushItem(self._forwarding, node as ListNode<any>)
LinkedList.append(self._forwarding, node as ListNode<any>)
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<any>)
LinkedList.remove(self._forwarding, node as ListNode<any>)
return
end
node = node.next
Expand Down
21 changes: 18 additions & 3 deletions spec/emitter_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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])
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion spec/integ.tl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e1321ee

Please sign in to comment.