On This Page

React to Events and Collect Replies

Handle gateway events, detect community joins, follow threads, wait for one event and collect replies or reactions for a limited time

Fluxer sends live events, such as new messages, community joins and reactions, over the gateway connection. A handler registered for an event name runs each time that event arrives. In a runBot bot, handlers go in the events option, as the last section shows

The helpers below take the client from the first bot example. Register handlers before connecting so they also receive startup events. Unlike sending or fetching messages, waiting for future events needs a connected gateway

Distinguish a Join from Community Availability

The guildCreate event arrives when the bot joins a community, and also when a community's data becomes available at startup or after an outage. Check isNewJoin when a handler should react only to a new join

export function observeGuildJoins(client, rememberGuild) {
    return client.on("guildCreate", (guild) => {
        if (!guild.isNewJoin)
            return
        rememberGuild(guild.id)
    })
}
import type { Client } from "@neontechspace/fluxerly"

export function observeGuildJoins(client: Client, rememberGuild: (id: string) => void) {
    return client.on("guildCreate", (guild) => {
        if (!guild.isNewJoin) return
        rememberGuild(guild.id)
    })
}

The client.on method returns a subscription. Call its close() method when the handler is no longer needed

The event can also carry memberCount, onlineCount and joinedAt, as Fluxer reported them when it delivered the event. They are a snapshot that neither guildUpdate nor the guild cache refreshes, so call guilds.fetchCounts for current totals. Here onlineCount is the guild-wide figure every recipient receives, while guilds.fetchCounts counts only the online members the bot can see, so the two can differ

How Reliable Is isNewJoin?

The isNewJoin flag requires a Fluxer gateway that distinguishes new joins from startup community data. It does not guess from timing, readiness or an empty cache, and communities the bot already belonged to when it connected are not reported as new joins. A replayed event can run the handler again, so check whether a welcome or onboarding action already ran before repeating it

A successful connect() means Fluxer accepted the session, not that every community or resource has finished arriving

What Else Does a Handler Receive?

A client.on handler receives the event, an AbortSignal that is aborted when the subscription closes or the client shuts down, and an event context. The context's shardId names the shard that received the event, sessionEpoch is that shard's session when the event arrived, receipt numbers events in the order the client received them, and receivedBytes is the size of the gateway message that carried it. Compare receipts to tell which of two events came first, as EventContext.receipt describes. Work that is not an event, such as a timer, can read client.lastReceipt when it starts and treat any event with a larger receipt as newer. For a change or removal, the context's previous holds the cached value from before it, as the next section shows Pass the signal to SDK operations inside the handler so they stop with it. To run code around every handler, such as timing or tracing, register event middleware with client.use

A handler runs one event at a time by default, except a messageCreate handler, which runs up to eight at a time so that one slow message does not stall the next ones. Those messages can finish out of order, and { concurrency: 1 } or a partition keeps related messages in order. Later events wait in a queue of up to 256 events. When the queue is full, the oldest waiting event is dropped and a Warn record is logged, while the handler keeps running. Pass { overflow: "stop" } as the third argument of client.on to close the subscription instead

To handle several events at a time and still keep related events in order, pass a partition option. The value "channel" keys each event by its channel, "guild" by its community, and a function returns a key of its own. Events with different keys run side by side, up to eight at a time unless concurrency says otherwise, while events with the same key run one after another in the order they arrived. When the queue is full, a partitioned handler drops from the longest backlog instead of the oldest event overall. The overflow reference lists how the backlog to drop from is chosen

Log Edits and Deletions

Update and delete events carry only the new state or the removed IDs. When the matching cache is enabled, the handler's context also holds previous, the value that cache held just before the change, so a log can show what changed. Enable the message cache for message edits and deletions, the member and role caches for member and role changes, the channel cache for channels and threads, and the guild cache for community changes

Fluxer sends only IDs for a bulk deletion, such as the messages a ban removes, so its previous is the only source of their content and authors. It lists the messages the cache held and leaves out the rest, so compare it with the event's ids to see how many are unknown

Writes through the SDK's resource methods, such as messages.edit, update these caches too. After an edit by the bot, the event that follows can hold the value the edit stored, or no previous at all where the write removed the entry, such as a member after a role change. A log that must show the value from before the bot's own edits has to keep that value itself

// Create the client with createClient({ token, cache: { messages: true } })
export function logMessageChanges(client, log) {
    const edits = client.on("messageUpdate", (message, _signal, { previous }) => {
        if (previous !== undefined && previous.content !== message.content)
            log(`Message ${message.id} was edited from "${previous.content}" to "${message.content}"`)
    })
    const deletions = client.on("messageDelete", (deletion, _signal, { previous }) => {
        log(`Message ${deletion.id} was deleted${previous === undefined ? "" : `: "${previous.content}"`}`)
    })
    const purges = client.on("messageDeleteBulk", (batch, _signal, { previous }) => {
        log(`${batch.ids.length} messages were deleted, ${previous?.length ?? 0} of them cached`)
    })
    return [edits, deletions, purges]
}
import type { Client } from "@neontechspace/fluxerly"

// Create the client with createClient({ token, cache: { messages: true } })
export function logMessageChanges(client: Client, log: (line: string) => void) {
    const edits = client.on("messageUpdate", (message, _signal, { previous }) => {
        if (previous !== undefined && previous.content !== message.content)
            log(`Message ${message.id} was edited from "${previous.content}" to "${message.content}"`)
    })
    const deletions = client.on("messageDelete", (deletion, _signal, { previous }) => {
        log(`Message ${deletion.id} was deleted${previous === undefined ? "" : `: "${previous.content}"`}`)
    })
    const purges = client.on("messageDeleteBulk", (batch, _signal, { previous }) => {
        log(`${batch.ids.length} messages were deleted, ${previous?.length ?? 0} of them cached`)
    })
    return [edits, deletions, purges]
}

The snapshot is the client's own last observation, never fetched. It is undefined when the cache is off, never saw the entity, or already removed it, such as after an expiry, a capacity limit or a lost connection. The PreviousSnapshots reference lists the events that carry one

Wait for a Community's Setup

A partitioned handler's key can need its own setup before its events run, such as a community that is still loading its data after a restart. Waiting inside the handler holds one of the shared slots, so starting communities would stall every other one. The partitionReady option moves that wait out of the handler: The SDK calls it with the key before the first waiting event of that key starts, and the key's events stay in the queue, using no slot, until the returned promise settles. Events of other keys keep running meanwhile

export function afterServerSetup(client, setups) {
    return client.on("messageCreate", (message) => console.log(message.content), {
        partition: "guild",
        // A direct message has no community and is keyed by its channel, so an unknown key counts as ready
        partitionReady: (key) => setups.get(key),
    })
}
import type { Client } from "@neontechspace/fluxerly"

export function afterServerSetup(client: Client, setups: Map<string, Promise<void>>) {
    return client.on("messageCreate", (message) => console.log(message.content), {
        partition: "guild",
        // A direct message has no community and is keyed by its channel, so an unknown key counts as ready
        partitionReady: (key) => setups.get(key),
    })
}

The hook is valid only together with partition, and in the Effect entry point it returns an Effect. A promise that never settles holds the key until the subscription closes, and a draining shutdown waits for held events until its drainMs passes. The SDK remembers that a key is ready only while it has waiting or running events, so the hook is called again for the next burst after the key went idle. Hooks for different keys run at the same time, so keep the hook a cheap check that answers at once when the key is ready. A hook that throws or rejects reports every waiting event of that key as a failure of its handler, and those handlers do not run. Held events count toward maxPendingMessages, 256 by default, so a bot that waits for many starting communities needs a larger limit

Follow Threads

Five events report threads. The threadCreate event arrives when a thread is created and also when the bot joins or is added to an existing thread, so check isNewlyCreated to react only to new threads

export function welcomeNewThreads(client) {
    return client.on("threadCreate", (thread) => thread.isNewlyCreated ? client.messages.send(thread.id, `Welcome to ${thread.name}`) : undefined)
}
import type { Client } from "@neontechspace/fluxerly"

export function welcomeNewThreads(client: Client) {
    return client.on("threadCreate", (thread) =>
        thread.isNewlyCreated ? client.messages.send(thread.id, `Welcome to ${thread.name}`) : undefined,
    )
}

The other four are threadUpdate, with the thread after a change such as a rename, archive or lock, threadDelete, with the deleted thread's ID and parent, threadListSync, with the bot's active threads after Fluxer replaced them for a community or some of its channels, and threadMembersUpdate, with the accounts that joined or left a thread

Deleting a text, announcement, forum or media channel also deletes its threads, and Fluxer sends no threadDelete for them. To notice that, handle guildChannelDelete and treat every thread whose parentId is the deleted channel as gone

What Do Thread Events Change in the Cache?

With cache.channels enabled, thread create and update events store the thread, a thread deletion removes it and a thread list replaces the cached threads it covers. A community's snapshot at startup also fills the cache with its active threads. When the bot's own membership of a thread changes, the SDK removes that cached thread, because its membership field would be out of date. Fluxer does not replay thread lists after a lost connection, so a resumed connection also removes its communities' cached threads. The message cache removes a deleted thread's messages

Wait for One Future Event

Use waitFor to get the next matching event. The filter runs synchronously, so keep it small and do not make requests inside it

import { describeError } from "@neontechspace/fluxerly"

export async function waitForTyping(client, channelId, userId) {
    const result = await client.waitFor("typingStart", {
        filter: (event) => event.channelId === channelId && event.userId === userId,
        timeoutMs: 10_000,
    })
    if (result.isErr()) {
        console.error(describeError(result.error))
        return
    }
    return result.value
}
import { describeError, type Client } from "@neontechspace/fluxerly"

export async function waitForTyping(client: Client, channelId: string, userId: string) {
    const result = await client.waitFor("typingStart", {
        filter: (event) => event.channelId === channelId && event.userId === userId,
        timeoutMs: 10_000,
    })
    if (result.isErr()) {
        console.error(describeError(result.error))
        return
    }
    return result.value
}

If no matching event arrives within 10 seconds, waitFor returns a failed Result. Only a future event counts, not one that already happened. To send a prompt and then wait for the answer, use a collector as shown next, because it starts listening before the prompt is sent

Ask a Question Without Missing a Fast Reply

A collector gathers matching messages or reactions for a limited time. Create the collector first, then send the question. This helper collects one person's next message in the channel, waiting at most 30 seconds

import { orThrow } from "@neontechspace/fluxerly"

export async function askPreferredName(client, channelId, userId) {
    await using collector = client.messages.collect(channelId, {
        filter: (message) => message.author.id === userId && !message.author.isBot,
        maxMessages: 1,
        timeoutMs: 30_000,
    })
    orThrow(await client.messages.send(channelId, "What name should the bot use?"))

    const answer = orThrow(await collector.result())
    return answer.messages[0]?.content
}
import { orThrow, type Client } from "@neontechspace/fluxerly"

export async function askPreferredName(client: Client, channelId: string, userId: string) {
    await using collector = client.messages.collect(channelId, {
        filter: (message) => message.author.id === userId && !message.author.isBot,
        maxMessages: 1,
        timeoutMs: 30_000,
    })
    orThrow(await client.messages.send(channelId, "What name should the bot use?"))

    const answer = orThrow(await collector.result())
    return answer.messages[0]?.content
}

The helper returns the reply's text, or undefined when no one answers in time, because a collector that times out still succeeds, possibly with no messages. The orThrow helper returns a successful value or throws the failure

The await using declaration closes the collector when the helper returns or throws, including when sending fails, and waits for its cleanup

When Does a Collector Fail?

A gateway gap, such as a lost connection, fails collection because messages may have been missed, and so does a client that shuts down while the collector runs. A per-message or per-reaction callback that throws fails collection with a CollectorError whose reason is handler. Do not shut the client down or await the same collector's result() from inside its own callbacks

Accept a Confirmation Reaction

Use an existing bot message as the target. Start this helper before inviting the person to react, and keep the client connected

import { orThrow } from "@neontechspace/fluxerly"

export async function waitForConfirmation(client, message, userId) {
    await using collector = client.messages.collectReactions(message, {
        emoji: "✅",
        filter: (reaction) => reaction.userId === userId,
        maxReactions: 1,
        timeoutMs: 20_000,
    })
    const result = orThrow(await collector.result())
    return result.reactions.length > 0
}
import { orThrow, type Client, type MessageReference } from "@neontechspace/fluxerly"

export async function waitForConfirmation(client: Client, message: MessageReference, userId: string) {
    await using collector = client.messages.collectReactions(message, {
        emoji: "✅",
        filter: (reaction) => reaction.userId === userId,
        maxReactions: 1,
        timeoutMs: 20_000,
    })
    const result = orThrow(await collector.result())
    return result.reactions.length > 0
}

The helper returns true when the person adds ✅ within 20 seconds. It watches for new reactions, not reactions already on the message. Removing a reaction does not undo a collected one, and repeated additions are not a count of unique voters, so keep a poll's votes in application state

For a community channel, pass its known guildId in the collector options so recovery is limited to that community's shard. Do not assume a channel is private merely because an event omitted community context. See the collector reference for buffer limits and subscription ownership

Show Pages with Reactions

Fluxer has no buttons, so a long list can be shown as pages that people flip with ◀ and ▶ reactions. The messages.paginate method sends the first page, adds both arrows as the bot and shows the previous or next page on each click, wrapping around at the ends. Pass users to choose who may flip, such as [userId], or () => true for anyone

export function showRules(client, channelId, userId) {
    return client.messages.paginate(channelId, ["Rules 1 to 5", "Rules 6 to 10"], { users: [userId] })
}
import type { Client } from "@neontechspace/fluxerly"

export function showRules(client: Client, channelId: string, userId: string) {
    return client.messages.paginate(channelId, ["Rules 1 to 5", "Rules 6 to 10"], { users: [userId] })
}

With several pages, the returned Result settles when listening ends, after idleMs without a click, 60 seconds by default, or timeoutMs after the first page, 5 minutes by default. The bot then removes its own arrows, and the current page stays shown. A page is text or an object with content and embeds. Adding or removing an arrow turns the page, so the bot needs only Add Reactions and Read Message History. With removeClicks: true, only additions count and the bot removes each click, which needs Manage Messages. Several pages need a connected gateway, while a single page is sent without a collector

In a command, the context's paginate(pages) lets the command's author flip by default, and sendHelp() sends the help pages this way. See help in Commands

Wire It Up

In a runBot bot, event handlers go in events and receive the event and the client. A collector can run inside a command, which provides the client and the incoming message. This bot logs new community joins and answers !ask

import { orThrow, runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    onError: (report) => console.error(report.describe()),
    events: {
        guildCreate: ({ event }) => {
            if (event.isNewJoin)
                console.info(`Joined guild ${event.id}`)
        },
    },
    commands: {
        prefix: "!",
        commands: {
            ask: {
                description: "Ask for a preferred name",
import { orThrow, runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    onError: (report) => console.error(report.describe()),
    events: {
        guildCreate: ({ event }) => {
            if (event.isNewJoin) console.info(`Joined guild ${event.id}`)
        },
    },
    commands: {
        prefix: "!",
        commands: {
            ask: {
                description: "Ask for a preferred name",
                execute: async ({ client, message, reply }) => {

A failing handler or command does not stop the bot. The onError hook receives its report with the original error, and without the hook the SDK logs it in full at Error. By default, messageCreate handlers and commands run up to eight at a time and other events one at a time. The events option reference explains how to change that per event