@effect-uai/slack 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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/Slack.d.mts +43 -0
- package/dist/Slack.d.mts.map +1 -0
- package/dist/Slack.mjs +215 -0
- package/dist/Slack.mjs.map +1 -0
- package/dist/api-BBgliKo7.d.mts +51 -0
- package/dist/api-BBgliKo7.d.mts.map +1 -0
- package/dist/events-Cj5RRjK1.d.mts +317 -0
- package/dist/events-Cj5RRjK1.d.mts.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/internal/api.d.mts +2 -0
- package/dist/internal/api.mjs +79 -0
- package/dist/internal/api.mjs.map +1 -0
- package/dist/internal/events.d.mts +2 -0
- package/dist/internal/events.mjs +184 -0
- package/dist/internal/events.mjs.map +1 -0
- package/dist/internal/events.test.d.mts +1 -0
- package/dist/internal/events.test.mjs +174 -0
- package/dist/internal/events.test.mjs.map +1 -0
- package/dist/internal/socket.d.mts +2 -0
- package/dist/internal/socket.mjs +109 -0
- package/dist/internal/socket.mjs.map +1 -0
- package/dist/internal/socket.test.d.mts +1 -0
- package/dist/internal/socket.test.mjs +25 -0
- package/dist/internal/socket.test.mjs.map +1 -0
- package/dist/magic-string.es-CUAnjKYC.mjs +1014 -0
- package/dist/magic-string.es-CUAnjKYC.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/socket-DJgdt_ka.d.mts +54 -0
- package/dist/socket-DJgdt_ka.d.mts.map +1 -0
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs +13658 -0
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
- package/package.json +59 -0
- package/src/Slack.ts +419 -0
- package/src/index.ts +1 -0
- package/src/internal/api.ts +171 -0
- package/src/internal/events.test.ts +121 -0
- package/src/internal/events.ts +330 -0
- package/src/internal/socket.test.ts +25 -0
- package/src/internal/socket.ts +238 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Envelope, 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 bot = {
|
|
6
|
+
userId: "UBOT",
|
|
7
|
+
botId: "BBOT",
|
|
8
|
+
teamId: "T1"
|
|
9
|
+
};
|
|
10
|
+
const options = (replyIn = "thread") => ({
|
|
11
|
+
bot,
|
|
12
|
+
replyIn
|
|
13
|
+
});
|
|
14
|
+
/** One frame through the real decoder, as the socket hands it over. */
|
|
15
|
+
const frame = (raw, replyIn) => toEvents(options(replyIn))(Schema.decodeUnknownSync(Envelope)(raw), raw);
|
|
16
|
+
const event = (payload, replyIn) => frame({
|
|
17
|
+
type: "events_api",
|
|
18
|
+
envelope_id: "e1",
|
|
19
|
+
payload: {
|
|
20
|
+
event_id: "Ev1",
|
|
21
|
+
event: payload
|
|
22
|
+
}
|
|
23
|
+
}, replyIn)[0];
|
|
24
|
+
const message = (fields, replyIn) => event({
|
|
25
|
+
type: "message",
|
|
26
|
+
channel: "C1",
|
|
27
|
+
channel_type: "channel",
|
|
28
|
+
user: "U1",
|
|
29
|
+
ts: "1.1",
|
|
30
|
+
...fields
|
|
31
|
+
}, replyIn);
|
|
32
|
+
const mention = (fields, replyIn) => event({
|
|
33
|
+
type: "app_mention",
|
|
34
|
+
channel: "C1",
|
|
35
|
+
user: "U1",
|
|
36
|
+
ts: "1.1",
|
|
37
|
+
...fields
|
|
38
|
+
}, replyIn);
|
|
39
|
+
describe("addressed", () => {
|
|
40
|
+
it("is a DM or a mention, and a thread follow-up without one is neither", () => {
|
|
41
|
+
globalExpect(message({
|
|
42
|
+
channel: "D1",
|
|
43
|
+
channel_type: "im",
|
|
44
|
+
text: "hi"
|
|
45
|
+
})).toMatchObject({ addressed: true });
|
|
46
|
+
globalExpect(mention({ text: "<@UBOT> ping" })).toMatchObject({ addressed: true });
|
|
47
|
+
globalExpect(message({
|
|
48
|
+
text: "still there?",
|
|
49
|
+
thread_ts: "1.0"
|
|
50
|
+
})).toMatchObject({ addressed: false });
|
|
51
|
+
});
|
|
52
|
+
it("strips the bot's own mention, labelled or not, and leaves other users'", () => {
|
|
53
|
+
globalExpect(mention({ text: "<@UBOT|betty> ask <@U9> too" })).toMatchObject({ text: "ask <@U9> too" });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("one event per message", () => {
|
|
57
|
+
it("keeps the app_mention in a channel and the message in a DM", () => {
|
|
58
|
+
globalExpect(message({ text: "<@UBOT> ping" })).toBeUndefined();
|
|
59
|
+
globalExpect(mention({ text: "<@UBOT> ping" })).toMatchObject({
|
|
60
|
+
_tag: "Message",
|
|
61
|
+
id: "1.1"
|
|
62
|
+
});
|
|
63
|
+
globalExpect(message({
|
|
64
|
+
channel: "D1",
|
|
65
|
+
channel_type: "im",
|
|
66
|
+
text: "<@UBOT> hi"
|
|
67
|
+
})).toMatchObject({ text: "hi" });
|
|
68
|
+
globalExpect(mention({ channel: "D1" })).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
it("ignores edits and every bot, so two bots cannot answer each other", () => {
|
|
71
|
+
globalExpect(message({
|
|
72
|
+
subtype: "message_changed",
|
|
73
|
+
text: "edited"
|
|
74
|
+
})).toBeUndefined();
|
|
75
|
+
globalExpect(message({
|
|
76
|
+
bot_id: "B2",
|
|
77
|
+
text: "hi"
|
|
78
|
+
})).toBeUndefined();
|
|
79
|
+
globalExpect(message({
|
|
80
|
+
user: "UBOT",
|
|
81
|
+
text: "my own words"
|
|
82
|
+
})).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("conversation", () => {
|
|
86
|
+
it("mints a thread from a top-level mention only under replyIn thread", () => {
|
|
87
|
+
globalExpect(mention({ ts: "1.1" }, "thread")).toMatchObject({ conversation: {
|
|
88
|
+
channel: "C1",
|
|
89
|
+
thread: "1.1"
|
|
90
|
+
} });
|
|
91
|
+
globalExpect(mention({ ts: "1.1" }, "channel")).toMatchObject({ conversation: { channel: "C1" } });
|
|
92
|
+
});
|
|
93
|
+
it("keeps a follow-up in the thread it was written in, and a DM out of one", () => {
|
|
94
|
+
globalExpect(mention({
|
|
95
|
+
ts: "2.2",
|
|
96
|
+
thread_ts: "1.1"
|
|
97
|
+
})).toMatchObject({ conversation: {
|
|
98
|
+
channel: "C1",
|
|
99
|
+
thread: "1.1"
|
|
100
|
+
} });
|
|
101
|
+
globalExpect(message({
|
|
102
|
+
channel: "D1",
|
|
103
|
+
channel_type: "im",
|
|
104
|
+
text: "hi"
|
|
105
|
+
})).toMatchObject({ conversation: { channel: "D1" } });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("reactions", () => {
|
|
109
|
+
const added = (user = "U1") => event({
|
|
110
|
+
type: "reaction_added",
|
|
111
|
+
user,
|
|
112
|
+
reaction: "eyes",
|
|
113
|
+
item: {
|
|
114
|
+
channel: "C1",
|
|
115
|
+
ts: "1.1"
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
it("names the reacted message as its own thread, and drops the bot's own", () => {
|
|
119
|
+
globalExpect(added()).toMatchObject({
|
|
120
|
+
emoji: "eyes",
|
|
121
|
+
conversation: { thread: "1.1" }
|
|
122
|
+
});
|
|
123
|
+
globalExpect(added("UBOT")).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("commands and actions", () => {
|
|
127
|
+
it("drops the slash and keeps the rest as args", () => {
|
|
128
|
+
globalExpect(frame({
|
|
129
|
+
type: "slash_commands",
|
|
130
|
+
envelope_id: "e1",
|
|
131
|
+
payload: {
|
|
132
|
+
channel_id: "C1",
|
|
133
|
+
user_id: "U1",
|
|
134
|
+
command: "/start",
|
|
135
|
+
text: " now "
|
|
136
|
+
}
|
|
137
|
+
})[0]).toMatchObject({
|
|
138
|
+
_tag: "Command",
|
|
139
|
+
name: "start",
|
|
140
|
+
args: "now"
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
const interactive = (payload) => frame({
|
|
144
|
+
type: "interactive",
|
|
145
|
+
envelope_id: "e1",
|
|
146
|
+
payload
|
|
147
|
+
});
|
|
148
|
+
it("maps every pressed element, and ignores payloads needing their own reply", () => {
|
|
149
|
+
globalExpect(interactive({
|
|
150
|
+
type: "block_actions",
|
|
151
|
+
user: { id: "U1" },
|
|
152
|
+
container: {
|
|
153
|
+
channel_id: "C1",
|
|
154
|
+
thread_ts: "1.1"
|
|
155
|
+
},
|
|
156
|
+
actions: [{
|
|
157
|
+
action_id: "approve",
|
|
158
|
+
value: "12"
|
|
159
|
+
}, { action_id: "deny" }]
|
|
160
|
+
})).toMatchObject([{
|
|
161
|
+
actionId: "approve",
|
|
162
|
+
value: "12",
|
|
163
|
+
conversation: { thread: "1.1" }
|
|
164
|
+
}, { actionId: "deny" }]);
|
|
165
|
+
globalExpect(interactive({
|
|
166
|
+
type: "view_submission",
|
|
167
|
+
user: { id: "U1" }
|
|
168
|
+
})).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
//#endregion
|
|
172
|
+
export {};
|
|
173
|
+
|
|
174
|
+
//# 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, Envelope, type Options, type ReplyIn, toEvents } from \"./events.js\"\n\nconst bot: BotIdentity = { userId: \"UBOT\", botId: \"BBOT\", teamId: \"T1\" }\n\nconst options = (replyIn: ReplyIn = \"thread\"): Options => ({ bot, replyIn })\n\n/** One frame through the real decoder, as the socket hands it over. */\nconst frame = (raw: Record<string, unknown>, replyIn?: ReplyIn) =>\n toEvents(options(replyIn))(Schema.decodeUnknownSync(Envelope)(raw), raw)\n\nconst event = (payload: Record<string, unknown>, replyIn?: ReplyIn) =>\n frame(\n { type: \"events_api\", envelope_id: \"e1\", payload: { event_id: \"Ev1\", event: payload } },\n replyIn,\n )[0]\n\nconst message = (fields: Record<string, unknown>, replyIn?: ReplyIn) =>\n event(\n { type: \"message\", channel: \"C1\", channel_type: \"channel\", user: \"U1\", ts: \"1.1\", ...fields },\n replyIn,\n )\n\nconst mention = (fields: Record<string, unknown>, replyIn?: ReplyIn) =>\n event({ type: \"app_mention\", channel: \"C1\", user: \"U1\", ts: \"1.1\", ...fields }, replyIn)\n\ndescribe(\"addressed\", () => {\n it(\"is a DM or a mention, and a thread follow-up without one is neither\", () => {\n expect(message({ channel: \"D1\", channel_type: \"im\", text: \"hi\" })).toMatchObject({\n addressed: true,\n })\n expect(mention({ text: \"<@UBOT> ping\" })).toMatchObject({ addressed: true })\n expect(message({ text: \"still there?\", thread_ts: \"1.0\" })).toMatchObject({ addressed: false })\n })\n\n it(\"strips the bot's own mention, labelled or not, and leaves other users'\", () => {\n expect(mention({ text: \"<@UBOT|betty> ask <@U9> too\" })).toMatchObject({\n text: \"ask <@U9> too\",\n })\n })\n})\n\ndescribe(\"one event per message\", () => {\n // Slack sends a channel mention twice, as `app_mention` and `message`, and\n // sends a mention inside a DM only as `message.im`.\n it(\"keeps the app_mention in a channel and the message in a DM\", () => {\n expect(message({ text: \"<@UBOT> ping\" })).toBeUndefined()\n expect(mention({ text: \"<@UBOT> ping\" })).toMatchObject({ _tag: \"Message\", id: \"1.1\" })\n expect(message({ channel: \"D1\", channel_type: \"im\", text: \"<@UBOT> hi\" })).toMatchObject({\n text: \"hi\",\n })\n expect(mention({ channel: \"D1\" })).toBeUndefined()\n })\n\n it(\"ignores edits and every bot, so two bots cannot answer each other\", () => {\n expect(message({ subtype: \"message_changed\", text: \"edited\" })).toBeUndefined()\n expect(message({ bot_id: \"B2\", text: \"hi\" })).toBeUndefined()\n expect(message({ user: \"UBOT\", text: \"my own words\" })).toBeUndefined()\n })\n})\n\ndescribe(\"conversation\", () => {\n it(\"mints a thread from a top-level mention only under replyIn thread\", () => {\n expect(mention({ ts: \"1.1\" }, \"thread\")).toMatchObject({\n conversation: { channel: \"C1\", thread: \"1.1\" },\n })\n expect(mention({ ts: \"1.1\" }, \"channel\")).toMatchObject({ conversation: { channel: \"C1\" } })\n })\n\n it(\"keeps a follow-up in the thread it was written in, and a DM out of one\", () => {\n expect(mention({ ts: \"2.2\", thread_ts: \"1.1\" })).toMatchObject({\n conversation: { channel: \"C1\", thread: \"1.1\" },\n })\n expect(message({ channel: \"D1\", channel_type: \"im\", text: \"hi\" })).toMatchObject({\n conversation: { channel: \"D1\" },\n })\n })\n})\n\ndescribe(\"reactions\", () => {\n const added = (user = \"U1\") =>\n event({ type: \"reaction_added\", user, reaction: \"eyes\", item: { channel: \"C1\", ts: \"1.1\" } })\n\n it(\"names the reacted message as its own thread, and drops the bot's own\", () => {\n // `reaction_added` carries no `thread_ts`, so this matches a reaction on the\n // message that opened the thread and nothing deeper.\n expect(added()).toMatchObject({ emoji: \"eyes\", conversation: { thread: \"1.1\" } })\n expect(added(\"UBOT\")).toBeUndefined()\n })\n})\n\ndescribe(\"commands and actions\", () => {\n it(\"drops the slash and keeps the rest as args\", () => {\n expect(\n frame({\n type: \"slash_commands\",\n envelope_id: \"e1\",\n payload: { channel_id: \"C1\", user_id: \"U1\", command: \"/start\", text: \" now \" },\n })[0],\n ).toMatchObject({ _tag: \"Command\", name: \"start\", args: \"now\" })\n })\n\n const interactive = (payload: Record<string, unknown>) =>\n frame({ type: \"interactive\", envelope_id: \"e1\", payload })\n\n it(\"maps every pressed element, and ignores payloads needing their own reply\", () => {\n expect(\n interactive({\n type: \"block_actions\",\n user: { id: \"U1\" },\n container: { channel_id: \"C1\", thread_ts: \"1.1\" },\n actions: [{ action_id: \"approve\", value: \"12\" }, { action_id: \"deny\" }],\n }),\n ).toMatchObject([\n { actionId: \"approve\", value: \"12\", conversation: { thread: \"1.1\" } },\n { actionId: \"deny\" },\n ])\n expect(interactive({ type: \"view_submission\", user: { id: \"U1\" } })).toEqual([])\n })\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Recent, c as connect, i as RECENT, l as remember, n as DisconnectAction, o as Session, r as Incoming, s as classifyDisconnect, t as Config } from "../socket-DJgdt_ka.mjs";
|
|
2
|
+
export { Config, DisconnectAction, Incoming, RECENT, Recent, Session, classifyDisconnect, connect, remember };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { provider } from "./api.mjs";
|
|
2
|
+
import { Frame, eventId } from "./events.mjs";
|
|
3
|
+
import { Array, Deferred, Duration, Effect, Match, Option, Queue, Ref, 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/socket.ts
|
|
7
|
+
/**
|
|
8
|
+
* Slack names the one reason that must not be retried; the rolling refresh is
|
|
9
|
+
* scheduled rather than a failure, so it reconnects without backing off, and
|
|
10
|
+
* an unknown reason is an ordinary reconnect.
|
|
11
|
+
*/
|
|
12
|
+
const classifyDisconnect = (reason) => Match.value(reason).pipe(Match.when("link_disabled", () => "fatal"), Match.when("refresh_requested", () => "refresh"), Match.when("warning", () => "refresh"), Match.orElse(() => "reconnect"));
|
|
13
|
+
/** How many ids to hold. Redeliveries arrive at once, +1 minute and +5 minutes. */
|
|
14
|
+
const RECENT = 256;
|
|
15
|
+
/** `None` when `id` has been seen; otherwise the set with `id` in it, oldest dropped. */
|
|
16
|
+
const remember = (seen, id, cap = 256) => seen.includes(id) ? Option.none() : Option.some(Array.takeRight([...seen, id], cap));
|
|
17
|
+
const parseFrame = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
|
|
18
|
+
const decodeFrame = Schema.decodeUnknownEffect(Frame);
|
|
19
|
+
const socketReason = (error) => error.reason._tag === "SocketCloseError" ? error.reason.closeReason ?? error.reason.message : error.reason.message;
|
|
20
|
+
const reasonOf = (e) => e._tag === "SocketError" ? socketReason(e) : MessengerError.describe(e);
|
|
21
|
+
const backoff = (attempt) => Duration.millis(Math.min(6e4, 500 * 2 ** attempt));
|
|
22
|
+
/**
|
|
23
|
+
* One Socket Mode session: open, acknowledge, dedupe, reconnect.
|
|
24
|
+
*
|
|
25
|
+
* Returns once `hello` has arrived, so a rejected app token is a
|
|
26
|
+
* `MessengerConnectFailed` at layer build rather than a stream that dies a
|
|
27
|
+
* moment later. From then on reconnects are silent and only a `link_disabled`
|
|
28
|
+
* disconnect ends `envelopes`.
|
|
29
|
+
*/
|
|
30
|
+
const connect = (cfg) => Effect.gen(function* () {
|
|
31
|
+
const envelopes = yield* Queue.unbounded();
|
|
32
|
+
const ready = yield* Deferred.make();
|
|
33
|
+
const attempts = yield* Ref.make(0);
|
|
34
|
+
const seen = yield* Ref.make([]);
|
|
35
|
+
const once = Effect.gen(function* () {
|
|
36
|
+
const url = yield* cfg.open;
|
|
37
|
+
const socket = yield* Socket.makeWebSocket(url, { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 }).pipe(Effect.provide(Socket.layerWebSocketConstructorGlobal));
|
|
38
|
+
const write = yield* socket.writer;
|
|
39
|
+
const requested = yield* Ref.make(Option.none());
|
|
40
|
+
const acknowledge = (envelopeId) => write(JSON.stringify({ envelope_id: envelopeId }));
|
|
41
|
+
const offer = (envelope, raw) => Effect.gen(function* () {
|
|
42
|
+
yield* acknowledge(envelope.envelope_id);
|
|
43
|
+
const id = eventId(envelope);
|
|
44
|
+
if (Option.isSome(id)) {
|
|
45
|
+
const next = remember(yield* Ref.get(seen), id.value);
|
|
46
|
+
if (Option.isNone(next)) return;
|
|
47
|
+
yield* Ref.set(seen, next.value);
|
|
48
|
+
}
|
|
49
|
+
yield* Queue.offer(envelopes, {
|
|
50
|
+
envelope,
|
|
51
|
+
raw
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
const disconnect = (reason) => Effect.gen(function* () {
|
|
55
|
+
const action = classifyDisconnect(reason);
|
|
56
|
+
yield* Ref.set(requested, Option.some({
|
|
57
|
+
action,
|
|
58
|
+
reason: reason ?? "disconnect"
|
|
59
|
+
}));
|
|
60
|
+
yield* write(new Socket.CloseEvent(1e3, reason ?? "disconnect"));
|
|
61
|
+
});
|
|
62
|
+
const handle = (text) => Effect.gen(function* () {
|
|
63
|
+
const raw = yield* parseFrame(text).pipe(Effect.option);
|
|
64
|
+
if (Option.isNone(raw)) return;
|
|
65
|
+
const frame = yield* decodeFrame(raw.value).pipe(Effect.option);
|
|
66
|
+
if (Option.isNone(frame)) return;
|
|
67
|
+
yield* Match.value(frame.value).pipe(Match.when({ type: "hello" }, () => Ref.set(attempts, 0).pipe(Effect.andThen(Deferred.succeed(ready, void 0)))), Match.when({ type: "disconnect" }, ({ reason }) => disconnect(reason)), Match.orElse((envelope) => offer(envelope, raw.value)));
|
|
68
|
+
}).pipe(Effect.ignore);
|
|
69
|
+
const ended = yield* socket.runString(handle).pipe(Effect.as({
|
|
70
|
+
action: "reconnect",
|
|
71
|
+
reason: "closed"
|
|
72
|
+
}), Effect.catch((error) => Effect.succeed({
|
|
73
|
+
action: "reconnect",
|
|
74
|
+
reason: reasonOf(error)
|
|
75
|
+
})));
|
|
76
|
+
return Option.getOrElse(yield* Ref.get(requested), () => ended);
|
|
77
|
+
}).pipe(Effect.scoped, Effect.catch((e) => Effect.map(Deferred.isDone(ready), (live) => ({
|
|
78
|
+
action: live ? "reconnect" : "fatal",
|
|
79
|
+
reason: reasonOf(e)
|
|
80
|
+
}))));
|
|
81
|
+
const cycle = Effect.gen(function* () {
|
|
82
|
+
const ended = yield* once;
|
|
83
|
+
if (ended.action === "fatal") return yield* new MessengerError.MessengerTransportClosed({
|
|
84
|
+
provider,
|
|
85
|
+
reason: ended.reason
|
|
86
|
+
});
|
|
87
|
+
const attempt = yield* Ref.get(attempts);
|
|
88
|
+
yield* Ref.set(attempts, attempt + 1);
|
|
89
|
+
const wait = ended.action === "refresh" ? Duration.zero : backoff(attempt);
|
|
90
|
+
yield* Effect.logDebug("socket mode reconnecting", {
|
|
91
|
+
reason: ended.reason,
|
|
92
|
+
action: ended.action,
|
|
93
|
+
attempt,
|
|
94
|
+
in: Duration.toSeconds(wait)
|
|
95
|
+
});
|
|
96
|
+
yield* Effect.sleep(wait);
|
|
97
|
+
});
|
|
98
|
+
yield* Effect.forever(cycle).pipe(Effect.catch((closed) => Deferred.fail(ready, new MessengerError.MessengerConnectFailed({
|
|
99
|
+
provider,
|
|
100
|
+
reason: closed.reason,
|
|
101
|
+
raw: closed.raw
|
|
102
|
+
})).pipe(Effect.andThen(Queue.fail(envelopes, closed)))), Effect.ensuring(Queue.end(envelopes)), Effect.forkScoped);
|
|
103
|
+
yield* Deferred.await(ready);
|
|
104
|
+
return { envelopes };
|
|
105
|
+
});
|
|
106
|
+
//#endregion
|
|
107
|
+
export { RECENT, classifyDisconnect, connect, remember };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=socket.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.mjs","names":["Arr","Events.Frame","Events.eventId"],"sources":["../../src/internal/socket.ts"],"sourcesContent":["import {\n Array as Arr,\n type Cause,\n Deferred,\n Duration,\n Effect,\n Match,\n Option,\n Queue,\n Ref,\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 \"./api.js\"\n\n// ---------------------------------------------------------------------------\n// Disconnects\n// ---------------------------------------------------------------------------\n\n/** What the session does once a connection is gone. */\nexport type DisconnectAction = \"refresh\" | \"reconnect\" | \"fatal\"\n\n/**\n * Slack names the one reason that must not be retried; the rolling refresh is\n * scheduled rather than a failure, so it reconnects without backing off, and\n * an unknown reason is an ordinary reconnect.\n */\nexport const classifyDisconnect = (reason: string | undefined): DisconnectAction =>\n Match.value(reason).pipe(\n Match.when(\"link_disabled\", (): DisconnectAction => \"fatal\"),\n Match.when(\"refresh_requested\", (): DisconnectAction => \"refresh\"),\n Match.when(\"warning\", (): DisconnectAction => \"refresh\"),\n Match.orElse((): DisconnectAction => \"reconnect\"),\n )\n\n// ---------------------------------------------------------------------------\n// Redelivery\n// ---------------------------------------------------------------------------\n\n/**\n * Transport state, not conversation state: Slack redelivers an envelope whose\n * acknowledgement it missed, and this exists only to drop the second copy.\n */\nexport type Recent = ReadonlyArray<string>\n\n/** How many ids to hold. Redeliveries arrive at once, +1 minute and +5 minutes. */\nexport const RECENT = 256\n\n/** `None` when `id` has been seen; otherwise the set with `id` in it, oldest dropped. */\nexport const remember = (seen: Recent, id: string, cap: number = RECENT): Option.Option<Recent> =>\n seen.includes(id) ? Option.none() : Option.some(Arr.takeRight([...seen, id], cap))\n\n// ---------------------------------------------------------------------------\n// Session\n// ---------------------------------------------------------------------------\n\nexport type Config = {\n /**\n * A fresh single-use Socket Mode URL, called for every connection. Failing\n * before `hello` has ever arrived is what makes a bad app token a connect\n * failure rather than a silent retry.\n */\n readonly open: Effect.Effect<string, MessengerError.MessengerError>\n}\n\n/** A decoded envelope beside the frame it came from, which `raw` carries. */\nexport type Incoming = {\n readonly envelope: Events.Envelope\n readonly raw: unknown\n}\n\nexport type Session = {\n /**\n * Envelopes in arrival order, already acknowledged, ended when the scope\n * closes and failed with `MessengerTransportClosed` on a disconnect Slack\n * says not to retry.\n */\n readonly envelopes: Queue.Queue<Incoming, MessengerError.MessengerError | Cause.Done>\n}\n\ntype Ended = { readonly action: DisconnectAction; readonly reason: string }\n\nconst parseFrame = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))\nconst decodeFrame = Schema.decodeUnknownEffect(Events.Frame)\n\nconst socketReason = (error: Socket.SocketError): string =>\n error.reason._tag === \"SocketCloseError\"\n ? (error.reason.closeReason ?? error.reason.message)\n : error.reason.message\n\nconst reasonOf = (e: MessengerError.MessengerError | Socket.SocketError): string =>\n e._tag === \"SocketError\" ? socketReason(e) : MessengerError.describe(e)\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\n/**\n * One Socket Mode session: open, acknowledge, dedupe, reconnect.\n *\n * Returns once `hello` has arrived, so a rejected app token is a\n * `MessengerConnectFailed` at layer build rather than a stream that dies a\n * moment later. From then on reconnects are silent and only a `link_disabled`\n * disconnect ends `envelopes`.\n */\nexport const connect = (\n cfg: Config,\n): Effect.Effect<Session, MessengerError.MessengerConnectFailed, Scope.Scope> =>\n Effect.gen(function* () {\n const envelopes = yield* Queue.unbounded<Incoming, MessengerError.MessengerError | Cause.Done>()\n const ready = yield* Deferred.make<void, MessengerError.MessengerConnectFailed>()\n const attempts = yield* Ref.make(0)\n const seen = yield* Ref.make<Recent>([])\n\n // -- one connection ----------------------------------------------------\n\n const once = Effect.gen(function* () {\n const url = yield* cfg.open\n const socket = yield* Socket.makeWebSocket(url, {\n // Effect treats every close as an error by default; the standard clean\n // codes are not, and Slack's close codes say nothing a reconnect\n // cannot fix, so a `disconnect` frame is the only fatal signal.\n closeCodeIsError: (code: number) => code !== 1000 && code !== 1001 && code !== 1005,\n // The ticket is in the URL, so every runtime's global `WebSocket` is enough.\n }).pipe(Effect.provide(Socket.layerWebSocketConstructorGlobal))\n const write = yield* socket.writer\n // Set by a `disconnect` frame, the one end a close code cannot express.\n const requested = yield* Ref.make(Option.none<Ended>())\n\n // Acknowledged before the envelope is offered, well inside Slack's three\n // second deadline, so the platform is never waiting on the recipe.\n const acknowledge = (envelopeId: string) => write(JSON.stringify({ envelope_id: envelopeId }))\n\n // A redelivery is acknowledged like any other envelope and then dropped,\n // so Slack stops retrying it and the recipe never sees it twice.\n const offer = (envelope: Events.Envelope, raw: unknown) =>\n Effect.gen(function* () {\n yield* acknowledge(envelope.envelope_id)\n const id = Events.eventId(envelope)\n if (Option.isSome(id)) {\n const next = remember(yield* Ref.get(seen), id.value)\n if (Option.isNone(next)) return\n yield* Ref.set(seen, next.value)\n }\n yield* Queue.offer(envelopes, { envelope, raw })\n })\n\n const disconnect = (reason: string | undefined) =>\n Effect.gen(function* () {\n const action = classifyDisconnect(reason)\n yield* Ref.set(requested, Option.some<Ended>({ action, reason: reason ?? \"disconnect\" }))\n yield* write(new Socket.CloseEvent(1000, reason ?? \"disconnect\"))\n })\n\n const handle = (text: string) =>\n Effect.gen(function* () {\n const raw = yield* parseFrame(text).pipe(Effect.option)\n if (Option.isNone(raw)) return\n const frame = yield* decodeFrame(raw.value).pipe(Effect.option)\n if (Option.isNone(frame)) return\n yield* Match.value(frame.value).pipe(\n Match.when({ type: \"hello\" }, () =>\n Ref.set(attempts, 0).pipe(Effect.andThen(Deferred.succeed(ready, undefined))),\n ),\n Match.when({ type: \"disconnect\" }, ({ reason }) => disconnect(reason)),\n Match.orElse((envelope) => offer(envelope, raw.value)),\n )\n }).pipe(Effect.ignore)\n\n // A clean 1000 is only ever our own teardown or a disconnect we asked\n // for; anything else is a drop, and every drop reconnects.\n const ended = yield* socket.runString(handle).pipe(\n Effect.as<Ended>({ action: \"reconnect\", reason: \"closed\" }),\n Effect.catch((error: Socket.SocketError) =>\n Effect.succeed<Ended>({ action: \"reconnect\", reason: reasonOf(error) }),\n ),\n )\n return Option.getOrElse(yield* Ref.get(requested), () => ended)\n }).pipe(\n Effect.scoped,\n // A URL we cannot get, or a socket that will not open, is fatal only\n // before `hello`: a rejected app token fails the layer, while the same\n // failure later is one more reconnect.\n Effect.catch((e: MessengerError.MessengerError | Socket.SocketError) =>\n Effect.map(Deferred.isDone(ready), (live): Ended => ({\n action: live ? \"reconnect\" : \"fatal\",\n reason: reasonOf(e),\n })),\n ),\n )\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 })\n }\n const attempt = yield* Ref.get(attempts)\n yield* Ref.set(attempts, attempt + 1)\n // A refresh is Slack cycling the connection on schedule, so the next one\n // opens at once; anything else backs off.\n const wait = ended.action === \"refresh\" ? Duration.zero : backoff(attempt)\n yield* Effect.logDebug(\"socket mode reconnecting\", {\n reason: ended.reason,\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 `hello` has landed, so the same end\n // 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(envelopes, closed))),\n ),\n Effect.ensuring(Queue.end(envelopes)),\n Effect.forkScoped,\n )\n\n yield* Deferred.await(ready)\n return { envelopes }\n })\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { classifyDisconnect, remember } from "./socket.mjs";
|
|
2
|
+
import { n as describe, r as it, t as globalExpect } from "../test.DNmyFkvJ-N3QqMRwZ.mjs";
|
|
3
|
+
import { Option } from "effect";
|
|
4
|
+
//#region src/internal/socket.test.ts
|
|
5
|
+
describe("classifyDisconnect", () => {
|
|
6
|
+
it("ends only on the reason Slack says not to retry", () => {
|
|
7
|
+
globalExpect(classifyDisconnect("link_disabled")).toBe("fatal");
|
|
8
|
+
globalExpect(["refresh_requested", "warning"].map(classifyDisconnect)).toEqual(["refresh", "refresh"]);
|
|
9
|
+
globalExpect(classifyDisconnect(void 0)).toBe("reconnect");
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
describe("remember", () => {
|
|
13
|
+
it("drops a redelivery of an id still in the set", () => {
|
|
14
|
+
const seen = Option.getOrThrow(remember([], "Ev1"));
|
|
15
|
+
globalExpect(remember(seen, "Ev1")).toEqual(Option.none());
|
|
16
|
+
globalExpect(Option.getOrThrow(remember(seen, "Ev2"))).toEqual(["Ev1", "Ev2"]);
|
|
17
|
+
});
|
|
18
|
+
it("forgets the oldest once the set is full, so it cannot grow without bound", () => {
|
|
19
|
+
globalExpect(Option.getOrThrow(remember(["a", "b"], "c", 2))).toEqual(["b", "c"]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//#endregion
|
|
23
|
+
export {};
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=socket.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.test.mjs","names":[],"sources":["../../src/internal/socket.test.ts"],"sourcesContent":["import { Option } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport { classifyDisconnect, remember } from \"./socket.js\"\n\ndescribe(\"classifyDisconnect\", () => {\n it(\"ends only on the reason Slack says not to retry\", () => {\n expect(classifyDisconnect(\"link_disabled\")).toBe(\"fatal\")\n // A rolling refresh is scheduled, so it reopens without backing off.\n expect([\"refresh_requested\", \"warning\"].map(classifyDisconnect)).toEqual([\"refresh\", \"refresh\"])\n expect(classifyDisconnect(undefined)).toBe(\"reconnect\")\n })\n})\n\ndescribe(\"remember\", () => {\n it(\"drops a redelivery of an id still in the set\", () => {\n const seen = Option.getOrThrow(remember([], \"Ev1\"))\n expect(remember(seen, \"Ev1\")).toEqual(Option.none())\n expect(Option.getOrThrow(remember(seen, \"Ev2\"))).toEqual([\"Ev1\", \"Ev2\"])\n })\n\n it(\"forgets the oldest once the set is full, so it cannot grow without bound\", () => {\n const full = [\"a\", \"b\"]\n expect(Option.getOrThrow(remember(full, \"c\", 2))).toEqual([\"b\", \"c\"])\n })\n})\n"],"mappings":";;;;AAIA,SAAS,4BAA4B;CACnC,GAAG,yDAAyD;EAC1D,aAAO,mBAAmB,eAAe,CAAC,CAAC,CAAC,KAAK,OAAO;EAExD,aAAO,CAAC,qBAAqB,SAAS,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,SAAS,CAAC;EAC/F,aAAO,mBAAmB,KAAA,CAAS,CAAC,CAAC,CAAC,KAAK,WAAW;CACxD,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB;CACzB,GAAG,sDAAsD;EACvD,MAAM,OAAO,OAAO,WAAW,SAAS,CAAC,GAAG,KAAK,CAAC;EAClD,aAAO,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC,QAAQ,OAAO,KAAK,CAAC;EACnD,aAAO,OAAO,WAAW,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,KAAK,CAAC;CACzE,CAAC;CAED,GAAG,kFAAkF;EAEnF,aAAO,OAAO,WAAW,SAAS,CADpB,KAAK,GACkB,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC;CACtE,CAAC;AACH,CAAC"}
|