split stuff up + handle immediate renders
This commit is contained in:
@@ -4,29 +4,79 @@ import type {
|
|||||||
Message,
|
Message,
|
||||||
MessageEditOptions,
|
MessageEditOptions,
|
||||||
MessageOptions,
|
MessageOptions,
|
||||||
|
TextBasedChannel,
|
||||||
} from "discord.js"
|
} from "discord.js"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import ReactReconciler from "react-reconciler"
|
import type { ReacordOptions } from "./reacord"
|
||||||
import { DefaultEventPriority } from "react-reconciler/constants"
|
import { createReacordInstanceManager } from "./reacord"
|
||||||
|
|
||||||
export function createReacordDiscordJs(client: Client) {
|
export function createReacordDiscordJs(
|
||||||
|
client: Client,
|
||||||
|
options: ReacordOptions = {},
|
||||||
|
) {
|
||||||
|
const manager = createReacordInstanceManager(options)
|
||||||
return {
|
return {
|
||||||
send(channelId: string, initialContent?: ReactNode) {
|
send(channelId: string, initialContent?: ReactNode) {
|
||||||
let message: Message | undefined
|
const messageUpdater = createMessageUpdater()
|
||||||
|
return manager.createInstance(initialContent, async (tree) => {
|
||||||
const tree: MessageTree = {
|
|
||||||
children: [],
|
|
||||||
render: async () => {
|
|
||||||
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)
|
||||||
|
await messageUpdater.update(messageOptions, channel)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
try {
|
reply(interaction: Interaction, initialContent?: ReactNode) {},
|
||||||
if (message) {
|
|
||||||
await message.edit(messageOptions)
|
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {},
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMessageUpdater() {
|
||||||
|
type UpdatePayload = {
|
||||||
|
options: MessageOptions & MessageEditOptions
|
||||||
|
channel: TextBasedChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let message: Message | undefined
|
||||||
|
|
||||||
|
const queue: UpdatePayload[] = []
|
||||||
|
let queuePromise: Promise<void> | undefined
|
||||||
|
|
||||||
|
async function update(
|
||||||
|
options: MessageOptions & MessageEditOptions,
|
||||||
|
channel: TextBasedChannel,
|
||||||
|
) {
|
||||||
|
queue.push({ options, channel })
|
||||||
|
|
||||||
|
if (queuePromise) {
|
||||||
|
return queuePromise
|
||||||
|
}
|
||||||
|
|
||||||
|
queuePromise = runQueue()
|
||||||
|
try {
|
||||||
|
await queuePromise
|
||||||
|
} finally {
|
||||||
|
queuePromise = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runQueue() {
|
||||||
|
let payload: UpdatePayload | undefined
|
||||||
|
while ((payload = queue.shift())) {
|
||||||
|
if (message) {
|
||||||
|
await message.edit(payload.options)
|
||||||
|
} else {
|
||||||
|
message = await payload.channel.send(payload.options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { update }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTextChannel(client: Client<boolean>, channelId: string) {
|
||||||
let channel = client.channels.cache.get(channelId)
|
let channel = client.channels.cache.get(channelId)
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
channel = (await client.channels.fetch(channelId)) ?? undefined
|
channel = (await client.channels.fetch(channelId)) ?? undefined
|
||||||
@@ -37,175 +87,5 @@ export function createReacordDiscordJs(client: Client) {
|
|||||||
if (!channel.isTextBased()) {
|
if (!channel.isTextBased()) {
|
||||||
throw new Error(`Channel ${channelId} is not a text channel`)
|
throw new Error(`Channel ${channelId} is not a text channel`)
|
||||||
}
|
}
|
||||||
message = await channel.send(messageOptions)
|
return channel
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"Reacord encountered an error while rendering.",
|
|
||||||
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 = {
|
|
||||||
render(content: ReactNode) {
|
|
||||||
reconciler.updateContainer(content, container)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialContent !== undefined) {
|
|
||||||
instance.render(initialContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance
|
|
||||||
},
|
|
||||||
reply(interaction: Interaction, initialContent?: ReactNode) {},
|
|
||||||
ephemeralReply(interaction: Interaction, initialContent?: ReactNode) {},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageTree = {
|
|
||||||
children: TextNode[]
|
|
||||||
render: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextNode = {
|
|
||||||
type: "text"
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const reconciler = ReactReconciler<
|
|
||||||
string, // Type
|
|
||||||
Record<string, unknown>, // Props
|
|
||||||
MessageTree, // Container
|
|
||||||
never, // Instance
|
|
||||||
TextNode, // TextInstance
|
|
||||||
never, // SuspenseInstance
|
|
||||||
never, // HydratableInstance
|
|
||||||
never, // PublicInstance
|
|
||||||
{}, // HostContext
|
|
||||||
true, // UpdatePayload
|
|
||||||
never, // ChildSet
|
|
||||||
NodeJS.Timeout, // TimeoutHandle
|
|
||||||
-1 // NoTimeout
|
|
||||||
>({
|
|
||||||
isPrimaryRenderer: true,
|
|
||||||
supportsMutation: true,
|
|
||||||
supportsHydration: false,
|
|
||||||
supportsPersistence: false,
|
|
||||||
scheduleTimeout: setTimeout,
|
|
||||||
cancelTimeout: clearTimeout,
|
|
||||||
noTimeout: -1,
|
|
||||||
|
|
||||||
createInstance() {
|
|
||||||
throw new Error("Not implemented")
|
|
||||||
},
|
|
||||||
|
|
||||||
createTextInstance(text) {
|
|
||||||
return { type: "text", text }
|
|
||||||
},
|
|
||||||
|
|
||||||
appendInitialChild(parent, child) {},
|
|
||||||
|
|
||||||
appendChild(parentInstance, child) {},
|
|
||||||
|
|
||||||
appendChildToContainer(container, child) {
|
|
||||||
container.children.push(child)
|
|
||||||
},
|
|
||||||
|
|
||||||
insertBefore(parentInstance, child, beforeChild) {},
|
|
||||||
|
|
||||||
insertInContainerBefore(container, child, beforeChild) {
|
|
||||||
const index = container.children.indexOf(beforeChild)
|
|
||||||
if (index !== -1) container.children.splice(index, 0, child)
|
|
||||||
},
|
|
||||||
|
|
||||||
removeChild(parentInstance, child) {},
|
|
||||||
|
|
||||||
removeChildFromContainer(container, child) {
|
|
||||||
container.children = container.children.filter((c) => c !== child)
|
|
||||||
},
|
|
||||||
|
|
||||||
clearContainer(container) {
|
|
||||||
container.children = []
|
|
||||||
},
|
|
||||||
|
|
||||||
commitTextUpdate(textInstance, oldText, newText) {
|
|
||||||
textInstance.text = newText
|
|
||||||
},
|
|
||||||
|
|
||||||
commitUpdate(
|
|
||||||
instance,
|
|
||||||
updatePayload,
|
|
||||||
type,
|
|
||||||
prevProps,
|
|
||||||
nextProps,
|
|
||||||
internalHandle,
|
|
||||||
) {},
|
|
||||||
|
|
||||||
prepareForCommit() {
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
resetAfterCommit(container) {
|
|
||||||
container.render()
|
|
||||||
},
|
|
||||||
|
|
||||||
finalizeInitialChildren() {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
|
|
||||||
prepareUpdate() {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldSetTextContent() {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
|
|
||||||
getRootHostContext() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildHostContext() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
|
|
||||||
getPublicInstance() {
|
|
||||||
throw new Error("Refs are not supported")
|
|
||||||
},
|
|
||||||
|
|
||||||
preparePortalMount() {},
|
|
||||||
|
|
||||||
getCurrentEventPriority() {
|
|
||||||
return DefaultEventPriority
|
|
||||||
},
|
|
||||||
|
|
||||||
getInstanceFromNode() {
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeActiveInstanceBlur() {},
|
|
||||||
afterActiveInstanceBlur() {},
|
|
||||||
prepareScopeUpdate() {},
|
|
||||||
getInstanceFromScope() {
|
|
||||||
// eslint-disable-next-line unicorn/no-null
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
detachDeletedInstance() {},
|
|
||||||
})
|
|
||||||
|
|||||||
2
packages/reacord/library.new/main.ts
Normal file
2
packages/reacord/library.new/main.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { createReacordDiscordJs } from "./discord-js"
|
||||||
|
export { type ReacordInstance, type ReacordOptions } from "./reacord"
|
||||||
9
packages/reacord/library.new/message-tree.ts
Normal file
9
packages/reacord/library.new/message-tree.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export type MessageTree = {
|
||||||
|
children: TextNode[]
|
||||||
|
render: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextNode = {
|
||||||
|
type: "text"
|
||||||
|
text: string
|
||||||
|
}
|
||||||
91
packages/reacord/library.new/reacord.ts
Normal file
91
packages/reacord/library.new/reacord.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import type { ReactNode } from "react"
|
||||||
|
import type { MessageTree } from "./message-tree"
|
||||||
|
import { reconciler } from "./reconciler"
|
||||||
|
|
||||||
|
export type ReacordOptions = {
|
||||||
|
/**
|
||||||
|
* The max number of active instances.
|
||||||
|
* When this limit is exceeded, the oldest instances will be disabled.
|
||||||
|
*/
|
||||||
|
maxInstances?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReacordInstance = {
|
||||||
|
/** Render some JSX to this instance (edits the message) */
|
||||||
|
render: (content: ReactNode) => void
|
||||||
|
|
||||||
|
/** Remove this message */
|
||||||
|
destroy: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as destroy, but keeps the message and disables the components on it.
|
||||||
|
* This prevents it from listening to user interactions.
|
||||||
|
*/
|
||||||
|
deactivate: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReacordInstanceManager({
|
||||||
|
maxInstances = 50,
|
||||||
|
}: ReacordOptions) {
|
||||||
|
const instances: ReacordInstance[] = []
|
||||||
|
|
||||||
|
function createInstance(...args: Parameters<typeof createReacordInstance>) {
|
||||||
|
const instance = createReacordInstance(...args)
|
||||||
|
instances.push(instance)
|
||||||
|
|
||||||
|
if (instances.length > maxInstances) {
|
||||||
|
instances.shift()?.deactivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
return { createInstance }
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReacordInstance(
|
||||||
|
initialContent: ReactNode,
|
||||||
|
render: (tree: MessageTree) => unknown,
|
||||||
|
): ReacordInstance {
|
||||||
|
const tree: MessageTree = {
|
||||||
|
children: [],
|
||||||
|
render: async () => {
|
||||||
|
try {
|
||||||
|
await render(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)
|
||||||
|
},
|
||||||
|
destroy() {},
|
||||||
|
deactivate() {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialContent !== undefined) {
|
||||||
|
instance.render(initialContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
125
packages/reacord/library.new/reconciler.ts
Normal file
125
packages/reacord/library.new/reconciler.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import ReactReconciler from "react-reconciler"
|
||||||
|
import { DefaultEventPriority } from "react-reconciler/constants"
|
||||||
|
import type { MessageTree, TextNode } from "./message-tree"
|
||||||
|
|
||||||
|
export const reconciler = ReactReconciler<
|
||||||
|
string,
|
||||||
|
Record<string, unknown>,
|
||||||
|
MessageTree,
|
||||||
|
never,
|
||||||
|
TextNode,
|
||||||
|
never,
|
||||||
|
never,
|
||||||
|
never,
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
never,
|
||||||
|
NodeJS.Timeout,
|
||||||
|
-1 // NoTimeout
|
||||||
|
>({
|
||||||
|
isPrimaryRenderer: true,
|
||||||
|
supportsMutation: true,
|
||||||
|
supportsHydration: false,
|
||||||
|
supportsPersistence: false,
|
||||||
|
scheduleTimeout: setTimeout,
|
||||||
|
cancelTimeout: clearTimeout,
|
||||||
|
noTimeout: -1,
|
||||||
|
|
||||||
|
createInstance() {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
},
|
||||||
|
|
||||||
|
createTextInstance(text) {
|
||||||
|
return { type: "text", text }
|
||||||
|
},
|
||||||
|
|
||||||
|
appendInitialChild(parent, child) {},
|
||||||
|
|
||||||
|
appendChild(parentInstance, child) {},
|
||||||
|
|
||||||
|
appendChildToContainer(container, child) {
|
||||||
|
container.children.push(child)
|
||||||
|
},
|
||||||
|
|
||||||
|
insertBefore(parentInstance, child, beforeChild) {},
|
||||||
|
|
||||||
|
insertInContainerBefore(container, child, beforeChild) {
|
||||||
|
const index = container.children.indexOf(beforeChild)
|
||||||
|
if (index !== -1) container.children.splice(index, 0, child)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeChild(parentInstance, child) {},
|
||||||
|
|
||||||
|
removeChildFromContainer(container, child) {
|
||||||
|
container.children = container.children.filter((c) => c !== child)
|
||||||
|
},
|
||||||
|
|
||||||
|
clearContainer(container) {
|
||||||
|
container.children = []
|
||||||
|
},
|
||||||
|
|
||||||
|
commitTextUpdate(textInstance, oldText, newText) {
|
||||||
|
textInstance.text = newText
|
||||||
|
},
|
||||||
|
|
||||||
|
commitUpdate(
|
||||||
|
instance,
|
||||||
|
updatePayload,
|
||||||
|
type,
|
||||||
|
prevProps,
|
||||||
|
nextProps,
|
||||||
|
internalHandle,
|
||||||
|
) {},
|
||||||
|
|
||||||
|
prepareForCommit() {
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
resetAfterCommit(container) {
|
||||||
|
container.render()
|
||||||
|
},
|
||||||
|
|
||||||
|
finalizeInitialChildren() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareUpdate() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldSetTextContent() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
getRootHostContext() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildHostContext() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPublicInstance() {
|
||||||
|
throw new Error("Refs are not supported")
|
||||||
|
},
|
||||||
|
|
||||||
|
preparePortalMount() {},
|
||||||
|
|
||||||
|
getCurrentEventPriority() {
|
||||||
|
return DefaultEventPriority
|
||||||
|
},
|
||||||
|
|
||||||
|
getInstanceFromNode() {
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeActiveInstanceBlur() {},
|
||||||
|
afterActiveInstanceBlur() {},
|
||||||
|
prepareScopeUpdate() {},
|
||||||
|
getInstanceFromScope() {
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
detachDeletedInstance() {},
|
||||||
|
})
|
||||||
@@ -52,3 +52,9 @@ await createTest("basic", (channel) => {
|
|||||||
|
|
||||||
reacord.send(channel.id, <Timer />)
|
reacord.send(channel.id, <Timer />)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await createTest("immediate renders", async (channel) => {
|
||||||
|
const instance = reacord.send(channel.id)
|
||||||
|
instance.render("hi world")
|
||||||
|
instance.render("hi moon")
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user