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 { ReacordDiscordJs } from "./reacord-discord-js"
|
||||||
export {
|
export {
|
||||||
type ReacordInstance,
|
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"
|
} 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user