Files
galaxy/crates/repo_metadata/src/wrapper_model.rs
T

379 lines
14 KiB
Rust

//! Unified repository metadata model.
//!
//! [`RepoMetadataModel`] is the singleton entry point for all repository metadata
//! queries. It holds handles to [`LocalRepoMetadataModel`] and
//! [`RemoteRepoMetadataModel`] and dispatches operations based on
//! [`RepositoryIdentifier`].
#[cfg(feature = "local_fs")]
use std::path::Path;
use galaxy_core::HostId;
use galaxy_util::standardized_path::StandardizedPath;
use galaxyui::{AppContext, ModelContext, ModelHandle, SingletonEntity};
use crate::file_tree_store::FileTreeState;
use crate::file_tree_update::RepoMetadataUpdate;
use crate::local_model::{
GetContentsArgs, IndexedRepoState, LocalRepoMetadataModel, RepoContent, RepositoryMetadataEvent,
};
use crate::remote_model::{RemoteRepoMetadataModel, RemoteRepositoryMetadataEvent};
use crate::repository_identifier::{RemoteRepositoryIdentifier, RepositoryIdentifier};
use crate::RepoMetadataError;
/// Unified events emitted by the [`RepoMetadataModel`] wrapper.
///
/// These are mapped from the sub-model events into a common enum keyed by
/// [`RepositoryIdentifier`].
#[derive(Debug)]
pub enum RepoMetadataEvent {
/// A repository was added or updated.
RepositoryUpdated { id: RepositoryIdentifier },
/// A repository was removed.
RepositoryRemoved { id: RepositoryIdentifier },
/// File trees for repositories were updated.
FileTreeUpdated { ids: Vec<RepositoryIdentifier> },
/// A file tree entry was updated.
FileTreeEntryUpdated { id: RepositoryIdentifier },
/// Updating a repository failed.
UpdatingRepositoryFailed { id: RepositoryIdentifier },
/// An incremental file tree update is ready to be sent to the remote
/// client. Only emitted when the local model has
/// `emit_incremental_updates` enabled.
IncrementalUpdateReady { update: RepoMetadataUpdate },
}
/// Singleton wrapper that provides a unified API over local and remote
/// repository metadata models.
///
/// All consumers should interact with this type rather than accessing the
/// sub-models directly. The wrapper does **not** expose `.local()` or
/// `.remote()` accessors — encapsulation ensures consumers are decoupled
/// from the local/remote split.
pub struct RepoMetadataModel {
local: ModelHandle<LocalRepoMetadataModel>,
remote: ModelHandle<RemoteRepoMetadataModel>,
}
impl RepoMetadataModel {
/// Creates a new `RepoMetadataModel`, instantiating both sub-models and
/// subscribing to their events for forwarding.
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
let local = ctx.add_model(LocalRepoMetadataModel::new);
let remote = ctx.add_model(RemoteRepoMetadataModel::new);
ctx.subscribe_to_model(&local, Self::forward_local_event);
ctx.subscribe_to_model(&remote, Self::forward_remote_event);
Self { local, remote }
}
/// Creates a new `RepoMetadataModel` with incremental update emission
/// enabled on the local sub-model. Used by the remote server.
pub fn new_with_incremental_updates(ctx: &mut ModelContext<Self>) -> Self {
let local = ctx.add_model(|ctx| {
let mut model = LocalRepoMetadataModel::new(ctx);
model.set_emit_incremental_updates(true);
model
});
let remote = ctx.add_model(RemoteRepoMetadataModel::new);
ctx.subscribe_to_model(&local, Self::forward_local_event);
ctx.subscribe_to_model(&remote, Self::forward_remote_event);
Self { local, remote }
}
// ── Event forwarding ─────────────────────────────────────────────
fn forward_local_event(
&mut self,
event: &RepositoryMetadataEvent,
ctx: &mut ModelContext<Self>,
) {
let unified = match event {
RepositoryMetadataEvent::RepositoryUpdated { path } => {
RepoMetadataEvent::RepositoryUpdated {
id: RepositoryIdentifier::local(path.clone()),
}
}
RepositoryMetadataEvent::RepositoryRemoved { path } => {
RepoMetadataEvent::RepositoryRemoved {
id: RepositoryIdentifier::local(path.clone()),
}
}
RepositoryMetadataEvent::FileTreeUpdated { paths } => {
RepoMetadataEvent::FileTreeUpdated {
ids: paths
.iter()
.map(|p| RepositoryIdentifier::local(p.clone()))
.collect(),
}
}
RepositoryMetadataEvent::FileTreeEntryUpdated { path } => {
RepoMetadataEvent::FileTreeEntryUpdated {
id: RepositoryIdentifier::local(path.clone()),
}
}
RepositoryMetadataEvent::UpdatingRepositoryFailed { path } => {
RepoMetadataEvent::UpdatingRepositoryFailed {
id: RepositoryIdentifier::local(path.clone()),
}
}
RepositoryMetadataEvent::IncrementalUpdateReady { update } => {
RepoMetadataEvent::IncrementalUpdateReady {
update: update.clone(),
}
}
};
ctx.emit(unified);
}
fn forward_remote_event(
&mut self,
event: &RemoteRepositoryMetadataEvent,
ctx: &mut ModelContext<Self>,
) {
let unified = match event {
RemoteRepositoryMetadataEvent::RepositoryUpdated { id } => {
RepoMetadataEvent::RepositoryUpdated {
id: RepositoryIdentifier::Remote(id.clone()),
}
}
RemoteRepositoryMetadataEvent::RepositoryRemoved { id } => {
RepoMetadataEvent::RepositoryRemoved {
id: RepositoryIdentifier::Remote(id.clone()),
}
}
RemoteRepositoryMetadataEvent::FileTreeUpdated { ids } => {
RepoMetadataEvent::FileTreeUpdated {
ids: ids
.iter()
.cloned()
.map(RepositoryIdentifier::Remote)
.collect(),
}
}
RemoteRepositoryMetadataEvent::FileTreeEntryUpdated { id } => {
RepoMetadataEvent::FileTreeEntryUpdated {
id: RepositoryIdentifier::Remote(id.clone()),
}
}
};
ctx.emit(unified);
}
// ── Unified query API ────────────────────────────────────────────
/// Returns the [`FileTreeState`] for a repository identified by `id`.
pub fn get_repository<'a>(
&self,
id: &RepositoryIdentifier,
ctx: &'a AppContext,
) -> Option<&'a FileTreeState> {
match id {
RepositoryIdentifier::Local(path) => self.local.as_ref(ctx).get_repository(path),
RepositoryIdentifier::Remote(remote_id) => {
self.remote.as_ref(ctx).get_repository(remote_id)
}
}
}
/// Returns whether the given repository is indexed.
pub fn has_repository(&self, id: &RepositoryIdentifier, ctx: &AppContext) -> bool {
match id {
RepositoryIdentifier::Local(path) => self.local.as_ref(ctx).has_repository(path),
RepositoryIdentifier::Remote(remote_id) => {
self.remote.as_ref(ctx).has_repository(remote_id)
}
}
}
/// Returns the current [`IndexedRepoState`] for a repository.
pub fn repository_state<'a>(
&self,
id: &RepositoryIdentifier,
ctx: &'a AppContext,
) -> Option<&'a IndexedRepoState> {
match id {
RepositoryIdentifier::Local(path) => self.local.as_ref(ctx).repository_state(path),
RepositoryIdentifier::Remote(remote_id) => {
self.remote.as_ref(ctx).repository_state(remote_id)
}
}
}
/// Returns repository contents for the specified repository.
pub fn get_repo_contents<'a>(
&self,
id: &RepositoryIdentifier,
args: GetContentsArgs,
ctx: &'a AppContext,
) -> Option<Vec<RepoContent<'a>>> {
match id {
RepositoryIdentifier::Local(path) => {
self.local.as_ref(ctx).get_repo_contents(path, args)
}
RepositoryIdentifier::Remote(remote_id) => {
self.remote.as_ref(ctx).get_repo_contents(remote_id, args)
}
}
}
/// Finds the repository root that contains the given local path.
#[cfg(feature = "local_fs")]
pub fn find_repository_for_path(
&self,
path: &Path,
ctx: &AppContext,
) -> Option<StandardizedPath> {
self.local.as_ref(ctx).find_repository_for_path(path)
}
// ── Local-specific operations ────────────────────────────────────
// These delegate to the local sub-model. Remote equivalents will be
// added once the remote client ↔ server sync layer is in place.
/// Indexes a local repository from the given repository handle.
#[cfg(feature = "local_fs")]
pub fn index_directory(
&self,
repository: ModelHandle<crate::repository::Repository>,
ctx: &mut ModelContext<Self>,
) -> Result<(), RepoMetadataError> {
self.local
.update(ctx, |local, ctx| local.index_directory(repository, ctx))
}
/// Lazily indexes a local standalone path with only the first level of children.
#[cfg(feature = "local_fs")]
pub fn index_lazy_loaded_path(
&self,
path: &StandardizedPath,
ctx: &mut ModelContext<Self>,
) -> Result<(), RepoMetadataError> {
let path = path.clone();
self.local
.update(ctx, |local, ctx| local.index_lazy_loaded_path(&path, ctx))
}
/// Loads a specific directory inside an already-tracked local tree.
#[cfg(feature = "local_fs")]
pub fn load_directory(
&self,
repo_root: &StandardizedPath,
dir_path: &StandardizedPath,
ctx: &mut ModelContext<Self>,
) -> Result<(), RepoMetadataError> {
let repo_root = repo_root.clone();
let dir_path = dir_path.clone();
self.local.update(ctx, |local, ctx| {
local.load_directory(&repo_root, &dir_path, ctx)
})
}
/// Removes a lazily-loaded local standalone path from tracking.
#[cfg(feature = "local_fs")]
pub fn remove_lazy_loaded_path(&self, path: &StandardizedPath, ctx: &mut ModelContext<Self>) {
let path = path.clone();
self.local
.update(ctx, |local, ctx| local.remove_lazy_loaded_path(&path, ctx));
}
// ── Remote-specific operations ─────────────────────────────────
// These delegate to the remote sub-model and are called by the
// RemoteServerManager event subscription in the app layer.
/// Inserts or replaces a remote repository from a snapshot push event.
pub fn insert_remote_snapshot(
&self,
host_id: HostId,
update: &RepoMetadataUpdate,
ctx: &mut ModelContext<Self>,
) {
self.remote.update(ctx, |remote, ctx| {
remote.insert_from_snapshot(host_id, update, ctx);
});
}
/// Applies an incremental remote repo metadata update.
pub fn apply_remote_incremental_update(
&self,
host_id: &HostId,
update: &RepoMetadataUpdate,
ctx: &mut ModelContext<Self>,
) {
let host_id = host_id.clone();
self.remote.update(ctx, |remote, ctx| {
remote.apply_incremental_update(&host_id, update, ctx);
});
}
/// Removes all remote repositories for the given host (e.g. on disconnect).
pub fn remove_remote_repositories_for_host(
&self,
host_id: &HostId,
ctx: &mut ModelContext<Self>,
) {
let host_id = host_id.clone();
self.remote.update(ctx, |remote, ctx| {
remote.remove_repositories_for_host(&host_id, ctx);
});
}
/// Removes a repository (local or remote) from tracking.
pub fn remove_repository(
&self,
id: &RepositoryIdentifier,
ctx: &mut ModelContext<Self>,
) -> Result<(), RepoMetadataError> {
match id {
RepositoryIdentifier::Local(path) => {
let path = path.clone();
self.local
.update(ctx, |local, ctx| local.remove_repository(&path, ctx))
}
RepositoryIdentifier::Remote(remote_id) => {
let remote_id = remote_id.clone();
self.remote
.update(ctx, |remote, ctx| remote.remove_repository(&remote_id, ctx));
Ok(())
}
}
}
/// Returns all tracked remote repository identifiers.
pub fn remote_repository_ids<'a>(
&self,
ctx: &'a AppContext,
) -> impl Iterator<Item = &'a RemoteRepositoryIdentifier> {
self.remote.as_ref(ctx).remote_repository_ids()
}
/// Returns whether the given local path is tracked as a lazily-loaded standalone path.
pub fn is_lazy_loaded_path(&self, path: &StandardizedPath, ctx: &AppContext) -> bool {
self.local.as_ref(ctx).is_lazy_loaded_path(path)
}
}
impl galaxyui::Entity for RepoMetadataModel {
type Event = RepoMetadataEvent;
}
impl SingletonEntity for RepoMetadataModel {}
#[cfg(any(test, feature = "test-util"))]
impl RepoMetadataModel {
/// Inserts repository state directly into the local sub-model for testing.
pub fn insert_test_state(
&self,
repo_path: StandardizedPath,
state: FileTreeState,
ctx: &mut ModelContext<Self>,
) {
self.local.update(ctx, |local, _ctx| {
local.insert_test_state(repo_path, state);
});
}
}