# 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](https://fluxerly.neontechspace.com/docs/quick-start.md) works, read earlier messages from a channel, search them or keep recent ones in a local [cache](https://fluxerly.neontechspace.com/docs/glossary.md#cache). Reading history is a request to Fluxer and does not need the [gateway](https://fluxerly.neontechspace.com/docs/glossary.md#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`](https://fluxerly.neontechspace.com/docs/api/js-ts/client/)

```ts
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](https://fluxerly.neontechspace.com/docs/glossary.md#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`](https://fluxerly.neontechspace.com/docs/api/js-ts/messages/#messages-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`](https://fluxerly.neontechspace.com/docs/api/js-ts/messages/#messages-fetchhistory), as the [last section](https://fluxerly.neontechspace.com/docs/history-and-cache.md#wire-it-up) 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

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

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

<details>
<summary>Why can a message be missing from the cache?</summary>

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](https://fluxerly.neontechspace.com/docs/threads.md) 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`](https://fluxerly.neontechspace.com/docs/api/js-ts/options/#clientoptions-cache) and [`MessageCacheSettings`](https://fluxerly.neontechspace.com/docs/api/js-ts/caching/#messagecachesettings) for the complete cache contract

</details>

## 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](https://fluxerly.neontechspace.com/docs/announcement-channels.md#find-where-a-copy-came-from) identifies only a source channel, not a message to fetch

```ts
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](https://fluxerly.neontechspace.com/docs/api/js-ts/messages/#messages-fetch) the message with a five-second deadline, and `orThrow` throws if that fails

The [`get`](https://fluxerly.neontechspace.com/docs/api/js-ts/messages/#messages-get) method returns `undefined` when the client has no usable copy. A cached copy is a [frozen snapshot](https://fluxerly.neontechspace.com/docs/glossary.md#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`

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

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