On This Page

Read History and Use the Message Cache

Read recent messages with fetchHistory and iterateHistory, search the index and use locally cached copies

After the first reply bot works, read earlier messages from a channel, search them or keep recent ones in a local cache. Reading history is a request to Fluxer and does not need the gateway connection

Scan for One Message

Find the first recent message that matches a rule. Add this helper to a module that already has a bot Client

export async function findRecentMessage(client, channelId, matches) {
    for await (const item of client.messages.iterateHistory(channelId, { maxItems: 200 })) {
        if (item.isErr())
            return item
        if (matches(item.value))
            return item
    }
    return undefined
}
import type { Client, Message } from "@neontechspace/fluxerly"

export async function findRecentMessage(
    client: Client,
    channelId: string,
    matches: (message: Message) => boolean,
) {
    for await (const item of client.messages.iterateHistory(channelId, { maxItems: 200 })) {
        if (item.isErr()) return item
        if (matches(item.value)) return item
    }
    return undefined
}

The helper returns a Result with the matching message or the failure that stopped the read. It returns undefined if none of the 200 newest messages match

The iterateHistory method reads newest first and requests the next page only when the loop needs it, so stopping early saves requests. The required maxItems bounds the scan, so older messages can remain unread. To read a single page of up to 100 messages instead, use fetchHistory, as the last section shows

Read Pinned Messages

Use messages.fetchPins for one page or messages.iteratePins with maxItems for a bounded scan. Pins are ordered by pin time, not message creation time. Page ordering and cursor progress compare every fractional timestamp digit and normalize timezone offsets, so distinct pins less than a millisecond apart can advance the scan. Equal instants remain equal and can stall a multi-page scan. Preserve nextBefore unchanged in a manual loop and skip duplicate message IDs

Search the Message Index

When the text to look for is known, ask Fluxer's search index for one page of results. The index can lag behind the newest messages

export async function searchChannel(client, channelId, text) {
    const result = await client.messages.search({ channelId }, { content: text, limit: 10 })
    if (result.isErr())
        return result
    if (result.value.indexing)
        return undefined
    return result.value.messages
}
import type { Client } from "@neontechspace/fluxerly"

export async function searchChannel(client: Client, channelId: string, text: string) {
    const result = await client.messages.search({ channelId }, { content: text, limit: 10 })
    if (result.isErr()) return result
    if (result.value.indexing) return undefined
    return result.value.messages
}

The helper returns the matching messages, a failed Result, or undefined while Fluxer is still preparing its search index. It does not wait for the index or add results to the cache. To read the next page, increase page and keep limit unchanged

Search pages also include channel snapshots. An unnamed group DM has name: null, while another private channel may omit the name. The includeNsfw filter requests age-restricted channel results. In an age-restricted community, guild search includes those results for eligible callers, including bots, even when the filter is omitted or false. Channel access checks still apply

To search several pages, use iterateSearch with maxItems as the limit. Results can shift while the index changes during the scan

Limit the Message Cache

The message cache keeps recent messages in memory so they can be read without a request. It is off until configured. Create a client with a token supplied by the application

import { createClient } from "@neontechspace/fluxerly"

export function createCachedClient(token) {
    return createClient({
        token,
        cache: {
            messages: {
                maxEntries: 500,
                maxBytes: 2_000_000,
                maxAgeMs: 60_000,
            },
        },
    })
}
import { createClient } from "@neontechspace/fluxerly"

export function createCachedClient(token: string) {
    return createClient({
        token,
        cache: {
            messages: {
                maxEntries: 500,
                maxBytes: 2_000_000,
                maxAgeMs: 60_000,
            },
        },
    })
}

This client keeps up to 500 messages from sends, reads, history pages and gateway events, within about two megabytes and for at most one minute each. Without these settings, an enabled message cache keeps up to 1,000 messages in 8 MiB with no age limit. The same cache option works in runBot

A function as maxAgeMs chooses the age limit per message, such as maxAgeMs: (message) => (message.author.isBot ? 0 : 60_000) to keep only messages from people. Zero skips keeping a message, and null keeps it without an age limit. Every cache category accepts the same kind of function

To release cached data while the bot runs, call client.cache.clear("messages"), or client.cache.clear() for every category. The client.cache.delete(kind, key) method removes one entry, with keys such as channelId:messageId for messages. Neither changes anything on Fluxer

Why Can a Message Be Missing from the Cache?

The cache stores data only in memory. A message may be missing because it was removed to make room, expired or conflicted with another observation of the same message. A lost gateway connection also clears the messages cached before it, even when the connection recovers, because changes during the gap may have been missed. The entry and byte limits do not count runtime overhead or copies held elsewhere in the application. To react to cache changes, register a listener with client.cache.onChange, which reports each stored, deleted or cleared entry. A listener that returns a promise has at most 256 unfinished calls, and up to 256 more changes wait for them. Beyond that, the oldest waiting change is dropped and logged as cache.changesDropped. Shutdown does not wait for unfinished calls, so changes still waiting then are dropped and logged the same way. Pass { concurrency, maxPendingChanges } as the second argument to change these limits

Deleting a channel or a thread removes its cached messages. Deleting a text, announcement, forum or media channel also deletes its threads, and Fluxer sends no thread deletion for them. A message does not record which channel holds its thread, so the cache then also removes the messages of every channel in that community that the channel cache does not hold. Messages read through REST carry no community, so they count as possibly in that community. Without cache.channels, all of these messages go. With it, which also fills from each community's startup snapshot, messages in the channels and threads it still holds stay

See ClientOptions.cache and MessageCacheSettings for the complete cache contract

Use the Cache, Then Fetch if Needed

Use a cached message when a recent copy is good enough, and fetch it from Fluxer otherwise. Pass a MessageReference with both channelId and id

The received message.messageReference is a different type, MessageContextReference, whose id is optional. Check id before building the MessageReference. An announcement-channel follow notice identifies only a source channel, not a message to fetch

import { orThrow } from "@neontechspace/fluxerly"

export async function readCachedOrFetch(client, message) {
    const cached = client.messages.get(message)
    if (cached !== undefined)
        return cached

    return orThrow(await client.messages.fetch(message, { timeoutMs: 5_000 }))
}
import { orThrow, type Client, type MessageReference } from "@neontechspace/fluxerly"

export async function readCachedOrFetch(client: Client, message: MessageReference) {
    const cached = client.messages.get(message)
    if (cached !== undefined) return cached

    return orThrow(await client.messages.fetch(message, { timeoutMs: 5_000 }))
}

A cached copy returns immediately without a request. Otherwise the helper fetches the message with a five-second deadline, and orThrow throws if that fails

The get method returns undefined when the client has no usable copy. A cached copy is a frozen snapshot of the message as it was when observed, so use fetch when the task needs its current state

Check Whether the Limits Fit

Each category in client.diagnostics().caches keeps running totals since the client was created. The hits and misses values count lookups such as client.messages.get, including lookups the SDK makes itself. The evictions value counts entries the cache removed on its own, with capacity for room within maxEntries or maxBytes and expiry for entries older than maxAgeMs

export function messageCacheReport(client) {
    const { hits, misses, evictions } = client.diagnostics().caches.messages
    const lookups = hits + misses
    return {
        hitRate: lookups === 0 ? null : hits / lookups,
        removedForRoom: evictions.capacity,
        expired: evictions.expiry,
    }
}
import type { Client } from "@neontechspace/fluxerly"

export function messageCacheReport(client: Client) {
    const { hits, misses, evictions } = client.diagnostics().caches.messages
    const lookups = hits + misses
    return {
        hitRate: lookups === 0 ? null : hits / lookups,
        removedForRoom: evictions.capacity,
        expired: evictions.expiry,
    }
}

Frequent misses together with many capacity evictions suggest the limits are too small for the messages the bot reads again. Frequent misses with few evictions mean the bot looks up messages it never kept, so larger limits will not help. Counting costs one increment per lookup or eviction, so the totals are always on. A disabled category reports zero

Wire It Up

In a runBot bot, cache settings sit beside the token, and commands read history through the client in their context. This bot answers !recent with the people who spoke in the channel's 20 newest messages

import { runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    cache: { messages: { maxEntries: 500, maxAgeMs: 600_000 } },
    commands: {
        prefix: "!",
        commands: {
            recent: {
                description: "List recent speakers",
                execute: async ({ client, message, reply, signal }) => {
                    const page = await client.messages.fetchHistory(message.channelId, { limit: 20 }, { signal })
                    if (page.isErr())
                        return page
                    const people = page.value.filter((item) => !item.author.isBot)
                    const names = new Set(people.map((item) => item.author.username))
                    return reply(`Recent speakers: ${[...names].join(", ") || "none"}`)
                },
            },
        },
    },
})
import { runBot } from "@neontechspace/fluxerly"

await runBot({
    token: process.env.FLUXER_BOT_TOKEN,
    cache: { messages: { maxEntries: 500, maxAgeMs: 600_000 } },
    commands: {
        prefix: "!",
        commands: {
            recent: {
                description: "List recent speakers",
                execute: async ({ client, message, reply, signal }) => {
                    const page = await client.messages.fetchHistory(message.channelId, { limit: 20 }, { signal })
                    if (page.isErr()) return page
                    const people = page.value.filter((item) => !item.author.isBot)
                    const names = new Set(people.map((item) => item.author.username))
                    return reply(`Recent speakers: ${[...names].join(", ") || "none"}`)
                },
            },
        },
    },
})

Passing the command's signal stops the read when the bot shuts down. A failed read is returned from execute, so the router reports it with the command name. With the cache enabled, the page read can also store those messages for later client.messages.get lookups