Skip to content

Callback (tm.callback) ​

Context: client and server. A request and answer call over net events, in both directions, with timeouts and cancellation. It is tm_lib's own transport: it does not patch or wrap ox_lib's lib.callback, and the two do not see each other's callbacks.

lua
-- server
tm.callback.register('myResource:getThing', function(source, id)
    return getThing(id)
end)

-- client (inside a thread)
local thing = tm.callback.await('myResource:getThing', nil, 5)

Names are global across all resources, so prefix them with your resource name. Register at file level (not inside a delayed thread) and wait for the database or other systems inside the handler instead.

Server ​

FunctionDescription
tm.callback.register(name, fn)fn(source, ...) returns any number of values. The handler runs on its own thread and may yield. Registering the same name again from the same resource replaces the handler. A name owned by another running resource raises an error.
tm.callback.unregister(name)Stops answering a name.
tm.callback.await(name, playerId, ...)Calls a callback registered on that player's client and waits. Run it inside a thread.
tm.callback.tryAwait(name, playerId, ...)Never raises. Returns true, ...values or false, reason.
tm.callback.trigger(name, playerId, cb, ...)Non-blocking, cb(...) receives the values.
tm.callback.isRegistered(name), getRegistry(), getOwner(name)This resource's registrations (name -> { resource }) and who owns a name.

If a handler errors, the error and a traceback are printed and the caller receives false.

Server await situationResult
Player not connected, or leaves mid-callreturns nothing immediately
Client handler errorsreturns false
No answer in timeraises callback 'x' timed out
No client resource registered the name (after retries)raises callback 'x' does not exist
The client resource that owns it stops mid-callraises callback 'x' cancelled: resource 'y' stopped

tryAwait reasons: 'gone', 'timeout', 'missing', 'cancelled', 'error'.

Client ​

FunctionDescription
tm.callback.register(name, fn)fn(...) answers server calls (no source). Same replace and ownership rules as the server.
tm.callback.await(name, opts, ...)Calls a server callback and waits.
tm.callback.tryAwait(name, opts, ...)Never raises. Returns true, ...values or false, reason ('timeout', 'missing', 'cancelled', 'error', 'throttled').
tm.callback.trigger(name, cb, ...)Non-blocking, cb(...) receives the values.
tm.callback(name, delay, cb, ...)Non-blocking call style with an optional throttle delay. tm.callback(name, cb, ...) also works.
tm.callback.exists(name), getOwner(name)Whether a server callback is announced, and its resource.
unregister(name), isRegistered(name), getRegistered()This resource's client registrations.

opts may be false or nil, a number (throttle delay in ms: a second call to the same name inside the delay is dropped and returns nothing), or a table:

OptionDefaultDescription
timeout60000Milliseconds to wait for the answer.
retries6Retries (100, 200, 400, 800, then 1000 ms apart) while the name is not announced yet, for example right after joining.
delaynoneThrottle, see above.
priorityAccepted for compatibility, has no effect.

await returns what the handler returned, false when the handler errored, and raises when the call times out, the callback does not exist after the retries, or its server resource stops mid-call.

Both sides ​

tm.callback.configure({ timeout = ms, retries = n }) changes the default timeout and retries for this resource (hook it to a setting if you want it editable in game). tm.callback.getSettings() returns the current values and tm.callback.pendingCount() the number of calls waiting.

How it works ​

  • Registry. Registering a name publishes its owning resource in a state bag under tm_lib:cb:<name>: GlobalState for server callbacks, the player's own state for client callbacks. The caller reads it (state bags replicate to joining clients on their own) and sends one event straight to the owner. Stopping a resource clears its keys.
  • Request ids and one sweeper. Each call gets an id, a promise (Citizen.Await) and a deadline. A single thread, running only while something is pending, times calls out. Late or duplicate answers are dropped.
  • Cancellation. A player leaving settles every call to that player. A resource stopping broadcasts tm_lib:cb:gone so calls waiting on it end at once instead of timing out.
  • Retries. If the name is not announced yet, or the owner answers invalid, the call retries with backoff before raising does not exist.

Net events ​

All names use the tm_lib:cb: prefix. <resource> is the resource name.

EventDirectionPurpose
tm_lib:cb:s:<owner>client to serverCall a server callback owned by <owner>. Arguments: id, name, callerResource, ....
tm_lib:cb:r:<caller>server to clientAnswer to a client call, delivered only to the calling resource: id, status, ....
tm_lib:cb:c:<owner>server to clientCall a client callback owned by <owner>.
tm_lib:cb:rs:<caller>client to serverAnswer to a server call, delivered only to the calling resource.
tm_lib:cb:gonebothA resource stopped. Arguments: resource.

status is ok, error (the handler failed) or invalid (no such callback). Because each call and answer targets one resource's own event, adding more resources that use callbacks does not add work per call.

Security ​

  • Callback arguments from clients are untrusted. Validate types and ranges, and never read an identity, price, permission or item name from the arguments as if it were true. source (the first handler argument on the server) is the only value the server vouches for.
  • Values a client answers with are untrusted too.
  • The server only accepts an answer from the player the call went to, so another client cannot forge a result. Answers with unknown ids or statuses, and malformed requests, are ignored.
  • There is no built-in rate limit. A handler that is expensive or changes data should limit how often one player can call it.