Effect, When You Want It

Optional. The same SDK with typed failures and scoped cleanup

A plug sliding into a socket, the Fluxer waves on the plug

Start with One Request

Effect is a TypeScript library for programs whose failures and cleanup are part of the code. It is optional here: The default API has the same capabilities with async functions and Results. The examples need Effect 4 installed next to the SDK

An Effect describes work without running it. Effect.scoped gives the client a cleanup lifetime, called a scope, and Effect.runPromise runs the program from the application's top-level code

The message is sent only when Effect.runPromise runs the program

One Request in the First Effect Bot Guide
TypeScript
import { Effect } from "effect"import { createClient } from "@neontechspace/fluxerly/effect"export function sendHello(token: string, channelId: string) {    const program = Effect.scoped(        Effect.gen(function* () {            const client = yield* createClient({ token })            return yield* client.messages.send(channelId, "Hello from Effect")        }),    )    return Effect.runPromise(program)}

Run the Complete Bot

The Effect runBot takes one options object, like the default one. Each handler returns an Effect, here the reply, so the SDK runs that work and can interrupt it

Ctrl+C stops the bot. The SDK interrupts what is still running and waits for its cleanup, and a failure that stops the bot is logged once with a failing exit code

Stopping the bot interrupts its work and waits for cleanup

The Complete Bot in the First Effect Bot Guide
bot.ts
import { Effect } from "effect"import { runBot } from "@neontechspace/fluxerly/effect"const program = runBot({    token: process.env.FLUXER_BOT_TOKEN,    processSignals: true,    events: {        messageCreate: ({ message, reply }) => {            if (message.content !== "!ping") return Effect.void            // A failed reply is reported to the log without stopping the bot            return reply("Pong!")        },    },})// A failure that stops the bot, such as a rejected token, is logged and sets a failing exit codeawait Effect.runPromiseExit(program)

Handle Success and Expected Failure

An expected failure, such as a refused send, is part of the Effect's type, so the compiler knows it can happen. Effect.match turns either outcome into a plain value, here for a status panel

Unexpected faults, called defects, and interruption stay separate and still reach the caller

Failures are in the type

Expected Failure in the Effect Workflows Guide
TypeScript
import { Effect } from "effect"import type { Client } from "@neontechspace/fluxerly/effect"export function sendNotice(client: Client, channelId: string) {    return client.messages.send(channelId, "The event starts in ten minutes").pipe(        Effect.match({            onSuccess: message => ({ status: "confirmed" as const, messageId: message.id }),            onFailure: error => ({ status: "failed" as const, error }),        }),    )}

Collect Replies and Clean Up

A collector gathers matching messages for a limited time. It registers before the question is sent, so a fast reply is not missed

Opened inside its own scope, the collector is released when the conversation ends, whether it succeeded, failed or was interrupted. The bot's client stays open

Cleanup is owned by the scope

Collectors in the Effect Workflows Guide
TypeScript
import { Effect } from "effect"import type { Client } from "@neontechspace/fluxerly/effect"export function askPreferredName(client: Client, channelId: string, userId: string) {    return Effect.scoped(        Effect.gen(function* () {            const collector = yield* client.messages.collect(channelId, {                filter: message => message.author.id === userId && !message.author.isBot,                maxMessages: 1,                timeoutMs: 30_000,            })            yield* client.messages.send(channelId, "What name should the bot use?")            const result = yield* collector.result()            return result.messages[0]?.content        }),    )}

Run Independent Reads Together

Effect.all fetches a community and its channels at the same time, with at most two requests at once. The SDK's request limits and rate-limit handling still apply

If one read fails, the other is interrupted, and its cleanup finishes before the failure arrives

If one read fails, the other stops and cleans up

Independent Reads in the Effect Workflows Guide
TypeScript
import { Effect } from "effect"import type { Client } from "@neontechspace/fluxerly/effect"export function readGuildOverview(client: Client, guildId: string) {    return Effect.all({        guild: client.guilds.fetch(guildId),        channels: client.channels.fetchAll(guildId),    }, { concurrency: 2 })}

Services That a Test Can Replace

The FluxerClient service holds the SDK's client. A Layer, the way an Effect application supplies a service, builds it from the token in the environment, and a test can supply a different Layer

Here the handler asks for a greeting store and a reply service, never the client itself. The test provides fakes and moves a test clock forward instead of waiting, with no gateway and nothing of the SDK replaced

A test supplies fake services through a Layer

The Effect Application Testing Guide
services.ts
import { Config, Context, Effect, Layer } from "effect";import { FluxerClient, type Message } from "@neontechspace/fluxerly/effect";export class GreetingStore extends Context.Service<    GreetingStore,    { readonly greetingFor: (userId: string) => Effect.Effect<string> }>()("example/GreetingStore") {}export class BotReplies extends Context.Service<    BotReplies,    {        readonly reply: (            message: Message,            content: string,        ) => Effect.Effect<void, unknown>;    }>()("example/BotReplies") {}export const handlePing = (message: Message) =>    Effect.gen(function* () {        if (message.author.isBot || message.content !== "!ping") return;        const store = yield* GreetingStore;        const replies = yield* BotReplies;        yield* Effect.sleep("100 millis");        yield* replies.reply(            message,            yield* store.greetingFor(message.author.id),        );    });export const clientLayer = FluxerClient.layerConfig({    token: Config.Redacted("FLUXER_BOT_TOKEN"),});export const repliesLayer = Layer.effect(    BotReplies,    FluxerClient.use((client) =>        Effect.succeed({            reply: (message: Message, content: string) =>                client.messages.reply(message, content).pipe(Effect.asVoid),        }),    ),).pipe(Layer.provideMerge(clientLayer));
TypeScript
import { Effect, Fiber, Layer } from "effect";import { TestClock } from "effect/testing";import type { Message } from "@neontechspace/fluxerly/effect";import { BotReplies, GreetingStore, handlePing } from "./services.ts";export function testPing(message: Message, replies: Array<string>) {    const fakes = Layer.mergeAll(        Layer.succeed(GreetingStore, {            greetingFor: (userId) => Effect.succeed(`Hello ${userId}`),        }),        Layer.succeed(BotReplies, {            reply: (_message, content) =>                Effect.sync(() => replies.push(content)),        }),    );    return Effect.gen(function* () {        const fiber = yield* handlePing(message).pipe(            Effect.provide(fakes),            Effect.forkChild,        );        yield* TestClock.adjust("100 millis");        yield* Fiber.join(fiber);    }).pipe(Effect.provide(TestClock.layer()));}