Online data
Load a player's data on join, keep it in memory while they play, flush it on a timer, save it on quit.
The lifecycle every plugin with per-player data writes, and usually writes with at least one bug in it.
OnlineDataManager<Profile> profiles = new OnlineDataManager<>(
plugin, scheduler, store, Duration.ofMinutes(5));
profiles.installListener(plugin); // wires join and quit
profiles.start(); // begins the periodic flush
Profile profile = profiles.get(uuid);
profiles.isLoaded(uuid);
profiles.onlineCount();
profiles.flush();
profiles.stop(); // in onDisable
handleJoin and handleQuit are exposed too, for a host that already has its own listeners and does
not want a second pair registered.
The store seam
DataStore<Profile> store = new DataStore<>() {
@Override public Profile load(UUID id) { return repository.findById(id).orElseGet(Profile::new); }
@Override public void save(UUID id, Profile value) { repository.save(value); }
};
Two blocking methods, keyed by UUID. Point it at a Repository, a WriteBehindStorage, a flat file,
or an in-memory map in a test.
The seam lives in uxmlib-integration rather than depending on uxmlib-storage, which is what keeps
the JDBC stack off consumers who do not want it.
Threading
Every store call runs off the main thread on scheduler.async: the load on join, the save on quit,
and the periodic flush. A blocking backend never stalls the server.
The cache is a concurrent map, so the async hand-back of a loaded value and a main-thread get are
safe without locking.
Your DataStore implementation may block on I/O and must never touch the Bukkit API.
The load is asynchronous, so a player's value is not there the instant they join. Check isLoaded
before acting on get, or handle the null.
Flush policy
The periodic flush saves every currently-online value. It does not consult a dirty flag, so the value type does not need one, and at worst it writes an unchanged value.
That is deliberately simple and safe. If you want dirty-only flushing, layer it into your DataStore:
put a WriteBehindStorage behind the seam and the coalescing happens there.
A load failure
If your load throws, the manager routes the failure to its error sink and leaves that player
unloaded rather than caching a broken value or half-loading. isLoaded then reports false, which is
the signal to deny an action rather than proceed on defaults.
Was this page helpful?