--[[
--------------------------------------------------
--- EventListener
--- - 1.1.0.0
--- - MyPad
---
--------------------------------------------------
---
--- Changelog:
--- - Updated current implementation.
---
--------------------------------------------------
---
--- A
]]
if Debug and Debug.beginFile then Debug.beginFile("EventListener") end
do
--------------------------------------------------
--- Configuration section
--------------------------------------------------
local config = {
-- Determines the default amount of times a function can recursively be called
-- before the system treats it as a runaway infinite loop.
MAX_RECURSION = 8,
-- The maximum number of times a function can repeatedly be called
-- within a stack before it is considered non-halting (infinite loop).
SANITY_MAX_RECURSION = 32,
-- Appends the current index of the executing function as the first parameter
-- among the list of parameters to be used.
APPEND_INDEX = false,
-- Default function
DEF_FUN = DoNothing,
-- Blacklisted functions.
BLACKLIST = {
[_G['DoNothing']] = true,
}
}
--------------------------------------------------
--- End Configuration section
--------------------------------------------------
---@alias callable fun() | table
---@class EventListener
local eventListener = {}
EventListener = eventListener
---@class Subscriber
local subscriber = {}
---@class Subscriber
local nullSubscriber = setmetatable({}, subscriber)
-----------------------------------------
--- Define class members
-----------------------------------------
do
---@class EventListener
---@field private _subList Subscriber[]
---@field private _curDepth integer The current call stack depth of the object.
---@field private _curIndex integer The current sub index of the running object.
---@field private _curFun Subscriber
---@field private _size integer The number of subscribers to an EventListener.
---@field _maxDepth integer The number of subscribers to an EventListener.
---@class Subscriber
---@field _callable callable
---@field _ableCounter integer The counter value which determines if a callable action should be executed or not.
---@field _depthCounter integer The current depth of the callable action in the call stack.
---@field _event EventListener A pointer to the holding EventListener object.
end
-----------------------------------------
--- Define read and write access actions.
-----------------------------------------
do
local rawset = rawset
eventListener.__index = eventListener
eventListener.__metatable = eventListener
eventListener.__newindex =
function (t, k, v)
if eventListener[k] then return eventListener end
return rawset(t, k, v)
end
function eventListener:__len()
return self._size or 0
end
subscriber.__index = subscriber
subscriber.__metatable = subscriber
subscriber.__newindex =
function (t, k, v)
if eventListener[k] then return eventListener end
return rawset(t, k, v)
end
end
local isCallable = IsCallable
if not isCallable then
isCallable =
function (obj)
return (type(obj) == 'function') or
(type(obj) == 'table') and
(isCallable(getmetatable(obj)))
end
end
local try = try or
function (fun, ...)
return select(2, xpcall(fun, print, ...))
end
---Creates a new instance.
---@param o? table | nil
---@return EventListener
function eventListener.create(o)
o = o or {
_subList = {},
_curDepth = 0,
_curIndex = 0,
_maxDepth = config.MAX_RECURSION,
_size = 0,
_curFun = nullSubscriber,
}
setmetatable(o, eventListener)
return o
end
--- This will try to remove sparseness in the list assuming
--- the full size of the list is known.
---@param list table
---@param trueSize integer
---@param startIndex? integer=1
local function densifyList(list, trueSize, startIndex)
local i = startIndex or 1
local j, n = 0, 0
while (i <= trueSize) do
if (list[i] ~= nil) then
i = i + 1
goto continue
end
j = j + 1
n = i + j
if (n > #list) then
break
end
if list[n] then
list[i] = list[n]
list[n] = nil
end
::continue::
end
end
do
local insert, remove = table.insert, table.remove
---@param obj callable
---@param initDisabled? boolean
---@return Subscriber
function eventListener:register(obj, initDisabled)
if (not isCallable(obj)) or
(config.BLACKLIST[obj]) then
return nullSubscriber
end
local newSub = setmetatable({
_callable = obj,
_ableCounter = (initDisabled and 0) or 1,
_depthCounter = 0,
_event = self
}, subscriber)
insert(self._subList, newSub)
self._size = self._size + 1
return newSub
end
---@param subscribed Subscriber
---@param allowSparse? boolean
function eventListener:unregister(subscribed, allowSparse)
if ((not subscribed) or (subscribed == nullSubscriber)) or
(subscribed._event ~= self) then
return
end
--- Locate the subscribed object's index and remove
--- it from the subscriber list.
for i = 1, self._size do
if (self._subList[i] == subscribed) then
remove(self._subList, i)
self._size = self._size - 1
if allowSparse then
self._subList[i] = nil
goto terminate
end
densifyList(self._subList, self._size, i)
::terminate::
break
end
end
-- Not necessary, but I like to clear all relevant data.
do
subscribed._callable = nil
subscribed._ableCounter = nil
subscribed._depthCounter = nil
subscribed._event = nil
end
do
local mt = subscriber.__metatable
subscriber.__metatable = nil
setmetatable(subscribed, nil)
subscriber.__metatable = mt
end
end
function subscriber:unsub()
if (not self) or (self == nullSubscriber) then
return
end
self._event:unregister(self)
end
function subscriber:isNonHalting()
if (not self) or (self == nullSubscriber) then
return true
end
local maxDepth = config.SANITY_MAX_RECURSION
if (self._event._maxDepth > 0) and
(self._event._maxDepth <= maxDepth) then
maxDepth = self._event._maxDepth
end
return self._depthCounter >= maxDepth
end
function subscriber:isEnabled()
if (not self) or (self == nullSubscriber) then
return false
end
return self._ableCounter > 0
end
function subscriber:run(...)
if (not self:isEnabled()) or (self:isNonHalting()) then
return
end
---@diagnostic disable-next-line: param-type-mismatch
try(self._callable, ...)
end
function subscriber:enable(flag)
if (not self) or (self == nullSubscriber)then
return self
end
local inc = ((flag) and 1) or -1
self._ableCounter = self._ableCounter + inc
return self
end
function subscriber:forceEnable(flag)
if (not self) or (self == nullSubscriber)then
return self
end
local inc = ((flag) and 1) or 0
self._ableCounter = inc
return self
end
end
function eventListener:clear()
while (#self > 0) do
self:unregister(self._subList[#self], true)
end
return self
end
-- Destroys the instance.
function eventListener:destroy()
self:clear()
do
self._subList = nil
self._curDepth = nil
self._curIndex = nil
self._curFun = nil
self._maxDepth = nil
self._size = nil
end
do
local mt = eventListener.__metatable
eventListener.__metatable = nil
setmetatable(self, nil)
eventListener.__metatable = mt
end
end
-- Invokes all Subscribers using the supplied arguments as the input.
-- Input arguments do not change.
---@vararg any
---@return EventListener
function eventListener:run(...)
if (not self._curDepth) then
return self
end
self._curDepth = self._curDepth + 1
local prevIndex, prevFun = self._curIndex, self._curFun
if (#self < 1) then
goto endpoint
end
for i = 1, #self do
self._curIndex = i
self._curFun = self._subList[self._curIndex]
self._curFun._depthCounter = self._curFun._depthCounter + 1
if config.APPEND_INDEX then
self._curFun:run(i, ...)
else
self._curFun:run(...)
end
if (not self._subList) then
-- In case the object was actually destroyed mid-run.
break
end
if (self._curFun == nullSubscriber) then
-- In case the object might be set to nullSubscriber.
goto continue
end
self._curFun._depthCounter = self._curFun._depthCounter - 1
::continue::
end
::endpoint::
if (not self._curDepth) then
goto final_outcome
end
self._curDepth = self._curDepth - 1
self._curIndex, self._curFun = prevIndex, prevFun
::final_outcome::
return self
end
function eventListener:getMaxDepth()
return self._maxDepth or config.MAX_RECURSION
end
function eventListener:setMaxDepth(new)
self._maxDepth = new
return self
end
function eventListener:getCurrentSubscriber()
return self._curFun
end
function eventListener:getCurrentIndex()
return self._curIndex
end
function eventListener:getCurrentDepth()
return self._curDepth
end
function eventListener:peekSubscriber(index)
index = index or 1
index = (((index > #self) or (index < 1)) and #self) or index
return self._subList[index]
end
--- Swaps the order of two Subscribers.
---@param subOne Subscriber
---@param subTwo Subscriber
---@return EventListener
function eventListener:swap(subOne, subTwo)
if (subOne._event ~= self) or (subTwo._event ~= self) then
return self
end
local i, j = 1, 1
while (i <= #self) and (self._subList[i] ~= subOne) do
i = i + 1
end
while (j <= #self) and (self._subList[j] ~= subTwo) do
j = j + 1
end
self._subList[i] = subTwo
self._subList[j] = subOne
return self
end
--- Swaps the order of two Subscribers based on the indices.
---@param i integer
---@param j integer
---@return EventListener
function eventListener:swapByIndex(i, j)
if (i == j) or (not self._subList) then
return self
end
local temp = self._subList[i]
self._subList[i] = self._subList[j]
self._subList[j] = temp
return self
end
end
if Debug and Debug.endFile then Debug.endFile() end