Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/utils/display.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,15 @@ class DisplayUpdateManager {
return DisplayUpdateManager.instance;
}

public addUpdate(update: DisplayUpdate): void {
public addUpdate(update: DisplayUpdate): Promise<void> {
const { queueId } = update;
if (this.updatedQueueIds.has(queueId)) {
// Coalesced into the next setInterval drain — caller cannot await the eventual update.
this.pendingQueueIds.set(queueId, update);
return Promise.resolve();
}
else {
DisplayUtils.updateDisplays(update);
return DisplayUtils.updateDisplays(update);
}
}

Expand Down Expand Up @@ -140,14 +142,14 @@ export namespace DisplayUtils {
return { deletedDisplays, updatedQueueIds };
}

export function requestDisplayUpdate(displayUpdate: DisplayUpdate): void {
updateManager.addUpdate(displayUpdate);
export function requestDisplayUpdate(displayUpdate: DisplayUpdate): Promise<void> {
return updateManager.addUpdate(displayUpdate);
}

export function requestDisplaysUpdate(displaysUpdate: DisplaysUpdate): void {
uniq(displaysUpdate.queueIds).forEach(queueId =>
export async function requestDisplaysUpdate(displaysUpdate: DisplaysUpdate): Promise<void> {
await Promise.all(uniq(displaysUpdate.queueIds).map(queueId =>
requestDisplayUpdate({ ...displaysUpdate, queueId })
);
));
}

export async function updateDisplays(displayUpdate: DisplayUpdate): Promise<void> {
Expand Down
143 changes: 76 additions & 67 deletions src/utils/event.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,18 @@ export namespace EventUtils {
const roomCount = Number(event.roomCount);
const roles: EventQueueRole[] = [EventQueueRole.Room, EventQueueRole.Sub];

// Lock all existing event queues up-front so the sync runs from a known-locked baseline.
// Newly-created queues in Step A pick up correct lock state via insertEventQueueRowWithoutDisplay,
// and Step E re-evaluates every queue to unlock those whose pre-start window contains now.
// Lock every existing event queue up-front so the sync runs from a known-locked baseline.
// Step A's new queues lock themselves via insertEventQueueRowWithoutDisplay; Step E unlocks
// any whose pre-start window contains now. Direct store.updateQueue (not QueueUtils.updateQueues)
// — its requestDisplaysUpdate is fire-and-forget and would race Step C. No display refresh
// needed: Step C reposts every display.
{
const existingEqs = Queries.selectManyEventQueues({ guildId: store.guild.id, eventId: event.id });
const existingQueues = compact(existingEqs.map(eq =>
Queries.selectQueue({ guildId: store.guild.id, id: eq.queueId })
));
if (existingQueues.length > 0) {
await QueueUtils.updateQueues(store, existingQueues, { lockToggle: true } as Partial<DbQueue>);
for (const q of existingQueues) {
store.updateQueue({ id: q.id, lockToggle: true });
}
}

Expand Down Expand Up @@ -336,67 +338,14 @@ export namespace EventUtils {
}

// Step C — re-show every queue display in queue-index order in the event's display channels.
// Insert the row directly + `await updateDisplays` so per-queue posts are sequential
// (DisplayUtils.insertDisplays also fires a non-awaited update, which would race with the explicit await below).
for (const role of roles) {
const displayChannelId = role === EventQueueRole.Room
? event.roomQueuesChannelId
: event.subQueuesChannelId;
const orderedEqs = Queries.selectManyEventQueues({ guildId: store.guild.id, eventId: event.id })
.filter(eq => eq.queueRole === role)
.sort((a, b) => Number(a.queueIndex) - Number(b.queueIndex));

for (const eq of orderedEqs) {
const queue = Queries.selectQueue({ guildId: store.guild.id, id: eq.queueId });
if (!queue) continue;

const existingDisplays = [...store.dbDisplays().filter(d => d.queueId === queue.id).values()];
for (const display of existingDisplays) {
if (display.lastMessageId) {
const channel = await store.jsChannel(display.displayChannelId) as GuildTextBasedChannel | undefined;
if (channel) {
const message = await channel.messages.fetch(display.lastMessageId).catch(e => {
console.error(`EventUtils.syncEventQueues: failed to fetch stale display message ${display.lastMessageId} in channel ${display.displayChannelId}:`, e);
return null;
});
if (message) {
await message.delete().catch(e => {
console.error(`EventUtils.syncEventQueues: failed to delete stale display message ${display.lastMessageId} in channel ${display.displayChannelId}:`, e);
return null;
});
}
}
}
store.deleteDisplay({ id: display.id });
}

const newDisplay = store.insertDisplay({
guildId: store.guild.id,
queueId: queue.id,
displayChannelId,
});
if (!newDisplay) continue;

await DisplayUtils.updateDisplays({
store,
queueId: queue.id,
opts: {
displayIds: [newDisplay.id],
updateTypeOverride: DisplayUpdateType.Replace,
},
});

reshownCount++;
}
}
reshownCount = await reshowEventQueueDisplays(store, event);

// Step D — reconcile channels + auto-created room roles
if (event.roomCategoryId) {
await EventChannelUtils.reconcileRoomChannels(store, event);
}

// Step E — unlock any event queues whose role-appropriate pre-start window currently contains now.
// All other queues stay locked from the up-front lock above.
// Step E — unlock any event queues whose role-appropriate pre-start window contains now.
{
const allEqs = Queries.selectManyEventQueues({ guildId: store.guild.id, eventId: event.id });
const toUnlock: DbQueue[] = [];
Expand Down Expand Up @@ -638,6 +587,70 @@ export namespace EventUtils {
return { occurrence, event, store, eventQueues, queues };
}

// Sequentially re-shows every event-queue display in queue-index order across the event's Room
// and Sub display channels: deletes each existing display (and its posted Discord message) then
// inserts a fresh row and awaits a Replace refresh. Awaiting per queue guarantees the new
// messages have landed before the caller continues (used by `syncEventQueues` Step C and
// `runOpenAction` so a same-channel announcement stays as the most-recent message).
async function reshowEventQueueDisplays(store: Store, event: DbEvent): Promise<number> {
let reshownCount = 0;
const roles: EventQueueRole[] = [EventQueueRole.Room, EventQueueRole.Sub];

for (const role of roles) {
const displayChannelId = role === EventQueueRole.Room
? event.roomQueuesChannelId
: event.subQueuesChannelId;
const orderedEqs = Queries.selectManyEventQueues({ guildId: store.guild.id, eventId: event.id })
.filter(eq => eq.queueRole === role)
.sort((a, b) => Number(a.queueIndex) - Number(b.queueIndex));

for (const eq of orderedEqs) {
const queue = Queries.selectQueue({ guildId: store.guild.id, id: eq.queueId });
if (!queue) continue;

const existingDisplays = [...store.dbDisplays().filter(d => d.queueId === queue.id).values()];
for (const display of existingDisplays) {
if (display.lastMessageId) {
const channel = await store.jsChannel(display.displayChannelId) as GuildTextBasedChannel | undefined;
if (channel) {
const message = await channel.messages.fetch(display.lastMessageId).catch(e => {
console.error(`EventUtils.reshowEventQueueDisplays: failed to fetch stale display message ${display.lastMessageId} in channel ${display.displayChannelId}:`, e);
return null;
});
if (message) {
await message.delete().catch(e => {
console.error(`EventUtils.reshowEventQueueDisplays: failed to delete stale display message ${display.lastMessageId} in channel ${display.displayChannelId}:`, e);
return null;
});
}
}
}
store.deleteDisplay({ id: display.id });
}

const newDisplay = store.insertDisplay({
guildId: store.guild.id,
queueId: queue.id,
displayChannelId,
});
if (!newDisplay) continue;

await DisplayUtils.updateDisplays({
store,
queueId: queue.id,
opts: {
displayIds: [newDisplay.id],
updateTypeOverride: DisplayUpdateType.Replace,
},
});

reshownCount++;
}
}

return reshownCount;
}

async function runOpenAction(occurrenceId: bigint) {
const ctx = await getEventContext(occurrenceId);
if (!ctx) return;
Expand All @@ -648,13 +661,9 @@ export namespace EventUtils {
await QueueUtils.updateQueues(store, queues, { lockToggle: false } as Partial<DbQueue>);
}

// Force-refresh displays
const queueIds = queues.map(q => q.id);
DisplayUtils.requestDisplaysUpdate({
store,
queueIds,
opts: { updateTypeOverride: DisplayUpdateType.Replace },
});
// Re-show every event-queue display sequentially before announcing so the announcement
// remains the most-recent message when announcementChannelId coincides with a display channel.
await reshowEventQueueDisplays(store, event);

// Send announcement
if (event.announcementChannelId && event.announcementMessage) {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/queue.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export namespace QueueUtils {
const updatedQueues = compact(map(queues, queue => store.updateQueue({ id: queue.id, ...update })));
const updatedQueueIds = updatedQueues.map(queue => queue.id);

DisplayUtils.requestDisplaysUpdate({ store, queueIds: updatedQueueIds });
await DisplayUtils.requestDisplaysUpdate({ store, queueIds: updatedQueueIds });

if (update.roleInQueueId) {
await QueueUtils.setRoleInQueue(store, updatedQueues);
Expand Down