@wolfstar/plugin-cache 0.0.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.
@@ -0,0 +1,1436 @@
1
+ import { GatewayDispatchEvents } from "discord-api-types/v10";
2
+ import { promisify } from "node:util";
3
+ import { brotliCompress, brotliDecompress, gunzip, gzip } from "node:zlib";
4
+
5
+ //#region src/lib/keys.ts
6
+ /**
7
+ * Creates a stable key for a guild-scoped entity.
8
+ */
9
+ function guildScopedKey(guildId, id) {
10
+ return `${guildId}:${id}`;
11
+ }
12
+ /**
13
+ * Creates a stable key for a channel message.
14
+ */
15
+ function messageKey(channelId, messageId) {
16
+ return `${channelId}:${messageId}`;
17
+ }
18
+ /**
19
+ * Creates a stable key for a guild member.
20
+ */
21
+ function memberKey(guildId, userId) {
22
+ return guildScopedKey(guildId, userId);
23
+ }
24
+ /**
25
+ * Creates a stable key for a guild presence.
26
+ */
27
+ function presenceKey(guildId, userId) {
28
+ return guildScopedKey(guildId, userId);
29
+ }
30
+ /**
31
+ * Creates a stable key for a guild voice state.
32
+ */
33
+ function voiceStateKey(guildId, userId) {
34
+ return guildScopedKey(guildId, userId);
35
+ }
36
+ /**
37
+ * Creates a stable key for a guild role.
38
+ */
39
+ function roleKey(guildId, roleId) {
40
+ return guildScopedKey(guildId, roleId);
41
+ }
42
+ /**
43
+ * Creates a stable key for a guild emoji.
44
+ */
45
+ function emojiKey(guildId, emojiId) {
46
+ return guildScopedKey(guildId, emojiId);
47
+ }
48
+ /**
49
+ * Creates a stable key for a guild sticker.
50
+ */
51
+ function stickerKey(guildId, stickerId) {
52
+ return guildScopedKey(guildId, stickerId);
53
+ }
54
+ /**
55
+ * Creates a stable key for a guild scheduled event.
56
+ */
57
+ function scheduledEventKey(guildId, scheduledEventId) {
58
+ return guildScopedKey(guildId, scheduledEventId);
59
+ }
60
+ /**
61
+ * Creates a stable key for a stage instance.
62
+ */
63
+ function stageInstanceKey(guildId, channelId) {
64
+ return guildScopedKey(guildId, channelId);
65
+ }
66
+ /**
67
+ * Creates a stable key for a guild soundboard sound.
68
+ */
69
+ function soundboardSoundKey(guildId, soundId) {
70
+ return guildScopedKey(guildId, soundId);
71
+ }
72
+ /**
73
+ * Creates a stable key for a guild auto moderation rule.
74
+ */
75
+ function autoModerationRuleKey(guildId, ruleId) {
76
+ return guildScopedKey(guildId, ruleId);
77
+ }
78
+ /**
79
+ * Creates a stable key for a guild ban.
80
+ */
81
+ function banKey(guildId, userId) {
82
+ return guildScopedKey(guildId, userId);
83
+ }
84
+ /**
85
+ * Creates a stable key for a guild integration.
86
+ */
87
+ function integrationKey(guildId, integrationId) {
88
+ return guildScopedKey(guildId, integrationId);
89
+ }
90
+ /**
91
+ * Creates a stable key for an invite, using `@global` for invites that do not belong to a guild.
92
+ */
93
+ function inviteKey(guildId, code) {
94
+ return `${guildId ?? "@global"}:${code}`;
95
+ }
96
+ /**
97
+ * Creates a stable key for a thread member.
98
+ */
99
+ function threadMemberKey(threadId, userId) {
100
+ return `${threadId}:${userId}`;
101
+ }
102
+ /**
103
+ * Creates a stable key for application command permissions.
104
+ */
105
+ function applicationCommandPermissionsKey(applicationId, guildId, commandId) {
106
+ return `${applicationId}:${guildId}:${commandId}`;
107
+ }
108
+
109
+ //#endregion
110
+ //#region src/lib/reactions.ts
111
+ /**
112
+ * Whether two reaction emojis are the same: custom emojis match by ID, Unicode emojis by name.
113
+ */
114
+ function isSameEmoji(a, b) {
115
+ return a.id ? a.id === b.id : !b.id && a.name === b.name;
116
+ }
117
+ /**
118
+ * Adds a `MESSAGE_REACTION_ADD` to a cached message's reactions.
119
+ *
120
+ * @param message The cached message.
121
+ * @param data The dispatch data.
122
+ * @param clientUserId The bot's user ID, to set `me` when the bot reacted.
123
+ */
124
+ function addReaction(message, data, clientUserId) {
125
+ const me = data.user_id === clientUserId;
126
+ const kind = data.burst ? "burst" : "normal";
127
+ const reactions = message.reactions ?? [];
128
+ const existing = reactions.find((reaction) => isSameEmoji(reaction.emoji, data.emoji));
129
+ if (!existing) {
130
+ const reaction = {
131
+ emoji: data.emoji,
132
+ count: 1,
133
+ count_details: {
134
+ normal: data.burst ? 0 : 1,
135
+ burst: data.burst ? 1 : 0
136
+ },
137
+ me: me && !data.burst,
138
+ me_burst: me && data.burst,
139
+ burst_colors: data.burst_colors ?? []
140
+ };
141
+ return {
142
+ ...message,
143
+ reactions: [...reactions, reaction]
144
+ };
145
+ }
146
+ return {
147
+ ...message,
148
+ reactions: reactions.map((reaction) => reaction === existing ? {
149
+ ...reaction,
150
+ count: reaction.count + 1,
151
+ count_details: {
152
+ ...reaction.count_details,
153
+ [kind]: reaction.count_details[kind] + 1
154
+ },
155
+ me: reaction.me || me && !data.burst,
156
+ me_burst: reaction.me_burst || me && data.burst,
157
+ burst_colors: data.burst ? data.burst_colors ?? reaction.burst_colors : reaction.burst_colors
158
+ } : reaction)
159
+ };
160
+ }
161
+ /**
162
+ * Removes a `MESSAGE_REACTION_REMOVE` from a cached message's reactions, dropping the reaction when its count reaches
163
+ * zero.
164
+ *
165
+ * @param message The cached message.
166
+ * @param data The dispatch data.
167
+ * @param clientUserId The bot's user ID, to clear `me` when the bot's reaction was removed.
168
+ */
169
+ function removeReaction(message, data, clientUserId) {
170
+ const me = data.user_id === clientUserId;
171
+ const kind = data.burst ? "burst" : "normal";
172
+ const reactions = (message.reactions ?? []).flatMap((reaction) => {
173
+ if (!isSameEmoji(reaction.emoji, data.emoji)) return [reaction];
174
+ if (reaction.count <= 1) return [];
175
+ return [{
176
+ ...reaction,
177
+ count: reaction.count - 1,
178
+ count_details: {
179
+ ...reaction.count_details,
180
+ [kind]: Math.max(0, reaction.count_details[kind] - 1)
181
+ },
182
+ me: reaction.me && !(me && !data.burst),
183
+ me_burst: reaction.me_burst && !(me && data.burst)
184
+ }];
185
+ });
186
+ return {
187
+ ...message,
188
+ reactions
189
+ };
190
+ }
191
+ /**
192
+ * Removes every reaction with one emoji from a cached message.
193
+ *
194
+ * @param message The cached message.
195
+ * @param emoji The emoji.
196
+ */
197
+ function removeReactionEmoji(message, emoji) {
198
+ return {
199
+ ...message,
200
+ reactions: (message.reactions ?? []).filter((reaction) => !isSameEmoji(reaction.emoji, emoji))
201
+ };
202
+ }
203
+ /**
204
+ * Counts a `MESSAGE_POLL_VOTE_ADD` or `MESSAGE_POLL_VOTE_REMOVE` in a cached message's poll results.
205
+ *
206
+ * @param message The cached message.
207
+ * @param data The dispatch data.
208
+ * @param delta `1` for a vote, `-1` for a removed vote.
209
+ * @param clientUserId The bot's user ID, to set `me_voted` when the bot voted.
210
+ */
211
+ function countPollVote(message, data, delta, clientUserId) {
212
+ const { poll } = message;
213
+ if (!poll) return message;
214
+ const me = data.user_id === clientUserId;
215
+ const results = poll.results ?? {
216
+ is_finalized: false,
217
+ answer_counts: []
218
+ };
219
+ const counts = results.answer_counts.some((count) => count.id === data.answer_id) ? results.answer_counts : [...results.answer_counts, {
220
+ id: data.answer_id,
221
+ count: 0,
222
+ me_voted: false
223
+ }];
224
+ return {
225
+ ...message,
226
+ poll: {
227
+ ...poll,
228
+ results: {
229
+ ...results,
230
+ answer_counts: counts.map((count) => count.id === data.answer_id ? {
231
+ ...count,
232
+ count: Math.max(0, count.count + delta),
233
+ me_voted: me ? delta === 1 : count.me_voted
234
+ } : count)
235
+ }
236
+ }
237
+ };
238
+ }
239
+
240
+ //#endregion
241
+ //#region src/lib/operations.ts
242
+ const cacheEntityNameRecord = {
243
+ applicationCommandPermissions: true,
244
+ auditLogEntries: true,
245
+ autoModerationRules: true,
246
+ bans: true,
247
+ channels: true,
248
+ emojis: true,
249
+ entitlements: true,
250
+ guilds: true,
251
+ integrations: true,
252
+ invites: true,
253
+ members: true,
254
+ messages: true,
255
+ presences: true,
256
+ roles: true,
257
+ scheduledEvents: true,
258
+ soundboardSounds: true,
259
+ stageInstances: true,
260
+ stickers: true,
261
+ subscriptions: true,
262
+ threadMembers: true,
263
+ threads: true,
264
+ users: true,
265
+ voiceStates: true
266
+ };
267
+ /**
268
+ * The name of every entity cache held by a {@link Cache}.
269
+ */
270
+ const CacheEntityNames = Object.keys(cacheEntityNameRecord);
271
+ /**
272
+ * Translates a gateway dispatch into the list of {@link CacheOperation}s it implies, including the cascades (e.g.
273
+ * `CHANNEL_DELETE` also drops that channel's messages).
274
+ *
275
+ * @remarks
276
+ * This is a pure function, it does not touch any cache. Use {@link applyGatewayDispatch} to apply them.
277
+ * `INTERACTION_CREATE` is intentionally ignored: interactions are short-lived and never cached.
278
+ *
279
+ * @param payload The gateway dispatch payload.
280
+ * @param context The bot's user ID, for the `me` flags of reactions and poll votes.
281
+ */
282
+ function createCacheOperations(payload, context = {}) {
283
+ const { clientUserId } = context;
284
+ const operations = [];
285
+ switch (payload.t) {
286
+ case GatewayDispatchEvents.ApplicationCommandPermissionsUpdate: {
287
+ const data = payload.d;
288
+ operations.push({
289
+ type: "upsert",
290
+ store: "applicationCommandPermissions",
291
+ key: applicationCommandPermissionsKey(data.application_id, data.guild_id, data.id),
292
+ raw: data
293
+ });
294
+ break;
295
+ }
296
+ case GatewayDispatchEvents.AutoModerationRuleCreate:
297
+ case GatewayDispatchEvents.AutoModerationRuleUpdate: {
298
+ const data = payload.d;
299
+ operations.push({
300
+ type: "upsert",
301
+ store: "autoModerationRules",
302
+ key: autoModerationRuleKey(data.guild_id, data.id),
303
+ raw: data
304
+ });
305
+ break;
306
+ }
307
+ case GatewayDispatchEvents.AutoModerationRuleDelete: {
308
+ const data = payload.d;
309
+ operations.push({
310
+ type: "delete",
311
+ store: "autoModerationRules",
312
+ key: autoModerationRuleKey(data.guild_id, data.id)
313
+ });
314
+ break;
315
+ }
316
+ case GatewayDispatchEvents.ChannelCreate:
317
+ case GatewayDispatchEvents.ChannelUpdate: {
318
+ const data = payload.d;
319
+ operations.push({
320
+ type: "upsert",
321
+ store: "channels",
322
+ key: data.id,
323
+ raw: data,
324
+ merge: payload.t === GatewayDispatchEvents.ChannelUpdate
325
+ });
326
+ break;
327
+ }
328
+ case GatewayDispatchEvents.ChannelDelete: {
329
+ const data = payload.d;
330
+ operations.push({
331
+ type: "delete",
332
+ store: "channels",
333
+ key: data.id
334
+ });
335
+ operations.push({
336
+ type: "deletePrefix",
337
+ store: "messages",
338
+ prefix: `${data.id}:`
339
+ });
340
+ break;
341
+ }
342
+ case GatewayDispatchEvents.EntitlementCreate:
343
+ case GatewayDispatchEvents.EntitlementUpdate: {
344
+ const data = payload.d;
345
+ operations.push({
346
+ type: "upsert",
347
+ store: "entitlements",
348
+ key: data.id,
349
+ raw: data
350
+ });
351
+ break;
352
+ }
353
+ case GatewayDispatchEvents.EntitlementDelete:
354
+ operations.push({
355
+ type: "delete",
356
+ store: "entitlements",
357
+ key: payload.d.id
358
+ });
359
+ break;
360
+ case GatewayDispatchEvents.GuildAuditLogEntryCreate: {
361
+ const data = payload.d;
362
+ operations.push({
363
+ type: "upsert",
364
+ store: "auditLogEntries",
365
+ key: guildScopedKey(data.guild_id, data.id),
366
+ raw: data
367
+ });
368
+ break;
369
+ }
370
+ case GatewayDispatchEvents.GuildBanAdd: {
371
+ const data = payload.d;
372
+ operations.push({
373
+ type: "upsert",
374
+ store: "bans",
375
+ key: banKey(data.guild_id, data.user.id),
376
+ raw: data
377
+ });
378
+ operations.push({
379
+ type: "upsert",
380
+ store: "users",
381
+ key: data.user.id,
382
+ raw: data.user
383
+ });
384
+ break;
385
+ }
386
+ case GatewayDispatchEvents.GuildBanRemove: {
387
+ const data = payload.d;
388
+ operations.push({
389
+ type: "delete",
390
+ store: "bans",
391
+ key: banKey(data.guild_id, data.user.id)
392
+ });
393
+ operations.push({
394
+ type: "upsert",
395
+ store: "users",
396
+ key: data.user.id,
397
+ raw: data.user
398
+ });
399
+ break;
400
+ }
401
+ case GatewayDispatchEvents.GuildCreate:
402
+ hydrateGuildCreate(operations, payload.d);
403
+ break;
404
+ case GatewayDispatchEvents.GuildUpdate: {
405
+ const { roles, emojis, stickers, ...data } = payload.d;
406
+ operations.push({
407
+ type: "upsert",
408
+ store: "guilds",
409
+ key: data.id,
410
+ raw: data,
411
+ merge: true
412
+ });
413
+ for (const role of roles ?? []) hydrateRole(operations, data.id, role);
414
+ for (const emoji of emojis ?? []) hydrateEmoji(operations, data.id, emoji);
415
+ for (const sticker of stickers ?? []) hydrateSticker(operations, data.id, sticker);
416
+ break;
417
+ }
418
+ case GatewayDispatchEvents.GuildDelete: {
419
+ const data = payload.d;
420
+ if (data.unavailable) {
421
+ operations.push({
422
+ type: "upsert",
423
+ store: "guilds",
424
+ key: data.id,
425
+ raw: data,
426
+ merge: true
427
+ });
428
+ break;
429
+ }
430
+ operations.push({
431
+ type: "delete",
432
+ store: "guilds",
433
+ key: data.id
434
+ });
435
+ deleteGuildScopedResources(operations, data.id);
436
+ break;
437
+ }
438
+ case GatewayDispatchEvents.GuildEmojisUpdate: {
439
+ const data = payload.d;
440
+ operations.push({
441
+ type: "deletePrefix",
442
+ store: "emojis",
443
+ prefix: `${data.guild_id}:`
444
+ });
445
+ for (const emoji of data.emojis) {
446
+ if (!emoji.id) continue;
447
+ operations.push({
448
+ type: "upsert",
449
+ store: "emojis",
450
+ key: emojiKey(data.guild_id, emoji.id),
451
+ raw: withGuildId(emoji, data.guild_id)
452
+ });
453
+ }
454
+ break;
455
+ }
456
+ case GatewayDispatchEvents.GuildStickersUpdate: {
457
+ const data = payload.d;
458
+ operations.push({
459
+ type: "deletePrefix",
460
+ store: "stickers",
461
+ prefix: `${data.guild_id}:`
462
+ });
463
+ for (const sticker of data.stickers) operations.push({
464
+ type: "upsert",
465
+ store: "stickers",
466
+ key: stickerKey(data.guild_id, sticker.id),
467
+ raw: withGuildId(sticker, data.guild_id)
468
+ });
469
+ break;
470
+ }
471
+ case GatewayDispatchEvents.GuildMemberAdd:
472
+ case GatewayDispatchEvents.GuildMemberUpdate: {
473
+ const data = payload.d;
474
+ operations.push({
475
+ type: "upsert",
476
+ store: "members",
477
+ key: memberKey(data.guild_id, data.user.id),
478
+ raw: data,
479
+ merge: true
480
+ });
481
+ operations.push({
482
+ type: "upsert",
483
+ store: "users",
484
+ key: data.user.id,
485
+ raw: data.user
486
+ });
487
+ break;
488
+ }
489
+ case GatewayDispatchEvents.GuildMemberRemove: {
490
+ const data = payload.d;
491
+ operations.push({
492
+ type: "delete",
493
+ store: "members",
494
+ key: memberKey(data.guild_id, data.user.id)
495
+ });
496
+ operations.push({
497
+ type: "upsert",
498
+ store: "users",
499
+ key: data.user.id,
500
+ raw: data.user
501
+ });
502
+ break;
503
+ }
504
+ case GatewayDispatchEvents.GuildMembersChunk:
505
+ hydrateGuildMembersChunk(operations, payload.d);
506
+ break;
507
+ case GatewayDispatchEvents.GuildRoleCreate:
508
+ case GatewayDispatchEvents.GuildRoleUpdate: {
509
+ const data = payload.d;
510
+ operations.push({
511
+ type: "upsert",
512
+ store: "roles",
513
+ key: roleKey(data.guild_id, data.role.id),
514
+ raw: withGuildId(data.role, data.guild_id),
515
+ merge: payload.t === GatewayDispatchEvents.GuildRoleUpdate
516
+ });
517
+ break;
518
+ }
519
+ case GatewayDispatchEvents.GuildRoleDelete: {
520
+ const data = payload.d;
521
+ operations.push({
522
+ type: "delete",
523
+ store: "roles",
524
+ key: roleKey(data.guild_id, data.role_id)
525
+ });
526
+ break;
527
+ }
528
+ case GatewayDispatchEvents.GuildScheduledEventCreate:
529
+ case GatewayDispatchEvents.GuildScheduledEventUpdate: {
530
+ const data = payload.d;
531
+ operations.push({
532
+ type: "upsert",
533
+ store: "scheduledEvents",
534
+ key: scheduledEventKey(data.guild_id, data.id),
535
+ raw: data,
536
+ merge: true
537
+ });
538
+ if (data.creator) operations.push({
539
+ type: "upsert",
540
+ store: "users",
541
+ key: data.creator.id,
542
+ raw: data.creator
543
+ });
544
+ break;
545
+ }
546
+ case GatewayDispatchEvents.GuildScheduledEventDelete: {
547
+ const data = payload.d;
548
+ operations.push({
549
+ type: "delete",
550
+ store: "scheduledEvents",
551
+ key: scheduledEventKey(data.guild_id, data.id)
552
+ });
553
+ break;
554
+ }
555
+ case GatewayDispatchEvents.GuildSoundboardSoundCreate:
556
+ case GatewayDispatchEvents.GuildSoundboardSoundUpdate: {
557
+ const data = payload.d;
558
+ if (!data.guild_id) break;
559
+ operations.push({
560
+ type: "upsert",
561
+ store: "soundboardSounds",
562
+ key: soundboardSoundKey(data.guild_id, data.sound_id),
563
+ raw: data,
564
+ merge: payload.t === GatewayDispatchEvents.GuildSoundboardSoundUpdate
565
+ });
566
+ break;
567
+ }
568
+ case GatewayDispatchEvents.GuildSoundboardSoundDelete: {
569
+ const data = payload.d;
570
+ operations.push({
571
+ type: "delete",
572
+ store: "soundboardSounds",
573
+ key: soundboardSoundKey(data.guild_id, data.sound_id)
574
+ });
575
+ break;
576
+ }
577
+ case GatewayDispatchEvents.GuildSoundboardSoundsUpdate:
578
+ hydrateGuildSoundboardSoundsUpdate(operations, payload.d);
579
+ break;
580
+ case GatewayDispatchEvents.SoundboardSounds:
581
+ hydrateSoundboardSounds(operations, payload.d);
582
+ break;
583
+ case GatewayDispatchEvents.IntegrationCreate:
584
+ case GatewayDispatchEvents.IntegrationUpdate: {
585
+ const data = payload.d;
586
+ operations.push({
587
+ type: "upsert",
588
+ store: "integrations",
589
+ key: integrationKey(data.guild_id, data.id),
590
+ raw: data,
591
+ merge: true
592
+ });
593
+ break;
594
+ }
595
+ case GatewayDispatchEvents.IntegrationDelete: {
596
+ const data = payload.d;
597
+ operations.push({
598
+ type: "delete",
599
+ store: "integrations",
600
+ key: integrationKey(data.guild_id, data.id)
601
+ });
602
+ break;
603
+ }
604
+ case GatewayDispatchEvents.InviteCreate: {
605
+ const data = payload.d;
606
+ operations.push({
607
+ type: "upsert",
608
+ store: "invites",
609
+ key: inviteKey(data.guild_id, data.code),
610
+ raw: data
611
+ });
612
+ break;
613
+ }
614
+ case GatewayDispatchEvents.InviteDelete: {
615
+ const data = payload.d;
616
+ operations.push({
617
+ type: "delete",
618
+ store: "invites",
619
+ key: inviteKey(data.guild_id, data.code)
620
+ });
621
+ break;
622
+ }
623
+ case GatewayDispatchEvents.MessageCreate:
624
+ case GatewayDispatchEvents.MessageUpdate: {
625
+ const data = payload.d;
626
+ operations.push({
627
+ type: "upsert",
628
+ store: "messages",
629
+ key: messageKey(data.channel_id, data.id),
630
+ raw: data,
631
+ merge: payload.t === GatewayDispatchEvents.MessageUpdate
632
+ });
633
+ const author = "author" in data ? data.author : void 0;
634
+ if (author) {
635
+ operations.push({
636
+ type: "upsert",
637
+ store: "users",
638
+ key: author.id,
639
+ raw: author
640
+ });
641
+ if (data.member && data.guild_id) operations.push({
642
+ type: "upsert",
643
+ store: "members",
644
+ key: memberKey(data.guild_id, author.id),
645
+ raw: withGuildId(data.member, data.guild_id),
646
+ merge: true
647
+ });
648
+ }
649
+ break;
650
+ }
651
+ case GatewayDispatchEvents.MessageDelete: {
652
+ const data = payload.d;
653
+ operations.push({
654
+ type: "delete",
655
+ store: "messages",
656
+ key: messageKey(data.channel_id, data.id)
657
+ });
658
+ break;
659
+ }
660
+ case GatewayDispatchEvents.MessageDeleteBulk: {
661
+ const data = payload.d;
662
+ for (const id of data.ids) operations.push({
663
+ type: "delete",
664
+ store: "messages",
665
+ key: messageKey(data.channel_id, id)
666
+ });
667
+ break;
668
+ }
669
+ case GatewayDispatchEvents.MessageReactionAdd: {
670
+ const data = payload.d;
671
+ operations.push(updateMessage(data.channel_id, data.message_id, (message) => addReaction(message, data, clientUserId)));
672
+ const user = data.member?.user;
673
+ if (user && data.guild_id) {
674
+ operations.push({
675
+ type: "upsert",
676
+ store: "users",
677
+ key: user.id,
678
+ raw: user
679
+ });
680
+ operations.push({
681
+ type: "upsert",
682
+ store: "members",
683
+ key: memberKey(data.guild_id, user.id),
684
+ raw: withGuildId(data.member, data.guild_id),
685
+ merge: true
686
+ });
687
+ }
688
+ break;
689
+ }
690
+ case GatewayDispatchEvents.MessageReactionRemove: {
691
+ const data = payload.d;
692
+ operations.push(updateMessage(data.channel_id, data.message_id, (message) => removeReaction(message, data, clientUserId)));
693
+ break;
694
+ }
695
+ case GatewayDispatchEvents.MessageReactionRemoveAll: {
696
+ const data = payload.d;
697
+ operations.push(updateMessage(data.channel_id, data.message_id, (message) => ({
698
+ ...message,
699
+ reactions: []
700
+ })));
701
+ break;
702
+ }
703
+ case GatewayDispatchEvents.MessageReactionRemoveEmoji: {
704
+ const data = payload.d;
705
+ operations.push(updateMessage(data.channel_id, data.message_id, (message) => removeReactionEmoji(message, data.emoji)));
706
+ break;
707
+ }
708
+ case GatewayDispatchEvents.MessagePollVoteAdd:
709
+ case GatewayDispatchEvents.MessagePollVoteRemove: {
710
+ const data = payload.d;
711
+ const delta = payload.t === GatewayDispatchEvents.MessagePollVoteAdd ? 1 : -1;
712
+ operations.push(updateMessage(data.channel_id, data.message_id, (message) => countPollVote(message, data, delta, clientUserId)));
713
+ break;
714
+ }
715
+ case GatewayDispatchEvents.PresenceUpdate: {
716
+ const data = payload.d;
717
+ operations.push({
718
+ type: "upsert",
719
+ store: "presences",
720
+ key: presenceKey(data.guild_id, data.user.id),
721
+ raw: data,
722
+ merge: true
723
+ });
724
+ operations.push({
725
+ type: "upsert",
726
+ store: "users",
727
+ key: data.user.id,
728
+ raw: data.user,
729
+ merge: true
730
+ });
731
+ break;
732
+ }
733
+ case GatewayDispatchEvents.Ready: {
734
+ const data = payload.d;
735
+ operations.push({
736
+ type: "upsert",
737
+ store: "users",
738
+ key: data.user.id,
739
+ raw: data.user
740
+ });
741
+ for (const guild of data.guilds) operations.push({
742
+ type: "upsert",
743
+ store: "guilds",
744
+ key: guild.id,
745
+ raw: guild,
746
+ merge: true
747
+ });
748
+ break;
749
+ }
750
+ case GatewayDispatchEvents.StageInstanceCreate:
751
+ case GatewayDispatchEvents.StageInstanceUpdate: {
752
+ const data = payload.d;
753
+ operations.push({
754
+ type: "upsert",
755
+ store: "stageInstances",
756
+ key: stageInstanceKey(data.guild_id, data.channel_id),
757
+ raw: data,
758
+ merge: true
759
+ });
760
+ break;
761
+ }
762
+ case GatewayDispatchEvents.StageInstanceDelete: {
763
+ const data = payload.d;
764
+ operations.push({
765
+ type: "delete",
766
+ store: "stageInstances",
767
+ key: stageInstanceKey(data.guild_id, data.channel_id)
768
+ });
769
+ break;
770
+ }
771
+ case GatewayDispatchEvents.SubscriptionCreate:
772
+ case GatewayDispatchEvents.SubscriptionUpdate: {
773
+ const data = payload.d;
774
+ operations.push({
775
+ type: "upsert",
776
+ store: "subscriptions",
777
+ key: data.id,
778
+ raw: data
779
+ });
780
+ break;
781
+ }
782
+ case GatewayDispatchEvents.SubscriptionDelete:
783
+ operations.push({
784
+ type: "delete",
785
+ store: "subscriptions",
786
+ key: payload.d.id
787
+ });
788
+ break;
789
+ case GatewayDispatchEvents.ThreadCreate:
790
+ case GatewayDispatchEvents.ThreadUpdate: {
791
+ const data = payload.d;
792
+ operations.push({
793
+ type: "upsert",
794
+ store: "threads",
795
+ key: data.id,
796
+ raw: data,
797
+ merge: payload.t === GatewayDispatchEvents.ThreadUpdate
798
+ });
799
+ const member = "member" in data ? data.member : void 0;
800
+ if (member?.user_id) operations.push({
801
+ type: "upsert",
802
+ store: "threadMembers",
803
+ key: threadMemberKey(data.id, member.user_id),
804
+ raw: data.guild_id ? withGuildId(member, data.guild_id) : member,
805
+ merge: true
806
+ });
807
+ break;
808
+ }
809
+ case GatewayDispatchEvents.ThreadDelete: {
810
+ const data = payload.d;
811
+ operations.push({
812
+ type: "delete",
813
+ store: "threads",
814
+ key: data.id
815
+ });
816
+ operations.push({
817
+ type: "deletePrefix",
818
+ store: "threadMembers",
819
+ prefix: `${data.id}:`
820
+ });
821
+ operations.push({
822
+ type: "deletePrefix",
823
+ store: "messages",
824
+ prefix: `${data.id}:`
825
+ });
826
+ break;
827
+ }
828
+ case GatewayDispatchEvents.ThreadListSync:
829
+ hydrateThreadListSync(operations, payload.d);
830
+ break;
831
+ case GatewayDispatchEvents.ThreadMemberUpdate: {
832
+ const data = payload.d;
833
+ if (!data.id || !data.user_id) break;
834
+ operations.push({
835
+ type: "upsert",
836
+ store: "threadMembers",
837
+ key: threadMemberKey(data.id, data.user_id),
838
+ raw: data,
839
+ merge: true
840
+ });
841
+ break;
842
+ }
843
+ case GatewayDispatchEvents.ThreadMembersUpdate:
844
+ hydrateThreadMembersUpdate(operations, payload.d);
845
+ break;
846
+ case GatewayDispatchEvents.UserUpdate: {
847
+ const data = payload.d;
848
+ operations.push({
849
+ type: "upsert",
850
+ store: "users",
851
+ key: data.id,
852
+ raw: data,
853
+ merge: true
854
+ });
855
+ break;
856
+ }
857
+ case GatewayDispatchEvents.VoiceStateUpdate: {
858
+ const data = payload.d;
859
+ if (!data.guild_id) break;
860
+ const key = voiceStateKey(data.guild_id, data.user_id);
861
+ if (data.channel_id) operations.push({
862
+ type: "upsert",
863
+ store: "voiceStates",
864
+ key,
865
+ raw: data,
866
+ merge: true
867
+ });
868
+ else operations.push({
869
+ type: "delete",
870
+ store: "voiceStates",
871
+ key
872
+ });
873
+ const user = data.member?.user;
874
+ if (data.member && user) {
875
+ operations.push({
876
+ type: "upsert",
877
+ store: "users",
878
+ key: user.id,
879
+ raw: user
880
+ });
881
+ operations.push({
882
+ type: "upsert",
883
+ store: "members",
884
+ key: memberKey(data.guild_id, user.id),
885
+ raw: withGuildId(data.member, data.guild_id),
886
+ merge: true
887
+ });
888
+ }
889
+ break;
890
+ }
891
+ }
892
+ return operations;
893
+ }
894
+ /**
895
+ * Applies a list of {@link CacheOperation}s to a {@link Cache}, sequentially and in order.
896
+ *
897
+ * @param cache The cache to mutate.
898
+ * @param operations The operations to apply, usually created by {@link createCacheOperations}.
899
+ */
900
+ async function applyCacheOperations(cache, operations) {
901
+ for (const operation of operations) {
902
+ const store = cache[operation.store];
903
+ switch (operation.type) {
904
+ case "upsert": {
905
+ const value = operation.merge ? mergeValues(await store.get(operation.key), operation.raw) : operation.raw;
906
+ await store.set(operation.key, value);
907
+ break;
908
+ }
909
+ case "update": {
910
+ const existing = await store.get(operation.key);
911
+ if (existing !== void 0) await store.set(operation.key, operation.update(existing));
912
+ break;
913
+ }
914
+ case "delete":
915
+ await store.delete(operation.key);
916
+ break;
917
+ case "deletePrefix":
918
+ for (const key of await store.keys()) if (key.startsWith(operation.prefix)) await store.delete(key);
919
+ break;
920
+ case "deleteWhere": for (const [key, value] of await store.entries()) if (operation.predicate(value)) await store.delete(key);
921
+ }
922
+ }
923
+ }
924
+ /**
925
+ * Writes a gateway dispatch into every relevant entity cache of a {@link Cache}.
926
+ *
927
+ * @param cache The cache to mutate.
928
+ * @param payload The gateway dispatch payload.
929
+ * @param context The bot's user ID, for the `me` flags of reactions and poll votes.
930
+ */
931
+ function applyGatewayDispatch(cache, payload, context) {
932
+ return applyCacheOperations(cache, createCacheOperations(payload, context));
933
+ }
934
+ /**
935
+ * Shallow-merges `value` onto `existing` when both are plain objects, returning `value` otherwise.
936
+ */
937
+ function mergeValues(existing, value) {
938
+ if (isObject(existing) && isObject(value)) return {
939
+ ...existing,
940
+ ...value
941
+ };
942
+ return value;
943
+ }
944
+ function hydrateGuildCreate(operations, guild) {
945
+ if ("unavailable" in guild && guild.unavailable) {
946
+ operations.push({
947
+ type: "upsert",
948
+ store: "guilds",
949
+ key: guild.id,
950
+ raw: guild,
951
+ merge: true
952
+ });
953
+ return;
954
+ }
955
+ const { channels, threads, members, presences, voice_states: voiceStates, stage_instances: stageInstances, guild_scheduled_events: scheduledEvents, soundboard_sounds: soundboardSounds, roles, emojis, stickers, ...rest } = guild;
956
+ operations.push({
957
+ type: "upsert",
958
+ store: "guilds",
959
+ key: guild.id,
960
+ raw: rest
961
+ });
962
+ for (const channel of channels ?? []) operations.push({
963
+ type: "upsert",
964
+ store: "channels",
965
+ key: channel.id,
966
+ raw: withGuildId(channel, guild.id)
967
+ });
968
+ for (const thread of threads ?? []) {
969
+ operations.push({
970
+ type: "upsert",
971
+ store: "threads",
972
+ key: thread.id,
973
+ raw: withGuildId(thread, guild.id)
974
+ });
975
+ if (thread.member?.user_id) operations.push({
976
+ type: "upsert",
977
+ store: "threadMembers",
978
+ key: threadMemberKey(thread.id, thread.member.user_id),
979
+ raw: withGuildId(thread.member, guild.id)
980
+ });
981
+ }
982
+ for (const member of members ?? []) hydrateMember(operations, guild.id, member);
983
+ for (const presence of presences ?? []) operations.push({
984
+ type: "upsert",
985
+ store: "presences",
986
+ key: presenceKey(guild.id, presence.user.id),
987
+ raw: withGuildId(presence, guild.id)
988
+ });
989
+ for (const role of roles ?? []) hydrateRole(operations, guild.id, role);
990
+ for (const emoji of emojis ?? []) hydrateEmoji(operations, guild.id, emoji);
991
+ for (const sticker of stickers ?? []) hydrateSticker(operations, guild.id, sticker);
992
+ for (const voiceState of voiceStates ?? []) {
993
+ if (!voiceState.channel_id) continue;
994
+ operations.push({
995
+ type: "upsert",
996
+ store: "voiceStates",
997
+ key: voiceStateKey(guild.id, voiceState.user_id),
998
+ raw: withGuildId(voiceState, guild.id)
999
+ });
1000
+ }
1001
+ for (const stageInstance of stageInstances ?? []) operations.push({
1002
+ type: "upsert",
1003
+ store: "stageInstances",
1004
+ key: stageInstanceKey(guild.id, stageInstance.channel_id),
1005
+ raw: stageInstance
1006
+ });
1007
+ for (const scheduledEvent of scheduledEvents ?? []) operations.push({
1008
+ type: "upsert",
1009
+ store: "scheduledEvents",
1010
+ key: scheduledEventKey(guild.id, scheduledEvent.id),
1011
+ raw: scheduledEvent
1012
+ });
1013
+ for (const sound of soundboardSounds ?? []) hydrateSoundboardSound(operations, guild.id, sound);
1014
+ }
1015
+ function hydrateGuildMembersChunk(operations, data) {
1016
+ for (const member of data.members) hydrateMember(operations, data.guild_id, member);
1017
+ for (const presence of data.presences ?? []) operations.push({
1018
+ type: "upsert",
1019
+ store: "presences",
1020
+ key: presenceKey(data.guild_id, presence.user.id),
1021
+ raw: withGuildId(presence, data.guild_id)
1022
+ });
1023
+ }
1024
+ function hydrateGuildSoundboardSoundsUpdate(operations, data) {
1025
+ operations.push({
1026
+ type: "deletePrefix",
1027
+ store: "soundboardSounds",
1028
+ prefix: `${data.guild_id}:`
1029
+ });
1030
+ for (const sound of data.soundboard_sounds) hydrateSoundboardSound(operations, data.guild_id, sound);
1031
+ }
1032
+ function hydrateSoundboardSounds(operations, data) {
1033
+ for (const sound of data.soundboard_sounds) hydrateSoundboardSound(operations, data.guild_id, sound);
1034
+ }
1035
+ function hydrateThreadListSync(operations, data) {
1036
+ for (const thread of data.threads) operations.push({
1037
+ type: "upsert",
1038
+ store: "threads",
1039
+ key: thread.id,
1040
+ raw: withGuildId(thread, data.guild_id),
1041
+ merge: true
1042
+ });
1043
+ for (const member of data.members) {
1044
+ if (!member.id || !member.user_id) continue;
1045
+ operations.push({
1046
+ type: "upsert",
1047
+ store: "threadMembers",
1048
+ key: threadMemberKey(member.id, member.user_id),
1049
+ raw: withGuildId(member, data.guild_id),
1050
+ merge: true
1051
+ });
1052
+ }
1053
+ }
1054
+ function hydrateThreadMembersUpdate(operations, data) {
1055
+ for (const member of data.added_members ?? []) {
1056
+ if (!member.user_id) continue;
1057
+ operations.push({
1058
+ type: "upsert",
1059
+ store: "threadMembers",
1060
+ key: threadMemberKey(data.id, member.user_id),
1061
+ raw: withGuildId(member, data.guild_id),
1062
+ merge: true
1063
+ });
1064
+ }
1065
+ for (const userId of data.removed_member_ids ?? []) operations.push({
1066
+ type: "delete",
1067
+ store: "threadMembers",
1068
+ key: threadMemberKey(data.id, userId)
1069
+ });
1070
+ }
1071
+ function hydrateMember(operations, guildId, member) {
1072
+ if (!member.user) return;
1073
+ operations.push({
1074
+ type: "upsert",
1075
+ store: "members",
1076
+ key: memberKey(guildId, member.user.id),
1077
+ raw: withGuildId(member, guildId),
1078
+ merge: true
1079
+ });
1080
+ operations.push({
1081
+ type: "upsert",
1082
+ store: "users",
1083
+ key: member.user.id,
1084
+ raw: member.user
1085
+ });
1086
+ }
1087
+ function hydrateRole(operations, guildId, role) {
1088
+ operations.push({
1089
+ type: "upsert",
1090
+ store: "roles",
1091
+ key: roleKey(guildId, role.id),
1092
+ raw: withGuildId(role, guildId)
1093
+ });
1094
+ }
1095
+ function hydrateEmoji(operations, guildId, emoji) {
1096
+ if (!emoji.id) return;
1097
+ operations.push({
1098
+ type: "upsert",
1099
+ store: "emojis",
1100
+ key: emojiKey(guildId, emoji.id),
1101
+ raw: withGuildId(emoji, guildId)
1102
+ });
1103
+ }
1104
+ function hydrateSticker(operations, guildId, sticker) {
1105
+ operations.push({
1106
+ type: "upsert",
1107
+ store: "stickers",
1108
+ key: stickerKey(guildId, sticker.id),
1109
+ raw: withGuildId(sticker, guildId)
1110
+ });
1111
+ }
1112
+ function hydrateSoundboardSound(operations, guildId, sound) {
1113
+ operations.push({
1114
+ type: "upsert",
1115
+ store: "soundboardSounds",
1116
+ key: soundboardSoundKey(guildId, sound.sound_id),
1117
+ raw: withGuildId(sound, guildId)
1118
+ });
1119
+ }
1120
+ function deleteGuildScopedResources(operations, guildId) {
1121
+ for (const store of [
1122
+ "auditLogEntries",
1123
+ "autoModerationRules",
1124
+ "bans",
1125
+ "emojis",
1126
+ "integrations",
1127
+ "members",
1128
+ "presences",
1129
+ "roles",
1130
+ "scheduledEvents",
1131
+ "soundboardSounds",
1132
+ "stageInstances",
1133
+ "stickers",
1134
+ "voiceStates"
1135
+ ]) operations.push({
1136
+ type: "deletePrefix",
1137
+ store,
1138
+ prefix: `${guildId}:`
1139
+ });
1140
+ for (const store of [
1141
+ "applicationCommandPermissions",
1142
+ "channels",
1143
+ "threads",
1144
+ "threadMembers",
1145
+ "messages",
1146
+ "invites"
1147
+ ]) operations.push({
1148
+ type: "deleteWhere",
1149
+ store,
1150
+ predicate: (value) => isObject(value) && value.guild_id === guildId
1151
+ });
1152
+ }
1153
+ function updateMessage(channelId, messageId, update) {
1154
+ return {
1155
+ type: "update",
1156
+ store: "messages",
1157
+ key: messageKey(channelId, messageId),
1158
+ update: (value) => update(value)
1159
+ };
1160
+ }
1161
+ function withGuildId(value, guildId) {
1162
+ return {
1163
+ ...value,
1164
+ guild_id: guildId
1165
+ };
1166
+ }
1167
+ function isObject(value) {
1168
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1169
+ }
1170
+
1171
+ //#endregion
1172
+ //#region src/lib/gateway.ts
1173
+ /**
1174
+ * Writes gateway dispatches into a cache. Call the returned function to stop listening.
1175
+ * Use this when a gateway has no GatewayClient managing its dispatch queue.
1176
+ */
1177
+ function attachCacheToGateway(gateway, cache, options = {}) {
1178
+ const listener = (payload, shardId) => {
1179
+ applyGatewayDispatch(cache, payload, { clientUserId: options.clientUserId }).catch((error) => {
1180
+ if (options.onError) options.onError(error, payload, shardId);
1181
+ else console.error("Failed to cache gateway dispatch", error);
1182
+ });
1183
+ };
1184
+ gateway.on("dispatch", listener);
1185
+ return () => gateway.off?.("dispatch", listener);
1186
+ }
1187
+
1188
+ //#endregion
1189
+ //#region src/lib/memory.ts
1190
+ /**
1191
+ * An {@link EntityCache} backed by a `Map`, optionally bounded as a least-recently-used cache.
1192
+ */
1193
+ var MemoryEntityCache = class {
1194
+ /**
1195
+ * The maximum amount of entries, `Infinity` for an unbounded cache.
1196
+ */
1197
+ maxSize;
1198
+ #items = /* @__PURE__ */ new Map();
1199
+ constructor(maxSize = Infinity) {
1200
+ if (maxSize !== Infinity && (!Number.isInteger(maxSize) || maxSize < 0)) throw new RangeError(`maxSize must be a non-negative integer or Infinity, received ${maxSize}`);
1201
+ this.maxSize = maxSize;
1202
+ }
1203
+ get(key) {
1204
+ const value = this.#items.get(key);
1205
+ if (value !== void 0 && this.maxSize !== Infinity) {
1206
+ this.#items.delete(key);
1207
+ this.#items.set(key, value);
1208
+ }
1209
+ return value;
1210
+ }
1211
+ set(key, value) {
1212
+ if (this.maxSize === 0) return;
1213
+ this.#items.delete(key);
1214
+ this.#items.set(key, value);
1215
+ if (this.#items.size > this.maxSize) this.#items.delete(this.#items.keys().next().value);
1216
+ }
1217
+ has(key) {
1218
+ return this.#items.has(key);
1219
+ }
1220
+ delete(key) {
1221
+ return this.#items.delete(key);
1222
+ }
1223
+ clear() {
1224
+ this.#items.clear();
1225
+ }
1226
+ getSize() {
1227
+ return this.#items.size;
1228
+ }
1229
+ keys() {
1230
+ return [...this.#items.keys()];
1231
+ }
1232
+ values() {
1233
+ return [...this.#items.values()];
1234
+ }
1235
+ entries() {
1236
+ return [...this.#items.entries()];
1237
+ }
1238
+ };
1239
+ /**
1240
+ * Creates a {@link Cache} that keeps everything in the process' memory.
1241
+ *
1242
+ * @example
1243
+ * ```typescript
1244
+ * import { createInMemoryCache } from '@wolfstar/plugin-cache';
1245
+ *
1246
+ * // Keep at most 1000 messages around, everything else is unbounded.
1247
+ * const cache = createInMemoryCache({ maxSize: { messages: 1_000 } });
1248
+ * ```
1249
+ *
1250
+ * @param options The options for the cache.
1251
+ */
1252
+ function createInMemoryCache(options = {}) {
1253
+ const { maxSize } = options;
1254
+ const resolve = (name) => typeof maxSize === "number" ? maxSize : maxSize?.[name] ?? Infinity;
1255
+ return Object.freeze(Object.fromEntries(CacheEntityNames.map((name) => [name, new MemoryEntityCache(resolve(name))])));
1256
+ }
1257
+
1258
+ //#endregion
1259
+ //#region src/lib/redis.ts
1260
+ const gzipAsync = promisify(gzip);
1261
+ const gunzipAsync = promisify(gunzip);
1262
+ const brotliCompressAsync = promisify(brotliCompress);
1263
+ const brotliDecompressAsync = promisify(brotliDecompress);
1264
+ /**
1265
+ * Thrown when a value stored in Redis cannot be read back: invalid JSON, or compressed bytes that fail to decompress.
1266
+ *
1267
+ * @remarks
1268
+ * A missing value is not an error, `get` resolves to `undefined` for it. Redis connection errors are not wrapped
1269
+ * either, they propagate as the client throws them.
1270
+ */
1271
+ var CacheValueError = class extends Error {
1272
+ /**
1273
+ * The Redis key holding the unreadable value.
1274
+ */
1275
+ key;
1276
+ constructor(key, cause) {
1277
+ super(`Cannot read the cached value at "${key}"`, { cause });
1278
+ this.name = "CacheValueError";
1279
+ this.key = key;
1280
+ }
1281
+ };
1282
+ const CompressionMarkers = {
1283
+ gzip: "gz:",
1284
+ brotli: "br:"
1285
+ };
1286
+ /**
1287
+ * An {@link EntityCache} backed by Redis.
1288
+ *
1289
+ * @remarks
1290
+ * Every entry is stored as its own string key (`<prefix>:<key>`), and a sorted set (`<prefix>:@index`) tracks the
1291
+ * stored keys with their expiration time as score, which is what `keys`, `entries`, `getSize`, and `clear` read from.
1292
+ */
1293
+ var RedisEntityCache = class {
1294
+ prefix;
1295
+ ttl;
1296
+ compression;
1297
+ compressionThreshold;
1298
+ #redis;
1299
+ constructor(redis, options) {
1300
+ if (options.ttl !== void 0 && !(options.ttl > 0)) throw new RangeError(`ttl must be a positive amount of seconds, received ${options.ttl}`);
1301
+ this.#redis = redis;
1302
+ this.prefix = options.prefix;
1303
+ this.ttl = options.ttl;
1304
+ this.compression = options.compression ?? "none";
1305
+ this.compressionThreshold = options.compressionThreshold ?? 1024;
1306
+ }
1307
+ async get(key) {
1308
+ const valueKey = this.valueKey(key);
1309
+ const value = await this.#redis.get(valueKey);
1310
+ return value === null ? void 0 : this.deserialize(valueKey, value);
1311
+ }
1312
+ async set(key, value) {
1313
+ const serialized = await this.serialize(value);
1314
+ const transaction = this.#redis.multi();
1315
+ if (this.ttl === void 0) transaction.set(this.valueKey(key), serialized).zadd(this.indexKey, "+inf", key);
1316
+ else {
1317
+ const now = Date.now();
1318
+ const milliseconds = Math.round(this.ttl * 1e3);
1319
+ transaction.set(this.valueKey(key), serialized, "PX", milliseconds).zadd(this.indexKey, now + milliseconds, key).zremrangebyscore(this.indexKey, "-inf", now);
1320
+ }
1321
+ await execute(transaction);
1322
+ }
1323
+ async has(key) {
1324
+ return await this.#redis.exists(this.valueKey(key)) > 0;
1325
+ }
1326
+ async delete(key) {
1327
+ const [deleted] = await execute(this.#redis.multi().del(this.valueKey(key)).zrem(this.indexKey, key));
1328
+ return deleted > 0;
1329
+ }
1330
+ async clear() {
1331
+ const keys = await this.#redis.zrange(this.indexKey, "0", "-1");
1332
+ if (keys.length > 0) await this.#redis.del(...keys.map((key) => this.valueKey(key)));
1333
+ await this.#redis.del(this.indexKey);
1334
+ }
1335
+ async getSize() {
1336
+ await this.prune();
1337
+ return this.#redis.zcard(this.indexKey);
1338
+ }
1339
+ async keys() {
1340
+ await this.prune();
1341
+ return this.#redis.zrange(this.indexKey, "0", "-1");
1342
+ }
1343
+ async values() {
1344
+ return (await this.entries()).map(([, value]) => value);
1345
+ }
1346
+ async entries() {
1347
+ const keys = await this.keys();
1348
+ if (keys.length === 0) return [];
1349
+ const values = await this.#redis.mget(...keys.map((key) => this.valueKey(key)));
1350
+ const entries = [];
1351
+ for (const [index, value] of values.entries()) if (value !== null) {
1352
+ const key = keys[index];
1353
+ entries.push([key, await this.deserialize(this.valueKey(key), value)]);
1354
+ }
1355
+ return entries;
1356
+ }
1357
+ /**
1358
+ * Gets the Redis key a value is stored at.
1359
+ * @param key The entity cache key.
1360
+ */
1361
+ valueKey(key) {
1362
+ return `${this.prefix}:${key}`;
1363
+ }
1364
+ /**
1365
+ * The Redis key of the sorted set indexing the stored keys.
1366
+ */
1367
+ get indexKey() {
1368
+ return `${this.prefix}:@index`;
1369
+ }
1370
+ async prune() {
1371
+ if (this.ttl !== void 0) await this.#redis.zremrangebyscore(this.indexKey, "-inf", Date.now());
1372
+ }
1373
+ async serialize(value) {
1374
+ const json = JSON.stringify(value);
1375
+ if (this.compression === "none" || Buffer.byteLength(json) < this.compressionThreshold) return json;
1376
+ const compressed = this.compression === "gzip" ? await gzipAsync(json) : await brotliCompressAsync(json);
1377
+ return `${CompressionMarkers[this.compression]}${compressed.toString("base64")}`;
1378
+ }
1379
+ async deserialize(valueKey, value) {
1380
+ try {
1381
+ if (value.startsWith(CompressionMarkers.gzip)) return JSON.parse((await gunzipAsync(decode(value))).toString("utf8"));
1382
+ if (value.startsWith(CompressionMarkers.brotli)) return JSON.parse((await brotliDecompressAsync(decode(value))).toString("utf8"));
1383
+ return JSON.parse(value);
1384
+ } catch (error) {
1385
+ throw new CacheValueError(valueKey, error);
1386
+ }
1387
+ }
1388
+ };
1389
+ /**
1390
+ * Runs a transaction, throwing the first command error, and resolves to the command results.
1391
+ */
1392
+ async function execute(transaction) {
1393
+ const results = await transaction.exec();
1394
+ if (results === null) throw new Error("The Redis transaction was aborted");
1395
+ return results.map(([error, result]) => {
1396
+ if (error) throw error;
1397
+ return result;
1398
+ });
1399
+ }
1400
+ function decode(value) {
1401
+ return Buffer.from(value.slice(3), "base64");
1402
+ }
1403
+ /**
1404
+ * The default prefix of every Redis key owned by a cache created with {@link createRedisCache}.
1405
+ */
1406
+ const DefaultRedisCachePrefix = "wolfstar:cache";
1407
+ /**
1408
+ * Creates a {@link Cache} stored in Redis, optionally compressing its values.
1409
+ *
1410
+ * @example
1411
+ * ```typescript
1412
+ * import { createRedisCache } from '@wolfstar/plugin-cache';
1413
+ * import { Redis } from 'ioredis';
1414
+ *
1415
+ * const cache = createRedisCache({
1416
+ * redis: new Redis(process.env.REDIS_URL!),
1417
+ * compression: 'gzip',
1418
+ * ttl: { guilds: 60 * 60, users: 30 * 60 },
1419
+ * });
1420
+ * ```
1421
+ *
1422
+ * @param options The options for the cache.
1423
+ */
1424
+ function createRedisCache(options) {
1425
+ const { redis, prefix = DefaultRedisCachePrefix, compression, compressionThreshold, ttl } = options;
1426
+ return Object.freeze(Object.fromEntries(CacheEntityNames.map((name) => [name, new RedisEntityCache(redis, {
1427
+ prefix: `${prefix}:${name}`,
1428
+ ttl: ttl?.[name],
1429
+ compression,
1430
+ compressionThreshold
1431
+ })])));
1432
+ }
1433
+
1434
+ //#endregion
1435
+ export { CacheEntityNames, CacheValueError, DefaultRedisCachePrefix, MemoryEntityCache, RedisEntityCache, applicationCommandPermissionsKey, applyCacheOperations, applyGatewayDispatch, attachCacheToGateway, autoModerationRuleKey, banKey, createCacheOperations, createInMemoryCache, createRedisCache, emojiKey, guildScopedKey, integrationKey, inviteKey, memberKey, mergeValues, messageKey, presenceKey, roleKey, scheduledEventKey, soundboardSoundKey, stageInstanceKey, stickerKey, threadMemberKey, voiceStateKey };
1436
+ //# sourceMappingURL=index.js.map