trying to reduce "layers of conversion"

one problem with the current iteration of reacord is the number of conversation layers there are between internals and the adapter.

the flow is: elements -> node tree -> reacord objects -> adapter objects -> adapter renderer

so far it looks like I can reduce this to: elements -> node tree -> adapter renderer
This commit is contained in:
itsMapleLeaf
2022-07-24 15:02:07 -05:00
parent cfd88fe110
commit 35fbf93be7
7 changed files with 122 additions and 88 deletions

View File

@@ -0,0 +1,27 @@
export class Container<T> {
private items: T[] = []
getItems(): readonly T[] {
return this.items
}
add(item: T) {
this.items.push(item)
}
remove(item: T) {
const index = this.items.indexOf(item)
if (index === -1) return
this.items.splice(index, 1)
}
clear() {
this.items = []
}
insertBefore(item: T, beforeItem: T) {
const index = this.items.indexOf(beforeItem)
if (index === -1) return
this.items.splice(index, 0, item)
}
}

View File

@@ -1,3 +1,5 @@
export { Button, type ButtonProps } from "./button"
export { type ButtonSharedProps } from "./button-shared-props"
export { ReacordDiscordJs } from "./reacord-discord-js" export { ReacordDiscordJs } from "./reacord-discord-js"
export { export {
type ReacordInstance, type ReacordInstance,

View File

@@ -1,9 +0,0 @@
export type MessageTree = {
children: TextNode[]
render: () => void
}
export type TextNode = {
type: "text"
text: string
}

View File

@@ -0,0 +1,20 @@
export type Node = {
readonly type: string
readonly props?: Record<string, unknown>
children?: Node[]
getText?: () => string
}
export class TextNode implements Node {
readonly type = "text"
constructor(private text: string) {}
getText() {
return this.text
}
setText(text: string) {
this.text = text
}
}

View File

@@ -8,7 +8,11 @@ import type {
} from "discord.js" } from "discord.js"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import { AsyncQueue } from "./async-queue" import { AsyncQueue } from "./async-queue"
import type { ReacordOptions } from "./reacord-instance-pool" import type { Node } from "./node"
import type {
ReacordMessageRenderer,
ReacordOptions,
} from "./reacord-instance-pool"
import { ReacordInstancePool } from "./reacord-instance-pool" import { ReacordInstancePool } from "./reacord-instance-pool"
export class ReacordDiscordJs { export class ReacordDiscordJs {
@@ -19,36 +23,8 @@ export class ReacordDiscordJs {
} }
send(channelId: string, initialContent?: ReactNode) { send(channelId: string, initialContent?: ReactNode) {
const renderer = new MessageRenderer() const renderer = new ChannelMessageRenderer(this.client, channelId)
return this.instances.create({ initialContent, renderer })
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(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) {} reply(interaction: Interaction, initialContent?: ReactNode) {}
@@ -56,22 +32,34 @@ export class ReacordDiscordJs {
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {} ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {}
} }
class MessageRenderer { class ChannelMessageRenderer implements ReacordMessageRenderer {
private message: Message | undefined private message: Message | undefined
private channel: TextBasedChannel | undefined
private active = true private active = true
private readonly queue = new AsyncQueue() private readonly queue = new AsyncQueue()
update( constructor(
options: MessageOptions & MessageEditOptions, private readonly client: Client,
channel: TextBasedChannel, private readonly channelId: string,
) { ) {}
update(nodes: readonly Node[]) {
const options: MessageOptions & MessageEditOptions = {
content: nodes.map((node) => node.getText?.() || "").join(""),
}
return this.queue.add(async () => { return this.queue.add(async () => {
if (!this.active) return if (!this.active) {
return
}
if (this.message) { if (this.message) {
await this.message.edit(options) await this.message.edit(options)
} else { return
this.message = await channel.send(options)
} }
const channel = await this.getChannel()
this.message = await channel.send(options)
}) })
} }
@@ -88,21 +76,22 @@ class MessageRenderer {
// TODO: disable message components // TODO: disable message components
}) })
} }
}
async function getTextChannel( private async getChannel() {
client: Client<boolean>, if (this.channel) {
channelId: string, return this.channel
): Promise<TextBasedChannel> { }
const channel = const channel =
client.channels.cache.get(channelId) ?? this.client.channels.cache.get(this.channelId) ??
(await client.channels.fetch(channelId)) (await this.client.channels.fetch(this.channelId))
if (!channel) { if (!channel) {
throw new Error(`Channel ${channelId} not found`) throw new Error(`Channel ${this.channelId} not found`)
} }
if (!channel.isTextBased()) { if (!channel.isTextBased()) {
throw new Error(`Channel ${channelId} is not a text channel`) throw new Error(`Channel ${this.channelId} is not a text channel`)
}
return (this.channel = channel)
} }
return channel
} }

View File

@@ -1,5 +1,6 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import type { MessageTree } from "./message-tree" import { Container } from "./container"
import type { Node } from "./node"
import { reconciler } from "./reconciler" import { reconciler } from "./reconciler"
export type ReacordOptions = { export type ReacordOptions = {
@@ -26,7 +27,11 @@ export type ReacordInstance = {
export type ReacordInstanceOptions = { export type ReacordInstanceOptions = {
initialContent: ReactNode initialContent: ReactNode
update: (tree: MessageTree) => Promise<void> renderer: ReacordMessageRenderer
}
export type ReacordMessageRenderer = {
update: (nodes: readonly Node[]) => Promise<void>
deactivate: () => Promise<void> deactivate: () => Promise<void>
destroy: () => Promise<void> destroy: () => Promise<void>
} }
@@ -39,20 +44,19 @@ export class ReacordInstancePool {
this.options = { maxInstances } this.options = { maxInstances }
} }
create(options: ReacordInstanceOptions) { create({ initialContent, renderer }: ReacordInstanceOptions) {
const tree: MessageTree = { const nodes = new Container<Node>()
children: [],
render: async () => { const render = async () => {
try { try {
await options.update(tree) await renderer.update(nodes.getItems())
} catch (error) { } catch (error) {
console.error("Failed to update message.", error) console.error("Failed to update message.", error)
} }
},
} }
const container = reconciler.createContainer( const container = reconciler.createContainer(
tree, { nodes, render },
0, 0,
// eslint-disable-next-line unicorn/no-null // eslint-disable-next-line unicorn/no-null
null, null,
@@ -72,7 +76,7 @@ export class ReacordInstancePool {
deactivate: async () => { deactivate: async () => {
this.instances.delete(instance) this.instances.delete(instance)
try { try {
await options.deactivate() await renderer.deactivate()
} catch (error) { } catch (error) {
console.error("Failed to deactivate message.", error) console.error("Failed to deactivate message.", error)
} }
@@ -80,15 +84,15 @@ export class ReacordInstancePool {
destroy: async () => { destroy: async () => {
this.instances.delete(instance) this.instances.delete(instance)
try { try {
await options.destroy() await renderer.destroy()
} catch (error) { } catch (error) {
console.error("Failed to destroy message.", error) console.error("Failed to destroy message.", error)
} }
}, },
} }
if (options.initialContent !== undefined) { if (initialContent !== undefined) {
instance.render(options.initialContent) instance.render(initialContent)
} }
if (this.instances.size > this.options.maxInstances) { if (this.instances.size > this.options.maxInstances) {

View File

@@ -1,11 +1,13 @@
import ReactReconciler from "react-reconciler" import ReactReconciler from "react-reconciler"
import { DefaultEventPriority } from "react-reconciler/constants" import { DefaultEventPriority } from "react-reconciler/constants"
import type { MessageTree, TextNode } from "./message-tree" import type { Container } from "./container"
import type { Node } from "./node"
import { TextNode } from "./node"
export const reconciler = ReactReconciler< export const reconciler = ReactReconciler<
string, // Type string, // Type
Record<string, unknown>, // Props Record<string, unknown>, // Props
MessageTree, // Container { nodes: Container<Node>; render: () => void }, // Container
never, // Instance never, // Instance
TextNode, // TextInstance TextNode, // TextInstance
never, // SuspenseInstance never, // SuspenseInstance
@@ -30,7 +32,7 @@ export const reconciler = ReactReconciler<
}, },
createTextInstance(text) { createTextInstance(text) {
return { type: "text", text } return new TextNode(text)
}, },
appendInitialChild(parent, child) {}, appendInitialChild(parent, child) {},
@@ -38,28 +40,27 @@ export const reconciler = ReactReconciler<
appendChild(parentInstance, child) {}, appendChild(parentInstance, child) {},
appendChildToContainer(container, child) { appendChildToContainer(container, child) {
container.children.push(child) container.nodes.add(child)
}, },
insertBefore(parentInstance, child, beforeChild) {}, insertBefore(parentInstance, child, beforeChild) {},
insertInContainerBefore(container, child, beforeChild) { insertInContainerBefore(container, child, beforeChild) {
const index = container.children.indexOf(beforeChild) container.nodes.insertBefore(child, beforeChild)
if (index !== -1) container.children.splice(index, 0, child)
}, },
removeChild(parentInstance, child) {}, removeChild(parentInstance, child) {},
removeChildFromContainer(container, child) { removeChildFromContainer(container, child) {
container.children = container.children.filter((c) => c !== child) container.nodes.remove(child)
}, },
clearContainer(container) { clearContainer(container) {
container.children = [] container.nodes.clear()
}, },
commitTextUpdate(textInstance, oldText, newText) { commitTextUpdate(textInstance, oldText, newText) {
textInstance.text = newText textInstance.setText(newText)
}, },
commitUpdate( commitUpdate(