feat(callback): add timeout to callback events

Should help when people await broken or unregistered events.
Defaults to 60 seconds, adjusted with convar ox:callbackTimeout.
This commit is contained in:
Linden
2024-05-03 15:49:33 +10:00
parent 8bb0587b39
commit 2166b076da
4 changed files with 76 additions and 57 deletions

View File

@@ -1,26 +1,29 @@
local events = {} local pendingCallbacks = {}
local timers = {} local timers = {}
local cbEvent = ('__ox_cb_%s') local cbEvent = '__ox_cb_%s'
local callbackTimeout = GetConvarInt('ox:callbackTimeout', 60000)
RegisterNetEvent(cbEvent:format(cache.resource), function(key, ...) RegisterNetEvent(cbEvent:format(cache.resource), function(key, ...)
local cb = events[key] local cb = pendingCallbacks[key]
return cb and cb(...) pendingCallbacks[key] = nil
return cb and cb(...)
end) end)
---@param event string ---@param event string
---@param delay number | false prevent the event from being called for the given time ---@param delay number | false prevent the event from being called for the given time
local function eventTimer(event, delay) local function eventTimer(event, delay)
if delay and type(delay) == 'number' and delay > 0 then if delay and type(delay) == 'number' and delay > 0 then
local time = GetGameTimer() local time = GetGameTimer()
if (timers[event] or 0) > time then if (timers[event] or 0) > time then
return false return false
end end
timers[event] = time + delay timers[event] = time + delay
end end
return true return true
end end
---@param _ any ---@param _ any
@@ -30,59 +33,61 @@ end
---@param ... any ---@param ... any
---@return ... ---@return ...
local function triggerServerCallback(_, event, delay, cb, ...) local function triggerServerCallback(_, event, delay, cb, ...)
if not eventTimer(event, delay) then return end if not eventTimer(event, delay) then return end
local key local key
repeat repeat
key = ('%s:%s'):format(event, math.random(0, 100000)) key = ('%s:%s'):format(event, math.random(0, 100000))
until not events[key] until not pendingCallbacks[key]
TriggerServerEvent(cbEvent:format(event), cache.resource, key, ...) TriggerServerEvent(cbEvent:format(event), cache.resource, key, ...)
---@type promise | false ---@type promise | false
local promise = not cb and promise.new() local promise = not cb and promise.new()
events[key] = function(response, ...) pendingCallbacks[key] = function(response, ...)
response = { response, ... } response = { response, ... }
events[key] = nil
if promise then if promise then
return promise:resolve(response) return promise:resolve(response)
end end
if cb then if cb then
cb(table.unpack(response)) cb(table.unpack(response))
end end
end end
if promise then if promise then
return table.unpack(Citizen.Await(promise)) SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
end
return table.unpack(Citizen.Await(promise))
end
end end
---@overload fun(event: string, delay: number | false, cb: function, ...) ---@overload fun(event: string, delay: number | false, cb: function, ...)
lib.callback = setmetatable({}, { lib.callback = setmetatable({}, {
__call = triggerServerCallback __call = triggerServerCallback
}) })
---@param event string ---@param event string
---@param delay? number | false prevent the event from being called for the given time. ---@param delay? number | false prevent the event from being called for the given time.
---Sends an event to the server and halts the current thread until a response is returned. ---Sends an event to the server and halts the current thread until a response is returned.
function lib.callback.await(event, delay, ...) function lib.callback.await(event, delay, ...)
return triggerServerCallback(nil, event, delay, false, ...) return triggerServerCallback(nil, event, delay, false, ...)
end end
local function callbackResponse(success, result, ...) local function callbackResponse(success, result, ...)
if not success then if not success then
if result then if result then
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result , Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or '')) return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result,
end Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
end
return false return false
end end
return result, ... return result, ...
end end
local pcall = pcall local pcall = pcall
@@ -91,9 +96,9 @@ local pcall = pcall
---@param cb function ---@param cb function
--- Registers an event handler and callback function to respond to server requests. --- Registers an event handler and callback function to respond to server requests.
function lib.callback.register(name, cb) function lib.callback.register(name, cb)
RegisterNetEvent(cbEvent:format(name), function(resource, key, ...) RegisterNetEvent(cbEvent:format(name), function(resource, key, ...)
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...))) TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
end) end)
end end
return lib.callback return lib.callback

View File

@@ -1,8 +1,11 @@
local events = {} local pendingCallbacks = {}
local cbEvent = ('__ox_cb_%s') local cbEvent = '__ox_cb_%s'
local callbackTimeout = GetConvarInt('ox:callbackTimeout', 60000)
RegisterNetEvent(cbEvent:format(cache.resource), function(key, ...) RegisterNetEvent(cbEvent:format(cache.resource), function(key, ...)
local cb = events[key] local cb = pendingCallbacks[key]
pendingCallbacks[key] = nil
return cb and cb(...) return cb and cb(...)
end) end)
@@ -17,16 +20,15 @@ local function triggerClientCallback(_, event, playerId, cb, ...)
repeat repeat
key = ('%s:%s:%s'):format(event, math.random(0, 100000), playerId) key = ('%s:%s:%s'):format(event, math.random(0, 100000), playerId)
until not events[key] until not pendingCallbacks[key]
TriggerClientEvent(cbEvent:format(event), playerId, cache.resource, key, ...) TriggerClientEvent(cbEvent:format(event), playerId, cache.resource, key, ...)
---@type promise | false ---@type promise | false
local promise = not cb and promise.new() local promise = not cb and promise.new()
events[key] = function(response, ...) pendingCallbacks[key] = function(response, ...)
response = { response, ... } response = { response, ... }
events[key] = nil
if promise then if promise then
return promise:resolve(response) return promise:resolve(response)
@@ -38,6 +40,8 @@ local function triggerClientCallback(_, event, playerId, cb, ...)
end end
if promise then if promise then
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
return table.unpack(Citizen.Await(promise)) return table.unpack(Citizen.Await(promise))
end end
end end

View File

@@ -1,9 +1,12 @@
import { cache } from '../cache'; import { cache } from '../cache';
const activeEvents: Record<string, (...args: any[]) => void> = {}; const pendingCallbacks: Record<string, (...args: any[]) => void> = {};
const callbackTimeout = GetConvarInt('ox:callbackTimeout', 60000);
onNet(`__ox_cb_${cache.resource}`, (key: string, ...args: any) => { onNet(`__ox_cb_${cache.resource}`, (key: string, ...args: any) => {
const resolve = activeEvents[key]; const resolve = pendingCallbacks[key];
delete pendingCallbacks[key];
return resolve && resolve(...args); return resolve && resolve(...args);
}); });
@@ -32,12 +35,14 @@ export function triggerServerCallback<T = unknown>(
do { do {
key = `${eventName}:${Math.floor(Math.random() * (100000 + 1))}`; key = `${eventName}:${Math.floor(Math.random() * (100000 + 1))}`;
} while (activeEvents[key]); } while (pendingCallbacks[key]);
emitNet(`__ox_cb_${eventName}`, cache.resource, key, ...args); emitNet(`__ox_cb_${eventName}`, cache.resource, key, ...args);
return new Promise<T>((resolve) => { return new Promise<T>((resolve, reject) => {
activeEvents[key] = resolve; pendingCallbacks[key] = resolve;
setTimeout(reject, callbackTimeout, `callback event '${key}' timed out`);
}); });
} }

View File

@@ -1,9 +1,12 @@
import { cache } from '../cache'; import { cache } from '../cache';
const activeEvents: Record<string, (...args: any[]) => void> = {}; const pendingCallbacks: Record<string, (...args: any[]) => void> = {};
const callbackTimeout = GetConvarInt('ox:callbackTimeout', 60000);
onNet(`__ox_cb_${cache.resource}`, (key: string, ...args: any) => { onNet(`__ox_cb_${cache.resource}`, (key: string, ...args: any) => {
const resolve = activeEvents[key]; const resolve = pendingCallbacks[key];
delete pendingCallbacks[key];
return resolve && resolve(...args); return resolve && resolve(...args);
}); });
@@ -16,12 +19,14 @@ export function triggerClientCallback<T = unknown>(
do { do {
key = `${eventName}:${Math.floor(Math.random() * (100000 + 1))}:${playerId}`; key = `${eventName}:${Math.floor(Math.random() * (100000 + 1))}:${playerId}`;
} while (activeEvents[key]); } while (pendingCallbacks[key]);
emitNet(`__ox_cb_${eventName}`, playerId, cache.resource, key, ...args); emitNet(`__ox_cb_${eventName}`, playerId, cache.resource, key, ...args);
return new Promise<T>((resolve) => { return new Promise<T>((resolve, reject) => {
activeEvents[key] = resolve; pendingCallbacks[key] = resolve;
setTimeout(reject, callbackTimeout, `callback event '${key}' timed out`);
}); });
} }