dfx 0.42.1 → 0.42.3
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/Cache/driver.js.map +1 -1
- package/Cache/memory.js.map +1 -1
- package/Cache/memoryTTL.js.map +1 -1
- package/Cache/prelude.js.map +1 -1
- package/Cache.js.map +1 -1
- package/DiscordConfig.js.map +1 -1
- package/DiscordGateway/DiscordWS.js +1 -1
- package/DiscordGateway/DiscordWS.js.map +1 -1
- package/DiscordGateway/Shard/heartbeats.js.map +1 -1
- package/DiscordGateway/Shard/identify.js.map +1 -1
- package/DiscordGateway/Shard/invalidSession.js.map +1 -1
- package/DiscordGateway/Shard/sendEvents.js.map +1 -1
- package/DiscordGateway/Shard/utils.js.map +1 -1
- package/DiscordGateway/Shard.js.map +1 -1
- package/DiscordGateway/ShardStore.js.map +1 -1
- package/DiscordGateway/Sharder.js.map +1 -1
- package/DiscordGateway/WS.d.ts +3 -3
- package/DiscordGateway/WS.js +25 -18
- package/DiscordGateway/WS.js.map +1 -1
- package/DiscordGateway.js.map +1 -1
- package/DiscordREST/types.js.map +1 -1
- package/DiscordREST/utils.js.map +1 -1
- package/DiscordREST.js.map +1 -1
- package/Helpers/flags.js.map +1 -1
- package/Helpers/intents.js.map +1 -1
- package/Helpers/interactions.js.map +1 -1
- package/Helpers/members.js.map +1 -1
- package/Helpers/permissions.js.map +1 -1
- package/Helpers/ui.js.map +1 -1
- package/Interactions/context.js.map +1 -1
- package/Interactions/definitions.js.map +1 -1
- package/Interactions/gateway.js.map +1 -1
- package/Interactions/handlers.js.map +1 -1
- package/Interactions/index.js.map +1 -1
- package/Interactions/utils.js.map +1 -1
- package/Interactions/webhook.js.map +1 -1
- package/Log.js.map +1 -1
- package/RateLimit/memory.js.map +1 -1
- package/RateLimit/utils.js.map +1 -1
- package/RateLimit.js.map +1 -1
- package/_common.js.map +1 -1
- package/gateway.js.map +1 -1
- package/global.js.map +1 -1
- package/index.js.map +1 -1
- package/package.json +50 -52
- package/src/Cache/driver.ts +31 -0
- package/src/Cache/memory.ts +76 -0
- package/src/Cache/memoryTTL.ts +201 -0
- package/src/Cache/prelude.ts +215 -0
- package/src/Cache.ts +140 -0
- package/src/DiscordConfig.ts +48 -0
- package/src/DiscordGateway/DiscordWS.ts +74 -0
- package/src/DiscordGateway/Shard/heartbeats.ts +42 -0
- package/src/DiscordGateway/Shard/identify.ts +52 -0
- package/src/DiscordGateway/Shard/invalidSession.ts +10 -0
- package/src/DiscordGateway/Shard/sendEvents.ts +37 -0
- package/src/DiscordGateway/Shard/utils.ts +14 -0
- package/src/DiscordGateway/Shard.ts +152 -0
- package/src/DiscordGateway/ShardStore.ts +33 -0
- package/src/DiscordGateway/Sharder.ts +102 -0
- package/src/DiscordGateway/WS.ts +122 -0
- package/src/DiscordGateway.ts +43 -0
- package/src/DiscordREST/types.ts +13 -0
- package/src/DiscordREST/utils.ts +33 -0
- package/src/DiscordREST.ts +203 -0
- package/src/Helpers/flags.ts +68 -0
- package/src/Helpers/intents.ts +34 -0
- package/src/Helpers/interactions.ts +229 -0
- package/src/Helpers/members.ts +14 -0
- package/src/Helpers/permissions.ts +140 -0
- package/src/Helpers/ui.ts +103 -0
- package/src/Interactions/context.ts +132 -0
- package/src/Interactions/definitions.ts +309 -0
- package/src/Interactions/gateway.ts +71 -0
- package/src/Interactions/handlers.ts +130 -0
- package/src/Interactions/index.ts +108 -0
- package/src/Interactions/utils.ts +81 -0
- package/src/Interactions/webhook.ts +110 -0
- package/src/Log.ts +17 -0
- package/src/RateLimit/memory.ts +57 -0
- package/src/RateLimit/utils.ts +27 -0
- package/src/RateLimit.ts +69 -0
- package/src/_common.ts +43 -0
- package/src/gateway.ts +38 -0
- package/src/global.ts +45 -0
- package/src/index.ts +20 -0
- package/src/package.json +52 -0
- package/src/types.ts +6368 -0
- package/src/utils/effect.ts +0 -0
- package/src/utils/hub.ts +47 -0
- package/src/utils/json.d.ts +1 -0
- package/src/utils/tsplus.ts +10 -0
- package/src/webhooks.ts +41 -0
- package/tsconfig.json +23 -0
- package/tsplus.config.json +8 -0
- package/types.js.map +1 -1
- package/utils/effect.js.map +1 -1
- package/utils/hub.js.map +1 -1
- package/utils/tsplus.js.map +1 -1
- package/webhooks.js.map +1 -1
package/src/Cache.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { CacheDriver, ParentCacheDriver } from "./Cache/driver.js"
|
|
2
|
+
|
|
3
|
+
export * from "./Cache/driver.js"
|
|
4
|
+
export {
|
|
5
|
+
create as memoryDriver,
|
|
6
|
+
createWithParent as memoryParentDriver,
|
|
7
|
+
} from "./Cache/memory.js"
|
|
8
|
+
export {
|
|
9
|
+
create as memoryTTLDriver,
|
|
10
|
+
createWithParent as memoryTTLParentDriver,
|
|
11
|
+
} from "./Cache/memoryTTL.js"
|
|
12
|
+
|
|
13
|
+
export type ParentCacheOp<T> =
|
|
14
|
+
| { op: "create"; parentId: string; resourceId: string; resource: T }
|
|
15
|
+
| { op: "update"; parentId: string; resourceId: string; resource: T }
|
|
16
|
+
| { op: "delete"; parentId: string; resourceId: string }
|
|
17
|
+
| { op: "parentDelete"; parentId: string }
|
|
18
|
+
|
|
19
|
+
export type CacheOp<T> =
|
|
20
|
+
| { op: "create"; resourceId: string; resource: T }
|
|
21
|
+
| { op: "update"; resourceId: string; resource: T }
|
|
22
|
+
| { op: "delete"; resourceId: string }
|
|
23
|
+
|
|
24
|
+
export const makeWithParent = <EOps, EDriver, EMiss, EPMiss, A>({
|
|
25
|
+
driver,
|
|
26
|
+
ops = Stream.empty,
|
|
27
|
+
id,
|
|
28
|
+
onMiss,
|
|
29
|
+
onParentMiss,
|
|
30
|
+
}: {
|
|
31
|
+
driver: ParentCacheDriver<EDriver, A>
|
|
32
|
+
ops?: Stream<never, EOps, ParentCacheOp<A>>
|
|
33
|
+
id: (_: A) => Effect<never, EMiss, readonly [parentId: string, id: string]>
|
|
34
|
+
onMiss: (parentId: string, id: string) => Effect<never, EMiss, A>
|
|
35
|
+
onParentMiss: (
|
|
36
|
+
parentId: string,
|
|
37
|
+
) => Effect<never, EPMiss, [id: string, resource: A][]>
|
|
38
|
+
}) => {
|
|
39
|
+
const sync = ops.tap((op): Effect<never, EDriver, void> => {
|
|
40
|
+
switch (op.op) {
|
|
41
|
+
case "create":
|
|
42
|
+
case "update":
|
|
43
|
+
return driver.set(op.parentId, op.resourceId, op.resource)
|
|
44
|
+
|
|
45
|
+
case "delete":
|
|
46
|
+
return driver.delete(op.parentId, op.resourceId)
|
|
47
|
+
|
|
48
|
+
case "parentDelete":
|
|
49
|
+
return driver.parentDelete(op.parentId)
|
|
50
|
+
}
|
|
51
|
+
}).runDrain
|
|
52
|
+
|
|
53
|
+
const get = (parentId: string, id: string) =>
|
|
54
|
+
driver
|
|
55
|
+
.get(parentId, id)
|
|
56
|
+
.someOrElseEffect(() =>
|
|
57
|
+
onMiss(parentId, id).tap(a => driver.set(parentId, id, a)),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const put = (_: A) =>
|
|
61
|
+
id(_).flatMap(([parentId, id]) => driver.set(parentId, id, _))
|
|
62
|
+
|
|
63
|
+
const update = <R, E>(
|
|
64
|
+
parentId: string,
|
|
65
|
+
id: string,
|
|
66
|
+
f: (_: A) => Effect<R, E, A>,
|
|
67
|
+
) =>
|
|
68
|
+
get(parentId, id)
|
|
69
|
+
.flatMap(f)
|
|
70
|
+
.tap(_ => driver.set(parentId, id, _))
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...driver,
|
|
74
|
+
|
|
75
|
+
get,
|
|
76
|
+
put,
|
|
77
|
+
update,
|
|
78
|
+
|
|
79
|
+
getForParent: (parentId: string) =>
|
|
80
|
+
driver.getForParent(parentId).someOrElseEffect(() =>
|
|
81
|
+
onParentMiss(parentId)
|
|
82
|
+
.tap(entries =>
|
|
83
|
+
Effect.allPar(
|
|
84
|
+
entries.map(([id, a]) => driver.set(parentId, id, a)),
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
.map(entries => new Map(entries) as ReadonlyMap<string, A>),
|
|
88
|
+
),
|
|
89
|
+
|
|
90
|
+
run: sync.zipParRight(driver.run),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const make = <EOps, EDriver, EMiss, A>({
|
|
95
|
+
driver,
|
|
96
|
+
ops = Stream.empty,
|
|
97
|
+
id,
|
|
98
|
+
onMiss,
|
|
99
|
+
}: {
|
|
100
|
+
driver: CacheDriver<EDriver, A>
|
|
101
|
+
ops?: Stream<never, EOps, CacheOp<A>>
|
|
102
|
+
id: (_: A) => string
|
|
103
|
+
onMiss: (id: string) => Effect<never, EMiss, A>
|
|
104
|
+
}) => {
|
|
105
|
+
const sync = ops.tap((op): Effect<never, EDriver, void> => {
|
|
106
|
+
switch (op.op) {
|
|
107
|
+
case "create":
|
|
108
|
+
case "update":
|
|
109
|
+
return driver.set(op.resourceId, op.resource)
|
|
110
|
+
|
|
111
|
+
case "delete":
|
|
112
|
+
return driver.delete(op.resourceId)
|
|
113
|
+
}
|
|
114
|
+
}).runDrain
|
|
115
|
+
|
|
116
|
+
const get = (id: string) =>
|
|
117
|
+
driver
|
|
118
|
+
.get(id)
|
|
119
|
+
.someOrElseEffect(() => onMiss(id).tap(a => driver.set(id, a)))
|
|
120
|
+
|
|
121
|
+
const put = (_: A) => driver.set(id(_), _)
|
|
122
|
+
|
|
123
|
+
const update = <R, E>(id: string, f: (_: A) => Effect<R, E, A>) =>
|
|
124
|
+
get(id)
|
|
125
|
+
.flatMap(f)
|
|
126
|
+
.tap(_ => driver.set(id, _))
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...driver,
|
|
130
|
+
get,
|
|
131
|
+
put,
|
|
132
|
+
update,
|
|
133
|
+
run: sync.zipParRight(driver.run),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class CacheMissError {
|
|
138
|
+
readonly _tag = "CacheMissError"
|
|
139
|
+
constructor(readonly cacheName: string, readonly id: string) {}
|
|
140
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const VERSION = 10
|
|
2
|
+
|
|
3
|
+
export interface DiscordConfig {
|
|
4
|
+
token: ConfigSecret
|
|
5
|
+
rest: {
|
|
6
|
+
baseUrl: string
|
|
7
|
+
globalRateLimit: {
|
|
8
|
+
limit: number
|
|
9
|
+
window: Duration
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
gateway: {
|
|
13
|
+
intents: number
|
|
14
|
+
presence?: Discord.UpdatePresence
|
|
15
|
+
shardCount?: number
|
|
16
|
+
|
|
17
|
+
identifyRateLimit: readonly [window: number, limit: number]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const DiscordConfig = Tag<DiscordConfig>()
|
|
21
|
+
|
|
22
|
+
export interface MakeOpts {
|
|
23
|
+
token: ConfigSecret
|
|
24
|
+
rest?: Partial<DiscordConfig["rest"]>
|
|
25
|
+
gateway?: Partial<DiscordConfig["gateway"]>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const make = ({ token, rest, gateway }: MakeOpts): DiscordConfig => ({
|
|
29
|
+
token,
|
|
30
|
+
rest: {
|
|
31
|
+
baseUrl: `https://discord.com/api/v${VERSION}`,
|
|
32
|
+
...(rest ?? {}),
|
|
33
|
+
globalRateLimit: {
|
|
34
|
+
limit: 50,
|
|
35
|
+
window: Duration.seconds(1),
|
|
36
|
+
...(rest?.globalRateLimit ?? {}),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
gateway: {
|
|
40
|
+
intents: Discord.GatewayIntents.GUILDS,
|
|
41
|
+
identifyRateLimit: [5000, 1],
|
|
42
|
+
...(gateway ?? {}),
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export const makeLayer = flow(make, _ => Layer.succeed(DiscordConfig, _))
|
|
47
|
+
export const makeFromConfig = (_: Config.Wrap<MakeOpts>) =>
|
|
48
|
+
Config.unwrap(_).config.map(make).toLayer(DiscordConfig)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LiveWS,
|
|
3
|
+
Reconnect,
|
|
4
|
+
WS,
|
|
5
|
+
WebSocketCloseError,
|
|
6
|
+
WebSocketError,
|
|
7
|
+
} from "dfx/DiscordGateway/WS"
|
|
8
|
+
import WebSocket from "isomorphic-ws"
|
|
9
|
+
|
|
10
|
+
export type Message = Discord.GatewayPayload | Reconnect
|
|
11
|
+
|
|
12
|
+
export interface OpenOpts {
|
|
13
|
+
url?: string
|
|
14
|
+
version?: number
|
|
15
|
+
encoding?: DiscordWSCodec
|
|
16
|
+
outbound: Effect<never, never, Message>
|
|
17
|
+
onReconnect?: Effect<never, never, void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DiscordWSCodec {
|
|
21
|
+
type: "json" | "etf"
|
|
22
|
+
encode: (p: Discord.GatewayPayload) => string
|
|
23
|
+
decode: (p: WebSocket.Data) => Discord.GatewayPayload
|
|
24
|
+
}
|
|
25
|
+
export const DiscordWSCodec = Tag<DiscordWSCodec>()
|
|
26
|
+
export const LiveJsonDiscordWSCodec = Layer.succeed(DiscordWSCodec, {
|
|
27
|
+
type: "json",
|
|
28
|
+
encode: p => JSON.stringify(p),
|
|
29
|
+
decode: p => JSON.parse(p.toString("utf8")),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const make = Do($ => {
|
|
33
|
+
const ws = $(WS)
|
|
34
|
+
const encoding = $(DiscordWSCodec)
|
|
35
|
+
|
|
36
|
+
const connect = ({
|
|
37
|
+
url = "wss://gateway.discord.gg/",
|
|
38
|
+
version = 10,
|
|
39
|
+
outbound,
|
|
40
|
+
onReconnect,
|
|
41
|
+
}: OpenOpts) =>
|
|
42
|
+
Do($ => {
|
|
43
|
+
const urlRef = $(
|
|
44
|
+
Ref.make(`${url}?v=${version}&encoding=${encoding.type}`),
|
|
45
|
+
)
|
|
46
|
+
const setUrl = (url: string) =>
|
|
47
|
+
urlRef.set(`${url}?v=${version}&encoding=${encoding.type}`)
|
|
48
|
+
const takeOutbound = outbound.map(a =>
|
|
49
|
+
a === Reconnect ? a : encoding.encode(a),
|
|
50
|
+
)
|
|
51
|
+
const socket = $(ws.connect(urlRef, takeOutbound, onReconnect))
|
|
52
|
+
const take = socket.take.map(encoding.decode)
|
|
53
|
+
|
|
54
|
+
const run = socket.run.retry(
|
|
55
|
+
Schedule.exponential(Duration.seconds(0.5)).whileInput(
|
|
56
|
+
(_: WebSocketError | WebSocketCloseError) =>
|
|
57
|
+
(_._tag === "WebSocketCloseError" && _.code < 2000) ||
|
|
58
|
+
(_._tag === "WebSocketError" && _.reason === "open-timeout"),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
run,
|
|
64
|
+
take,
|
|
65
|
+
setUrl,
|
|
66
|
+
} as const
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return { connect } as const
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export interface DiscordWS extends Effect.Success<typeof make> {}
|
|
73
|
+
export const DiscordWS = Tag<DiscordWS>()
|
|
74
|
+
export const LiveDiscordWS = LiveWS >> make.toLayer(DiscordWS)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { millis } from "@effect/data/Duration"
|
|
2
|
+
import * as SendEvents from "./sendEvents.js"
|
|
3
|
+
import * as DiscordWS from "dfx/DiscordGateway/DiscordWS"
|
|
4
|
+
import { Reconnect } from "../WS.js"
|
|
5
|
+
|
|
6
|
+
const payload = (ref: Ref<boolean>, seqRef: Ref<Maybe<number>>) =>
|
|
7
|
+
seqRef.get
|
|
8
|
+
.map(a => SendEvents.heartbeat(a.getOrNull))
|
|
9
|
+
.tap(() => ref.set(false))
|
|
10
|
+
|
|
11
|
+
const payloadOrReconnect = (ref: Ref<boolean>, seqRef: Ref<Maybe<number>>) =>
|
|
12
|
+
ref.get.flatMap(
|
|
13
|
+
(acked): Effect<never, never, DiscordWS.Message> =>
|
|
14
|
+
acked ? payload(ref, seqRef) : Effect.succeed(Reconnect),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export const send = (
|
|
18
|
+
hellos: Dequeue<Discord.GatewayPayload>,
|
|
19
|
+
acks: Dequeue<Discord.GatewayPayload>,
|
|
20
|
+
seqRef: Ref<Maybe<number>>,
|
|
21
|
+
send: (p: DiscordWS.Message) => Effect<never, never, boolean>,
|
|
22
|
+
) =>
|
|
23
|
+
Do($ => {
|
|
24
|
+
const ackedRef = $(Ref.make(true))
|
|
25
|
+
|
|
26
|
+
const heartbeats = hellos
|
|
27
|
+
.take()
|
|
28
|
+
.tap(() => ackedRef.set(true))
|
|
29
|
+
.foreverSwitch(p =>
|
|
30
|
+
payloadOrReconnect(ackedRef, seqRef)
|
|
31
|
+
.tap(send)
|
|
32
|
+
.schedule(
|
|
33
|
+
Schedule.duration(
|
|
34
|
+
millis(p.d!.heartbeat_interval * Math.random()),
|
|
35
|
+
).andThen(Schedule.fixed(millis(p.d!.heartbeat_interval))),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const run = acks.take().tap(() => ackedRef.set(true)).forever
|
|
40
|
+
|
|
41
|
+
return $(run.zipParLeft(heartbeats))
|
|
42
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as SendEvents from "./sendEvents.js"
|
|
2
|
+
import * as OS from "os"
|
|
3
|
+
|
|
4
|
+
export interface Options {
|
|
5
|
+
token: string
|
|
6
|
+
intents: number
|
|
7
|
+
shard: [number, number]
|
|
8
|
+
presence?: Discord.UpdatePresence
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Requirements {
|
|
12
|
+
latestReady: Ref<Maybe<Discord.ReadyEvent>>
|
|
13
|
+
latestSequence: Ref<Maybe<number>>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const identify = ({ token, intents, shard, presence }: Options) =>
|
|
17
|
+
SendEvents.identify({
|
|
18
|
+
token,
|
|
19
|
+
intents,
|
|
20
|
+
properties: {
|
|
21
|
+
os: OS.platform(),
|
|
22
|
+
browser: "dfx",
|
|
23
|
+
device: "dfx",
|
|
24
|
+
},
|
|
25
|
+
shard,
|
|
26
|
+
presence,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const resume = (token: string, ready: Discord.ReadyEvent, seq: number) =>
|
|
30
|
+
SendEvents.resume({
|
|
31
|
+
token,
|
|
32
|
+
session_id: ready.session_id,
|
|
33
|
+
seq,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const identifyOrResume = (
|
|
37
|
+
opts: Options,
|
|
38
|
+
ready: Ref<Maybe<Discord.ReadyEvent>>,
|
|
39
|
+
seq: Ref<Maybe<number>>,
|
|
40
|
+
) =>
|
|
41
|
+
Do($ => {
|
|
42
|
+
const readyEvent = $(ready.get)
|
|
43
|
+
const seqNumber = $(seq.get)
|
|
44
|
+
|
|
45
|
+
return Maybe.struct({
|
|
46
|
+
readyEvent,
|
|
47
|
+
seqNumber,
|
|
48
|
+
}).match(
|
|
49
|
+
() => identify(opts),
|
|
50
|
+
({ readyEvent, seqNumber }) => resume(opts.token, readyEvent, seqNumber),
|
|
51
|
+
)
|
|
52
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Message } from "../DiscordWS.js"
|
|
2
|
+
import { Reconnect } from "../WS.js"
|
|
3
|
+
|
|
4
|
+
export const fromPayload = (
|
|
5
|
+
p: Discord.GatewayPayload,
|
|
6
|
+
latestReady: Ref<Maybe<Discord.ReadyEvent>>,
|
|
7
|
+
) =>
|
|
8
|
+
(p.d ? Effect.unit() : latestReady.set(Maybe.none())).map(
|
|
9
|
+
(): Message => Reconnect,
|
|
10
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Discord } from "dfx/_common"
|
|
2
|
+
|
|
3
|
+
export const heartbeat = (d: Discord.Heartbeat): Discord.GatewayPayload => ({
|
|
4
|
+
op: Discord.GatewayOpcode.HEARTBEAT,
|
|
5
|
+
d,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export const identify = (d: Discord.Identify): Discord.GatewayPayload => ({
|
|
9
|
+
op: Discord.GatewayOpcode.IDENTIFY,
|
|
10
|
+
d,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const resume = (d: Discord.Resume): Discord.GatewayPayload => ({
|
|
14
|
+
op: Discord.GatewayOpcode.RESUME,
|
|
15
|
+
d,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const requestGuildMembers = (
|
|
19
|
+
d: Discord.RequestGuildMember,
|
|
20
|
+
): Discord.GatewayPayload => ({
|
|
21
|
+
op: Discord.GatewayOpcode.REQUEST_GUILD_MEMBERS,
|
|
22
|
+
d,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const voiceStateUpdate = (
|
|
26
|
+
d: Discord.UpdateVoiceState,
|
|
27
|
+
): Discord.GatewayPayload => ({
|
|
28
|
+
op: Discord.GatewayOpcode.VOICE_STATE_UPDATE,
|
|
29
|
+
d,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const presenceUpdate = (
|
|
33
|
+
d: Discord.UpdatePresence,
|
|
34
|
+
): Discord.GatewayPayload => ({
|
|
35
|
+
op: Discord.GatewayOpcode.PRESENCE_UPDATE,
|
|
36
|
+
d,
|
|
37
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const opCode =
|
|
2
|
+
<R, E>(source: Stream<R, E, Discord.GatewayPayload>) =>
|
|
3
|
+
<T = any>(code: Discord.GatewayOpcode) =>
|
|
4
|
+
source.filter((p): p is Discord.GatewayPayload<T> => p.op === code)
|
|
5
|
+
|
|
6
|
+
const maybeUpdateRef = <T>(
|
|
7
|
+
f: (p: Discord.GatewayPayload) => Maybe<T>,
|
|
8
|
+
ref: Ref<Maybe<T>>,
|
|
9
|
+
) => flow(f, o => o.match(Effect.unit, a => ref.set(Maybe.some(a))))
|
|
10
|
+
|
|
11
|
+
export const latest = <T>(f: (p: Discord.GatewayPayload) => Maybe<T>) =>
|
|
12
|
+
Ref.make<Maybe<T>>(Maybe.none()).map(
|
|
13
|
+
ref => [ref, maybeUpdateRef(f, ref)] as const,
|
|
14
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { DiscordConfig } from "dfx/DiscordConfig"
|
|
2
|
+
import { LiveRateLimiter, RateLimiter } from "dfx/RateLimit"
|
|
3
|
+
import { DiscordWS, LiveDiscordWS, Message } from "./DiscordWS.js"
|
|
4
|
+
import * as Heartbeats from "./Shard/heartbeats.js"
|
|
5
|
+
import * as Identify from "./Shard/identify.js"
|
|
6
|
+
import * as InvalidSession from "./Shard/invalidSession.js"
|
|
7
|
+
import * as Utils from "./Shard/utils.js"
|
|
8
|
+
import { Reconnect } from "./WS.js"
|
|
9
|
+
|
|
10
|
+
export const make = Do($ => {
|
|
11
|
+
const { token, gateway } = $(DiscordConfig)
|
|
12
|
+
const limiter = $(RateLimiter)
|
|
13
|
+
const dws = $(DiscordWS)
|
|
14
|
+
|
|
15
|
+
const connect = (
|
|
16
|
+
shard: [id: number, count: number],
|
|
17
|
+
hub: Hub<Discord.GatewayPayload<Discord.ReceiveEvent>>,
|
|
18
|
+
) =>
|
|
19
|
+
Do($ => {
|
|
20
|
+
const outboundQueue = $(Queue.unbounded<Message>())
|
|
21
|
+
const pendingQueue = $(Queue.unbounded<Message>())
|
|
22
|
+
const connecting = $(Ref.make(true))
|
|
23
|
+
const outbound = outboundQueue
|
|
24
|
+
.take()
|
|
25
|
+
.tap(() =>
|
|
26
|
+
limiter.maybeWait("dfx.shard.send", Duration.minutes(1), 120),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const send = (p: Message) =>
|
|
30
|
+
connecting.get.flatMap(_ =>
|
|
31
|
+
_ ? pendingQueue.offer(p) : outboundQueue.offer(p),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const prioritySend = (p: Message) => outboundQueue.offer(p)
|
|
35
|
+
|
|
36
|
+
const resume = connecting
|
|
37
|
+
.set(false)
|
|
38
|
+
.zipRight(pendingQueue.takeAll())
|
|
39
|
+
.tap(_ => outboundQueue.offerAll(_)).asUnit
|
|
40
|
+
|
|
41
|
+
const onReconnect = outboundQueue
|
|
42
|
+
.takeAll()
|
|
43
|
+
.tap(_ =>
|
|
44
|
+
pendingQueue.offerAll(
|
|
45
|
+
_.filter(
|
|
46
|
+
msg =>
|
|
47
|
+
msg !== Reconnect &&
|
|
48
|
+
msg.op !== Discord.GatewayOpcode.IDENTIFY &&
|
|
49
|
+
msg.op !== Discord.GatewayOpcode.RESUME &&
|
|
50
|
+
msg.op !== Discord.GatewayOpcode.HEARTBEAT,
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
.zipRight(connecting.set(true))
|
|
55
|
+
|
|
56
|
+
const socket = $(dws.connect({ outbound, onReconnect }))
|
|
57
|
+
|
|
58
|
+
const [latestReady, updateLatestReady] = $(
|
|
59
|
+
Utils.latest(p =>
|
|
60
|
+
Maybe.some(p)
|
|
61
|
+
.filter(
|
|
62
|
+
(p): p is Discord.GatewayPayload<Discord.ReadyEvent> =>
|
|
63
|
+
p.op === Discord.GatewayOpcode.DISPATCH && p.t === "READY",
|
|
64
|
+
)
|
|
65
|
+
.map(p => p.d!),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
const [latestSequence, updateLatestSequence] = $(
|
|
69
|
+
Utils.latest(p => Maybe.fromNullable(p.s)),
|
|
70
|
+
)
|
|
71
|
+
const maybeUpdateUrl = (p: Discord.GatewayPayload) =>
|
|
72
|
+
Maybe.some(p)
|
|
73
|
+
.filter(
|
|
74
|
+
(p): p is Discord.GatewayPayload<Discord.ReadyEvent> =>
|
|
75
|
+
p.op === Discord.GatewayOpcode.DISPATCH && p.t === "READY",
|
|
76
|
+
)
|
|
77
|
+
.map(p => p.d!)
|
|
78
|
+
.match(
|
|
79
|
+
() => Effect.unit(),
|
|
80
|
+
a => socket.setUrl(a.resume_gateway_url),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const hellos = $(Queue.unbounded<Discord.GatewayPayload>())
|
|
84
|
+
const acks = $(Queue.unbounded<Discord.GatewayPayload>())
|
|
85
|
+
|
|
86
|
+
// heartbeats
|
|
87
|
+
const heartbeats = Heartbeats.send(hellos, acks, latestSequence, send)
|
|
88
|
+
|
|
89
|
+
// identify
|
|
90
|
+
const identify = Identify.identifyOrResume(
|
|
91
|
+
{
|
|
92
|
+
token: token.value,
|
|
93
|
+
shard,
|
|
94
|
+
intents: gateway.intents,
|
|
95
|
+
presence: gateway.presence,
|
|
96
|
+
},
|
|
97
|
+
latestReady,
|
|
98
|
+
latestSequence,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const onPayload = (p: Discord.GatewayPayload) =>
|
|
102
|
+
Do($ => {
|
|
103
|
+
$(
|
|
104
|
+
updateLatestReady(p)
|
|
105
|
+
.zipPar(updateLatestSequence(p))
|
|
106
|
+
.zipPar(maybeUpdateUrl(p)),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
let effect = Effect.unit()
|
|
110
|
+
|
|
111
|
+
switch (p.op) {
|
|
112
|
+
case Discord.GatewayOpcode.HELLO:
|
|
113
|
+
effect = identify.tap(prioritySend).zipPar(hellos.offer(p))
|
|
114
|
+
break
|
|
115
|
+
case Discord.GatewayOpcode.HEARTBEAT_ACK:
|
|
116
|
+
effect = acks.offer(p)
|
|
117
|
+
break
|
|
118
|
+
case Discord.GatewayOpcode.INVALID_SESSION:
|
|
119
|
+
effect = InvalidSession.fromPayload(p, latestReady).tap(send)
|
|
120
|
+
break
|
|
121
|
+
case Discord.GatewayOpcode.DISPATCH:
|
|
122
|
+
if (p.t === "READY" || p.t === "RESUMED") {
|
|
123
|
+
effect = resume.zipRight(hub.publish(p))
|
|
124
|
+
} else {
|
|
125
|
+
effect = hub.publish(p)
|
|
126
|
+
}
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$(effect)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const run = socket.take
|
|
134
|
+
.flatMap(onPayload)
|
|
135
|
+
.forever.zipParLeft(heartbeats)
|
|
136
|
+
.zipParLeft(socket.run)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
run,
|
|
140
|
+
connected: connecting.get,
|
|
141
|
+
send: (p: Discord.GatewayPayload) => send(p),
|
|
142
|
+
reconnect: send(Reconnect),
|
|
143
|
+
} as const
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return { connect } as const
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
export interface Shard extends Effect.Success<typeof make> {}
|
|
150
|
+
export const Shard = Tag<Shard>()
|
|
151
|
+
export const LiveShard =
|
|
152
|
+
(LiveDiscordWS + LiveRateLimiter) >> make.toLayer(Shard)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ClaimIdContext {
|
|
2
|
+
sharderCount: number
|
|
3
|
+
totalCount: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ShardStore {
|
|
7
|
+
claimId: (ctx: ClaimIdContext) => Effect<never, never, Maybe<number>>
|
|
8
|
+
allClaimed: (totalCount: number) => Effect<never, never, boolean>
|
|
9
|
+
heartbeat?: (shardId: number) => Effect<never, never, void>
|
|
10
|
+
}
|
|
11
|
+
export const ShardStore = Tag<ShardStore>()
|
|
12
|
+
|
|
13
|
+
// Very basic shard id store, that does no health checks
|
|
14
|
+
const memoryStore = (): ShardStore => {
|
|
15
|
+
let currentId = 0
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
claimId: ({ totalCount }) =>
|
|
19
|
+
Effect.sync(() => {
|
|
20
|
+
if (currentId >= totalCount) {
|
|
21
|
+
return Maybe.none()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const id = currentId
|
|
25
|
+
currentId++
|
|
26
|
+
return Maybe.some(id)
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
allClaimed: totalCount => Effect.sync(() => currentId >= totalCount),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const LiveMemoryShardStore = Layer.sync(ShardStore, memoryStore)
|