|
1 | 1 | ---@class CronJob |
2 | | ----@field h number |
3 | | ----@field m number |
4 | | ----@field cb function|table |
| 2 | +---@field h number Hour (0-23) |
| 3 | +---@field m number Minute (0-59) |
| 4 | +---@field cb function|table Callback function to execute |
| 5 | +---@field lastRun number|nil Timestamp of last execution (prevents duplicates) |
5 | 6 |
|
6 | 7 | ---@type CronJob[] |
7 | 8 | local cronJobs = {} |
| 9 | + |
8 | 10 | ---@type number|false |
9 | 11 | local lastTimestamp = false |
10 | 12 |
|
11 | | ----@param h number |
12 | | ----@param m number |
13 | | ----@param cb function|table |
| 13 | +---Registers a new cron job to run at specified time daily |
| 14 | +---@param h number Hour (0-23) |
| 15 | +---@param m number Minute (0-59) |
| 16 | +---@param cb function|table Callback function to execute |
14 | 17 | function RunAt(h, m, cb) |
15 | 18 | cronJobs[#cronJobs + 1] = { |
16 | 19 | h = h, |
17 | 20 | m = m, |
18 | 21 | cb = cb, |
| 22 | + lastRun = nil |
19 | 23 | } |
20 | 24 | end |
21 | 25 |
|
22 | | ----@return number |
| 26 | +---Gets current Unix timestamp |
| 27 | +---@return number Current timestamp |
23 | 28 | function GetUnixTimestamp() |
24 | 29 | return os.time() |
25 | 30 | end |
26 | 31 |
|
27 | | ----@param timestamp number |
| 32 | +---Checks and executes due cron jobs for the current timestamp |
| 33 | +---@param timestamp number Current Unix timestamp |
28 | 34 | function OnTime(timestamp) |
29 | 35 | for i = 1, #cronJobs, 1 do |
| 36 | + -- Calculate today's scheduled timestamp for this job |
30 | 37 | local scheduledTimestamp = os.time({ |
31 | 38 | hour = cronJobs[i].h, |
32 | 39 | min = cronJobs[i].m, |
33 | | - sec = 0, -- Assuming tasks run at the start of the minute |
| 40 | + sec = 0, |
34 | 41 | day = os.date("%d", timestamp), |
35 | 42 | month = os.date("%m", timestamp), |
36 | 43 | year = os.date("%Y", timestamp), |
37 | 44 | }) |
38 | 45 |
|
39 | | - if timestamp >= scheduledTimestamp and (not lastTimestamp or lastTimestamp < scheduledTimestamp) then |
40 | | - local d = os.date('*t', scheduledTimestamp).wday |
41 | | - cronJobs[i].cb(d, cronJobs[i].h, cronJobs[i].m) |
| 46 | + -- Execute if current time >= scheduled time and hasn't run today |
| 47 | + if timestamp >= scheduledTimestamp and (not cronJobs[i].lastRun or cronJobs[i].lastRun < scheduledTimestamp) then |
| 48 | + local dayOfWeek = os.date('*t', scheduledTimestamp).wday |
| 49 | + |
| 50 | + -- Execute the callback with day, hour, minute parameters |
| 51 | + cronJobs[i].cb(dayOfWeek, cronJobs[i].h, cronJobs[i].m) |
| 52 | + |
| 53 | + -- Mark this job as executed for today |
| 54 | + cronJobs[i].lastRun = scheduledTimestamp |
42 | 55 | end |
43 | 56 | end |
44 | 57 | end |
45 | 58 |
|
46 | | ----@return nil |
| 59 | +---Main tick function that checks for minute changes and processes jobs |
| 60 | +---Automatically reschedules itself for precise minute-boundary timing |
47 | 61 | function Tick() |
48 | 62 | local timestamp = GetUnixTimestamp() |
49 | 63 |
|
| 64 | + -- Only process jobs when minute changes to avoid duplicate checks |
50 | 65 | if not lastTimestamp or os.date("%M", timestamp) ~= os.date("%M", lastTimestamp) then |
51 | 66 | OnTime(timestamp) |
52 | 67 | lastTimestamp = timestamp |
53 | 68 | end |
54 | 69 |
|
55 | | - SetTimeout(60000, Tick) |
| 70 | + -- Schedule next check at the start of the next minute for precision |
| 71 | + local currentSeconds = tonumber(os.date("%S", timestamp)) |
| 72 | + local msToNextMinute = (60 - currentSeconds) * 1000 |
| 73 | + SetTimeout(msToNextMinute, Tick) |
56 | 74 | end |
57 | 75 |
|
58 | 76 | lastTimestamp = GetUnixTimestamp() |
59 | 77 | Tick() |
60 | 78 |
|
61 | | ----@param h number |
62 | | ----@param m number |
63 | | ----@param cb function|table |
| 79 | +---Event handler for external resources to register cron jobs |
| 80 | +---Usage: TriggerEvent('cron:runAt', hour, minute, callback) |
64 | 81 | AddEventHandler("cron:runAt", function(h, m, cb) |
65 | 82 | local invokingResource = GetInvokingResource() or "Unknown" |
| 83 | + |
| 84 | + -- Validate parameters with detailed error messages |
66 | 85 | local typeH = type(h) |
67 | 86 | local typeM = type(m) |
68 | 87 | local typeCb = type(cb) |
69 | 88 |
|
70 | | - assert(typeH == "number", ("Expected number for h, got %s. Invoking Resource: '%s'"):format(typeH, invokingResource)) |
71 | | - assert(typeM == "number", ("Expected number for m, got %s. Invoking Resource: '%s'"):format(typeM, invokingResource)) |
72 | | - assert(typeCb == "function" or (typeCb == "table" and type(getmetatable(cb)?.__call) == "function"), ("Expected function for cb, got %s. Invoking Resource: '%s'"):format(typeCb, invokingResource)) |
| 89 | + assert(typeH == "number", ("Expected number for hour, got %s. Invoking Resource: '%s'"):format(typeH, invokingResource)) |
| 90 | + assert(typeM == "number", ("Expected number for minute, got %s. Invoking Resource: '%s'"):format(typeM, invokingResource)) |
| 91 | + assert(typeCb == "function" or (typeCb == "table" and type(getmetatable(cb)?.__call) == "function"), ("Expected function for callback, got %s. Invoking Resource: '%s'"):format(typeCb, invokingResource)) |
| 92 | + |
| 93 | + -- Validate time ranges |
| 94 | + assert(h >= 0 and h <= 23, ("Hour must be between 0-23, got %d. Invoking Resource: '%s'"):format(h, invokingResource)) |
| 95 | + assert(m >= 0 and m <= 59, ("Minute must be between 0-59, got %d. Invoking Resource: '%s'"):format(m, invokingResource)) |
73 | 96 |
|
74 | 97 | RunAt(h, m, cb) |
75 | 98 | end) |
0 commit comments