I know there’s a bunch of debounce modules out there, I just wanted to try making something simple for my first community resource.
Usage:
--example preventing remote event spam in some server script
local remote = game.ReplicatedStorage.RemoteEvent
remote.OnServerEvent:Connect(function(player)
local allowed = bouncer:check(player.Name)
if not allowed then
return
end
--do something here
end
-
Put module somewhere, I’m putting it in replicatedstorage so both client and server scripts can access it
-
In script:
local db = require(game.ReplicatedStorage.debounceutilities)
- Get bouncer
local bouncer = db.new()
No parameters makes a bouncer that allows 1 check per id, no resetting
optional parameters:
-
Max requests/checks per id (id can be number or string)
-
resetduration (how many seconds until list is reset)
When resetting, all ids (keys) are removed from the bouncer’s list. This prevents memory leaks from new players joining and leaving clogging up the list.
If I want to allow every player to make 5 requests/calls/remote event fires every 30 seconds, I would set up the bouncer like so
local bouncer = db.new(5,30)
Use bouncer:check(id) method to see if that particular id is allowed through. Returns true or false.
if bouncer:check(id) then
--this id hasn't used up all their checks yet
else
--they didn't pass
end
Note: If using a bouncer to guard a remote event listener on a server script, create the bouncer outside the event connection so that all the different firings of the event reference the same bouncer with the same list
local bouncer = db.new(5,30)
...OnServerEvent:Connect(function(player)
if not bouncer:check(player.UserId) then
return
end
--if they got here they passed
end)
Old Version
--debounceutilities
--CarefreeCarrot 2024
local db = {}
--STATIC MEMBERS------------
db.log = {}
db.__index = db
----------------------------
--MEMBER VARIABLES---------------------------
db.maxrequestsperid = 1
--false for no resetting
--otherwise number in seconds since last reset that must pass for it to reset again
db.resetduration = false
--auto reset thread closes itself when specific debouncer has been inactive
--for a while, starts again upon next call to debouncer
db.resetthreadactive = false
---------------------------------------------
--------------STATIC MEMBER FUNCTIONS------------------------------------
function db.new(maxreqperid, resetdur) --returns reference to new anonymous debouncer
local debouncer = setmetatable({}, db)
--MEMBER VARIABLE (placed here because lua doesn't deep copy tables when metatable templating)
--reset completely erases id references from list table to prevent memory leaks
--when players enter and leave over time
debouncer.list = {}
--if no params supplied, give default debouncer
--default debouncer doesn't reset and maxes at 1 req per id
if type(maxreqperid) == "number" then
debouncer.maxrequestsperid = maxreqperid
end
if type(resetdur) == "number" then
debouncer.resetduration = resetdur
end
return debouncer
end
--Gets reference to specific global key-binded debouncer if it already exists
--Otherwise makes new debouncer and binds to key
function db.getglobaldebouncer(key)
if db.log[key] then
return db.log[key]
end
local newdebouncer = db.new()
db.log[key] = newdebouncer
return newdebouncer
end
function db.destroyglobaldebouncer(key)
if db.log[key] then
db.log[key] = nil
end
end
-------------------------------------------------------------------------
--=================INDIVIDUAL DEBOUNCER OBJECT METHODS=============================
function db:reset()
for k,v in pairs(self.list) do
self.list[k] = nil
end
end
--If specific debouncer doesn't reset or reset thread is already active
--then this method does nothing
function db:spawnresetthread()
if (not self.resetduration) or (self.resetthreadactive) then
return
end
self.resetthreadactive = true
task.defer(function()
task.wait(self.resetduration)
self:reset()
self.resetthreadactive = false
end)
end
function db:check(id)
--if a reset occurs in the middle of a check, existing value for an id
--does not get garbage collected as long as a singular reference exists
--thus, check function creates a temporary reference
local checks = self.list[id]
local canproceed = false
if checks == nil then
self.list[id] = 0 --for edge cases
checks = 0 --make into number so incrementation doesn't error
canproceed = true
elseif checks < self.maxrequestsperid then
canproceed = true
end
checks = checks + 1
--spawn reset thread to wait a specific duration of time, then reset
--list for this specific debouncer object
--or do nothing if resetting is disabled
if self.list[id] ~= nil then --if was nil before, should have become 0
self.list[id] = checks
end
self:spawnresetthread()
return canproceed
end
--=======================================================================
return db
New Version
local dbu :DebounceUtilities = {}
dbu.__index = dbu
export type debouncer = {
check :(self, id :any) -> boolean,
}
type DebounceUtilities = {
CreateAdmitter :(AdmitUntilThisLimit :number, TimeOut :number) -> debouncer,
CreateRejecter :(RejectPastThisLimit :number, TimeOut :number) -> debouncer,
}
function dbu.CreateAdmitter(AdmitUntilThisLimit :number, TimeOut :number) :debouncer
if not AdmitUntilThisLimit then AdmitUntilThisLimit = 1 end
if not TimeOut then TimeOut = false end
local NewDebouncer :debouncer = setmetatable({}, dbu)
NewDebouncer.list = {}
NewDebouncer.threshold = AdmitUntilThisLimit
NewDebouncer.TimeOut = TimeOut
NewDebouncer.ResetThreadActive = false
NewDebouncer.invert = false
if not TimeOut then NewDebouncer.ResetThreadActive = true end --Never reset if no timeout provided
return NewDebouncer
end
function dbu.CreateRejecter(RejectPastThisLimit :number, TimeOut :number) :debouncer
local NewDebouncer = dbu.CreateAdmitter(RejectPastThisLimit, TimeOut)
NewDebouncer.invert = true
return NewDebouncer
end
local function SpawnResetThread(db :debouncer)
if db.ResetThreadActive then return end
db.ResetThreadActive = true
task.defer(function()
task.wait(db.TimeOut)
db.list = {}
db.ResetThreadActive = false
end)
end
function dbu:check(id :any) :boolean
local checks = self.list[id]
if not checks then checks = 0 end
local pass = checks < self.threshold
self.list[id] = checks + 1
SpawnResetThread(self)
if self.invert then return not pass end
return pass
end
return dbu