@wolfstar/plugin-gateway 0.8.0 → 0.9.0-next-20261001183204

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,9 +23,9 @@ interactions continue to use the framework's existing client.
23
23
  The gateway connection uses [`@discordjs/ws`](https://www.npmjs.com/package/@discordjs/ws) for
24
24
  sharding, reconnects, and session resumes. Actions turn dispatches into events containing structures
25
25
  such as `Message`, `User`, and `Guild`; `EventGatewayListener` lets pieces in the `listeners`
26
- directory handle those events. Managers such as `client.users` and `client.guilds` read from an
27
- optional [`@wolfstar/plugin-cache`](../plugin-cache) cache and fetch missing data through
28
- `@discordjs/core`. The same core API is available as `client.api`.
26
+ directory handle those events. Managers such as `client.users` and `client.guilds` read through
27
+ `manager.cache`, in memory by default, optionally backed by an [`@wolfstar/plugin-cache`](../plugin-cache)
28
+ store, and fetch missing data through `@discordjs/core`. The same core API is available as `client.api`.
29
29
 
30
30
  > [!NOTE]
31
31
  > A gateway connection is long-lived: a `GatewayClient` needs a persistent process, unlike a bot
@@ -40,18 +40,16 @@ pnpm add @wolfstar/plugin-gateway @wolfstar/plugin-cache
40
40
  ## Usage
41
41
 
42
42
  ```ts
43
- import { createInMemoryCache } from "@wolfstar/plugin-cache";
44
43
  import { GatewayClient } from "@wolfstar/plugin-gateway";
45
44
  import { GatewayIntentBits } from "discord-api-types/v10";
46
45
 
47
46
  const client = new GatewayClient({
48
47
  intents:
49
48
  GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent,
50
- cache: createInMemoryCache(),
51
49
  });
52
50
 
53
- client.on("messageCreate", async (message) => {
54
- const guild = message.guildId ? await client.guilds.get(message.guildId) : undefined;
51
+ client.on("messageCreate", (message) => {
52
+ const guild = message.guildId ? client.guilds.cache.get(message.guildId) : undefined;
55
53
  console.log(`${message.author.username} in ${guild?.name ?? "a DM"}: ${message.content}`);
56
54
  });
57
55
 
@@ -62,19 +60,21 @@ await client.start({ listen: { port: 8080 } }); // loads pieces, starts HTTP, co
62
60
 
63
61
  On top of the `Client` options:
64
62
 
65
- | Option | Default | Description |
66
- | --------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
67
- | `intents` | — | The gateway intents. |
68
- | `cache` | `undefined` | A `Cache` from `@wolfstar/plugin-cache`, see [Caching](#caching). |
69
- | `shardCount` | `null` | Total shards across every process, `null` for Discord's recommendation. |
70
- | `shardIds` | `null` | The shards this client runs, as an array or a `{ start, end }` range. `null` for all. |
71
- | `gateway` | `{}` | Extra `@discordjs/ws` `WebSocketManager` options (`compression`, `initialPresence`, ...). |
72
- | `cacheFailure` | `"skip"` | On a cache read/write failure, `"skip"` drops the event, `"emitUncached"` emits it from the payload. |
73
- | `dispatchTimeout` | `30_000` | Milliseconds after which a dispatch still processing is reported as a `DispatchTimeoutError`. `null` disables it. |
74
- | `sessionStore` | `undefined` | A `GatewaySessionStore` keeping the shards' sessions across restarts, see [Resuming sessions](#resuming-sessions-across-restarts). |
75
- | `sessionStoreTimeout` | `5_000` | Milliseconds a shard waits for `sessionStore` to read its session before identifying. `null` waits forever. |
76
- | `partials` | `[]` | The structures to build partially for uncached entities, see [Partials](#partials). |
77
- | `waitGuildTimeout` | `15_000` | Milliseconds `clientReady` waits for initially unavailable guilds before emitting anyway, see [Events](#events). |
63
+ | Option | Default | Description |
64
+ | --------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `intents` | — | The gateway intents. |
66
+ | `cacheConstructor` | `CollectionCache` | Builds `manager.cache`, the cache of structure instances each manager reads through, see [Cache](#cache). |
67
+ | `cacheOptions` | `undefined` | Per-entity options of those caches, e.g. `{ messages: { maxSize: 1_000 } }`, see [Bounding memory](#bounding-memory). |
68
+ | `cache` | `undefined` | A `@wolfstar/plugin-cache` cache whose raw stores back the managers instead, see [Cache](#cache). |
69
+ | `shardCount` | `null` | Total shards across every process, `null` for Discord's recommendation. |
70
+ | `shardIds` | `null` | The shards this client runs, as an array or a `{ start, end }` range. `null` for all. |
71
+ | `gateway` | `{}` | Extra `@discordjs/ws` `WebSocketManager` options (`compression`, `initialPresence`, ...). |
72
+ | `cacheFailure` | `"skip"` | On a cache read/write failure, `"skip"` drops the event, `"emitUncached"` emits it from the payload. |
73
+ | `dispatchTimeout` | `30_000` | Milliseconds after which a dispatch still processing is reported as a `DispatchTimeoutError`. `null` disables it. |
74
+ | `sessionStore` | `undefined` | A `GatewaySessionStore` keeping the shards' sessions across restarts, see [Resuming sessions](#resuming-sessions-across-restarts). |
75
+ | `sessionStoreTimeout` | `5_000` | Milliseconds a shard waits for `sessionStore` to read its session before identifying. `null` waits forever. |
76
+ | `partials` | `[]` | The structures to build partially for uncached entities, see [Partials](#partials). |
77
+ | `waitGuildTimeout` | `15_000` | Milliseconds `clientReady` waits for initially unavailable guilds before emitting anyway, see [Events](#events). |
78
78
 
79
79
  `client.gateway` exposes the underlying `WebSocketManager`, e.g. to send presence updates.
80
80
 
@@ -263,53 +263,185 @@ framework's `container.client`. The latter stays typed as the base `Client`: a m
263
263
  cannot redeclare it with another type (TypeScript reports TS2717, or silently keeps `Client` under
264
264
  `skipLibCheck`).
265
265
 
266
- ## Caching
266
+ ## Cache
267
+
268
+ Every manager exposes its cache as `manager.cache`, the `Cache` of the discord.js RFC:
269
+
270
+ ```typescript
271
+ const user = client.users.cache.get(userId);
272
+ const member = client.members.cache.get(client.members.resolveKey(guildId, userId));
273
+
274
+ client.users.cache.has(userId);
275
+ client.users.cache.getSize();
276
+ ```
267
277
 
268
- The cache only holds raw API data, managers build the structures:
278
+ By default, entities are kept in memory by `CollectionCache`, a `Collection` of structure instances: updates patch
279
+ the cached instance in place, as in discord.js, and every method is synchronous. Its relations (e.g. `member.voice`)
280
+ are re-resolved on every `cache.get`; instances reached by iterating the collection itself (`find`, `filter`, `map`,
281
+ ...) carry the relations of their last `get`.
269
282
 
270
- Unlike the structure cache in `discordjs/next`, each cache read here builds a fresh structure. This
271
- keeps the same behavior with in-memory and Redis stores and preserves the previous state of update
272
- events.
283
+ ```typescript
284
+ import { createRedisCache } from "@wolfstar/plugin-cache";
273
285
 
274
- | Manager | `get` / `fetch` / `refresh` arguments |
275
- | ----------------- | ------------------------------------- |
276
- | `client.users` | `userId` |
277
- | `client.guilds` | `guildId` |
278
- | `client.channels` | `channelId` (threads included) |
279
- | `client.threads` | `threadId` |
280
- | `client.messages` | `channelId`, `messageId` |
281
- | `client.members` | `guildId`, `userId` |
282
- | `client.roles` | `guildId`, `roleId` |
286
+ new GatewayClient({ intents }); // CollectionCache, in memory
287
+ new GatewayClient({ intents, cache: createRedisCache(redis) }); // raw data in Redis
288
+ new GatewayClient({ intents, cache: null }); // nothing is cached
289
+ new GatewayClient({ intents, cacheConstructor: MyCache }); // your own Cache, see "Custom caches"
290
+ ```
283
291
 
284
- - `get` only reads the cache, resolving to `undefined` on a miss;
292
+ > [!IMPORTANT]
293
+ > The default keeps **every entity the gateway sends in memory until a dispatch removes it** (`MESSAGE_DELETE`,
294
+ > `GUILD_DELETE`, ...): nothing expires, so messages, users, and presences grow for as long as the process runs.
295
+ > See [Bounding memory](#bounding-memory).
296
+
297
+ With a `@wolfstar/plugin-cache` store, the cache holds raw API data and builds a structure on every read: the
298
+ methods return promises when the store is remote (`await` works with every cache, `manager.cache.synchronous` tells
299
+ them apart, see [Synchronous reads](#synchronous-reads)), and two reads return two objects. `policies` apply to
300
+ every write, in every mode (dispatches and manager writes alike); their `ttl` only applies to plugin-cache stores,
301
+ see [Store-backed caches](#store-backed-caches).
302
+
303
+ `manager.fetch(...)` reads the cache first and falls back to the REST API. `manager.cache` is keyed by a single ID
304
+ for most managers, and by `manager.resolveKey(...)` for the ones taking more than one argument:
305
+
306
+ | Manager | `resolveKey` / `fetch` / `refresh` arguments |
307
+ | ----------------- | -------------------------------------------- |
308
+ | `client.users` | `userId` |
309
+ | `client.guilds` | `guildId` |
310
+ | `client.channels` | `channelId` (threads included) |
311
+ | `client.threads` | `threadId` |
312
+ | `client.messages` | `channelId`, `messageId` |
313
+ | `client.members` | `guildId`, `userId` |
314
+ | `client.roles` | `guildId`, `roleId` |
315
+
316
+ - `cache.get` only reads the cache, resolving to `undefined` on a miss;
285
317
  - `fetch` reads the cache, falling back to the REST API (and caching the result). Pass
286
318
  `{ force: true }` after the IDs to always hit the API, `{ cache: false }` not to store the result:
287
319
  `client.messages.fetch(channelId, messageId, { force: true })`;
288
320
  - `refresh` is `fetch` with `{ force: true }`;
289
- - `resolve` takes a structure (returned as is) or a cache key, like discord.js's `resolve`;
290
- - `cached` is `get` without the `await`, for a synchronous cache, see below.
291
-
292
- Like discord.js's `CachedManager#_add`, every API payload goes through the manager's `_add`, which
293
- merges it into the cached entry (the fields a partial payload lacks keep their cached value) and
294
- builds the structure. Relations are resolved from the cache too: `message.author` is the entry of
295
- `client.users`, `message.member` the one of `client.members`, and the same goes for
296
- `member.user`, `emoji.author`, `sticker.user`, and `invite.inviter`. Every structure of a guild
297
- (channels, threads, members, roles, messages, emojis, stickers, invites) has `guild`, the cached
298
- guild, and messages have `channel`. These are `null` when the entity is not cached; `fetchGuild()`
299
- and `fetchChannel()` always get it. `_add` is asynchronous,
300
- since the cache can be Redis. A structure built by hand, with `new Message(data)`, falls back to
301
- the copy embedded in its payload.
321
+ - `resolve` takes a structure (returned as is) or a cache key, like discord.js's `resolve`.
322
+
323
+ Every API payload is written to the cache: a cached entry is patched with it (the fields a partial payload lacks
324
+ keep their cached value) and the patched instance, or the newly built structure, is returned. Relations are
325
+ resolved from the cache too: `message.author` is the entry of `client.users`, `message.member` the one of
326
+ `client.members`, and the same goes for `member.user`, `emoji.author`, `sticker.user`, and `invite.inviter`. Every
327
+ structure of a guild (channels, threads, members, roles, messages, emojis, stickers, invites) has `guild`, built
328
+ from the cached guild, and messages have `channel`. These are `null` when the entity is not cached; `fetchGuild()`
329
+ and `fetchChannel()` always get it. A structure built by hand, with `new Message(data)`, falls back to the copy
330
+ embedded in its payload.
331
+
332
+ Which object a relation is depends on the relation. With the default cache, `message.author` and `message.channel`
333
+ are the cached instances, the very objects `client.users.cache.get(id)` and `client.channels.cache.get(id)` return.
334
+ `guild` is not: it is a shallow copy of the cached guild, holding the same data at the time the structure was read,
335
+ but `message.guild !== client.guilds.cache.get(message.guildId)`. Compare guilds by `id`, and read
336
+ `client.guilds.cache.get(id)` when you need the instance that later updates patch.
337
+
338
+ The same goes for what the client hands out outside of `manager.cache`: the structures delivered by events (the
339
+ message of `messageCreate`, the `new` of update events such as `guildMemberUpdate`, ...) and the ones `listCached`
340
+ returns are freshly built from the cache, they are not the cached instances and later dispatches do not patch them.
341
+ The previous state of update and delete events is a copy of the cached instance taken before the write; with the
342
+ default cache it carries the relations of the entity's last read rather than re-resolving them.
343
+ Only `manager.cache.get` (and `fetch`, `resolve`, which read it) returns the cached instance.
344
+
345
+ ### Bounding memory
346
+
347
+ With the default cache, nothing is evicted unless a dispatch removes it. There are three ways to bound it:
348
+
349
+ - **`cacheOptions`** sets a `maxSize` per entity: once reached, the oldest entry is evicted for each new one, and
350
+ `0` holds nothing. It is passed to the `cacheConstructor`, the default `CollectionCache` included.
351
+ The bound is per entity, not per channel: unlike discord.js, a busy channel can evict the messages of a quiet
352
+ one.
353
+
354
+ `client.channels.cache` spans the channel and the thread caches, so it is a `Cache` but not a `Collection`: it
355
+ has no `size`, `filter`, `find` or iteration, and `maxSize` applies to channels and threads separately.
356
+
357
+ ```ts
358
+ // The 1000 most recent messages, and no presence.
359
+ new GatewayClient({
360
+ intents,
361
+ cacheOptions: { messages: { maxSize: 1_000 }, presences: { maxSize: 0 } },
362
+ });
363
+ ```
364
+
365
+ - **`policies.filter`** decides entry by entry: a rejected entry is not cached, and the entry already cached under
366
+ its key is deleted.
367
+
368
+ ```ts
369
+ // No bot user, and no message written by a bot.
370
+ new GatewayClient({
371
+ intents,
372
+ policies: {
373
+ users: { filter: (user) => !user.bot },
374
+ messages: { filter: (message) => !message.author.bot },
375
+ },
376
+ });
377
+ ```
302
378
 
303
- Swapping `createInMemoryCache()` for `createRedisCache({ redis })` changes nothing else, see
304
- [`@wolfstar/plugin-cache`](../plugin-cache).
379
+ - **`cache: null`** caches nothing at all: `cache.get` resolves to `undefined` and `fetch` always hits the API.
380
+
381
+ ```ts
382
+ new GatewayClient({ intents, cache: null });
383
+ ```
384
+
385
+ `cacheOptions` only applies to caches built by a constructor: like `cacheConstructor`, combining it with `cache` or
386
+ `makeCache` throws (those stores have their own bounds), and it is ignored with `cache: null`.
387
+
388
+ ### Custom caches
305
389
 
306
- ### Zero caching and cache control
390
+ `cacheConstructor` takes a class implementing `Cache`, instantiated once per entity with
391
+ `(creator, name, options)`:
307
392
 
308
- Every feature works without a cache, following the
309
- [discord.js RFC #11426](https://github.com/discordjs/discord.js/issues/11426): leave `cache` out
310
- and every `fetch` hits the API, every event is still emitted, and the previous state of update and
311
- delete events is `null` (or a partial, see [Partials](#partials)). Each entity kind can be cached,
312
- or not, on its own:
393
+ - `creator` builds a structure out of raw data: it is the cache's `construct`;
394
+ - `name` is the entity's name, e.g. `"users"`;
395
+ - `options` (`CacheConstructorOptions`) carries:
396
+ - `keyOf(data)`, the cache key of raw data. `add` **must** key its entries with it: most entities are not keyed
397
+ by `id` (a member is keyed by guild and user, a message by channel and ID, ...);
398
+ - `refresh(value)`, which resolves the relations of an instance again and returns it. Call it on what `get` and
399
+ `add` hand out, or long-lived instances keep the relations of the day they were built (`member.voice` would
400
+ not follow `VOICE_STATE_UPDATE`);
401
+ - the entity's `cacheOptions`, i.e. `maxSize`.
402
+
403
+ The recommended way is to extend `CollectionCache`, which does all of this:
404
+
405
+ ```ts
406
+ import {
407
+ CollectionCache,
408
+ type CacheConstructorOptions,
409
+ type RawAPIType,
410
+ type StructureCreator,
411
+ type StructureMixin,
412
+ } from "@wolfstar/plugin-gateway";
413
+ import type { CacheEntityName } from "@wolfstar/plugin-cache";
414
+
415
+ class BoundedCache<
416
+ Value extends StructureMixin<object>,
417
+ Raw extends RawAPIType<Value> = RawAPIType<Value>,
418
+ > extends CollectionCache<Value, Raw> {
419
+ public constructor(
420
+ creator: StructureCreator<Value, Raw>,
421
+ name: CacheEntityName,
422
+ options: CacheConstructorOptions<Value, Raw>,
423
+ ) {
424
+ // Forward `keyOf` and `refresh`, with a default bound for the entities `cacheOptions` does not set.
425
+ super(creator, name, { ...options, maxSize: options.maxSize ?? 10_000 });
426
+ }
427
+ }
428
+
429
+ new GatewayClient({ intents, cacheConstructor: BoundedCache });
430
+ ```
431
+
432
+ - The cache must be synchronous: structures are built synchronously from it.
433
+ - A cache that is not a `Map` cannot be enumerated, so what needs to list its entries does not work with it: the
434
+ dispatch cascades (`GUILD_DELETE` and `CHANNEL_DELETE` leave the guild's or channel's entries behind), the
435
+ reconciliation of guilds left while offline on `READY`, the granular emoji and sticker diff events, and
436
+ `listCached`.
437
+ - `cacheConstructor` cannot be combined with `cache` or `makeCache` (it throws). With `cache: null`, `null` wins:
438
+ nothing is cached and the class is never instantiated.
439
+
440
+ ### Store-backed caches
441
+
442
+ `cache` and `makeCache` (`@wolfstar/plugin-cache`) keep the managers backed by raw stores, in memory or Redis,
443
+ instead of `CollectionCache`: a structure is built on every read. `makeCache` takes precedence over `cache`, and is
444
+ called once per entity kind, `null` not to cache that kind. `cacheConstructor` cannot be combined with either.
313
445
 
314
446
  ```ts
315
447
  import { MemoryEntityCache } from "@wolfstar/plugin-cache";
@@ -319,52 +451,54 @@ const client = new GatewayClient({
319
451
  // Called once per entity kind: `null` not to cache it.
320
452
  makeCache: (entity) =>
321
453
  ["guilds", "channels", "roles"].includes(entity) ? new MemoryEntityCache() : null,
322
- // Entry by entry, for dispatches and managers alike.
454
+ // Entry by entry, for dispatches and managers alike; `ttl` only applies to these stores.
323
455
  policies: { users: { filter: (user) => !user.bot }, messages: { ttl: () => 3_600_000 } },
324
456
  // A failing store (e.g. Redis down) is a cache miss, reported through `cacheError`.
325
457
  cacheErrors: "miss",
326
458
  });
327
459
  ```
328
460
 
329
- `makeCache` takes precedence over `cache`, and `policies` apply to either. What needs a store:
330
-
331
- | Without the store of… | What happens |
332
- | ---------------------------- | -------------------------------------------------------------------------- |
333
- | any entity | `get` resolves to `undefined`, `fetch` hits the API, previous state `null` |
334
- | `emojis` / `stickers` | only `guildEmojisUpdate` / `guildStickersUpdate`, no granular diff events |
335
- | `guilds` (able to enumerate) | guilds left while offline are not reconciled on `READY` |
336
- | `presences` | `presences.fetch` rejects: presences only come from the gateway |
337
- | `roles` | overwrite types and member roles are read from one `GET /guilds/:id/roles` |
338
- | `threadMembers` | `thread.joined` is `null` unless the payload carries the bot's member |
339
- | a relation's entity | the relation getter (`message.guild`, `member.voice`, ...) returns `null` |
461
+ Swapping `createInMemoryCache()` for `createRedisCache({ redis })` changes nothing else, see
462
+ [`@wolfstar/plugin-cache`](../plugin-cache). Every feature still works without a store for an entity kind (or with
463
+ `cache: null`, for every entity), following the
464
+ [discord.js RFC #11426](https://github.com/discordjs/discord.js/issues/11426): every event is still emitted, and
465
+ the previous state of update and delete events is `null` (or a partial, see [Partials](#partials)). What else needs
466
+ a store:
467
+
468
+ | Without the store of… | What happens |
469
+ | ---------------------------- | -------------------------------------------------------------------------------- |
470
+ | any entity | `cache.get` resolves to `undefined`, `fetch` hits the API, previous state `null` |
471
+ | `emojis` / `stickers` | only `guildEmojisUpdate` / `guildStickersUpdate`, no granular diff events |
472
+ | `guilds` (able to enumerate) | guilds left while offline are not reconciled on `READY` |
473
+ | `presences` | `presences.fetch` rejects: presences only come from the gateway |
474
+ | `roles` | overwrite types and member roles are read from one `GET /guilds/:id/roles` |
475
+ | `threadMembers` | `thread.joined` is `null` unless the payload carries the bot's member |
476
+ | a relation's entity | the relation getter (`message.guild`, `member.voice`, ...) returns `null` |
340
477
 
341
478
  `listCached` resolves to `[]` without a store, and throws a `TypeError` for a store that cannot
342
479
  enumerate its entries. With `cacheErrors: "throw"`, a failing store rejects instead of missing.
343
480
 
344
481
  ### Synchronous reads
345
482
 
346
- Every manager method above is asynchronous since the cache can be Redis, which makes a hot path
347
- such as a message filter await every lookup even when the cache lives in memory. With a synchronous
348
- cache (`createInMemoryCache()`), `cached` takes the same arguments as `get` and returns the same
349
- structure, relations included, without a promise:
483
+ `manager.cache`'s methods are `Awaitable`: synchronous with the default `CollectionCache`, a promise with a
484
+ `@wolfstar/plugin-cache` store, unless every store the entity's relations are read from is synchronous too.
485
+ `manager.cache.synchronous` tells them apart, so a hot path such as a message filter can skip `await` when it can:
350
486
 
351
487
  ```ts
352
- client.on("messageCreate", (message) => {
353
- const member = client.members.cached(message.guildId!, message.author.id);
354
- if (member?.roleIds.includes(mutedRoleId)) return;
355
- // ...
356
- });
488
+ const user = client.users.cache.synchronous
489
+ ? client.users.cache.get(userId)
490
+ : await client.users.cache.get(userId);
357
491
  ```
358
492
 
359
- It returns `undefined` on a miss, or when the entity is not cached, like `get`. An asynchronous cache
360
- cannot tell a miss apart without awaiting it, so there `cached` throws a `TypeError` rather than
361
- silently returning `undefined`; the same goes for a custom cache mixing both kinds, when a relation
362
- lives in an asynchronous entity cache. `manager.cache?.synchronous` tells which path to take:
493
+ `await` works either way, since a non-promise value resolves to itself:
363
494
 
364
495
  ```ts
365
- const user = client.users.cache?.synchronous
366
- ? client.users.cached(userId)
367
- : await client.users.get(userId);
496
+ client.on("messageCreate", async (message) => {
497
+ const key = message.guildId && client.members.resolveKey(message.guildId, message.author.id);
498
+ const member = key ? await client.members.cache.get(key) : undefined;
499
+ if (member?.roleIds.includes(mutedRoleId)) return;
500
+ // ...
501
+ });
368
502
  ```
369
503
 
370
504
  ## Resuming sessions across restarts
@@ -394,8 +528,8 @@ process.once("SIGTERM", async () => {
394
528
  ```
395
529
 
396
530
  - **Pair it with a persistent cache.** A resumed session only replays the missed events, not the
397
- guilds: with `createInMemoryCache()`, a restarted process resumes with an empty cache that
398
- `GUILD_CREATE` never refills.
531
+ guilds: with an in-memory cache, the default `CollectionCache` or `createInMemoryCache()`, a
532
+ restarted process resumes with an empty cache that `GUILD_CREATE` never refills.
399
533
  - **Shut down with `destroy({ resumable: true })`.** By default `destroy()` closes the connections
400
534
  with code `1000`, which makes Discord invalidate the sessions, and `@discordjs/ws` drops them from
401
535
  the store. With `resumable`, the shards close with code `4200` and their sessions stay stored.
@@ -512,9 +646,9 @@ The client also has discord.js's `fetchSticker`, `fetchStickerPacks`, and `fetch
512
646
 
513
647
  ### Users, members and roles
514
648
 
515
- They follow discord.js's API, with one difference: anything discord.js reads synchronously from its
516
- cache is asynchronous here, since the cache can be Redis. With an in-memory cache, `cached` reads
517
- `client.users`, `client.members` and `client.roles` synchronously, see
649
+ They follow discord.js's API. `client.users.cache`, `client.members.cache`, and `client.roles.cache`
650
+ read synchronously with the default `CollectionCache`, like discord.js; only a `@wolfstar/plugin-cache`
651
+ store that is not synchronous (e.g. Redis) makes them asynchronous, see
518
652
  [Synchronous reads](#synchronous-reads).
519
653
 
520
654
  ```ts
@@ -544,7 +678,7 @@ apply through `member.fetchPermissionsIn(channel)`, see below.
544
678
  `guild.members.fetch()`) asks the guild's shard for its members over the gateway instead of REST:
545
679
  every member by default, those matching a `query` (with a `limit`), or up to 100 `userIds`, with
546
680
  their `presences` if asked. It resolves with the `GuildMember`s once Discord's last
547
- `GUILD_MEMBERS_CHUNK` for its `nonce` is cached, so `client.members.get` sees them, and rejects
681
+ `GUILD_MEMBERS_CHUNK` for its `nonce` is cached, so `client.members.cache.get` sees them, and rejects
548
682
  with a `GuildMembersTimeoutError` when no chunk arrives for `time` milliseconds (120 seconds by
549
683
  default), or a `GuildMembersRateLimitError` when Discord answers with `RATE_LIMITED`. Every chunk
550
684
  is also emitted as `guildMembersChunk`, whose `data.not_found` lists the requested IDs that are