Circular dependencies
There are two kinds of cycle and Corvis treats them differently: between modules (A imports B and B imports A) and between providers (A needs B to be built and B needs A).
Cycles between modules: allowed
Circular imports between static modules work without doing anything special. When walking the graph, a module that is already being processed is not created again: the second import points to the same node. The export lookup also keeps track of visited modules, so it doesn't loop.
#[module(imports = [BillingModule], providers = [OrdersService], exports = [OrdersService])]
pub struct OrdersModule;
#[module(imports = [OrdersModule], providers = [InvoicesService], exports = [InvoicesService])]
pub struct BillingModule;No deferred reference is needed for modules. That the graph allows it doesn't mean it is good design: a cycle between modules usually indicates there is a third responsibility that deserves its own module.
Cycles between providers: detected and fail
If OrdersService has an Arc<InvoicesService> field and InvoicesService has an Arc<OrdersService> field, neither can be built before the other. Corvis detects it at startup and returns an error with the full chain:
A circular dependency has been detected: OrdersService -> InvoicesService -> OrdersService.There is no deferred-reference mechanism on fields (forwardRef in other containers). The error message mentions forwardRef() as a legacy of those containers, but Corvis has no such function: use one of the following solutions.
1. Extract the shared part
The preferred solution. Almost always, each service needs only a part of the other. Move that part to a third provider that both depend on:
OrdersService ──► PricingService ◄── InvoicesService2. Resolve one of the sides later with ModuleRef
One side of the cycle stops asking for the other as a field and looks it up when it uses it. Since all singletons are created at startup, at request time the instance already exists:
#[injectable]
pub struct OrdersService {
module_ref: ModuleRef,
}
impl OrdersService {
fn invoices(&self) -> Arc<InvoicesService> {
self.module_ref.get::<InvoicesService>().expect("InvoicesService is registered")
}
pub async fn close(&self, id: u64) -> HttpResult<()> {
self.invoices().issue_for(id).await
}
}
#[injectable]
pub struct InvoicesService {
orders: Arc<OrdersService>, // this side remains a normal field
}ModuleRef::get first looks up using the module's visibility rules and, if it doesn't find it, across the whole application. To strictly respect encapsulation use get_token_in_context. See Module reference.
Don't call get inside the constructor (for example in an #[init(...)]): during construction the other side doesn't exist yet.
3. Communicate with events
If a service only needs to notify the other, emit an event with EventEmitter and let the other one listen with #[on_event]. Tertulia does it this way: RoomsService emits room.created and ChatGateway listens to it, without RoomsService depending on the gateway:
// RoomsService
self.events.emit("room.created", room.clone());
// ChatGateway
#[handlers]
impl ChatGateway {
#[on_event("room.created")]
fn on_room_created(&self, room: Room) {
self.server.emit("room_created", json!({ "room": room, "members": [] }));
}
}See Events.