One scheduler for every Mides plugin and service: Loom, Facet, Harbor and Jarvis run their coroutines on the same one, and the only thing that changes between Bukkit, Folia, Velocity, a Ktor server or a plain JVM is how each thread is reached.
| Module | What it is | Who uses it | JVM |
|---|---|---|---|
api |
Threading (the thread contract) and Scheduler (pool + scope + tasks + sync) |
Loom, Facet, Harbor, Jarvis | 8 |
platform-bukkit-legacy |
BukkitThreading, from 1.8.8 up; detect() picks Folia when the server is Folia |
Harbor, Loom bukkit-legacy, Facet | 8 |
platform-folia |
FoliaThreading: global / region / entity / async. Also Paper 1.20+ |
Loom folia and modern | 21 |
platform-velocity |
VelocityThreading |
Harbor, Loom velocity | 17 |
platform-ktor |
KtorThreading and Application.tempo(), one scheduler per application |
Edge and other Ktor services | 17 |
platform-fallback |
FallbackThreading: a dedicated game thread and a pool, with no platform at all |
tools, test harnesses, undetected platforms | 8 |
interface Threading {
val isGlobal: Boolean
fun owns(owner: Any): Boolean
fun global(block: Runnable)
fun owned(owner: Any, block: Runnable, retired: Runnable = Runnable {})
fun async(block: Runnable)
}owned exists because of Folia, where a player or a location lives on the thread of its region. On
every other platform it is global. Code written as scheduler.sync(player) { } is correct everywhere
and only on Folia does something different. Threading.Inline runs everything on the calling thread,
for tests and code with no platform; FallbackThreading is the same idea with real thread separation.
One per plugin. Harbor builds it with the plugin's name and logger and closes it at shutdown; the frameworks running inside the plugin receive that same scheduler.
scheduler.async {
val profile = backend.load(uuid) // on the Tempo-<plugin> pool
sync(player) { player.sendMessage(profile.greeting) } // on the thread that owns the player
}
scheduler.repeatTicks(20) { board.refresh() }
withContext(scheduler.global) { world.spawn(...) } // as a dispatcherThe scheduler is fully thread-safe: launch, cancel, query and close from any thread, concurrently.
Every task is registered before it starts and removed when it completes, so no window leaves an
orphaned entry. close() is idempotent; whatever is launched afterwards is born cancelled and never
runs. A sync whose caller was cancelled while waiting skips its block once the game thread reaches
it. Every Threading implementation is safe to call from any thread — that is part of the contract.
Whoever creates a scheduler closes it. Harbor closes the plugin's; a Loom registry only closes a
scheduler it built itself, never one it was handed. A Threading that owns threads of its own
(FallbackThreading) is closed by its creator, not by the scheduler built on it.