> ## Documentation Index
> Fetch the complete documentation index at: https://stars-components.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# @wolfstar/plugin-gateway

> Add Discord gateway events, cached structures, and API managers to an HTTP Framework bot.

```bash npm theme={"system"}
npm install @wolfstar/plugin-gateway @wolfstar/plugin-cache
```

```bash pnpm theme={"system"}
pnpm add @wolfstar/plugin-gateway @wolfstar/plugin-cache
```

```bash yarn theme={"system"}
yarn add @wolfstar/plugin-gateway @wolfstar/plugin-cache
```

```bash bun theme={"system"}
bun add @wolfstar/plugin-gateway @wolfstar/plugin-cache
```

> [card] [npm](https://npmx.dev/package/@wolfstar/plugin-gateway)

> [card] [Source](https://github.com/wolfstar-project/plugins/tree/main/packages/plugin-gateway)

> [!TIP]
>
> See the [Plugins guide](https://stars-components.js.org/documentation/guide/plugins.md) for how plugins from wolfstar-project/plugins fit alongside the core framework.

## Description

`@wolfstar/plugin-gateway` extends the [`@wolfstar/http-framework`](https://stars-components.js.org/documentation/packages/http-framework) client with
Discord gateway events and API access. `GatewayClient` is a subclass of the framework's `Client`. Commands and HTTP
interactions keep working exactly as before, and the same process also receives gateway dispatches such as
`messageCreate` or `guildMemberAdd`.

The package connects shards with [`@discordjs/ws`](https://www.npmjs.com/package/@discordjs/ws), which handles
sharding, reconnects, and session resumes. Actions turn dispatches into events carrying structures such as `Message`,
`User`, and `Guild`. Managers such as `client.users` and `client.guilds` expose a discord.js-style `manager.cache`,
in memory by default or backed by [`@wolfstar/plugin-cache`](https://npmx.dev/package/@wolfstar/plugin-cache), and
fetch missing data through `@discordjs/core`, which is also available as `client.api`.

> [!NOTE]
>
> A gateway connection is long-lived. A `GatewayClient` needs a persistent process, unlike a bot that only serves HTTP interactions.

## Requirements

* Node.js `>=24.17.0`. The package builds on `@discordjs/structures`, which is only published as `dev` snapshots that
  require Node.js 24.17.
* `@wolfstar/http-framework` `^3.4.0` or `^5.0.0` as a peer dependency.
* `@wolfstar/plugin-cache` if you want to cache Discord entities in Redis or resume sessions across restarts.

## No side-effect registration

Unlike most plugins, `@wolfstar/plugin-gateway` is a library. Its `@wolfstar/plugin-gateway/register` entry point exists
only so the [Stars CLI](https://stars-components.js.org/documentation/packages/cli.md) build can import it like every other plugin, and it does nothing.
You opt in to the gateway by constructing a `GatewayClient` instead of the framework's `Client`.

## Stars module

On framework 6.1 and later, list the `./module` entry in `modules` in `stars.config` to add the package to the auto
imports. It needs the optional [`@wolfstar/kit`](https://stars-components.js.org/documentation/packages/kit) peer dependency, at `^0.2.0 || ^0.3.0`.

```ts theme={"system"}
// stars.config.ts
export default defineConfig({
	modules: ['@wolfstar/plugin-gateway/module']
});
```

The module registers no plugin and takes no options. It reserves the `gateway` key of `stars.config` for future options,
so no other module can claim it. You still construct the `GatewayClient` yourself.

## Usage

Create a `GatewayClient` where you would create the framework's `Client`, then call `start()`. It loads your pieces,
starts the HTTP interaction endpoint, and connects the gateway shards in one call:

```ts theme={"system"}
import { GatewayClient } from '@wolfstar/plugin-gateway';
import { GatewayIntentBits } from 'discord-api-types/v10';

const client = new GatewayClient({
	intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent
});

client.on('messageCreate', (message) => {
	const guild = message.guildId ? client.guilds.cache.get(message.guildId) : undefined;
	console.log(`${message.author.username} in ${guild?.name ?? 'a DM'}: ${message.content}`);
});

await client.start({ listen: { port: 8080 } });
```

To control each step yourself, call `load()`, `listen()`, and `connect()` separately. `client.gateway` exposes the
underlying `WebSocketManager`, for example to send presence updates.

### Options

`GatewayClient` accepts every `Client` option, plus:

| Option | Default | Description |
| - | - | - |
| `intents` | None | The gateway intents. |
| `cacheConstructor` | `CollectionCache` | The class that builds each `manager.cache`. See [Caching](#caching). |
| `cacheOptions` | `undefined` | Per-entity cache options such as `{ messages: { maxSize: 1_000 } }`. See [Bounding memory](#bounding-memory). |
| `sweepers` | `undefined` | Evicts cache entries on a timer, for example `DefaultSweeperSettings`. See [Sweepers](#sweepers). |
| `cache` | `undefined` | A `@wolfstar/plugin-cache` cache whose raw stores back the managers, or `null` to cache nothing. See [Caching](#caching). |
| `makeCache` | `undefined` | A function returning a store per entity kind, or `null` not to cache it. Takes precedence over `cache`. |
| `policies` | `undefined` | Per-entity `filter` and `ttl` rules applied to dispatches and managers. |
| `cacheErrors` | `"miss"` | `"miss"` treats a failing store as a cache miss reported through `cacheError`. `"throw"` rejects instead. |
| `shardCount` | `null` | Total shards across every process. `null` uses Discord's recommendation. |
| `shardIds` | `null` | The shards this client runs, as an array or a `{ start, end }` range. `null` runs all of them. |
| `gateway` | `{}` | Extra `@discordjs/ws` `WebSocketManager` options, such as `compression` or `initialPresence`. |
| `cacheFailure` | `"skip"` | On a cache read or write failure, `"skip"` drops the event and `"emitUncached"` emits it from the payload. |
| `dispatchTimeout` | `30_000` | Milliseconds after which a dispatch still processing is reported as a `DispatchTimeoutError`. `null` disables it. |
| `sessionStore` | `undefined` | A `GatewaySessionStore` that keeps shard sessions across restarts. See [Resuming sessions](#resuming-sessions-across-restarts). |
| `sessionStoreTimeout` | `5_000` | Milliseconds a shard waits for `sessionStore` to read its session before identifying. `null` waits forever. |
| `partials` | `[]` | The structures to build partially for uncached entities, such as `Partials.Message` or `Partials.User`. |
| `waitGuildTimeout` | `15_000` | Milliseconds `clientReady` waits for initially unavailable guilds before emitting anyway. |

### Access the client from pieces

`GatewayClient` registers itself as `container.gatewayClient` when you construct it, next to the framework's
`container.client`. `container.gatewayClient` is typed as `GatewayClient`, so any listener, command, or interaction
handler can use its managers without a cast:

```ts theme={"system"}
import { Command } from '@wolfstar/http-framework';

export class GuildNameCommand extends Command {
	public override async chatInputRun(interaction: Command.ChatInputInteraction) {
		const guild = await this.container.gatewayClient.guilds.fetch(interaction.guildId!);
		return interaction.reply({ content: guild.name });
	}
}
```

`container.client` stays typed as the base `Client`, because a module augmentation cannot redeclare it with another
type. Outside a piece, `getGatewayClient()` returns the same client.

## Events

`GatewayClient` emits gateway events on the client itself. The `GatewayEvents` enum lists every event name. Each member's
value is the plain event name, like the framework's own `Events`, so it is interchangeable with the string literal:

```ts theme={"system"}
import { GatewayEvents } from '@wolfstar/plugin-gateway';

client.on(GatewayEvents.MessageCreate, (message) => console.log(message.content));
```

Update events receive the previous state from the cache, and delete events receive the cached entity. Both are `null`
when the entity was not cached. Delete events also receive `data`, the raw dispatch data, which always identifies the
deleted entity. Every dispatch is also emitted as `raw`, with the payload and shard ID.

`entitlementCreate` and `entitlementDelete` receive the `Entitlement`. `entitlementUpdate` receives the previous
entitlement, or `null`, and the new one. See [Monetization](#monetization).

### Replaying dispatches on another process

A client that never connects to Discord can emit the events of a connected client that shares its cache. The connected
client emits `dispatch` after writing to the cache, with the payload, the shard ID, and the previous `state` its handler
read. `client.serializeDispatchState(type, state)` turns that state into JSON-safe data, and
`client.reviveDispatchState(type, serialized, data)` rebuilds it on the receiving client.
`client.replayDispatch(payload, shardId, state)` then emits `raw` and the matching event without touching the cache:

```ts theme={"system"}
connected.on('dispatch', async (payload, shardId, state) => {
	await transport.send({ payload, shardId, state: connected.serializeDispatchState(payload.t, state) });
});

transport.receive(async ({ payload, shardId, state }) => {
	const revived = await worker.reviveDispatchState(payload.t, state, payload.d);
	await worker.replayDispatch(payload, shardId, revived);
});
```

`READY` and `INTERACTION_CREATE` are not replayed, so `client.user` stays `null` and `client.application` stays partial
until you call `client.application.fetch()`. `client.replayDispatchTypes` lists the types `replayDispatch` handles.

On `READY`, the client drops the cached guilds of that shard which `READY` no longer lists, and emits `guildDelete` for
each. These are guilds the bot left while disconnected, or while the process was down with a persistent cache. Discord
does not replay those removals. Each drop also emits `dispatch` as a `GUILD_DELETE` (with `s: 0` and no `raw`), so a
process replaying this client's dispatches sees these deletions too. This is best effort: a failure, such as an
unreachable cache or an unknown shard count, is reported through `error`, and the remaining guilds are kept.

### The `clientReady` event

`clientReady` is emitted once, like discord.js's `Client#clientReady`. It fires after every shard this client manages has
connected and every guild that `READY` listed as unavailable became available. If those guilds take longer than
`waitGuildTimeout` (15 seconds by default), `clientReady` fires anyway. The timeout only bounds the wait on guilds, never
the wait on shards connecting.

```ts theme={"system"}
client.on(GatewayEvents.ClientReady, (client) => console.log(`Logged in as ${client.user!.username}`));
```

After the fact, `client.isClientReady()` returns whether the event fired, and `client.clientReadyAt` holds the `Date`
it fired at, or `null`.

### Dispatch ordering

`GatewayClient` processes dispatches of the same guild, or the same direct message channel, in order, so an asynchronous
cache never reorders them. Different guilds proceed concurrently, so a slow guild does not hold the others back.
Dispatches that belong to no guild, such as `READY` or `USER_UPDATE`, wait for everything queued before them on their
shard. `client.queueStats` reports the pending dispatches.

### Listeners

`EventGatewayListener` is a listener piece for gateway events. It pins the emitter to the client and types `run` after
the event. Place it in your `listeners` directory:

```ts theme={"system"}
// src/listeners/log-messages.ts
import { EventGatewayListener, type Message } from '@wolfstar/plugin-gateway';

export class LogMessagesListener extends EventGatewayListener<'messageCreate'> {
	public constructor(context: EventGatewayListener.LoaderContext) {
		super(context, { event: 'messageCreate' });
	}

	public override run(message: Message) {
		console.log(`${message.author.username}: ${message.content}`);
	}
}
```

To skip the constructor, use the `RegisterAsGatewayListener` decorator. With `once: true`, the listener unloads itself
after its first run:

```ts theme={"system"}
import { EventGatewayListener, RegisterAsGatewayListener, type Message } from '@wolfstar/plugin-gateway';

@RegisterAsGatewayListener('messageCreate', { once: false })
export class LogMessagesListener extends EventGatewayListener<'messageCreate'> {
	public override run(message: Message) {
		console.log(`${message.author.username}: ${message.content}`);
	}
}
```

#### Run a listener on one process

With [`@wolfstar/plugin-broker`](https://stars-components.js.org/documentation/packages/plugin-broker), the gateway process handles each dispatch, and
every worker handles it again when it replays it. Pieces loaded on both processes would run twice. Use the
`RegisterAsDispatchListener` decorator instead of `RegisterAsGatewayListener` to run a listener on one process only. It
takes the same arguments:

* While the gateway client forwards its dispatches, the listener runs on the workers and stays idle on the gateway
  process. `forwardGatewayDispatches` marks the client as forwarding, which `client.forwardsDispatches` reports.
* Otherwise, the listener runs on the gateway client itself, so it also works on a bot that does not use the broker.

```ts theme={"system"}
import { EventGatewayListener, RegisterAsDispatchListener, type Message } from '@wolfstar/plugin-gateway';

@RegisterAsDispatchListener('messageCreate')
export class LogMessagesListener extends EventGatewayListener<'messageCreate'> {
	public override run(message: Message) {
		console.log(`${message.author.username}: ${message.content}`);
	}
}
```

The decorator checks `forwardsDispatches` on every emit, so the order in which forwarding starts and pieces load does not
matter, and stopping the forwarding brings the listener back. A `once` listener is not spent while it idles.

Only the gateway client emits `clientReady`, `shardReady`, `shardResume`, `shardClose`, `shardError`, `dispatch`,
`cacheError`, and `cacheSweep`, so they never reach a worker. `RegisterAsDispatchListener` rejects them in its types and
throws a `TypeError` at runtime. Keep `RegisterAsGatewayListener` for those events.

> [!WARNING]
>
> With `events` set on `forwardGatewayDispatches`, a listener of an event built from a dispatch that is not forwarded runs nowhere. Workers receive
> dispatches at least once, so keep the listener idempotent.

## Caching

Every manager exposes its cache as `manager.cache`, the `Cache` of
[discord.js RFC #11426](https://github.com/discordjs/discord.js/issues/11426). `manager.fetch(...)` reads the cache first
and falls back to the REST API:

```ts theme={"system"}
const user = client.users.cache.get(userId);
const member = guild.members.cache.get(userId);

client.users.cache.has(userId);
client.users.cache.getSize();
```

By default, `CollectionCache` keeps entities in memory as a `Collection` of structure instances. Dispatches patch the
cached instance in place, as in discord.js, and every method is synchronous. Choose another mode with the client
options:

```ts theme={"system"}
import { createRedisCache } from '@wolfstar/plugin-cache';

new GatewayClient({ intents }); // CollectionCache, in memory
new GatewayClient({ intents, cache: createRedisCache({ redis }) }); // raw data in Redis
new GatewayClient({ intents, cache: null }); // nothing is cached
new GatewayClient({ intents, cacheConstructor: MyCache }); // your own Cache class
```

> [!WARNING]
>
> The default cache keeps every entity the gateway sends in memory until a dispatch such as `MESSAGE_DELETE` or `GUILD_DELETE` removes it. Nothing
> expires, so messages, users, and presences grow for as long as the process runs. See [Bounding memory](#bounding-memory).

`manager.cache` is keyed by a single ID for most managers. Client managers that take more than one ID build the key
with `manager.resolveKey(...)`. For those, prefer the guild- or channel-scoped managers in
[Guild-scoped managers](#guild-scoped-managers):

| Manager | `resolveKey` / `fetch` / `refresh` arguments |
| - | - |
| `client.users` | `userId` |
| `client.guilds` | `guildId` |
| `client.channels` | `channelId` (threads included) |
| `client.threads` | `threadId` |
| `client.messages` | `channelId`, `messageId` |
| `client.members` | `guildId`, `userId` |
| `client.roles` | `guildId`, `roleId` |

* `cache.get` only reads the cache and returns `undefined` on a miss.
* `fetch` reads the cache and falls back to the REST API, caching the result. Pass `{ force: true }` after the IDs to
  always hit the API, or `{ cache: false }` to skip storing the result.
* `refresh` is `fetch` with `{ force: true }`.
* `resolve` takes a structure, returned as is, or a cache key, like discord.js's `resolve`. `resolveId` returns the ID.
* `guild.members.me` returns the bot's own member from the cache, like discord.js. It is `null` when the member is not
  cached. Without a `Guild`, call `client.members.me(guildId)`. `client.members.fetchMe(guildId)` falls back to the API.
* `manager.valueOf()` returns `manager.cache`, like discord.js's `DataManager#valueOf`.

Only `manager.cache.get`, and `fetch` and `resolve`, which read it, return the cached instance. Structures delivered by
events are freshly built from the cache, and later dispatches do not patch them. The previous state of update and
delete events is a copy of the cached instance taken before the write.

Relations are resolved from the cache too. `message.author` is the entry of `client.users`, `message.member` the one of
`client.members`, and every guild structure has `guild`. These getters return `null` when the entity is not cached.
`fetchGuild()` and `fetchChannel()` always get it. `guild` is a copy of the cached guild, so compare guilds by `id`.

### Guild-scoped managers

Like discord.js, a guild, channel, or thread hands out its own managers. The first ID is filled in, so `cache` is
keyed by the entity's ID alone. Use them instead of building keys with `resolveKey`, which keeps working:

```ts theme={"system"}
const member = await guild.members.cache.get(userId);
// instead of client.members.cache.get(client.members.resolveKey(guildId, userId))

await guild.roles.cache.has(roleId);
await channel.messages.cache.get(messageId);
await guild.members.kick(userId, 'spam'); // client.members.kick(guildId, userId, 'spam')
```

| Manager | From the client | `cache` is keyed by |
| - | - | - |
| `guild.members` | `client.guilds.members(guildId)` | `userId` |
| `guild.roles` | `client.guilds.roles(guildId)` | `roleId` |
| `guild.voiceStates` | `client.guilds.voiceStates(guildId)` | `userId` |
| `guild.presences` | `client.guilds.presences(guildId)` | `userId` |
| `channel.messages` | `new ChannelMessageManager(…)` | `messageId` |
| `thread.members` | `new ThreadChannelMemberManager(…)` | `userId` |

Call `client.guilds.members(guildId)` and the other client methods when you only hold the guild's ID:

```ts theme={"system"}
const presence = await client.guilds.presences(guildId).cache.get(userId);
const voiceState = await guild.voiceStates.cache.get(userId);
await guild.presences.resolve(member); // null on a miss
```

The guild managers are the client's managers built for one guild. `guild.members` is a `GuildMemberManager<true>`, and
`guild.roles`, `guild.voiceStates`, and `guild.presences` are typed the same way. They have every method of the
client's manager with the guild's ID filled in, and a `guildId` property. Their `cache` is a `Cache` like the client's.
It counts and clears the guild's entries alone, and is synchronous when the client's is. The `cache` of
`channel.messages` and `thread.members` only has `get`, `has`, and `delete`.

On the client's `GuildMemberManager`, `me` is now a property holding a function instead of a prototype method. You still
call it as `client.members.me(guildId)`.

Other managers follow discord.js too:

* `category.children` is a `CategoryChannelChildManager`. Its `cache` lists the cached channels of the category, and
  is a promise with an asynchronous store. `create(options)` makes a channel inside the category, and `resolve()` and
  `resolveId()` look one up. `cache` is read from the channel store on each access, so a store that cannot enumerate
  its entries throws `CacheNotIterable`. `create()` rejects a category as the channel type.
* `answer.voters` is a `PollAnswerVoterManager`. `fetch()` lists the users who voted for the poll answer, and
  `answer.fetchVoters()` now goes through it. It has no `cache`.
* `guild.channels.channelCountWithoutThreads` counts the guild's cached channels, threads excluded. It throws
  `CacheNotIterable` when the channel store cannot enumerate its entries.
* `guild.emojis.resolveIdentifier(emoji)` returns the identifier used in reactions. Unlike discord.js, it throws for a
  value it cannot resolve. An uncached emoji ID still returns `null`.
* `client.guilds.widgetImageURL(guild, style?)` returns the URL of a guild's widget image. `style` defaults to
  `GuildWidgetStyle.Shield`.
* `client.guilds.fetchSoundboardSounds(guildIds, { time })` requests the soundboard sounds of several guilds over the
  gateway, caches them, and resolves with a `Collection` of each guild's sounds, keyed by guild ID. It sends one
  request per shard, so the client must run every shard of the guilds. It rejects with a
  `GuildSoundboardSoundsTimeoutError` when no reply arrives for `time` milliseconds (10 seconds by default). Each reply
  restarts the timer. The replies carry no nonce, so only the process that sent the request resolves it.

```ts theme={"system"}
const sounds = await client.guilds.fetchSoundboardSounds([guildId, otherGuildId]);
console.log(sounds.get(guildId)?.size);

const voters = await answer.voters.fetch();
const channel = await category.children.create({ name: 'announcements' });
```

### Migrating from `get` and `cached`

Earlier releases cached nothing by default and exposed `get` and `cached` on each manager. Both are removed:

| Before | After |
| - | - |
| `client.users.get(id)` | `client.users.cache.get(id)` |
| `client.members.get(guildId, userId)` | `guild.members.cache.get(userId)` |
| `client.users.cached(id)` | `client.users.cache.get(id)` |
| `client.users.cache?.get(id)` (raw data) | `client.cache?.users?.get(id)` |
| `client.users.construct(raw)` | `client.users.cache.construct(raw)` |
| `new GatewayClient({ intents })` (no cache) | `new GatewayClient({ intents, cache: null })` |

`client.presences.resolve(id)` now takes a user ID instead of a cache key. Read a presence with
`guild.presences.cache.get(userId)`.

### Bounding memory

With the default cache, nothing is evicted unless a dispatch removes it. You can bound it in four ways:

* `cacheOptions` sets a `maxSize` per entity. Once the cache reaches it, `CollectionCache` evicts the oldest entry for
  each new one, and `0` holds nothing. The bound applies to the whole entity, so a busy channel can evict the messages
  of a quiet one.
* `policies.filter` decides entry by entry. A rejected entry is not cached, and the entry already cached under its key
  is deleted.
* `cache: null` caches nothing. `cache.get` returns `undefined` and every `fetch` hits the API.
* [`sweepers`](#sweepers) evict entries on a timer.

`cacheOptions` also takes `keepOverLimit(value, key, cache)`, like discord.js's `LimitedCollection`. Once `maxSize` is
reached, the cache evicts the oldest entry it returns `false` for, and grows past `maxSize` if it keeps them all.
`cacheWithLimits` builds `cacheOptions` from plain numbers:

```ts theme={"system"}
import { cacheWithLimits, GatewayClient } from '@wolfstar/plugin-gateway';

const client = new GatewayClient({
	intents,
	// 200 messages, no presences, and up to 1000 members plus the ones in a voice channel.
	cacheOptions: cacheWithLimits({
		messages: 200,
		presences: 0,
		members: { maxSize: 1_000, keepOverLimit: (member) => member.voice?.channelId != null }
	}),
	policies: { users: { filter: (user) => !user.bot } }
});
```

`client.channels.cache` spans the channel and thread caches. It is a `Cache` but not a `Collection`, and `maxSize`
applies to channels and threads separately. Combining `cacheConstructor` or `cacheOptions` with `cache` or `makeCache`
throws. With `cache: null`, both are ignored.

### Sweepers

Like discord.js's, `sweepers` evict the entries of a cache every `interval` seconds. Each entity takes either a
`lifetime` in seconds or a `filter` factory that returns the predicate of that sweep, or `null` to skip it. Only
`invites`, `messages`, and `threads` take a `lifetime`. A message is swept when it was last edited or created longer ago
than that, a thread when it has been archived for longer, and an invite when it has expired for longer.

```ts theme={"system"}
import { GatewayClient, Sweepers } from '@wolfstar/plugin-gateway';

const client = new GatewayClient({
	intents,
	sweepers: {
		messages: { interval: 3_600, lifetime: 1_800 },
		users: { interval: 3_600, filter: () => (user) => user.bot },
		members: {
			interval: 3_600,
			filter: Sweepers.filterByLifetime({
				lifetime: 7_200,
				getComparisonTimestamp: (member) => member.joinedTimestamp,
				excludeFromSweep: (member) => member.voice?.channelId != null
			})
		}
	}
});

// Sweep on demand. Each method returns how many entries it evicted.
client.sweepers.sweepMessages(600);
client.sweepers.sweepUsers(() => (user) => user.bot);
```

`DefaultSweeperSettings` sweeps messages untouched for 30 minutes and threads archived for four hours, every hour.
Unlike discord.js's, it is not empty. Every sweep emits `cacheSweep(entity, swept)`. A failing filter emits
`cacheError` with `operation: "sweep"` instead of throwing. `client.destroy()` stops the timers.

Sweepers walk the caches of structure instances, so combining them with `cache` or `makeCache` throws. Set the `ttl` of
`policies` for those stores instead.

### Custom caches

`cacheConstructor` takes a class implementing `Cache`. The client instantiates it once per entity with
`(creator, name, options)`. `options` is a `CacheConstructorOptions` holding `keyOf(data)`, the cache key of raw data,
`refresh(value)`, which resolves an instance's relations again, and the entity's `cacheOptions`. Extend
`CollectionCache` to get all of this:

```ts theme={"system"}
import { CollectionCache, type CacheConstructorOptions, type RawAPIType, type StructureCreator, type StructureMixin } from '@wolfstar/plugin-gateway';
import type { CacheEntityName } from '@wolfstar/plugin-cache';

class BoundedCache<Value extends StructureMixin<object>, Raw extends RawAPIType<Value> = RawAPIType<Value>> extends CollectionCache<Value, Raw> {
	public constructor(creator: StructureCreator<Value, Raw>, name: CacheEntityName, options: CacheConstructorOptions<Value, Raw>) {
		super(creator, name, { ...options, maxSize: options.maxSize ?? 10_000 });
	}
}

new GatewayClient({ intents, cacheConstructor: BoundedCache });
```

The cache must be synchronous. A cache that is not a `Map` cannot be enumerated, so it gets no dispatch cascades on
`GUILD_DELETE` and `CHANNEL_DELETE`, no reconciliation of guilds left while offline, no granular emoji and sticker diff
events, and no `listCached`.

### Store-backed caches

`cache` and `makeCache` back the managers with raw [`@wolfstar/plugin-cache`](https://stars-components.js.org/documentation/packages/plugin-cache)
stores, in memory or Redis, instead of `CollectionCache`. Each read builds a new structure. `makeCache` takes precedence
over `cache`, and returns a store per entity kind, or `null` not to cache it:

```ts theme={"system"}
import { MemoryEntityCache } from '@wolfstar/plugin-cache';

const client = new GatewayClient({
	intents,
	makeCache: (entity) => (['guilds', 'channels', 'roles'].includes(entity) ? new MemoryEntityCache() : null),
	policies: { users: { filter: (user) => !user.bot }, messages: { ttl: () => 3_600_000 } },
	cacheErrors: 'miss'
});
```

`policies` apply to every write in every mode, but `ttl` only applies to these stores. Every feature works without a
store for an entity kind: every event is still emitted, and the previous state of update and delete events is `null`.
Some features need a store. Without `emojis` or `stickers`, you only get `guildEmojisUpdate` and `guildStickersUpdate`
instead of granular diff events. Without `presences`, `presences.fetch` rejects and `presences.resolve` returns `null`.

### Synchronous reads

`manager.cache` methods return a value with the default `CollectionCache` and a promise with an asynchronous store such
as Redis. `manager.cache.synchronous` tells them apart, so a hot path can skip `await`. `await` works either way:

```ts theme={"system"}
client.on('messageCreate', async (message) => {
	const member = message.guildId ? await client.guilds.members(message.guildId).cache.get(message.author.id) : undefined;
	if (member?.roleIds.includes(mutedRoleId)) return;
	// ...
});
```

Structures also have discord.js's synchronous getters, which read the cache alone and never call the API:

* `GuildMember`: `permissions`, `permissionsIn(channel)`, `manageable`, `kickable`, `bannable`, `moderatable`,
  `displayColor`, and `displayHexColor`.
* `Message`: `editable`, `deletable`, `bulkDeletable`, `pinnable`, `crosspostable`, and `threadable`. `threadable` is
  `true` when the bot can start a thread from the message: a text or announcement channel, no thread yet, and
  `ViewChannel` plus `CreatePublicThreads`. Unlike discord.js, an administrator bot can always start one.
* `Role`: `editable` and `permissionsIn(channel)`. Guild channels: `permissionsFor(target)`.
* `GuildEmoji#deletable` and `GuildInvite#deletable`.

```ts theme={"system"}
if (member.permissions.has('BanMembers')) await member.ban();
if (member.kickable) await member.kick();
channel.permissionsFor(member).has('SendMessages');
```

When an entity a getter needs is missing from the cache, it throws a `GatewayError` with the code `GuildUncached`,
`GuildUncachedMe`, `ChannelUncached`, or `GuildMemberUncached`. Uncached roles are skipped without an error, as in
discord.js. See [Checks on a cache miss](#checks-on-a-cache-miss). If your cache may lack an entity, for example because it is size-limited or filtered, fetch what is missing, then read the
getter:

```ts theme={"system"}
await client.guilds.fetch(guildId);
const me = await client.members.fetchMe(guildId);
await me.roles.fetch();

if (member.kickable) await member.kick();
```

With an asynchronous store, the same getters return promises. Declare the cache asynchronous once so TypeScript types
them as promises:

```ts theme={"system"}
declare module '@wolfstar/plugin-gateway' {
	interface GatewayCacheConfig {
		asynchronous: true;
	}
}

if ((await member.permissions).has('BanMembers')) await member.ban();
if (await member.kickable) await member.kick();
```

> [!WARNING]
>
> Without the declaration, the getters are typed as plain values. A promise is always truthy, so `if (member.kickable)` passes for every member.

Relation getters such as `message.guild`, `member.voice`, and `channel.parent` stay `null` with an asynchronous store
and never throw. Use the entity fetches, such as `fetchGuild()`, `fetchChannel()`, and `member.fetchPresence()`.

#### Checks on a cache miss

The getters skip a role that is not in the cache without an error. `member.roles.highest` returns the highest role the
cache holds, and `member.permissions` and `permissionsIn(channel)` combine only the cached roles. With a filtered or
size-limited cache, or on a `plugin-broker` worker, a role-hierarchy or permission check can under-report.
`manageable`, `kickable`, `bannable`, and `moderatable` read `roles.highest`, so they under-report too.

When a check must be right on a cache miss, use the calls that fetch the missing roles from the API. With a complete
cache, they make no request:

* `member.roles.fetchHighest()` instead of `member.roles.highest`.
* `member.fetchPermissions()` instead of `member.permissions`.
* `member.fetchPermissionsIn(channel)` instead of `member.permissionsIn(channel)`. It also fetches the channel.
* For `manageable`, `kickable`, `bannable`, and `moderatable`, call `await member.roles.fetch()` and
  `await me.roles.fetch()` for the bot's member before you read the getter.

```ts theme={"system"}
// Right even when a role of either member is not cached:
const [mine, theirs] = await Promise.all([me.roles.fetchHighest(), author.roles.fetchHighest()]);
const canManageRoles = (await me.fetchPermissions()).has('ManageRoles');

await Promise.all([member.roles.fetch(), me.roles.fetch()]);
if (member.kickable) await member.kick();
```

#### Deprecated `fetch*` twins

The other derived getters also have a `fetch*` twin that asks the API for what is not cached, such as
`member.fetchKickable()` or `message.fetchDeletable()`. These twins are deprecated and will be removed in a later
release, so use the getter instead. `Message#fetchEditable()` is removed. `roles.fetchHighest()`,
`fetchPermissions()`, and `fetchPermissionsIn()` are not deprecated. Entity fetches such as `fetchGuild`,
`fetchMember`, `client.members.fetchMe`, `roles.fetch()`, and `member.fetchPresence()` stay.

## Requesting guild members

`client.members.request(guildId, options)`, or `guild.requestMembers(options)`, asks the guild's shard for its members
over the gateway instead of REST. By default, it requests every member. Pass `query` with a `limit` to search by name,
or up to 100 `userIds`. Set `presences: true` to include presences.

```ts theme={"system"}
const members = await client.members.request(guildId);
const [wolf] = await client.members.request(guildId, { userIds: [userId], presences: true });
```

The promise resolves with the `GuildMember`s once the last `GUILD_MEMBERS_CHUNK` for the request is cached, so
`client.members.cache.get` sees them. It rejects with:

* `GuildMembersTimeoutError` when no chunk arrives for `time` milliseconds (120 seconds by default).
* `GuildMembersRateLimitError` when Discord answers with `RATE_LIMITED`. Its `retryAfter` holds the delay.

Every chunk is also emitted as `guildMembersChunk`. Its `data.not_found` lists the requested IDs that are not members.
Requesting every member or a query needs the `GuildMembers` intent, and presences need the `GuildPresences` intent.

## Resuming sessions across restarts

`@discordjs/ws` resumes a dropped connection within the same process. Without a `sessionStore`, every deploy or crash
identifies every shard again, spends identify quota, misses the events sent meanwhile, and triggers a full
`GUILD_CREATE` burst. A `sessionStore` keeps sessions outside the process, so the next process resumes them and Discord
replays what it missed:

```ts theme={"system"}
import { createRedisCache, createRedisSessionStore } from '@wolfstar/plugin-cache';
import { GatewayClient } from '@wolfstar/plugin-gateway';
import { GatewayIntentBits } from 'discord-api-types/v10';
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);
const client = new GatewayClient({
	intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages,
	cache: createRedisCache({ redis }),
	sessionStore: createRedisSessionStore({ redis })
});

process.once('SIGTERM', async () => {
	await client.destroy({ resumable: true });
	process.exit(0);
});
```

* **Pair it with a persistent cache.** A resumed session only replays missed events, not guilds. With the default
  in-memory cache, a restarted process resumes with an empty cache that `GUILD_CREATE` never refills.
* **Shut down with `destroy({ resumable: true })`.** By default, `destroy()` closes connections with code `1000`, which
  makes Discord invalidate the sessions. With `resumable`, shards close with code `4200` and their sessions stay stored.
* **Reads never stall a shard.** Each shard reads the store once when it first connects. A read that fails or takes
  longer than `sessionStoreTimeout` is reported as a `GatewaySessionStoreError` through `error`, and the shard identifies.
* **Writes run in the background.** After a crash, the stored sequence may be a few dispatches behind. Discord replays
  those dispatches, so listeners can see them twice.

`sessionStore` replaces the `gateway.retrieveSessionInfo` and `gateway.updateSessionInfo` options. Passing either
alongside it throws.

## Structures

Structures extend the classes [`@discordjs/structures`](https://github.com/discordjs/discord.js/tree/main/packages/structures)
ships, such as `User`, `Message`, `Emoji`, `Invite`, `Webhook`, and every channel type. They add relations resolved from
the cache, CDN URLs, and discord.js-style actions. Structures without a counterpart yet, such as `Guild`, `GuildMember`,
and `Role`, extend its base `Structure`.

Every structure has `client`, like discord.js's `Base#client`: the client whose manager built it. It is not part of the
structure's data, so `toJSON()` never serializes it and the cache never stores it.

Channels get one class per type (`TextChannel`, `VoiceChannel`, `ForumChannel`, `PublicThreadChannel`, `DMChannel`, and
more), and `instanceof Channel` matches any of them:

```ts theme={"system"}
import { TextChannel } from '@wolfstar/plugin-gateway';

client.on('channelCreate', (channel) => {
	if (channel instanceof TextChannel) console.log(channel.name, channel.topic);
});
```

Guilds, members, roles, channels, threads, messages, webhooks, and moderation structures follow discord.js's API.
Getters that discord.js reads from its cache, such as `member.permissions` or `member.kickable`, read `manager.cache`
here too. See [Synchronous reads](#synchronous-reads). Position setters such as `setPosition` sort siblings the way
Discord does. Every structure is valued by its ID through `valueOf()`, like discord.js's `Base`.

`equals()` compares structures the way discord.js v14 and v15 do. `GuildChannel#equals`, on every guild channel and
thread, and `SoundboardSound#equals` are new, and both accept the structure or its raw payload.

> [!WARNING]
>
> `equals()` compares different fields than in earlier releases:
>
> * `Message#equals(message, rawData?)` compares the ID, author, content, `nonce`, `tts`, attachments by ID, and embeds
>   through `Embed#equals`. With `rawData`, it also compares `mention_everyone` and the timestamps. `pinned` and the
>   content of attachments are no longer compared.
> * `Embed#equals` deep-compares two embeds, and compares a raw embed field by field, so `type` and proxy URLs no longer
>   matter.
> * `Guild#equals` no longer compares the AFK and system channels, content filter, MFA level, banner, description, or
>   vanity code. It compares features in order.
> * `GuildMember#equals` also compares `partial`, banner, `pending`, the avatar decoration, and the nameplate, and
>   compares roles in order.
> * `Role#equals` compares the three `colors` instead of `color`.
> * `User#equals` also compares the avatar decoration SKU, the nameplate, and the primary guild.
> * `Sticker#equals` also compares the type, pack, and sort value. `GuildEmoji#equals` and `Sticker#equals` compare
>   fewer fields against raw data.
> * `GuildChannel#equals` compares the ID, type, name, topic, position, and permission overwrites in any order. It
>   does not compare the category.
>
> `Guild`, `GuildMember`, `Sticker`, `GuildEmoji`, `GuildChannel`, and `SoundboardSound` return `false` for a foreign
> argument instead of throwing. `Role`, `User`, `Embed`, and `Message` accept `null` and `undefined`. Update events such
> as `messageUpdate` do not rely on `equals()`, but `emojiUpdate` and `stickerUpdate` still do.

```ts theme={"system"}
const member = await client.members.fetch(guildId, userId);

await member.roles.add(roleId, 'verified');
if (member.kickable) await member.kick();
```

### Members, roles, and users

`member.roles` and `emoji.roles` are discord.js's `GuildMemberRoleManager` and `GuildEmojiRoleManager`. `add`, `remove`,
and `set` take a `Role`, an ID, an array, or a `Collection`, and resolve to the updated member or emoji. Their `cache`
is a `Collection` of the cached roles, `@everyone` included. `member.roles` also has `highest`, `hoist`, `color`,
`icon`, `premiumSubscriberRole`, and `botRole`. Unlike discord.js, `cache` and these getters return a promise with an
asynchronous store, so `await` them. `highest` is `null` when no role is cached.

```ts theme={"system"}
const highest = await member.roles.highest;
await highest?.setColors({ primaryColor: 0xff0000 });
```

With `Partials.GuildMember`, `guildMemberUpdate`, `guildMemberRemove`, and `typingStart` can hand out a member built
from its IDs alone. Those payloads are typed as `GuildMember | PartialGuildMember | null`. Narrow them with
`member.isPartial()` before passing them to code that expects a `GuildMember`. `member.roles.partial` reads
`member.partial`.

`client.users` accepts any `UserResolvable`: a `User`, a `GuildMember`, a `ThreadMember`, a `Message` for its author, or
an ID. `client.users.createDM(user)` returns the cached direct message channel unless you pass `{ force: true }`.
`client.users.dmChannel(user)` and `user.dmChannel` read the cached channel, or `null`. `client.users.deleteDM(user)`
throws `UserNoDMChannel` when no channel is cached. With `cache: null` or a Redis store, `dmChannel` is `null` and
`createDM` always calls the API.

```ts theme={"system"}
await client.users.send(member, 'Welcome!');
```

`client.presences.resolve(value, guildId?)` and `resolveId(value)` accept a `PresenceResolvable`: a `Presence`, a
`GuildMember`, a `User`, a `ThreadMember`, a `Message`, or a user ID. `resolve` reads the cached presence of the user in
the member's, message's, or given guild, and returns `null` on a miss.

`Structure`, `StructureMixin`, `initStructure`, `Mixin`, and the `kData`, `kPatch`, and `kClone` symbols are exported,
so you can subclass structures and write new mixins.

### Application

`client.application` is a `ClientApplication`, like discord.js's `Client#application`. Use it for what the application
owns rather than a guild, such as its profile, owner, and role connection metadata. It lives on the client, not in the
cache.

Unlike discord.js, where it is `null` until `READY`, `client.application` is never `null`. The client builds it from
`clientId`, so it exists before `READY` and without a gateway connection. Until you fetch or edit it, `partial` is
`true`. Before `READY`, only `id` is known. `READY` patches the same instance with the application's `flags`, so
references you hold stay valid. `fetch()` calls `GET /applications/@me` and patches in everything else:

```ts theme={"system"}
client.application.partial; // true until fetch() or edit() resolves
await client.application.fetch();
client.application.name; // 'Wolf'
client.application.owner; // a Team, a User, or null
client.application.iconURL();

await client.application.edit({ description: 'Awoo', tags: ['wolf'] });
```

After a fetch, `ClientApplication` exposes `tags`, `installParams`, `integrationTypesConfig`, `customInstallURL`,
`botPublic`, `botRequireCodeGrant`, `guildId` and the cached `guild`, `approximateGuildCount`,
`approximateUserInstallCount`, `interactionsEndpointURL`, `roleConnectionsVerificationURL`, and the event webhook
settings `eventWebhooksURL`, `eventWebhooksStatus`, and `eventWebhooksTypes`.

`owner` is a `Team` when a team owns the application, with its `members` as `TeamMember`s. Otherwise it is the owner
`User`. It is `null` until the application is fetched.

| Method | Description |
| - | - |
| `fetch()` | Fetches the application and patches `client.application` with it. |
| `edit(options)` | Edits the application and patches `client.application` with it. |
| `fetchRoleConnectionMetadataRecords()` | Fetches the application's role connection metadata records. |
| `editRoleConnectionMetadataRecords(records)` | Replaces the application's role connection metadata records. |
| `fetchSKUs()` | Fetches the application's SKUs as a `Collection` of `SKU`s. |
| `subscriptions(sku)` | Returns the `SubscriptionManager` of a SKU or its ID. |

`Application`, `ClientApplication`, `Team`, and `TeamMember` are exported.

#### Application commands

`client.application.commands` is an `ApplicationCommandManager` for global commands. Pass a `guildId` option to its
methods to reach the commands of a guild. `guild.commands`, or `client.guilds.commands(guildId)` when you only hold the
ID, is a `GuildApplicationCommandManager` for the commands of one guild. Each access returns a new manager.

These managers are fetch-only. Discord sends no event when a command changes, so the commands are never cached. Every
method calls the API and returns new `ApplicationCommand` structures. The managers have `fetch`, `create`, `set`,
`edit`, and `delete`. `create` and `edit` accept camel case data, raw payloads, and builders, or anything with
`toJSON()`.

```ts theme={"system"}
import { ApplicationCommandOptionType } from 'discord-api-types/v10';

const commands = await client.application.commands.fetch(); // Collection<id, ApplicationCommand>
const one = await guild.commands.fetch('123456789012345678');
const localized = await client.application.commands.fetch({ locale: 'fr', withLocalizations: true });

const created = await guild.commands.create({
	name: 'awoo',
	description: 'Howl',
	defaultMemberPermissions: ['ManageGuild'],
	options: [{ type: ApplicationCommandOptionType.Boolean, name: 'loud', description: 'Howl loud' }]
});
await created.setDescription('Howl at the moon'); // patches `created` in place
await created.delete();
```

`ApplicationCommand` has the command's fields, `edit()`, the `set*()` helpers, `delete()`, and `permissions`.
`command.equals(other)` compares every field `other` has, options included, and `command.equals(other, true)` also
compares the order of the options. It returns `false` for a foreign argument.

> [!WARNING]
>
> `set(commands)` replaces every command of the application, or of the guild, with the given ones. `@wolfstar/http-framework` registers the commands
> of your pieces with the same API call, so `set()` deletes the commands the framework registered. Nothing in this package calls `set()` for you.
> Use it only for commands the framework does not manage.

`ApplicationCommandPermissionsManager` manages the permissions of commands in a guild. It is
`client.application.commands.permissions` (pass `guild` and `command`), `guild.commands.permissions` (pass `command`),
and `command.permissions`. Without a guild, global command permissions throw `GlobalCommandPermissions`.

* `fetch` and `has` use the bot's authorization. For one command, they read the `applicationCommandPermissions` cache,
  which `APPLICATION_COMMAND_PERMISSIONS_UPDATE` keeps up to date. Pass `force: true` to `fetch` to skip the cache.
* `set`, `add`, and `remove` need an OAuth2 bearer `token` of a user who can manage the guild, with the
  `applications.commands.permissions.update` scope. Discord rejects the bot's token for these writes. Without `token`,
  they throw `ApplicationCommandPermissionsTokenMissing`. The package has no OAuth2 flow, so you obtain the token
  yourself.

```ts theme={"system"}
import { ApplicationCommandPermissionType } from 'discord-api-types/v10';

await command.permissions.fetch({ guild: guildId, force: true });
await guild.commands.permissions.has({ command, permissionId: roleId });
await guild.commands.permissions.add({
	command,
	token: userToken,
	permissions: [{ id: roleId, type: ApplicationCommandPermissionType.Role, permission: false }]
});
```

#### Application emojis

`client.application.emojis` is an `ApplicationEmojiManager` for the emojis the application owns. The bot can use them in
any guild. Discord sends no event for these emojis, so the manager is fetch-only and caches nothing. `fetch()` lists
them in a `Collection`, and `fetch(id)` gets one. The manager also has `create`, `edit`, `delete`, and `fetchAuthor`.

`ApplicationEmoji` extends `Emoji` with `author`, `application`, `edit`, `setName`, `delete`, and `fetchAuthor`. Unlike
`GuildEmoji`, it has no `roles`, `managed`, `available`, or `requiresColons`, because the API does not send them.
`equals()` compares only the ID, name, and `animated`.

```ts theme={"system"}
const emojis = await client.application.emojis.fetch(); // Collection<string, ApplicationEmoji>
const emoji = await client.application.emojis.create({ attachment: 'data:image/png;base64,...', name: 'howl' });

await emoji.setName('awoo');
const author = await emoji.fetchAuthor();
await emoji.delete();
```

#### Monetization

`client.application.entitlements` is an `EntitlementManager`, a cached manager like the others. `ENTITLEMENT_CREATE`,
`ENTITLEMENT_UPDATE`, and `ENTITLEMENT_DELETE` keep `entitlements.cache` up to date and emit `entitlementCreate`,
`entitlementUpdate`, and `entitlementDelete`. Processes that share a store share the cache. `fetch` takes an
entitlement or its ID and reads the cache first, or takes filters and lists entitlements from the API:

```ts theme={"system"}
const { entitlements } = client.application;

await entitlements.fetch(entitlementId); // one, from the cache or the API
await entitlements.fetch(entitlementId, { force: true });
await entitlements.fetch({ user, skus: [sku], excludeEnded: true, limit: 50 }); // a Collection, from the API

await entitlements.createTest({ sku, user }); // or { sku, guild }
await entitlements.deleteTest(entitlement);
await entitlements.consume(entitlement); // one-time purchase consumables, also entitlement.consume()

entitlement.isActive(); // not deleted, and not ended
entitlement.isTest();
entitlement.isGuildSubscription(); // or isUserSubscription()
```

`createTest` takes either a `guild` or a `user`. Passing both or neither throws `EntitlementCreateInvalidOwner`.

`client.application.fetchSKUs()` resolves to a `Collection` of `SKU`s. Each `SKU` has `flags`, a `SKUFlagsBitField`,
and `subscriptions`, the `SubscriptionManager` of that SKU. When you only hold the SKU's ID, use
`client.application.subscriptions(skuId)`. Subscriptions are fetch-only and never cached, because nothing keeps them up
to date for the bot:

```ts theme={"system"}
const skus = await client.application.fetchSKUs();
const subscription = await skus.first()!.subscriptions.fetch(subscriptionId);
const subscriptions = await client.application.subscriptions(skuId).fetch({ user, limit: 50 }); // a Collection
```

`Entitlement`, `SKU`, and `Subscription` are exported. `EntitlementResolvable`, `SKUResolvable`, and
`SubscriptionResolvable` accept the structures or their IDs.

### Components

Message components are structures too, like in discord.js: `ActionRow`, `InteractiveButtonComponent`,
`LinkButtonComponent`, the select menus, `TextInputComponent`, and the Components v2 layout classes such as
`ContainerComponent`, `SectionComponent`, `TextDisplayComponent`, and `MediaGalleryComponent`. `createComponent()`
builds the matching class from raw component data, and `findComponentByCustomId()` finds a component in a tree.

### Messages and `MessagePayload`

`Message` follows discord.js, with `reply`, `edit`, `delete`, `forward`, `pin`, `react`, and more. As in discord.js,
`attachments`, `stickers`, `messageSnapshots`, and `reactions.cache` are `Collection`s keyed by ID. Reactions are keyed
by the ID of a custom emoji or the name of a Unicode one. `react()` resolves to the `MessageReaction`, counting the bot.
`partial` is `true` for a message that lacks its content or its author.

```ts theme={"system"}
const reaction = await message.react('🐺');
console.log(reaction.count, message.attachments.first()?.url);
```

`Message` is generic like discord.js's. `Message<true>` has a `guildId` string and a guild text-based `channel`, and
`inGuild()` narrows to it. `reply`, `edit`, and the other actions resolve to messages whose `channel` is never a group
DM. `message.fetch(false)` answers from the cache.

Text channels have `send` and `sendTyping`. Guild text-based channels also have `bulkDelete`, which takes messages,
their IDs, a `Collection`, or a count. Direct message channels do not have it, as in discord.js. `bulkDelete` resolves
to a `Collection` of the deleted messages by ID. Each value is the cached `Message`, a `PartialMessage` with
`Partials.Message`, or `undefined`. Narrow a `Message | PartialMessage` with `partial`:

```ts theme={"system"}
const deleted = await channel.bulkDelete(10, true);
const ids = [...deleted.keys()];
```

`channel.messages` is one `ChannelMessageManager` for every kind of channel, where discord.js has a manager per kind.
In a direct message or group direct message, its guild-only methods `crosspost`, `bulkDelete`, and `startThread` throw
`ChannelMessagesGuildOnly` before any request. The other methods work in every channel. `crosspost` takes a message or
its ID. `client.channels.createMessage(channel, options)` sends a message to a channel or its ID, like discord.js.
`client.messages.send(channelId, options)` and `channel.send(options)` do the same.

```ts theme={"system"}
await client.channels.createMessage(channelId, { content: 'Awoo' });
await announcements.messages.crosspost(message);
```

#### Creating threads

`channel.threads.create(options)` takes `ThreadCreateOptions`, which is `TextThreadCreateOptions` or
`ForumThreadCreateOptions`. Pass `type` and `invitable` for a thread in a text or announcement channel. Pass `message`
and `appliedTags` for a post in a forum or media channel. `name`, `autoArchiveDuration`, and `rateLimitPerUser` apply to
both. Mixing the two is a compile error, and `create` throws a `GatewayTypeError` before any request:

* `ThreadOptionsPostOnly` for `appliedTags` without `message`, or `message` in a text or announcement channel.
* `ThreadOptionsThreadOnly` for `type` or `invitable` together with `message`.
* `ThreadPostMessageRequired` for a forum or media channel without `message`.

```ts theme={"system"}
import { ChannelType } from 'discord-api-types/v10';

const thread = await textChannel.threads.create({ name: 'howls', type: ChannelType.PrivateThread, invitable: false });
const post = await forum.threads.create({ name: 'Awoo', message: { content: 'First howl' }, appliedTags: [tagId] });
```

To start a thread from an existing message, call `message.startThread()`. Unlike discord.js, an explicit `type` is not
overridden in an announcement channel.

### Searching guild messages

`guild.searchMessages(options)`, or `client.guilds.searchMessages(guildId, options)`, runs Discord's guild message
search. Options include `content`, `authorIds`, `channelIds`, `mentions`, `has`, `pinned`, `sortBy`, `sortOrder`,
`limit`, and `offset`. The ID options take the usual resolvables. The result holds `Collection`s of `messages`,
`threads`, and `threadMembers`, plus `totalResults`. Results are cached unless you pass `cache: false`.

```ts theme={"system"}
const { messages, totalResults } = await guild.searchMessages({ content: 'awoo', authorIds: [member], limit: 25 });
```

While Discord indexes the guild, the search waits the `retry_after` delay and retries. Pass
`retryOnMissingIndex: false` to throw `SearchIndexNotYetAvailable` instead, and `signal` to cancel. A query over
Discord's documented limits throws a `GatewayRangeError` before the request.

`MessagePayload` normalizes message options into the REST body and files Discord expects, like discord.js's class of the
same name. It accepts camel case options, builders, reply and forward shortcuts, stickers, polls, and files from paths,
URLs, streams, or buffers:

```ts theme={"system"}
import { MessagePayload } from '@wolfstar/plugin-gateway';

const payload = MessagePayload.create(channel, {
	content: 'Awoo',
	reply: { messageReference: message },
	files: ['./wolf.png', { attachment: buffer, name: 'howl.ogg', description: 'A howl' }]
});
await channel.send(payload);
```

#### `AttachmentBuilder`

Files to send are `AttachmentBuilder`s, as in discord.js v14. `new AttachmentBuilder(file, { name, description })`
takes a buffer, a path, a URL, a stream, or a blob. These methods chain:

* `setFile(file, name?)`, `setName(name)`, and `setDescription(description)`.
* `setSpoiler(spoiler = true)` adds or removes the `SPOILER_` prefix of the name. The `spoiler` getter reads it.
* `setTitle(title)`, plus `setDuration(seconds)` and `setWaveform(waveform)` for voice messages.

Pass builders in the `files` of a message. `AttachmentBuilder.from(other)` copies a builder, an attachment payload, or a
received `Attachment`. `Attachment#attachment` returns the URL of a received attachment, as in discord.js, so you can
send it again:

```ts theme={"system"}
import { AttachmentBuilder } from '@wolfstar/plugin-gateway';

const file = new AttachmentBuilder('./howl.ogg').setDescription('A howl').setSpoiler();
await channel.send({ files: [file] });

const original = message.attachments.first();
if (original) await channel.send({ files: [AttachmentBuilder.from(original).setName('copy.png')] });
```

### Resolvable types

Methods accept discord.js's `Resolvable` types, so you can pass either a structure or an ID. Examples include
`UserResolvable`, `GuildResolvable`, `ChannelResolvable`, `RoleResolvable`, `MessageResolvable`, `EmojiResolvable`,
`EmojiIdentifierResolvable`, `BufferResolvable`, `AttachmentResolvable`, and `MessagePayloadResolvable`.

## Utilities

The package exports the discord.js helpers its structures use:

* `parseEmoji(text)` parses a Unicode emoji, a custom emoji mention such as `<a:name:id>`, or a `name:id` pair. It
  returns `null` when the string holds a colon but no custom emoji.
* `resolvePartialEmoji(emoji)` resolves any `EmojiIdentifierResolvable` without a cache lookup. A bare ID resolves to
  `{ id }`.
* `resolveGuildEmoji(client, emojiId)` finds a custom emoji by ID across every cached guild, like discord.js's
  `resolveGuildEmoji`. It resolves to the `GuildEmoji`, or `null` when no cached guild has it. It never calls the API.
  It enumerates the whole emoji cache, so it needs a store that can list its entries, and throws a `TypeError` when the
  store cannot.
* DataResolver functions: `resolveFile`, `resolveBase64`, `resolveImage`, `resolveInviteCode`, and
  `resolveGuildTemplateCode`.
* Transformers such as `toSnakeCase` and the `transformAPI*` functions convert between API payloads and discord.js-style
  camel case objects.
* Util functions such as `resolveColor`, `cleanContent`, `parseWebhookURL`, `discordSort`, and `transformResolved`.
* `transformResolved({ client, guildId, channelId }, data)` resolves the `users`, `members`, `roles`, `channels`,
  `messages`, and `attachments` of data such as an interaction's `resolved` into maps of structures. It writes nothing
  to the cache. Message IDs are read from the cache of the `channelId` channel, so without `channelId` only raw messages
  are resolved. Raw messages resolve to the cached message or are built from the data. Attachments are always built as
  `Attachment`s.

```ts theme={"system"}
import { parseEmoji, resolveGuildEmoji, resolvePartialEmoji } from '@wolfstar/plugin-gateway';

parseEmoji('<a:howl:123456789012345678>'); // { animated: true, name: 'howl', id: '123456789012345678' }
resolvePartialEmoji('123456789012345678'); // { id: '123456789012345678' }
await resolveGuildEmoji(client, '123456789012345678'); // the cached GuildEmoji, or null
```

## Errors

Like discord.js's `DiscordjsError`, every error the package throws or emits carries a `code` from `GatewayErrorCodes`,
and its message comes from `GatewayErrorMessages`. `GatewayError`, `GatewayTypeError`, and `GatewayRangeError` extend
`Error`, `TypeError`, and `RangeError`, and are named after their code, for example
`GatewayError [WebhookTokenUnavailable]`.

```ts theme={"system"}
import { GatewayError, GatewayErrorCodes } from '@wolfstar/plugin-gateway';

try {
	await webhook.send('Awoo');
} catch (error) {
	if (error instanceof GatewayError && error.code === GatewayErrorCodes.WebhookTokenUnavailable) {
		// The webhook was fetched without its token.
	}
}
```

These errors extend `GatewayError` and carry extra data:

| Error | When | Extra properties |
| - | - | - |
| `DispatchTimeoutError` | A dispatch is still processing after `dispatchTimeout`. It is not cancelled. | `type`, `shardId`, `partition`, `timeout` |
| `GuildMembersTimeoutError` | A member request receives no chunk for `time` milliseconds. | `guildId`, `nonce`, `timeout` |
| `GuildSoundboardSoundsTimeoutError` | `fetchSoundboardSounds` receives no reply for `time` milliseconds. | `guildIds`, `timeout` |
| `GuildMembersRateLimitError` | Discord rate limits a member request. | `guildId`, `nonce`, `retryAfter` |
| `GatewaySessionStoreError` | A `sessionStore` read or write fails or times out. | `operation`, `shardId`, `cause` |

The client reports dispatch, cache, and session store failures through its `error` event.

Codes thrown by the application, monetization, and message APIs include:

| Code | When |
| - | - |
| `ApplicationCommandPermissionsTokenMissing` | `set`, `add`, or `remove` on command permissions is called without a bearer `token`. |
| `GlobalCommandPermissions` | A command permissions method has no guild to read or write the permissions in. |
| `EntitlementCreateInvalidOwner` | `entitlements.createTest` receives both a `guild` and a `user`, or neither. |
| `ThreadOptionsPostOnly` | `threads.create` receives `message` or `appliedTags` where only a thread is allowed. |
| `ThreadOptionsThreadOnly` | `threads.create` receives `type` or `invitable` together with `message`. |
| `ThreadPostMessageRequired` | `threads.create` in a forum or media channel has no `message`. |
| `ChannelMessagesGuildOnly` | `crosspost`, `bulkDelete`, or `startThread` is called in a direct message channel. |

## Limitations

* A `GatewayClient` connects its shards from a single process. You can set `@discordjs/ws`'s `WorkerShardingStrategy`
  through `gateway.buildStrategy`.
* Interaction payloads are handled by the HTTP endpoint as before. They do not read through `client.users` and the other
  managers.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.