Data Request API helper #10

Merged
Oliver merged 94 commits from feat/data-requests into main 2025-11-22 02:51:15 +00:00
4 changed files with 187 additions and 177 deletions
Showing only changes of commit df0c69c731 - Show all commits

View file

@ -1,6 +1,6 @@
import { __ID__, filePath } from "../consts.mjs"; import { __ID__, filePath } from "../consts.mjs";
import { get as getQuery, requery } from "../utils/QueryManager.mjs";
import { Logger } from "../utils/Logger.mjs"; import { Logger } from "../utils/Logger.mjs";
import { QueryManager } from "../utils/QueryManager.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -62,7 +62,7 @@ export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) {
}; };
async _prepareUsers(ctx) { async _prepareUsers(ctx) {
const query = QueryManager.get(this.requestID); const query = getQuery(this.requestID);
if (!query) { return }; if (!query) { return };
const users = []; const users = [];
@ -85,7 +85,7 @@ export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) {
static async promptUser($e, element) { static async promptUser($e, element) {
const userID = element.closest(`[data-user-id]`)?.dataset.userId; const userID = element.closest(`[data-user-id]`)?.dataset.userId;
if (!userID) { return }; if (!userID) { return };
QueryManager.requery(this.requestID, [ userID ]); requery(this.requestID, [ userID ]);
}; };
/** @this {QueryStatus} */ /** @this {QueryStatus} */

View file

@ -1,6 +1,6 @@
import { QueryManager } from "../utils/QueryManager.mjs"; import { userActivity } from "../utils/QueryManager.mjs";
Hooks.on(`userConnected`, (user, connected) => { Hooks.on(`userConnected`, (user, connected) => {
if (user.isSelf) { return }; if (user.isSelf) { return };
QueryManager.userActivity(user.id, connected); userActivity(user.id, connected);
}); });

View file

@ -1,4 +1,4 @@
import { QueryManager } from "../utils/QueryManager.mjs"; import { addResponse, has as hasQuery } from "../utils/QueryManager.mjs";
export function submitRequest(payload, user) { export function submitRequest(payload, user) {
const { const {
@ -17,6 +17,6 @@ export function submitRequest(payload, user) {
return; return;
}; };
if (!QueryManager.has(id)) { return }; if (!hasQuery(id)) { return };
QueryManager.addResponse(id, user.id, answers); addResponse(id, user.id, answers);
}; };

View file

@ -20,211 +20,221 @@ import { filePath } from "../consts.mjs";
import { Logger } from "./Logger.mjs"; import { Logger } from "./Logger.mjs";
import { QueryStatus } from "../apps/QueryStatus.mjs"; import { QueryStatus } from "../apps/QueryStatus.mjs";
/** @type {Map<string, QueryData>} */
const queries = new Map();
/** @type {Map<string, Promise>} */
const promises = new Map();
async function sendBasicNotification(userID, answers) { async function sendBasicNotification(userID, answers) {
const content = await foundry.applications.handlebars.renderTemplate( const content = await foundry.applications.handlebars.renderTemplate(
filePath(`templates/query-response.hbs`), filePath(`templates/query-response.hbs`),
{ answers }, { answers },
); );
QueryManager.notify(userID, content, { includeGM: false }); await notify(userID, content, { includeGM: false });
}; };
export class QueryManager { export function has(requestID) {
/** @type {Map<string, QueryData>} */ return queries.has(requestID);
static #queries = new Map(); };
static #promises = new Map();
static has(requestID) { /** @returns {Omit<QueryData, "resolve"|"onSubmit"|"app">} */
return this.#queries.has(requestID); 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;
}; };
/** @returns {Omit<QueryData, "resolve"|"onSubmit"|"app">} */ game.socket.emit(`system.taf`, {
static get(requestID) { event: `query.prompt`,
if (!this.#queries.has(requestID)) { return null }; payload: {
const query = this.#queries.get(requestID); id: request.id,
const cloned = foundry.utils.deepClone(query); users,
request,
config,
},
});
delete cloned.onSubmit; if (promises.has(request.id)) {
delete cloned.resolve; return null;
delete cloned.app;
return foundry.utils.deepFreeze(cloned);
}; };
static async query( users ??= game.users
request, .filter(u => u.id !== game.user.id)
{ .map(u => u.id);
onSubmit = sendBasicNotification,
users = null, const promise = new Promise((resolve) => {
showStatusApp = true,
...config /** @type {UserStatus} */
} = {}, const status = {};
) { for (const user of users) {
if (!request.id) { status[user] = game.users.get(user).active ? `waiting` : `unprompted`;
ui.notifications.error(game.i18n.localize(`taf.notifs.error.missing-id`));
return;
}; };
game.socket.emit(`system.taf`, { queries.set(
event: `query.prompt`, request.id,
payload: { {
id: request.id,
users, users,
request, request,
config, config,
responses: {},
resolve,
onSubmit,
app: null,
status,
}, },
}); );
});
if (this.#promises.has(request.id)) { if (showStatusApp) {
return null; const app = new QueryStatus({ requestID: request.id });
}; app.render({ force: true });
queries.get(request.id).app = app;
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`;
};
this.#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 });
this.#queries.get(request.id).app = app;
};
return promise;
}; };
static async requery(requestID, users) { return promise;
const query = this.#queries.get(requestID); };
if (!query) { return };
game.socket.emit(`system.taf`, { export async function requery(requestID, users) {
event: `query.prompt`, const query = queries.get(requestID);
payload: { if (!query) { return };
id: requestID,
users,
request: query.request,
config: query.config,
},
});
for (const user of users) { game.socket.emit(`system.taf`, {
query.status[user] = `waiting`; 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"
Oliver marked this conversation as resolved Outdated

Add check to ensure that the query isn't undefined

Add check to ensure that the query isn't undefined
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);
} else {
query.app?.render({ parts: [ `users` ] }); query.app?.render({ parts: [ `users` ] });
}; };
};
static async addResponse(requestID, userID, answers) { export async function notify(userID, content, { includeGM = false } = {}) {
const data = this.#queries.get(requestID); game.socket.emit(`system.taf`, {
data.responses[userID] = answers; event: `query.notify`,
data.status[userID] = `finished`; payload: {
userID,
content,
includeGM,
},
});
};
await data.onSubmit?.(userID, answers); export async function cancel(requestID) {
this.maybeResolve(requestID); // prevent cancelling other people's queries
if (!queries.has(requestID)) { return };
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;
};
static async maybeResolve(requestID) { export async function userActivity(userID, connected) {
const data = this.#queries.get(requestID); for (const [id, query] of queries.entries()) {
if (query.users.includes(userID)) {
// Determine how many users are considered "finished" // Update the user's status to allow for the app to re-prompt them
let finishedUserCount = 0; if (query.status[userID] !== `finished`) {
for (const user of data.users) { if (connected) {
const hasApp = data.app != null; query.status[userID] = `unprompted`;
} else {
switch (data.status[user]) { query.status[userID] = `disconnected`;
case `finished`: {
finishedUserCount++;
break;
};
case `cancelled`:
case `disconnected`:
case `unprompted`: {
if (!hasApp) {
finishedUserCount++;
};
break;
}; };
maybeResolve(id);
}; };
};
// Ensure that we have a finished response from everyone prompted query.app?.render({ parts: [ `users` ] });
if (data.users.length === finishedUserCount) {
data.app?.close();
data.resolve(data.responses);
} else {
data.app?.render({ parts: [ `users` ] });
};
};
static async notify(userID, content, { includeGM = false } = {}) {
game.socket.emit(`system.taf`, {
event: `query.notify`,
payload: {
userID,
content,
includeGM,
},
});
}
static async cancel(requestID) {
// prevent cancelling other people's queries
if (!this.#queries.has(requestID)) { return };
game.socket.emit(`system.taf`, {
event: `query.cancel`,
payload: { id: requestID },
});
};
static async setApplication(requestID, app) {
if (!this.#queries.has(requestID)) { return };
if (!(app instanceof QueryStatus)) { return };
const query = this.#queries.get(requestID);
if (query.app) {
Logger.error(`Cannot set an application for a query that has one already`);
return;
};
query.app = app;
};
static async userActivity(userID, connected) {
for (const [id, query] of this.#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`;
};
this.maybeResolve(id);
};
query.app?.render({ parts: [ `users` ] });
};
}; };
}; };
}; };
export const QueryManager = {
has, get,
query, requery,
addResponse,
notify,
cancel,
setApplication,
userActivity,
};