diff --git a/docs/core-features/schedulers.md b/docs/core-features/schedulers.md index 0891412..7f511b3 100644 --- a/docs/core-features/schedulers.md +++ b/docs/core-features/schedulers.md @@ -4,104 +4,212 @@ description: Execute tasks synchronously and asynchronously with fluent API. # Schedulers -## Basic Usage +`Schedulers` provides static access to sync and async `Scheduler` instances. Each `Scheduler` returns `Promise` objects from one-shot methods and `Task` objects from repeating methods, both of which implement `Terminable` for automatic lifecycle management. -### Sync Tasks +## Scheduler access + +| Method | Returns | Description | +|---|---|---| +| `Schedulers.sync()` | `Scheduler` | Main server thread | +| `Schedulers.async()` | `Scheduler` | Async thread pool | +| `Schedulers.get(ThreadContext)` | `Scheduler` | Determined by `SYNC` or `ASYNC` context | +| `Schedulers.bukkit()` | `BukkitScheduler` | Raw Bukkit scheduler | +| `Schedulers.builder()` | `TaskBuilder` | Fluent task builder (see below) | + +## Scheduler methods + +All one-shot methods return `Promise` (or `Promise` for `run`/`runLater`). Repeating methods return `Task`. + +| Method | Parameters | Description | +|---|---|---| +| `run(Runnable)` | runnable | Execute immediately | +| `supply(Supplier)` | supplier | Execute immediately, return value | +| `call(Callable)` | callable | Execute immediately, return value (checked exceptions) | +| `runLater(Runnable, long)` | runnable, delayTicks | Execute after delay | +| `supplyLater(Supplier, long)` | supplier, delayTicks | Execute after delay, return value | +| `callLater(Callable, long)` | callable, delayTicks | Execute after delay, return value | +| `runRepeating(Runnable, long, long)` | runnable, delayTicks, intervalTicks | Repeat on interval | +| `runRepeating(Consumer, long, long)` | consumer, delayTicks, intervalTicks | Repeat with access to the `Task` handle | + +`runLater`, `supplyLater`, `callLater`, and `runRepeating` each have a `TimeUnit` overload (e.g., `runLater(Runnable, long, TimeUnit)`). + +## Sync and async task execution ```java -// Run on main thread Schedulers.sync().run(() -> { player.teleport(location); }); -// Delayed (20 ticks = 1 second) Schedulers.sync().runLater(() -> { player.sendMessage("Delayed message"); }, 20L); -// Repeating (every 20 ticks) Schedulers.sync().runRepeating(() -> { player.sendActionBar("Repeating message"); }, 0L, 20L); ``` -### Async Tasks - ```java -// Run off main thread Schedulers.async().run(() -> { - // Heavy computation, database queries, etc. PlayerData data = database.load(uuid); }); -// Delayed async Schedulers.async().runLater(() -> { - // Heavy async task + database.cleanup(); }, 20L); -// Repeating async Schedulers.async().runRepeating(() -> { - // Periodic background task + database.heartbeat(); }, 0L, 100L); ``` ## Promises -Async computations with callbacks: +All one-shot methods (`run`, `supply`, `call`, and their `*Later` variants) return a `Promise`, which supports chaining across thread contexts. + +**Promise chain methods:** + +| Method | Runs on | Input | Output | +|---|---|---|---| +| `thenApplySync(Function)` | main thread | value | value | +| `thenApplyAsync(Function)` | async thread | value | value | +| `thenAcceptSync(Consumer)` | main thread | value | void | +| `thenAcceptAsync(Consumer)` | async thread | value | void | +| `thenRunSync(Runnable)` | main thread | nothing | void | +| `thenRunAsync(Runnable)` | async thread | nothing | void | +| `thenComposeSync(Function>)` | main thread | value | flattened promise | +| `thenComposeAsync(Function>)` | async thread | value | flattened promise | + +Each of these also has a delayed variant (e.g., `thenApplyDelayedSync(Function, long)` and `thenApplyDelayedSync(Function, long, TimeUnit)`). ```java -// Supply value asynchronously Schedulers.async().supply(() -> { - // Heavy computation return database.loadPlayer(uuid); -}).thenApplySync(data -> { - // Back on main thread +}).thenAcceptSync(data -> { player.sendMessage("Data loaded: " + data); - return data; }); ``` -## Binding Tasks +## TaskBuilder API + +`Schedulers.builder()` provides a fluent builder for constructing tasks with explicit thread context and timing. + +**Builder chain:** + +`Schedulers.builder()` -> `.sync()` or `.async()` -> timing -> terminal + +**Timing methods** (on `ThreadContextual`): + +| Method | Description | +|---|---| +| `now()` | Execute immediately. Returns `ContextualPromiseBuilder`. | +| `after(long ticks)` | Execute after delay. Returns `DelayedTick`. | +| `after(long, TimeUnit)` | Execute after delay. Returns `DelayedTime`. | +| `every(long ticks)` | Repeat with no initial delay. Returns `ContextualTaskBuilder`. | +| `afterAndEvery(long ticks)` | Delay then repeat at same interval. Returns `ContextualTaskBuilder`. | +| `afterAndEvery(long, TimeUnit)` | Delay then repeat at same interval. Returns `ContextualTaskBuilder`. | -Automatically cancel tasks when plugin disables: +**Terminal methods:** + +`ContextualPromiseBuilder` (one-shot): `run(Runnable)`, `supply(Supplier)`, `call(Callable)` + +`ContextualTaskBuilder` (repeating): `run(Runnable)`, `consume(Consumer)` + +`DelayedTick`/`DelayedTime` extend `ContextualPromiseBuilder` and add `.every(...)` to convert into a repeating task. + +```java +Schedulers.builder() + .async() + .afterAndEvery(5, TimeUnit.MINUTES) + .run(() -> saveAllPlayerData()); + +Schedulers.builder() + .sync() + .after(60) + .run(() -> player.teleport(spawn)); + +Schedulers.builder() + .async() + .now() + .supply(() -> database.loadPlayer(uuid)) + .thenAcceptSync(data -> applyPlayerData(player, data)); +``` + +## Binding tasks for automatic cleanup + +`Task` extends `Terminable`, so repeating tasks can be bound to any `TerminableConsumer`. `Promise` also extends `Terminable`. ```java Schedulers.sync().runRepeating(() -> { - // This task auto-cancels on plugin disable + updateScoreboard(); }, 0L, 20L).bindWith(this); + +Schedulers.builder() + .async() + .every(100) + .run(() -> database.heartbeat()) + .bindWith(this); +``` + +## Tick conversions + +The `Ticks` utility converts between ticks and standard `TimeUnit` values. + +| Method | Description | +|---|---| +| `Ticks.from(long duration, TimeUnit unit)` | Convert duration to ticks | +| `Ticks.to(long ticks, TimeUnit unit)` | Convert ticks to duration | + +| Constant | Value | +|---|---| +| `Ticks.TICKS_PER_SECOND` | 20 | +| `Ticks.MILLISECONDS_PER_SECOND` | 1000 | +| `Ticks.MILLISECONDS_PER_TICK` | 50 | + +## Thread safety + +The Bukkit API is not thread-safe. Only call Bukkit API methods from the main thread. + +```java +// WRONG: Bukkit API from async thread +Schedulers.async().run(() -> { + player.teleport(location); +}); + +// CORRECT: async computation, sync application +Schedulers.async().supply(() -> { + return database.loadData(); +}).thenAcceptSync(data -> { + player.teleport(location); +}); ``` -## Complete Example +## Complete example ```java public class MyPlugin extends BasePlugin { @Override protected void enable() { - // Load player data async, apply sync Events.subscribe(PlayerJoinEvent.class) .handler(e -> { Player player = e.getPlayer(); UUID uuid = player.getUniqueId(); - // Async database load Schedulers.async().supply(() -> loadPlayerData(uuid)) - .thenApplySync(data -> { - // Back on main thread + .thenAcceptSync(data -> { applyPlayerData(player, data); Text.tell(player, "&aData loaded!"); - return data; }); }) .bindWith(this); - // Periodic save (every 5 minutes) - Schedulers.async().runRepeating(() -> { - saveAllPlayerData(); - }, 0L, 20L * 60 * 5) // 0 delay, 5 minutes interval + Schedulers.builder() + .async() + .afterAndEvery(5, TimeUnit.MINUTES) + .run(() -> saveAllPlayerData()) .bindWith(this); - // Delayed teleport Events.subscribe(PlayerInteractEvent.class) .handler(e -> { Player player = e.getPlayer(); @@ -110,50 +218,9 @@ public class MyPlugin extends BasePlugin { Schedulers.sync().runLater(() -> { player.teleport(spawn); Text.tell(player, "&aTeleported!"); - }, 60L); // 3 seconds = 60 ticks + }, 60L); }) .bindWith(this); } - - private PlayerData loadPlayerData(UUID uuid) { - // Database query (async safe) - return database.load(uuid); - } - - private void applyPlayerData(Player player, PlayerData data) { - // Apply to player (must be on main thread) - player.setLevel(data.getLevel()); - } - - private void saveAllPlayerData() { - // Save all data (async safe) - database.saveAll(); - } } ``` - -## Thread Safety - -**Important**: Bukkit API is not thread-safe! - -```java -// WRONG - Bukkit API from async thread -Schedulers.async().run(() -> { - player.teleport(location); // ERROR! Not thread-safe! -}); - -// CORRECT - Bukkit API on main thread -Schedulers.async().supply(() -> { - return database.loadData(); // Heavy work async -}).thenApplySync(data -> { - player.teleport(location); // Bukkit API on main thread - return data; -}); -``` - -## Time Units - -* **1 tick** = 50ms (1/20th second) -* **20 ticks** = 1 second -* **1200 ticks** = 1 minute -* **72000 ticks** = 1 hour diff --git a/docs/core-features/terminables.md b/docs/core-features/terminables.md index 3d81336..24fcb0a 100644 --- a/docs/core-features/terminables.md +++ b/docs/core-features/terminables.md @@ -6,29 +6,42 @@ description: >- # Terminables -## What are Terminables? +Terminables represent resources with a defined lifecycle. Binding a `Terminable` to a `TerminableConsumer` (such as `BasePlugin`) guarantees cleanup when the consumer shuts down, eliminating manual unregister and cancel calls. -Terminables are resources that can be automatically cleaned up when no longer needed. Using `.bindWith()` ensures resources are properly closed when your plugin disables. +## Interface hierarchy -## Basic Usage +| Interface | Extends | Role | +|---|---|---| +| `Terminable` | `AutoCloseable` | Single closeable resource. Provides `close()`, `bindWith(TerminableConsumer)`, `closeSilently()`, `isClosed()`. | +| `TerminableConsumer` | -- | Accepts resources via `bind(AutoCloseable)` and `bindModule(TerminableModule)`. | +| `CompositeTerminable` | `Terminable`, `TerminableConsumer` | Groups multiple terminables. Closes in LIFO order. Created via `CompositeTerminable.create()`. | +| `TerminableModule` | -- | Encapsulates related setup logic. Receives a `TerminableConsumer` in `setup(TerminableConsumer)`. | +| `Task` | `Terminable` | Repeating scheduler task. `close()` delegates to `stop()`. | +| `Promise` | `Future`, `Terminable` | Async computation result. | + +`BasePlugin` implements `TerminableConsumer`, so `bind()` and `bindModule()` are available directly via `this` in any `BasePlugin` subclass. + +## Binding resources ### Events ```java Events.subscribe(PlayerJoinEvent.class) .handler(e -> {}) - .bindWith(this); // Auto-unregister on plugin disable + .bindWith(this); ``` -### Schedulers +### Scheduler tasks ```java Schedulers.sync().runRepeating(() -> { - // Repeating task -}, 0L, 20L).bindWith(this); // Auto-cancel on plugin disable + updateScoreboard(); +}, 0L, 20L).bindWith(this); ``` -### Custom Resources +### Custom resources + +Any class implementing `Terminable` (or `AutoCloseable`) can be bound. ```java public class DatabaseConnection implements Terminable { @@ -46,120 +59,110 @@ public class DatabaseConnection implements Terminable { } } -// Usage DatabaseConnection db = new DatabaseConnection(); -bind(db); // Auto-close on plugin disable +bind(db); ``` ## CompositeTerminable -Group multiple terminables together: +Groups multiple terminables under a single handle. Useful for subsystems that need independent lifecycle control. ```java public class GameArena { private final CompositeTerminable terminables = CompositeTerminable.create(); public void start() { - // Register events for this arena Events.subscribe(PlayerMoveEvent.class) .filter(e -> isInArena(e.getPlayer())) .handler(this::handleMove) .bindWith(terminables); - // Start arena tasks Schedulers.sync().runRepeating(() -> { updateArena(); }, 0L, 20L).bindWith(terminables); } public void stop() { - // Clean up ALL arena resources at once terminables.close(); } } ``` +`CompositeTerminable` also provides: + +| Method | Description | +|---|---| +| `create()` | New instance with strong references | +| `createWeak()` | New instance with weak references | +| `with(AutoCloseable)` | Add a resource (returns `this` for chaining) | +| `withAll(AutoCloseable...)` | Add multiple resources | +| `withAll(Iterable)` | Add multiple resources from iterable | +| `cleanup()` | Remove already-terminated entries | +| `bind(AutoCloseable)` | Same as `with()`, returns the bound resource | +| `bindModule(TerminableModule)` | Inherited from `TerminableConsumer` | + ## TerminableModule -Organize related functionality: +Encapsulates a group of related resources behind a `setup()` method. The consumer passed to `setup()` handles binding. ```java public class ScoreboardModule implements TerminableModule { @Override public void setup(TerminableConsumer consumer) { - // Events Events.subscribe(PlayerJoinEvent.class) .handler(this::createScoreboard) .bindWith(consumer); - // Tasks Schedulers.sync().runRepeating(() -> { updateScoreboards(); }, 0L, 20L).bindWith(consumer); } - - private void createScoreboard(PlayerJoinEvent event) { - // Create scoreboard for player - } - - private void updateScoreboards() { - // Update all scoreboards - } } -// Usage in plugin public class MyPlugin extends BasePlugin { @Override protected void enable() { - bindModule(new ScoreboardModule()); // Auto-cleanup on disable + bindModule(new ScoreboardModule()); } } ``` -## Complete Example +## Complete example ```java public class MyPlugin extends BasePlugin { @Override protected void enable() { - // Events - auto cleanup Events.subscribe(PlayerJoinEvent.class) .handler(this::onJoin) .bindWith(this); - // Tasks - auto cleanup Schedulers.sync().runRepeating(() -> { saveData(); }, 0L, 6000L).bindWith(this); - // Modules - auto cleanup bindModule(new ChatModule()); bindModule(new ScoreboardModule()); - // Custom resources - auto cleanup DatabaseConnection db = new DatabaseConnection(); bind(db); } @Override protected void disable() { - // All resources automatically cleaned up! - // No manual unregister/cancel needed + // All bound resources are automatically cleaned up. } } ``` -## Benefits - -✅ **No memory leaks** - Resources always cleaned up ✅ **Less boilerplate** - No manual unregister code ✅ **Safer** - Can't forget to clean up ✅ **Organized** - Group related resources together +## Comparison with manual cleanup -## Without Terminables +Without terminables, you must track and cancel every resource individually: ```java -// Traditional approach - easy to forget cleanup! private Task task; private Listener listener; @@ -175,27 +178,26 @@ public void onEnable() { @Override public void onDisable() { - HandlerList.unregisterAll(listener); // Easy to forget! - task.cancel(); // Easy to forget! + HandlerList.unregisterAll(listener); + task.cancel(); } ``` -## With Terminables +With terminables, binding handles all of this: ```java -// PluginBase approach - automatic! @Override protected void enable() { Events.subscribe(PlayerJoinEvent.class) .handler(e -> {}) - .bindWith(this); // Auto cleanup + .bindWith(this); Schedulers.sync().runRepeating(() -> {}, 0L, 20L) - .bindWith(this); // Auto cleanup + .bindWith(this); } @Override protected void disable() { - // Nothing to do - automatic cleanup! + // Nothing needed. } ```