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
|
/// Keyword alert settings (saved in app data, handed to the page runtime): serves
|
||||||
/// [`commands::ALERTS`]; the match and the notification are the app's.
|
/// [`commands::ALERTS`]; the match and the notification are the app's.
|
||||||
pub alerts: Option<&'static crate::alerts_commands::AlertsConfig>,
|
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.
|
/// Native updates and the hotfix channel: serves [`commands::UPDATE`] and runs the update loop.
|
||||||
/// Needs `updater` (the plugin) and the `net` feature.
|
/// Needs `updater` (the plugin) and the `net` feature.
|
||||||
#[cfg(feature = "net")]
|
#[cfg(feature = "net")]
|
||||||
|
|
@ -134,6 +138,8 @@ impl AppConfig {
|
||||||
dev: false,
|
dev: false,
|
||||||
themes: None,
|
themes: None,
|
||||||
alerts: None,
|
alerts: None,
|
||||||
|
#[cfg(feature = "vault")]
|
||||||
|
vault: None,
|
||||||
#[cfg(feature = "net")]
|
#[cfg(feature = "net")]
|
||||||
updates: None,
|
updates: None,
|
||||||
}
|
}
|
||||||
|
|
@ -164,6 +170,13 @@ impl AppConfig {
|
||||||
self
|
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`]).
|
/// Serves the sync commands ([`commands::SYNC`]).
|
||||||
pub const fn with_sync(mut self) -> Self {
|
pub const fn with_sync(mut self) -> Self {
|
||||||
self.sync = true;
|
self.sync = true;
|
||||||
|
|
@ -1281,6 +1294,14 @@ fn alerts_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync +
|
||||||
crate::alerts_commands::handler()
|
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`]).
|
/// The theme commands ([`commands::THEME`]).
|
||||||
fn theme_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
fn theme_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||||
crate::theme_commands::handler()
|
crate::theme_commands::handler()
|
||||||
|
|
@ -1356,6 +1377,7 @@ impl App {
|
||||||
let dev_handler = dev_handler();
|
let dev_handler = dev_handler();
|
||||||
let theme_handler = theme_handler();
|
let theme_handler = theme_handler();
|
||||||
let alerts_handler = alerts_handler();
|
let alerts_handler = alerts_handler();
|
||||||
|
let vault_handler = vault_handler();
|
||||||
let backup_handler = backup_handler();
|
let backup_handler = backup_handler();
|
||||||
let builder = tauri::Builder::<Wry>::default()
|
let builder = tauri::Builder::<Wry>::default()
|
||||||
// Must be first: a second launch focuses the running window instead of fighting over
|
// 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::dev_commands::init(&handle);
|
||||||
crate::theme_commands::init(&handle);
|
crate::theme_commands::init(&handle);
|
||||||
crate::alerts_commands::init(&handle);
|
crate::alerts_commands::init(&handle);
|
||||||
|
#[cfg(feature = "vault")]
|
||||||
|
crate::vault_commands::init(&handle)?;
|
||||||
#[cfg(feature = "net")]
|
#[cfg(feature = "net")]
|
||||||
crate::fonts_commands::init(&handle);
|
crate::fonts_commands::init(&handle);
|
||||||
#[cfg(feature = "net")]
|
#[cfg(feature = "net")]
|
||||||
|
|
@ -1480,14 +1504,30 @@ impl App {
|
||||||
theme_handler(invoke)
|
theme_handler(invoke)
|
||||||
} else if commands::ALERTS.contains(&invoke.message.command()) {
|
} else if commands::ALERTS.contains(&invoke.message.command()) {
|
||||||
alerts_handler(invoke)
|
alerts_handler(invoke)
|
||||||
|
} else if commands::VAULT.contains(&invoke.message.command()) {
|
||||||
|
vault_handler(invoke)
|
||||||
} else if commands::is_host(invoke.message.command()) {
|
} else if commands::is_host(invoke.message.command()) {
|
||||||
host_handler(invoke)
|
host_handler(invoke)
|
||||||
} else {
|
} else {
|
||||||
handler(invoke)
|
handler(invoke)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.run(context)
|
.build(context)
|
||||||
.unwrap_or_else(|error| panic!("Failed to launch {}: {error}", config.title));
|
.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`).
|
/// (`testkit::Capabilities::assert_shell_only_group`).
|
||||||
pub const ALERTS: &[&str] = &["alerts_get", "alerts_set"];
|
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).
|
/// 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 {
|
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`).
|
/// 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.
|
/// once). Leaked on purpose: tauri-build wants `&'static` and a build script runs once.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```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] {
|
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());
|
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) {
|
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) {
|
if !all.contains(command) {
|
||||||
all.push(command);
|
all.push(command);
|
||||||
}
|
}
|
||||||
|
|
@ -162,14 +167,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manifest_adds_the_host_commands_once() {
|
fn manifest_adds_the_host_commands_once() {
|
||||||
let all = manifest(&["draft_list", "tabs_sync"]);
|
let all = manifest(&["capture_draft", "tabs_sync"]);
|
||||||
assert_eq!(all[0], "draft_list");
|
assert_eq!(all[0], "capture_draft");
|
||||||
assert_eq!(all.iter().filter(|c| **c == "tabs_sync").count(), 1);
|
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!(all.contains(command), "{command}");
|
||||||
assert!(is_host(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");
|
assert_eq!(permission("tab_link_drag"), "allow-tab-link-drag");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,8 @@ pub mod theme_commands;
|
||||||
pub mod tab_commands;
|
pub mod tab_commands;
|
||||||
#[cfg(feature = "app")]
|
#[cfg(feature = "app")]
|
||||||
pub mod alerts_commands;
|
pub mod alerts_commands;
|
||||||
|
#[cfg(all(feature = "app", feature = "vault"))]
|
||||||
|
pub mod vault_commands;
|
||||||
#[cfg(feature = "app")]
|
#[cfg(feature = "app")]
|
||||||
mod backup_commands;
|
mod backup_commands;
|
||||||
#[cfg(all(feature = "app", feature = "net"))]
|
#[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.
|
/// The backup commands' wrappers check their caller first. Runs against the host's source.
|
||||||
pub fn assert_backup_guards() {
|
pub fn assert_backup_guards() {
|
||||||
let source = include_str!("backup_commands.rs");
|
let source = include_str!("backup_commands.rs");
|
||||||
|
|
@ -303,6 +311,7 @@ mod tests {
|
||||||
assert_backup_guards();
|
assert_backup_guards();
|
||||||
assert_theme_guards();
|
assert_theme_guards();
|
||||||
assert_alerts_guards();
|
assert_alerts_guards();
|
||||||
|
assert_vault_guards();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn caps_with(shell_extra: &str, tab_extra: &str, site_extra: &str) -> Capabilities {
|
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.
|
/// What an app decides about its drafts.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct VaultRules {
|
pub struct VaultRules {
|
||||||
pub kinds: &'static [&'static str],
|
pub kinds: &'static [&'static str],
|
||||||
pub sources: &'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