chapterjs 0.1.2 → 0.2.0

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.
Files changed (318) hide show
  1. package/dist/assets/asset.d.ts +46 -0
  2. package/dist/assets/asset.js +86 -0
  3. package/dist/assets/public.d.ts +19 -0
  4. package/dist/assets/public.js +120 -0
  5. package/dist/cache/cache.d.ts +62 -0
  6. package/dist/cache/cache.js +80 -0
  7. package/dist/cache/store.d.ts +45 -0
  8. package/dist/cache/store.js +53 -0
  9. package/dist/cli/build.d.ts +41 -0
  10. package/dist/cli/build.js +205 -0
  11. package/dist/cli/cli.js +108 -25
  12. package/dist/cli/cluster.d.ts +70 -0
  13. package/dist/cli/cluster.js +213 -0
  14. package/dist/cli/dev.d.ts +18 -1
  15. package/dist/cli/dev.js +253 -198
  16. package/dist/cli/env.d.ts +31 -0
  17. package/dist/cli/env.js +93 -0
  18. package/dist/cli/log.d.ts +13 -0
  19. package/dist/cli/log.js +17 -0
  20. package/dist/cli/node-version.d.ts +8 -0
  21. package/dist/cli/node-version.js +13 -0
  22. package/dist/cli/preflight.d.ts +36 -0
  23. package/dist/cli/preflight.js +122 -0
  24. package/dist/cli/project.d.ts +135 -0
  25. package/dist/cli/project.js +517 -0
  26. package/dist/cli/start.d.ts +25 -0
  27. package/dist/cli/start.js +225 -0
  28. package/dist/commands/autocomplete.d.ts +13 -0
  29. package/dist/commands/autocomplete.js +159 -0
  30. package/dist/commands/command.d.ts +360 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/declaration.d.ts +77 -0
  33. package/dist/commands/declaration.js +492 -0
  34. package/dist/commands/register.d.ts +18 -0
  35. package/dist/commands/register.js +42 -0
  36. package/dist/commands/router.d.ts +20 -0
  37. package/dist/commands/router.js +228 -0
  38. package/dist/commands/tree.d.ts +47 -0
  39. package/dist/commands/tree.js +216 -0
  40. package/dist/commands/twins.d.ts +15 -0
  41. package/dist/commands/twins.js +78 -0
  42. package/dist/components/button.d.ts +79 -0
  43. package/dist/components/button.js +106 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/custom-id.d.ts +72 -0
  47. package/dist/components/custom-id.js +128 -0
  48. package/dist/components/declaration.d.ts +42 -0
  49. package/dist/components/declaration.js +342 -0
  50. package/dist/components/declared.d.ts +29 -0
  51. package/dist/components/declared.js +49 -0
  52. package/dist/components/embed.d.ts +29 -0
  53. package/dist/components/embed.js +11 -0
  54. package/dist/components/instance.d.ts +100 -0
  55. package/dist/components/instance.js +86 -0
  56. package/dist/components/layout.d.ts +103 -0
  57. package/dist/components/layout.js +279 -0
  58. package/dist/components/modal.d.ts +245 -0
  59. package/dist/components/modal.js +398 -0
  60. package/dist/components/options.d.ts +41 -0
  61. package/dist/components/options.js +101 -0
  62. package/dist/components/render.d.ts +12 -0
  63. package/dist/components/render.js +58 -0
  64. package/dist/components/router.d.ts +19 -0
  65. package/dist/components/router.js +251 -0
  66. package/dist/components/select.d.ts +146 -0
  67. package/dist/components/select.js +179 -0
  68. package/dist/core/bot.d.ts +99 -0
  69. package/dist/core/bot.js +174 -0
  70. package/dist/core/memory.d.ts +66 -0
  71. package/dist/core/memory.js +115 -0
  72. package/dist/discord/api.d.ts +112 -0
  73. package/dist/discord/api.js +115 -0
  74. package/dist/discord/cdn.d.ts +67 -0
  75. package/dist/discord/cdn.js +100 -0
  76. package/dist/discord/codes.d.ts +171 -0
  77. package/dist/discord/codes.js +421 -0
  78. package/dist/discord/endpoint.d.ts +56 -0
  79. package/dist/discord/endpoint.js +53 -0
  80. package/dist/discord/endpoints.d.ts +1529 -0
  81. package/dist/discord/endpoints.js +1139 -0
  82. package/dist/discord/formatting.d.ts +67 -0
  83. package/dist/discord/formatting.js +87 -0
  84. package/dist/discord/index.d.ts +10 -0
  85. package/dist/discord/index.js +13 -0
  86. package/dist/discord/intents.d.ts +48 -0
  87. package/dist/discord/intents.js +128 -0
  88. package/dist/discord/permissions.d.ts +94 -0
  89. package/dist/discord/permissions.js +185 -0
  90. package/dist/discord/snowflake.d.ts +21 -0
  91. package/dist/discord/snowflake.js +36 -0
  92. package/dist/discord/types/application-command.d.ts +332 -0
  93. package/dist/discord/types/application-command.js +48 -0
  94. package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
  95. package/dist/discord/types/application-role-connection-metadata.js +22 -0
  96. package/dist/discord/types/application.d.ts +208 -0
  97. package/dist/discord/types/application.js +48 -0
  98. package/dist/discord/types/audit-log.d.ts +255 -0
  99. package/dist/discord/types/audit-log.js +144 -0
  100. package/dist/discord/types/auto-moderation.d.ts +167 -0
  101. package/dist/discord/types/auto-moderation.js +52 -0
  102. package/dist/discord/types/channel.d.ts +557 -0
  103. package/dist/discord/types/channel.js +88 -0
  104. package/dist/discord/types/common.d.ts +54 -0
  105. package/dist/discord/types/common.js +38 -0
  106. package/dist/discord/types/component.d.ts +775 -0
  107. package/dist/discord/types/component.js +76 -0
  108. package/dist/discord/types/emoji.d.ts +74 -0
  109. package/dist/discord/types/emoji.js +1 -0
  110. package/dist/discord/types/entitlement.d.ts +84 -0
  111. package/dist/discord/types/entitlement.js +22 -0
  112. package/dist/discord/types/gateway-events.d.ts +1148 -0
  113. package/dist/discord/types/gateway-events.js +54 -0
  114. package/dist/discord/types/gateway.d.ts +38 -0
  115. package/dist/discord/types/gateway.js +1 -0
  116. package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
  117. package/dist/discord/types/guild-scheduled-event.js +68 -0
  118. package/dist/discord/types/guild-template.d.ts +51 -0
  119. package/dist/discord/types/guild-template.js +1 -0
  120. package/dist/discord/types/guild.d.ts +910 -0
  121. package/dist/discord/types/guild.js +153 -0
  122. package/dist/discord/types/index.d.ts +31 -0
  123. package/dist/discord/types/index.js +31 -0
  124. package/dist/discord/types/interaction.d.ts +321 -0
  125. package/dist/discord/types/interaction.js +47 -0
  126. package/dist/discord/types/invite.d.ts +158 -0
  127. package/dist/discord/types/invite.js +39 -0
  128. package/dist/discord/types/message.d.ts +834 -0
  129. package/dist/discord/types/message.js +159 -0
  130. package/dist/discord/types/oauth2.d.ts +22 -0
  131. package/dist/discord/types/oauth2.js +1 -0
  132. package/dist/discord/types/permissions.d.ts +183 -0
  133. package/dist/discord/types/permissions.js +118 -0
  134. package/dist/discord/types/poll.d.ts +106 -0
  135. package/dist/discord/types/poll.js +8 -0
  136. package/dist/discord/types/sku.d.ts +47 -0
  137. package/dist/discord/types/sku.js +26 -0
  138. package/dist/discord/types/soundboard.d.ts +71 -0
  139. package/dist/discord/types/soundboard.js +1 -0
  140. package/dist/discord/types/stage-instance.d.ts +58 -0
  141. package/dist/discord/types/stage-instance.js +10 -0
  142. package/dist/discord/types/sticker.d.ts +115 -0
  143. package/dist/discord/types/sticker.js +20 -0
  144. package/dist/discord/types/subscription.d.ts +54 -0
  145. package/dist/discord/types/subscription.js +12 -0
  146. package/dist/discord/types/team.d.ts +55 -0
  147. package/dist/discord/types/team.js +21 -0
  148. package/dist/discord/types/user.d.ts +273 -0
  149. package/dist/discord/types/user.js +83 -0
  150. package/dist/discord/types/voice.d.ts +72 -0
  151. package/dist/discord/types/voice.js +1 -0
  152. package/dist/discord/types/webhook.d.ts +184 -0
  153. package/dist/discord/types/webhook.js +12 -0
  154. package/dist/events/declaration.d.ts +16 -0
  155. package/dist/events/declaration.js +80 -0
  156. package/dist/events/event.d.ts +45 -0
  157. package/dist/events/event.js +34 -0
  158. package/dist/events/registry.d.ts +697 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +46 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/gateway/errors.d.ts +18 -0
  163. package/dist/gateway/errors.js +47 -0
  164. package/dist/gateway/gateway.d.ts +64 -0
  165. package/dist/gateway/gateway.js +117 -0
  166. package/dist/gateway/identify-queue.d.ts +25 -0
  167. package/dist/gateway/identify-queue.js +29 -0
  168. package/dist/gateway/shard.d.ts +94 -0
  169. package/dist/gateway/shard.js +414 -0
  170. package/dist/gateway/state.d.ts +11 -0
  171. package/dist/gateway/state.js +184 -0
  172. package/dist/index.d.ts +57 -18
  173. package/dist/index.js +40 -12
  174. package/dist/interactions/cooldown.d.ts +33 -0
  175. package/dist/interactions/cooldown.js +74 -0
  176. package/dist/interactions/dispatch.d.ts +91 -0
  177. package/dist/interactions/dispatch.js +110 -0
  178. package/dist/loader/generated.d.ts +10 -0
  179. package/dist/loader/generated.js +80 -0
  180. package/dist/loader/hot.d.ts +13 -0
  181. package/dist/loader/hot.js +85 -0
  182. package/dist/loader/loader.d.ts +85 -0
  183. package/dist/loader/loader.js +119 -0
  184. package/dist/loader/locate.d.ts +10 -0
  185. package/dist/loader/locate.js +36 -0
  186. package/dist/loader/watch.d.ts +17 -0
  187. package/dist/loader/watch.js +66 -0
  188. package/dist/main.d.ts +2 -0
  189. package/dist/main.js +5 -0
  190. package/dist/messages/commands.d.ts +23 -0
  191. package/dist/messages/commands.js +142 -0
  192. package/dist/messages/declaration.d.ts +89 -0
  193. package/dist/messages/declaration.js +455 -0
  194. package/dist/messages/messages.d.ts +241 -0
  195. package/dist/messages/messages.js +31 -0
  196. package/dist/messages/phrases.d.ts +23 -0
  197. package/dist/messages/phrases.js +44 -0
  198. package/dist/messages/translate.d.ts +100 -0
  199. package/dist/messages/translate.js +249 -0
  200. package/dist/presence/declaration.d.ts +19 -0
  201. package/dist/presence/declaration.js +118 -0
  202. package/dist/presence/presence.d.ts +57 -0
  203. package/dist/presence/presence.js +27 -0
  204. package/dist/rest/errors.d.ts +55 -0
  205. package/dist/rest/errors.js +92 -0
  206. package/dist/rest/rate-limiter.d.ts +46 -0
  207. package/dist/rest/rate-limiter.js +153 -0
  208. package/dist/rest/rest.d.ts +63 -0
  209. package/dist/rest/rest.js +194 -0
  210. package/dist/structures/base.d.ts +48 -0
  211. package/dist/structures/base.js +84 -0
  212. package/dist/structures/channel.d.ts +380 -0
  213. package/dist/structures/channel.js +627 -0
  214. package/dist/structures/context.d.ts +30 -0
  215. package/dist/structures/context.js +7 -0
  216. package/dist/structures/emoji.d.ts +46 -0
  217. package/dist/structures/emoji.js +65 -0
  218. package/dist/structures/entities.d.ts +62 -0
  219. package/dist/structures/entities.js +226 -0
  220. package/dist/structures/guild.d.ts +348 -0
  221. package/dist/structures/guild.js +466 -0
  222. package/dist/structures/interaction.d.ts +197 -0
  223. package/dist/structures/interaction.js +396 -0
  224. package/dist/structures/invite.d.ts +38 -0
  225. package/dist/structures/invite.js +57 -0
  226. package/dist/structures/known.d.ts +28 -0
  227. package/dist/structures/known.js +58 -0
  228. package/dist/structures/member.d.ts +140 -0
  229. package/dist/structures/member.js +260 -0
  230. package/dist/structures/message.d.ts +197 -0
  231. package/dist/structures/message.js +268 -0
  232. package/dist/structures/payload.d.ts +91 -0
  233. package/dist/structures/payload.js +184 -0
  234. package/dist/structures/role.d.ts +69 -0
  235. package/dist/structures/role.js +112 -0
  236. package/dist/structures/user.d.ts +59 -0
  237. package/dist/structures/user.js +94 -0
  238. package/dist/structures/webhook.d.ts +46 -0
  239. package/dist/structures/webhook.js +79 -0
  240. package/dist/tasks/declaration.d.ts +16 -0
  241. package/dist/tasks/declaration.js +56 -0
  242. package/dist/tasks/schedule.d.ts +54 -0
  243. package/dist/tasks/schedule.js +236 -0
  244. package/dist/tasks/scheduler.d.ts +35 -0
  245. package/dist/tasks/scheduler.js +107 -0
  246. package/dist/tasks/task.d.ts +75 -0
  247. package/dist/tasks/task.js +27 -0
  248. package/dist/util/case.d.ts +13 -0
  249. package/dist/util/case.js +54 -0
  250. package/dist/util/duration.d.ts +8 -0
  251. package/dist/util/duration.js +29 -0
  252. package/dist/util/websocket.d.ts +9 -0
  253. package/dist/util/websocket.js +31 -0
  254. package/dist/voice/connection.d.ts +90 -0
  255. package/dist/voice/connection.js +494 -0
  256. package/dist/voice/dave.d.ts +109 -0
  257. package/dist/voice/dave.js +245 -0
  258. package/dist/voice/opus.d.ts +19 -0
  259. package/dist/voice/opus.js +273 -0
  260. package/dist/voice/session.d.ts +55 -0
  261. package/dist/voice/session.js +405 -0
  262. package/dist/voice/source.d.ts +22 -0
  263. package/dist/voice/source.js +195 -0
  264. package/dist/voice/transport.d.ts +43 -0
  265. package/dist/voice/transport.js +140 -0
  266. package/dist/voice/usage.d.ts +4 -0
  267. package/dist/voice/usage.js +33 -0
  268. package/package.json +26 -7
  269. package/vendor/dave/LICENSE +231 -0
  270. package/vendor/dave/libdave.d.mts +99 -0
  271. package/vendor/dave/libdave.mjs +2 -0
  272. package/vendor/dave/libdave.wasm +0 -0
  273. package/dist/components.d.ts +0 -97
  274. package/dist/components.js +0 -260
  275. package/dist/context.d.ts +0 -72
  276. package/dist/context.js +0 -273
  277. package/dist/discord/client.d.ts +0 -35
  278. package/dist/discord/client.js +0 -66
  279. package/dist/discord/constants.d.ts +0 -49
  280. package/dist/discord/constants.js +0 -61
  281. package/dist/discord/events.d.ts +0 -129
  282. package/dist/discord/events.js +0 -102
  283. package/dist/discord/gateway.d.ts +0 -61
  284. package/dist/discord/gateway.js +0 -320
  285. package/dist/discord/payloads.d.ts +0 -275
  286. package/dist/discord/payloads.js +0 -3
  287. package/dist/discord/presence.d.ts +0 -42
  288. package/dist/discord/presence.js +0 -51
  289. package/dist/discord/rest.d.ts +0 -49
  290. package/dist/discord/rest.js +0 -271
  291. package/dist/discord/types.d.ts +0 -334
  292. package/dist/discord/types.js +0 -4
  293. package/dist/events.d.ts +0 -94
  294. package/dist/events.js +0 -255
  295. package/dist/presence.d.ts +0 -92
  296. package/dist/presence.js +0 -250
  297. package/dist/structures/Channel.d.ts +0 -110
  298. package/dist/structures/Channel.js +0 -171
  299. package/dist/structures/Guild.d.ts +0 -70
  300. package/dist/structures/Guild.js +0 -105
  301. package/dist/structures/Interaction.d.ts +0 -113
  302. package/dist/structures/Interaction.js +0 -274
  303. package/dist/structures/Member.d.ts +0 -70
  304. package/dist/structures/Member.js +0 -124
  305. package/dist/structures/Message.d.ts +0 -87
  306. package/dist/structures/Message.js +0 -182
  307. package/dist/structures/Reaction.d.ts +0 -38
  308. package/dist/structures/Reaction.js +0 -64
  309. package/dist/structures/Role.d.ts +0 -43
  310. package/dist/structures/Role.js +0 -77
  311. package/dist/structures/User.d.ts +0 -41
  312. package/dist/structures/User.js +0 -79
  313. package/dist/structures/utils.d.ts +0 -72
  314. package/dist/structures/utils.js +0 -59
  315. package/dist/utils/errors.d.ts +0 -5
  316. package/dist/utils/errors.js +0 -28
  317. package/dist/utils/modules.d.ts +0 -39
  318. package/dist/utils/modules.js +0 -156
@@ -0,0 +1,414 @@
1
+ // One WebSocket connection to the Discord gateway, kept alive for as long
2
+ // as the bot runs: handshake, heartbeats, resuming after a disconnect.
3
+ // https://docs.discord.com/developers/events/gateway#connections
4
+ import { setTimeout as sleep } from 'node:timers/promises';
5
+ import { API_VERSION } from '../discord/api.js';
6
+ import { FATAL_GATEWAY_CLOSE_CODES, GatewayCloseCode, GatewayOpcode, } from '../discord/codes.js';
7
+ import { onSocketEnd } from '../util/websocket.js';
8
+ import { GatewayFatalError } from './errors.js';
9
+ /**
10
+ * Apps can send 120 gateway events per connection every 60 seconds.
11
+ * @see https://docs.discord.com/developers/events/gateway#rate-limiting
12
+ */
13
+ const SEND_LIMIT = 120;
14
+ const SEND_WINDOW = 60_000;
15
+ /** Kept free for heartbeats, which must never wait. */
16
+ const SEND_RESERVE = 5;
17
+ /**
18
+ * An event sent to Discord must not exceed 4096 bytes.
19
+ * @see https://docs.discord.com/developers/events/gateway#sending-events
20
+ */
21
+ const MAX_PAYLOAD_BYTES = 4096;
22
+ /**
23
+ * "Clients may only update their game status 5 times per 20 seconds."
24
+ * @see https://docs.discord.com/developers/events/gateway-events#update-presence
25
+ */
26
+ const PRESENCE_LIMIT = 5;
27
+ const PRESENCE_WINDOW = 20_000;
28
+ /**
29
+ * The code used to close a connection the shard wants to resume: any code
30
+ * but 1000 and 1001 keeps the session alive.
31
+ * @see https://docs.discord.com/developers/events/gateway#initiating-a-disconnect
32
+ */
33
+ const CLOSE_AND_RESUME = 4000;
34
+ /**
35
+ * Close codes after which the session is gone: the shard reconnects and
36
+ * identifies again.
37
+ */
38
+ const SESSION_LOST = new Set([
39
+ 1000,
40
+ 1001,
41
+ GatewayCloseCode.NotAuthenticated,
42
+ GatewayCloseCode.InvalidSeq,
43
+ GatewayCloseCode.SessionTimedOut,
44
+ ]);
45
+ const defaultBackoff = (attempt) => attempt === 0
46
+ ? 0
47
+ : Math.min(1000 * 2 ** (attempt - 1), 60_000) * (0.5 + Math.random() / 2);
48
+ export class Shard {
49
+ id;
50
+ #options;
51
+ #status = 'idle';
52
+ #ws = null;
53
+ /** Whether the current connection got its Hello. */
54
+ #greeted = false;
55
+ // What a Resume needs, kept from the Ready event and the last dispatch.
56
+ #sessionId = null;
57
+ #resumeUrl = null;
58
+ #sequence = null;
59
+ // Heartbeats.
60
+ #heartbeatTimer = null;
61
+ #acknowledged = true;
62
+ #heartbeatSentAt = 0;
63
+ #ping = null;
64
+ /** Tries in a row that did not reach Ready or Resumed. */
65
+ #attempts = 0;
66
+ #retryTimer = null;
67
+ /** When the last events were sent, to respect the send limit. */
68
+ #sent = [];
69
+ // The presence: what Identify sends, and what Update Presence changes.
70
+ #presence;
71
+ /** Whether the current presence still has to be sent to Discord. */
72
+ #presencePending = false;
73
+ /** When the last presence updates were sent, to respect their limit. */
74
+ #presenceSent = [];
75
+ #presenceTimer = null;
76
+ // Who is waiting for the shard to be ready.
77
+ #readyWaiters = [];
78
+ #fatal = null;
79
+ constructor(options) {
80
+ this.id = options.id;
81
+ this.#options = options;
82
+ this.#presence = options.presence;
83
+ }
84
+ get status() {
85
+ return this.#status;
86
+ }
87
+ /**
88
+ * How long Discord took to acknowledge the last heartbeat, in
89
+ * milliseconds; `null` before the first one.
90
+ */
91
+ get ping() {
92
+ return this.#ping;
93
+ }
94
+ /**
95
+ * Connects, and resolves once Discord said the shard is ready. Rejects if
96
+ * Discord refuses the connection for good. After that the shard stays
97
+ * connected by itself.
98
+ */
99
+ connect() {
100
+ if (this.#status !== 'idle') {
101
+ throw new Error(`Shard ${this.id} was already started.`);
102
+ }
103
+ const ready = this.#whenReady();
104
+ this.#open();
105
+ return ready;
106
+ }
107
+ /**
108
+ * Sends an event to Discord, once the shard is ready and the send limit
109
+ * allows it.
110
+ * @see https://docs.discord.com/developers/events/gateway-events#send-events
111
+ */
112
+ async send(op, data) {
113
+ const text = JSON.stringify({ op, d: data });
114
+ const bytes = Buffer.byteLength(text);
115
+ if (bytes > MAX_PAYLOAD_BYTES) {
116
+ throw new RangeError(`This gateway event is ${bytes} bytes long: Discord accepts ${MAX_PAYLOAD_BYTES} at most.`);
117
+ }
118
+ for (;;) {
119
+ if (this.#status !== 'ready')
120
+ await this.#whenReady();
121
+ const now = Date.now();
122
+ while (this.#sent.length > 0 && now - this.#sent[0] >= SEND_WINDOW) {
123
+ this.#sent.shift();
124
+ }
125
+ if (this.#sent.length < SEND_LIMIT - SEND_RESERVE)
126
+ break;
127
+ await sleep(this.#sent[0] + SEND_WINDOW - now);
128
+ }
129
+ this.#write(text);
130
+ }
131
+ /**
132
+ * Changes the presence of the bot on this shard. It is sent right away
133
+ * when the shard is ready and Discord's limit allows it, otherwise as
134
+ * soon as one of them does (only the latest presence is sent then); a
135
+ * session started later identifies with it.
136
+ * @see https://docs.discord.com/developers/events/gateway-events#update-presence
137
+ */
138
+ setPresence(presence) {
139
+ this.#presence = presence;
140
+ this.#presencePending = true;
141
+ this.#flushPresence();
142
+ }
143
+ #flushPresence() {
144
+ if (!this.#presencePending || this.#presenceTimer)
145
+ return;
146
+ // Not connected: the next Identify carries it, or Resumed calls again.
147
+ if (this.#status !== 'ready')
148
+ return;
149
+ const now = Date.now();
150
+ const window = this.#options.presenceWindow ?? PRESENCE_WINDOW;
151
+ const sent = this.#presenceSent;
152
+ while (sent.length > 0 && now - sent[0] >= window)
153
+ sent.shift();
154
+ if (sent.length >= PRESENCE_LIMIT) {
155
+ this.#presenceTimer = setTimeout(() => {
156
+ this.#presenceTimer = null;
157
+ this.#flushPresence();
158
+ }, sent[0] + window - now);
159
+ return;
160
+ }
161
+ this.#presencePending = false;
162
+ sent.push(now);
163
+ this.#write(JSON.stringify({ op: GatewayOpcode.PresenceUpdate, d: this.#presence }));
164
+ }
165
+ /**
166
+ * Closes the connection for good. The session is ended, so the bot
167
+ * appears offline right away instead of staying online as a ghost.
168
+ * @see https://docs.discord.com/developers/events/gateway#initiating-a-disconnect
169
+ */
170
+ async close() {
171
+ if (this.#status === 'closed')
172
+ return;
173
+ const ws = this.#ws;
174
+ this.#finish(new Error(`Shard ${this.id} was closed.`));
175
+ if (!ws || ws.readyState === WebSocket.CLOSED)
176
+ return;
177
+ await new Promise(resolve => {
178
+ const timer = setTimeout(resolve, 2000);
179
+ ws.addEventListener('close', () => {
180
+ clearTimeout(timer);
181
+ resolve();
182
+ });
183
+ ws.close(1000);
184
+ });
185
+ }
186
+ // -- Connection ------------------------------------------------------------
187
+ #whenReady() {
188
+ if (this.#fatal)
189
+ return Promise.reject(this.#fatal);
190
+ return new Promise((resolve, reject) => {
191
+ this.#readyWaiters.push({ resolve, reject });
192
+ });
193
+ }
194
+ #open() {
195
+ this.#status = 'connecting';
196
+ this.#greeted = false;
197
+ const base = (this.#sessionId && this.#resumeUrl ? this.#resumeUrl : this.#options.url).replace(/\/+$/, '');
198
+ // When resuming, the same version and encoding must be given again.
199
+ const ws = new WebSocket(`${base}/?v=${API_VERSION}&encoding=json`);
200
+ this.#ws = ws;
201
+ ws.addEventListener('message', event => {
202
+ if (ws === this.#ws)
203
+ this.#onMessage(String(event.data));
204
+ });
205
+ onSocketEnd(ws, closeCode => {
206
+ // 1005 and 1006 mean "no close code was received".
207
+ const code = closeCode === 1005 || closeCode === 1006 ? null : closeCode;
208
+ if (ws === this.#ws)
209
+ this.#onClose(code);
210
+ });
211
+ }
212
+ #write(text) {
213
+ const ws = this.#ws;
214
+ if (!ws || ws.readyState !== WebSocket.OPEN)
215
+ return;
216
+ this.#sent.push(Date.now());
217
+ ws.send(text);
218
+ }
219
+ #onMessage(text) {
220
+ let payload;
221
+ try {
222
+ payload = JSON.parse(text);
223
+ }
224
+ catch {
225
+ return;
226
+ }
227
+ switch (payload.op) {
228
+ case GatewayOpcode.Hello:
229
+ this.#greeted = true;
230
+ this.#startHeartbeat(payload.d.heartbeat_interval);
231
+ void this.#handshake();
232
+ break;
233
+ case GatewayOpcode.HeartbeatAck:
234
+ this.#acknowledged = true;
235
+ this.#ping = Date.now() - this.#heartbeatSentAt;
236
+ break;
237
+ case GatewayOpcode.Heartbeat:
238
+ // Discord asks for a heartbeat right now.
239
+ this.#sendHeartbeat();
240
+ break;
241
+ case GatewayOpcode.Reconnect:
242
+ this.#reconnect(true);
243
+ break;
244
+ case GatewayOpcode.InvalidSession:
245
+ // `d` says whether the session may be resumed.
246
+ this.#reconnect(payload.d === true);
247
+ break;
248
+ case GatewayOpcode.Dispatch:
249
+ this.#onDispatch(payload);
250
+ break;
251
+ }
252
+ }
253
+ async #handshake() {
254
+ const ws = this.#ws;
255
+ if (this.#sessionId !== null && this.#sequence !== null) {
256
+ this.#status = 'resuming';
257
+ const resume = {
258
+ token: this.#options.token,
259
+ session_id: this.#sessionId,
260
+ seq: this.#sequence,
261
+ };
262
+ this.#write(JSON.stringify({ op: GatewayOpcode.Resume, d: resume }));
263
+ return;
264
+ }
265
+ this.#status = 'identifying';
266
+ await this.#options.identifyGate.wait(this.id);
267
+ // The connection may have dropped while waiting for its turn.
268
+ if (ws !== this.#ws)
269
+ return;
270
+ const identify = {
271
+ token: this.#options.token,
272
+ intents: this.#options.intents,
273
+ properties: {
274
+ os: process.platform,
275
+ browser: 'chapterjs',
276
+ device: 'chapterjs',
277
+ },
278
+ shard: [this.id, this.#options.count],
279
+ ...(this.#presence ? { presence: this.#presence } : {}),
280
+ };
281
+ // The new session starts with the current presence.
282
+ this.#presencePending = false;
283
+ this.#write(JSON.stringify({ op: GatewayOpcode.Identify, d: identify }));
284
+ }
285
+ #onDispatch(payload) {
286
+ if (payload.s !== null && payload.s !== undefined) {
287
+ this.#sequence = payload.s;
288
+ }
289
+ const event = payload.t;
290
+ if (event === 'READY') {
291
+ const ready = payload.d;
292
+ this.#sessionId = ready.session_id;
293
+ this.#resumeUrl = ready.resume_gateway_url;
294
+ }
295
+ // The cache must know about Ready before anyone is told the shard is.
296
+ this.#options.onDispatch(event, payload.d, this.id);
297
+ if (event === 'READY' || event === 'RESUMED') {
298
+ this.#status = 'ready';
299
+ this.#attempts = 0;
300
+ this.#options.onEvent?.({
301
+ type: 'ready',
302
+ shardId: this.id,
303
+ resumed: event === 'RESUMED',
304
+ });
305
+ for (const waiter of this.#readyWaiters.splice(0))
306
+ waiter.resolve();
307
+ // A presence changed while disconnected: a resumed session kept the
308
+ // old one.
309
+ this.#flushPresence();
310
+ }
311
+ }
312
+ // -- Heartbeats ------------------------------------------------------------
313
+ /** @see https://docs.discord.com/developers/events/gateway#sending-heartbeats */
314
+ #startHeartbeat(interval) {
315
+ this.#stopHeartbeat();
316
+ this.#acknowledged = true;
317
+ // The first heartbeat is sent after `heartbeat_interval * jitter`.
318
+ this.#heartbeatTimer = setTimeout(() => {
319
+ this.#sendHeartbeat();
320
+ this.#heartbeatTimer = setInterval(() => {
321
+ if (!this.#acknowledged) {
322
+ // A "zombied" connection: terminate it and resume.
323
+ this.#reconnect(true);
324
+ return;
325
+ }
326
+ this.#sendHeartbeat();
327
+ }, interval);
328
+ }, interval * Math.random());
329
+ }
330
+ #stopHeartbeat() {
331
+ if (this.#heartbeatTimer) {
332
+ clearTimeout(this.#heartbeatTimer);
333
+ clearInterval(this.#heartbeatTimer);
334
+ this.#heartbeatTimer = null;
335
+ }
336
+ }
337
+ #sendHeartbeat() {
338
+ this.#acknowledged = false;
339
+ this.#heartbeatSentAt = Date.now();
340
+ this.#write(JSON.stringify({ op: GatewayOpcode.Heartbeat, d: this.#sequence }));
341
+ }
342
+ // -- Disconnections --------------------------------------------------------
343
+ /** Drops the current connection, whose events are ignored from now on. */
344
+ #detach(code) {
345
+ this.#stopHeartbeat();
346
+ const ws = this.#ws;
347
+ this.#ws = null;
348
+ if (ws && ws.readyState !== WebSocket.CLOSED) {
349
+ try {
350
+ ws.close(code);
351
+ }
352
+ catch {
353
+ // Already closing.
354
+ }
355
+ }
356
+ }
357
+ /** The shard itself decides to reconnect (Discord asked, or no ACK). */
358
+ #reconnect(resume) {
359
+ // Ending the session cleanly when it can't be resumed anyway.
360
+ this.#detach(resume ? CLOSE_AND_RESUME : 1000);
361
+ this.#retry(null, resume);
362
+ }
363
+ /** The connection was closed by Discord or by the network. */
364
+ #onClose(code) {
365
+ this.#stopHeartbeat();
366
+ this.#ws = null;
367
+ if (code !== null && FATAL_GATEWAY_CLOSE_CODES.has(code)) {
368
+ const error = new GatewayFatalError(code, this.#options.intents);
369
+ this.#finish(error);
370
+ this.#options.onEvent?.({ type: 'fatal', shardId: this.id, error });
371
+ return;
372
+ }
373
+ // A resume that could not even reach Discord: start over from the URL
374
+ // of Get Gateway Bot, as the documentation says.
375
+ const resumeFailed = this.#status === 'connecting' && !this.#greeted;
376
+ this.#retry(code, !(resumeFailed || (code !== null && SESSION_LOST.has(code))));
377
+ }
378
+ #retry(code, resume) {
379
+ const resumable = resume && this.#sessionId !== null && this.#sequence !== null;
380
+ if (!resumable) {
381
+ this.#sessionId = null;
382
+ this.#resumeUrl = null;
383
+ this.#sequence = null;
384
+ }
385
+ this.#status = 'reconnecting';
386
+ const retryIn = Math.round((this.#options.backoff ?? defaultBackoff)(this.#attempts++));
387
+ this.#options.onEvent?.({
388
+ type: 'disconnected',
389
+ shardId: this.id,
390
+ code,
391
+ resumable,
392
+ retryIn,
393
+ });
394
+ this.#retryTimer = setTimeout(() => {
395
+ this.#retryTimer = null;
396
+ this.#open();
397
+ }, retryIn);
398
+ }
399
+ /** Stops everything, for good. */
400
+ #finish(error) {
401
+ this.#status = 'closed';
402
+ this.#fatal = error;
403
+ this.#stopHeartbeat();
404
+ if (this.#retryTimer)
405
+ clearTimeout(this.#retryTimer);
406
+ this.#retryTimer = null;
407
+ if (this.#presenceTimer)
408
+ clearTimeout(this.#presenceTimer);
409
+ this.#presenceTimer = null;
410
+ this.#ws = null;
411
+ for (const waiter of this.#readyWaiters.splice(0))
412
+ waiter.reject(error);
413
+ }
414
+ }
@@ -0,0 +1,11 @@
1
+ import type { GatewayDispatchEventName, GatewayDispatchEvents } from '../discord/types/gateway-events.js';
2
+ import type { Context } from '../structures/context.js';
3
+ /** Where an event comes from. */
4
+ export interface DispatchOrigin {
5
+ shardId: number;
6
+ shardCount: number;
7
+ }
8
+ /** Whether an event changes what the bot remembers. */
9
+ export declare function tracksState(event: GatewayDispatchEventName): boolean;
10
+ /** Applies one dispatched event to the cache. */
11
+ export declare function applyDispatch<E extends GatewayDispatchEventName>(ctx: Context, event: E, data: GatewayDispatchEvents[E], origin: DispatchOrigin): void;
@@ -0,0 +1,184 @@
1
+ // Keeps the cache up to date with what Discord dispatches, so that reading
2
+ // it never costs a request. One entry per event; an event that changes
3
+ // nothing the bot remembers has no entry.
4
+ // https://docs.discord.com/developers/events/gateway#tracking-state
5
+ import { dataOf, patch } from '../structures/base.js';
6
+ import { messagesOf } from '../structures/channel.js';
7
+ import { storesOf } from '../structures/guild.js';
8
+ import { shardIdFor } from './gateway.js';
9
+ const guildOf = (ctx, id) => id ? ctx.cache.guilds.get(id) : undefined;
10
+ /** Adds to the member count of a server, when it is known exactly. */
11
+ function countMembers(guild, change) {
12
+ const count = dataOf(guild).member_count;
13
+ if (typeof count === 'number') {
14
+ patch(guild, { member_count: Math.max(0, count + change) });
15
+ }
16
+ }
17
+ const upsertChannel = (ctx, data) => {
18
+ ctx.entities.channel(data);
19
+ };
20
+ const handlers = {
21
+ READY(ctx, data, origin) {
22
+ ctx.self = { userId: data.user.id, applicationId: data.application.id };
23
+ ctx.entities.user(data.user);
24
+ // A new session: servers the bot left while it was away are forgotten.
25
+ const current = new Set(data.guilds.map(guild => guild.id));
26
+ for (const id of [...ctx.cache.guilds.keys()]) {
27
+ if (shardIdFor(id, origin.shardCount) === origin.shardId &&
28
+ !current.has(id)) {
29
+ ctx.entities.removeGuild(id);
30
+ }
31
+ }
32
+ },
33
+ USER_UPDATE(ctx, data) {
34
+ ctx.entities.user(data);
35
+ },
36
+ GUILD_CREATE(ctx, data) {
37
+ if (!('name' in data)) {
38
+ // Still unavailable because of an outage.
39
+ const guild = guildOf(ctx, data.id);
40
+ if (guild)
41
+ patch(guild, { unavailable: true });
42
+ return;
43
+ }
44
+ const guild = ctx.entities.guild(data);
45
+ patch(guild, { unavailable: false });
46
+ // The payload lists every channel and active thread: what is no longer
47
+ // in it was deleted while the bot was away.
48
+ const listed = new Set([...data.channels, ...data.threads].map(channel => channel.id));
49
+ for (const id of [...storesOf(guild).channels.keys()]) {
50
+ if (!listed.has(id))
51
+ ctx.entities.removeChannel(id);
52
+ }
53
+ },
54
+ GUILD_UPDATE(ctx, data) {
55
+ ctx.entities.guild(data);
56
+ },
57
+ GUILD_DELETE(ctx, data) {
58
+ // With `unavailable`, an outage; without, the bot is no longer in it.
59
+ const guild = guildOf(ctx, data.id);
60
+ if (!guild)
61
+ return;
62
+ if (data.unavailable)
63
+ patch(guild, { unavailable: true });
64
+ else
65
+ ctx.entities.removeGuild(data.id);
66
+ },
67
+ GUILD_ROLE_CREATE(ctx, data) {
68
+ ctx.entities.role(data.guild_id, data.role);
69
+ },
70
+ GUILD_ROLE_UPDATE(ctx, data) {
71
+ ctx.entities.role(data.guild_id, data.role);
72
+ },
73
+ GUILD_ROLE_DELETE(ctx, data) {
74
+ const guild = guildOf(ctx, data.guild_id);
75
+ if (guild)
76
+ storesOf(guild).roles.delete(data.role_id);
77
+ },
78
+ GUILD_MEMBER_ADD(ctx, data) {
79
+ const guild = guildOf(ctx, data.guild_id);
80
+ const { guild_id: guildId, ...member } = data;
81
+ if (guild && !guild.members.has(member.user.id))
82
+ countMembers(guild, 1);
83
+ ctx.entities.member(guildId, member);
84
+ },
85
+ GUILD_MEMBER_UPDATE(ctx, data) {
86
+ const { guild_id: guildId, ...member } = data;
87
+ // Fields this event leaves out keep the value they had.
88
+ ctx.entities.member(guildId, member);
89
+ },
90
+ GUILD_MEMBER_REMOVE(ctx, data) {
91
+ const guild = guildOf(ctx, data.guild_id);
92
+ if (!guild)
93
+ return;
94
+ storesOf(guild).members.delete(data.user.id);
95
+ storesOf(guild).voiceStates.delete(data.user.id);
96
+ countMembers(guild, -1);
97
+ },
98
+ GUILD_MEMBERS_CHUNK(ctx, data) {
99
+ for (const member of data.members) {
100
+ ctx.entities.member(data.guild_id, member);
101
+ }
102
+ },
103
+ VOICE_STATE_UPDATE(ctx, data) {
104
+ if (!data.guild_id)
105
+ return;
106
+ if (data.member)
107
+ ctx.entities.member(data.guild_id, data.member);
108
+ ctx.entities.voiceState(data.guild_id, data);
109
+ },
110
+ GUILD_EMOJIS_UPDATE(ctx, data) {
111
+ const guild = guildOf(ctx, data.guild_id);
112
+ if (!guild)
113
+ return;
114
+ storesOf(guild).emojis.clear();
115
+ for (const emoji of data.emojis)
116
+ ctx.entities.emoji(data.guild_id, emoji);
117
+ },
118
+ CHANNEL_CREATE: upsertChannel,
119
+ CHANNEL_UPDATE: upsertChannel,
120
+ CHANNEL_DELETE(ctx, data) {
121
+ ctx.entities.removeChannel(data.id);
122
+ },
123
+ CHANNEL_PINS_UPDATE(ctx, data) {
124
+ const channel = ctx.cache.channels.get(data.channel_id);
125
+ if (channel && data.last_pin_timestamp !== undefined) {
126
+ patch(channel, { last_pin_timestamp: data.last_pin_timestamp });
127
+ }
128
+ },
129
+ THREAD_CREATE: upsertChannel,
130
+ THREAD_UPDATE: upsertChannel,
131
+ THREAD_DELETE(ctx, data) {
132
+ ctx.entities.removeChannel(data.id);
133
+ },
134
+ THREAD_LIST_SYNC(ctx, data) {
135
+ const guild = guildOf(ctx, data.guild_id);
136
+ if (guild) {
137
+ // Threads of the synced channels (all of them when none is named)
138
+ // that are not listed are no longer active.
139
+ const synced = data.channel_ids ? new Set(data.channel_ids) : null;
140
+ const active = new Set(data.threads.map(thread => thread.id));
141
+ for (const channel of [...guild.channels.values()]) {
142
+ if (channel.isThread() &&
143
+ !active.has(channel.id) &&
144
+ (!synced || (channel.parentId && synced.has(channel.parentId)))) {
145
+ ctx.entities.removeChannel(channel.id);
146
+ }
147
+ }
148
+ }
149
+ for (const thread of data.threads) {
150
+ ctx.entities.channel(thread, data.guild_id);
151
+ }
152
+ },
153
+ MESSAGE_CREATE(ctx, data) {
154
+ const channel = ctx.cache.channels.get(data.channel_id);
155
+ if (channel)
156
+ patch(channel, { last_message_id: data.id });
157
+ ctx.entities.message(data, data.guild_id);
158
+ },
159
+ MESSAGE_UPDATE(ctx, data) {
160
+ ctx.entities.message(data, data.guild_id);
161
+ },
162
+ MESSAGE_DELETE(ctx, data) {
163
+ const channel = ctx.cache.channels.get(data.channel_id);
164
+ if (channel)
165
+ messagesOf(channel).delete(data.id);
166
+ },
167
+ MESSAGE_DELETE_BULK(ctx, data) {
168
+ const channel = ctx.cache.channels.get(data.channel_id);
169
+ if (!channel)
170
+ return;
171
+ const messages = messagesOf(channel);
172
+ for (const id of data.ids)
173
+ messages.delete(id);
174
+ },
175
+ };
176
+ /** Whether an event changes what the bot remembers. */
177
+ export function tracksState(event) {
178
+ return event in handlers;
179
+ }
180
+ /** Applies one dispatched event to the cache. */
181
+ export function applyDispatch(ctx, event, data, origin) {
182
+ const handler = handlers[event];
183
+ handler?.(ctx, data, origin);
184
+ }