mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-21 08:11:24 +02:00
Got models and event system working
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -10400,6 +10400,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
"ts-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { RpcSchema } from "../../crates-proxy/yaak-proxy-lib/bindings/gen_rpc";
|
import { listen as tauriListen } from "@tauri-apps/api/event";
|
||||||
|
import type {
|
||||||
|
RpcEventSchema,
|
||||||
|
RpcSchema,
|
||||||
|
} from "../../crates-proxy/yaak-proxy-lib/bindings/gen_rpc";
|
||||||
|
|
||||||
type Req<K extends keyof RpcSchema> = RpcSchema[K][0];
|
type Req<K extends keyof RpcSchema> = RpcSchema[K][0];
|
||||||
type Res<K extends keyof RpcSchema> = RpcSchema[K][1];
|
type Res<K extends keyof RpcSchema> = RpcSchema[K][1];
|
||||||
@@ -10,3 +14,15 @@ export async function rpc<K extends keyof RpcSchema>(
|
|||||||
): Promise<Res<K>> {
|
): Promise<Res<K>> {
|
||||||
return invoke("rpc", { cmd, payload }) as Promise<Res<K>>;
|
return invoke("rpc", { cmd, payload }) as Promise<Res<K>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Subscribe to a backend event. Returns an unsubscribe function. */
|
||||||
|
export function listen<K extends keyof RpcEventSchema>(
|
||||||
|
event: K & string,
|
||||||
|
callback: (payload: RpcEventSchema[K]) => void,
|
||||||
|
): () => void {
|
||||||
|
let unsub: (() => void) | null = null;
|
||||||
|
tauriListen<RpcEventSchema[K]>(event, (e) => callback(e.payload))
|
||||||
|
.then((fn) => (unsub = fn))
|
||||||
|
.catch(console.error);
|
||||||
|
return () => unsub?.();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type ModelChangeEvent = { "type": "upsert" } | { "type": "delete" };
|
||||||
|
|
||||||
|
export type ModelPayload = { model: ProxyEntry, change: ModelChangeEvent, };
|
||||||
|
|
||||||
export type ProxyEntry = { id: string, createdAt: string, updatedAt: string, url: string, method: string, reqHeaders: Array<ProxyHeader>, reqBody: Array<number> | null, resStatus: number | null, resHeaders: Array<ProxyHeader>, resBody: Array<number> | null, error: string | null, };
|
export type ProxyEntry = { id: string, createdAt: string, updatedAt: string, url: string, method: string, reqHeaders: Array<ProxyHeader>, reqBody: Array<number> | null, resStatus: number | null, resHeaders: Array<ProxyHeader>, resBody: Array<number> | null, error: string | null, };
|
||||||
|
|
||||||
export type ProxyHeader = { name: string, value: string, };
|
export type ProxyHeader = { name: string, value: string, };
|
||||||
|
|||||||
3
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
3
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
@@ -1,4 +1,5 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { ModelPayload } from "./gen_models";
|
||||||
|
|
||||||
export type ProxyStartRequest = { port: number | null, };
|
export type ProxyStartRequest = { port: number | null, };
|
||||||
|
|
||||||
@@ -6,4 +7,6 @@ export type ProxyStartResponse = { port: number, alreadyRunning: boolean, };
|
|||||||
|
|
||||||
export type ProxyStopRequest = Record<string, never>;
|
export type ProxyStopRequest = Record<string, never>;
|
||||||
|
|
||||||
|
export type RpcEventSchema = { model_write: ModelPayload, };
|
||||||
|
|
||||||
export type RpcSchema = { proxy_start: [ProxyStartRequest, ProxyStartResponse], proxy_stop: [ProxyStopRequest, boolean], };
|
export type RpcSchema = { proxy_start: [ProxyStartRequest, ProxyStartResponse], proxy_stop: [ProxyStopRequest, boolean], };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CREATE TABLE proxy_entries
|
CREATE TABLE http_exchanges
|
||||||
(
|
(
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
|||||||
@@ -7,24 +7,26 @@ use std::sync::Mutex;
|
|||||||
use log::warn;
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_database::UpdateSource;
|
use yaak_database::{ModelChangeEvent, UpdateSource};
|
||||||
use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState};
|
use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState};
|
||||||
use yaak_rpc::{RpcError, define_rpc};
|
use yaak_rpc::{RpcError, RpcEventEmitter, define_rpc};
|
||||||
use crate::db::ProxyQueryManager;
|
use crate::db::ProxyQueryManager;
|
||||||
use crate::models::{ProxyEntry, ProxyHeader};
|
use crate::models::{HttpExchange, ModelPayload, ProxyHeader};
|
||||||
|
|
||||||
// -- Context --
|
// -- Context --
|
||||||
|
|
||||||
pub struct ProxyCtx {
|
pub struct ProxyCtx {
|
||||||
handle: Mutex<Option<ProxyHandle>>,
|
handle: Mutex<Option<ProxyHandle>>,
|
||||||
pub db: ProxyQueryManager,
|
pub db: ProxyQueryManager,
|
||||||
|
pub events: RpcEventEmitter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxyCtx {
|
impl ProxyCtx {
|
||||||
pub fn new(db_path: &Path) -> Self {
|
pub fn new(db_path: &Path, events: RpcEventEmitter) -> Self {
|
||||||
Self {
|
Self {
|
||||||
handle: Mutex::new(None),
|
handle: Mutex::new(None),
|
||||||
db: ProxyQueryManager::new(db_path),
|
db: ProxyQueryManager::new(db_path),
|
||||||
|
events,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +70,8 @@ fn proxy_start(ctx: &ProxyCtx, req: ProxyStartRequest) -> Result<ProxyStartRespo
|
|||||||
// Spawn event loop before storing the handle
|
// Spawn event loop before storing the handle
|
||||||
if let Some(event_rx) = proxy_handle.take_event_rx() {
|
if let Some(event_rx) = proxy_handle.take_event_rx() {
|
||||||
let db = ctx.db.clone();
|
let db = ctx.db.clone();
|
||||||
std::thread::spawn(move || run_event_loop(event_rx, db));
|
let events = ctx.events.clone();
|
||||||
|
std::thread::spawn(move || run_event_loop(event_rx, db, events));
|
||||||
}
|
}
|
||||||
|
|
||||||
*handle = Some(proxy_handle);
|
*handle = Some(proxy_handle);
|
||||||
@@ -85,7 +88,7 @@ fn proxy_stop(ctx: &ProxyCtx, _req: ProxyStopRequest) -> Result<bool, RpcError>
|
|||||||
|
|
||||||
// -- Event loop --
|
// -- Event loop --
|
||||||
|
|
||||||
fn run_event_loop(rx: std::sync::mpsc::Receiver<ProxyEvent>, db: ProxyQueryManager) {
|
fn run_event_loop(rx: std::sync::mpsc::Receiver<ProxyEvent>, db: ProxyQueryManager, events: RpcEventEmitter) {
|
||||||
let mut in_flight: HashMap<u64, CapturedRequest> = HashMap::new();
|
let mut in_flight: HashMap<u64, CapturedRequest> = HashMap::new();
|
||||||
|
|
||||||
while let Ok(event) = rx.recv() {
|
while let Ok(event) = rx.recv() {
|
||||||
@@ -140,22 +143,22 @@ fn run_event_loop(rx: std::sync::mpsc::Receiver<ProxyEvent>, db: ProxyQueryManag
|
|||||||
r.response_body_size = size;
|
r.response_body_size = size;
|
||||||
r.elapsed_ms = r.elapsed_ms.or(Some(elapsed_ms));
|
r.elapsed_ms = r.elapsed_ms.or(Some(elapsed_ms));
|
||||||
r.state = RequestState::Complete;
|
r.state = RequestState::Complete;
|
||||||
write_entry(&db, &r);
|
write_entry(&db, &events, &r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProxyEvent::Error { id, error } => {
|
ProxyEvent::Error { id, error } => {
|
||||||
if let Some(mut r) = in_flight.remove(&id) {
|
if let Some(mut r) = in_flight.remove(&id) {
|
||||||
r.error = Some(error);
|
r.error = Some(error);
|
||||||
r.state = RequestState::Error;
|
r.state = RequestState::Error;
|
||||||
write_entry(&db, &r);
|
write_entry(&db, &events, &r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_entry(db: &ProxyQueryManager, r: &CapturedRequest) {
|
fn write_entry(db: &ProxyQueryManager, events: &RpcEventEmitter, r: &CapturedRequest) {
|
||||||
let entry = ProxyEntry {
|
let entry = HttpExchange {
|
||||||
url: r.url.clone(),
|
url: r.url.clone(),
|
||||||
method: r.method.clone(),
|
method: r.method.clone(),
|
||||||
req_headers: r.request_headers.iter()
|
req_headers: r.request_headers.iter()
|
||||||
@@ -171,8 +174,14 @@ fn write_entry(db: &ProxyQueryManager, r: &CapturedRequest) {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
db.with_conn(|ctx| {
|
db.with_conn(|ctx| {
|
||||||
if let Err(e) = ctx.upsert(&entry, &UpdateSource::Background) {
|
match ctx.upsert(&entry, &UpdateSource::Background) {
|
||||||
warn!("Failed to write proxy entry: {e}");
|
Ok((saved, created)) => {
|
||||||
|
events.emit("model_write", &ModelPayload {
|
||||||
|
model: saved,
|
||||||
|
change: ModelChangeEvent::Upsert { created },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => warn!("Failed to write proxy entry: {e}"),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -181,6 +190,11 @@ fn write_entry(db: &ProxyQueryManager, r: &CapturedRequest) {
|
|||||||
|
|
||||||
define_rpc! {
|
define_rpc! {
|
||||||
ProxyCtx;
|
ProxyCtx;
|
||||||
"proxy_start" => proxy_start(ProxyStartRequest) -> ProxyStartResponse,
|
commands {
|
||||||
"proxy_stop" => proxy_stop(ProxyStopRequest) -> bool,
|
proxy_start(ProxyStartRequest) -> ProxyStartResponse,
|
||||||
|
proxy_stop(ProxyStopRequest) -> bool,
|
||||||
|
}
|
||||||
|
events {
|
||||||
|
model_write(ModelPayload),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use rusqlite::Row;
|
|||||||
use sea_query::{IntoColumnRef, IntoIden, IntoTableRef, Order, SimpleExpr, enum_def};
|
use sea_query::{IntoColumnRef, IntoIden, IntoTableRef, Order, SimpleExpr, enum_def};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_database::{Result as DbResult, UpdateSource, UpsertModelInfo, generate_prefixed_id, upsert_date};
|
use yaak_database::{ModelChangeEvent, Result as DbResult, UpdateSource, UpsertModelInfo, generate_prefixed_id, upsert_date};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -16,8 +16,8 @@ pub struct ProxyHeader {
|
|||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
#[enum_def(table_name = "proxy_entries")]
|
#[enum_def(table_name = "http_exchanges")]
|
||||||
pub struct ProxyEntry {
|
pub struct HttpExchange {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
@@ -31,21 +31,29 @@ pub struct ProxyEntry {
|
|||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for ProxyEntry {
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
|
pub struct ModelPayload {
|
||||||
|
pub model: HttpExchange,
|
||||||
|
pub change: ModelChangeEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpsertModelInfo for HttpExchange {
|
||||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||||
ProxyEntryIden::Table
|
HttpExchangeIden::Table
|
||||||
}
|
}
|
||||||
|
|
||||||
fn id_column() -> impl IntoIden + Eq + Clone {
|
fn id_column() -> impl IntoIden + Eq + Clone {
|
||||||
ProxyEntryIden::Id
|
HttpExchangeIden::Id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_id() -> String {
|
fn generate_id() -> String {
|
||||||
generate_prefixed_id("pe")
|
generate_prefixed_id("he")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn order_by() -> (impl IntoColumnRef, Order) {
|
fn order_by() -> (impl IntoColumnRef, Order) {
|
||||||
(ProxyEntryIden::CreatedAt, Order::Desc)
|
(HttpExchangeIden::CreatedAt, Order::Desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_id(&self) -> String {
|
fn get_id(&self) -> String {
|
||||||
@@ -56,7 +64,7 @@ impl UpsertModelInfo for ProxyEntry {
|
|||||||
self,
|
self,
|
||||||
source: &UpdateSource,
|
source: &UpdateSource,
|
||||||
) -> DbResult<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
|
) -> DbResult<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
|
||||||
use ProxyEntryIden::*;
|
use HttpExchangeIden::*;
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
(CreatedAt, upsert_date(source, self.created_at)),
|
(CreatedAt, upsert_date(source, self.created_at)),
|
||||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||||
@@ -73,15 +81,15 @@ impl UpsertModelInfo for ProxyEntry {
|
|||||||
|
|
||||||
fn update_columns() -> Vec<impl IntoIden> {
|
fn update_columns() -> Vec<impl IntoIden> {
|
||||||
vec![
|
vec![
|
||||||
ProxyEntryIden::UpdatedAt,
|
HttpExchangeIden::UpdatedAt,
|
||||||
ProxyEntryIden::Url,
|
HttpExchangeIden::Url,
|
||||||
ProxyEntryIden::Method,
|
HttpExchangeIden::Method,
|
||||||
ProxyEntryIden::ReqHeaders,
|
HttpExchangeIden::ReqHeaders,
|
||||||
ProxyEntryIden::ReqBody,
|
HttpExchangeIden::ReqBody,
|
||||||
ProxyEntryIden::ResStatus,
|
HttpExchangeIden::ResStatus,
|
||||||
ProxyEntryIden::ResHeaders,
|
HttpExchangeIden::ResHeaders,
|
||||||
ProxyEntryIden::ResBody,
|
HttpExchangeIden::ResBody,
|
||||||
ProxyEntryIden::Error,
|
HttpExchangeIden::Error,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use log::error;
|
use log::error;
|
||||||
use tauri::{Manager, RunEvent, State};
|
use tauri::{Emitter, Manager, RunEvent, State};
|
||||||
use yaak_proxy_lib::ProxyCtx;
|
use yaak_proxy_lib::ProxyCtx;
|
||||||
use yaak_rpc::RpcRouter;
|
use yaak_rpc::{RpcEventEmitter, RpcRouter};
|
||||||
use yaak_window::window::CreateWindowConfig;
|
use yaak_window::window::CreateWindowConfig;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -20,8 +20,21 @@ pub fn run() {
|
|||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let data_dir = app.path().app_data_dir().expect("no app data dir");
|
let data_dir = app.path().app_data_dir().expect("no app data dir");
|
||||||
std::fs::create_dir_all(&data_dir).expect("failed to create app data dir");
|
std::fs::create_dir_all(&data_dir).expect("failed to create app data dir");
|
||||||
app.manage(ProxyCtx::new(&data_dir.join("proxy.db")));
|
|
||||||
|
let (emitter, event_rx) = RpcEventEmitter::new();
|
||||||
|
app.manage(ProxyCtx::new(&data_dir.join("proxy.db"), emitter));
|
||||||
app.manage(yaak_proxy_lib::build_router());
|
app.manage(yaak_proxy_lib::build_router());
|
||||||
|
|
||||||
|
// Drain RPC events and forward as Tauri events
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for event in event_rx {
|
||||||
|
if let Err(e) = app_handle.emit(event.event, event.payload) {
|
||||||
|
error!("Failed to emit RPC event: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![rpc])
|
.invoke_handler(tauri::generate_handler![rpc])
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
|
|||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
ts-rs = { workspace = true }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub use db_context::DbContext;
|
|||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use migrate::run_migrations;
|
pub use migrate::run_migrations;
|
||||||
pub use traits::{UpsertModelInfo, upsert_date};
|
pub use traits::{UpsertModelInfo, upsert_date};
|
||||||
pub use update_source::UpdateSource;
|
pub use update_source::{ModelChangeEvent, UpdateSource};
|
||||||
pub use util::{generate_id, generate_id_of_length, generate_prefixed_id};
|
pub use util::{generate_id, generate_id_of_length, generate_prefixed_id};
|
||||||
|
|
||||||
// Re-export pool types that consumers will need
|
// Re-export pool types that consumers will need
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
@@ -15,3 +16,10 @@ impl UpdateSource {
|
|||||||
Self::Window { label: label.into() }
|
Self::Window { label: label.into() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
|
pub enum ModelChangeEvent {
|
||||||
|
Upsert { created: bool },
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
/// Type-erased handler function: takes context + JSON payload, returns JSON or error.
|
/// Type-erased handler function: takes context + JSON payload, returns JSON or error.
|
||||||
type HandlerFn<Ctx> = Box<dyn Fn(&Ctx, serde_json::Value) -> Result<serde_json::Value, RpcError> + Send + Sync>;
|
type HandlerFn<Ctx> = Box<dyn Fn(&Ctx, serde_json::Value) -> Result<serde_json::Value, RpcError> + Send + Sync>;
|
||||||
@@ -101,29 +102,97 @@ impl<Ctx> RpcRouter<Ctx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define RPC commands with a single source of truth.
|
/// A named event carrying a JSON payload, emitted from backend to frontend.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct RpcEvent {
|
||||||
|
pub event: &'static str,
|
||||||
|
pub payload: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Channel-based event emitter. The backend calls `emit()`, the transport
|
||||||
|
/// adapter (Tauri, WebSocket, etc.) drains the receiver and delivers events.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RpcEventEmitter {
|
||||||
|
tx: mpsc::Sender<RpcEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcEventEmitter {
|
||||||
|
pub fn new() -> (Self, mpsc::Receiver<RpcEvent>) {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
(Self { tx }, rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a typed event. Serializes the payload to JSON.
|
||||||
|
pub fn emit<T: Serialize>(&self, event: &'static str, payload: &T) {
|
||||||
|
if let Ok(value) = serde_json::to_value(payload) {
|
||||||
|
let _ = self.tx.send(RpcEvent { event, payload: value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define RPC commands and events with a single source of truth.
|
||||||
///
|
///
|
||||||
/// Generates:
|
/// Generates:
|
||||||
/// - `build_router()` — creates an `RpcRouter` with all handlers registered
|
/// - `build_router()` — creates an `RpcRouter` with all handlers registered
|
||||||
/// - `RpcSchema` — a struct with ts-rs derives for TypeScript type generation
|
/// - `RpcSchema` — a struct with ts-rs derives for TypeScript type generation
|
||||||
|
/// - `RpcEventSchema` — (if events declared) a struct mapping event names to payload types
|
||||||
|
///
|
||||||
|
/// The wire name for each command/event is derived from `stringify!($ident)`.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// define_rpc! {
|
/// define_rpc! {
|
||||||
/// ProxyCtx;
|
/// ProxyCtx;
|
||||||
/// "proxy_start" => proxy_start(ProxyStartRequest) -> ProxyStartResponse,
|
/// commands {
|
||||||
/// "proxy_stop" => proxy_stop(ProxyStopRequest) -> bool,
|
/// proxy_start(ProxyStartRequest) -> ProxyStartResponse,
|
||||||
|
/// proxy_stop(ProxyStopRequest) -> bool,
|
||||||
|
/// }
|
||||||
|
/// events {
|
||||||
|
/// model_write(ModelPayload),
|
||||||
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! define_rpc {
|
macro_rules! define_rpc {
|
||||||
|
// With both commands and events
|
||||||
(
|
(
|
||||||
$ctx:ty;
|
$ctx:ty;
|
||||||
$( $name:literal => $handler:ident ( $req:ty ) -> $res:ty ),* $(,)?
|
commands {
|
||||||
|
$( $handler:ident ( $req:ty ) -> $res:ty ),* $(,)?
|
||||||
|
}
|
||||||
|
events {
|
||||||
|
$( $evt_ident:ident ( $evt_payload:ty ) ),* $(,)?
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
pub fn build_router() -> $crate::RpcRouter<$ctx> {
|
pub fn build_router() -> $crate::RpcRouter<$ctx> {
|
||||||
let mut router = $crate::RpcRouter::new();
|
let mut router = $crate::RpcRouter::new();
|
||||||
$( router.register($name, $crate::rpc_handler!($handler)); )*
|
$( router.register(stringify!($handler), $crate::rpc_handler!($handler)); )*
|
||||||
|
router
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ts_rs::TS)]
|
||||||
|
#[ts(export, export_to = "gen_rpc.ts")]
|
||||||
|
pub struct RpcSchema {
|
||||||
|
$( pub $handler: ($req, $res), )*
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ts_rs::TS)]
|
||||||
|
#[ts(export, export_to = "gen_rpc.ts")]
|
||||||
|
pub struct RpcEventSchema {
|
||||||
|
$( pub $evt_ident: $evt_payload, )*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Commands only (no events)
|
||||||
|
(
|
||||||
|
$ctx:ty;
|
||||||
|
commands {
|
||||||
|
$( $handler:ident ( $req:ty ) -> $res:ty ),* $(,)?
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
pub fn build_router() -> $crate::RpcRouter<$ctx> {
|
||||||
|
let mut router = $crate::RpcRouter::new();
|
||||||
|
$( router.register(stringify!($handler), $crate::rpc_handler!($handler)); )*
|
||||||
router
|
router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::collections::BTreeMap;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_core::WorkspaceContext;
|
use yaak_core::WorkspaceContext;
|
||||||
|
|
||||||
pub use yaak_database::{generate_id, generate_id_of_length, generate_prefixed_id};
|
pub use yaak_database::{ModelChangeEvent, generate_id, generate_id_of_length, generate_prefixed_id};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -21,14 +21,6 @@ pub struct ModelPayload {
|
|||||||
pub change: ModelChangeEvent,
|
pub change: ModelChangeEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
|
||||||
pub enum ModelChangeEvent {
|
|
||||||
Upsert { created: bool },
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
|
|||||||
Reference in New Issue
Block a user