Effect, When You Want It
Optional. The same SDK with typed failures and scoped cleanup

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 Guideimport { 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 Guideimport { 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 Guideimport { 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 Guideimport { 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 Guideimport { 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 Guideimport { 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)); 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())); }




