classes are fine, actually! + simplified things more
This commit is contained in:
@@ -1,27 +1,25 @@
|
|||||||
export type AsyncCallback = () => unknown
|
export type AsyncCallback = () => unknown
|
||||||
|
|
||||||
export function createAsyncQueue() {
|
export class AsyncQueue {
|
||||||
const callbacks: AsyncCallback[] = []
|
private callbacks: AsyncCallback[] = []
|
||||||
let promise: Promise<void> | undefined
|
private promise: Promise<void> | undefined
|
||||||
|
|
||||||
async function add(callback: AsyncCallback) {
|
async add(callback: AsyncCallback) {
|
||||||
callbacks.push(callback)
|
this.callbacks.push(callback)
|
||||||
if (promise) return promise
|
if (this.promise) return this.promise
|
||||||
|
|
||||||
promise = runQueue()
|
this.promise = this.runQueue()
|
||||||
try {
|
try {
|
||||||
await promise
|
await this.promise
|
||||||
} finally {
|
} finally {
|
||||||
promise = undefined
|
this.promise = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runQueue() {
|
private async runQueue() {
|
||||||
let callback: AsyncCallback | undefined
|
let callback: AsyncCallback | undefined
|
||||||
while ((callback = callbacks.shift())) {
|
while ((callback = this.callbacks.shift())) {
|
||||||
await callback()
|
await callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { add }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,82 +7,97 @@ import type {
|
|||||||
TextBasedChannel,
|
TextBasedChannel,
|
||||||
} from "discord.js"
|
} from "discord.js"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import { createAsyncQueue } from "./async-queue"
|
import { AsyncQueue } from "./async-queue"
|
||||||
import type { ReacordOptions } from "./reacord"
|
import type { ReacordOptions } from "./reacord"
|
||||||
import { createReacordInstanceManager } from "./reacord"
|
import { ReacordInstancePool } from "./reacord"
|
||||||
|
|
||||||
export function createReacordDiscordJs(
|
export class ReacordDiscordJs {
|
||||||
client: Client,
|
private instances
|
||||||
options: ReacordOptions = {},
|
|
||||||
) {
|
constructor(private readonly client: Client, options: ReacordOptions = {}) {
|
||||||
const manager = createReacordInstanceManager(options)
|
this.instances = new ReacordInstancePool(options)
|
||||||
return {
|
}
|
||||||
send(channelId: string, initialContent?: ReactNode) {
|
|
||||||
const handler = createMessageHandler()
|
send(channelId: string, initialContent?: ReactNode) {
|
||||||
return manager.createInstance({
|
const renderer = new MessageRenderer()
|
||||||
initialContent,
|
|
||||||
update: async (tree) => {
|
return this.instances.create({
|
||||||
|
initialContent,
|
||||||
|
update: async (tree) => {
|
||||||
|
try {
|
||||||
const messageOptions: MessageOptions & MessageEditOptions = {
|
const messageOptions: MessageOptions & MessageEditOptions = {
|
||||||
content: tree.children.map((child) => child.text).join(""),
|
content: tree.children.map((child) => child.text).join(""),
|
||||||
}
|
}
|
||||||
const channel = await getTextChannel(client, channelId)
|
const channel = await getTextChannel(this.client, channelId)
|
||||||
await handler.update(messageOptions, channel)
|
await renderer.update(messageOptions, channel)
|
||||||
},
|
} catch (error) {
|
||||||
destroy: () => handler.destroy(),
|
console.error("Error updating message:", error)
|
||||||
deactivate: () => handler.deactivate(),
|
}
|
||||||
})
|
},
|
||||||
},
|
destroy: async () => {
|
||||||
|
try {
|
||||||
reply(interaction: Interaction, initialContent?: ReactNode) {},
|
await renderer.destroy()
|
||||||
|
} catch (error) {
|
||||||
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {},
|
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() {
|
class MessageRenderer {
|
||||||
let message: Message | undefined
|
private message: Message | undefined
|
||||||
let active = true
|
private active = true
|
||||||
const queue = createAsyncQueue()
|
private readonly queue = new AsyncQueue()
|
||||||
|
|
||||||
async function update(
|
update(
|
||||||
options: MessageOptions & MessageEditOptions,
|
options: MessageOptions & MessageEditOptions,
|
||||||
channel: TextBasedChannel,
|
channel: TextBasedChannel,
|
||||||
) {
|
) {
|
||||||
return queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
if (!active) return
|
if (!this.active) return
|
||||||
if (message) {
|
if (this.message) {
|
||||||
await message.edit(options)
|
await this.message.edit(options)
|
||||||
} else {
|
} else {
|
||||||
message = await channel.send(options)
|
this.message = await channel.send(options)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function destroy() {
|
destroy() {
|
||||||
return queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
active = false
|
this.active = false
|
||||||
await message?.delete()
|
await this.message?.delete()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deactivate() {
|
deactivate() {
|
||||||
return queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
active = false
|
this.active = false
|
||||||
// TODO: disable message components
|
// TODO: disable message components
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { update, destroy, deactivate }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTextChannel(
|
async function getTextChannel(
|
||||||
client: Client<boolean>,
|
client: Client<boolean>,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
): Promise<TextBasedChannel> {
|
): Promise<TextBasedChannel> {
|
||||||
let channel = client.channels.cache.get(channelId)
|
const channel =
|
||||||
if (!channel) {
|
client.channels.cache.get(channelId) ??
|
||||||
channel = (await client.channels.fetch(channelId)) ?? undefined
|
(await client.channels.fetch(channelId))
|
||||||
}
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
throw new Error(`Channel ${channelId} not found`)
|
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"
|
export { type ReacordInstance, type ReacordOptions } from "./reacord"
|
||||||
|
|||||||
@@ -24,103 +24,77 @@ export type ReacordInstance = {
|
|||||||
deactivate: () => void
|
deactivate: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReacordInstanceOptions = {
|
export type ReacordInstanceOptions = {
|
||||||
initialContent: ReactNode
|
initialContent: ReactNode
|
||||||
update: (tree: MessageTree) => unknown
|
update: (tree: MessageTree) => Promise<void>
|
||||||
deactivate: () => unknown
|
deactivate: () => Promise<void>
|
||||||
destroy: () => unknown
|
destroy: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createReacordInstanceManager({
|
export class ReacordInstancePool {
|
||||||
maxInstances = 50,
|
private readonly options: Required<ReacordOptions>
|
||||||
}: ReacordOptions) {
|
private readonly instances = new Set<ReacordInstance>()
|
||||||
const instances: ReacordInstance[] = []
|
|
||||||
|
|
||||||
function createInstance(options: ReacordInstanceOptions) {
|
constructor({ maxInstances = 50 }: ReacordOptions) {
|
||||||
const instance = createReacordInstance({
|
this.options = { maxInstances }
|
||||||
...options,
|
}
|
||||||
deactivate() {
|
|
||||||
instances.splice(instances.indexOf(instance), 1)
|
create(options: ReacordInstanceOptions) {
|
||||||
return options.deactivate()
|
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) {
|
if (this.instances.size > this.options.maxInstances) {
|
||||||
instances.shift()?.deactivate()
|
;[...this.instances][0]?.deactivate()
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance
|
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