diff --git a/src/container.ts b/src/container.ts index 11f55f3..fcc8a85 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,40 +1,72 @@ import type { Message, MessageOptions, TextBasedChannels } from "discord.js" +type Action = + | { type: "updateMessage"; options: MessageOptions } + | { type: "deleteMessage" } + export class ReacordContainer { channel: TextBasedChannels message?: Message + actions: Action[] = [] + runningActions = false constructor(channel: TextBasedChannels) { this.channel = channel } render(instances: string[]) { - if (instances.length === 0) { - if (this.message) { - this.channel.messages.cache.delete(this.message.id) - this.message.delete().catch(console.error) - this.message = undefined - } - return - } - const messageOptions: MessageOptions = { - content: instances.join(""), + content: instances.join("") || undefined, // empty strings are not allowed } - if (this.message) { - this.message.edit(messageOptions).catch(console.error) + const hasContent = messageOptions.content !== undefined + if (hasContent) { + this.addAction({ type: "updateMessage", options: messageOptions }) } else { - this.channel.send(messageOptions).then((message) => { - this.message = message - }, console.error) + this.addAction({ type: "deleteMessage" }) } } - // clear() { - // for (const instance of this.instances) { - // instance.destroy() - // } - // this.instances.clear() - // } + private addAction(action: Action) { + const lastAction = this.actions[this.actions.length - 1] + if (lastAction?.type === action.type) { + this.actions[this.actions.length - 1] = action + } else { + this.actions.push(action) + } + void this.runActions() + } + + private runActions() { + if (this.runningActions) return + this.runningActions = true + + queueMicrotask(async () => { + let action: Action | undefined + while ((action = this.actions.shift())) { + try { + switch (action.type) { + case "updateMessage": + if (this.message) { + await this.message.edit(action.options) + } else { + this.message = await this.channel.send(action.options) + } + break + case "deleteMessage": + if (this.message) { + await this.message.delete() + this.message = undefined + } + break + } + } catch (error) { + console.error(`Failed to run action:`, action) + console.error(error) + } + } + + this.runningActions = false + }) + } } diff --git a/src/render.test.ts b/src/render.test.ts index 095e76d..e8c8a5b 100644 --- a/src/render.test.ts +++ b/src/render.test.ts @@ -1,6 +1,7 @@ import test from "ava" import { Client, TextChannel } from "discord.js" import { nanoid } from "nanoid" +import { setTimeout } from "node:timers/promises" import { raise } from "./helpers/raise.js" import { waitForWithTimeout } from "./helpers/wait-for-with-timeout.js" import { render } from "./render.js" @@ -31,31 +32,77 @@ test.after(() => { client.destroy() }) -test("rendering text", async (t) => { +test.serial("rendering text", async (t) => { const content = nanoid() const root = render(content, channel) - await waitForWithTimeout(async () => { - const messages = await channel.messages.fetch() - return messages.some((m) => m.content === content) - }, 4000) + await waitForWithTimeout( + async () => { + const messages = await channel.messages.fetch() + return messages.some((m) => m.content === content) + }, + 10_000, + "Message not found", + ) const newContent = nanoid() root.rerender(newContent) - await waitForWithTimeout(async () => { - const messages = await channel.messages.fetch({ limit: 1 }) - return messages.some((m) => m.content === content) - }, 4000) + await waitForWithTimeout( + async () => { + const messages = await channel.messages.fetch() + return messages.some((m) => m.content === newContent) + }, + 10_000, + "Message not found", + ) root.destroy() - await waitForWithTimeout(async () => { - const messages = await channel.messages.fetch({ limit: 1 }) - return messages - .filter((m) => !m.deleted) - .every((m) => m.content !== content) - }, 4000) + await waitForWithTimeout( + async () => { + await setTimeout(1000) + const messages = await channel.messages.fetch() + return messages + .filter((m) => !m.deleted) + .every((m) => m.content !== content) + }, + 10_000, + "Message was not deleted", + ) + + t.pass() +}) + +test.serial("rapid updates", async (t) => { + const content = nanoid() + const newContent = nanoid() + + const root = render(content, channel) + root.rerender(newContent) + + await waitForWithTimeout( + async () => { + const messages = await channel.messages.fetch() + return messages.some((m) => m.content === newContent) + }, + 10_000, + "Message not found", + ) + + root.rerender(content) + root.destroy() + + await waitForWithTimeout( + async () => { + const messages = await channel.messages.fetch() + return messages + .filter((m) => !m.deleted) + .every((m) => m.content !== content) + }, + 10_000, + "Message was not deleted", + ) t.pass() })