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:
27
packages/reacord/library.new/container.ts
Normal file
27
packages/reacord/library.new/container.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export type MessageTree = {
|
||||
children: TextNode[]
|
||||
render: () => void
|
||||
}
|
||||
|
||||
export type TextNode = {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
20
packages/reacord/library.new/node.ts
Normal file
20
packages/reacord/library.new/node.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
const channel =
|
||||
client.channels.cache.get(channelId) ??
|
||||
(await client.channels.fetch(channelId))
|
||||
private async getChannel() {
|
||||
if (this.channel) {
|
||||
return this.channel
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
throw new Error(`Channel ${channelId} not found`)
|
||||
const channel =
|
||||
this.client.channels.cache.get(this.channelId) ??
|
||||
(await this.client.channels.fetch(this.channelId))
|
||||
|
||||
if (!channel) {
|
||||
throw new Error(`Channel ${this.channelId} not found`)
|
||||
}
|
||||
if (!channel.isTextBased()) {
|
||||
throw new Error(`Channel ${this.channelId} is not a text channel`)
|
||||
}
|
||||
return (this.channel = channel)
|
||||
}
|
||||
if (!channel.isTextBased()) {
|
||||
throw new Error(`Channel ${channelId} is not a text channel`)
|
||||
}
|
||||
return channel
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
try {
|
||||
await options.update(tree)
|
||||
} catch (error) {
|
||||
console.error("Failed to update message.", error)
|
||||
}
|
||||
},
|
||||
create({ initialContent, renderer }: ReacordInstanceOptions) {
|
||||
const nodes = new Container<Node>()
|
||||
|
||||
const render = async () => {
|
||||
try {
|
||||
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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user