classes are fine, actually! + simplified things more
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { createReacordDiscordJs } from "./discord-js"
|
||||
export { ReacordDiscordJs } from "./discord-js"
|
||||
export { type ReacordInstance, type ReacordOptions } from "./reacord"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user