Timer-like module

does this module look okay?

--[[
	Service made by @natxnek [discord: @natxnek]
	Original: http://github.com/natxnekk/TimerService/
	
	Memory usage:
		- Variable: ~600 Bytes
		- Worker:   ~600 Bytes
	
	Feel free to fork the service and update it for your purposes
	
	License: Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
	Copyright (c) 2025 @natxnek
	
	For the full license, visit: https://creativecommons.org/licenses/by-nc/4.0/
]]

local class = {} :: class
class.__index = class

--Types
type class = {
	__index: class,
	new: ( length: number, endedCallback: () -> () ) -> object,

	start: (self: object) -> (),
	pause: (self: object) -> (),
	reset: (self: object, pause: boolean) -> (),
	destroy: (self: object) -> (),
	getIsRunning: (self: object) -> boolean,
	getTimeLeft: (self: object) -> number,
}

export type object = typeof(setmetatable({} :: {

	_length: number,
	_timeLeft: number,
	_isRunning: boolean,

	_endedCallback: () -> (),

	_thread: thread?

}, {} :: class))

--Functions

--[[
	@param  length: number              --The length of the timer
	@param  endedCallback: () -> ()     --The function that is executed when a timer is finished
	@return object                      --Returns a new Timer object

	Creates a new Timer object (metatable)
]]
function class.new(length, endedCallback)
	return setmetatable({
		_length = length,
		_timeLeft = 0,
		_endedCallback = endedCallback or function() end,
		_isRunning = false,

		_thread = nil
	}, class :: class) :: object
end

--[[
	Resumes a timer when it's initialized or 
	creates a new one when a timer doesn't exist yet
]]
function class:start()
	if self._thread and coroutine.status(self._thread) == "suspended" then
		self._isRunning = true
		coroutine.resume(self._thread)
		return
	end

	self._timeLeft = self._length
	self._isRunning = true

	self._thread = task.spawn(function()
		while self._timeLeft > 0 do
			if not self._isRunning then
				coroutine.yield(self._thread)
				continue
			end

			self._timeLeft -= task.wait()

			if self._timeLeft <= 0 then
				self._endedCallback()

				self._isRunning = false
				self._timeLeft = 0

				break
			end
		end
	end)
end

--[[
	Pauses the timer for later resumption
]]
function class:pause()
	self._isRunning = false
end

--[[
	@param  pause: boolean              --Should the timer be paused after resetting
	
	Resets the timer's time left to timer's default length
]]
function class:reset(pause)
	if pause then
		self._isRunning = false
	end

	self._timeLeft = self._length
end

--[[
	Cleans the memory from the timer
]]
function class:destroy()
	if self._thread then
		coroutine.close(self._thread)
		self._thread = nil
	end

	table.clear(self)
end
--[[
	@return boolean                   --Returns if the timer is currently running       
]]
function class:getIsRunning()
	return self._isRunning
end

--[[
	@return number                   --Returns time left for the callback to be executed
]]
function class:getTimeLeft()
	return self._timeLeft
end

return class
1 Like

Looks good! Well done.

Your code is readable! (somehow a lot of people on the forum don’t know how to do this)

3 Likes

This feels like you wrapped a task.wait() in a metatable and called it a day :skull:

You’re over-abstracting your own code for no gain - removing direct access to state and adding overhead with closures for no reason

If I were to write this, I’d just return a thread with a watchdog and be done with it:

--!strict
--!optimize 2
local function DefaultWatchDog(t:thread):()
	if coroutine.status(t)=="dead" then return end
	task.cancel(t)
end

return function(func:(...any)->(...any),WatchDog:number,CustomWatchDog:((t:thread)->())?):thread
	local thread = task.spawn(func)
	task.delay(WatchDog,CustomWatchDog or DefaultWatchDog,thread)
	return thread
end
2 Likes

It’s nice and short which I like. Overall quite good. There are two things that are more so ideas than things you should implement, for the sake of exploring different ways of doing things

The first one is, OOP. OOP has it’s place (and I would say, in cases where you have something that is an “object”, which your timer works well as), but I’ve ended using an OOP approach that doesn’t use metatables. I think people assume that OOP in luau has to use metatables, but nah

function class.new(length, endedCallback)
	
	local timer = {}

	local lenght = lenght -- Redundant, but whatever
	local timeLeft = 0
	local endedCallback = endedCallback or function() end
	local isRunning = false
	
	local thread = nil

	function timer:start()
		if thread and coroutine.status(thread) == "suspended" then
			isRunning = true
			coroutine.resume(thread)
			return
		end

		timeLeft = length
		isRunning = true

		thread = task.spawn(function()
			while timeLeft > 0 do
				if not isRunning then
					coroutine.yield(thread)
					continue
				end

				timeLeft -= task.wait()

				if timeLeft <= 0 then
					endedCallback()

					isRunning = false
					timeLeft = 0

					break
				end
			end
		end)
	end

	-- [...]

	return timer
end

I like this approach because it’s simpler. It’s also clean. In terms of performance, it might be a bit faster (because no metatable), but take up more memory, because each timer has it’s own function, instead of reusing the same functions (I think luau optimizes this, if the function has no upvalues, but in this case, the function has upvalues (lenght, timeLeft, etc)). Performance difference probably fairly insignificant

Second point is that, if you want to optimize this further, you can try to get rid of the loop that runs every frame when a timer is running. Though, doing so will complexify the code a lot, for a very marginal gain (the code the loop is running is very cheap, so it’ll take an absolutely ridiculous amount of timers for it to lag a game).
Achieving this would probably be done by putting the threads in a sorted list, and having a global loop check every frame, if the threads at the beginning of the list should be resumed (once it hits a thread that’s still waiting, it can ignore the later threads in the list). Alternatively, you can use the task library to make the thread yield (with task.wait or task.delay) for X amount of time, and using task.cancel() when pausing, to be able to modify that yield time when starting it again

1 Like