diff --git a/packages/reacord/library.new/discord-js.ts b/packages/reacord/library.new/discord-js.ts index e1e00a0..084bfa0 100644 --- a/packages/reacord/library.new/discord-js.ts +++ b/packages/reacord/library.new/discord-js.ts @@ -18,13 +18,18 @@ export function createReacordDiscordJs( const manager = createReacordInstanceManager(options) return { send(channelId: string, initialContent?: ReactNode) { - const messageUpdater = createMessageUpdater() - return manager.createInstance(initialContent, async (tree) => { - const messageOptions: MessageOptions & MessageEditOptions = { - content: tree.children.map((child) => child.text).join(""), - } - const channel = await getTextChannel(client, channelId) - await messageUpdater.update(messageOptions, channel) + const handler = createMessageHandler() + return manager.createInstance({ + initialContent, + update: async (tree) => { + const messageOptions: MessageOptions & MessageEditOptions = { + content: tree.children.map((child) => child.text).join(""), + } + const channel = await getTextChannel(client, channelId) + await handler.update(messageOptions, channel) + }, + destroy: () => handler.destroy(), + deactivate: () => handler.deactivate(), }) }, @@ -34,8 +39,9 @@ export function createReacordDiscordJs( } } -function createMessageUpdater() { +function createMessageHandler() { let message: Message | undefined + let active = true const queue = createAsyncQueue() async function update( @@ -43,6 +49,7 @@ function createMessageUpdater() { channel: TextBasedChannel, ) { return queue.add(async () => { + if (!active) return if (message) { await message.edit(options) } else { @@ -51,10 +58,27 @@ function createMessageUpdater() { }) } - return { update } + async function destroy() { + return queue.add(async () => { + active = false + await message?.delete() + }) + } + + async function deactivate() { + return queue.add(async () => { + active = false + // TODO: disable message components + }) + } + + return { update, destroy, deactivate } } -async function getTextChannel(client: Client, channelId: string) { +async function getTextChannel( + client: Client, + channelId: string, +): Promise { let channel = client.channels.cache.get(channelId) if (!channel) { channel = (await client.channels.fetch(channelId)) ?? undefined diff --git a/packages/reacord/library.new/reacord.ts b/packages/reacord/library.new/reacord.ts index 0fd18c7..63a6ac7 100644 --- a/packages/reacord/library.new/reacord.ts +++ b/packages/reacord/library.new/reacord.ts @@ -24,13 +24,31 @@ export type ReacordInstance = { deactivate: () => void } +type ReacordInstanceOptions = { + initialContent: ReactNode + update: (tree: MessageTree) => unknown + deactivate: () => unknown + destroy: () => unknown +} + export function createReacordInstanceManager({ maxInstances = 50, }: ReacordOptions) { const instances: ReacordInstance[] = [] - function createInstance(...args: Parameters) { - const instance = createReacordInstance(...args) + function createInstance(options: ReacordInstanceOptions) { + const instance = createReacordInstance({ + ...options, + deactivate() { + instances.splice(instances.indexOf(instance), 1) + return options.deactivate() + }, + destroy() { + instances.splice(instances.indexOf(instance), 1) + return options.destroy() + }, + }) + instances.push(instance) if (instances.length > maxInstances) { @@ -44,14 +62,13 @@ export function createReacordInstanceManager({ } function createReacordInstance( - initialContent: ReactNode, - render: (tree: MessageTree) => unknown, + options: ReacordInstanceOptions, ): ReacordInstance { const tree: MessageTree = { children: [], render: async () => { try { - await render(tree) + await options.update(tree) } catch (error) { console.error( "Reacord encountered an error while updating the message.", @@ -79,12 +96,30 @@ function createReacordInstance( render(content: ReactNode) { reconciler.updateContainer(content, container) }, - destroy() {}, - deactivate() {}, + async deactivate() { + try { + await options.deactivate() + } catch (error) { + console.error( + "Reacord encountered an error while deactivating an instance.", + error, + ) + } + }, + async destroy() { + try { + await options.destroy() + } catch (error) { + console.error( + "Reacord encountered an error while destroying an instance.", + error, + ) + } + }, } - if (initialContent !== undefined) { - instance.render(initialContent) + if (options.initialContent !== undefined) { + instance.render(options.initialContent) } return instance diff --git a/packages/reacord/scripts/discordjs-manual-test.tsx b/packages/reacord/scripts/discordjs-manual-test.tsx index c9cd146..7f363a1 100644 --- a/packages/reacord/scripts/discordjs-manual-test.tsx +++ b/packages/reacord/scripts/discordjs-manual-test.tsx @@ -3,6 +3,8 @@ import { ChannelType, Client, IntentsBitField } from "discord.js" import "dotenv/config" import { kebabCase } from "lodash-es" import React, { useEffect, useState } from "react" +import { raise } from "../helpers/raise" +import { waitFor } from "../helpers/wait-for" import { createReacordDiscordJs } from "../library.new/discord-js" const client = new Client({ intents: IntentsBitField.Flags.Guilds }) @@ -26,6 +28,7 @@ for (const [, channel] of category.children.cache) { let prefix = 0 const createTest = async ( name: string, + description: string, block: (channel: TextChannel) => void | Promise, ) => { prefix += 1 @@ -33,10 +36,11 @@ const createTest = async ( type: ChannelType.GuildText, name: `${String(prefix).padStart(3, "0")}-${kebabCase(name)}`, }) + await channel.edit({ topic: description }) await block(channel) } -await createTest("basic", (channel) => { +await createTest("basic", "should update over time", (channel) => { function Timer() { const [count, setCount] = useState(0) @@ -53,8 +57,40 @@ await createTest("basic", (channel) => { reacord.send(channel.id, ) }) -await createTest("immediate renders", async (channel) => { - const instance = reacord.send(channel.id) - instance.render("hi world") - instance.render("hi moon") -}) +await createTest( + "immediate renders", + `should process renders in sequence; this should show "hi moon"`, + async (channel) => { + const instance = reacord.send(channel.id) + instance.render("hi world") + instance.render("hi moon") + }, +) + +await createTest( + "destroy", + "should remove the message; this channel should be empty", + async (channel) => { + const instance = reacord.send(channel.id) + instance.render("hi world") + instance.render("hi moon") + await waitFor(async () => { + const messages = await channel.messages.fetch({ limit: 1 }) + if (messages.first()?.content !== "hi moon") { + raise("not ready") + } + }) + instance.destroy() + }, +) + +await createTest( + "immediate destroy", + "should never show if called immediately; this channel should be empty", + async (channel) => { + const instance = reacord.send(channel.id) + instance.render("hi world") + instance.render("hi moon") + instance.destroy() + }, +)