entity_graph_tauri/
commands.rs

1//! commands.rs
2//!
3//! Every `#[tauri::command]` exposed by the entity-graph plugin.
4//!
5//! Every command is annotated with `#[specta::specta]` and registered in the
6//! tauri-specta collection so TypeScript bindings are generated from Rust.
7//!
8//! Naming convention: Tauri surfaces these as
9//!   `plugin:entity-graph-tauri|<fn_name>`
10//! matching the plugin name registered in `lib.rs`.
11
12use tauri::{AppHandle, Emitter, Runtime, State};
13
14use crate::state::GraphPluginState;
15use crate::types::{
16    GetEntityResult, GetListResult, PatchEntityPayload, PersistSnapshotPayload, PlatformPing,
17    RemoveEntityPayload, RestoreSnapshotPayload, RestoreSnapshotResult, SetListPayload,
18    SnapshotPersistedEvent, SnapshotRestoredEvent, UpsertEntityPayload,
19};
20use crate::EntityGraphExt;
21
22// ── Commands registered with specta (no generic AppHandle) ───────────────────
23
24/// Upsert an entity into the plugin's in-memory mirror.
25#[tauri::command]
26#[cfg_attr(feature = "generate-bindings", specta::specta)]
27pub async fn graph_upsert_entity(
28    state: State<'_, GraphPluginState>,
29    payload: UpsertEntityPayload,
30) -> Result<(), String> {
31    state
32        .upsert_entity(&payload.entity_type, &payload.entity_id, payload.data)
33        .await;
34    Ok(())
35}
36
37/// Remove an entity from the plugin's in-memory mirror.
38#[tauri::command]
39#[cfg_attr(feature = "generate-bindings", specta::specta)]
40pub async fn graph_remove_entity(
41    state: State<'_, GraphPluginState>,
42    payload: RemoveEntityPayload,
43) -> Result<(), String> {
44    state
45        .remove_entity(&payload.entity_type, &payload.entity_id)
46        .await;
47    Ok(())
48}
49
50/// Record a UI-only patch overlay (acknowledged but not mirrored on Rust side).
51#[tauri::command]
52#[cfg_attr(feature = "generate-bindings", specta::specta)]
53pub async fn graph_patch_entity(
54    _state: State<'_, GraphPluginState>,
55    _payload: PatchEntityPayload,
56) -> Result<(), String> {
57    Ok(())
58}
59
60/// Set the ordered ID array for a list query key.
61#[tauri::command]
62#[cfg_attr(feature = "generate-bindings", specta::specta)]
63pub async fn graph_set_list(
64    state: State<'_, GraphPluginState>,
65    payload: SetListPayload,
66) -> Result<(), String> {
67    state
68        .set_list(&payload.query_key, payload.ids, payload.total)
69        .await;
70    Ok(())
71}
72
73/// Read a single entity from the in-memory mirror.
74#[tauri::command]
75#[cfg_attr(feature = "generate-bindings", specta::specta)]
76pub async fn graph_get_entity(
77    state: State<'_, GraphPluginState>,
78    entity_type: String,
79    entity_id: String,
80) -> Result<GetEntityResult, String> {
81    let data = state.get_entity(&entity_type, &entity_id).await;
82    Ok(GetEntityResult { data })
83}
84
85/// Read a list from the in-memory mirror.
86#[tauri::command]
87#[cfg_attr(feature = "generate-bindings", specta::specta)]
88pub async fn graph_get_list(
89    state: State<'_, GraphPluginState>,
90    query_key: String,
91) -> Result<GetListResult, String> {
92    match state.get_list(&query_key).await {
93        Some(entry) => Ok(GetListResult {
94            ids: entry.ids,
95            total: entry.total,
96        }),
97        None => Ok(GetListResult {
98            ids: vec![],
99            total: None,
100        }),
101    }
102}
103
104/// Invoke the registered desktop, Android, or iOS native bridge.
105#[tauri::command]
106#[cfg_attr(feature = "generate-bindings", specta::specta)]
107pub fn graph_platform_ping<R: Runtime>(app: AppHandle<R>) -> Result<PlatformPing, String> {
108    app.entity_graph().ping().map_err(|error| error.to_string())
109}
110
111/// Clear all entities and lists from the in-memory mirror.
112#[tauri::command]
113#[cfg_attr(feature = "generate-bindings", specta::specta)]
114pub async fn graph_clear(state: State<'_, GraphPluginState>) -> Result<(), String> {
115    state.clear_entities().await;
116    state.clear_lists().await;
117    Ok(())
118}
119
120// ── Snapshot commands (generic over the host runtime) ────────────────────────
121
122/// Persist a JSON-serialised graph snapshot to the in-memory store and emit
123/// a `SnapshotPersistedEvent`.
124///
125#[tauri::command]
126#[cfg_attr(feature = "generate-bindings", specta::specta)]
127pub async fn graph_persist_snapshot<R: Runtime>(
128    app: AppHandle<R>,
129    state: State<'_, GraphPluginState>,
130    payload: PersistSnapshotPayload,
131) -> Result<(), String> {
132    let key = payload
133        .storage_key
134        .unwrap_or_else(|| "entity-graph:snapshot".to_owned());
135
136    let byte_size = payload.snapshot.len();
137    state.set_snapshot(&key, payload.snapshot).await;
138
139    let now = monotonic_ts();
140    let _ = app.emit(
141        "plugin:entity-graph-tauri:snapshot-persisted",
142        SnapshotPersistedEvent {
143            storage_key: key,
144            persisted_at: now,
145            byte_size,
146        },
147    );
148
149    Ok(())
150}
151
152/// Return a previously persisted snapshot string and emit a
153/// `SnapshotRestoredEvent`.
154///
155#[tauri::command]
156#[cfg_attr(feature = "generate-bindings", specta::specta)]
157pub async fn graph_restore_snapshot<R: Runtime>(
158    app: AppHandle<R>,
159    state: State<'_, GraphPluginState>,
160    payload: RestoreSnapshotPayload,
161) -> Result<RestoreSnapshotResult, String> {
162    let key = payload
163        .storage_key
164        .unwrap_or_else(|| "entity-graph:snapshot".to_owned());
165
166    let snapshot = state.get_snapshot(&key).await;
167
168    if snapshot.is_some() {
169        let now = monotonic_ts();
170        let _ = app.emit(
171            "plugin:entity-graph-tauri:snapshot-restored",
172            SnapshotRestoredEvent {
173                storage_key: key,
174                restored_at: now,
175            },
176        );
177    }
178
179    Ok(RestoreSnapshotResult { snapshot })
180}
181
182// ── Utility ───────────────────────────────────────────────────────────────────
183
184fn monotonic_ts() -> String {
185    use std::time::{SystemTime, UNIX_EPOCH};
186    let ms = SystemTime::now()
187        .duration_since(UNIX_EPOCH)
188        .unwrap_or_default()
189        .as_millis();
190    format!("{ms}")
191}