@effect-uai/discord 0.17.0 → 0.18.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/README.md +1 -1
- package/dist/Discord.d.mts +3 -3
- package/dist/Discord.d.mts.map +1 -1
- package/dist/Discord.mjs +7 -5
- package/dist/Discord.mjs.map +1 -1
- package/dist/dist-DHer1cql.mjs +1533 -0
- package/dist/dist-DHer1cql.mjs.map +1 -0
- package/dist/{gateway-C_Ae1HYB.d.mts → gateway-CYyZYm4m.d.mts} +3 -2
- package/dist/gateway-CYyZYm4m.d.mts.map +1 -0
- package/dist/{test.DNmyFkvJ-N3QqMRwZ.mjs → index.C4tKZ0yW-CUBm3Bmz.mjs} +9446 -5818
- package/dist/index.C4tKZ0yW-CUBm3Bmz.mjs.map +1 -0
- package/dist/internal/events.test.mjs +1 -1
- package/dist/internal/gateway.d.mts +1 -1
- package/dist/internal/gateway.mjs +23 -14
- package/dist/internal/gateway.mjs.map +1 -1
- package/dist/internal/gateway.test.mjs +259 -7
- package/dist/internal/gateway.test.mjs.map +1 -1
- package/dist/internal/rest.d.mts +1 -1
- package/dist/internal/rest.mjs +1 -1
- package/dist/internal/rest.mjs.map +1 -1
- package/dist/{rest-CZSW-VJK.d.mts → rest-SmIBt9Nh.d.mts} +2 -2
- package/dist/{rest-CZSW-VJK.d.mts.map → rest-SmIBt9Nh.d.mts.map} +1 -1
- package/dist/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-EAsCxrDo.mjs} +2 -1
- package/package.json +4 -4
- package/src/Discord.ts +7 -4
- package/src/internal/gateway.test.ts +87 -1
- package/src/internal/gateway.ts +32 -22
- package/src/internal/rest.ts +1 -1
- package/dist/gateway-C_Ae1HYB.d.mts.map +0 -1
- package/dist/magic-string.es-CUAnjKYC.mjs +0 -1014
- package/dist/magic-string.es-CUAnjKYC.mjs.map +0 -1
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region \0rolldown/runtime.js
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
3
4
|
var __exportAll = (all, no_symbols) => {
|
|
4
5
|
let target = {};
|
|
5
6
|
for (var name in all) __defProp(target, name, {
|
|
@@ -10,4 +11,4 @@ var __exportAll = (all, no_symbols) => {
|
|
|
10
11
|
return target;
|
|
11
12
|
};
|
|
12
13
|
//#endregion
|
|
13
|
-
export { __exportAll as t };
|
|
14
|
+
export { __exportAll as n, __commonJSMin as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/discord",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Discord provider for @effect-uai/core (Messenger capability over the gateway and REST API).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"effect": "4.0.
|
|
47
|
+
"effect": "4.0.1",
|
|
48
48
|
"typescript": "^7.0.2",
|
|
49
|
-
"@effect-uai/core": "0.
|
|
49
|
+
"@effect-uai/core": "0.18.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@effect-uai/core": ">=0.15.0 <1",
|
|
53
|
-
"effect": ">=4.0.
|
|
53
|
+
"effect": ">=4.0.1 <5.0.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsdown",
|
package/src/Discord.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
type Cause,
|
|
4
4
|
Context,
|
|
5
5
|
Effect,
|
|
6
|
-
Encoding,
|
|
7
6
|
Layer,
|
|
8
7
|
Match,
|
|
9
8
|
Option,
|
|
@@ -13,7 +12,9 @@ import {
|
|
|
13
12
|
type Scope,
|
|
14
13
|
Stream,
|
|
15
14
|
} from "effect"
|
|
16
|
-
import {
|
|
15
|
+
import { Base64 } from "effect/encoding"
|
|
16
|
+
import { HttpClient } from "effect/http"
|
|
17
|
+
import * as Socket from "effect/socket/Socket"
|
|
17
18
|
import type { MediaSource } from "@effect-uai/core/Media"
|
|
18
19
|
import {
|
|
19
20
|
CurrentConversation,
|
|
@@ -208,11 +209,13 @@ export const make = (
|
|
|
208
209
|
|
|
209
210
|
// -- inbound -----------------------------------------------------------
|
|
210
211
|
|
|
212
|
+
// The gateway takes its token in the identify payload, not a header, so
|
|
213
|
+
// every runtime's global `WebSocket` is enough.
|
|
211
214
|
const session = yield* Gateway.connect({
|
|
212
215
|
token: cfg.token,
|
|
213
216
|
intents: cfg.intents ?? defaultIntents,
|
|
214
217
|
url,
|
|
215
|
-
})
|
|
218
|
+
}).pipe(Effect.provide(Socket.layerWebSocketConstructorGlobal))
|
|
216
219
|
const toEvents = Events.toEvents(session.bot)
|
|
217
220
|
const inbox = yield* Queue.unbounded<InboundEvent, MessengerError.MessengerError | Cause.Done>()
|
|
218
221
|
|
|
@@ -280,7 +283,7 @@ export const make = (
|
|
|
280
283
|
Match.tag("url", ({ url }) => sendUrl(at, fields, body, url)),
|
|
281
284
|
Match.tag("bytes", ({ bytes, mimeType }) => send(bytes, mimeType)),
|
|
282
285
|
Match.tag("base64", ({ base64, mimeType }) =>
|
|
283
|
-
Effect.fromResult(
|
|
286
|
+
Effect.fromResult(Base64.decode(base64)).pipe(
|
|
284
287
|
Effect.mapError(invalidMedia),
|
|
285
288
|
Effect.flatMap((bytes) => send(bytes, mimeType)),
|
|
286
289
|
),
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { it as effect } from "@effect/vitest"
|
|
2
|
+
import { Effect, Layer, Queue, Redacted } from "effect"
|
|
3
|
+
import * as Socket from "effect/socket/Socket"
|
|
4
|
+
import * as FakeWebSocket from "@effect-uai/core/testing/FakeWebSocket"
|
|
1
5
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { classifyClose, closeReason } from "./gateway.js"
|
|
6
|
+
import { classifyClose, closeReason, connect } from "./gateway.js"
|
|
3
7
|
|
|
4
8
|
describe("classifyClose", () => {
|
|
5
9
|
it("never retries a close reconnecting cannot fix", () => {
|
|
@@ -25,3 +29,85 @@ describe("closeReason", () => {
|
|
|
25
29
|
expect(closeReason(1006, "connection reset")).toBe("connection reset")
|
|
26
30
|
})
|
|
27
31
|
})
|
|
32
|
+
|
|
33
|
+
const cfg = { token: Redacted.make("token"), intents: 0, url: "wss://gateway.test" }
|
|
34
|
+
|
|
35
|
+
// Long enough that no heartbeat lands mid-test.
|
|
36
|
+
const HELLO = { op: 10, d: { heartbeat_interval: 3_600_000 } }
|
|
37
|
+
|
|
38
|
+
const READY = {
|
|
39
|
+
op: 0,
|
|
40
|
+
t: "READY",
|
|
41
|
+
s: 1,
|
|
42
|
+
d: {
|
|
43
|
+
user: { id: "1", username: "bot" },
|
|
44
|
+
session_id: "session",
|
|
45
|
+
resume_gateway_url: "wss://resume.test",
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Identify gets READY, resume gets RESUMED.
|
|
50
|
+
const answers: Record<number, ReadonlyArray<unknown>> = {
|
|
51
|
+
2: [READY],
|
|
52
|
+
6: [{ op: 0, t: "RESUMED", s: 2 }],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const gateway = FakeWebSocket.make({
|
|
56
|
+
greeting: [HELLO],
|
|
57
|
+
reply: (frame) => Effect.succeed(answers[(frame as { op: number }).op] ?? []),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/** One fake per connection, in dial order; the last one takes any extra dials. */
|
|
61
|
+
const dialing = (servers: ReadonlyArray<FakeWebSocket.FakeWebSocketServer>) => {
|
|
62
|
+
const urls: Array<string> = []
|
|
63
|
+
const layer = Layer.succeed(Socket.WebSocketConstructor)((url, protocols) => {
|
|
64
|
+
const server = servers[Math.min(urls.length, servers.length - 1)]!
|
|
65
|
+
urls.push(url)
|
|
66
|
+
return server.connect(url, protocols)
|
|
67
|
+
})
|
|
68
|
+
return { urls, layer }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("gateway close handling", () => {
|
|
72
|
+
effect.live.each([1000, 4000])("resumes on the resume host after a %i close", (code) =>
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const first = yield* gateway
|
|
75
|
+
const second = yield* gateway
|
|
76
|
+
const { urls, layer } = dialing([first, second])
|
|
77
|
+
const session = yield* connect(cfg).pipe(Effect.provide(layer))
|
|
78
|
+
expect(session.bot.username).toBe("bot")
|
|
79
|
+
|
|
80
|
+
yield* first.close(code)
|
|
81
|
+
yield* second.awaitSent(1)
|
|
82
|
+
|
|
83
|
+
expect(urls[1]).toMatch(/^wss:\/\/resume\.test\//)
|
|
84
|
+
expect(yield* second.sent).toEqual([
|
|
85
|
+
{ op: 6, d: { token: "token", session_id: "session", seq: 1 } },
|
|
86
|
+
])
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
effect.live("fails the dispatches on a fatal close after READY", () =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
const server = yield* gateway
|
|
93
|
+
const session = yield* connect(cfg).pipe(Effect.provide(dialing([server]).layer))
|
|
94
|
+
|
|
95
|
+
yield* server.close(4014)
|
|
96
|
+
const error = yield* Queue.take(session.dispatches).pipe(Effect.flip)
|
|
97
|
+
|
|
98
|
+
expect(error).toMatchObject({ _tag: "MessengerTransportClosed", raw: { code: 4014 } })
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
effect.live("fails the connect on a fatal close before READY", () =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const server = yield* FakeWebSocket.make({ greeting: [HELLO] })
|
|
105
|
+
yield* server.awaitSent(1).pipe(Effect.andThen(server.close(4004)), Effect.forkScoped)
|
|
106
|
+
|
|
107
|
+
const error = yield* connect(cfg).pipe(Effect.provide(dialing([server]).layer), Effect.flip)
|
|
108
|
+
|
|
109
|
+
expect(error._tag).toBe("MessengerConnectFailed")
|
|
110
|
+
expect(error.reason).toContain("authentication failed")
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
})
|
package/src/internal/gateway.ts
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
Schedule,
|
|
12
12
|
Schema,
|
|
13
13
|
type Scope,
|
|
14
|
+
Stream,
|
|
14
15
|
} from "effect"
|
|
15
|
-
import * as Socket from "effect/
|
|
16
|
+
import * as Socket from "effect/socket/Socket"
|
|
16
17
|
import * as MessengerError from "@effect-uai/core/MessengerError"
|
|
18
|
+
import { isCleanClose } from "@effect-uai/core/WebSocketSession"
|
|
17
19
|
import * as Events from "./events.js"
|
|
18
20
|
import { provider } from "./rest.js"
|
|
19
21
|
|
|
@@ -140,6 +142,14 @@ const socketReason = (error: Socket.SocketError): string =>
|
|
|
140
142
|
? (error.reason.closeReason ?? error.reason.message)
|
|
141
143
|
: error.reason.message
|
|
142
144
|
|
|
145
|
+
// Effect's `Socket` fails on every close. A clean one is only ever our own
|
|
146
|
+
// teardown; anything else carries the close code the classifier reads.
|
|
147
|
+
const endedBy = (error: Socket.SocketError): Ended => {
|
|
148
|
+
if (isCleanClose(error)) return { action: "resume", code: 1000, reason: "closed" }
|
|
149
|
+
const code = socketCode(error)
|
|
150
|
+
return { action: classifyClose(code), code, reason: closeReason(code, socketReason(error)) }
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
// Discord asks for a random 1-5 s pause before identifying on a fresh session.
|
|
144
154
|
const freshSessionDelay = Effect.sync(() => Duration.millis(1000 + Math.random() * 4000))
|
|
145
155
|
|
|
@@ -159,7 +169,11 @@ const identity = { os: "effect-uai", browser: "effect-uai", device: "effect-uai"
|
|
|
159
169
|
*/
|
|
160
170
|
export const connect = (
|
|
161
171
|
cfg: Config,
|
|
162
|
-
): Effect.Effect<
|
|
172
|
+
): Effect.Effect<
|
|
173
|
+
Session,
|
|
174
|
+
MessengerError.MessengerConnectFailed,
|
|
175
|
+
Scope.Scope | Socket.WebSocketConstructor
|
|
176
|
+
> =>
|
|
163
177
|
Effect.gen(function* () {
|
|
164
178
|
const dispatches = yield* Queue.unbounded<
|
|
165
179
|
Incoming,
|
|
@@ -168,6 +182,8 @@ export const connect = (
|
|
|
168
182
|
const ready = yield* Deferred.make<Events.BotIdentity, MessengerError.MessengerConnectFailed>()
|
|
169
183
|
const resume = yield* Ref.make(Option.none<Resume>())
|
|
170
184
|
const attempts = yield* Ref.make(0)
|
|
185
|
+
// Outlives a connection: a resume replays from the last sequence seen.
|
|
186
|
+
const seq = yield* Ref.make(Option.none<number>())
|
|
171
187
|
|
|
172
188
|
// -- one connection ----------------------------------------------------
|
|
173
189
|
|
|
@@ -176,16 +192,16 @@ export const connect = (
|
|
|
176
192
|
// A resumable session has its own host; a fresh one starts at the URL
|
|
177
193
|
// `GET /gateway/bot` gave us.
|
|
178
194
|
const host = Option.match(from, { onNone: () => cfg.url, onSome: (r: Resume) => r.url })
|
|
179
|
-
const socket = yield* Socket.makeWebSocket(`${host}/?v=10&encoding=json
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
const socket = yield* Socket.makeWebSocket(`${host}/?v=10&encoding=json`)
|
|
196
|
+
const writer = yield* socket.writer
|
|
197
|
+
// The writer waits for the next connection rather than failing, so a
|
|
198
|
+
// beat or handshake after this one's reader ended is dropped here.
|
|
199
|
+
const closed = yield* Ref.make(false)
|
|
200
|
+
const write = (chunk: string | Socket.CloseEvent) =>
|
|
201
|
+
Effect.flatMap(Ref.get(closed), (isClosed) =>
|
|
202
|
+
isClosed ? Effect.void : writer.write(chunk),
|
|
203
|
+
)
|
|
187
204
|
const send = (payload: unknown) => write(JSON.stringify(payload))
|
|
188
|
-
const seq = yield* Ref.make(Option.none<number>())
|
|
189
205
|
const acked = yield* Ref.make(true)
|
|
190
206
|
// Set by op 9 with `d: false`, the one case the close code cannot express.
|
|
191
207
|
const requested = yield* Ref.make(Option.none<CloseAction>())
|
|
@@ -301,18 +317,12 @@ export const connect = (
|
|
|
301
317
|
)
|
|
302
318
|
}).pipe(Effect.ignore)
|
|
303
319
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
320
|
+
const ended = yield* Stream.fromPull(Socket.readerString(socket)).pipe(
|
|
321
|
+
Stream.scoped,
|
|
322
|
+
Stream.runForEach(handle),
|
|
323
|
+
Effect.ensuring(Ref.set(closed, true)),
|
|
307
324
|
Effect.as<Ended>({ action: "resume", code: 1000, reason: "closed" }),
|
|
308
|
-
Effect.catch((error: Socket.SocketError) =>
|
|
309
|
-
const code = socketCode(error)
|
|
310
|
-
return Effect.succeed<Ended>({
|
|
311
|
-
action: classifyClose(code),
|
|
312
|
-
code,
|
|
313
|
-
reason: closeReason(code, socketReason(error)),
|
|
314
|
-
})
|
|
315
|
-
}),
|
|
325
|
+
Effect.catch((error: Socket.SocketError) => Effect.succeed(endedBy(error))),
|
|
316
326
|
)
|
|
317
327
|
const override = yield* Ref.get(requested)
|
|
318
328
|
return Option.match(override, {
|
package/src/internal/rest.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Data, Duration, Effect, Redacted, Schema } from "effect"
|
|
2
|
-
import { HttpClient, HttpClientRequest } from "effect/
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
3
3
|
import * as MessengerError from "@effect-uai/core/MessengerError"
|
|
4
4
|
import * as Multipart from "@effect-uai/core/Multipart"
|
|
5
5
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-C_Ae1HYB.d.mts","names":[],"sources":["../src/internal/gateway.ts"],"mappings":";;;;cAmCa,OAAK,OAAA;;;;;;;;KAKN,eAAe,MAAM;;cAGpB,aAAW,OAAA;gBAAX,OAAA;;;KAoBD;;;;;cAmBC,gBAAa,iBAAmB;;cAchC,cAAW,cAAgB;KAY5B;WACD,OAAO,SAAS;WAChB;;WAEA;;;KAIC;WACD,UAAU;WACV;;KAGC;WACD,KAAK;;;;;WAKL,YAAY,MAAM,MAAM,UAAU,eAAe,iBAAiB,MAAM;;;;;;;;;;cAgCtE,UAAO,KACb,WACJ,OAAO,OAAO,SAAS,eAAe,wBAAwB,MAAM"}
|