Skip to content

Inline Stroage Backends for Plugins

Petrus Pradella edited this page Jul 29, 2026 · 4 revisions

Inline Backends for Plugins

The core's storage.yml routes EverNifeCore's data (PlayerData, PDSections, accounts). Sometimes a plugin wants to route its own data - a big payload that doesn't belong in a PDSection - to a backend its own config declares. ECStorage.open(...) is that door: a plugin depending on EverNifeCore opens a fresh, plugin-owned EveryDatabase Storage from an inline single-backend block, using the same engine and the same drivers the core already downloaded at boot.

This is the "store a pointer in PlayerData, the volume elsewhere" pattern: keep a small per-player index in a PDSection (routed by the core's storage.yml) and put the fat data on a backend the plugin's config points at.


The inline backend shape

An inline backend is a simplified form of a storage.yml backend: the single child key IS the backend type, enabling is implicit (declaring it is enabling it), and exactly one is declared - there is no enabled and no separate type field.

# in YOUR plugin's config.yml
storage:
  mongo:                          # the child key names the type
    url: "mongodb://localhost:27017"
    db: myplugin

Switching backend means replacing that one block with another type:

storage:
  groupedfile:
    path: plugins/MyPlugin/Data/groupedfile
    format: yaml

Valid types are the same as storage.yml: groupedfile | localfile | sql | postgresql | h2 | mongo | memory (see Storage Backends).


Seeding the block: writeInlineBackendTemplate

Rather than ship a static default, seed the block programmatically so it arrives commented and with your plugin's preferred default. It is idempotent: if a backend is already declared it returns untouched, keeping the admin's choice - safe to call on every load.

import br.com.finalcraft.evernifecore.storage.config.StorageYamlDefaults;
import br.com.finalcraft.everyconfig.config.section.ConfigSection;

ConfigSection storage = config.getConfigSection("storage");

// Convenience default: a groupedfile (YAML) backend, fully documented.
StorageYamlDefaults.writeInlineBackendTemplate(storage, "plugins/MyPlugin/Data");

The full signature lets you pick the seeded type, the file format and a compact vs. full header:

StorageYamlDefaults.writeInlineBackendTemplate(
    storage,
    "plugins/MyPlugin/Data",              // base folder; each file backend gets a /<type> subfolder
    BackendType.MONGO,                    // the type to seed (null = groupedfile)
    null,                                 // FileFormat (null = YAML); ignored for non-file types
    false);                               // false = full type catalog in the comment; true = compact

format is meaningful only for the file backends (groupedfile/localfile); on any other type it is dropped (there is no file format to pick). Each file backend is rooted in its own baseStoragePath/<type> subfolder, so switching type never makes two backends share a directory.


Opening it: ECStorage.open

import br.com.finalcraft.evernifecore.storage.ECStorage;
import br.com.finalcraft.everydatabase.manager.CachingManager;
import br.com.finalcraft.everydatabase.manager.cache.CachePolicy;

// onEnable - the storage is created AND connected before this returns. Passing your ECPluginData
// seeds a groupedfile default under your data folder when the block is empty, and logs through you:
STORAGE = ECStorage.open(getEcPluginData(), config.getConfigSection("storage")).join();

CachingManager<UUID, Snapshot> snapshots = STORAGE.manager(MY_DESCRIPTOR, CachePolicy.always());
snapshots.saveAndCache(new Snapshot(...)).join();

open parses the inline block, creates the storage and init()s (connects) it before completing. The returned ECStorage is a self-contained handle: the live Storage (storage()), the definition it came from (definition()), the RefRegistry its managers register in (refRegistry()), and the defaultCodec(Class) shortcut. A backend that cannot be reached throws a StorageConfigException directly - unwrapped from the CompletionException that join() would otherwise nest it in, so a plain catch (StorageConfigException e) catches it and e.getCause() is the original connection failure. On that failure the half-created storage is torn down for you, so a failed open leaves no orphaned pool. Because the drivers are already on the classpath from the core's boot, whichever type the admin picks just works - no per-plugin driver management.

Overloads let you pass the seed definition explicitly, an explicit StorageLogConfig, or skip the plugin entirely (open(ConfigSection) / open(BackendDefinition) - advanced, and see the reload caveat below).

manager(...) is memoized per entity type, so calling it on every access is the intended style - do not cache the result in a field. A field would survive a reload the manager behind it did not.

Lifecycle: it's yours to close

The handle is yours. It does not touch the core's registry, and you must close it when your plugin disables:

// onDisable
STORAGE.close().join();

Open it only after the core's storage bootstrap has run - i.e. from your onEnable, not onLoad. Opening earlier is not an error, but the handle falls back to a private RefRegistry, and a Ref between it and your PDSections will not resolve; the core warns when it sees that.


Reloads: register the callback, always

A core reload builds fresh per-plugin RefRegistry instances. A handle opened before it keeps the old one, so anything created through it becomes invisible to your PDSections - which were rebound to the new registry. The core detects this and marks the handle detached: it then refuses manager(...), repository(...) and defaultCodec(...) with a message naming the fix, while still allowing flushManagers() and close() so you never lose what was dirty.

The fix is one registration, made once, on enable:

// onEnable, BEFORE the first open - the core warns about a handle nobody would re-open
PlayerController.onStorageReload(getEcPluginData(), this::openStorage);

private void openStorage() {
    STORAGE = ECStorage.openOrReload(getEcPluginData(), config.getConfigSection("storage"), STORAGE).join();
}

openOrReload covers all three entry points, because it tells them apart on its own:

What changed What it does
nothing (same block) reuses the handle, wipes every cache (dirty cells discarded)
only the core's registry rebinds onto the fresh one - the connection stays up
the target backend closes the old handle and connects a new one

Call flushManagers() first if the dirty data must survive the reload. In every case the managers are dropped and you re-derive them with manager(...) - which is exactly why you should not hold one in a field.


The on-disk format comes from your codec

⚠️ Gotcha worth knowing. For a plugin-owned backend opened this way, the file format field in the inline block does not automatically decide how your entities are written to disk - the codec on your EntityDescriptor does. open hands you the storage, but it does not attach a codec to your descriptor for you; your descriptor's codec (e.g. a JacksonJsonCodec) is what serialises your entity. So if you want YAML files, give your descriptor a YAML codec - flipping format: yaml on the inline block alone will not change what your repository writes. The shortcut is STORAGE.defaultCodec(Type.class): it derives exactly the codec the core would use for that format (a file backend picks YAML or pretty JSON; every other backend uses compact JSON), so you can honour the configured format without hand-picking one.

Define your entity and its codec the EveryDatabase way - Defining Entities and Codecs.


See also

Clone this wiki locally