Skip to content
tim-smartPublic

About

A Discord library for effect-ts

Resources

Stars

146 stars

Watchers

3 watching

Forks

Latest commit

 

History

925 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dfx

Discord

A Discord library built on top of effect

  • Supports both the gateway and webhooks
  • Simple yet powerful abstractions to build Discord bots

Example

import { NodeHttpClient, NodeRuntime, NodeSocket } from "@effect/platform-node"
import { DiscordConfig, Ix } from "dfx"
import { DiscordIxLive, InteractionsRegistry } from "dfx/gateway"
import { Config, Effect, Layer } from "effect"

// Create a layer for the discord services
const DiscordLayer = DiscordIxLive.pipe(
  Layer.provide([
    DiscordConfig.layerConfig({
      token: Config.redacted("DISCORD_BOT_TOKEN"),
    }),
    NodeHttpClient.layerUndici,
    NodeSocket.layerWebSocketConstructor,
  ]),
)

// Create hello service
const HelloLayer = Layer.effectDiscard(
  Effect.gen(function* () {
    const registry = yield* InteractionsRegistry

    // Create hello command that responds with "Hello!"
    const hello = Ix.global(
      {
        name: "hello",
        description: "A basic command",
      },
      Effect.succeed({
        type: 4,
        data: {
          content: "Hello!",
        },
      }),
    )

    // register the command(s) and handle errors
    yield* registry.register(
      Ix.builder.add(hello).catchAllCause(Effect.logError),
    )
  }),
).pipe(
  // provide discord layer
  Layer.provide(DiscordLayer),
)

// Construct the main layer
const MainLive = Layer.mergeAll(
  // add your other services here
  HelloLayer,
)

// run it
NodeRuntime.runMain(Layer.launch(MainLive))

Handling dispatches before they are stored for RESUME

By default a shard stores each dispatch's sequence before it publishes the dispatch. If your process stops before a handler finishes, the next RESUME starts after that dispatch and Discord won't send it again.

To handle dispatches at least once, provide a DispatchHook. It runs for each dispatch before the sequence is stored:

import { DispatchHook, DiscordIxLive } from "dfx/gateway"
import { Effect, Layer } from "effect"

const InboxHook = Layer.effect(
  DispatchHook,
  Effect.gen(function* () {
    const inbox = yield* Inbox // your durable queue
    return (payload, { shard, sessionId }) =>
      inbox.append({ key: [shard, sessionId, payload.s], payload })
  }),
)

const DiscordLayer = DiscordIxLive.pipe(
  Layer.provide(InboxHook),
  // ...the layers from the example above
)

What the hook gives you:

  • It runs once per received dispatch, one at a time per shard, in sequence order.
  • If the shard shuts down while the hook runs, the hook is interrupted and the sequence isn't stored, so the next RESUME replays that dispatch.
  • If the hook fails, the shard logs the error, reconnects and resumes from the last stored sequence. READY is the exception, see below.
  • A hook that keeps failing doesn't flood Discord. The shard drops the connection straight away, but waits before its next IDENTIFY or RESUME: 1 second after the first failure, doubling with each consecutive failure up to 2 minutes, and back to 1 second once a dispatch is stored. Socket reconnects in the meantime, whatever causes them, don't shorten the wait.
  • Heartbeats keep reporting the latest sequence received, however long the hook takes.

What it doesn't give you:

  • Exactly-once delivery. Dispatches replayed after a RESUME go through the hook again and are published to every subscriber again. Dedupe on (shard, sessionId, payload.s), since sequences restart with each new session, or make handling idempotent.
  • Delivery beyond Discord's resume window. If the session can't be resumed, Discord sends an invalid session, the shard identifies again, and the missed dispatches are gone.
  • Recovery when a new session's READY fails. The session is only stored once the hook and the store write for READY succeed. If either fails there is no session to resume, so the shard identifies again and gets a new READY. Dispatches the abandoned session sent after its READY are not replayed.
  • A guarantee about Discord's daily IDENTIFY limit. The backoff limits how often each shard retries, so a single shard failing on READY identifies at most every 2 minutes. Several shards failing at once can still use up the limit between them.

The hook runs on the shard's read loop, which also processes heartbeat ACKs. Keep it short, for example by appending to a durable inbox and returning. A hook that takes longer than the heartbeat interval can make the shard reconnect.

About

A Discord library for effect-ts

Resources

Stars

146 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages