/** * An object containing information about the current status for all users involved * with the data request. * @typedef {Record} UserStatus */ /** * @typedef QueryData * @property {string[]} users * @property {Function} resolve * @property {Record} responses * @property {(() => Promise)|null} onSubmit * @property {QueryStatus|null} app * @property {UserStatus} status * @property {object} request The data used to form the initial request * @property {object} config The data used to create the initial config */ import { filePath } from "../consts.mjs"; import { Logger } from "./Logger.mjs"; import { QueryStatus } from "../apps/QueryStatus.mjs"; /** @type {Map} */ const queries = new Map(); /** @type {Map} */ const promises = new Map(); async function sendBasicNotification(userID, answers) { const content = await foundry.applications.handlebars.renderTemplate( filePath(`templates/query-response.hbs`), { answers }, ); await notify(userID, content, { includeGM: false }); }; export function has(requestID) { return queries.has(requestID); }; /** @returns {Omit} */ export function get(requestID) { if (!queries.has(requestID)) { return null }; const query = queries.get(requestID); const cloned = foundry.utils.deepClone(query); delete cloned.onSubmit; delete cloned.resolve; delete cloned.app; return foundry.utils.deepFreeze(cloned); }; export async function query( request, { onSubmit = sendBasicNotification, users = null, showStatusApp = true, ...config } = {}, ) { if (!request.id) { ui.notifications.error(game.i18n.localize(`taf.notifs.error.missing-id`)); return; }; game.socket.emit(`system.taf`, { event: `query.prompt`, payload: { id: request.id, users, request, config, }, }); if (promises.has(request.id)) { return null; }; users ??= game.users .filter(u => u.id !== game.user.id) .map(u => u.id); const promise = new Promise((resolve) => { /** @type {UserStatus} */ const status = {}; for (const user of users) { status[user] = game.users.get(user).active ? `waiting` : `unprompted`; }; queries.set( request.id, { users, request, config, responses: {}, resolve, onSubmit, app: null, status, }, ); }); if (showStatusApp) { const app = new QueryStatus({ requestID: request.id }); app.render({ force: true }); queries.get(request.id).app = app; }; return promise; }; export async function requery(requestID, users) { const query = queries.get(requestID); if (!query) { return }; game.socket.emit(`system.taf`, { event: `query.prompt`, payload: { id: requestID, users, request: query.request, config: query.config, }, }); for (const user of users) { query.status[user] = `waiting`; }; query.app?.render({ parts: [ `users` ] }); }; export async function addResponse(requestID, userID, answers) { const data = queries.get(requestID); data.responses[userID] = answers; data.status[userID] = `finished`; await data.onSubmit?.(userID, answers); await maybeResolve(requestID); }; async function maybeResolve(requestID) { const query = queries.get(requestID); // Determine how many users are considered "finished" let finishedUserCount = 0; for (const user of query.users) { const hasApp = query.app != null; switch (query.status[user]) { case `finished`: { finishedUserCount++; break; }; case `cancelled`: case `disconnected`: case `unprompted`: { if (!hasApp) { finishedUserCount++; }; break; }; }; }; // Ensure that we have a finished response from everyone prompted if (query.users.length === finishedUserCount) { query.app?.close(); query.resolve(query.responses); queries.delete(requestID); promises.delete(requestID); } else { query.app?.render({ parts: [ `users` ] }); }; }; export async function notify(userID, content, { includeGM = false } = {}) { game.socket.emit(`system.taf`, { event: `query.notify`, payload: { userID, content, includeGM, }, }); }; export async function finish(requestID) { // prevent finishing other people's queries if (!queries.has(requestID)) { return }; const query = queries.get(requestID); query.resolve(query.responses); queries.delete(requestID); promises.delete(requestID); game.socket.emit(`system.taf`, { event: `query.cancel`, payload: { id: requestID }, }); }; export async function cancel(requestID) { // prevent cancelling other people's queries if (!queries.has(requestID)) { return }; const query = queries.get(requestID); query.resolve(null); queries.delete(requestID); promises.delete(requestID); game.socket.emit(`system.taf`, { event: `query.cancel`, payload: { id: requestID }, }); }; export async function setApplication(requestID, app) { if (!queries.has(requestID)) { return }; if (!(app instanceof QueryStatus)) { return }; const query = queries.get(requestID); if (query.app) { Logger.error(`Cannot set an application for a query that has one already`); return; }; query.app = app; }; export async function userActivity(userID, connected) { for (const [id, query] of queries.entries()) { if (query.users.includes(userID)) { // Update the user's status to allow for the app to re-prompt them if (query.status[userID] !== `finished`) { if (connected) { query.status[userID] = `unprompted`; } else { query.status[userID] = `disconnected`; }; maybeResolve(id); }; query.app?.render({ parts: [ `users` ] }); }; }; }; export const QueryManager = { has, get, query, requery, addResponse, notify, finish, cancel, setApplication, userActivity, };