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 { get as getQuery, requery } from "../utils/QueryManager.mjs";
import { Logger } from "../utils/Logger.mjs";
import { QueryManager } from "../utils/QueryManager.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -62,7 +62,7 @@ export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) {
};
async _prepareUsers(ctx) {
const query = QueryManager.get(this.requestID);
const query = getQuery(this.requestID);
if (!query) { return };
const users = [];
@ -85,7 +85,7 @@ export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) {
static async promptUser($e, element) {
const userID = element.closest(`[data-user-id]`)?.dataset.userId;
if (!userID) { return };
QueryManager.requery(this.requestID, [ userID ]);
requery(this.requestID, [ userID ]);
};
/** @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) => {
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) {
const {
@ -17,6 +17,6 @@ export function submitRequest(payload, user) {
return;
};
if (!QueryManager.has(id)) { return };
QueryManager.addResponse(id, user.id, answers);
if (!hasQuery(id)) { return };
addResponse(id, user.id, answers);
};

View file

@ -20,211 +20,221 @@ import { filePath } from "../consts.mjs";
import { Logger } from "./Logger.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) {
const content = await foundry.applications.handlebars.renderTemplate(
filePath(`templates/query-response.hbs`),
{ answers },
);
QueryManager.notify(userID, content, { includeGM: false });
await notify(userID, content, { includeGM: false });
};
export class QueryManager {
/** @type {Map<string, QueryData>} */
static #queries = new Map();
static #promises = new Map();
export function has(requestID) {
return queries.has(requestID);
};
static has(requestID) {
return this.#queries.has(requestID);
/** @returns {Omit<QueryData, "resolve"|"onSubmit"|"app">} */
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">} */
static get(requestID) {
if (!this.#queries.has(requestID)) { return null };
const query = this.#queries.get(requestID);
const cloned = foundry.utils.deepClone(query);
game.socket.emit(`system.taf`, {
event: `query.prompt`,
payload: {
id: request.id,
users,
request,
config,
},
});
delete cloned.onSubmit;
delete cloned.resolve;
delete cloned.app;
return foundry.utils.deepFreeze(cloned);
if (promises.has(request.id)) {
return null;
};
static async query(
request,
{
onSubmit = sendBasicNotification,
users = null,
showStatusApp = true,
...config
} = {},
) {
if (!request.id) {
ui.notifications.error(game.i18n.localize(`taf.notifs.error.missing-id`));
return;
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`;
};
game.socket.emit(`system.taf`, {
event: `query.prompt`,
payload: {
id: request.id,
queries.set(
request.id,
{
users,
request,
config,
responses: {},
resolve,
onSubmit,
app: null,
status,
},
});
);
});
if (this.#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`;
};
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;
if (showStatusApp) {
const app = new QueryStatus({ requestID: request.id });
app.render({ force: true });
queries.get(request.id).app = app;
};
static async requery(requestID, users) {
const query = this.#queries.get(requestID);
if (!query) { return };
return promise;
};
game.socket.emit(`system.taf`, {
event: `query.prompt`,
payload: {
id: requestID,
users,
request: query.request,
config: query.config,
},
});
export async function requery(requestID, users) {
const query = queries.get(requestID);
if (!query) { return };
for (const user of users) {
query.status[user] = `waiting`;
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"
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` ] });
};
};
static async addResponse(requestID, userID, answers) {
const data = this.#queries.get(requestID);
data.responses[userID] = answers;
data.status[userID] = `finished`;
export async function notify(userID, content, { includeGM = false } = {}) {
game.socket.emit(`system.taf`, {
event: `query.notify`,
payload: {
userID,
content,
includeGM,
},
});
};
await data.onSubmit?.(userID, answers);
this.maybeResolve(requestID);
export async function cancel(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) {
const data = this.#queries.get(requestID);
export async function userActivity(userID, connected) {
for (const [id, query] of queries.entries()) {
if (query.users.includes(userID)) {
// Determine how many users are considered "finished"
let finishedUserCount = 0;
for (const user of data.users) {
const hasApp = data.app != null;
switch (data.status[user]) {
case `finished`: {
finishedUserCount++;
break;
};
case `cancelled`:
case `disconnected`:
case `unprompted`: {
if (!hasApp) {
finishedUserCount++;
};
break;
// 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);
};
};
// Ensure that we have a finished response from everyone prompted
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` ] });
};
query.app?.render({ parts: [ `users` ] });
};
};
};
export const QueryManager = {
has, get,
query, requery,
addResponse,
notify,
cancel,
setApplication,
userActivity,
};