dfx 0.42.2 → 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.
Files changed (97) hide show
  1. package/Cache/driver.js.map +1 -1
  2. package/Cache/memory.js.map +1 -1
  3. package/Cache/memoryTTL.js.map +1 -1
  4. package/Cache/prelude.js.map +1 -1
  5. package/Cache.js.map +1 -1
  6. package/DiscordConfig.js.map +1 -1
  7. package/DiscordGateway/DiscordWS.js.map +1 -1
  8. package/DiscordGateway/Shard/heartbeats.js.map +1 -1
  9. package/DiscordGateway/Shard/identify.js.map +1 -1
  10. package/DiscordGateway/Shard/invalidSession.js.map +1 -1
  11. package/DiscordGateway/Shard/sendEvents.js.map +1 -1
  12. package/DiscordGateway/Shard/utils.js.map +1 -1
  13. package/DiscordGateway/Shard.js.map +1 -1
  14. package/DiscordGateway/ShardStore.js.map +1 -1
  15. package/DiscordGateway/Sharder.js.map +1 -1
  16. package/DiscordGateway/WS.js.map +1 -1
  17. package/DiscordGateway.js.map +1 -1
  18. package/DiscordREST/types.js.map +1 -1
  19. package/DiscordREST/utils.js.map +1 -1
  20. package/DiscordREST.js.map +1 -1
  21. package/Helpers/flags.js.map +1 -1
  22. package/Helpers/intents.js.map +1 -1
  23. package/Helpers/interactions.js.map +1 -1
  24. package/Helpers/members.js.map +1 -1
  25. package/Helpers/permissions.js.map +1 -1
  26. package/Helpers/ui.js.map +1 -1
  27. package/Interactions/context.js.map +1 -1
  28. package/Interactions/definitions.js.map +1 -1
  29. package/Interactions/gateway.js.map +1 -1
  30. package/Interactions/handlers.js.map +1 -1
  31. package/Interactions/index.js.map +1 -1
  32. package/Interactions/utils.js.map +1 -1
  33. package/Interactions/webhook.js.map +1 -1
  34. package/Log.js.map +1 -1
  35. package/RateLimit/memory.js.map +1 -1
  36. package/RateLimit/utils.js.map +1 -1
  37. package/RateLimit.js.map +1 -1
  38. package/_common.js.map +1 -1
  39. package/gateway.js.map +1 -1
  40. package/global.js.map +1 -1
  41. package/index.js.map +1 -1
  42. package/package.json +50 -52
  43. package/src/Cache/driver.ts +31 -0
  44. package/src/Cache/memory.ts +76 -0
  45. package/src/Cache/memoryTTL.ts +201 -0
  46. package/src/Cache/prelude.ts +215 -0
  47. package/src/Cache.ts +140 -0
  48. package/src/DiscordConfig.ts +48 -0
  49. package/src/DiscordGateway/DiscordWS.ts +74 -0
  50. package/src/DiscordGateway/Shard/heartbeats.ts +42 -0
  51. package/src/DiscordGateway/Shard/identify.ts +52 -0
  52. package/src/DiscordGateway/Shard/invalidSession.ts +10 -0
  53. package/src/DiscordGateway/Shard/sendEvents.ts +37 -0
  54. package/src/DiscordGateway/Shard/utils.ts +14 -0
  55. package/src/DiscordGateway/Shard.ts +152 -0
  56. package/src/DiscordGateway/ShardStore.ts +33 -0
  57. package/src/DiscordGateway/Sharder.ts +102 -0
  58. package/src/DiscordGateway/WS.ts +122 -0
  59. package/src/DiscordGateway.ts +43 -0
  60. package/src/DiscordREST/types.ts +13 -0
  61. package/src/DiscordREST/utils.ts +33 -0
  62. package/src/DiscordREST.ts +203 -0
  63. package/src/Helpers/flags.ts +68 -0
  64. package/src/Helpers/intents.ts +34 -0
  65. package/src/Helpers/interactions.ts +229 -0
  66. package/src/Helpers/members.ts +14 -0
  67. package/src/Helpers/permissions.ts +140 -0
  68. package/src/Helpers/ui.ts +103 -0
  69. package/src/Interactions/context.ts +132 -0
  70. package/src/Interactions/definitions.ts +309 -0
  71. package/src/Interactions/gateway.ts +71 -0
  72. package/src/Interactions/handlers.ts +130 -0
  73. package/src/Interactions/index.ts +108 -0
  74. package/src/Interactions/utils.ts +81 -0
  75. package/src/Interactions/webhook.ts +110 -0
  76. package/src/Log.ts +17 -0
  77. package/src/RateLimit/memory.ts +57 -0
  78. package/src/RateLimit/utils.ts +27 -0
  79. package/src/RateLimit.ts +69 -0
  80. package/src/_common.ts +43 -0
  81. package/src/gateway.ts +38 -0
  82. package/src/global.ts +45 -0
  83. package/src/index.ts +20 -0
  84. package/src/package.json +52 -0
  85. package/src/types.ts +6368 -0
  86. package/src/utils/effect.ts +0 -0
  87. package/src/utils/hub.ts +47 -0
  88. package/src/utils/json.d.ts +1 -0
  89. package/src/utils/tsplus.ts +10 -0
  90. package/src/webhooks.ts +41 -0
  91. package/tsconfig.json +23 -0
  92. package/tsplus.config.json +8 -0
  93. package/types.js.map +1 -1
  94. package/utils/effect.js.map +1 -1
  95. package/utils/hub.js.map +1 -1
  96. package/utils/tsplus.js.map +1 -1
  97. 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)