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.
-- 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
| Function | Description |
|---|---|
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 situation | Result |
|---|---|
| Player not connected, or leaves mid-call | returns nothing immediately |
| Client handler errors | returns false |
| No answer in time | raises 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-call | raises callback 'x' cancelled: resource 'y' stopped |
tryAwait reasons: 'gone', 'timeout', 'missing', 'cancelled', 'error'.
Client
| Function | Description |
|---|---|
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:
| Option | Default | Description |
|---|---|---|
timeout | 60000 | Milliseconds to wait for the answer. |
retries | 6 | Retries (100, 200, 400, 800, then 1000 ms apart) while the name is not announced yet, for example right after joining. |
delay | none | Throttle, see above. |
priority | Accepted 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>:GlobalStatefor 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:goneso 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 raisingdoes not exist.
Net events
All names use the tm_lib:cb: prefix. <resource> is the resource name.
| Event | Direction | Purpose |
|---|---|---|
tm_lib:cb:s:<owner> | client to server | Call a server callback owned by <owner>. Arguments: id, name, callerResource, .... |
tm_lib:cb:r:<caller> | server to client | Answer to a client call, delivered only to the calling resource: id, status, .... |
tm_lib:cb:c:<owner> | server to client | Call a client callback owned by <owner>. |
tm_lib:cb:rs:<caller> | client to server | Answer to a server call, delivered only to the calling resource. |
tm_lib:cb:gone | both | A 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.