- Wire invite request, accept, and decline events through the UI bridge - Add client and server handlers for invite success and failure responses - Extend portal state and UI to support member invite actions
Forge Repositories
This crate provides the data access layer for the Forge application, implementing the repository pattern to abstract database operations from business logic.
Architecture
The repository layer sits between the service layer and the database, providing a clean abstraction for data persistence.
graph TD
Services[Services Layer]
Repositories[Repositories Layer<br/>#40;This Module#41;]
Database[Database]
Services --> Repositories
Repositories --> Database
Dual Storage Strategy
The implementation uses a dual storage strategy in Redis to optimize for different access patterns:
- Hash Maps (
HMSET/HGETALL): Used for entity data (Actors, Organizations) to allow O(1) access to specific fields and efficient partial updates. - Sets (
SADD/SMEMBERS): Used for relationships (e.g., Organization Members) to ensure uniqueness and provide efficient membership testing.
Key Features
- Redis Integration: Efficient hash-based storage for data
- JSON Serialization: Automatic conversion between Rust structs and Redis
- Type Safety: Strong typing with error handling
- Performance Optimized: Hash operations for fast field-level access
- Flexible Client: Generic over Redis client implementations
- Atomic Operations: Uses Redis atomicity guarantees for data integrity
Actor Repository
The ActorRepository handles persistence for player data.
Storage Format
Actors are stored in Redis as hash maps:
actor:{uid} -> Hash {
"uid": "76561198123456789",
"name": "PlayerName",
"bank": "1500.0",
...
}
Usage Example
use forge_repositories::ActorRepository;
use forge_models::Actor;
async fn example_usage<R: ActorRepository>(repo: &R) -> Result<(), String> {
// 1. Create
let actor = Actor::new("76561198123456789".to_string())?;
repo.create(&actor)?;
// 2. Retrieve
if let Some(retrieved) = repo.get_by_id("76561198123456789")? {
println!("Found actor: {}", retrieved.name());
}
// 3. Update
// Updates are atomic and preserve fields not present in the update
let mut actor_to_update = repo.get_by_id("76561198123456789")?.unwrap();
// ... modify actor ...
repo.update(&actor_to_update)?;
// 4. Check Existence
if repo.exists("76561198123456789")? {
println!("Actor exists");
}
// 5. Delete
repo.delete("76561198123456789")?;
Ok(())
}
Organization Repository
The OrgRepository handles persistence for organizations (guilds/clans) and their members.
Storage Format
- Org Data:
org:{org_id}(Hash) - Members:
org:{org_id}:members(Set)
Usage Example
use forge_repositories::OrgRepository;
use forge_models::{Org, MemberSummary};
async fn example_usage<R: OrgRepository>(repo: &R) -> Result<(), String> {
// 1. Create Organization
let org = Org::new("elite_squad".to_string(), "leader_uid".to_string(), "Elite Squad".to_string())?;
repo.create(&org)?;
// 2. Manage Members
// Add a member (idempotent, handles duplicates)
repo.add_member("elite_squad", "member_uid_1")?;
// Get all members
let members = repo.get_members("elite_squad")?;
for member in members {
println!("Member: {} ({})", member.name, member.uid);
}
// Remove a member
repo.remove_member("elite_squad", "member_uid_1")?;
// 3. Update Organization
let mut org_update = repo.get_by_id("elite_squad")?.unwrap();
// ... modify org ...
repo.update(&org_update)?;
// 4. Delete Organization
// Note: This removes the org data but may require separate cleanup for members depending on implementation
repo.delete("elite_squad")?;
Ok(())
}
Performance & Implementation Details
Atomicity
- Upserts:
createandupdateoperations useHMSETwhich is atomic. This means either all fields are updated or none are. - Schema Evolution: New fields added to the Rust structs are automatically persisted to Redis. Old fields in Redis that are no longer in the struct are preserved (not deleted) during updates, allowing for safe backward compatibility.
Thread Safety
All repository implementations are Send + Sync, allowing them to be safely shared across threads. The underlying RedisClient handles connection pooling and concurrent access.
Error Handling
Repositories return Result<T, String> (or custom error types) to propagate database failures, serialization errors, or validation issues up to the service layer.
Contributing
We welcome contributions to the Forge Repository Layer! This guide will help you understand how to add new repositories and maintain the existing codebase.
Adding a New Repository
To add a new repository (e.g., ItemRepository), follow these steps:
-
Create the Module: Create a new file in
src/(e.g.,src/item.rs). -
Define the Trait: Define a trait that specifies the data access operations. Ensure it requires
Send + Sync.pub trait ItemRepository: Send + Sync { fn create(&self, item: &Item) -> Result<(), String>; fn get_by_id(&self, id: &str) -> Result<Option<Item>, String>; // ... other methods } -
Implement for Redis: Implement the trait for a generic
RedisClient. Useforge_sharedhelpers for value conversion if needed.use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; pub struct RedisItemRepository<C: RedisClient> { client: C, } impl<C: RedisClient> RedisItemRepository<C> { pub fn new(client: C) -> Self { Self { client } } } impl<C: RedisClient> ItemRepository for RedisItemRepository<C> { fn create(&self, item: &Item) -> Result<(), String> { let redis_key = format!("item:{}", item.id); // ... serialization logic ... // Use self.client to interact with Redis self.client.hash_mset(redis_key, fields) } // ... other methods } -
Register the Module: Add your new module to
src/lib.rsand export the trait and implementation.pub mod item; pub use item::{ItemRepository, RedisItemRepository};
Testing
- Integration Tests: Write integration tests that use a real Redis instance (if available) or a mock.
- Mocking: For unit testing services, you don't need to test the repository implementation itself, but you should ensure the repository trait is easy to mock.
Best Practices
- Abstraction: Keep the repository trait agnostic of the underlying database technology (e.g., don't expose Redis-specific types in the trait signature).
- Serialization: Handle serialization/deserialization within the repository implementation. The service layer should work with domain models, not raw JSON or database rows.
- Keyspace: Use a consistent naming convention for Redis keys (e.g.,
entity:id). - Atomicity: Use Redis transactions or atomic commands where possible to ensure data consistency.