Sockets provide a way for different clients connected to the same server to communicate with each other.
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.
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.
socketlib
is a library module which provides useful abstractions for many common Foundry specific socket patterns.
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.
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.
These are common ways to interact with the Foundry socket framework.
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?