Initial commit

This commit is contained in:
2021-04-14 13:05:40 +02:00
commit 376004c6c2
5 changed files with 458 additions and 0 deletions

41
src/errors.js Normal file
View File

@@ -0,0 +1,41 @@
export class SocketlibError extends Error {
constructor(...args) {
super(...args);
this.name = "SocketlibError";
}
}
export class SocketlibInternalError extends SocketlibError {
constructor(...args) {
super(...args);
this.name = "SocketlibInternalError";
}
}
export class SocketlibInvalidUserError extends SocketlibError {
constructor(...args) {
super(...args);
this.name = "SocketlibInvalidUserError";
}
}
export class SocketlibNoGMConnectedError extends SocketlibError {
constructor(...args) {
super(...args);
this.name = "SocketlibNoGMConnectedError";
}
}
export class SocketlibRemoteException extends SocketlibError {
constructor(...args) {
super(...args);
this.name = "SocketlibRemoteException";
}
}
export class SocketlibUnregisteredHandlerError extends SocketlibError {
constructor(...args) {
super(...args);
this.name = "SocketlibUnregisteredHandlerError";
}
}

251
src/socketlib.js Normal file
View File

@@ -0,0 +1,251 @@
import * as errors from "./errors.js";
const RECIPIENT_TYPES = {
ONE_GM: 0,
ALL_GMS: 1,
EVERYONE: 2,
}
const MESSAGE_TYPES = {
COMMAND: 0,
REQUEST: 1,
RESPONSE: 2,
RESULT: 3,
EXCEPTION: 4,
UNREGISTERED: 5,
}
Hooks.once("init", () => {
window.socketlib = new Socketlib();
Hooks.callAll("socketlib.ready");
});
class Socketlib {
constructor() {
this.modules = new Map();
this.errors = errors;
}
registerModule(moduleName) {
const existingSocket = this.modules.get(moduleName);
if (existingSocket)
return existingSocket;
const module = game.modules.get(moduleName);
if (!module?.active) {
console.error(`socketlib | Someone tried to register module '${moduleName}', but no module with that name is active. As a result the registration request has been ignored.`);
return undefined;
}
if (!module.data.socket) {
console.error(`socketlib | Failed to register socket for module '${moduleName}'. Please set '"socket":true' in your manifset and restart foundry (you need to reload your world - simply reloading your browser won't do).`);
return undefined;
}
const newSocket = new SocketlibSocket(moduleName);
this.modules.set(moduleName, newSocket);
return newSocket;
}
}
class SocketlibSocket {
constructor(moduleName) {
this.moduleName = moduleName;
this.functions = new Map();
this.socketName = `module.${moduleName}`;
this.pendingRequests = new Map();
game.socket.on(this.socketName, this._onSocketReceived.bind(this));
}
register(name, func) {
if (!(func instanceof Function)) {
console.error(`socketlib | Cannot register non-function as socket handler for '${name}' for '${this.socketName}'.`);
return;
}
if (this.functions.has(name)) {
console.warn(`socketlib | Function '${name}' is already registered for '${this.socketName}'. Ignoring registration request.`);
return;
}
this.functions.set(name, func);
}
async executeAsGM(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
if (game.user.isGM) {
return func(...args);
}
else {
if (!game.users.find(user => user.isGM && user.active)) {
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);
}
}
async executeAsUser(handler, userId, ...args) {
const [name, func] = this._resolveFunction(handler);
const user = game.users.get(userId);
if (!user)
throw new SocketlibInvalidUserError(`No user with id '${userId}' exists.`);
if (!user.active)
throw new SocketlibInvalidUserError(`User '${user.name}' (${userId}) is not connected.`);
return this._sendRequest(name, args, [userId]);
}
async executeForAllGMs(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
this._sendCommand(name, args, RECIPIENT_TYPES.ALL_GMS);
}
async executeForEveryone(handler, ...args) {
const [name, func] = this._resolveFunction(handler);
this._sendCommand(name, args, RECIPIENT_TYPES.EVERYONE);
}
async executeForUsers(handler, recipients, ...args) {
if (!(recipients instanceof Array))
throw new TypeError("Recipients parameter must be an array of user ids.");
const [name, func] = this._resolveFunction(handler);
this._sendCommand(name, args, recipients);
}
_sendRequest(handlerName, args, recipient) {
const message = {handlerName, args, recipient};
message.id = randomID();
message.type = MESSAGE_TYPES.REQUEST;
const promise = new Promise((resolve, reject) => this.pendingRequests.set(message.id, {handlerName, resolve, reject}));
game.socket.emit(this.socketName, message);
return promise;
}
_sendCommand(handlerName, args, recipient) {
const message = {handlerName, args, recipient};
message.id = randomID();
message.type = MESSAGE_TYPES.COMMAND;
game.socket.emit(this.socketName, message);
}
_sendResult(id, result) {
const message = {id, result};
message.type = MESSAGE_TYPES.RESULT;
game.socket.emit(this.socketName, message);
}
_sendError(id, type) {
const message = {id, type};
message.userId = game.userId;
game.socket.emit(this.socketName, message);
}
_resolveFunction(func) {
if (func instanceof Function) {
const entry = Array.from(this.functions.entries()).find(([key, val]) => val === func);
if (!entry)
throw new errors.SocketlibUnregisteredHandlerError(`Function '${func.name}' has not been registered as a socket handler.`);
return [entry[0], func];
}
else {
const fn = this.functions.get(func);
if (!fn)
throw new errors.SocketlibUnregisteredHandlerError(`No socket handler with the name '${func}' has been registered.`)
return [func, fn];
}
}
_onSocketReceived(message) {
if (message.type === MESSAGE_TYPES.COMMAND || message.type === MESSAGE_TYPES.REQUEST)
this._handleRequest(message);
else
this._handleResponse(message);
}
async _handleRequest(message) {
const {handlerName, args, recipient, id, type} = message;
// Check if we're the recipient of the received message. If not, return early.
if (recipient instanceof Array) {
if (!recipient.includes(game.userId))
return;
}
else {
switch (recipient) {
case RECIPIENT_TYPES.ONE_GM:
if (!isResponsibleGM())
return;
break;
case RECIPIENT_TYPES.ALL_GMS:
if (!game.user.isGM)
return;
break;
case RECIPIENT_TYPES.EVERYONE:
break;
default:
console.error(`Unkown recipient '${recipient}' when trying to execute '${handlerName}' for '${this.socketName}'. This should never happen. If you see this message, please open an issue in the bug tracker of the socketlib repository.`);
return;
}
}
let name, func;
try {
[name, func] = this._resolveFunction(handlerName);
}
catch (e) {
if (e instanceof errors.SocketlibUnregisteredHandlerError) {
this._sendError(id, MESSAGE_TYPES.UNREGISTERED);
}
throw e;
}
if (type === MESSAGE_TYPES.COMMAND) {
func(...args);
}
else {
let result;
try {
result = await func(...args);
}
catch (e) {
console.error(`An exception occured while executing handler '${name}'.`);
this._sendError(id, MESSAGE_TYPES.EXCEPTION);
throw e;
}
this._sendResult(id, result);
}
}
_handleResponse(message) {
const {id, result, type} = message;
const request = this.pendingRequests.get(id);
if (!request)
return;
switch (type) {
case MESSAGE_TYPES.RESULT:
request.resolve(result);
break;
case MESSAGE_TYPES.EXCEPTION:
request.reject(new errors.SocketlibRemoteException(`An exception occured during remote execution of handler '${request.handlerName}'. Please see ${game.users.get(message.userId).name}'s error console for details.`));
break;
case MESSAGE_TYPES.UNREGISTERED:
request.reject(new errors.SocketlibUnregisteredHandlerError(`Executing the handler '${request.handlerName}' has been refused by ${game.users.get(message.userId).name}'s client, because this handler hasn't been registered on that client.`));
break;
default:
request.reject(new errors.SocketlibInternalError(`Unknown result type '${type}' for handler '${request.handlerName}'. This should never happen. If you see this message, please open an issue in the bug tracker of the socketlib repository.`));
break;
}
this.pendingRequests.delete(id);
}
}
function isResponsibleGM() {
const connectedGMs = game.users.filter(user => user.isGM && user.active);
if (!game.user.isGM)
return false;
return !connectedGMs.some(other => strIsSmallerThan(other.data._id, game.user.data._id));
}
function strIsSmallerThan(small, big) {
let shortestLength = Math.min(small.length, big.length);
for (let i = 0;i < shortestLength;i++) {
const smallCode = small.charCodeAt(i);
const bigCode = big.charCodeAt(i);
if (smallCode < bigCode)
return true;
if (bigCode < smallCode)
return false;
}
return small.length < shortestLength;
}