# Purging history

> Delete edits for good. Build a request, count it, start it, follow its progress, and read the result. Purge a world that is not loaded.

Source: https://oasis-protection.com/docs/api/purges/

A purge deletes edits from the history for good. It is the same thing staff run with `/oasis purge`, with the same checks and the same history afterwards. Nothing brings the edits back, and the store gives their disk space back.

`oasis.purges()` has one method:

| Method                       | Does                                     |
| ---------------------------- | ---------------------------------------- |
| `purge(Selection selection)` | Deletes the edits the selection chooses. |

It returns a `PurgeRequest`: a purge you have not started yet. The `Purge` it starts fires a [`PurgeEndEvent`](/docs/api/events/#purgeendevent) when it ends.

```java
oasis.purges().purge(Selection.all().in("old_world"))
    .requestedBy(staff)
    .start()
    .result().thenAccept(result -> staff.sendMessage("Deleted " + result.deleted() + " edits."));
```

A purge cannot be undone

`/oasis undo` and `OperationRequest` reverse a rollback. Nothing reverses a purge. Call `count()` first when a person asks for it, and ask for a confirmation.

## The selection

A purge takes a [`Selection`](/docs/api/selections/), and the selection must narrow. `purge` throws `IllegalArgumentException` for `Selection.all()`, so a purge never deletes the whole history by mistake. Any part narrows: a person, a time, a world, a box, a kind, a cause or a material.

`purge` also throws `IllegalArgumentException` when the selection names `Actor.ROLLBACK`, or leaves it out with `not().by(Actor.ROLLBACK)`. The rollback edits mark what a rollback reversed. If a purge deleted them, the reversed edits would stand again. A purge of anything else keeps the rollback edits, with one exception: a purge of whole worlds.

A purge by age (a selection with only an `until` time) also hides the [incidents](/docs/api/incidents/) older than its cut. The store and the cairn keep them.

A selection with no `until` time deletes the edits up to the moment of `start()`. A selection with an `until` time in the future is cut back to that moment. A purge never deletes an edit that is newer than its start.

### A purge of whole worlds

A selection that names worlds and no other part, except `until`, is a purge of whole worlds. It deletes the whole history of those worlds, up to the cut:

```java
// Every edit in two worlds that you retired.
Selection retired = Selection.all().in("old_world", "old_nether");

// The same, but only the edits older than thirty days.
Selection oldRetired = retired.until(Instant.now().minus(Duration.ofDays(30)));
```

A purge of whole worlds differs from a narrower purge in three ways:

* It deletes the rollback and undo edits of those worlds too. A narrower purge keeps them.
* It deletes the entries of those worlds from the operation log, for each operation that finished before the cut. A cut that falls inside an operation keeps the record and the undo of that operation. An operation that covered several worlds stays undoable in the other worlds.
* It gives back the disk space of that log, as a purge by age does.

Add any other part, such as `by(player)` or `kinds(Kind.BLOCK)`, and the purge is a narrower purge again. A narrower purge in a world keeps its rollback edits.

`in(World...)` takes the worlds that are loaded. `in(String...)` takes worlds by their stored names.

### A world that is not loaded

A purge reads and writes the store alone, so it does not need the world. It takes a world that is not loaded, and a world whose folder is gone. Use `in(String...)` with the name the world had when Oasis logged its edits. Names are exact, as `World.getName()` reads them.

Oasis checks every world name of the selection when you ask for `count()` and when you start. A name that is not loaded under that exact name, that Oasis never saw, and that the store holds no edits of fails with `Failure.Reason.NO_SUCH_WORLD`, so a typo never deletes anything. The command says `No world named X` for the same case.

After a purge that names worlds, Oasis forgets each of those worlds that is not loaded and holds no edit any more. A later request that names such a world fails with `NO_SUCH_WORLD`. A world that is loaded stays known, because Oasis logs the edits in it.

A rollback or a restore is different: it writes the world. Oasis loads an unloaded world for the pass, through the [world loader](/docs/api/worlds/) of the plugin that keeps it or from its folder. When nobody can load it, the operation does not run. See [Selecting edits](/docs/api/selections/#a-world-that-is-not-loaded).

## The request

A request is immutable. Each method returns a new request, so you can keep one in a field and start it as often as you like.

| Method                              | Sets                                                                                         | Default                        |
| ----------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------ |
| `requestedBy(UUID person)`          | Who asked. `PurgeEndEvent.requester()` reads this id.                                        | Your plugin, `#plugin:<Name>`. |
| `requestedBy(CommandSender sender)` | Who asked: the player, or `Actor.CONSOLE` for any other sender.                              |                                |
| `count()`                           | Nothing. It counts the edits the purge deletes. See [Count and confirm](#count-and-confirm). |                                |
| `start()`                           | Nothing. It starts the purge now and returns a `Purge`.                                      |                                |

A purge has no `audience`. Oasis draws no bar and sends no summary for a purge that a plugin starts. Draw your own with [`onProgress`](#onprogress).

## Count and confirm

Oasis never asks for a confirmation when you start a purge through the API. When your plugin wants to ask one, call `count()` first:

```java
PurgeRequest request = oasis.purges()
    .purge(Selection.all().in("old_world"))
    .requestedBy(staff);

request.count().thenAccept(edits ->
    staff.sendMessage("This deletes " + edits + " edits for good."));
```

`count()` completes with a `Long`: the edits the purge deletes if it starts now. It counts each edit, so it reads as many rows as the purge deletes. A narrower purge keeps the rollback edits and the FAWE undo and redo edits, and the count leaves them out. Ask for it only when you show it to someone.

When a world of the selection does not exist, or the store can’t answer, the future completes exceptionally with an `OasisException`. [When a read fails](/docs/api/reading-history/#when-a-read-fails) lists its reasons and shows how to handle it.

## Start and follow a purge

`start()` returns a `Purge` at once. The purge begins on the same tick when you call `start()` on the main thread, and on the next tick when you call it from another thread.

| Method                                         | Returns                                                                             |
| ---------------------------------------------- | ----------------------------------------------------------------------------------- |
| `progress()`                                   | Where the purge is now, as a `PurgeProgress`.                                       |
| `onProgress(Consumer<PurgeProgress> listener)` | Adds a listener and returns the same `Purge`. See [onProgress](#onprogress).        |
| `result()`                                     | A future of the `PurgeResult`. It completes on the main thread once the purge ends. |

You can read a `Purge` from any thread. A purge has no `cancel()`.

### Progress

The store keeps the history in files. A purge rewrites or deletes each file that can hold an edit it deletes, so the files measure how far it has come.

| Field         | Type      | Holds                                               |
| ------------- | --------- | --------------------------------------------------- |
| `deleted()`   | `long`    | The edits deleted so far.                           |
| `files()`     | `int`     | The files the purge rewrites, or 0 before it plans. |
| `doneFiles()` | `int`     | The files it has finished.                          |
| `finished()`  | `boolean` | True in the last call, when the purge ended.        |
| `startedAt()` | `Instant` | When the purge started.                             |

`fraction()` is how far the purge has come, from 0 to 1: the finished files over all its files. It reads 0 before the purge plans.

### onProgress

`onProgress` calls your listener on the main thread, every two seconds while the purge runs. When the purge ends, the listener always gets one last call with `finished()` true, before `result()` completes. A purge that does not start gets only the last call. A listener you add after the end gets no call.

When a listener throws a `RuntimeException`, Oasis logs it as a warning in the server log and goes on. The other listeners still get their calls, and the result still completes.

Add your listeners right after `start()`, in the same tick. Oasis gives a purge that cannot start its last call and its result one tick later, so a listener you add in the tick of `start()` still gets it.

## The result

`result()` completes on the main thread with a `PurgeResult`. It never completes exceptionally: a purge that could not start or that failed ends with `Outcome.FAILED`.

| Field        | Type                | Holds                                                                                    |
| ------------ | ------------------- | ---------------------------------------------------------------------------------------- |
| `outcome()`  | `Outcome`           | `FINISHED` or `FAILED`. A purge has no cancel, so it never ends `CANCELLED`.             |
| `failure()`  | `Optional<Failure>` | Why it failed, or empty when it finished. See [When a purge fails](#when-a-purge-fails). |
| `deleted()`  | `long`              | The edits the purge deleted.                                                             |
| `duration()` | `Duration`          | How long the purge ran, from `start()` to its end.                                       |

## One purge at a time

Oasis runs one purge at a time, for commands and plugins alike. A purge also cannot start while an operation runs that can hold its edits. A purge that names no player can hold the edits of any operation, so any running operation refuses it. A purge that names players is refused only while an operation runs that names one of them.

A purge that is refused ends with `Outcome.FAILED` and `Failure.Reason.BUSY`. Nothing waits in a queue.

To start yours as soon as the other ends, listen for [`PurgeEndEvent`](/docs/api/events/#purgeendevent) or [`OperationEndEvent`](/docs/api/events/#operationendevent). When the event fires, the slot is free, and you can start your purge from inside the listener.

## When a purge fails

`PurgeResult.failure()` holds a `Failure` with two fields. Test `reason()` in code, and show `message()` to people. The message is the line a staff member reads for the same failure of a command.

| `Failure.Reason` | What happened                                                                                                                                                             | What to do                                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `BUSY`           | A purge runs, or an operation runs that can hold the edits of this purge.                                                                                                 | Start it again when that one ends.                                                      |
| `REBUILDING`     | The store rebuilds the runs that another build of Oasis wrote, after an update.                                                                                           | Start it again when the rebuild ends.                                                   |
| `MOVING`         | The history moves to another store with `/oasis move`.                                                                                                                    | Start it again when the move ends.                                                      |
| `STARTING`       | Oasis still opens its store, just after the server started.                                                                                                               | Start it again shortly.                                                                 |
| `OVERLOADED`     | The store holds as many requests as it takes.                                                                                                                             | Start it again shortly.                                                                 |
| `NO_SUCH_WORLD`  | The selection names a world, chosen or left out, that is not loaded under that exact name, that Oasis never saw, and that the store holds no edits of. Most often a typo. | Check the name.                                                                         |
| `STORE`          | The store failed, and the purge stopped.                                                                                                                                  | Tell staff. The purge does not run again by itself, and the server log names the error. |

A purge that fails with `STORE` may have deleted some edits before it stopped. `PurgeResult.deleted()` and `Purge.progress().deleted()` hold that count.

```java
private void explain(Player staff, PurgeResult result)
{
    Failure failure = result.failure().orElseThrow();
    switch (failure.reason())
    {
        case NO_SUCH_WORLD -> staff.sendMessage(failure.message() + " Check the name of the world.");
        case BUSY, REBUILDING, MOVING, STARTING, OVERLOADED ->
            staff.sendMessage(failure.message() + " Run the command again in a moment.");
        default -> staff.sendMessage("The purge failed: " + failure.message());
    }
}
```

A purge that Oasis refuses before it begins, such as one that ends with `BUSY` or `NO_SUCH_WORLD`, fires no `PurgeEndEvent`, because no purge ran.

## Example: /purgeworld

This plugin adds `/purgeworld <world>`. It counts the edits of the world, asks the staff member to confirm, draws the progress in a boss bar, and reports the result. The world can be unloaded, or even deleted. A name that no world and no edit knows fails the count with `NO_SUCH_WORLD`.

```java
package com.example.purgeworld;

import com.oasis.api.Failure;
import com.oasis.api.Oasis;
import com.oasis.api.OasisApi;
import com.oasis.api.OasisException;
import com.oasis.api.Outcome;
import com.oasis.api.Purge;
import com.oasis.api.PurgeRequest;
import com.oasis.api.Selection;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

public final class PurgeWorld extends JavaPlugin
{
    private final Map<UUID, PurgeRequest> toConfirm = new HashMap<>();
    private OasisApi oasis;

    @Override
    public void onEnable()
    {
        oasis = Oasis.api(this);
        registerCommand("purgeworld", "Delete the history of a world, after a confirmation", new Command());
    }

    private final class Command implements BasicCommand
    {
        @Override
        public void execute(CommandSourceStack source, String[] args)
        {
            if (!(source.getSender() instanceof Player staff))
            {
                source.getSender().sendMessage("Run /purgeworld in game.");
                return;
            }
            if (args.length != 1)
            {
                staff.sendMessage(Component.text("Usage: /purgeworld <world>", NamedTextColor.RED));
                return;
            }
            if (args[0].toLowerCase(Locale.ROOT).equals("confirm"))
            {
                start(staff, toConfirm.remove(staff.getUniqueId()));
            }
            else
            {
                ask(staff, args[0]);
            }
        }

        @Override
        public String permission()
        {
            return "purgeworld.use";
        }
    }

    /** Step 1: count the edits, and ask the staff member to confirm. */
    private void ask(Player staff, String world)
    {
        // The name needs no loaded world. Oasis reads the store alone.
        PurgeRequest request = oasis.purges()
            .purge(Selection.all().in(world))
            .requestedBy(staff.getUniqueId());

        request.count().whenComplete((edits, error) ->
        {
            if (error != null)
            {
                refused(staff, "Oasis could not count the edits", error);
                return;
            }
            if (edits == 0)
            {
                staff.sendMessage(Component.text("The history holds no edit in " + world + ".", NamedTextColor.GRAY));
                return;
            }
            toConfirm.put(staff.getUniqueId(), request);
            staff.sendMessage(Component.text("Delete " + edits + " edits in " + world + " for good? ")
                .append(Component.text("[Confirm]", NamedTextColor.RED)
                    .clickEvent(ClickEvent.runCommand("/purgeworld confirm"))));
        });
    }

    /** Step 2: start the request the staff member confirmed, with a boss bar of its progress. */
    private void start(Player staff, PurgeRequest request)
    {
        if (request == null)
        {
            staff.sendMessage(Component.text("There is nothing to confirm. Run /purgeworld <world> again.",
                NamedTextColor.RED));
            return;
        }
        BossBar bar = BossBar.bossBar(Component.text("Purging"), 0f, BossBar.Color.RED, BossBar.Overlay.PROGRESS);
        staff.showBossBar(bar);

        Purge purge = request.start();
        purge.onProgress(progress -> bar.progress((float) progress.fraction()));
        purge.result().thenAccept(result ->
        {
            staff.hideBossBar(bar);
            if (result.outcome() == Outcome.FAILED)
            {
                String why = result.failure().orElseThrow().message();
                staff.sendMessage(Component.text("The purge failed after deleting " + result.deleted()
                    + " edits: " + why, NamedTextColor.RED));
                return;
            }
            staff.sendMessage(Component.text("Deleted " + result.deleted() + " edits in "
                + result.duration().toSeconds() + "s.", NamedTextColor.GREEN));
        });
    }

    /** A count fails with an OasisException that names the reason. */
    private void refused(Player staff, String what, Throwable error)
    {
        if (error instanceof OasisException failed)
        {
            Failure failure = failed.failure();
            staff.sendMessage(Component.text(what + ": " + failure.message(), NamedTextColor.RED));
            return;
        }
        staff.sendMessage(Component.text(what + ". The server log has the details.", NamedTextColor.RED));
        getLogger().log(Level.WARNING, what, error);
    }
}
```

The flow has two steps:

1. `/purgeworld old_world` builds a request and calls `count()`. Nothing changes yet. The staff member reads the size and clicks **Confirm**.
2. `/purgeworld confirm` starts the request it kept. The purge deletes the edits up to that start.

When the purge ends, Oasis forgets `old_world` if it is not loaded and holds no edit any more. A later request that names `old_world` fails with `NO_SUCH_WORLD`.
