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"
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,
};