From 07d6eb3c1272c2526a4749724c07c4fd530893d4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 2 Nov 2024 20:35:52 +1300 Subject: [PATCH] Fix existingPartZero assertion error --- src/d2m/actions/retrigger.js | 33 +++++++++++++++++++++++---- src/d2m/converters/edit-to-changes.js | 30 ++++++++++++++---------- src/d2m/event-dispatcher.js | 2 +- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/d2m/actions/retrigger.js b/src/d2m/actions/retrigger.js index 030ffbf9..aa79a798 100644 --- a/src/d2m/actions/retrigger.js +++ b/src/d2m/actions/retrigger.js @@ -12,6 +12,7 @@ function debugRetrigger(message) { } } +const paused = new Set() const emitter = new EventEmitter() /** @@ -25,13 +26,15 @@ const emitter = new EventEmitter() * @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered */ function eventNotFoundThenRetrigger(messageID, fn, ...rest) { - const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get() - if (eventID) { - debugRetrigger(`[retrigger] OK mid <-> eid = ${messageID} <-> ${eventID}`) - return false // event was found so don't retrigger + if (!paused.has(messageID)) { + const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get() + if (eventID) { + debugRetrigger(`[retrigger] OK mid <-> eid = ${messageID} <-> ${eventID}`) + return false // event was found so don't retrigger + } } - debugRetrigger(`[retrigger] WAIT mid <-> eid = ${messageID} <-> ${eventID}`) + debugRetrigger(`[retrigger] WAIT mid = ${messageID}`) emitter.once(messageID, () => { debugRetrigger(`[retrigger] TRIGGER mid = ${messageID}`) fn(...rest) @@ -46,6 +49,25 @@ function eventNotFoundThenRetrigger(messageID, fn, ...rest) { return true // event was not found, then retrigger } +/** + * Anything calling retrigger during the callback will be paused and retriggered after the callback resolves. + * @template T + * @param {string} messageID + * @param {Promise} promise + * @returns {Promise} + */ +async function pauseChanges(messageID, promise) { + try { + debugRetrigger(`[retrigger] PAUSE mid = ${messageID}`) + paused.add(messageID) + return await promise + } finally { + debugRetrigger(`[retrigger] RESUME mid = ${messageID}`) + paused.delete(messageID) + messageFinishedBridging(messageID) + } +} + /** * Triggers any pending operations that were waiting on the corresponding event ID. * @param {string} messageID @@ -59,3 +81,4 @@ function messageFinishedBridging(messageID) { module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger module.exports.messageFinishedBridging = messageFinishedBridging +module.exports.pauseChanges = pauseChanges diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index f93c510b..c38c24ef 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -122,35 +122,41 @@ async function editToChanges(message, guild, api) { eventsToReplace = eventsToReplace.filter(eventCanBeEdited) // We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times. + // This would be disrupted if existing events that are (reaction_)part = 0 will be redacted. + // If that is the case, pick a different existing or newly sent event to be (reaction_)part = 0. /** @type {({column: string, eventID: string, value?: number} | {column: string, nextEvent: true})[]} */ const promotions = [] for (const column of ["part", "reaction_part"]) { const candidatesForParts = unchangedEvents.concat(eventsToReplace) // If no events with part = 0 exist (or will exist), we need to do some management. if (!candidatesForParts.some(e => e.old[column] === 0)) { + // Try to find an existing event to promote. Bigger order is better. if (candidatesForParts.length) { - // We can choose an existing event to promote. Bigger order is better. const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.old.event_subtype === "m.text") candidatesForParts.sort((a, b) => order(b) - order(a)) if (column === "part") { promotions.push({column, eventID: candidatesForParts[0].old.event_id}) // part should be the first one + } else if (eventsToSend.length) { + promotions.push({column, nextEvent: true}) // reaction_part should be the last one } else { promotions.push({column, eventID: candidatesForParts[candidatesForParts.length - 1].old.event_id}) // reaction_part should be the last one } - } else { - // No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0. + } + // Or, if there are no existing events to promote and new events will be sent, whatever gets sent will be the next part = 0. + else { promotions.push({column, nextEvent: true}) } } - // If adding events, try to keep reactions attached to the bottom of the group (unless reactions have already been added) - if (eventsToSend.length && !promotions.length) { - const existingReaction = select("reaction", "message_id", {message_id: message.id}).pluck().get() - if (!existingReaction) { - const existingPartZero = candidatesForParts.find(p => p.old.reaction_part === 0) - assert(existingPartZero) // will exist because a reaction_part=0 always exists and no events are being removed - promotions.push({column: "reaction_part", eventID: existingPartZero.old.event_id, value: 1}) // update the current reaction_part to 1 - promotions.push({column: "reaction_part", nextEvent: true}) // the newly created event will have reaction_part = 0 - } + } + + // If adding events, try to keep reactions attached to the bottom of the group (unless reactions have already been added) + if (eventsToSend.length && !promotions.length) { + const existingReaction = select("reaction", "message_id", {message_id: message.id}).pluck().get() + if (!existingReaction) { + const existingPartZero = unchangedEvents.concat(eventsToReplace).find(p => p.old.reaction_part === 0) + assert(existingPartZero) // will exist because a reaction_part=0 always exists and no events are being removed + promotions.push({column: "reaction_part", eventID: existingPartZero.old.event_id, value: 1}) // update the current reaction_part to 1 + promotions.push({column: "reaction_part", nextEvent: true}) // the newly created event will have reaction_part = 0 } } diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index c68e11af..c84d6a8d 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -304,7 +304,7 @@ module.exports = { assert(guild) // @ts-ignore - await editMessage.editMessage(message, guild, row) + await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild, row)) }, /**