@wolfstar/plugin-gateway 0.2.0-next-20260926221949 → 0.2.0-next-20260926222321

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/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Client, Listener, container } from "@wolfstar/http-framework";
2
- import { WebSocketManager, WebSocketShardEvents } from "@discordjs/ws";
2
+ import { CloseCodes, DefaultWebSocketManagerOptions, WebSocketManager, WebSocketShardEvents } from "@discordjs/ws";
3
3
  import { Client as Client$1 } from "@discordjs/core";
4
4
  import { applyGatewayDispatch, autoModerationRuleKey, banKey, emojiKey, integrationKey, inviteKey, memberKey, messageKey, presenceKey, roleKey, scheduledEventKey, soundboardSoundKey, stageInstanceKey, stickerKey, threadMemberKey, voiceStateKey } from "@wolfstar/plugin-cache";
5
5
  import { ActivityFlags, ActivityType, AttachmentFlags, AuditLogEvent, ChannelFlags, ChannelType, FormattingPatterns, GatewayDispatchEvents, GatewayIntentBits, GatewayOpcodes, GuildFeature, GuildMemberFlags, GuildPremiumTier, GuildScheduledEventStatus, GuildSystemChannelFlags, GuildWidgetStyle, InviteType, MessageFlags, MessageReferenceType, MessageType, OverwriteType, PermissionFlagsBits, PresenceUpdateStatus, RoleFlags, RouteBases, Routes, StickerFormatType, UserFlags, WebhookType } from "discord-api-types/v10";
@@ -6915,6 +6915,29 @@ var GuildMembersRateLimitError = class extends Error {
6915
6915
  this.retryAfter = retryAfter;
6916
6916
  }
6917
6917
  };
6918
+ /**
6919
+ * Emitted as an `error` when `GatewayClientOptions.sessionStore` fails to read or write the session of a shard.
6920
+ *
6921
+ * @remarks
6922
+ * Neither failure stops the shard: a failed (or timed out) read identifies instead of resuming, and a failed write
6923
+ * only means the session may not be resumed after a restart. The store's error is the `cause`.
6924
+ */
6925
+ var GatewaySessionStoreError = class extends Error {
6926
+ /**
6927
+ * Whether the store failed to read (`"get"`) or write (`"set"`) the session.
6928
+ */
6929
+ operation;
6930
+ /**
6931
+ * The shard whose session was read or written.
6932
+ */
6933
+ shardId;
6934
+ constructor(operation, shardId, cause) {
6935
+ super(`Cannot ${operation === "get" ? "read" : "write"} the session of shard ${shardId} in the session store`, { cause });
6936
+ this.name = "GatewaySessionStoreError";
6937
+ this.operation = operation;
6938
+ this.shardId = shardId;
6939
+ }
6940
+ };
6918
6941
 
6919
6942
  //#endregion
6920
6943
  //#region src/managers/GuildMemberManager.ts
@@ -11206,6 +11229,79 @@ var DispatchQueue = class {
11206
11229
  }
11207
11230
  };
11208
11231
 
11232
+ //#endregion
11233
+ //#region src/util/sessions.ts
11234
+ /**
11235
+ * Sits between `@discordjs/ws` and a {@link GatewaySessionStore}, which `@discordjs/ws` would otherwise read on every
11236
+ * dispatch and heartbeat, and write on every sequence change, while holding the dispatch back.
11237
+ *
11238
+ * @remarks
11239
+ * The session of a shard is read from the store once, when the shard first connects, then served from memory. Writes
11240
+ * never hold a dispatch back either: they run in the background, one at a time per shard, and the ones queued while a
11241
+ * write is in flight collapse into a single write of the latest session. A stored sequence is therefore slightly
11242
+ * behind after a crash, which Discord handles by replaying the dispatches since then.
11243
+ *
11244
+ * @internal
11245
+ */
11246
+ var GatewaySessionMirror = class {
11247
+ #store;
11248
+ #timeout;
11249
+ #onError;
11250
+ #sessions = /* @__PURE__ */ new Map();
11251
+ #pending = /* @__PURE__ */ new Map();
11252
+ #writes = /* @__PURE__ */ new Map();
11253
+ constructor(store, timeout, onError) {
11254
+ this.#store = store;
11255
+ this.#timeout = timeout;
11256
+ this.#onError = onError;
11257
+ }
11258
+ async get(shardId) {
11259
+ if (this.#sessions.has(shardId)) return this.#sessions.get(shardId);
11260
+ const session = await this.read(shardId);
11261
+ if (!this.#sessions.has(shardId)) this.#sessions.set(shardId, session);
11262
+ return this.#sessions.get(shardId);
11263
+ }
11264
+ set(shardId, info) {
11265
+ this.#sessions.set(shardId, info);
11266
+ const queued = this.#pending.has(shardId);
11267
+ this.#pending.set(shardId, info);
11268
+ if (queued) return;
11269
+ const previous = this.#writes.get(shardId) ?? Promise.resolve();
11270
+ this.#writes.set(shardId, previous.then(() => this.write(shardId)));
11271
+ }
11272
+ /**
11273
+ * Resolves once every session written so far reached the store.
11274
+ */
11275
+ async flush() {
11276
+ await Promise.all(this.#writes.values());
11277
+ }
11278
+ async read(shardId) {
11279
+ let timer;
11280
+ try {
11281
+ const read = Promise.resolve(this.#store.get(shardId));
11282
+ if (this.#timeout === null) return await read;
11283
+ const timeout = this.#timeout;
11284
+ return await Promise.race([read, new Promise((_, reject) => {
11285
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timed out after ${timeout}ms`)), timeout);
11286
+ })]);
11287
+ } catch (error) {
11288
+ this.#onError(new GatewaySessionStoreError("get", shardId, error));
11289
+ return null;
11290
+ } finally {
11291
+ if (timer) clearTimeout(timer);
11292
+ }
11293
+ }
11294
+ async write(shardId) {
11295
+ const info = this.#pending.get(shardId);
11296
+ this.#pending.delete(shardId);
11297
+ try {
11298
+ await this.#store.set(shardId, info);
11299
+ } catch (error) {
11300
+ this.#onError(new GatewaySessionStoreError("set", shardId, error));
11301
+ }
11302
+ }
11303
+ };
11304
+
11209
11305
  //#endregion
11210
11306
  //#region src/GatewayClient.ts
11211
11307
  /**
@@ -11267,6 +11363,9 @@ var GatewayClient = class extends Client {
11267
11363
  dispatchTimeout;
11268
11364
  #queue = new DispatchQueue();
11269
11365
  #shardCount;
11366
+ #sessions = null;
11367
+ #keepSessions = false;
11368
+ #destroying = null;
11270
11369
  constructor(options) {
11271
11370
  super(options);
11272
11371
  container.gatewayClient = this;
@@ -11288,6 +11387,7 @@ var GatewayClient = class extends Client {
11288
11387
  this.presences = new PresenceManager(this);
11289
11388
  this.gateway = new WebSocketManager({
11290
11389
  ...options.gateway,
11390
+ ...this.sessionCallbacks(options),
11291
11391
  token: options.discordToken ?? process.env.DISCORD_TOKEN,
11292
11392
  intents: options.intents,
11293
11393
  rest: container.rest,
@@ -11322,9 +11422,33 @@ var GatewayClient = class extends Client {
11322
11422
  }
11323
11423
  /**
11324
11424
  * Disconnects every shard from the gateway. The HTTP server, if listening, is left untouched.
11425
+ *
11426
+ * @remarks
11427
+ * By default, the shards close their sessions, which Discord invalidates. Pass `resumable: true` to keep them
11428
+ * resumable instead, e.g. on a graceful shutdown before a deploy. Either way, it waits for the sessions to be
11429
+ * written to {@link GatewayClientOptions.sessionStore}.
11430
+ *
11431
+ * A call made while another one is still disconnecting the shards joins it, keeping the first call's `resumable`.
11432
+ *
11433
+ * @param options Whether to keep the sessions resumable.
11325
11434
  */
11326
- async destroy() {
11327
- await this.gateway.destroy();
11435
+ destroy(options = {}) {
11436
+ this.#destroying ??= this.disconnect(options.resumable ?? false).finally(() => {
11437
+ this.#destroying = null;
11438
+ });
11439
+ return this.#destroying;
11440
+ }
11441
+ async disconnect(resumable) {
11442
+ this.#keepSessions = resumable;
11443
+ try {
11444
+ await this.gateway.destroy(this.#keepSessions ? {
11445
+ code: CloseCodes.Resuming,
11446
+ reason: "Resumable shutdown"
11447
+ } : {});
11448
+ } finally {
11449
+ this.#keepSessions = false;
11450
+ }
11451
+ await this.#sessions?.flush();
11328
11452
  await this.idle();
11329
11453
  }
11330
11454
  /**
@@ -11503,6 +11627,23 @@ var GatewayClient = class extends Client {
11503
11627
  if (timer) clearTimeout(timer);
11504
11628
  }
11505
11629
  }
11630
+ sessionCallbacks(options) {
11631
+ const { sessionStore, gateway } = options;
11632
+ if (sessionStore && (gateway?.retrieveSessionInfo || gateway?.updateSessionInfo)) throw new TypeError("sessionStore replaces gateway.retrieveSessionInfo and gateway.updateSessionInfo, pass one or the other");
11633
+ let retrieve = gateway?.retrieveSessionInfo ?? DefaultWebSocketManagerOptions.retrieveSessionInfo;
11634
+ let update = gateway?.updateSessionInfo ?? DefaultWebSocketManagerOptions.updateSessionInfo;
11635
+ if (sessionStore) {
11636
+ const timeout = options.sessionStoreTimeout === void 0 ? 5e3 : options.sessionStoreTimeout;
11637
+ const sessions = new GatewaySessionMirror(sessionStore, timeout, (error) => this.reportError(error, `the session store ${error.operation}`, error.shardId));
11638
+ this.#sessions = sessions;
11639
+ retrieve = (shardId) => sessions.get(shardId);
11640
+ update = (shardId, info) => sessions.set(shardId, info);
11641
+ }
11642
+ return {
11643
+ retrieveSessionInfo: retrieve,
11644
+ updateSessionInfo: (shardId, info) => info === null && this.#keepSessions ? void 0 : update(shardId, info)
11645
+ };
11646
+ }
11506
11647
  reportError(error, type, shardId) {
11507
11648
  if (this.listenerCount("error") > 0) this.emit("error", error);
11508
11649
  else this.logger.error(`[Gateway] [Shard ${shardId}] Failed to process ${type}:`, error);
@@ -11547,5 +11688,5 @@ function RegisterAsGatewayListener(event, options = {}) {
11547
11688
  }
11548
11689
 
11549
11690
  //#endregion
11550
- export { Action, ActionsManager, Activity, ActivityFlagsBitField, AnnouncementChannel, AnnouncementThreadChannel, AnonymousGuild, AppliedTagsMixin, Attachment, AttachmentFlagsBitField, AutoModerationActionExecution, AutoModerationRule, AutoModerationRuleManager, BaseChannel, BaseChannelMixin, BaseInvite, BitField, CachedManager, CategoryChannel, Channel, ChannelFlagsBitField, ChannelManager, ChannelMessageManager, ChannelOwnerMixin, ChannelParentMixin, ChannelPermissionMixin, ChannelSlowmodeMixin, ChannelThreadManager, ChannelThreadsMixin, ChannelTopicMixin, ChannelWebhooksMixin, ClientUser, DMChannel, DMChannelMixin, DispatchAction, DispatchHandlers, DispatchQueue, DispatchTimeoutError, Embed, Emoji, EventGatewayListener, ForumChannel, GatewayClient, GroupDMChannel, GroupDMInvite, GroupDMMixin, Guild, GuildAuditLogsEntry, GuildBan, GuildBanManager, GuildChannelManager, GuildChannelMixin, GuildEmoji, GuildEmojiManager, GuildEmojiRoleManager, GuildIntegrationManager, GuildInvite, GuildInviteManager, GuildManager, GuildMember, GuildMemberFlagsBitField, GuildMemberManager, GuildMemberRoleManager, GuildMembersRateLimitError, GuildMembersTimeoutError, GuildOnboarding, GuildOnboardingPrompt, GuildOnboardingPromptOption, GuildPreview, GuildScheduledEvent, GuildScheduledEventManager, GuildSoundboardSoundManager, GuildStickerManager, GuildTemplate, GuildTemplateManager, Integration, IntentsBitField, InviteGuild, MediaChannel, Message, MessageFlagsBitField, MessageManager, MessageMentions, MessageReaction, Mixin, MultiDispatchAction, MultiDispatchHandlers, NumberFlagsBitField, PermissionOverwriteManager, PermissionOverwrites, PermissionsBitField, Poll, PollAnswer, Presence, PresenceManager, PrivateThreadChannel, PublicThreadChannel, ReactionEmoji, ReactionManager, ReactionUserManager, RegisterAsGatewayListener, RichPresenceAssets, Role, RoleFlagsBitField, RoleManager, SoundboardSound, StageChannel, StageInstance, StageInstanceManager, Sticker, StickerPack, Structure, SystemChannelFlagsBitField, TextChannel, TextChannelMixin, ThreadChannelMemberManager, ThreadChannelMixin, ThreadManager, ThreadMember, ThreadMemberManager, ThreadOnlyChannelMixin, Typing, User, UserFlagsBitField, UserManager, VoiceChannel, VoiceChannelMixin, VoiceState, VoiceStateManager, Webhook, WebhookManager, WelcomeScreen, Widget, WidgetMember, compareRolePositions, computeChannelPermissions, computeGuildPermissions, computePermissionsIn, computeTargetPermissions, createChannel, createInvite, dispatchPartition, getGatewayClient, isDeepEqual, isThreadChannelType, kClone, kData, kPatch, kRelations, resolveId, resolveMessageOptions, resolveOverwriteOptions, snowflakeTimestamp, toAPIOverwrite, toChannelBody };
11691
+ export { Action, ActionsManager, Activity, ActivityFlagsBitField, AnnouncementChannel, AnnouncementThreadChannel, AnonymousGuild, AppliedTagsMixin, Attachment, AttachmentFlagsBitField, AutoModerationActionExecution, AutoModerationRule, AutoModerationRuleManager, BaseChannel, BaseChannelMixin, BaseInvite, BitField, CachedManager, CategoryChannel, Channel, ChannelFlagsBitField, ChannelManager, ChannelMessageManager, ChannelOwnerMixin, ChannelParentMixin, ChannelPermissionMixin, ChannelSlowmodeMixin, ChannelThreadManager, ChannelThreadsMixin, ChannelTopicMixin, ChannelWebhooksMixin, ClientUser, DMChannel, DMChannelMixin, DispatchAction, DispatchHandlers, DispatchQueue, DispatchTimeoutError, Embed, Emoji, EventGatewayListener, ForumChannel, GatewayClient, GatewaySessionStoreError, GroupDMChannel, GroupDMInvite, GroupDMMixin, Guild, GuildAuditLogsEntry, GuildBan, GuildBanManager, GuildChannelManager, GuildChannelMixin, GuildEmoji, GuildEmojiManager, GuildEmojiRoleManager, GuildIntegrationManager, GuildInvite, GuildInviteManager, GuildManager, GuildMember, GuildMemberFlagsBitField, GuildMemberManager, GuildMemberRoleManager, GuildMembersRateLimitError, GuildMembersTimeoutError, GuildOnboarding, GuildOnboardingPrompt, GuildOnboardingPromptOption, GuildPreview, GuildScheduledEvent, GuildScheduledEventManager, GuildSoundboardSoundManager, GuildStickerManager, GuildTemplate, GuildTemplateManager, Integration, IntentsBitField, InviteGuild, MediaChannel, Message, MessageFlagsBitField, MessageManager, MessageMentions, MessageReaction, Mixin, MultiDispatchAction, MultiDispatchHandlers, NumberFlagsBitField, PermissionOverwriteManager, PermissionOverwrites, PermissionsBitField, Poll, PollAnswer, Presence, PresenceManager, PrivateThreadChannel, PublicThreadChannel, ReactionEmoji, ReactionManager, ReactionUserManager, RegisterAsGatewayListener, RichPresenceAssets, Role, RoleFlagsBitField, RoleManager, SoundboardSound, StageChannel, StageInstance, StageInstanceManager, Sticker, StickerPack, Structure, SystemChannelFlagsBitField, TextChannel, TextChannelMixin, ThreadChannelMemberManager, ThreadChannelMixin, ThreadManager, ThreadMember, ThreadMemberManager, ThreadOnlyChannelMixin, Typing, User, UserFlagsBitField, UserManager, VoiceChannel, VoiceChannelMixin, VoiceState, VoiceStateManager, Webhook, WebhookManager, WelcomeScreen, Widget, WidgetMember, compareRolePositions, computeChannelPermissions, computeGuildPermissions, computePermissionsIn, computeTargetPermissions, createChannel, createInvite, dispatchPartition, getGatewayClient, isDeepEqual, isThreadChannelType, kClone, kData, kPatch, kRelations, resolveId, resolveMessageOptions, resolveOverwriteOptions, snowflakeTimestamp, toAPIOverwrite, toChannelBody };
11551
11692
  //# sourceMappingURL=index.js.map