# 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](https://fluxerly.neontechspace.com/docs/glossary.md#event), such as new messages, community joins and reactions, over the [gateway](https://fluxerly.neontechspace.com/docs/glossary.md#gateway) connection. A [handler](https://fluxerly.neontechspace.com/docs/glossary.md#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](https://fluxerly.neontechspace.com/docs/events-and-collectors.md#wire-it-up) shows

The helpers below take the `client` from the [first bot example](https://fluxerly.neontechspace.com/docs/quick-start.md). 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

```ts
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

<details>
<summary>How reliable is isNewJoin?</summary>

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

</details>

<details>
<summary>What else does a handler receive?</summary>

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`](https://fluxerly.neontechspace.com/docs/api/js-ts/events-and-collectors/#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](https://fluxerly.neontechspace.com/docs/events-and-collectors.md#log-edits-and-deletions) 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](https://fluxerly.neontechspace.com/docs/glossary.md#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`](https://fluxerly.neontechspace.com/docs/api/js-ts/events-and-collectors/#handleroptions-overflow) reference lists how the backlog to drop from is chosen

</details>

## 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

```ts
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`](https://fluxerly.neontechspace.com/docs/api/js-ts/events-and-collectors/#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

```ts
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](https://fluxerly.neontechspace.com/docs/threads.md). 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

```ts
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

<details>
<summary>What do thread events change in the cache?</summary>

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](https://fluxerly.neontechspace.com/docs/history-and-cache.md#limit-the-message-cache) removes a deleted thread's messages

</details>

## 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

```ts
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](https://fluxerly.neontechspace.com/docs/glossary.md#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

```ts
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

<details>
<summary>When does a collector fail?</summary>

A [gateway gap](https://fluxerly.neontechspace.com/docs/glossary.md#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

</details>

## 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

```ts
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](https://fluxerly.neontechspace.com/docs/glossary.md#shard). Do not assume a channel is private merely because an event omitted community context. See the [collector reference](https://fluxerly.neontechspace.com/docs/api/js-ts/messages/#messages-collectreactions) 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

```ts
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](https://fluxerly.neontechspace.com/docs/commands.md#add-help-from-the-registered-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`

```ts
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 `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](https://fluxerly.neontechspace.com/docs/api/js-ts/client-and-lifecycle/#botoptions-events) explains how to change that per event
