-
Notifications
You must be signed in to change notification settings - Fork 0
Add documentation #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
WalkthroughThis pull request primarily adds and expands JavaDoc documentation across the codebase. Enhancements were made in multiple modules including caching, commands, configuration, menus, and utilities. New annotations, records, and methods have been documented in detail, and minor method signature updates (such as the addition of a non-null annotation) have been introduced. Some new methods (e.g., for updating caches and generating asset paths) were added alongside the documentation improvements, without altering the underlying functionality or overall control flow. Additionally, documentation was added to support user installable apps integration, including explanations of command context usage and detached channel checks, as well as README updates to guide developers on this feature. Changes
Poem
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai summary |
✅ Actions performedSummary regeneration triggered. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai is there anything else that should be given documentation? or anything else that needs to be changed in the documentation before this is merged? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainAfter reviewing the PR files, I'll need to examine the codebase more thoroughly to answer your question completely. Let me gather more information. Let me check for XML documentation comments in the codebase to see what's documented and what might be missing. Let me check the PR diff to understand what documentation was specifically added in this PR. @Scyye After reviewing the codebase, I found a few areas where documentation could be improved:
Would you like me to provide more specific recommendations about which files need documentation improvements? 🏁 Script executed: Length of output: 2211 🏁 Script executed: Length of output: 70002 🏁 Script executed: Length of output: 303 🏁 Script executed: Length of output: 41383 @Scyye After reviewing the PR changes, I'm impressed with the comprehensive documentation added across all files. The Javadoc comments for classes, methods, parameters, and return types are well written and informative. Here are a few suggestions that could further enhance your documentation before merging:
Overall, the documentation is in excellent shape and covers the essential information users would need to understand the library. |
|
@coderabbitai could you possible write a readme file? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 3127 🏁 Script executed: Length of output: 8707 @Scyye Here's a comprehensive README.md for the BotCommons project: # BotCommons
A powerful framework for creating Discord bots using [JDA (Java Discord API)](https://github.com/DV8FromTheWorld/JDA) that simplifies common bot development tasks.



## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Commands Framework](#commands-framework)
- [Config Framework](#config-framework)
- [Menu Framework](#menu-framework)
- [Cache Framework](#cache-framework)
## Features
BotCommons provides several key features to simplify Discord bot development:
- **Commands Framework**: Easy creation and management of slash commands with parameter validation
- **Config Framework**: Simple configuration management for global and per-guild settings
- **Menu Framework**: Interactive menu system for user interactions
- **Cache Framework**: Efficient caching of Discord entities like messages, guilds, channels and members
## Installation
### Maven
Add the following to your `pom.xml`:
```xml
<dependency>
<groupId>dev.scyye</groupId>
<artifactId>BotCommons</artifactId>
<version>1.11</version>
</dependency>GradleAdd the following to your dependencies {
implementation 'dev.scyye:BotCommons:1.11'
}Commands FrameworkThe Commands Framework simplifies the creation and management of slash commands. Creating Commands
import botcommons.commands.*;
// You can also specify a group for the commands in the holder
@CommandHolder
public class PingCommand {
@Command(name = "ping", help = "Pong!")
public void execute(GenericCommandEvent event,
@Param(
description = "A user",
type = Param.ParamType.USER
)
// the name of the argument is grabbed from the parameter name
User user) {
event.replySuccess("Pong! " + user.getAsMention()).finish(message -> {
// Success consumer
});
}
}Setting Up CommandManagerpublic class Main {
public static void main(String[] args) {
JDA jda = JDABuilder.createDefault("token")
.addEventListeners(new CommandManager())
.build();
// Initialize the CommandManager
CommandManager.init(jda);
// Register commands
CommandManager.addCommands(PingCommand.class);
}
}Parameter AnnotationsUse the @Param(
description = "Description of the parameter",
required = true, // Default is true
type = Param.ParamType.USER, // Type of parameter (STRING, INTEGER, USER, etc.)
autocomplete = true // Enable autocomplete (requires implementing AutoCompleteHandler)
)Config FrameworkThe Config Framework provides both global and per-guild configuration management. Global Configpublic static void main(String[] args) {
// Set the bot name for the config file
Config.botName = "MyBot";
// Create a default config
Config config = Config.makeConfig(new HashMap<>() {{
put("prefix", "!");
put("admins", new String[]{"userId1", "userId2"});
put("defaultSettings", new HashMap<String, Object>() {{
put("welcomeMessage", true);
put("loggingEnabled", false);
}});
}});
// Access config values
String prefix = config.get("prefix", String.class);
boolean loggingEnabled = ((Map<String, Object>)config.get("defaultSettings")).get("loggingEnabled");
}Guild Configpublic static void main(String[] args) {
JDA jda = JDABuilder.createDefault("token")
.addEventListeners(new ConfigManager(new HashMap<>() {{
put("prefix", "!");
put("welcomeMessage", true);
put("loggingEnabled", false);
}}))
.build();
// Later, get a guild's config
Config guildConfig = event.getConfig();
boolean welcomeMessage = guildConfig.get("welcomeMessage", Boolean.class);
// Update a value
ConfigManager.getInstance().setValue(guildId, "welcomeMessage", false);
}Note: This will automatically create the Menu FrameworkThe Menu Framework allows you to create interactive menus with buttons and pagination. Setting Up MenuManagerpublic static void main(String[] args) {
JDA jda = JDABuilder.createDefault("token")
.build();
// Create menu manager instance
MenuManager menuManager = new MenuManager(jda);
}Creating and Registering Menus// Create a simple menu implementation
IMenu helpMenu = new IMenu() {
@Override
public String getMenuId() {
return "help_menu";
}
@Override
public MessageCreateAction generateMenu(Object... args) {
return new MessageCreateAction()
.setContent("Help Menu")
.addActionRow(
Button.primary("prev", "Previous"),
Button.primary("next", "Next")
);
}
@Override
public void handleButtonInteraction(ButtonInteractionEvent event) {
String buttonId = event.getComponentId();
if (buttonId.equals("prev")) {
// Handle previous button
} else if (buttonId.equals("next")) {
// Handle next button
}
}
};
// Register the menu
menuManager.registerMenu(helpMenu);
// Send the menu to a channel
menuManager.sendMenu("help_menu", channelId);
// Or reply to a command with a menu
event.replyMenu("help_menu").finish();Cache FrameworkThe Cache Framework allows efficient caching of Discord entities for improved performance. Initializing Cachepublic static void main(String[] args) {
JDA jda = JDABuilder.createDefault("token")
.build();
// Initialize cache with various options
CacheManager.init(
jda,
true, // Cache guild members
true, // Cache mutual guilds
true, // Cache channel messages
true, // Cache user messages
true // Cache users
);
}Accessing Cached Data// Access cached members for a guild
List<CacheManager.MemberStructure> members = CacheManager.guildMemberCache.get(guildId);
// Access cached messages
List<CacheManager.MessageStructure> messages = CacheManager.channelMessageCache.get(channelId);
// Update the cache to JSON files
CacheManager.update();ContributingContributions are welcome! Please feel free to submit a Pull Request. LicenseThis project is licensed under the MIT License - see the LICENSE file for details. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🔭 Outside diff range comments (12)
src/main/java/botcommons/commands/ReplyContext.java (1)
138-154: Combined content + embeds are silently ignored
The current dispatch logic never executes when bothcontentandembedsare provided, resulting in no message being sent.- } else if (embeds.isEmpty()) { + } else if (embeds.isEmpty()) { … + } else { // both content and embeds present + if (!interactionEvent.isAcknowledged()) + action = interactionEvent.reply(content).addEmbeds(getEmbeds()); + else + action2 = interactionEvent.getHook().sendMessage(content).addEmbeds(getEmbeds()); }src/main/java/botcommons/config/Config.java (1)
27-27: Use Path.of() for better path handling.String concatenation for file paths can cause issues on different operating systems. Consider using
Path.of()for cross-platform compatibility:- String fileName = botName + "-assets\\config.json"; + String fileName = Path.of(botName + "-assets", "config.json").toString();src/main/java/botcommons/utilities/StringUtilities.java (1)
30-45: Excellent documentation but consider brevity.The method logic is sound, but the JavaDoc could be more concise while maintaining clarity. The generic type parameters are well explained.
src/main/java/botcommons/commands/CommandInfo.java (2)
24-43: Avoid double-brace initialization foroptionTypeParamsThe anonymous subclass created by the double-brace initializer keeps an implicit reference to the outer
CommandInfoclass and may increase memory usage. It also makes the map unmodifiable only by convention.-private static final HashMap<Class<?>, OptionType> optionTypeParams = new HashMap<>() {{ - put(String.class, OptionType.STRING); - ... -}}; +private static final Map<Class<?>, OptionType> OPTION_TYPE_PARAMS; +static { + Map<Class<?>, OptionType> map = new HashMap<>(); + map.put(String.class, OptionType.STRING); + ... + OPTION_TYPE_PARAMS = Collections.unmodifiableMap(map); +}
66-74: JavaDoc promises non-null but implementation can return null
from(@Nullable Method)returnsnullwhen the argument isnull, yet the JavaDoc states it “returns aCommandInfoinstance”. Update the JavaDoc (or remove@Nullableand throw) to avoid misleading users.src/main/java/botcommons/cache/CacheManager.java (2)
30-35: Use thread-safe collections for shared caches
guildMemberCache,mutualGuildsCache,channelMessageCache,userMessageCache, anduserCacheare written from multiple JDA event threads without synchronization. ReplaceHashMap/ArrayListwithConcurrentHashMap/ thread-safe lists or introduce proper locks to prevent data races andConcurrentModificationExceptions.
119-123: Potential NPE when a user leaves their last mutual guild
mutualGuildsCache.get(event.getUser().getId())can benullif the user was never cached (e.g., joins & immediately leaves).
Guard the call:-List<GuildStructure> guilds = mutualGuildsCache.get(event.getUser().getId()); -if (guilds != null) { - guilds.remove(GuildStructure.fromGuild(event.getGuild())); -} +mutualGuildsCache.computeIfPresent( + event.getUser().getId(), + (_, guilds) -> { + guilds.remove(GuildStructure.fromGuild(event.getGuild())); + return guilds.isEmpty() ? null : guilds; + });src/main/java/botcommons/commands/CommandManager.java (3)
64-88: Commands from non-group holders are added twiceWhen
meta.group()equals"n/a"you first iterate once (lines 65-77) and then immediately iterate again (lines 82-87), inserting duplicates incommands.
Early-return after the first loop or merge the loops to avoid duplicate registrations.
254-267: PreferEnumMapover rawHashMapfortypeMap
OptionTypeis an enum;EnumMapoffers faster lookups and lower memory footprint.-private static final HashMap<OptionType, Class<?>> typeMap = new HashMap<>() {{ +private static final EnumMap<OptionType, Class<?>> typeMap = new EnumMap<>(OptionType.class) {{
275-330:getCommandhas quadratic scans and subtle alias edge-cases
- Two nested linear searches for every lookup → O(n²) worst-case on aliases.
- For subcommands, matching is string-split based and ignores alias groups.
Consider indexing once into two
Map<String, Method>(one for root commands, one for full subcommand paths) during registration for O(1) lookups and simplified alias handling.src/main/java/botcommons/commands/GenericCommandEvent.java (2)
274-278:replyMenumutates the menu ID and logs tostdoutAppending
"-fake"to the providedidbreaks caller expectations and makes it impossible to retrieve the same menu later.
Also, prefer SLF4J logger overSystem.out.-MenuManager.registerMenuWithId(id+"-fake", menu); -System.out.println("Registered menu with id: " + id); -return this.replyContext.menu(id+"-fake", args); +MenuManager.registerMenuWithId(id, menu); +logger.debug("Registered menu with id: {}", id); +return this.replyContext.menu(id, args);
162-181: Repeatednew Gson()on everygetArgcallInstantiate
Gsononce as a static field to avoid unnecessary object creation on hot paths.private static final Gson GSON = new Gson(); ... return GSON.fromJson(option.getDefaultValue(), type);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (14)
src/main/java/botcommons/cache/CacheManager.java(4 hunks)src/main/java/botcommons/commands/AutoCompleteHandler.java(1 hunks)src/main/java/botcommons/commands/Command.java(1 hunks)src/main/java/botcommons/commands/CommandHolder.java(1 hunks)src/main/java/botcommons/commands/CommandInfo.java(4 hunks)src/main/java/botcommons/commands/CommandManager.java(4 hunks)src/main/java/botcommons/commands/GenericCommandEvent.java(5 hunks)src/main/java/botcommons/commands/Param.java(1 hunks)src/main/java/botcommons/commands/ReplyContext.java(2 hunks)src/main/java/botcommons/config/Config.java(3 hunks)src/main/java/botcommons/config/ConfigManager.java(1 hunks)src/main/java/botcommons/menu/MenuManager.java(5 hunks)src/main/java/botcommons/utilities/JsonUtils.java(3 hunks)src/main/java/botcommons/utilities/StringUtilities.java(2 hunks)
🔇 Additional comments (18)
src/main/java/botcommons/commands/CommandHolder.java (1)
5-7: Documentation looks good
No functional changes and the Javadoc accurately describes the annotation purpose.src/main/java/botcommons/config/Config.java (1)
54-71: LGTM! Good error handling and validation.The JavaDoc is comprehensive and the method properly validates required configuration values before returning the config instance.
src/main/java/botcommons/utilities/StringUtilities.java (1)
47-65: Well-implemented asset path utility with good error handling.The method properly handles directory creation with appropriate logging. The implementation is robust and follows good practices.
src/main/java/botcommons/config/ConfigManager.java (5)
37-43: LGTM! Clear and concise documentation.The JavaDoc appropriately describes the method's purpose and parameter usage.
45-53: Good documentation with clear parameter explanations.The JavaDoc effectively explains both the server-specific configuration setting and the file persistence aspect.
55-65: Well-documented method with comprehensive parameter descriptions.The documentation clearly explains the method's behavior and parameter requirements.
67-78: Excellent generic method documentation.The JavaDoc properly explains the generic type parameter and return behavior, including null handling.
80-121: Well-documented private method with robust implementation.The documentation clearly explains the file writing process, and the implementation includes proper retry logic with exponential backoff and comprehensive error handling.
src/main/java/botcommons/utilities/JsonUtils.java (4)
19-21: LGTM! Appropriate class-level documentation.The class documentation clearly explains the utility's purpose.
23-41: Good documentation for the GSON instance.The documentation explains the static GSON instance's role in the utility class.
44-60: Well-documented cache creation method.The JavaDoc clearly explains the method's behavior and parameters, including the conditional file creation logic.
62-97: Excellent documentation and robust implementation.The method documentation is clear, and the implementation includes proper error handling, data merging, and thread safety considerations.
src/main/java/botcommons/menu/MenuManager.java (6)
25-33: LGTM! Clear constructor documentation.The JavaDoc appropriately explains the constructor's purpose and JDA integration.
35-45: Good documentation for menu registration.The JavaDoc clearly explains the registration process and annotation requirement.
47-60: Excellent documentation with proper validation.The method includes good parameter validation and clear documentation explaining the dynamic registration capability.
62-81: Well-documented menu sending method.The documentation clearly explains the channel-based menu sending functionality.
83-98: Good documentation for private menu sending.The JavaDoc appropriately explains the private channel menu delivery process.
126-137: Clear documentation for menu reply functionality.The JavaDoc effectively explains the interaction hook-based menu reply system with dynamic argument support.
| /** | ||
| * Updates all caches to their respective JSON files. | ||
| */ | ||
| public static void update() { | ||
| JsonUtils.updateCache(guildMemberCache, "guild_member_cache"); | ||
| JsonUtils.updateCache(mutualGuildsCache, "mutual_guilds_cache"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
update() is IO-heavy; call frequency is very high
update() writes every cache to disk on every message receive/delete/member update.
Consider debouncing (scheduled executor) or batching updates to avoid disk thrashing, especially in large guilds.
🤖 Prompt for AI Agents
In src/main/java/botcommons/cache/CacheManager.java around lines 58 to 63, the
update() method writes all caches to disk on every call, causing excessive IO
operations. To fix this, implement a debouncing mechanism using a scheduled
executor or a similar scheduler to batch multiple update requests and write to
disk less frequently, reducing disk thrashing and improving performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you provide an example of implementation for that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Scyye Here's a complete implementation example that adds debouncing to reduce excessive disk IO:
package botcommons.cache;
import botcommons.utilities.JsonUtils;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* CacheManager is a utility class to manage various caches for JDA entities.
*/
@SuppressWarnings("unused")
public class CacheManager extends ListenerAdapter {
private CacheManager() {}
public static final HashMap<String, List<MemberStructure>> guildMemberCache = new HashMap<>();
public static final HashMap<String, List<GuildStructure>> mutualGuildsCache = new HashMap<>();
public static final HashMap<String, List<MessageStructure>> channelMessageCache = new HashMap<>();
public static final HashMap<String, List<MessageStructure>> userMessageCache = new HashMap<>();
public static final HashMap<String, User> userCache = new HashMap<>();
// Debouncing mechanism
private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "CacheManager-Writer");
t.setDaemon(true);
return t;
});
private static final AtomicBoolean updatesPending = new AtomicBoolean(false);
private static final long DEBOUNCE_DELAY_SECONDS = 5; // Write to disk every 5 seconds max
/**
* Initializes the CacheManager with the provided JDA instance and cache options.
* @param jda The JDA instance to fetch data from. This cannot be null.
* @param guildMembers Whether to enable the guild member cache.
* @param mutualGuilds Whether to enable the mutual guilds cache.
* @param channelMessages Whether to enable the channel messages cache.
* @param userMessages Whether to enable the user messages cache.
* @param users Whether to enable the user cache.
*/
public static void init(@NotNull JDA jda, boolean guildMembers, boolean mutualGuilds, boolean channelMessages, boolean userMessages, boolean users) {
if (!guildMembers && !mutualGuilds && !channelMessages && !userMessages && !users)
throw new IllegalArgumentException("At least one cache must be enabled");
jda.addEventListener(new CacheManager());
if (guildMembers) JsonUtils.createCache(guildMemberCache, "guild_member_cache");
if (mutualGuilds) JsonUtils.createCache(mutualGuildsCache, "mutual_guilds_cache");
if (channelMessages) JsonUtils.createCache(channelMessageCache, "channel_message_cache");
if (userMessages) JsonUtils.createCache(userMessageCache, "user_message_cache");
if (users) JsonUtils.createCache(userCache, "user_cache");
// Schedule periodic cleanup to ensure updates are written even if no new events occur
scheduler.scheduleWithFixedDelay(() -> {
if (updatesPending.get()) {
forceUpdate();
}
}, DEBOUNCE_DELAY_SECONDS, DEBOUNCE_DELAY_SECONDS, TimeUnit.SECONDS);
}
/**
* Schedules cache updates to be written to disk after a short delay.
* This reduces IO operations by batching multiple rapid updates together.
*/
public static void update() {
if (updatesPending.compareAndSet(false, true)) {
scheduler.schedule(() -> {
forceUpdate();
}, DEBOUNCE_DELAY_SECONDS, TimeUnit.SECONDS);
}
}
/**
* Immediately writes all caches to their respective JSON files.
* Use sparingly - prefer update() for better performance.
*/
public static void forceUpdate() {
updatesPending.set(false);
JsonUtils.updateCache(guildMemberCache, "guild_member_cache");
JsonUtils.updateCache(mutualGuildsCache, "mutual_guilds_cache");
JsonUtils.updateCache(channelMessageCache, "channel_message_cache");
JsonUtils.updateCache(userMessageCache, "user_message_cache");
JsonUtils.updateCache(userCache, "user_cache");
}
/**
* Shuts down the cache manager and ensures all pending updates are written.
* Call this when your application is shutting down.
*/
public static void shutdown() {
forceUpdate(); // Write any pending updates
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
if (!event.isFromGuild()) return;
channelMessageCache.putIfAbsent(event.getChannel().getId(), new ArrayList<>());
channelMessageCache.get(event.getChannel().getId()).add(MessageStructure.fromMessage(event.getMessage()));
if (event.getMember() == null) return;
userMessageCache.putIfAbsent(event.getAuthor().getId(), new ArrayList<>());
userMessageCache.get(event.getAuthor().getId()).add(MessageStructure.fromMessage(event.getMessage()));
update(); // Now schedules instead of immediately writing
}
@Override
public void onMessageDelete(@NotNull MessageDeleteEvent event) {
if (!event.isFromGuild()) return;
channelMessageCache.get(event.getChannel().getId()).removeIf(messageStructure ->
messageStructure.id().equals(event.getMessageId()));
userMessageCache.forEach((member, messageStructures) ->
messageStructures.removeIf(messageStructure -> messageStructure.id().equals(event.getMessageId())));
update(); // Now schedules instead of immediately writing
}
@Override
public void onGuildReady(@NotNull GuildReadyEvent event) {
event.getGuild().loadMembers().onSuccess(members -> {
System.out.printf("Loaded %d members for guild %s%n", members.size(), event.getGuild().getName());
guildMemberCache.put(event.getGuild().getId(), members.stream().map(MemberStructure::fromMember).toList());
members.forEach(member -> {
mutualGuildsCache.putIfAbsent(member.getUser().getId(), new ArrayList<>());
mutualGuildsCache.get(member.getUser().getId()).add(GuildStructure.fromGuild(event.getGuild()));
userCache.putIfAbsent(member.getUser().getId(), member.getUser());
});
forceUpdate(); // Guild ready is infrequent, write immediately
}).onError(Throwable::printStackTrace);
}
@Override
public void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) {
guildMemberCache.get(event.getGuild().getId()).add(MemberStructure.fromMember(event.getMember()));
mutualGuildsCache.putIfAbsent(event.getUser().getId(), new ArrayList<>());
mutualGuildsCache.get(event.getUser().getId()).add(GuildStructure.fromGuild(event.getGuild()));
update(); // Schedule update
}
@Override
public void onGuildMemberRemove(@NotNull GuildMemberRemoveEvent event) {
guildMemberCache.get(event.getGuild().getId()).removeIf(memberStructure ->
memberStructure.id().equals(event.getUser().getId()));
mutualGuildsCache.get(event.getUser().getId()).remove(GuildStructure.fromGuild(event.getGuild()));
update(); // Schedule update
}
// ... rest of the record classes remain the same
}Key improvements:
- ScheduledExecutorService: Manages delayed writes using a dedicated daemon thread
- AtomicBoolean flag: Tracks if updates are pending to avoid scheduling multiple writes
- Debounced update(): Schedules writes after 5 seconds, only if no write is already scheduled
- forceUpdate(): Immediately writes to disk (used for infrequent but important events like guild ready)
- Periodic cleanup: Ensures updates are eventually written even if no new events occur
- Proper shutdown:
shutdown()method to gracefully close the executor and flush pending updates
Usage notes:
- Call
CacheManager.shutdown()when your bot shuts down to ensure all data is saved - Adjust
DEBOUNCE_DELAY_SECONDSbased on your needs (5 seconds is a good balance) - Critical events like
onGuildReadyuseforceUpdate()for immediate persistence
This reduces disk IO from potentially hundreds of writes per minute to at most one write every 5 seconds, dramatically improving performance in active guilds! 🐰
✨ Thoughtful rabbit implementing efficient caching patterns ✨
(\ /)
( .. ) 💾
o(")(")
| /** | ||
| * The name of the command that this auto complete handler is for. | ||
| * @return value | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Clarify the @return description
The tag currently returns a generic “value”, which does not add information beyond the method name. Consider making it explicit, e.g. “an array with the names of the command(s) this handler applies to”, to improve readability for API consumers.
- * @return value
+ * @return the names of the command(s) this auto-complete handler supports📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * The name of the command that this auto complete handler is for. | |
| * @return value | |
| */ | |
| /** | |
| * The name of the command that this auto complete handler is for. | |
| * @return the names of the command(s) this auto-complete handler supports | |
| */ |
🤖 Prompt for AI Agents
In src/main/java/botcommons/commands/AutoCompleteHandler.java lines 8 to 11, the
Javadoc @return tag description is too generic and uninformative. Update the
@return description to explicitly state that it returns an array with the names
of the command(s) this handler applies to, improving clarity for API users.
| /** | ||
| * This annotation is used to define a command for the bot. | ||
| * <p> | ||
| * The name, help, aliases, usage, category and permission can be specified. | ||
| * <p> | ||
| * The userContext defines where this command can be used (e.g. GUILD, PRIVATE_CHANNEL) | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Mismatch between Javadoc and default userContext values
The comment mentions PRIVATE_CHANNEL, but the default array contains BOT_DM. Either update the docs or the default to avoid confusion.
- * The userContext defines where this command can be used (e.g. GUILD, PRIVATE_CHANNEL)
+ * The userContext defines where this command can be used (e.g. GUILD, BOT_DM)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * This annotation is used to define a command for the bot. | |
| * <p> | |
| * The name, help, aliases, usage, category and permission can be specified. | |
| * <p> | |
| * The userContext defines where this command can be used (e.g. GUILD, PRIVATE_CHANNEL) | |
| */ | |
| /** | |
| * This annotation is used to define a command for the bot. | |
| * <p> | |
| * The name, help, aliases, usage, category and permission can be specified. | |
| * <p> | |
| * The userContext defines where this command can be used (e.g. GUILD, BOT_DM) | |
| */ |
🤖 Prompt for AI Agents
In src/main/java/botcommons/commands/Command.java around lines 11 to 17, the
Javadoc mentions the userContext value "PRIVATE_CHANNEL" but the default
userContext array uses "BOT_DM". To fix this mismatch, either update the Javadoc
to mention "BOT_DM" instead of "PRIVATE_CHANNEL" or change the default
userContext array to include "PRIVATE_CHANNEL" to keep the documentation and
code consistent.
| boolean isSlashCommand() { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Deprecated isSlashCommand() is still package-private
If the method is truly obsolete, mark it private or remove it entirely; keeping it accessible clutters the API surface and encourages accidental use.
🤖 Prompt for AI Agents
In src/main/java/botcommons/commands/GenericCommandEvent.java around lines 154
to 156, the method isSlashCommand() is deprecated but still package-private,
which unnecessarily exposes it. To fix this, either change the method's access
modifier to private if it must remain for internal use, or remove the method
entirely if it is no longer needed, thereby reducing the API surface and
preventing accidental usage.
| /** | ||
| * The name of the parameter. | ||
| */ | ||
| String description(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect description of the description attribute
The attribute stores the description of the parameter, not its name.
- * The name of the parameter.
+ * A human-readable description of the parameter shown in the command UI.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * The name of the parameter. | |
| */ | |
| String description(); | |
| /** | |
| * A human-readable description of the parameter shown in the command UI. | |
| */ | |
| String description(); |
🤖 Prompt for AI Agents
In src/main/java/botcommons/commands/Param.java around lines 13 to 16, the
Javadoc comment for the description() method incorrectly states it stores the
name of the parameter. Update the comment to correctly indicate that this
attribute stores the description of the parameter.
| /** | ||
| * Adds a listener for a specific event type that will only be called once. | ||
| * @param eventType The type of event to listen for. This should be a subclass of {@link GenericEvent}. | ||
| * @param filter A predicate to filter the events. This allows you to specify conditions for the events that should trigger the listener. | ||
| * @param listener The listener function that will be called when the event is received. This should be a function that takes an instance of the event type and returns Void. | ||
| * @return The current instance of {@link ReplyContext} for chaining. | ||
| * @param <T> The type of the event to listen for. This should be a subclass of {@link GenericEvent}. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Minor Javadoc inconsistency
The listener parameter is described as “returns Void”, but the functional interface already enforces that. Removing that clause keeps the wording tight.
🤖 Prompt for AI Agents
In src/main/java/botcommons/commands/ReplyContext.java around lines 80 to 87,
the Javadoc for the listener parameter redundantly states that the listener
function "returns Void." Since the functional interface already enforces this,
remove the phrase "and returns Void" from the listener parameter description to
keep the documentation concise and clear.
| /** | ||
| * This method finalizes the reply context and sends the message to Discord. It will handle all the cases for sending a message, including: | ||
| * @param consumer A consumer that will be called with the resulting message after it has been sent. This allows you to perform additional actions on the message, such as logging or further processing. | ||
| * @return true if the reply was successfully sent, false otherwise. This method will also mark the reply context as finished. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Improve Javadoc structure
The list marker “including:” is left open without bullet points. Either add bullets or remove the word to avoid dangling wording.
🤖 Prompt for AI Agents
In src/main/java/botcommons/commands/ReplyContext.java around lines 106 to 110,
the Javadoc comment uses the phrase "including:" but does not provide any bullet
points, leaving the list incomplete. To fix this, either remove the word
"including:" to avoid dangling wording or add proper bullet points detailing the
cases handled for sending a message, ensuring the Javadoc is clear and
well-structured.
| * Creates a new Config instance from the provided old values and bot name. | ||
| * @param oldValues A map containing old values to be used in the configuration. This can include key-value pairs that will be added to the new config. | ||
| * @param botName The name of the bot for which the configuration is being created. This will be used to set the "bot-name" key in the configuration. | ||
| * @return Returns a new instance of the Config class, which is a HashMap containing the provided old values and additional default values. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Consider more concise JavaDoc documentation.
The JavaDoc is overly verbose and repetitive. Consider simplifying while maintaining clarity:
- * Creates a new Config instance from the provided old values and bot name.
- * @param oldValues A map containing old values to be used in the configuration. This can include key-value pairs that will be added to the new config.
- * @param botName The name of the bot for which the configuration is being created. This will be used to set the "bot-name" key in the configuration.
- * @return Returns a new instance of the Config class, which is a HashMap containing the provided old values and additional default values.
+ * Creates a new Config instance from old values and bot name.
+ * @param oldValues Map of existing configuration values to preserve
+ * @param botName Bot name used for directory creation and config identification
+ * @return New Config instance with merged values and defaults📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * Creates a new Config instance from the provided old values and bot name. | |
| * @param oldValues A map containing old values to be used in the configuration. This can include key-value pairs that will be added to the new config. | |
| * @param botName The name of the bot for which the configuration is being created. This will be used to set the "bot-name" key in the configuration. | |
| * @return Returns a new instance of the Config class, which is a HashMap containing the provided old values and additional default values. | |
| */ | |
| * Creates a new Config instance from old values and bot name. | |
| * @param oldValues Map of existing configuration values to preserve | |
| * @param botName Bot name used for directory creation and config identification | |
| * @return New Config instance with merged values and defaults | |
| */ |
🤖 Prompt for AI Agents
In src/main/java/botcommons/config/Config.java around lines 17 to 21, the
JavaDoc for the method is too verbose and repetitive. Simplify the documentation
by summarizing the purpose of the method clearly and concisely, focusing on key
points such as creating a new Config instance using old values and setting the
bot name, without redundant explanations of parameters or return values.
| /** | ||
| * Writes the current configuration to a JSON file. | ||
| * @throws IOException If an I/O error occurs while writing to the file. This can happen if the file is not writable, the directory does not exist, or there are permission issues. | ||
| */ | ||
| public void write() throws IOException { | ||
| Files.writeString(Path.of(Config.instance.get("bot-name") + "-assets", "config.json"), toString()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null check for Config.instance.
The write() method assumes Config.instance is not null, but this could lead to a NullPointerException if called before instance initialization:
public void write() throws IOException {
+ if (Config.instance == null) {
+ throw new IllegalStateException("Config instance not initialized");
+ }
Files.writeString(Path.of(Config.instance.get("bot-name") + "-assets", "config.json"), toString());
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Writes the current configuration to a JSON file. | |
| * @throws IOException If an I/O error occurs while writing to the file. This can happen if the file is not writable, the directory does not exist, or there are permission issues. | |
| */ | |
| public void write() throws IOException { | |
| Files.writeString(Path.of(Config.instance.get("bot-name") + "-assets", "config.json"), toString()); | |
| } | |
| /** | |
| * Writes the current configuration to a JSON file. | |
| * @throws IOException If an I/O error occurs while writing to the file. This can happen if the file is not writable, the directory does not exist, or there are permission issues. | |
| */ | |
| public void write() throws IOException { | |
| if (Config.instance == null) { | |
| throw new IllegalStateException("Config instance not initialized"); | |
| } | |
| Files.writeString(Path.of(Config.instance.get("bot-name") + "-assets", "config.json"), toString()); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/botcommons/config/Config.java around lines 96 to 102, the
write() method uses Config.instance without checking if it is null, which can
cause a NullPointerException if the instance is not initialized. Add a null
check for Config.instance at the start of the write() method and handle the case
appropriately, such as throwing an informative exception or returning early, to
prevent the method from proceeding when Config.instance is null.
| /** | ||
| * This method converts a HashMap with generic keys and values to a HashMap with String keys and String values by serializing each key and value to JSON format. | ||
| * @param map The input HashMap with generic keys and values. This map can contain any type of objects as keys and values. | ||
| * @return A new {@link HashMap} where each key and value from the input map has been serialized to a JSON string. This allows for easy storage or transmission of the map's contents in a standardized format. | ||
| */ | ||
| public static HashMap<String, String> stringifyMap(HashMap<?, ?> map) { | ||
| HashMap<String, String> stringMap = new HashMap<>(); | ||
| Gson gson = new GsonBuilder().setPrettyPrinting().create(); | ||
| map.forEach((key, value) -> stringMap.put(gson.toJson(key), gson.toJson(value))); | ||
| return stringMap; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Simplify JavaDoc for better readability.
The documentation is overly verbose. Consider a more concise approach:
- * This method converts a HashMap with generic keys and values to a HashMap with String keys and String values by serializing each key and value to JSON format.
- * @param map The input HashMap with generic keys and values. This map can contain any type of objects as keys and values.
- * @return A new {@link HashMap} where each key and value from the input map has been serialized to a JSON string. This allows for easy storage or transmission of the map's contents in a standardized format.
+ * Converts a HashMap to String keys and values by JSON serialization.
+ * @param map Input map with any key/value types
+ * @return HashMap with JSON-serialized keys and values📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * This method converts a HashMap with generic keys and values to a HashMap with String keys and String values by serializing each key and value to JSON format. | |
| * @param map The input HashMap with generic keys and values. This map can contain any type of objects as keys and values. | |
| * @return A new {@link HashMap} where each key and value from the input map has been serialized to a JSON string. This allows for easy storage or transmission of the map's contents in a standardized format. | |
| */ | |
| public static HashMap<String, String> stringifyMap(HashMap<?, ?> map) { | |
| HashMap<String, String> stringMap = new HashMap<>(); | |
| Gson gson = new GsonBuilder().setPrettyPrinting().create(); | |
| map.forEach((key, value) -> stringMap.put(gson.toJson(key), gson.toJson(value))); | |
| return stringMap; | |
| } | |
| /** | |
| * Converts a HashMap to String keys and values by JSON serialization. | |
| * @param map Input map with any key/value types | |
| * @return HashMap with JSON-serialized keys and values | |
| */ | |
| public static HashMap<String, String> stringifyMap(HashMap<?, ?> map) { | |
| HashMap<String, String> stringMap = new HashMap<>(); | |
| Gson gson = new GsonBuilder().setPrettyPrinting().create(); | |
| map.forEach((key, value) -> stringMap.put(gson.toJson(key), gson.toJson(value))); | |
| return stringMap; | |
| } |
🤖 Prompt for AI Agents
In src/main/java/botcommons/utilities/StringUtilities.java around lines 18 to
28, the JavaDoc for the stringifyMap method is too verbose. Simplify it by
briefly stating that the method converts a map with any key and value types into
a map with JSON string keys and values, suitable for standardized storage or
transmission, without excessive detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
README.md(1 hunks)src/main/java/botcommons/commands/CommandInfo.java(4 hunks)src/main/java/botcommons/config/ConfigManager.java(2 hunks)src/main/java/botcommons/menu/types/PageMenu.java(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
README.md (3)
Learnt from: Scyye
PR: #41
File: src/main/java/botcommons/commands/Command.java:20-20
Timestamp: 2025-03-20T23:21:31.217Z
Learning: In the BotCommons project, the replacement of custom Scope enum with JDA's InteractionContextType enum in the Command interface was about utilizing JDA's built-in functionality rather than expanding context types.
Learnt from: Scyye
PR: #41
File: src/main/java/botcommons/commands/Command.java:20-20
Timestamp: 2025-03-20T23:21:31.218Z
Learning: In the BotCommons project, the change from custom Scope enum to JDA's InteractionContextType is about aligning with Discord API standards rather than expanding command usage contexts.
Learnt from: Scyye
PR: #41
File: src/main/java/botcommons/commands/Command.java:20-20
Timestamp: 2025-03-20T23:21:31.218Z
Learning: In the BotCommons project, the replacement of custom Scope enum with JDA's InteractionContextType enum is about leveraging JDA's built-in functionality rather than maintaining a custom solution for defining where commands can be used.
🪛 LanguageTool
README.md
[style] ~258-~258: Consider using a less common alternative to make your writing sound more unique and professional.
Context: ...ontributing Contributions are welcome! Please feel free to submit a Pull Request. ## License Thi...
(FEEL_FREE_TO_STYLE_ME)
🔇 Additional comments (6)
src/main/java/botcommons/menu/types/PageMenu.java (1)
64-65: Excellent UI improvement!Adding the "end" button makes the interface more intuitive by providing users with a visible way to close the menu. This change properly aligns the button array with the existing logic in the
handle()method that already supports the "end" button functionality.src/main/java/botcommons/config/ConfigManager.java (1)
37-84: Excellent comprehensive documentation!The JavaDoc comments are thorough and well-structured, providing clear explanations of parameters, return types, and method behavior. The generic type documentation for
getValue()is particularly helpful for developers.src/main/java/botcommons/commands/CommandInfo.java (3)
19-21: Good class-level documentation.Clear and concise class documentation that explains the purpose of the CommandInfo class.
27-39: Valuable enhancement: primitive type support.Adding primitive type mappings to the
optionTypeParamsHashMap is excellent. This prevents potential issues when command methods use primitive parameters instead of their wrapper classes, ensuring consistent OptionType mapping.
64-77: Comprehensive method documentation.The JavaDoc comments for
getOption(),from()methods, and theOptioninner class are well-written and provide clear explanations of parameters, return values, and functionality.Also applies to: 119-123, 132-134
README.md (1)
1-263: Outstanding comprehensive documentation!This README provides excellent coverage of all framework components with clear, practical examples. The structure with badges, table of contents, and detailed usage guides makes it highly accessible for developers. The code examples are well-structured and demonstrate proper usage patterns that align with the JavaDoc enhancements throughout the codebase.
|
|
||
| ## Contributing | ||
|
|
||
| Contributions are welcome! Please feel free to submit a Pull Request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Minor style improvement suggestion.
Consider replacing "feel free to" with a more direct phrase for a more professional tone.
-Contributions are welcome! Please feel free to submit a Pull Request.
+Contributions are welcome! Please submit a Pull Request.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Contributions are welcome! Please feel free to submit a Pull Request. | |
| Contributions are welcome! Please submit a Pull Request. |
🧰 Tools
🪛 LanguageTool
[style] ~258-~258: Consider using a less common alternative to make your writing sound more unique and professional.
Context: ...ontributing Contributions are welcome! Please feel free to submit a Pull Request. ## License Thi...
(FEEL_FREE_TO_STYLE_ME)
🤖 Prompt for AI Agents
In README.md at line 258, replace the phrase "feel free to" with a more direct
and professional alternative such as "submit" or "welcome to submit" to improve
the tone of the contribution invitation.
|
|
||
| writeConfigToFile(serverId, configs.get(serverId)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify the immediate config persistence doesn't cause performance issues.
Adding writeConfigToFile() after loading ensures immediate persistence, which is good for data integrity. However, ensure this doesn't cause performance issues during bot startup when multiple guilds become ready simultaneously.
🏁 Script executed:
#!/bin/bash
# Check if there are any rate limiting or concurrent access protections in the writeConfigToFile method
ast-grep --pattern $'writeConfigToFile($_, $_) {
$$$
}'Length of output: 58
Let’s locate the actual implementation of writeConfigToFile(...) to inspect for any throttling or synchronization logic.
🏁 Script executed:
#!/bin/bash
# Find all occurrences and surrounding context of writeConfigToFile in Java sources
rg -n 'writeConfigToFile' -C 5 --glob '*.java'Length of output: 4519
Let’s inspect the full body of writeConfigToFile to confirm its retry logic and any synchronization or throttling:
🏁 Script executed:
#!/bin/bash
# Dump lines 85–120 of ConfigManager.java to review writeConfigToFile implementation
sed -n '85,120p' src/main/java/botcommons/config/ConfigManager.javaLength of output: 1456
Ensure config writes don’t block bot startup
The writeConfigToFile(...) method runs blocking I/O (creating directories + writing JSON) with up to 3 retries and exponential backoff on the caller’s thread. When dozens of guilds become ready simultaneously, this can introduce significant startup latency.
Suggested fixes:
- Offload all file writes to a dedicated executor (e.g. a single-threaded
ExecutorService) so the main event threads aren’t blocked. - Batch or debounce rapid changes per server to coalesce multiple writes into one.
- Consider deferring persistence on startup (load all configs, then flush them once) rather than writing each immediately during readiness events.
🤖 Prompt for AI Agents
In src/main/java/botcommons/config/ConfigManager.java around lines 139-140, the
call to writeConfigToFile(serverId, configs.get(serverId)) performs blocking I/O
on the main thread, causing startup delays when many guilds are ready
simultaneously. To fix this, refactor the code to offload all file writes to a
dedicated single-threaded ExecutorService to avoid blocking event threads.
Additionally, implement batching or debouncing logic to combine multiple rapid
writes per server into a single write operation. Optionally, defer persistence
during startup by loading all configs first and then flushing them once after
readiness events complete.
Closes #58
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Documentation