Sockets provide a way for different clients connected to the same server to communicate with each other. This page covers both directly using game.socket
as well as the v13 feature of interacting via registering queries.
Official Documentation
Legend
SocketInterface.dispatch // `.` indicates static method or property
Socket#on // `#` indicates instance method or property
Foundry Core uses socket.io v4 behind the scenes for its websocket connections between Server and Client. It exposes the active socket.io connection directly on game.socket
, allowing packages to emit and respond to events they create. As such, most of the socket.io documentation is directly applicable to foundry's usage.
Alternatively, one can register functions in CONFIG.queries
, providing predefined handlers for inter-client communication. Foundry includes two queries by default; dialog
and confirmTeleportToken
; the former of these is especially versatile and obviates many previous needs for sockets.
This is useful in cases where a package wants to send information or events to other connected clients directly without piggybacking on some other Document operation, such as creating a chat message or an update to an item.
For the purposes of this article, using game.socket
will be referred to as direct sockets. Registering functions in CONFIG.queries
will instead be reffered to as the query system.
Socket data must be JSON serializable; that is to say, it must consist only of values valid in a JSON file. Complex data structures such as a Data Model instance or even Sets must be transformed back to simpler forms; also keep in mind that if possible you should keep the data in the transfer as minimal as possible. If you need to reference a document, just send the UUID rather than all of its data, as the other client can just fetch from the UUID.
By default, direct sockets are emitted to every client, and it is the responsibility of the handler to perform any necessary filtering. By contrast, queries are always targeted, from one user to another, and so any mass-message system will need to call query each user separately. The upside is that each of those queries is its own promise that is fully and properly awaited for response by the queried user, unlike direct sockets which only returns a promise that confirms receipt by the server.
It's also worth noting that direct sockets do not have any built-in permission controls, while queries have the QUERY_USER
permission which is available to all players by default but can be taken away by stricter GMs.
These are common ways to interact with the Foundry socket framework.
Before a package can directly send and receive socket events, it must request a socket namespace from the server. This is done by putting "socket": true
in the manifest json.
All socket messages from a package must be emitted with the event name module.{module-name}
or system.{system-id}
(e.g. module.my-cool-module
).
Registering a query is as simple as adding a new entry to CONFIG.queries
. The key should be prefixed by your package ID, e.g. my-module.someEvent
. Your function must return JSON-serializable data, as whatever it returns will be passed back to the querying client.
async someEventHandler(queryData, {timeout}) {
// do stuff
return jsonData;
}
CONFIG.queries["my-module.someEvent"] = someEventHandler;
The first argument, queryData, is whatever JSON-serializable info you want to provide to the queried client. The second argument, queryOptions
, currently only may have timeout
information. It is destructured in every usage, so you can't use it to pass any futher arbitrary options; the correct spot for community developers to add more info is into that queryData
object.
Invoking a query involves using the User#query
method.
// Your business logic will determine how to pick the user to query
// The activeGM is a common choice for delegating permission-intensive actions
const user = game.users.activeGM;
// Your business logic will also dictate what you need to include in the payload
// to deliver to the other client
const queryData = { foo: "bar" };
// timeout is optional and is in *milliseconds*.
// Inline multiplication is an easy way to make sure your intended duration is more readable.
const queryValue = await user.query("my-module.someEvent", queryData, { timeout: 30 * 1000 });
// Now queryValue will be the return of whatever function you ran, if relevant.
Note that this socket event does not get broadcast to the emitting client.
socket.emit('module.my-module', 'foo', 'bar', 'bat');
All connected clients other than the emitting client will get an event broadcast of the same name with the arguments from the emission.
socket.on('module.my-module', (arg1, arg2, arg3) => {
console.log(arg1, arg2, arg3); // expected: "foo bar bat"
})
It can be useful to know when a socket event was processed by the server. This can be accomplished by wrapping the emit
call in a Promise which is resolved by the acknowledgement callback.
new Promise(resolve => {
// This is the acknowledgement callback
const ackCb = response => {
resolve(response);
};
socket.emit('module.my-module', arguments, ackCb);
});
The arguments of the acknowledgement callback are the same arguments that all other connected clients would get from the broadcast. Note that this is not the same as being able to fully await
any actions taken on the other clients - you would need a second socket event, sent by the other clients, to handle that.
Here are some helpful tips and tricks when working with sockets.
Since packages are only allotted one event name, using an pattern which employs an object as the socket event with a type
and payload
format can help overcome this limitation.
socket.emit('module.my-module', {
type: 'ACTION',
payload: 'Foo'
});
function handleAction(arg) {
console.log(arg); // expected 'Foo'
}
function handleSocketEvent({ type, payload }) {
switch (type) {
case "ACTION":
handleAction(payload);
break;
default:
throw new Error('unknown type');
}
}
socket.on('module.my-module', handleSocketEvent);
You can encapsulate this strategy by defining a helper class; the following example is inspired by SwadeSocketHandler:
class MyPackageSocketHandler {
constructor() {
this.identifier = "module.my-module" // whatever event name is correct for your package
this.registerSocketListeners()
}
registerSocketHandlers() {
game.socket.on(this.identifier, ({ type, payload }) => {
switch (type) {
case "ACTION":
this.#handleAction(payload);
break;
default:
throw new Error('unknown type');
}
}
}
emit(type, payload) {
return game.socket.emit(this.identifier, { type, payload })
}
#handleAction(arg) {
console.log(arg);
}
}
This helper class is then instantiated as part of the init
hook:
Hooks.once("init", () => {
const myPackage = game.modules.get("my-module") // or just game.system if you're a system
myPackage.socketHandler = new MyPackageSocketHandler()
});
// Emitting events works like this
game.modules.get("myPackage").socketHandler.emit("ACTION", "foo")
The expectation is to be able to call whatever method locally at the time of socket emission in addition to calling it in response to a broadcast.
// called by both the socket listener and emitter
function handleEvent(arg) {
console.log(arg);
}
// not triggered when this client does the emit
socket.on('module.my-module', handleEvent);
function emitEventToAll() {
const arg = 'foo';
socket.emit('module.my-module', arg);
handleEvent(arg);
}
Socket#emitWithAck: This method, despite being available as of v11, does not appear to be useful in the context of Foundry because the server acts as a middle-man for all socket events.
socketlib
has a handy abstraction for this pattern.
This is a common way to get around permission issues when player clients want to interact with Documents they do not typically have permission to modify (e.g. deducting the health of a monster after an attack).
function handleEvent(arg) {
if (game.user !== game.users.activeGM) return;
// do something
console.log(arg);
}
socket.on('module.my-module', handleEvent);
socketlib
has a handy abstraction for this pattern. This snippet is derived from its solution.
Some applications require a specific user to be targeted. This cannot be accomplished by the emit
call and instead must happen in the handler.
Emitter:
socket.emit('module.my-module', {
targetUserId: 'some-user-id',
payload: "Foo"
})
Socket Handler:
function handleEvent({ targetUserId, payload }) {
if (!!targetUserId && game.userId !== targetUserId) return;
// do something
console.log(payload);
}
socket.on('module.my-module', handleEvent);
socketlib
has a handy abstraction for this pattern.
Run through this checklist of common issues:
"socket": true
property mentioned in the Prerequisites?The following information comes directly from Atropos on the Mothership Discord's
dev-support
channel. [Link]
We use a pattern for the socket workflow that differentiates between the initial requester who receives an acknowledgement and all other connected clients who receive a broadcast.
This differentiation allows us to have handling on the initial requester side that can enclose the entire transaction into a single Promise. The basic pattern looks like this:
On the Server Side
socket.on(eventName, (request, ack) => { const response = doSomethingWithRequest(request) // Construct an object to send as a response ack(response); // This acknowledges completion of the task, sent back to the requesting client socket.broadcast.emit(eventName, response); });
For the Requesting Client
new Promise(resolve => { socket.emit(eventName, request, response => { doSomethingWithResponse(response); // This is the acknowledgement function resolve(response); // We can resolve the entire operation once acknowledged }); });
For all other Clients
socket.on(eventName, response => { doSomethingWithResponse(response); // Other clients only react to the broadcast });
Note in my example that both the requesting client and all other clients both
doSomethingWithResponse(response)
, but for the requesting client that work happens inside the acknowledgement which allows the entire transaction to be encapsulated inside a single Promise.