socketlib
is a library module which provides useful abstractions for many common Foundry specific socket patterns.
Foundry Core uses socket.io v4 behind the scenes for its websocket connections between Server and Client. It exposes the active socketio 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.
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 update cycle event.
Before a package can 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
).
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.
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.
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);
}
default:
throw new Error('unknown type');
}
}
socket.on('module.my-module', handleSocketEvent);
socketlib
has a handy abstraction for this pattern.
The emitting client does not recieve a broadcast with the event it emits. As a result, it is recommended to use the Acknowledgement callback pattern described above to handle firing an event on the emitting client as well as all other clients.
// called by both the socket listener and emitter's acknowledgement
function handleEvent(arg) {
console.log(arg);
}
// not triggered when this client does the emit
socket.on('module.my-module', handleEvent);
socket.emit('module.my-module', 'foo', handleEvent);
If the Acknowledgement Callback method doesn't work, 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);
}
socketlib
has a handy abstraction for this pattern. This snippet is derived from its solution.
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).
The tricky part here is to ensure that only one active GM is selected arbitrarily based on logic that is consistent on all connected clients.
function handleEvent(arg) {
if (!game.user.isGM) return;
// if the logged in user is the active GM with the lowest user id
const isResponsibleGM = game.users
.filter(user => user.isGM && user.isActive)
.some(other => other.data._id < game.user.data._id);
if (!isResponsibleGM) return;
// do something
console.log(arg);
}
socket.on('module.my-module', handleEvent);
socketlib
has a handy abstraction for this pattern.
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);
Run through this checklist of common issues:
socket: true
property mentioned in the Prerequisites?