# Effect gates

Write a gate that changes something, like spending a credit, with defineEffectGate(). Covers when the change runs, what happens when a later gate refuses, and effect gates inside an or().

Say a command costs one credit. Your gate reads the balance and deducts one. A later gate then refuses. The command never ran, and the caller lost a credit anyway.

[`defineEffectGate`](https://docs.seedcord.org/packages/core/latest/define-effect-gate) splits your gate in two. `check` reads and refuses. `commit` applies the change, and seedcord holds that half back until every gate on the handler has passed.

```ts title="src/gates/SpendCredit.ts"
export const SpendCredit = defineEffectGate(
    'SpendCredit',
    async (ctx) => {
        if (ctx.userId === null) return;
        if ((await credits.balance(ctx.userId)) < 1) {
            throw new OutOfCredits();
        }
    },
    async (ctx) => {
        if (ctx.userId !== null) await credits.deduct(ctx.userId);
    }
);
```

You attach `SpendCredit` like any other gate.

```ts
@Gated(SpendCredit, GuildOnly())
@SlashRoute('search')
export class Search extends SlashHandler<'search'> {
    public async execute(): Promise<void> {
        await this.reply('Searching.');
    }
}
```

## The order

seedcord runs every check *before* it runs any commit.

1. Each gate's `check`, left to right, stopping at the first refusal.
2. Every queued `commit`, in the order its gate ran.
3. `execute()`.

If someone runs that command in a DM, `SpendCredit` checks their balance first and queues its commit. [`GuildOnly`](https://docs.seedcord.org/packages/core/latest/guild-only) then refuses. Step two never runs, so the caller keeps the credit.

A refusal anywhere in step one drops every queued commit. The same holds when a check throws a [`Fault`](https://docs.seedcord.org/packages/core/latest/fault), a [`Silence`](https://docs.seedcord.org/packages/core/latest/silence), or a plain error.

> **Warning**
>
> Step two finishes before step three starts. Your handler runs after every commit, so a throw inside it leaves the credit spent. If your users paid for that credit or can see their balance, catch the throw in your handler and refund the credit there.
>
> Commits run one at a time. If a commit throws, the commits after it don't run, and neither does your handler. The commits before it already ran, so their changes stay.
>
> The same happens inside a single commit. If it writes twice and throws between the two writes, the first write stays. Give each commit one write.

### Inside an or

Each arm of an [`or`](https://docs.seedcord.org/packages/core/latest/or) can be a group of gates, like an `and`. If that group refuses, seedcord throws away the commits its gates queued, so they never run. The handler below passes `SpendCredit` to an `and`, and passes that `and` to an `or`.

```ts
@Gated(
    GuildOnly(),
    or(
        and(SpendCredit, RequireRole('221439658686939136')),
        OwnerOnly()
    ),
    Cooldown('1m')
)
@SlashRoute('history')
export class History extends SlashHandler<'history'> {
    public async execute(): Promise<void> {
        await this.reply('Here it is.');
    }
}
```

A bot owner who lacks that role runs it. Read the checks down, then the commits.

```txt output
1. every check, left to right
├─ GuildOnly            pass
├─ or
│  ├─ and
│  │  ├─ SpendCredit    pass, commit queued
│  │  └─ RequireRole    refuse
│  │
│  │  the and refuses, so this arm refuses
│  │  seedcord drops the queued commit
│  │
│  └─ OwnerOnly         pass, so the or passes
└─ Cooldown             pass, commit queued

2. every queued commit
└─ Cooldown             charges the slot

3. execute()
```

The owner keeps their credit and still spends a cooldown slot. Only the arm that passes keeps its commit. `OwnerOnly` passed without queuing a commit.

> **Note**
>
> Every commit runs after every check. A check never reads what another gate's commit wrote.

## What the two halves share

If you annotate `ctx` on either half, both take that context. Your gate then fits the same handlers as a [`defineGate`](https://docs.seedcord.org/packages/core/latest/define-gate) gate carrying that annotation.

Either half can return a promise. A synchronous one works too, so a commit that writes to a `Map` doesn't need `async`.

```ts
const uses = new Map<string, number>();

export const CountUse = defineEffectGate(
    'CountUse',
    () => {},
    (ctx) => {
        if (ctx.userId === null) return;
        uses.set(ctx.userId, (uses.get(ctx.userId) ?? 0) + 1);
    }
);
```

`CountUse` never refuses, so its `check` is empty. Its `commit` records the use in `uses` without awaiting anything.

## Cooldown

[`Cooldown`](https://docs.seedcord.org/packages/core/latest/cooldown) is an effect gate. It reads the window in `check` and charges the slot in `commit`, which is why a later refusal never records a use.
