Skip to content

Reading and Writing

GMalvestiti edited this page Aug 31, 2026 · 2 revisions
MyModConfig shared = holder.data();   // cheap, shared — treat as read-only
MyModConfig mine = holder.copy();     // deep copy you own, safe to mutate freely

data() returns the live published snapshot. It is cheap — no locking, no copying — and is the right choice for hot paths. The returned object is shared with every concurrent reader, so mutating it would corrupt the live state. Use copy() when several fields must come from one stable snapshot, or when you need to edit values locally before applying them.

holder.update(config -> config.showHints = false);   // publish in memory only
holder.updateAndSave(config -> config.hudScale = 3); // publish and write to disk

The lambda passed to update receives a private candidate copy, not the live state. The candidate is validated before publication. If validation fails, the live state is never touched and the failed update is reported on the returned UpdateResult. A mutator that throws is treated as a defect and propagates regardless of the failure policy.

UpdateResult result = holder.updateAndSave(config -> config.hudScale = 99);
if (!result.accepted()) {
    result.violations().forEach(v -> LOGGER.warn("{}: {}", v.id(), v.message()));
}

Every holder also provides asynchronous variants that queue work on the shared config worker and return CompletableFuture:

ConfigHolder<MyModConfig> holder = /* ... */;
holder.saveAsync();
holder.updateAndSaveAsync(config -> config.hudScale = 3).thenAccept(result -> { /* ... */ });

All lifecycle operations for one config are serialized on its worker lane. Synchronous methods wait for their queued work; asynchronous methods return its CompletableFuture. Starting another config operation from a mutator or lifecycle hook is rejected as NESTED_CONFIG_OPERATION, so compose follow-up work after the current call or future completes.

Clone this wiki locally