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 {
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"
import type { ReactNode } from "react"
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"
export class ReacordDiscordJs {
@@ -19,36 +23,8 @@ export class ReacordDiscordJs {
}
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(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)
}
},
})
const renderer = new ChannelMessageRenderer(this.client, channelId)
return this.instances.create({ initialContent, renderer })
}
reply(interaction: Interaction, initialContent?: ReactNode) {}
@@ -56,22 +32,34 @@ export class ReacordDiscordJs {
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {}
}
class MessageRenderer {
class ChannelMessageRenderer implements ReacordMessageRenderer {
private message: Message | undefined
private channel: TextBasedChannel | undefined
private active = true
private readonly queue = new AsyncQueue()
update(
options: MessageOptions & MessageEditOptions,
channel: TextBasedChannel,
) {
constructor(
private readonly client: Client,
private readonly channelId: string,
) {}
update(nodes: readonly Node[]) {
const options: MessageOptions & MessageEditOptions = {
content: nodes.map((node) => node.getText?.() || "").join(""),
}
return this.queue.add(async () => {
if (!this.active) return
if (!this.active) {
return
}
if (this.message) {
await this.message.edit(options)
} else {
this.message = await channel.send(options)
return
}
const channel = await this.getChannel()
this.message = await channel.send(options)
})
}
@@ -88,21 +76,22 @@ class MessageRenderer {
// TODO: disable message components
})
}
}
async function getTextChannel(
client: Client<boolean>,
channelId: string,
): Promise<TextBasedChannel> {
private async getChannel() {
if (this.channel) {
return this.channel
}
const channel =
client.channels.cache.get(channelId) ??
(await client.channels.fetch(channelId))
this.client.channels.cache.get(this.channelId) ??
(await this.client.channels.fetch(this.channelId))
if (!channel) {
throw new Error(`Channel ${channelId} not found`)
throw new Error(`Channel ${this.channelId} not found`)
}
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 { MessageTree } from "./message-tree"
import { Container } from "./container"
import type { Node } from "./node"
import { reconciler } from "./reconciler"
export type ReacordOptions = {
@@ -26,7 +27,11 @@ export type ReacordInstance = {
export type ReacordInstanceOptions = {
initialContent: ReactNode
update: (tree: MessageTree) => Promise<void>
renderer: ReacordMessageRenderer
}
export type ReacordMessageRenderer = {
update: (nodes: readonly Node[]) => Promise<void>
deactivate: () => Promise<void>
destroy: () => Promise<void>
}
@@ -39,20 +44,19 @@ export class ReacordInstancePool {
this.options = { maxInstances }
}
create(options: ReacordInstanceOptions) {
const tree: MessageTree = {
children: [],
render: async () => {
create({ initialContent, renderer }: ReacordInstanceOptions) {
const nodes = new Container<Node>()
const render = async () => {
try {
await options.update(tree)
await renderer.update(nodes.getItems())
} catch (error) {
console.error("Failed to update message.", error)
}
},
}
const container = reconciler.createContainer(
tree,
{ nodes, render },
0,
// eslint-disable-next-line unicorn/no-null
null,
@@ -72,7 +76,7 @@ export class ReacordInstancePool {
deactivate: async () => {
this.instances.delete(instance)
try {
await options.deactivate()
await renderer.deactivate()
} catch (error) {
console.error("Failed to deactivate message.", error)
}
@@ -80,15 +84,15 @@ export class ReacordInstancePool {
destroy: async () => {
this.instances.delete(instance)
try {
await options.destroy()
await renderer.destroy()
} catch (error) {
console.error("Failed to destroy message.", error)
}
},
}
if (options.initialContent !== undefined) {
instance.render(options.initialContent)
if (initialContent !== undefined) {
instance.render(initialContent)
}
if (this.instances.size > this.options.maxInstances) {

View File

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