@effect-uai/discord 0.14.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/Discord.d.mts +55 -0
  4. package/dist/Discord.d.mts.map +1 -0
  5. package/dist/Discord.mjs +196 -0
  6. package/dist/Discord.mjs.map +1 -0
  7. package/dist/events-Du1u-kVV.d.mts +172 -0
  8. package/dist/events-Du1u-kVV.d.mts.map +1 -0
  9. package/dist/gateway-C_Ae1HYB.d.mts +57 -0
  10. package/dist/gateway-C_Ae1HYB.d.mts.map +1 -0
  11. package/dist/index.d.mts +2 -0
  12. package/dist/index.mjs +2 -0
  13. package/dist/internal/events.d.mts +2 -0
  14. package/dist/internal/events.mjs +109 -0
  15. package/dist/internal/events.mjs.map +1 -0
  16. package/dist/internal/events.test.d.mts +1 -0
  17. package/dist/internal/events.test.mjs +130 -0
  18. package/dist/internal/events.test.mjs.map +1 -0
  19. package/dist/internal/gateway.d.mts +2 -0
  20. package/dist/internal/gateway.mjs +216 -0
  21. package/dist/internal/gateway.mjs.map +1 -0
  22. package/dist/internal/gateway.test.d.mts +1 -0
  23. package/dist/internal/gateway.test.mjs +40 -0
  24. package/dist/internal/gateway.test.mjs.map +1 -0
  25. package/dist/internal/rest.d.mts +2 -0
  26. package/dist/internal/rest.mjs +89 -0
  27. package/dist/internal/rest.mjs.map +1 -0
  28. package/dist/magic-string.es-CUAnjKYC.mjs +1014 -0
  29. package/dist/magic-string.es-CUAnjKYC.mjs.map +1 -0
  30. package/dist/rest-CZSW-VJK.d.mts +44 -0
  31. package/dist/rest-CZSW-VJK.d.mts.map +1 -0
  32. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  33. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs +13658 -0
  34. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
  35. package/package.json +59 -0
  36. package/src/Discord.ts +393 -0
  37. package/src/index.ts +1 -0
  38. package/src/internal/events.test.ts +89 -0
  39. package/src/internal/events.ts +197 -0
  40. package/src/internal/gateway.test.ts +27 -0
  41. package/src/internal/gateway.ts +368 -0
  42. package/src/internal/rest.ts +164 -0
@@ -0,0 +1,109 @@
1
+ import { Array, Match, Option, Schema } from "effect";
2
+ import { ChannelId, InboundEvent, MessageId, UserId } from "@effect-uai/core/Messenger";
3
+ //#region src/internal/events.ts
4
+ const User = Schema.Struct({
5
+ id: Schema.String,
6
+ username: Schema.optional(Schema.String),
7
+ bot: Schema.optional(Schema.Boolean)
8
+ });
9
+ /** Unicode reactions carry a `name` and a null `id`; custom ones carry both. */
10
+ const Emoji = Schema.Struct({
11
+ id: Schema.optional(Schema.NullOr(Schema.String)),
12
+ name: Schema.optional(Schema.NullOr(Schema.String))
13
+ });
14
+ const ReplyTarget = Schema.Struct({
15
+ id: Schema.String,
16
+ author: Schema.optional(User)
17
+ });
18
+ const MessageCreate = Schema.Struct({
19
+ id: Schema.String,
20
+ channel_id: Schema.String,
21
+ guild_id: Schema.optional(Schema.String),
22
+ author: User,
23
+ content: Schema.optional(Schema.String),
24
+ mentions: Schema.optional(Schema.Array(User)),
25
+ referenced_message: Schema.optional(Schema.NullOr(ReplyTarget))
26
+ });
27
+ const ReactionAdd = Schema.Struct({
28
+ user_id: Schema.String,
29
+ channel_id: Schema.String,
30
+ message_id: Schema.String,
31
+ emoji: Emoji
32
+ });
33
+ const InteractionData = Schema.Struct({
34
+ custom_id: Schema.optional(Schema.String),
35
+ values: Schema.optional(Schema.Array(Schema.String))
36
+ });
37
+ const InteractionCreate = Schema.Struct({
38
+ id: Schema.String,
39
+ token: Schema.String,
40
+ /** 1 ping, 2 slash command, 3 message component, 4 autocomplete, 5 modal. */
41
+ type: Schema.Number,
42
+ channel_id: Schema.optional(Schema.String),
43
+ user: Schema.optional(User),
44
+ member: Schema.optional(Schema.Struct({ user: Schema.optional(User) })),
45
+ data: Schema.optional(InteractionData)
46
+ });
47
+ /** The component interaction type; every other one is ignored in v1. */
48
+ const MESSAGE_COMPONENT = 3;
49
+ /**
50
+ * The dispatches this adapter reads, discriminated by the gateway's `t`.
51
+ * Anything else fails to decode and is dropped by the session.
52
+ */
53
+ const Dispatch = Schema.Union([
54
+ Schema.Struct({
55
+ t: Schema.Literal("MESSAGE_CREATE"),
56
+ d: MessageCreate
57
+ }),
58
+ Schema.Struct({
59
+ t: Schema.Literal("MESSAGE_REACTION_ADD"),
60
+ d: ReactionAdd
61
+ }),
62
+ Schema.Struct({
63
+ t: Schema.Literal("INTERACTION_CREATE"),
64
+ d: InteractionCreate
65
+ })
66
+ ]);
67
+ const conversation = (channelId) => ({ channel: ChannelId(channelId) });
68
+ const mentionsBot = (bot, message) => (message.mentions ?? []).some((user) => user.id === bot.id);
69
+ const isAddressed = (bot, message) => message.guild_id === void 0 || mentionsBot(bot, message) || message.referenced_message?.author?.id === bot.id;
70
+ /** Both mention spellings of the bot removed; other users' mentions stay. */
71
+ const withoutMention = (bot, text) => text.replaceAll(`<@${bot.id}>`, "").replaceAll(`<@!${bot.id}>`, "").trim();
72
+ const fromMessage = (bot, message, raw) => Option.liftPredicate(message, (m) => m.author.bot !== true).pipe(Option.map((m) => InboundEvent.Message({
73
+ conversation: conversation(m.channel_id),
74
+ id: MessageId(m.id),
75
+ author: UserId(m.author.id),
76
+ text: withoutMention(bot, m.content ?? ""),
77
+ addressed: isAddressed(bot, m),
78
+ ...m.referenced_message != null && { replyTo: MessageId(m.referenced_message.id) },
79
+ raw
80
+ })));
81
+ /** Unicode reactions are their own name; custom ones are `name:id`, as REST wants. */
82
+ const emojiName = (emoji) => Option.map(Option.fromNullishOr(emoji.name), (name) => emoji.id == null ? name : `${name}:${emoji.id}`);
83
+ const fromReaction = (bot, reaction, raw) => Option.liftPredicate(reaction, (r) => r.user_id !== bot.id).pipe(Option.flatMap((r) => Option.map(emojiName(r.emoji), (emoji) => InboundEvent.Reaction({
84
+ conversation: conversation(r.channel_id),
85
+ message: MessageId(r.message_id),
86
+ emoji,
87
+ author: UserId(r.user_id),
88
+ raw
89
+ }))));
90
+ const interactionAuthor = (interaction) => Option.fromNullishOr(interaction.user?.id ?? interaction.member?.user?.id);
91
+ /** A select delivers several values; the shared shape carries them comma-joined. */
92
+ const fromInteraction = (interaction, raw) => Option.liftPredicate(interaction, (i) => i.type === 3).pipe(Option.flatMap((i) => Option.all([Option.fromNullishOr(i.channel_id), interactionAuthor(i)]).pipe(Option.map(([channelId, author]) => InboundEvent.Action({
93
+ conversation: conversation(channelId),
94
+ actionId: i.data?.custom_id ?? "",
95
+ ...i.data?.values !== void 0 && { value: i.data.values.join(",") },
96
+ author: UserId(author),
97
+ raw
98
+ })))));
99
+ /**
100
+ * Everything one dispatch says to the agent. `raw` is the payload as it came
101
+ * off the wire, since decoding keeps only the fields above. Discord has no
102
+ * text-command convention, so nothing here becomes a `Command`: slash
103
+ * commands are a follow-up and need an interaction response of their own.
104
+ */
105
+ const toEvents = (bot) => (dispatch, raw) => Array.fromOption(Match.value(dispatch).pipe(Match.when({ t: "MESSAGE_CREATE" }, ({ d }) => fromMessage(bot, d, raw)), Match.when({ t: "MESSAGE_REACTION_ADD" }, ({ d }) => fromReaction(bot, d, raw)), Match.when({ t: "INTERACTION_CREATE" }, ({ d }) => fromInteraction(d, raw)), Match.exhaustive));
106
+ //#endregion
107
+ export { Dispatch, Emoji, InteractionCreate, InteractionData, MESSAGE_COMPONENT, MessageCreate, ReactionAdd, ReplyTarget, User, toEvents };
108
+
109
+ //# sourceMappingURL=events.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.mjs","names":["Arr"],"sources":["../../src/internal/events.ts"],"sourcesContent":["import { Array as Arr, Match, Option, Schema } from \"effect\"\nimport {\n ChannelId,\n type ConversationRef,\n InboundEvent,\n MessageId,\n UserId,\n} from \"@effect-uai/core/Messenger\"\n\n// ---------------------------------------------------------------------------\n// Wire shapes: the fields this adapter reads. The rest survives on `raw`.\n// ---------------------------------------------------------------------------\n\nexport const User = Schema.Struct({\n id: Schema.String,\n username: Schema.optional(Schema.String),\n bot: Schema.optional(Schema.Boolean),\n})\nexport type User = typeof User.Type\n\n/** Unicode reactions carry a `name` and a null `id`; custom ones carry both. */\nexport const Emoji = Schema.Struct({\n id: Schema.optional(Schema.NullOr(Schema.String)),\n name: Schema.optional(Schema.NullOr(Schema.String)),\n})\nexport type Emoji = typeof Emoji.Type\n\nexport const ReplyTarget = Schema.Struct({\n id: Schema.String,\n author: Schema.optional(User),\n})\nexport type ReplyTarget = typeof ReplyTarget.Type\n\nexport const MessageCreate = Schema.Struct({\n id: Schema.String,\n channel_id: Schema.String,\n // Absent in a DM, which is how a DM is recognised.\n guild_id: Schema.optional(Schema.String),\n author: User,\n content: Schema.optional(Schema.String),\n mentions: Schema.optional(Schema.Array(User)),\n referenced_message: Schema.optional(Schema.NullOr(ReplyTarget)),\n})\nexport type MessageCreate = typeof MessageCreate.Type\n\nexport const ReactionAdd = Schema.Struct({\n user_id: Schema.String,\n channel_id: Schema.String,\n message_id: Schema.String,\n emoji: Emoji,\n})\nexport type ReactionAdd = typeof ReactionAdd.Type\n\nexport const InteractionData = Schema.Struct({\n custom_id: Schema.optional(Schema.String),\n values: Schema.optional(Schema.Array(Schema.String)),\n})\nexport type InteractionData = typeof InteractionData.Type\n\nexport const InteractionCreate = Schema.Struct({\n id: Schema.String,\n token: Schema.String,\n /** 1 ping, 2 slash command, 3 message component, 4 autocomplete, 5 modal. */\n type: Schema.Number,\n channel_id: Schema.optional(Schema.String),\n user: Schema.optional(User),\n member: Schema.optional(Schema.Struct({ user: Schema.optional(User) })),\n data: Schema.optional(InteractionData),\n})\nexport type InteractionCreate = typeof InteractionCreate.Type\n\n/** The component interaction type; every other one is ignored in v1. */\nexport const MESSAGE_COMPONENT = 3\n\n/**\n * The dispatches this adapter reads, discriminated by the gateway's `t`.\n * Anything else fails to decode and is dropped by the session.\n */\nexport const Dispatch = Schema.Union([\n Schema.Struct({ t: Schema.Literal(\"MESSAGE_CREATE\"), d: MessageCreate }),\n Schema.Struct({ t: Schema.Literal(\"MESSAGE_REACTION_ADD\"), d: ReactionAdd }),\n Schema.Struct({ t: Schema.Literal(\"INTERACTION_CREATE\"), d: InteractionCreate }),\n])\nexport type Dispatch = typeof Dispatch.Type\n\nexport type BotIdentity = {\n readonly id: string\n readonly username: string\n}\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\n// A Discord thread is a channel of its own, so `thread` is never set here and\n// `conversationKey` still separates a thread from its parent.\nconst conversation = (channelId: string): ConversationRef => ({ channel: ChannelId(channelId) })\n\nconst mentionsBot = (bot: BotIdentity, message: MessageCreate): boolean =>\n (message.mentions ?? []).some((user) => user.id === bot.id)\n\n// `@everyone` and role mentions are not in `mentions`, so they never address.\nconst isAddressed = (bot: BotIdentity, message: MessageCreate): boolean =>\n message.guild_id === undefined ||\n mentionsBot(bot, message) ||\n message.referenced_message?.author?.id === bot.id\n\n/** Both mention spellings of the bot removed; other users' mentions stay. */\nconst withoutMention = (bot: BotIdentity, text: string): string =>\n text.replaceAll(`<@${bot.id}>`, \"\").replaceAll(`<@!${bot.id}>`, \"\").trim()\n\nconst fromMessage = (\n bot: BotIdentity,\n message: MessageCreate,\n raw: unknown,\n): Option.Option<InboundEvent> =>\n // Bot authors are dropped outright, so two bots cannot answer each other.\n Option.liftPredicate(message, (m) => m.author.bot !== true).pipe(\n Option.map((m) =>\n InboundEvent.Message({\n conversation: conversation(m.channel_id),\n id: MessageId(m.id),\n author: UserId(m.author.id),\n text: withoutMention(bot, m.content ?? \"\"),\n addressed: isAddressed(bot, m),\n ...(m.referenced_message != null && { replyTo: MessageId(m.referenced_message.id) }),\n raw,\n }),\n ),\n )\n\n/** Unicode reactions are their own name; custom ones are `name:id`, as REST wants. */\nconst emojiName = (emoji: Emoji): Option.Option<string> =>\n Option.map(Option.fromNullishOr(emoji.name), (name) =>\n emoji.id == null ? name : `${name}:${emoji.id}`,\n )\n\nconst fromReaction = (\n bot: BotIdentity,\n reaction: ReactionAdd,\n raw: unknown,\n): Option.Option<InboundEvent> =>\n Option.liftPredicate(reaction, (r) => r.user_id !== bot.id).pipe(\n Option.flatMap((r) =>\n Option.map(emojiName(r.emoji), (emoji) =>\n InboundEvent.Reaction({\n conversation: conversation(r.channel_id),\n message: MessageId(r.message_id),\n emoji,\n author: UserId(r.user_id),\n raw,\n }),\n ),\n ),\n )\n\nconst interactionAuthor = (interaction: InteractionCreate): Option.Option<string> =>\n Option.fromNullishOr(interaction.user?.id ?? interaction.member?.user?.id)\n\n/** A select delivers several values; the shared shape carries them comma-joined. */\nconst fromInteraction = (\n interaction: InteractionCreate,\n raw: unknown,\n): Option.Option<InboundEvent> =>\n Option.liftPredicate(interaction, (i) => i.type === MESSAGE_COMPONENT).pipe(\n Option.flatMap((i) =>\n Option.all([Option.fromNullishOr(i.channel_id), interactionAuthor(i)]).pipe(\n Option.map(([channelId, author]) =>\n InboundEvent.Action({\n conversation: conversation(channelId),\n actionId: i.data?.custom_id ?? \"\",\n ...(i.data?.values !== undefined && { value: i.data.values.join(\",\") }),\n author: UserId(author),\n raw,\n }),\n ),\n ),\n ),\n )\n\n/**\n * Everything one dispatch says to the agent. `raw` is the payload as it came\n * off the wire, since decoding keeps only the fields above. Discord has no\n * text-command convention, so nothing here becomes a `Command`: slash\n * commands are a follow-up and need an interaction response of their own.\n */\nexport const toEvents =\n (bot: BotIdentity) =>\n (dispatch: Dispatch, raw: unknown): ReadonlyArray<InboundEvent> =>\n Arr.fromOption(\n Match.value(dispatch).pipe(\n Match.when({ t: \"MESSAGE_CREATE\" }, ({ d }) => fromMessage(bot, d, raw)),\n Match.when({ t: \"MESSAGE_REACTION_ADD\" }, ({ d }) => fromReaction(bot, d, raw)),\n Match.when({ t: \"INTERACTION_CREATE\" }, ({ d }) => fromInteraction(d, raw)),\n Match.exhaustive,\n ),\n )\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,130 @@
1
+ import { Dispatch, toEvents } from "./events.mjs";
2
+ import { n as describe, r as it, t as globalExpect } from "../test.DNmyFkvJ-N3QqMRwZ.mjs";
3
+ import { Schema } from "effect";
4
+ //#region src/internal/events.test.ts
5
+ const events = toEvents({
6
+ id: "42",
7
+ username: "HelperBot"
8
+ });
9
+ const alice = {
10
+ id: "7",
11
+ username: "alice"
12
+ };
13
+ /** One dispatch through the real decoder, as the gateway hands it over. */
14
+ const dispatch = (t, d) => events(Schema.decodeUnknownSync(Dispatch)({
15
+ t,
16
+ d
17
+ }), d)[0];
18
+ const message = (fields) => dispatch("MESSAGE_CREATE", {
19
+ id: "10",
20
+ channel_id: "555",
21
+ guild_id: "100",
22
+ author: alice,
23
+ ...fields
24
+ });
25
+ describe("addressed", () => {
26
+ it("is a DM, a mention, or a reply to the bot, and nothing else", () => {
27
+ globalExpect(message({
28
+ guild_id: void 0,
29
+ content: "hi"
30
+ })).toMatchObject({ addressed: true });
31
+ globalExpect(message({
32
+ content: "<@42> ping",
33
+ mentions: [{ id: "42" }]
34
+ })).toMatchObject({ addressed: true });
35
+ globalExpect(message({
36
+ content: "and?",
37
+ referenced_message: {
38
+ id: "9",
39
+ author: { id: "42" }
40
+ }
41
+ })).toMatchObject({
42
+ addressed: true,
43
+ replyTo: "9"
44
+ });
45
+ globalExpect(message({
46
+ content: "and?",
47
+ referenced_message: {
48
+ id: "9",
49
+ author: alice
50
+ }
51
+ })).toMatchObject({
52
+ addressed: false,
53
+ replyTo: "9"
54
+ });
55
+ globalExpect(message({
56
+ content: "@everyone lunch?",
57
+ mention_everyone: true
58
+ })).toMatchObject({ addressed: false });
59
+ });
60
+ it("strips both spellings of the bot's own mention and leaves other users'", () => {
61
+ globalExpect(message({
62
+ content: "<@!42> ask <@7> too",
63
+ mentions: [{ id: "42" }, alice]
64
+ })).toMatchObject({ text: "ask <@7> too" });
65
+ });
66
+ it("drops messages another bot wrote, so two bots cannot answer each other", () => {
67
+ globalExpect(message({
68
+ content: "hello",
69
+ author: {
70
+ id: "99",
71
+ bot: true
72
+ }
73
+ })).toBeUndefined();
74
+ });
75
+ });
76
+ describe("reactions", () => {
77
+ const add = (emoji, user_id = "7") => dispatch("MESSAGE_REACTION_ADD", {
78
+ user_id,
79
+ channel_id: "555",
80
+ message_id: "10",
81
+ emoji
82
+ });
83
+ it("names unicode by itself and custom emoji as `name:id`", () => {
84
+ globalExpect(add({
85
+ id: null,
86
+ name: "👀"
87
+ })).toMatchObject({
88
+ _tag: "Reaction",
89
+ emoji: "👀"
90
+ });
91
+ globalExpect(add({
92
+ id: "8",
93
+ name: "blobcat"
94
+ })).toMatchObject({ emoji: "blobcat:8" });
95
+ });
96
+ it("drops the bot's own reactions", () => {
97
+ globalExpect(add({
98
+ id: null,
99
+ name: "👀"
100
+ }, "42")).toBeUndefined();
101
+ });
102
+ });
103
+ describe("interactions", () => {
104
+ const interaction = (type, data) => dispatch("INTERACTION_CREATE", {
105
+ id: "1",
106
+ token: "tok",
107
+ type,
108
+ channel_id: "555",
109
+ member: { user: alice },
110
+ data
111
+ });
112
+ it("maps a component press to an Action, joining a select's values", () => {
113
+ globalExpect(interaction(3, {
114
+ custom_id: "approve:12",
115
+ values: ["a", "b"]
116
+ })).toMatchObject({
117
+ _tag: "Action",
118
+ actionId: "approve:12",
119
+ value: "a,b",
120
+ author: "7"
121
+ });
122
+ });
123
+ it("ignores interaction types that need a response of their own", () => {
124
+ globalExpect(interaction(2, { name: "search" })).toBeUndefined();
125
+ });
126
+ });
127
+ //#endregion
128
+ export {};
129
+
130
+ //# sourceMappingURL=events.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.test.mjs","names":[],"sources":["../../src/internal/events.test.ts"],"sourcesContent":["import { Schema } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport { type BotIdentity, Dispatch, toEvents } from \"./events.js\"\n\nconst bot: BotIdentity = { id: \"42\", username: \"HelperBot\" }\nconst events = toEvents(bot)\n\nconst alice = { id: \"7\", username: \"alice\" }\n\n/** One dispatch through the real decoder, as the gateway hands it over. */\nconst dispatch = (t: string, d: Record<string, unknown>) =>\n events(Schema.decodeUnknownSync(Dispatch)({ t, d }), d)[0]\n\nconst message = (fields: Record<string, unknown>) =>\n dispatch(\"MESSAGE_CREATE\", {\n id: \"10\",\n channel_id: \"555\",\n guild_id: \"100\",\n author: alice,\n ...fields,\n })\n\ndescribe(\"addressed\", () => {\n it(\"is a DM, a mention, or a reply to the bot, and nothing else\", () => {\n expect(message({ guild_id: undefined, content: \"hi\" })).toMatchObject({ addressed: true })\n expect(message({ content: \"<@42> ping\", mentions: [{ id: \"42\" }] })).toMatchObject({\n addressed: true,\n })\n expect(\n message({ content: \"and?\", referenced_message: { id: \"9\", author: { id: \"42\" } } }),\n ).toMatchObject({ addressed: true, replyTo: \"9\" })\n expect(\n message({ content: \"and?\", referenced_message: { id: \"9\", author: alice } }),\n ).toMatchObject({ addressed: false, replyTo: \"9\" })\n // `@everyone` and role pings never land in `mentions`.\n expect(message({ content: \"@everyone lunch?\", mention_everyone: true })).toMatchObject({\n addressed: false,\n })\n })\n\n it(\"strips both spellings of the bot's own mention and leaves other users'\", () => {\n expect(\n message({ content: \"<@!42> ask <@7> too\", mentions: [{ id: \"42\" }, alice] }),\n ).toMatchObject({ text: \"ask <@7> too\" })\n })\n\n it(\"drops messages another bot wrote, so two bots cannot answer each other\", () => {\n expect(message({ content: \"hello\", author: { id: \"99\", bot: true } })).toBeUndefined()\n })\n})\n\ndescribe(\"reactions\", () => {\n const add = (emoji: Record<string, unknown>, user_id = \"7\") =>\n dispatch(\"MESSAGE_REACTION_ADD\", { user_id, channel_id: \"555\", message_id: \"10\", emoji })\n\n it(\"names unicode by itself and custom emoji as `name:id`\", () => {\n expect(add({ id: null, name: \"👀\" })).toMatchObject({ _tag: \"Reaction\", emoji: \"👀\" })\n expect(add({ id: \"8\", name: \"blobcat\" })).toMatchObject({ emoji: \"blobcat:8\" })\n })\n\n it(\"drops the bot's own reactions\", () => {\n expect(add({ id: null, name: \"👀\" }, \"42\")).toBeUndefined()\n })\n})\n\ndescribe(\"interactions\", () => {\n const interaction = (type: number, data: Record<string, unknown>) =>\n dispatch(\"INTERACTION_CREATE\", {\n id: \"1\",\n token: \"tok\",\n type,\n channel_id: \"555\",\n member: { user: alice },\n data,\n })\n\n it(\"maps a component press to an Action, joining a select's values\", () => {\n expect(interaction(3, { custom_id: \"approve:12\", values: [\"a\", \"b\"] })).toMatchObject({\n _tag: \"Action\",\n actionId: \"approve:12\",\n value: \"a,b\",\n author: \"7\",\n })\n })\n\n it(\"ignores interaction types that need a response of their own\", () => {\n expect(interaction(2, { name: \"search\" })).toBeUndefined()\n })\n})\n"],"mappings":";;;;AAKA,MAAM,SAAS,SAAS;CADG,IAAI;CAAM,UAAU;AACrB,CAAC;AAE3B,MAAM,QAAQ;CAAE,IAAI;CAAK,UAAU;AAAQ;;AAG3C,MAAM,YAAY,GAAW,MAC3B,OAAO,OAAO,kBAAkB,QAAQ,CAAC,CAAC;CAAE;CAAG;AAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,WAAW,WACf,SAAS,kBAAkB;CACzB,IAAI;CACJ,YAAY;CACZ,UAAU;CACV,QAAQ;CACR,GAAG;AACL,CAAC;AAEH,SAAS,mBAAmB;CAC1B,GAAG,qEAAqE;EACtE,aAAO,QAAQ;GAAE,UAAU,KAAA;GAAW,SAAS;EAAK,CAAC,CAAC,CAAC,CAAC,cAAc,EAAE,WAAW,KAAK,CAAC;EACzF,aAAO,QAAQ;GAAE,SAAS;GAAc,UAAU,CAAC,EAAE,IAAI,KAAK,CAAC;EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,EACjF,WAAW,KACb,CAAC;EACD,aACE,QAAQ;GAAE,SAAS;GAAQ,oBAAoB;IAAE,IAAI;IAAK,QAAQ,EAAE,IAAI,KAAK;GAAE;EAAE,CAAC,CACpF,CAAC,CAAC,cAAc;GAAE,WAAW;GAAM,SAAS;EAAI,CAAC;EACjD,aACE,QAAQ;GAAE,SAAS;GAAQ,oBAAoB;IAAE,IAAI;IAAK,QAAQ;GAAM;EAAE,CAAC,CAC7E,CAAC,CAAC,cAAc;GAAE,WAAW;GAAO,SAAS;EAAI,CAAC;EAElD,aAAO,QAAQ;GAAE,SAAS;GAAoB,kBAAkB;EAAK,CAAC,CAAC,CAAC,CAAC,cAAc,EACrF,WAAW,MACb,CAAC;CACH,CAAC;CAED,GAAG,gFAAgF;EACjF,aACE,QAAQ;GAAE,SAAS;GAAuB,UAAU,CAAC,EAAE,IAAI,KAAK,GAAG,KAAK;EAAE,CAAC,CAC7E,CAAC,CAAC,cAAc,EAAE,MAAM,eAAe,CAAC;CAC1C,CAAC;CAED,GAAG,gFAAgF;EACjF,aAAO,QAAQ;GAAE,SAAS;GAAS,QAAQ;IAAE,IAAI;IAAM,KAAK;GAAK;EAAE,CAAC,CAAC,CAAC,CAAC,cAAc;CACvF,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB;CAC1B,MAAM,OAAO,OAAgC,UAAU,QACrD,SAAS,wBAAwB;EAAE;EAAS,YAAY;EAAO,YAAY;EAAM;CAAM,CAAC;CAE1F,GAAG,+DAA+D;EAChE,aAAO,IAAI;GAAE,IAAI;GAAM,MAAM;EAAK,CAAC,CAAC,CAAC,CAAC,cAAc;GAAE,MAAM;GAAY,OAAO;EAAK,CAAC;EACrF,aAAO,IAAI;GAAE,IAAI;GAAK,MAAM;EAAU,CAAC,CAAC,CAAC,CAAC,cAAc,EAAE,OAAO,YAAY,CAAC;CAChF,CAAC;CAED,GAAG,uCAAuC;EACxC,aAAO,IAAI;GAAE,IAAI;GAAM,MAAM;EAAK,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc;CAC5D,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB;CAC7B,MAAM,eAAe,MAAc,SACjC,SAAS,sBAAsB;EAC7B,IAAI;EACJ,OAAO;EACP;EACA,YAAY;EACZ,QAAQ,EAAE,MAAM,MAAM;EACtB;CACF,CAAC;CAEH,GAAG,wEAAwE;EACzE,aAAO,YAAY,GAAG;GAAE,WAAW;GAAc,QAAQ,CAAC,KAAK,GAAG;EAAE,CAAC,CAAC,CAAC,CAAC,cAAc;GACpF,MAAM;GACN,UAAU;GACV,OAAO;GACP,QAAQ;EACV,CAAC;CACH,CAAC;CAED,GAAG,qEAAqE;EACtE,aAAO,YAAY,GAAG,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,cAAc;CAC3D,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { a as Ready, c as closeReason, i as Incoming, l as connect, n as Config, o as Session, r as GatewayInfo, s as classifyClose, t as CloseAction } from "../gateway-C_Ae1HYB.mjs";
2
+ export { CloseAction, Config, GatewayInfo, Incoming, Ready, Session, classifyClose, closeReason, connect };
@@ -0,0 +1,216 @@
1
+ import { Dispatch } from "./events.mjs";
2
+ import { provider } from "./rest.mjs";
3
+ import { Deferred, Duration, Effect, Match, Option, Queue, Redacted, Ref, Schedule, Schema } from "effect";
4
+ import * as MessengerError from "@effect-uai/core/MessengerError";
5
+ import * as Socket from "effect/unstable/socket/Socket";
6
+ //#region src/internal/gateway.ts
7
+ const Payload = Schema.Struct({
8
+ op: Schema.Number,
9
+ d: Schema.optional(Schema.Unknown),
10
+ s: Schema.optional(Schema.NullOr(Schema.Number)),
11
+ t: Schema.optional(Schema.NullOr(Schema.String))
12
+ });
13
+ const decodeFrame = Schema.decodeUnknownEffect(Schema.fromJsonString(Payload));
14
+ const Hello = Schema.Struct({ heartbeat_interval: Schema.Number });
15
+ const Ready = Schema.Struct({
16
+ user: Schema.Struct({
17
+ id: Schema.String,
18
+ username: Schema.String
19
+ }),
20
+ session_id: Schema.String,
21
+ resume_gateway_url: Schema.String
22
+ });
23
+ /** `GET /gateway/bot`, the only REST call the session needs. */
24
+ const GatewayInfo = Schema.Struct({ url: Schema.String });
25
+ const OP = {
26
+ dispatch: 0,
27
+ heartbeat: 1,
28
+ identify: 2,
29
+ resume: 6,
30
+ reconnect: 7,
31
+ invalidSession: 9,
32
+ hello: 10,
33
+ heartbeatAck: 11
34
+ };
35
+ const fatalReasons = {
36
+ 4004: "authentication failed: the bot token was rejected",
37
+ 4010: "invalid shard",
38
+ 4011: "sharding required",
39
+ 4012: "invalid API version",
40
+ 4013: "invalid intents",
41
+ 4014: "disallowed intents: enable the privileged intent in the developer portal"
42
+ };
43
+ const staleSessionCodes = [4007, 4009];
44
+ /**
45
+ * Everything else, transport drops included, resumes: Discord only names the
46
+ * codes that must not be retried, so an unknown code is a reconnect.
47
+ */
48
+ const classifyClose = (code) => Match.value(code).pipe(Match.when((c) => c in fatalReasons, () => "fatal"), Match.when((c) => staleSessionCodes.includes(c), () => "reidentify"), Match.orElse(() => "resume"));
49
+ /** Discord's own words for a fatal code, since its close frames carry no reason. */
50
+ const closeReason = (code, reason) => fatalReasons[code] ?? (reason === void 0 || reason === "" ? `gateway closed with ${code}` : reason);
51
+ const RESUMABLE_CLOSE = 4e3;
52
+ const socketCode = (error) => error.reason._tag === "SocketCloseError" ? error.reason.code : 1006;
53
+ const socketReason = (error) => error.reason._tag === "SocketCloseError" ? error.reason.closeReason ?? error.reason.message : error.reason.message;
54
+ const freshSessionDelay = Effect.sync(() => Duration.millis(1e3 + Math.random() * 4e3));
55
+ const backoff = (attempt) => Duration.millis(Math.min(6e4, 500 * 2 ** attempt));
56
+ const identity = {
57
+ os: "effect-uai",
58
+ browser: "effect-uai",
59
+ device: "effect-uai"
60
+ };
61
+ /**
62
+ * One gateway session: connect, identify, heartbeat, resume, reconnect.
63
+ *
64
+ * Returns once `READY` has arrived, so a bad token or a disallowed intent is
65
+ * a `MessengerConnectFailed` at layer build rather than a stream that dies a
66
+ * moment later. From then on reconnects are silent and only a fatal close
67
+ * ends `dispatches`.
68
+ */
69
+ const connect = (cfg) => Effect.gen(function* () {
70
+ const dispatches = yield* Queue.unbounded();
71
+ const ready = yield* Deferred.make();
72
+ const resume = yield* Ref.make(Option.none());
73
+ const attempts = yield* Ref.make(0);
74
+ const once = Effect.gen(function* () {
75
+ const from = yield* Ref.get(resume);
76
+ const host = Option.match(from, {
77
+ onNone: () => cfg.url,
78
+ onSome: (r) => r.url
79
+ });
80
+ const socket = yield* Socket.makeWebSocket(`${host}/?v=10&encoding=json`, { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 }).pipe(Effect.provide(Socket.layerWebSocketConstructorGlobal));
81
+ const write = yield* socket.writer;
82
+ const send = (payload) => write(JSON.stringify(payload));
83
+ const seq = yield* Ref.make(Option.none());
84
+ const acked = yield* Ref.make(true);
85
+ const requested = yield* Ref.make(Option.none());
86
+ const heartbeat = Effect.gen(function* () {
87
+ const s = yield* Ref.get(seq);
88
+ return yield* send({
89
+ op: OP.heartbeat,
90
+ d: Option.getOrNull(s)
91
+ });
92
+ });
93
+ const beat = Effect.gen(function* () {
94
+ if (!(yield* Ref.getAndSet(acked, false))) {
95
+ yield* write(new Socket.CloseEvent(RESUMABLE_CLOSE, "no heartbeat ack"));
96
+ return false;
97
+ }
98
+ yield* heartbeat;
99
+ return true;
100
+ });
101
+ const beating = (interval) => Effect.sleep(Duration.millis(interval * Math.random())).pipe(Effect.andThen(beat.pipe(Effect.repeat({
102
+ schedule: Schedule.spaced(Duration.millis(interval)),
103
+ while: (alive) => alive
104
+ }))), Effect.ignore, Effect.forkScoped);
105
+ const hello = (d) => Effect.gen(function* () {
106
+ const { heartbeat_interval } = yield* Schema.decodeUnknownEffect(Hello)(d);
107
+ yield* beating(heartbeat_interval);
108
+ const from = yield* Ref.get(resume);
109
+ const s = yield* Ref.get(seq);
110
+ return yield* Option.match(from, {
111
+ onNone: () => send({
112
+ op: OP.identify,
113
+ d: {
114
+ token: Redacted.value(cfg.token),
115
+ intents: cfg.intents,
116
+ properties: identity
117
+ }
118
+ }),
119
+ onSome: (r) => send({
120
+ op: OP.resume,
121
+ d: {
122
+ token: Redacted.value(cfg.token),
123
+ session_id: r.id,
124
+ seq: Option.getOrNull(s)
125
+ }
126
+ })
127
+ });
128
+ });
129
+ const onReady = (d) => Effect.gen(function* () {
130
+ const info = yield* Schema.decodeUnknownEffect(Ready)(d);
131
+ yield* Ref.set(resume, Option.some({
132
+ id: info.session_id,
133
+ url: info.resume_gateway_url
134
+ }));
135
+ yield* Ref.set(attempts, 0);
136
+ yield* Deferred.succeed(ready, info.user);
137
+ });
138
+ const onDispatch = (payload) => Effect.gen(function* () {
139
+ if (payload.s != null) yield* Ref.set(seq, Option.some(payload.s));
140
+ if (payload.t === "READY") return yield* onReady(payload.d);
141
+ if (payload.t === "RESUMED") return yield* Ref.set(attempts, 0);
142
+ const dispatch = yield* Schema.decodeUnknownEffect(Dispatch)({
143
+ t: payload.t,
144
+ d: payload.d
145
+ }).pipe(Effect.option);
146
+ if (Option.isSome(dispatch)) yield* Queue.offer(dispatches, {
147
+ dispatch: dispatch.value,
148
+ raw: payload.d
149
+ });
150
+ });
151
+ const invalidSession = (d) => Effect.gen(function* () {
152
+ if (d !== true) {
153
+ yield* Ref.set(resume, Option.none());
154
+ yield* Ref.set(requested, Option.some("reidentify"));
155
+ }
156
+ yield* write(new Socket.CloseEvent(RESUMABLE_CLOSE, "invalid session"));
157
+ });
158
+ const handle = (raw) => Effect.gen(function* () {
159
+ const payload = yield* decodeFrame(raw).pipe(Effect.option);
160
+ if (Option.isNone(payload)) return;
161
+ const message = payload.value;
162
+ yield* Match.value(message.op).pipe(Match.when(OP.dispatch, () => onDispatch(message)), Match.when(OP.hello, () => hello(message.d)), Match.when(OP.heartbeatAck, () => Ref.set(acked, true)), Match.when(OP.heartbeat, () => heartbeat), Match.when(OP.reconnect, () => write(new Socket.CloseEvent(RESUMABLE_CLOSE, "reconnect requested"))), Match.when(OP.invalidSession, () => invalidSession(message.d)), Match.orElse(() => Effect.void));
163
+ }).pipe(Effect.ignore);
164
+ const ended = yield* socket.runString(handle).pipe(Effect.as({
165
+ action: "resume",
166
+ code: 1e3,
167
+ reason: "closed"
168
+ }), Effect.catch((error) => {
169
+ const code = socketCode(error);
170
+ return Effect.succeed({
171
+ action: classifyClose(code),
172
+ code,
173
+ reason: closeReason(code, socketReason(error))
174
+ });
175
+ }));
176
+ const override = yield* Ref.get(requested);
177
+ return Option.match(override, {
178
+ onNone: () => ended,
179
+ onSome: (action) => ({
180
+ ...ended,
181
+ action
182
+ })
183
+ });
184
+ }).pipe(Effect.scoped);
185
+ const cycle = Effect.gen(function* () {
186
+ const ended = yield* once;
187
+ if (ended.action === "fatal") return yield* new MessengerError.MessengerTransportClosed({
188
+ provider,
189
+ reason: ended.reason,
190
+ raw: { code: ended.code }
191
+ });
192
+ const attempt = yield* Ref.get(attempts);
193
+ yield* Ref.set(attempts, attempt + 1);
194
+ const wait = ended.action === "reidentify" ? yield* freshSessionDelay : backoff(attempt);
195
+ yield* Effect.logDebug("gateway reconnecting", {
196
+ code: ended.code,
197
+ action: ended.action,
198
+ attempt,
199
+ in: Duration.toSeconds(wait)
200
+ });
201
+ yield* Effect.sleep(wait);
202
+ });
203
+ yield* Effect.forever(cycle).pipe(Effect.catch((closed) => Deferred.fail(ready, new MessengerError.MessengerConnectFailed({
204
+ provider,
205
+ reason: closed.reason,
206
+ raw: closed.raw
207
+ })).pipe(Effect.andThen(Queue.fail(dispatches, closed)))), Effect.ensuring(Queue.end(dispatches)), Effect.forkScoped);
208
+ return {
209
+ bot: yield* Deferred.await(ready),
210
+ dispatches
211
+ };
212
+ });
213
+ //#endregion
214
+ export { GatewayInfo, Ready, classifyClose, closeReason, connect };
215
+
216
+ //# sourceMappingURL=gateway.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.mjs","names":["Events.Dispatch"],"sources":["../../src/internal/gateway.ts"],"sourcesContent":["import {\n type Cause,\n Deferred,\n Duration,\n Effect,\n Match,\n Option,\n Queue,\n Redacted,\n Ref,\n Schedule,\n Schema,\n type Scope,\n} from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as MessengerError from \"@effect-uai/core/MessengerError\"\nimport * as Events from \"./events.js\"\nimport { provider } from \"./rest.js\"\n\n// ---------------------------------------------------------------------------\n// Wire shapes\n// ---------------------------------------------------------------------------\n\nconst Payload = Schema.Struct({\n op: Schema.Number,\n d: Schema.optional(Schema.Unknown),\n s: Schema.optional(Schema.NullOr(Schema.Number)),\n t: Schema.optional(Schema.NullOr(Schema.String)),\n})\ntype Payload = typeof Payload.Type\n\nconst decodeFrame = Schema.decodeUnknownEffect(Schema.fromJsonString(Payload))\n\nconst Hello = Schema.Struct({ heartbeat_interval: Schema.Number })\n\nexport const Ready = Schema.Struct({\n user: Schema.Struct({ id: Schema.String, username: Schema.String }),\n session_id: Schema.String,\n resume_gateway_url: Schema.String,\n})\nexport type Ready = typeof Ready.Type\n\n/** `GET /gateway/bot`, the only REST call the session needs. */\nexport const GatewayInfo = Schema.Struct({ url: Schema.String })\n\n// Send: 1 heartbeat, 2 identify, 6 resume. Receive: 0 dispatch, 7 reconnect,\n// 9 invalid session, 10 hello, 11 heartbeat ack.\nconst OP = {\n dispatch: 0,\n heartbeat: 1,\n identify: 2,\n resume: 6,\n reconnect: 7,\n invalidSession: 9,\n hello: 10,\n heartbeatAck: 11,\n} as const\n\n// ---------------------------------------------------------------------------\n// Close codes\n// ---------------------------------------------------------------------------\n\n/** What the session does once a connection is gone. */\nexport type CloseAction = \"resume\" | \"reidentify\" | \"fatal\"\n\n// Fatal: nothing about reconnecting would change the answer.\nconst fatalReasons: Record<number, string> = {\n 4004: \"authentication failed: the bot token was rejected\",\n 4010: \"invalid shard\",\n 4011: \"sharding required\",\n 4012: \"invalid API version\",\n 4013: \"invalid intents\",\n 4014: \"disallowed intents: enable the privileged intent in the developer portal\",\n}\n\n// The session is gone but the token is fine: identify fresh.\nconst staleSessionCodes = [4007, 4009]\n\n/**\n * Everything else, transport drops included, resumes: Discord only names the\n * codes that must not be retried, so an unknown code is a reconnect.\n */\nexport const classifyClose = (code: number): CloseAction =>\n Match.value(code).pipe(\n Match.when(\n (c) => c in fatalReasons,\n (): CloseAction => \"fatal\",\n ),\n Match.when(\n (c) => staleSessionCodes.includes(c),\n (): CloseAction => \"reidentify\",\n ),\n Match.orElse((): CloseAction => \"resume\"),\n )\n\n/** Discord's own words for a fatal code, since its close frames carry no reason. */\nexport const closeReason = (code: number, reason?: string): string =>\n fatalReasons[code] ??\n (reason === undefined || reason === \"\" ? `gateway closed with ${code}` : reason)\n\n// A close we send ourselves. Anything but 1000/1001 keeps the session alive on\n// Discord's side, which is what makes the next connection resumable.\nconst RESUMABLE_CLOSE = 4000\n\n// ---------------------------------------------------------------------------\n// Session\n// ---------------------------------------------------------------------------\n\nexport type Config = {\n readonly token: Redacted.Redacted\n readonly intents: number\n /** From `GET /gateway/bot`. Resumes go to `resume_gateway_url` instead. */\n readonly url: string\n}\n\n/** A decoded dispatch beside the payload it came from, which `raw` carries. */\nexport type Incoming = {\n readonly dispatch: Events.Dispatch\n readonly raw: unknown\n}\n\nexport type Session = {\n readonly bot: Events.BotIdentity\n /**\n * Dispatches in arrival order, ended when the scope closes and failed with\n * `MessengerTransportClosed` on a fatal gateway close.\n */\n readonly dispatches: Queue.Queue<Incoming, MessengerError.MessengerError | Cause.Done>\n}\n\ntype Resume = { readonly id: string; readonly url: string }\n\ntype Ended = { readonly action: CloseAction; readonly code: number; readonly reason: string }\n\nconst socketCode = (error: Socket.SocketError): number =>\n error.reason._tag === \"SocketCloseError\" ? error.reason.code : 1006\n\nconst socketReason = (error: Socket.SocketError): string =>\n error.reason._tag === \"SocketCloseError\"\n ? (error.reason.closeReason ?? error.reason.message)\n : error.reason.message\n\n// Discord asks for a random 1-5 s pause before identifying on a fresh session.\nconst freshSessionDelay = Effect.sync(() => Duration.millis(1000 + Math.random() * 4000))\n\n// Capped exponential, from a beat to a minute. Reset once a connection is live.\nconst backoff = (attempt: number): Duration.Duration =>\n Duration.millis(Math.min(60_000, 500 * 2 ** attempt))\n\nconst identity = { os: \"effect-uai\", browser: \"effect-uai\", device: \"effect-uai\" }\n\n/**\n * One gateway session: connect, identify, heartbeat, resume, reconnect.\n *\n * Returns once `READY` has arrived, so a bad token or a disallowed intent is\n * a `MessengerConnectFailed` at layer build rather than a stream that dies a\n * moment later. From then on reconnects are silent and only a fatal close\n * ends `dispatches`.\n */\nexport const connect = (\n cfg: Config,\n): Effect.Effect<Session, MessengerError.MessengerConnectFailed, Scope.Scope> =>\n Effect.gen(function* () {\n const dispatches = yield* Queue.unbounded<\n Incoming,\n MessengerError.MessengerError | Cause.Done\n >()\n const ready = yield* Deferred.make<Events.BotIdentity, MessengerError.MessengerConnectFailed>()\n const resume = yield* Ref.make(Option.none<Resume>())\n const attempts = yield* Ref.make(0)\n\n // -- one connection ----------------------------------------------------\n\n const once = Effect.gen(function* () {\n const from = yield* Ref.get(resume)\n // A resumable session has its own host; a fresh one starts at the URL\n // `GET /gateway/bot` gave us.\n const host = Option.match(from, { onNone: () => cfg.url, onSome: (r: Resume) => r.url })\n const socket = yield* Socket.makeWebSocket(`${host}/?v=10&encoding=json`, {\n // Effect treats every close as an error by default; the standard clean\n // codes are not, and Discord's 4xxx ones are what drives `classifyClose`.\n closeCodeIsError: (code: number) => code !== 1000 && code !== 1001 && code !== 1005,\n // The gateway takes its token in the identify payload, not a header,\n // so every runtime's global `WebSocket` is enough.\n }).pipe(Effect.provide(Socket.layerWebSocketConstructorGlobal))\n const write = yield* socket.writer\n const send = (payload: unknown) => write(JSON.stringify(payload))\n const seq = yield* Ref.make(Option.none<number>())\n const acked = yield* Ref.make(true)\n // Set by op 9 with `d: false`, the one case the close code cannot express.\n const requested = yield* Ref.make(Option.none<CloseAction>())\n\n const heartbeat = Effect.gen(function* () {\n const s = yield* Ref.get(seq)\n return yield* send({ op: OP.heartbeat, d: Option.getOrNull(s) })\n })\n\n // A beat with no ACK since the last one means the connection is a zombie:\n // close it non-cleanly so the next connection resumes.\n const beat = Effect.gen(function* () {\n const alive = yield* Ref.getAndSet(acked, false)\n if (!alive) {\n yield* write(new Socket.CloseEvent(RESUMABLE_CLOSE, \"no heartbeat ack\"))\n return false\n }\n yield* heartbeat\n return true\n })\n\n // Discord asks for the first beat to land at a random point in the\n // interval, so a fleet of bots does not beat in lockstep.\n const beating = (interval: number) =>\n Effect.sleep(Duration.millis(interval * Math.random())).pipe(\n Effect.andThen(\n beat.pipe(\n Effect.repeat({\n schedule: Schedule.spaced(Duration.millis(interval)),\n while: (alive: boolean) => alive,\n }),\n ),\n ),\n Effect.ignore,\n Effect.forkScoped,\n )\n\n const hello = (d: unknown) =>\n Effect.gen(function* () {\n const { heartbeat_interval } = yield* Schema.decodeUnknownEffect(Hello)(d)\n yield* beating(heartbeat_interval)\n const from = yield* Ref.get(resume)\n const s = yield* Ref.get(seq)\n return yield* Option.match(from, {\n onNone: () =>\n send({\n op: OP.identify,\n d: {\n token: Redacted.value(cfg.token),\n intents: cfg.intents,\n properties: identity,\n },\n }),\n onSome: (r) =>\n send({\n op: OP.resume,\n d: {\n token: Redacted.value(cfg.token),\n session_id: r.id,\n seq: Option.getOrNull(s),\n },\n }),\n })\n })\n\n const onReady = (d: unknown) =>\n Effect.gen(function* () {\n const info = yield* Schema.decodeUnknownEffect(Ready)(d)\n yield* Ref.set(resume, Option.some({ id: info.session_id, url: info.resume_gateway_url }))\n yield* Ref.set(attempts, 0)\n yield* Deferred.succeed(ready, info.user)\n })\n\n const onDispatch = (payload: Payload) =>\n Effect.gen(function* () {\n if (payload.s != null) yield* Ref.set(seq, Option.some(payload.s))\n if (payload.t === \"READY\") return yield* onReady(payload.d)\n if (payload.t === \"RESUMED\") return yield* Ref.set(attempts, 0)\n const dispatch = yield* Schema.decodeUnknownEffect(Events.Dispatch)({\n t: payload.t,\n d: payload.d,\n }).pipe(Effect.option)\n if (Option.isSome(dispatch)) {\n yield* Queue.offer(dispatches, { dispatch: dispatch.value, raw: payload.d })\n }\n })\n\n // `d: true` keeps the session, `d: false` drops it; both mean reconnect.\n const invalidSession = (d: unknown) =>\n Effect.gen(function* () {\n if (d !== true) {\n yield* Ref.set(resume, Option.none())\n yield* Ref.set(requested, Option.some<CloseAction>(\"reidentify\"))\n }\n yield* write(new Socket.CloseEvent(RESUMABLE_CLOSE, \"invalid session\"))\n })\n\n const handle = (raw: string) =>\n Effect.gen(function* () {\n const payload = yield* decodeFrame(raw).pipe(Effect.option)\n if (Option.isNone(payload)) return\n const message = payload.value\n yield* Match.value(message.op).pipe(\n Match.when(OP.dispatch, () => onDispatch(message)),\n Match.when(OP.hello, () => hello(message.d)),\n Match.when(OP.heartbeatAck, () => Ref.set(acked, true)),\n Match.when(OP.heartbeat, () => heartbeat),\n Match.when(OP.reconnect, () =>\n write(new Socket.CloseEvent(RESUMABLE_CLOSE, \"reconnect requested\")),\n ),\n Match.when(OP.invalidSession, () => invalidSession(message.d)),\n Match.orElse(() => Effect.void),\n )\n }).pipe(Effect.ignore)\n\n // A clean 1000 is only ever our own teardown; anything else carries the\n // close code the classifier reads.\n const ended = yield* socket.runString(handle).pipe(\n Effect.as<Ended>({ action: \"resume\", code: 1000, reason: \"closed\" }),\n Effect.catch((error: Socket.SocketError) => {\n const code = socketCode(error)\n return Effect.succeed<Ended>({\n action: classifyClose(code),\n code,\n reason: closeReason(code, socketReason(error)),\n })\n }),\n )\n const override = yield* Ref.get(requested)\n return Option.match(override, {\n onNone: (): Ended => ended,\n onSome: (action): Ended => ({ ...ended, action }),\n })\n }).pipe(Effect.scoped)\n\n // -- the reconnect loop ------------------------------------------------\n\n const cycle = Effect.gen(function* () {\n const ended = yield* once\n if (ended.action === \"fatal\") {\n return yield* new MessengerError.MessengerTransportClosed({\n provider,\n reason: ended.reason,\n raw: { code: ended.code },\n })\n }\n const attempt = yield* Ref.get(attempts)\n yield* Ref.set(attempts, attempt + 1)\n const wait = ended.action === \"reidentify\" ? yield* freshSessionDelay : backoff(attempt)\n // Reconnects never reach `events`, so this is the only sign of one. A\n // dropped connection is noticed only when a heartbeat goes unacked,\n // which is up to two intervals after the network actually went away.\n yield* Effect.logDebug(\"gateway reconnecting\", {\n code: ended.code,\n action: ended.action,\n attempt,\n in: Duration.toSeconds(wait),\n })\n yield* Effect.sleep(wait)\n })\n\n // Failing the deferred is a no-op once `READY` has landed, so the same\n // fatal close is a connect failure before it and a transport close after.\n yield* Effect.forever(cycle).pipe(\n Effect.catch((closed: MessengerError.MessengerTransportClosed) =>\n Deferred.fail(\n ready,\n new MessengerError.MessengerConnectFailed({\n provider,\n reason: closed.reason,\n raw: closed.raw,\n }),\n ).pipe(Effect.andThen(Queue.fail(dispatches, closed))),\n ),\n Effect.ensuring(Queue.end(dispatches)),\n Effect.forkScoped,\n )\n\n return { bot: yield* Deferred.await(ready), dispatches }\n })\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,40 @@
1
+ import { classifyClose, closeReason } from "./gateway.mjs";
2
+ import { n as describe, r as it, t as globalExpect } from "../test.DNmyFkvJ-N3QqMRwZ.mjs";
3
+ //#region src/internal/gateway.test.ts
4
+ describe("classifyClose", () => {
5
+ it("never retries a close reconnecting cannot fix", () => {
6
+ globalExpect([
7
+ 4004,
8
+ 4010,
9
+ 4011,
10
+ 4012,
11
+ 4013,
12
+ 4014
13
+ ].map(classifyClose)).toEqual(Array(6).fill("fatal"));
14
+ });
15
+ it("identifies fresh only where the session itself is gone", () => {
16
+ globalExpect([4007, 4009].map(classifyClose)).toEqual(["reidentify", "reidentify"]);
17
+ });
18
+ it("resumes on everything else, transport drops included", () => {
19
+ globalExpect([
20
+ 4e3,
21
+ 4003,
22
+ 4005,
23
+ 4008,
24
+ 1006,
25
+ 1011
26
+ ].map(classifyClose)).toEqual(Array(6).fill("resume"));
27
+ });
28
+ });
29
+ describe("closeReason", () => {
30
+ it("names a fatal code, since Discord's close frames carry no reason", () => {
31
+ globalExpect(closeReason(4014, "")).toContain("privileged intent");
32
+ });
33
+ it("falls back to whatever the socket reported", () => {
34
+ globalExpect(closeReason(1006, "connection reset")).toBe("connection reset");
35
+ });
36
+ });
37
+ //#endregion
38
+ export {};
39
+
40
+ //# sourceMappingURL=gateway.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.test.mjs","names":[],"sources":["../../src/internal/gateway.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\"\nimport { classifyClose, closeReason } from \"./gateway.js\"\n\ndescribe(\"classifyClose\", () => {\n it(\"never retries a close reconnecting cannot fix\", () => {\n expect([4004, 4010, 4011, 4012, 4013, 4014].map(classifyClose)).toEqual(Array(6).fill(\"fatal\"))\n })\n\n it(\"identifies fresh only where the session itself is gone\", () => {\n expect([4007, 4009].map(classifyClose)).toEqual([\"reidentify\", \"reidentify\"])\n })\n\n it(\"resumes on everything else, transport drops included\", () => {\n // 1006 is a dropped connection, 4000 the code the zombie check sends.\n expect([4000, 4003, 4005, 4008, 1006, 1011].map(classifyClose)).toEqual(Array(6).fill(\"resume\"))\n })\n})\n\ndescribe(\"closeReason\", () => {\n it(\"names a fatal code, since Discord's close frames carry no reason\", () => {\n expect(closeReason(4014, \"\")).toContain(\"privileged intent\")\n })\n\n it(\"falls back to whatever the socket reported\", () => {\n expect(closeReason(1006, \"connection reset\")).toBe(\"connection reset\")\n })\n})\n"],"mappings":";;;AAGA,SAAS,uBAAuB;CAC9B,GAAG,uDAAuD;EACxD,aAAO;GAAC;GAAM;GAAM;GAAM;GAAM;GAAM;EAAI,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,QAAQ,MAAM,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC;CAChG,CAAC;CAED,GAAG,gEAAgE;EACjE,aAAO,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,cAAc,YAAY,CAAC;CAC9E,CAAC;CAED,GAAG,8DAA8D;EAE/D,aAAO;GAAC;GAAM;GAAM;GAAM;GAAM;GAAM;EAAI,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,QAAQ,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC;CACjG,CAAC;AACH,CAAC;AAED,SAAS,qBAAqB;CAC5B,GAAG,0EAA0E;EAC3E,aAAO,YAAY,MAAM,EAAE,CAAC,CAAC,CAAC,UAAU,mBAAmB;CAC7D,CAAC;CAED,GAAG,oDAAoD;EACrD,aAAO,YAAY,MAAM,kBAAkB,CAAC,CAAC,CAAC,KAAK,kBAAkB;CACvE,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { a as Method, c as decoded, i as Fields, l as provider, n as ApiFailure, o as Upload, r as Config, s as call, t as ApiError, u as upload } from "../rest-CZSW-VJK.mjs";
2
+ export { ApiError, ApiFailure, Config, Fields, Method, Upload, call, decoded, provider, upload };