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 run 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 guild event arrives when the bot joins a community, and also when a community's data becomes available at startup or after an outage. Check is 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 method returns a subscription. Call its close() method when the handler is no longer needed
The event can also carry member, online and joined, as Fluxer reported them when it delivered the event. They are a snapshot that neither guild nor the guild cache refreshes, so call guilds for current totals. Here online is the guild-wide figure every recipient receives, while guilds counts only the online members the bot can see, so the two can differ
How Reliable Is isNewJoin?
The is 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 handler receives the event, an Abort that is aborted when the subscription closes or the client shuts down, and an event context. The context's shard names the shard that received the event, session is that shard's session when the event arrived, receipt numbers events in the order the client received them, and received is the size of the gateway message that carried it. Compare receipts to tell which of two events came first, as Event describes. Work that is not an event, such as a timer, can read client 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
A handler runs one event at a time by default, except a message 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 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, 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 Previous 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 partition 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 drain 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 max, 256 by default, so a bot that waits for many starting communities needs a larger limit
Follow Threads
Five events report threads. The thread event arrives when a thread is created and also when the bot joins or is added to an existing thread, so check is 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 thread, with the thread after a change such as a rename, archive or lock, thread, with the deleted thread's ID and parent, thread, with the bot's active threads after Fluxer replaced them for a community or some of its channels, and thread, 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 thread for them. To notice that, handle guild and treat every thread whose parent is the deleted channel as gone
What Do Thread Events Change in the Cache?
With cache 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 wait 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, wait 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 or 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 Collector 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 guild 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 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 [user, 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 idle without a click, 60 seconds by default, or timeout 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 remove, 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( lets the command's author flip by default, and send sends the help pages this way. See help in Commands
Wire It Up
In a run 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",
execute: async ({ client, message, reply }) => {
await using collector = client.messages.collect(message.channelId, {
filter: (answer) => answer.author.id === message.author.id,
maxMessages: 1,
timeoutMs: 30_000,
})
orThrow(await reply("What name should the bot use?"))
const answer = orThrow(await collector.result()).messages[0]
return reply(answer ? `Hello, ${answer.content}!` : "No answer in time")
},
},
},
},
})
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 }) => {
await using collector = client.messages.collect(message.channelId, {
filter: (answer) => answer.author.id === message.author.id,
maxMessages: 1,
timeoutMs: 30_000,
})
orThrow(await reply("What name should the bot use?"))
const answer = orThrow(await collector.result()).messages[0]
return reply(answer ? `Hello, ${answer.content}!` : "No answer in time")
},
},
},
},
})
A failing handler or command does not stop the bot. The on hook receives its report with the original error, and without the hook the SDK logs it in full at Error. By default, message 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