Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Effect-TS/discord-bot/llms.txt

Use this file to discover all available pages before exploring further.

The codebase uses Effect 4.x idioms consistently across every feature. This page catalogs each pattern with real examples from the source.

ServiceMap.Service — defining services

Every injectable service is a class that extends ServiceMap.Service. The class body holds the make factory (an Effect.gen block), and a static layer property that wraps it in a Layer.
// packages/discord-bot/src/Ai.ts
export class AiHelpers extends ServiceMap.Service<AiHelpers>()(
  "app/AiHelpers",
  {
    make: Effect.gen(function* () {
      const rest = yield* DiscordREST
      const model = yield* ChatModel
      const application = yield* DiscordApplication
      // ...
      return { generateTitle, generateDocs, generateSummary, generateAiInput } as const
    }),
  },
) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(OpenAiLive),
  )
}
Other services that follow the same pattern: Github, EffectRepo, Summarizer, ChannelsCache, MemberCache, Messages, DiscordApplication.

Layer — dependency injection and composition

Effect Layers are the dependency-injection mechanism. The bot uses several Layer combinators. Layer.effectDiscard — for features that register gateway handlers or slash commands and return nothing:
// packages/discord-bot/src/AutoThreads.ts
export const AutoThreadsLive = Layer.effectDiscard(make).pipe(
  Layer.provide(ChannelsCache.layer),
  Layer.provide(AiHelpers.layer),
  Layer.provide(DiscordGatewayLayer),
)
Layer.effect — for services whose value is consumed by other layers:
// packages/discord-bot/src/Summarizer.ts
static readonly layer = Layer.effect(this, this.make).pipe(
  Layer.provide(ChannelsCache.layer),
  Layer.provide(MemberCache.layer),
  Layer.provide(Messages.layer),
  Layer.provide(DiscordGatewayLayer),
)
Layer.provide — wires a dependency into a layer:
// packages/discord/src/DiscordGateway.ts
const DiscordLayer = DiscordIxLive.pipe(
  Layer.provideMerge(NodeHttpClient.layerUndici),
  Layer.provide(NodeSocket.layerWebSocketConstructor),
  Layer.provide(DiscordConfigLayer),
)
Layer.mergeAll — combines multiple independent feature layers in main.ts:
const MainLive = Layer.mergeAll(
  AiResponse,
  AutoThreadsLive,
  DocsLookupLive,
  Summarizer.layer,
  // ...
).pipe(Layer.provide(TracerLayer("discord-bot")), Layer.provide(LogLevelLive))

Effect.gen — generator-style composition

Effect.gen lets you write sequential Effect logic using yield* for a readable, imperative style. Every make factory and most handlers use it.
// packages/discord-bot/src/Summarizer.ts
make: Effect.gen(function* () {
  const rest = yield* DiscordREST
  const channels = yield* ChannelsCache
  const registry = yield* InteractionsRegistry
  const members = yield* MemberCache
  const messagesService = yield* Messages
  // ...
  yield* registry.register(ix)

  return { thread: summarizeThread, messages: summarizeWithMessages, message: summarizeMessage } as const
}),

Effect.fn / Effect.fnUntraced — named traced functions

Effect.fn wraps a generator function and attaches an OpenTelemetry span automatically. Effect.fnUntraced skips the span for hot-path helpers.
// packages/discord-bot/src/Ai.ts
const getOpeningMessage = Effect.fn("AiHelpers.getOpeningMessage")(
  function* (thread: Discord.GuildChannelResponse | Discord.ThreadResponse) {
    if (thread.parent_id == null) {
      return yield* rest.getMessage(thread.id, thread.id)
    }
    return yield* rest
      .getMessage(thread.parent_id, thread.id)
      .pipe(Effect.catch(() => rest.getMessage(thread.id, thread.id)))
  },
)
// packages/discord-bot/src/Summarizer.ts
const summarizeThread = Effect.fn("Summarizer.summarizeThread")(
  function* (channel: Discord.ThreadResponse, small: boolean = true) {
    const parentChannel = yield* channels.get(channel.guild_id!, channel.parent_id!)
    const threadMessages = yield* Stream.runCollect(
      messagesService.cleanForChannel(channel),
    ).pipe(Effect.map((items) => [...items].toReversed()))
    return yield* summarize(parentChannel, channel, threadMessages, small)
  },
)
Effect.fnUntraced is used for gateway event handlers where the outer span comes from Effect.withSpan:
// packages/discord-bot/src/AutoThreads.ts
const handleMessages = gateway.handleDispatch(
  "MESSAGE_CREATE",
  Effect.fnUntraced(
    function* (event) { /* ... */ },
    (effect, event) =>
      Effect.withSpan(effect, "AutoThreads.handleMessages", {
        attributes: { messageId: event.id },
      }),
    Effect.catchCause(Effect.logError),
  ),
)

Config / ConfigProvider — environment variables

All configuration is read through Effect’s Config module, keeping secrets out of code.
// packages/discord/src/DiscordConfig.ts
export const DiscordConfigLayer = DiscordConfig.layerConfig({
  token: Config.redacted("DISCORD_BOT_TOKEN"),
  gateway: {
    intents: Config.succeed(
      Intents.fromList(["GuildMessages", "MessageContent", "Guilds"]),
    ),
  },
})
// packages/discord-bot/src/Ai.ts
export const OpenAiLive = OpenAiClient.layerConfig({
  apiKey: Config.redacted("OPENAI_API_KEY"),
  // ...
})
ConfigProvider.nested + ConfigProvider.constantCase — AutoThreads scopes its config under a namespace so all variables are prefixed AUTOTHREADS_:
// packages/discord-bot/src/AutoThreads.ts
Effect.provideService(
  ConfigProvider.ConfigProvider,
  ConfigProvider.fromEnv().pipe(
    ConfigProvider.nested("autothreads"),
    ConfigProvider.constantCase,
  ),
)
// reads AUTOTHREADS_KEYWORD from the environment

Data.TaggedError — typed error classes

Data.TaggedError creates nominal error types with a _tag discriminant, making error handling exhaustive and precise.
// packages/discord-bot/src/AutoThreads.ts
export class NotValidMessageError extends Data.TaggedError(
  "NotValidMessageError",
)<{
  readonly reason: "non-default" | "from-bot" | "non-text-channel" | "disabled"
}> {}

export class PermissionsError extends Data.TaggedError("PermissionsError")<{
  readonly action: string
  readonly subject: string
}> {}
// packages/discord-bot/src/Summarizer.ts
export class NotInThreadError extends Data.TaggedError("NotInThreadError")<{}> {}
Tagged errors are caught by tag in Ix.builder:
Ix.builder
  .add(command)
  .catchTagRespond("NotInThreadError", () =>
    Effect.succeed(
      Ix.response({
        type: Discord.InteractionCallbackTypes.CHANNEL_MESSAGE_WITH_SOURCE,
        data: { content: "This command can only be used in a thread", flags: Discord.MessageFlags.Ephemeral },
      }),
    ),
  )
  .catchAllCause(Effect.logError)

Effect.withSpan — OpenTelemetry tracing

Effect.withSpan manually annotates an effect with a span name and optional attributes. It composes with Effect.fn (which adds spans automatically) for fine-grained traces.
// packages/discord-bot/src/Summarizer.ts
const summarizeWithMessages = (
  channel: Discord.ThreadResponse,
  messages: Array<Discord.MessageResponse>,
  small = true,
) =>
  pipe(
    channels.get(channel.guild_id!, channel.parent_id!),
    Effect.flatMap((parentChannel) =>
      summarize(parentChannel, channel, messages, small),
    ),
    Effect.withSpan("Summarizer.summarizeWithMessages"),
  )
// packages/discord-bot/src/AiResponse.ts
Effect.withSpan(effect, "AiResponse.generate (inner)"),
Span attributes are annotated on the current span with Effect.annotateCurrentSpan:
yield* Effect.annotateCurrentSpan({ channelId: channel.id, small })

FiberMap — managing concurrent fibers by key

FiberMap tracks running fibers by a key, preventing duplicate work and allowing cancellation by key.
// packages/discord-bot/src/AiResponse.ts
const fiberMap = yield* FiberMap.make<Discord.Snowflake>()

// Start (or replace) the AI generation fiber for this interaction
yield* FiberMap.run(fiberMap, context.id, generate(context, history, reasoning))

// Cancel the fiber when the user clicks "Cancel"
yield* FiberMap.remove(fiberMap, interactionId)

Schedule — retry and recurrence

Schedule drives retries and periodic tasks.
// packages/discord-bot/src/Ai.ts — exponential backoff on transient HTTP errors
export const OpenAiLive = OpenAiClient.layerConfig({
  apiKey: Config.redacted("OPENAI_API_KEY"),
  transformClient: HttpClient.retryTransient({
    times: 3,
    schedule: Schedule.exponential(500),
  }),
})
// packages/discord-bot/src/EffectRepo.ts — pull the git repo every 15 minutes
yield* Effect.gen(function* () {
  while (true) {
    yield* Effect.sleep("15 minutes")
    yield* git.pull(repoPath)
    yield* RcRef.invalidate(llmsMd)
  }
}).pipe(Effect.retry(Schedule.forever), Effect.forkScoped)

Build docs developers (and LLMs) love