#Socket Architecture
1 messages · Page 1 of 1 (latest)
Share some sample code for what you're doing and I can advise.
I just tried to replicate your example at the beginning of the thread before I do something on my own.
The piece you may be missing is that your module manifest needs to declare "socket": true. Modules are not given socket namespaces by default.
no, the emit works and socket.on('module.mymod', Mymod.handler) picks it up. but I can't seem to get the acknowledge to work. so the promise of the requester never resolves.
So, for example you are saying:
game.socket.emit("module.mymod", {foo: "bar"}, response => console.log(`Received response: ${response}`));
You don't get any acknowledgment from the initial emit?
aah, I see the problem
since the time that my initial message in this channel was posted the ordering of arguments for the socket request changed. They are now: (eventName, requestData, options, acknowledgment)
so you need an options object, even if it is undefined or empty
so try:
game.socket.emit("module.mymod", {foo: "bar"}, {}, response => console.log(`Received acknowledgment`));
thanks, this triggers the callback.
but since handleCustomSocket on the server side does not pass any parameters, like the message, to the callback one cannot use it this way to do something with with the message and return a result to the requestor.
i.e. response is undefined in the above example
or am I missing something?
The server sends the exactly data object you pass to other clients. There isn’t a need for a separate data response - you already know what data was sent.
We could change the API to send you back your own data as a reminder, but you already know what was sent so you could, for example, bind that data object to your handler function
cc @spiral notch (making a thread to make this easier to follow)
@upper obsidian
Does the acknowledgement callback here get the same arguments that every other connected client's socket.on callback would get?
Or is response here a simple object with like a status: "ok" inside it?
the acknowledgement contains the same response that other clients would receive through the broadcast - all clients react to the response in the same way, but the differentiation between acknowledgement and broadcast allows for the requesting client to understand when the workflow is complete (in the enclosed promise)
awesome, thanks
So this would be an example with multiple arguments being sent over the socket emission if I understand rightly.
function ackCb(arg1, arg2, arg3) {
console.log(arg1, arg2, arg3); // expected: "foo bar baz"
};
socket.emit('module.my-module', 'foo', 'bar', 'baz', ackCb);
@upper obsidian I think I need to drop some contextual code so I can relate my use case and ensure I have it straight in my head.
The scenario here is processing damage when an actor is Concentrating on a spell within the 5e system
a GM user is nominated to process the damage and then distribute prompts to any owners of the damaged actor if they are concentrating. That leads to here:
/**
* Distribute concentration prompts to affected users
* @param {*} actorId
* @param {*} users
*/
static _distributePrompts(actorId, userIds, dc){
if (!actorId || !userIds || !userIds?.length) return;
if (userIds.includes(game.userId)) {
Concentrator._displayPrompt(actorId, game.userId, dc);
const thisUserIndex = userIds.indexOf(game.userId);
userIds.splice(thisUserIndex, 1);
}
game.socket.emit(`module.${NAME}`, {
gadget: GADGETS.concentrator.name,
action: "prompt",
targetUserIds: userIds,
actorId,
dc
});
}
previously I just had the socket emission here but the emitting client was not seeming to get the broadcast in the socket listener here:
/**
* Socket dispatcher
* @param {*} message
*/
static _onSocket(message) {
if (!message?.gadget) return;
switch (message.gadget) {
case BUTLER.GADGETS.concentrator.name:
Concentrator._onSocket(message);
break;
default:
break;
}
}
/**
* Socket message handler
* @param {*} message
*/
static _onSocket(message) {
if (!message?.targetUserIds || !message?.targetUserIds?.includes(game.userId) || !message?.action) return;
switch (message.action) {
case "prompt":
if (!message.actorId) return;
Concentrator._displayPrompt(message.actorId, game.userId, message.dc);
break;
case "cancelOtherPrompts":
if (!message.actorId) return;
Concentrator._cancelPrompt(message.actorId, message.userId);
break;
default:
break;
}
}
```\
if I wanted the emitting client to also possibly receive a prompt, what is the correct way to handle the acknowledgement and process the emission as if that client were also receiving the broadcast, without simply adding non-socket code in the method as I have done?
If I've been following, you can add the first _onSocket method (the one with the switch) as the last argument in your socket.emit and it'll 'just work'.
game.socket.emit(`module.${NAME}`, {
gadget: GADGETS.concentrator.name,
action: "prompt",
targetUserIds: userIds,
actorId,
dc
},
this._onSocket); // or BUTLER or whereever this method lives
ohh OK that does make sense. so it becomes the acknowledgement callback. I'll try that out and report back
yes, that's right - you want to provide an acknowledgment callback so you (as the initiating client) can know when to take action yourself
For bonus points, this allows you to wrap your function in a promise to make the whole thing async, for example
async function requestConcentrationCheck(userIds, actorId, dc) {
return new Promise(resolve => {
const requestData = {
gadget: GADGETS.concentrator.name,
action: "prompt",
targetUserIds: userIds,
actorId,
dc
};
game.socket.emit(`module.${NAME}`, requestData, responseData => {
performConcentrationCheck(responseData); // you could in theory be requesting yourself to perform a check
resolve();
});
});
}```
Where presumably you have some other function where clients respond to concentration check requests like:
```js
function performConcentrationCheck(checkData) {
}
thanks that will make life easier
from what small part i understand, the Hooks class sits on socket.io, so was there a thought to make game.socket part of the Hooks ?
for me it would be more intuitive to call Hooks.call("module.name", message) and then Hooks.on("module.name")
but maybe i misunderstood
hooks are not networked in any way
hooks are called and handled all locally
like DOM events
so for say a document update, the call to the db and the callout for hooks are essentially separate?
I could probably answer this myself looking at the code, I'm sure I've looked at 100s of times without really parsing it
well, they aren't separate, but the hooks are called (locally) as part of that workflow
See ClientDatabaseBackend#_createDocuments for a good example
in the _preCreateDocumentArray method we dispatch the preCreate{type} hook
this occurs only for the requesting client
and then in _handleCreateDocuments we dispatch the create{type} hook which occurs for all clients
yeah that makes sense I think (can't look at the code right now but it tracks in my head)
I don't know what I'm doing wrong but the acknowledgement callback never seems to fire
/**
* Distribute concentration prompts to affected users
* @param {*} actorId
* @param {*} users
*/
static async _distributePrompts(actorId, userIds, dc){
if (!actorId || !userIds || !userIds?.length) return;
// if (userIds.includes(game.userId)) {
// Concentrator._displayPrompt(actorId, game.userId, dc);
// const thisUserIndex = userIds.indexOf(game.userId);
// userIds.splice(thisUserIndex, 1);
// }
return new Promise((resolve) => {
const requestData = {
gadget: GADGETS.concentrator.name,
action: "prompt",
targetUserIds: userIds,
actorId,
dc
};
game.socket.emit(`module.${NAME}`, requestData, (responseData) => {
// this never appears to be reached
Signal._onSocket(responseData);
resolve();
});
});
}
@upper obsidian coming back to this, I can't figure out why my socket emit is firing but the ack cb is not, any pointers of where I should dig first?
just took a look - and I think you're right. Something odd is happening here
im not seeing the acknowledgment being reflected back for a custom socket event (which are the ones that modules have access to)
i just left my dinky workaround in place for now and i'll make it smarter after a future update thanks!