classes are fine, actually! + simplified things more

This commit is contained in:
itsMapleLeaf
2022-07-24 13:39:55 -05:00
parent 533d8a0f60
commit f9564897aa
4 changed files with 135 additions and 148 deletions

View File

@@ -1,27 +1,25 @@
export type AsyncCallback = () => unknown
export function createAsyncQueue() {
const callbacks: AsyncCallback[] = []
let promise: Promise<void> | undefined
export class AsyncQueue {
private callbacks: AsyncCallback[] = []
private promise: Promise<void> | undefined
async function add(callback: AsyncCallback) {
callbacks.push(callback)
if (promise) return promise
async add(callback: AsyncCallback) {
this.callbacks.push(callback)
if (this.promise) return this.promise
promise = runQueue()
this.promise = this.runQueue()
try {
await promise
await this.promise
} finally {
promise = undefined
this.promise = undefined
}
}
async function runQueue() {
private async runQueue() {
let callback: AsyncCallback | undefined
while ((callback = callbacks.shift())) {
while ((callback = this.callbacks.shift())) {
await callback()
}
}
return { add }
}

View File

@@ -7,82 +7,97 @@ import type {
TextBasedChannel,
} from "discord.js"
import type { ReactNode } from "react"
import { createAsyncQueue } from "./async-queue"
import { AsyncQueue } from "./async-queue"
import type { ReacordOptions } from "./reacord"
import { createReacordInstanceManager } from "./reacord"
import { ReacordInstancePool } from "./reacord"
export function createReacordDiscordJs(
client: Client,
options: ReacordOptions = {},
) {
const manager = createReacordInstanceManager(options)
return {
send(channelId: string, initialContent?: ReactNode) {
const handler = createMessageHandler()
return manager.createInstance({
initialContent,
update: async (tree) => {
export class ReacordDiscordJs {
private instances
constructor(private readonly client: Client, options: ReacordOptions = {}) {
this.instances = new ReacordInstancePool(options)
}
send(channelId: string, initialContent?: ReactNode) {
const renderer = new MessageRenderer()
return this.instances.create({
initialContent,
update: async (tree) => {
try {
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(),
})
},
reply(interaction: Interaction, initialContent?: ReactNode) {},
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {},
const channel = await getTextChannel(this.client, channelId)
await renderer.update(messageOptions, channel)
} catch (error) {
console.error("Error updating message:", error)
}
},
destroy: async () => {
try {
await renderer.destroy()
} catch (error) {
console.error("Error destroying message:", error)
}
},
deactivate: async () => {
try {
await renderer.deactivate()
} catch (error) {
console.error("Error deactivating message:", error)
}
},
})
}
reply(interaction: Interaction, initialContent?: ReactNode) {}
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {}
}
function createMessageHandler() {
let message: Message | undefined
let active = true
const queue = createAsyncQueue()
class MessageRenderer {
private message: Message | undefined
private active = true
private readonly queue = new AsyncQueue()
async function update(
update(
options: MessageOptions & MessageEditOptions,
channel: TextBasedChannel,
) {
return queue.add(async () => {
if (!active) return
if (message) {
await message.edit(options)
return this.queue.add(async () => {
if (!this.active) return
if (this.message) {
await this.message.edit(options)
} else {
message = await channel.send(options)
this.message = await channel.send(options)
}
})
}
async function destroy() {
return queue.add(async () => {
active = false
await message?.delete()
destroy() {
return this.queue.add(async () => {
this.active = false
await this.message?.delete()
})
}
async function deactivate() {
return queue.add(async () => {
active = false
deactivate() {
return this.queue.add(async () => {
this.active = false
// TODO: disable message components
})
}
return { update, destroy, deactivate }
}
async function getTextChannel(
client: Client<boolean>,
channelId: string,
): Promise<TextBasedChannel> {
let channel = client.channels.cache.get(channelId)
if (!channel) {
channel = (await client.channels.fetch(channelId)) ?? undefined
}
const channel =
client.channels.cache.get(channelId) ??
(await client.channels.fetch(channelId))
if (!channel) {
throw new Error(`Channel ${channelId} not found`)
}

View File

@@ -1,2 +1,2 @@
export { createReacordDiscordJs } from "./discord-js"
export { ReacordDiscordJs } from "./discord-js"
export { type ReacordInstance, type ReacordOptions } from "./reacord"

View File

@@ -24,103 +24,77 @@ export type ReacordInstance = {
deactivate: () => void
}
type ReacordInstanceOptions = {
export type ReacordInstanceOptions = {
initialContent: ReactNode
update: (tree: MessageTree) => unknown
deactivate: () => unknown
destroy: () => unknown
update: (tree: MessageTree) => Promise<void>
deactivate: () => Promise<void>
destroy: () => Promise<void>
}
export function createReacordInstanceManager({
maxInstances = 50,
}: ReacordOptions) {
const instances: ReacordInstance[] = []
export class ReacordInstancePool {
private readonly options: Required<ReacordOptions>
private readonly instances = new Set<ReacordInstance>()
function createInstance(options: ReacordInstanceOptions) {
const instance = createReacordInstance({
...options,
deactivate() {
instances.splice(instances.indexOf(instance), 1)
return options.deactivate()
constructor({ maxInstances = 50 }: ReacordOptions) {
this.options = { maxInstances }
}
create(options: ReacordInstanceOptions) {
const tree: MessageTree = {
children: [],
render: async () => {
try {
await options.update(tree)
} catch (error) {
console.error("Failed to update message.", error)
}
},
destroy() {
instances.splice(instances.indexOf(instance), 1)
return options.destroy()
}
const container = reconciler.createContainer(
tree,
0,
// eslint-disable-next-line unicorn/no-null
null,
false,
// eslint-disable-next-line unicorn/no-null
null,
"reacord",
() => {},
// eslint-disable-next-line unicorn/no-null
null,
)
const instance: ReacordInstance = {
render: (content: ReactNode) => {
reconciler.updateContainer(content, container)
},
})
deactivate: async () => {
this.instances.delete(instance)
try {
await options.deactivate()
} catch (error) {
console.error("Failed to deactivate message.", error)
}
},
destroy: async () => {
this.instances.delete(instance)
try {
await options.destroy()
} catch (error) {
console.error("Failed to destroy message.", error)
}
},
}
instances.push(instance)
if (options.initialContent !== undefined) {
instance.render(options.initialContent)
}
if (instances.length > maxInstances) {
instances.shift()?.deactivate()
if (this.instances.size > this.options.maxInstances) {
;[...this.instances][0]?.deactivate()
}
return instance
}
return { createInstance }
}
function createReacordInstance(
options: ReacordInstanceOptions,
): ReacordInstance {
const tree: MessageTree = {
children: [],
render: async () => {
try {
await options.update(tree)
} catch (error) {
console.error(
"Reacord encountered an error while updating the message.",
error,
)
}
},
}
const container = reconciler.createContainer(
tree,
0,
// eslint-disable-next-line unicorn/no-null
null,
false,
// eslint-disable-next-line unicorn/no-null
null,
"reacord",
() => {},
// eslint-disable-next-line unicorn/no-null
null,
)
const instance: ReacordInstance = {
render(content: ReactNode) {
reconciler.updateContainer(content, container)
},
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 (options.initialContent !== undefined) {
instance.render(options.initialContent)
}
return instance
}