host: the drafts vault commands (draft_list/save/revisions/status, observation_list, data_export) are the host's (AppConfig::with_vault, commands::VAULT); the app takes the vault through vault_commands::lock
This commit is contained in:
parent
2e49577196
commit
8c2404b4cc
6 changed files with 147 additions and 11 deletions
|
|
@ -102,6 +102,10 @@ pub struct AppConfig {
|
|||
/// Keyword alert settings (saved in app data, handed to the page runtime): serves
|
||||
/// [`commands::ALERTS`]; the match and the notification are the app's.
|
||||
pub alerts: Option<&'static crate::alerts_commands::AlertsConfig>,
|
||||
/// The drafts vault (a SQLite database in app data with CAS revisions): serves
|
||||
/// [`commands::VAULT`]; the rules (kinds, limits, capture ownership) are the app's.
|
||||
#[cfg(feature = "vault")]
|
||||
pub vault: Option<&'static crate::vault_commands::VaultConfig>,
|
||||
/// Native updates and the hotfix channel: serves [`commands::UPDATE`] and runs the update loop.
|
||||
/// Needs `updater` (the plugin) and the `net` feature.
|
||||
#[cfg(feature = "net")]
|
||||
|
|
@ -134,6 +138,8 @@ impl AppConfig {
|
|||
dev: false,
|
||||
themes: None,
|
||||
alerts: None,
|
||||
#[cfg(feature = "vault")]
|
||||
vault: None,
|
||||
#[cfg(feature = "net")]
|
||||
updates: None,
|
||||
}
|
||||
|
|
@ -164,6 +170,13 @@ impl AppConfig {
|
|||
self
|
||||
}
|
||||
|
||||
/// Serves the drafts vault commands ([`commands::VAULT`]) over this database and rules.
|
||||
#[cfg(feature = "vault")]
|
||||
pub const fn with_vault(mut self, vault: &'static crate::vault_commands::VaultConfig) -> Self {
|
||||
self.vault = Some(vault);
|
||||
self
|
||||
}
|
||||
|
||||
/// Serves the sync commands ([`commands::SYNC`]).
|
||||
pub const fn with_sync(mut self) -> Self {
|
||||
self.sync = true;
|
||||
|
|
@ -1281,6 +1294,14 @@ fn alerts_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync +
|
|||
crate::alerts_commands::handler()
|
||||
}
|
||||
|
||||
/// The drafts vault commands ([`commands::VAULT`]); without the `vault` feature none are served.
|
||||
fn vault_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||
#[cfg(feature = "vault")]
|
||||
return crate::vault_commands::handler();
|
||||
#[cfg(not(feature = "vault"))]
|
||||
|_: tauri::ipc::Invoke<Wry>| false
|
||||
}
|
||||
|
||||
/// The theme commands ([`commands::THEME`]).
|
||||
fn theme_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||
crate::theme_commands::handler()
|
||||
|
|
@ -1356,6 +1377,7 @@ impl App {
|
|||
let dev_handler = dev_handler();
|
||||
let theme_handler = theme_handler();
|
||||
let alerts_handler = alerts_handler();
|
||||
let vault_handler = vault_handler();
|
||||
let backup_handler = backup_handler();
|
||||
let builder = tauri::Builder::<Wry>::default()
|
||||
// Must be first: a second launch focuses the running window instead of fighting over
|
||||
|
|
@ -1444,6 +1466,8 @@ impl App {
|
|||
crate::dev_commands::init(&handle);
|
||||
crate::theme_commands::init(&handle);
|
||||
crate::alerts_commands::init(&handle);
|
||||
#[cfg(feature = "vault")]
|
||||
crate::vault_commands::init(&handle)?;
|
||||
#[cfg(feature = "net")]
|
||||
crate::fonts_commands::init(&handle);
|
||||
#[cfg(feature = "net")]
|
||||
|
|
@ -1480,14 +1504,30 @@ impl App {
|
|||
theme_handler(invoke)
|
||||
} else if commands::ALERTS.contains(&invoke.message.command()) {
|
||||
alerts_handler(invoke)
|
||||
} else if commands::VAULT.contains(&invoke.message.command()) {
|
||||
vault_handler(invoke)
|
||||
} else if commands::is_host(invoke.message.command()) {
|
||||
host_handler(invoke)
|
||||
} else {
|
||||
handler(invoke)
|
||||
}
|
||||
})
|
||||
.run(context)
|
||||
.unwrap_or_else(|error| panic!("Failed to launch {}: {error}", config.title));
|
||||
.build(context)
|
||||
.unwrap_or_else(|error| panic!("Failed to launch {}: {error}", config.title))
|
||||
.run(|_, event| log_run_event(&event));
|
||||
}
|
||||
}
|
||||
|
||||
/// Why the app is going away, for `--diag` ([`crate::diag`]): an exit request with its code (None is a
|
||||
/// polite quit from outside: Cmd+Q, the Dock, or another app such as an auto-quitter closing the last
|
||||
/// window), window close/destroy events and the final exit. Nothing is written without diagnostics.
|
||||
fn log_run_event(event: &tauri::RunEvent) {
|
||||
match event {
|
||||
tauri::RunEvent::ExitRequested { code, .. } => crate::diag::log("host", &format!("exit requested code={code:?}")),
|
||||
tauri::RunEvent::Exit => crate::diag::log("host", "exit"),
|
||||
tauri::RunEvent::WindowEvent { label, event: tauri::WindowEvent::CloseRequested { .. }, .. } => crate::diag::log("host", &format!("window {label} close requested")),
|
||||
tauri::RunEvent::WindowEvent { label, event: tauri::WindowEvent::Destroyed, .. } => crate::diag::log("host", &format!("window {label} destroyed")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,9 +130,14 @@ pub const THEME: &[&str] = &["theme_get", "theme_set", "theme_preview", "theme_s
|
|||
/// (`testkit::Capabilities::assert_shell_only_group`).
|
||||
pub const ALERTS: &[&str] = &["alerts_get", "alerts_set"];
|
||||
|
||||
/// Shell-only, optional: the drafts vault (list, save with a revision check, revisions, archive and
|
||||
/// restore, observations, one consistent export), served when the app sets `AppConfig::vault`. An app
|
||||
/// that uses them grants these to the shell (`testkit::Capabilities::assert_shell_only_group`).
|
||||
pub const VAULT: &[&str] = &["draft_list", "draft_save", "draft_revisions", "draft_status", "observation_list", "data_export"];
|
||||
|
||||
/// Whether `command` is one of the host's (dispatched to the host, never to the app's handler).
|
||||
pub fn is_host(command: &str) -> bool {
|
||||
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) || UPDATE.contains(&command) || WORKSPACES.contains(&command) || SYNC.contains(&command) || DEV.contains(&command) || BACKUP.contains(&command) || THEME.contains(&command) || ALERTS.contains(&command)
|
||||
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) || UPDATE.contains(&command) || WORKSPACES.contains(&command) || SYNC.contains(&command) || DEV.contains(&command) || BACKUP.contains(&command) || THEME.contains(&command) || ALERTS.contains(&command) || VAULT.contains(&command)
|
||||
}
|
||||
|
||||
/// The capability permission tauri-build generates for `command` (`tabs_sync` → `allow-tabs-sync`).
|
||||
|
|
@ -144,11 +149,11 @@ pub fn permission(command: &str) -> String {
|
|||
/// once). Leaked on purpose: tauri-build wants `&'static` and a build script runs once.
|
||||
///
|
||||
/// ```ignore
|
||||
/// tauri_build::AppManifest::new().commands(waybetter_host::commands::manifest(&["draft_list"]))
|
||||
/// tauri_build::AppManifest::new().commands(waybetter_host::commands::manifest(&["capture_draft"]))
|
||||
/// ```
|
||||
pub fn manifest(app: &[&'static str]) -> &'static [&'static str] {
|
||||
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len() + UPDATE.len() + WORKSPACES.len() + SYNC.len() + DEV.len() + BACKUP.len() + THEME.len() + ALERTS.len());
|
||||
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP).chain(THEME).chain(ALERTS) {
|
||||
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len() + UPDATE.len() + WORKSPACES.len() + SYNC.len() + DEV.len() + BACKUP.len() + THEME.len() + ALERTS.len() + VAULT.len());
|
||||
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP).chain(THEME).chain(ALERTS).chain(VAULT) {
|
||||
if !all.contains(command) {
|
||||
all.push(command);
|
||||
}
|
||||
|
|
@ -162,14 +167,14 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn manifest_adds_the_host_commands_once() {
|
||||
let all = manifest(&["draft_list", "tabs_sync"]);
|
||||
assert_eq!(all[0], "draft_list");
|
||||
let all = manifest(&["capture_draft", "tabs_sync"]);
|
||||
assert_eq!(all[0], "capture_draft");
|
||||
assert_eq!(all.iter().filter(|c| **c == "tabs_sync").count(), 1);
|
||||
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP).chain(THEME).chain(ALERTS) {
|
||||
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP).chain(THEME).chain(ALERTS).chain(VAULT) {
|
||||
assert!(all.contains(command), "{command}");
|
||||
assert!(is_host(command));
|
||||
}
|
||||
assert!(!is_host("draft_list"));
|
||||
assert!(!is_host("capture_draft"));
|
||||
assert_eq!(permission("tab_link_drag"), "allow-tab-link-drag");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ pub mod theme_commands;
|
|||
pub mod tab_commands;
|
||||
#[cfg(feature = "app")]
|
||||
pub mod alerts_commands;
|
||||
#[cfg(all(feature = "app", feature = "vault"))]
|
||||
pub mod vault_commands;
|
||||
#[cfg(feature = "app")]
|
||||
mod backup_commands;
|
||||
#[cfg(all(feature = "app", feature = "net"))]
|
||||
|
|
|
|||
|
|
@ -280,6 +280,14 @@ pub fn assert_alerts_guards() {
|
|||
}
|
||||
}
|
||||
|
||||
/// The vault commands' wrappers check their caller first. Runs against the host's source.
|
||||
pub fn assert_vault_guards() {
|
||||
let source = include_str!("vault_commands.rs");
|
||||
for command in commands::VAULT {
|
||||
assert_guarded(source, command, "shell_only(&webview)?");
|
||||
}
|
||||
}
|
||||
|
||||
/// The backup commands' wrappers check their caller first. Runs against the host's source.
|
||||
pub fn assert_backup_guards() {
|
||||
let source = include_str!("backup_commands.rs");
|
||||
|
|
@ -303,6 +311,7 @@ mod tests {
|
|||
assert_backup_guards();
|
||||
assert_theme_guards();
|
||||
assert_alerts_guards();
|
||||
assert_vault_guards();
|
||||
}
|
||||
|
||||
fn caps_with(shell_extra: &str, tab_extra: &str, site_extra: &str) -> Capabilities {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub struct CaptureRules {
|
|||
}
|
||||
|
||||
/// What an app decides about its drafts.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VaultRules {
|
||||
pub kinds: &'static [&'static str],
|
||||
pub sources: &'static [&'static str],
|
||||
|
|
|
|||
80
crates/waybetter-host/src/vault_commands.rs
Normal file
80
crates/waybetter-host/src/vault_commands.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
//! The drafts vault, native side, for every app that sets [`crate::app::AppConfig::vault`]: opens the
|
||||
//! app's [`Vault`] in app data at launch and serves the shell-only `draft_list`, `draft_save`,
|
||||
//! `draft_revisions`, `draft_status`, `observation_list` and `data_export` ([`crate::commands::VAULT`]).
|
||||
//! A write is acknowledged only after its SQLite transaction committed. The app's own native code (its
|
||||
//! capture, a popover that reads drafts, a backup import) takes the same vault through [`lock`]; what
|
||||
//! it does with it is the app's.
|
||||
|
||||
use crate::app::config;
|
||||
use crate::host::{data_file, shell_only};
|
||||
use crate::vault::{Draft, DraftRevision, ExportData, Observation, SaveDraftInput, Vault, VaultRules};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use tauri::{AppHandle, Manager, Webview};
|
||||
|
||||
/// What an app declares about its vault: the database file in app data and its rules.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct VaultConfig {
|
||||
pub file: &'static str,
|
||||
pub rules: &'static VaultRules,
|
||||
}
|
||||
|
||||
struct VaultState(Mutex<Vault>);
|
||||
|
||||
/// Opens the database. Does nothing unless the app set [`crate::app::AppConfig::vault`]. A database
|
||||
/// that cannot be opened (a newer schema, a damaged file) stops the launch rather than starting empty.
|
||||
pub(crate) fn init(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(vault) = config(app).vault else { return Ok(()) };
|
||||
let path = data_file(app, vault.file).ok_or("STORAGE: no application data directory")?;
|
||||
app.manage(VaultState(Mutex::new(Vault::open(path, vault.rules).map_err(std::io::Error::other)?)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The vault, locked. Hold it only for the call that needs it; never across a webview call.
|
||||
pub fn lock(app: &AppHandle) -> Result<MutexGuard<'_, Vault>, String> {
|
||||
app.try_state::<VaultState>()
|
||||
.ok_or("DATABASE: vault unavailable; restart safely")?
|
||||
.inner()
|
||||
.0
|
||||
.lock()
|
||||
.map_err(|_| "DATABASE: workspace state unavailable; restart safely".into())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn draft_list(webview: Webview, app: AppHandle) -> Result<Vec<Draft>, String> {
|
||||
shell_only(&webview)?;
|
||||
lock(&app)?.list_drafts()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn draft_save(webview: Webview, app: AppHandle, input: SaveDraftInput) -> Result<Draft, String> {
|
||||
shell_only(&webview)?;
|
||||
lock(&app)?.save_draft(&input)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn draft_revisions(webview: Webview, app: AppHandle, draft_id: String) -> Result<Vec<DraftRevision>, String> {
|
||||
shell_only(&webview)?;
|
||||
lock(&app)?.revisions(&draft_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn draft_status(webview: Webview, app: AppHandle, id: String, expected_revision: i64, status: String) -> Result<Draft, String> {
|
||||
shell_only(&webview)?;
|
||||
lock(&app)?.set_status(&id, expected_revision, &status)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn observation_list(webview: Webview, app: AppHandle) -> Result<Vec<Observation>, String> {
|
||||
shell_only(&webview)?;
|
||||
lock(&app)?.observations()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn data_export(webview: Webview, app: AppHandle) -> Result<ExportData, String> {
|
||||
shell_only(&webview)?;
|
||||
lock(&app)?.export()
|
||||
}
|
||||
|
||||
pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![draft_list, draft_save, draft_revisions, draft_status, observation_list, data_export]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue