When a player disconnects, fail all unresolved requests that were sent to that player

This commit is contained in:
2021-04-27 09:41:22 +02:00
parent fb3aeaf813
commit b3b8b138bf
4 changed files with 110 additions and 3 deletions

61
src/libwrapper_shim.js Normal file
View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
'use strict';
// A shim for the libWrapper library
export let libWrapper = undefined;
Hooks.once('init', () => {
// Check if the real module is already loaded - if so, use it
if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
libWrapper = globalThis.libWrapper;
return;
}
// Fallback implementation
libWrapper = class {
static get is_fallback() { return true };
static register(module, target, fn, type="MIXED", {chain=undefined}={}) {
const is_setter = target.endsWith('#set');
target = !is_setter ? target : target.slice(0, -4);
const split = target.split('.');
const fn_name = split.pop();
const root_nm = split.splice(0,1)[0];
const _eval = eval; // The browser doesn't expose all global variables (e.g. 'Game') inside globalThis, but it does to an eval. We copy it to a variable to have it run in global scope.
const obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm));
let iObj = obj;
let descriptor = null;
while(iObj) {
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
if(descriptor) break;
iObj = Object.getPrototypeOf(iObj);
}
if(!descriptor || descriptor?.configurable === false) throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`;
let original = null;
const wrapper = (chain ?? type != 'OVERRIDE') ? function() { return fn.call(this, original.bind(this), ...arguments); } : function() { return fn.apply(this, arguments); };
if(!is_setter) {
if(descriptor.value) {
original = descriptor.value;
descriptor.value = wrapper;
}
else {
original = descriptor.get;
descriptor.get = wrapper;
}
}
else {
if(!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`;
original = descriptor.set;
descriptor.set = wrapper;
}
descriptor.configurable = true;
Object.defineProperty(obj, fn_name, descriptor);
}
}
});

View File

@@ -1,3 +1,4 @@
import {libWrapper} from "./libwrapper_shim.js";
import * as errors from "./errors.js";
const RECIPIENT_TYPES = {
@@ -17,8 +18,9 @@ const MESSAGE_TYPES = {
Hooks.once("init", () => {
window.socketlib = new Socketlib();
libWrapper.register("socketlib", "Users.prototype.constructor._handleUserActivity", handleUserActivity);
Hooks.callAll("socketlib.ready");
});
}, "WRAPPER");
class Socketlib {
constructor() {
@@ -88,7 +90,7 @@ class SocketlibSocket {
return this._executeLocal(func, ...args);
}
else {
if (!game.users.find(user => user.isGM && user.active)) {
if (!game.users.find(isActiveGM)) {
throw new errors.SocketlibNoGMConnectedError(`Could not execute handler '${name}' (${func.name}) as GM, because no GM is connected.`);
}
return this._sendRequest(name, args, RECIPIENT_TYPES.ONE_GM);
@@ -304,6 +306,48 @@ class SocketlibSocket {
function isResponsibleGM() {
if (!game.user.isGM)
return false;
const connectedGMs = game.users.filter(user => user.isGM && user.active);
const connectedGMs = game.users.filter(isActiveGM);
return !connectedGMs.some(other => other.data._id < game.user.data._id);
}
function isActiveGM(user) {
return user.active && user.isGM;
}
function handleUserActivity(wrapper, userId, activityData={}) {
const user = game.users.get(userId);
const wasActive = user.active;
const result = wrapper(userId, activityData);
// If user disconnected
if (!user.active && wasActive) {
const modules = Array.from(socketlib.modules.values());
if (socketlib.system)
modules.concat(socketlib.system);
const GMConnected = Boolean(game.users.find(isActiveGM));
// Reject all promises that are still waiting for a response from this player
for (const socket of modules) {
const failedRequests = Array.from(socket.pendingRequests.entries()).filter(([id, request]) => {
const recipient = request.recipient;
const handlerName = request.handlerName;
if (recipient === RECIPIENT_TYPES.ONE_GM) {
if (!GMConnected) {
request.reject(new errors.SocketlibNoGMConnectedError(`Could not execute handler '${handlerName}' as GM, because all GMs disconnected while the execution was being dispatched.`));
return true;
}
}
else if (recipient instanceof Array) {
if (recipient.includes(userId)) {
request.reject(new errors.SocketlibInvalidUserError(`User '${game.users.get(userId).name}' (${userId}) disconnected while handler '${handlerName}' was being dispatched.`));
return true;
}
}
return false;
});
for (const [id, request] of failedRequests) {
socket.pendingRequests.delete(id);
}
}
}
return result;
}