Lazy-Loaded Module System
The ErisPulse SDK provides a powerful lazy-loaded module system that allows modules to be initialized only when they are actually needed, significantly improving application startup speed and memory efficiency.
Overview
The lazy-loaded module system is one of the core features of ErisPulse, which works as follows:
- Lazy Initialization: Modules are only loaded and initialized when they are first accessed.
- Transparent Usage: For developers, lazy-loaded modules are almost indistinguishable from regular modules in usage.
- Automatic Dependency Management: Module dependencies are automatically initialized when they are used.
- Lifecycle Support: For modules that inherit from
BaseModule, lifecycle methods are automatically invoked.
Working Principle
LazyModule Class
The core of the lazy loading system is the LazyModule class, which acts as a wrapper that actually initializes the module only when it is first accessed.
Initialization Process
When a module is first accessed, LazyModule performs the following operations:
- Retrieves the
__init__parameter information of the module class. - Determines whether to pass the
sdkreference based on the parameters. - Sets the
moduleInfoattribute of the module. - For modules that inherit from
BaseModule, calls theon_loadmethod. - Triggers the
module.initlifecycle event.
Event-Driven Lazy Activation (activate_on)
Note
This feature requires ErisPulse 2.8.0+.
Modules with lazy_load=True are loaded only on their first attribute access by default. If a module registers command/event handlers, the traditional approach would require lazy_load=False to load immediately. activate_on provides a third option: declare triggers, and the module activates automatically when the first matching event/command arrives—neither staying in memory constantly nor losing the trigger entry.
from ErisPulse.loaders import ModuleLoadStrategy
class MyModule(BaseModule):
@staticmethod
def get_load_strategy():
return ModuleLoadStrategy(
lazy_load=True,
activate_on=[
# ---- Event triggers (passive arrival, no user awareness) ----
"message", # Type-level: any message event
{"notice": "group_member_increase"}, # Type + single detail_type
{"message": ["private", "group"]}, # Type + multiple detail_types
# ---- Command triggers (active input, placeholder commands visible in Help) ----
{"command": "roll"}, # Shorthand: command name
{"command": ["roll", "dice"]}, # List of command names
{"command": { # Dict declaration (name required)
"name": "dice",
"help": "Roll a dice",
"usage": "/dice",
"group": "Entertainment",
"aliases": ["d"],
"hidden": False,
}},
],
)
Command Dict Declaration Parameters
The dict form mirrors the user-level parameters of the @command() decorator, used to register placeholder commands before module loading:
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
Required | Command name; must match @command(name) in on_load, otherwise the placeholder is unregistered after activation, and the command becomes unavailable |
help |
str |
Fallback chain | Description shown in Help; if not declared, falls back to the chain (see below) |
usage |
str |
Auto-generated | Usage line, defaulting to {prefix}{name} |
group |
str |
None |
Command group |
aliases |
list[str] |
[] |
Aliases are registered simultaneously; inputting an alias also triggers activation |
hidden |
bool |
False |
If True, the placeholder command is hidden (aligned with the hidden semantics of the activated real command); users who know the command name can still trigger activation |
Not supported: priority / permission / master: The placeholder command's role is only to trigger activation. Permission checks are performed by the real command after activation (blocking permissions at the placeholder stage would make "activating on command input" ineffective).
Placeholder Command Help Fallback Chain
When the module is not loaded, the Help displays command descriptions according to the following priority (first match wins):
- The command-level
helpdeclared in the dict (most precise) - The module's
get_meta()description - The module's
__description__attribute - The package metadata's
Summary(PyPI package summary) - Generic prompt: "This command comes from a lazy-loaded module X. The module will be automatically loaded on first use."
Trigger Semantics
- Event stub: Registered to the corresponding event manager with very low priority (
ACTIVATION_STUB_PRIORITY), acting as a fallback after all regular handlers; after activation, the current event is forwarded to the module's real handler - Command stub: Registers a placeholder command; after activation, the placeholder is unregistered, and the real command takes over the current trigger
- Reentrancy protection: An
asyncio.Lockensures activation occurs only once, even under concurrent triggers - Scope filtering: The stub includes the module owner identity, and does not trigger if the module is not enabled for the Bot / session / platform
- Failure semantics: Activation failure does not retry; the stub is also unregistered
- Deduplication: When mixing shorthand and dict declarations of the same command name, deduplication occurs (dict takes precedence); if the dict is missing
nameor the eventdetail_typeis incorrectly written as a dict, a warning is issued and it is ignored
For architecture diagrams and full semantics, see Architecture Overview.
Configuring Lazy Loading
Global Configuration
Enable or disable global lazy loading in the configuration file:
[ErisPulse.framework]
enable_lazy_loading = true # true=enable lazy loading (default), false=disable lazy loading
Module-Level Control
Modules can control their loading strategy by implementing the get_load_strategy() static method:
from ErisPulse.Core.Bases import BaseModule
from ErisPulse.loaders import ModuleLoadStrategy
class MyModule(BaseModule):
@staticmethod
def get_load_strategy():
"""Returns the module loading strategy"""
return ModuleLoadStrategy(
lazy_load=False, # Return False to indicate immediate loading
priority=100 # Loading priority, higher value means higher priority
)
Using Lazy-Loaded Modules
Basic Usage
For developers, lazy-loaded modules are almost indistinguishable from regular modules in usage:
# Accessing a lazy-loaded module through the SDK
from ErisPulse import sdk
# The following access triggers the module's lazy loading
result = await sdk.my_module.my_method()
Unified Module Access Entry
Whether accessed through SDK attributes, module manager attributes, or via module.get(), for "registered but not yet loaded" lazy-loaded modules, the same lazy-loading proxy is returned. Accessing its attributes triggers the actual initialization:
# All three methods return the same lazy-loading proxy (when the module is not loaded), behaving consistently and transparently to the user
sdk.my_module # Entry point that triggers loading
sdk.module.my_module # Also returns the lazy-loading proxy
sdk.module.get("my_module") # Also returns the lazy-loading proxy, itself does not trigger loading
# Accessing any attribute of the proxy triggers the actual initialization of the module
result = await sdk.my_module.my_method()
module.get() is a query interface and does not trigger loading by itself:
- If the module is already loaded → returns the actual instance
- If the module is registered but not yet loaded → returns the lazy-loading proxy (initialization occurs when an attribute is accessed)
- If the module is not registered → returns
None
To explicitly trigger loading, use await sdk.load_module("my_module").
Asynchronous Initialization
For modules requiring asynchronous initialization, it is recommended to load them explicitly first:
# Explicitly load the module first
await sdk.load_module("my_module")
# Then use the module
result = await sdk.my_module.my_method()
Synchronous Initialization
For modules that do not require asynchronous initialization, you can access them directly:
# Direct access will automatically initialize synchronously
result = sdk.my_module.some_sync_method()
Best Practices
When choosing a loading strategy, you can refer to the following decision flow:
flowchart TD
A["Module Declaration<br/>get_load_strategy()"] --> B{"Do you need it ready at startup<br/>or frequently triggered?"}
B -->|"Yes"| C["lazy_load=False<br/>Load Immediately"]
B -->|"No"| D{"Registered Command / Event Handlers?"}
D -->|"Yes"| E["lazy_load=True + activate_on<br/>Activate when event/command arrives"]
D -->|"No"| F["lazy_load=True<br/>Load on first attribute access"]
C --> G["Call on_load() at startup"]
E --> H["Register stub → Instantiate on trigger"]
F --> I["LazyModule Proxy"]
Recommended Scenarios for Lazy Loading (lazy_load=True)
- Passive utility modules (e.g., data query modules, format converters, etc., which are only needed when called by other modules)
- Modules that register command/event handlers but are not frequently used — use
activate_onto declare triggers, and activate automatically when the first matching event/command arrives, without giving up lazy loading
Recommended Scenarios for Disabling Lazy Loading (lazy_load=False)
- Modules that need to be ready immediately at startup (e.g., core modules that provide foundational services to other modules)
- High-frequency listeners (e.g., every message must be processed) —
activate_onforwarding has some activation overhead, so immediate loading is more direct in high-frequency scenarios - Scheduled task modules
- Modules that need to be initialized at application startup
The
priorityparameter controls the initialization order of immediately loaded modules; higher values are initialized first. Modules with the same priority are loaded in registration order.
Notes
- If your module uses lazy loading, and other modules have never been called within ErisPulse, your module will never be initialized.
- If your module contains modules that listen for Events, or other actively listening modules, you have two options: declare an
activate_ontrigger (to keep lazy loading and automatically activate when the event arrives), or declare that it needs to be loaded immediately (lazy_load=False), otherwise it may affect the normal operation of your module. - We do not recommend disabling lazy loading unless there is a special requirement, otherwise it may cause issues such as dependency management and lifecycle events.
- In the
activate_oncommand dict declaration,namemust be consistent with the actual command name registered in the module'son_loadwith@command()—otherwise, after the module is activated, the placeholder command will be unregistered, and a command with inconsistent declaration and implementation will not exist.
Related Documentation
- Module Development Guide - Learn how to develop modules
- Best Practices - Learn more about best practices