Skip to content

Commit

Permalink
feat(callback): add timeout to callback events
Browse files Browse the repository at this point in the history
Should help when people await broken or unregistered events.
Defaults to 60 seconds, adjusted with convar ox:callbackTimeout.
  • Loading branch information
thelindat committed May 3, 2024
1 parent 8bb0587 commit 2166b07
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 57 deletions.
87 changes: 46 additions & 41 deletions imports/callback/client.lua
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
local events = {}
local pendingCallbacks = {}
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, ...)
local cb = events[key]
return cb and cb(...)
local cb = pendingCallbacks[key]
pendingCallbacks[key] = nil

return cb and cb(...)
end)

---@param event string
---@param delay number | false prevent the event from being called for the given time
local function eventTimer(event, delay)
if delay and type(delay) == 'number' and delay > 0 then
local time = GetGameTimer()
if delay and type(delay) == 'number' and delay > 0 then
local time = GetGameTimer()

if (timers[event] or 0) > time then
return false
end
if (timers[event] or 0) > time then
return false
end

timers[event] = time + delay
end
timers[event] = time + delay
end

return true
return true
end

---@param _ any
Expand All @@ -30,59 +33,61 @@ end
---@param ... any
---@return ...
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
key = ('%s:%s'):format(event, math.random(0, 100000))
until not events[key]
repeat
key = ('%s:%s'):format(event, math.random(0, 100000))
until not pendingCallbacks[key]

TriggerServerEvent(cbEvent:format(event), cache.resource, key, ...)
TriggerServerEvent(cbEvent:format(event), cache.resource, key, ...)

---@type promise | false
local promise = not cb and promise.new()
---@type promise | false
local promise = not cb and promise.new()

events[key] = function(response, ...)
pendingCallbacks[key] = function(response, ...)
response = { response, ... }
events[key] = nil

if promise then
return promise:resolve(response)
end
if promise then
return promise:resolve(response)
end

if cb then
cb(table.unpack(response))
end
end
end

if promise then
return table.unpack(Citizen.Await(promise))
end
if promise then
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)

return table.unpack(Citizen.Await(promise))
end
end

---@overload fun(event: string, delay: number | false, cb: function, ...)
lib.callback = setmetatable({}, {
__call = triggerServerCallback
__call = triggerServerCallback
})

---@param event string
---@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.
function lib.callback.await(event, delay, ...)
return triggerServerCallback(nil, event, delay, false, ...)
return triggerServerCallback(nil, event, delay, false, ...)
end

local function callbackResponse(success, result, ...)
if not success 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 ''))
end
if not success 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 ''))
end

return false
end
return false
end

return result, ...
return result, ...
end

local pcall = pcall
Expand All @@ -91,9 +96,9 @@ local pcall = pcall
---@param cb function
--- Registers an event handler and callback function to respond to server requests.
function lib.callback.register(name, cb)
RegisterNetEvent(cbEvent:format(name), function(resource, key, ...)
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
end)
RegisterNetEvent(cbEvent:format(name), function(resource, key, ...)
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
end)
end

return lib.callback
16 changes: 10 additions & 6 deletions imports/callback/server.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
local events = {}
local cbEvent = ('__ox_cb_%s')
local pendingCallbacks = {}
local cbEvent = '__ox_cb_%s'
local callbackTimeout = GetConvarInt('ox:callbackTimeout', 60000)

RegisterNetEvent(cbEvent:format(cache.resource), function(key, ...)
local cb = events[key]
local cb = pendingCallbacks[key]
pendingCallbacks[key] = nil

return cb and cb(...)
end)

Expand All @@ -17,16 +20,15 @@ local function triggerClientCallback(_, event, playerId, cb, ...)

repeat
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, ...)

---@type promise | false
local promise = not cb and promise.new()

events[key] = function(response, ...)
pendingCallbacks[key] = function(response, ...)
response = { response, ... }
events[key] = nil

if promise then
return promise:resolve(response)
Expand All @@ -38,6 +40,8 @@ local function triggerClientCallback(_, event, playerId, cb, ...)
end

if promise then
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)

return table.unpack(Citizen.Await(promise))
end
end
Expand Down
15 changes: 10 additions & 5 deletions package/client/resource/callback/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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) => {
const resolve = activeEvents[key];
const resolve = pendingCallbacks[key];
delete pendingCallbacks[key];

return resolve && resolve(...args);
});

Expand Down Expand Up @@ -32,12 +35,14 @@ export function triggerServerCallback<T = unknown>(

do {
key = `${eventName}:${Math.floor(Math.random() * (100000 + 1))}`;
} while (activeEvents[key]);
} while (pendingCallbacks[key]);

emitNet(`__ox_cb_${eventName}`, cache.resource, key, ...args);

return new Promise<T>((resolve) => {
activeEvents[key] = resolve;
return new Promise<T>((resolve, reject) => {
pendingCallbacks[key] = resolve;

setTimeout(reject, callbackTimeout, `callback event '${key}' timed out`);
});
}

Expand Down
15 changes: 10 additions & 5 deletions package/server/resource/callback/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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) => {
const resolve = activeEvents[key];
const resolve = pendingCallbacks[key];
delete pendingCallbacks[key];

return resolve && resolve(...args);
});

Expand All @@ -16,12 +19,14 @@ export function triggerClientCallback<T = unknown>(

do {
key = `${eventName}:${Math.floor(Math.random() * (100000 + 1))}:${playerId}`;
} while (activeEvents[key]);
} while (pendingCallbacks[key]);

emitNet(`__ox_cb_${eventName}`, playerId, cache.resource, key, ...args);

return new Promise<T>((resolve) => {
activeEvents[key] = resolve;
return new Promise<T>((resolve, reject) => {
pendingCallbacks[key] = resolve;

setTimeout(reject, callbackTimeout, `callback event '${key}' timed out`);
});
}

Expand Down

0 comments on commit 2166b07

Please sign in to comment.