Data Request API helper #10
4 changed files with 187 additions and 177 deletions
|
|
@ -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} */
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue