Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Keyed

PersistedListModel<T> — bridge between a reactive ListModel<T> and a single TOML file, merging by op, not by whole-document snapshot.

Public types

KindName
traitKeyed — An item with a stable, owned identity — the merge key PersistedListModel dedupes and diffs by
enumListOp — A replayable mutation of a PersistedListModel's backing list, expressed by key so it can be applied to any starting Vec<T> — in particular, the…
structListFile — On-disk shape for a persisted list: a versioned wrapper around Vec<T>
structPersistedListModel — A reactive, Keyed-item list whose mutations persist to a single TOML file by merging ops, not by overwriting a whole-document snapshot

Public functions

Keyed

ReturnsFunction
Methods
Self::Keykey()
Constants and types
typeKey

PersistedListModel

ReturnsFunction
Constructors
Result<Self, SettingsFileError>open(path: PathBuf, delay: Duration, migrator: Migrator<ListFile<T>>)
Methods
&ListModel<T>model()
upsert_front(item: T)
boolupdate_in_place(item: T)
boolremove(key: &T::Key)
clear()
Result<(), SettingsFileError>flush_now()
&Pathpath()

Detailed description

Why ops, not snapshots

The previous design re-derived the entire Vec<T> from the live model on every mutation and scheduled a debounced write of that whole snapshot. That is last-write-wins by construction: if a peer process added an entry to the same file in the meantime, this process's next flush would overwrite the peer's row right off the disk — the exact "a newly-opened project vanishes from Recents" bug this crate exists to fix.

Instead, every mutation records a small, replayable ListOp<T> and hands it to the shared debounced writer as a crate::flush::Patch: "given the file's current text, apply this one op to it." The patch is applied against the document read fresh off disk, under a lock, at flush time — so it replays cleanly on top of whatever a peer wrote in the meantime, key by key, instead of overwriting the whole thing.

Identity

Every item needs a stable identity to merge by — see Keyed. Ops are keyed, not indexed: Remove only needs to carry a key, never a value, which is exactly what a diff of "what's gone" can always produce even though the value itself is no longer available once removed.

Mutating through this type, not through .model()

.model() is for reading and for reactive binding (ListView / Repeater) — every UI observer wants live updates regardless of who mutates. Writing must go through upsert_front, update_in_place, remove and clear: those are the only places that both mutate the live model and enqueue the matching op. Mutating the ListModel returned by .model() directly updates what's on screen but is never persisted — there is no observer bridging arbitrary model mutations to disk any more (that observer was the whole-snapshot overwrite bug).

Example

use teksilo_settings::{Keyed, Migrator, PersistedListModel};
use serde::{Deserialize, Serialize};
use std::time::Duration;

#[derive(Serialize, Deserialize, Clone)]
struct Tag { name: String }

impl Keyed for Tag {
    type Key = String;
    fn key(&self) -> String { self.name.clone() }
}

let path = std::env::temp_dir().join("tags-list-doctest.toml");
let plm: PersistedListModel<Tag> =
    PersistedListModel::open(path, Duration::ZERO, Migrator::new())
        .expect("open failed");
plm.upsert_front(Tag { name: "rust".into() });
plm.flush_now().expect("flush");

API reference

📖 Full rustdoc API for this module

pub trait Keyed

Import: use teksilo::settings::Keyed;

An item with a stable, owned identity — the merge key PersistedListModel dedupes and diffs by.

Key is owned (not borrowed, unlike the old MruEntry::Key: ?Sized shape) because it must be captured into a Patch (crate::flush::Patch) closure that crosses to the shared I/O worker thread — a borrow into T cannot outlive the mutation call that produced it.

#![allow(unused)]
fn main() {
pub trait Keyed { /* associated items below */ }
}

Associated items

type Key: Eq + Hash + Clone + Send + 'static;

The key type. Typically String / PathBuf / a small Copy id.

fn key(&self) -> Self::Key;

This item's identity. Returned by value: cheap for the small key types this is meant for (clone a String/PathBuf/id), and it sidesteps borrow-lifetime issues entirely.

pub enum ListOp

Import: use teksilo::settings::ListOp;

A replayable mutation of a PersistedListModel's backing list, expressed by key so it can be applied to any starting Vec<T> — in particular, the fresh one read off disk at flush time, which may already include a peer process's concurrent changes.

#![allow(unused)]
fn main() {
pub enum ListOp<T: Keyed> { /* variants */ }
}

Variants

  • UpsertFront (T) — Remove any existing entry with this item's key, then insert T at the front. This is the "most recently used" operation: re-running it against any starting vector — including one a peer has already mutated — reproduces the same dedupe-and-promote-to-front invariant MruList::add relies on.
  • UpdateInPlace (T) — Replace the entry with this item's key in place (no reordering). A no-op if the key is no longer present — e.g. a peer concurrently removed it, in which case that removal wins.
  • Remove (T::Key) — Remove the entry with this key, if present. No-op otherwise.
  • Clear — Drop every entry.

pub struct ListFile

Import: use teksilo::settings::ListFile;

On-disk shape for a persisted list: a versioned wrapper around Vec<T>. Apps write migrations against this type, not the bare Vec.

#![allow(unused)]
fn main() {
pub struct ListFile<T> { /* fields */ }
}

Fields

  • version: u32 — Schema version, matched against Versioned::CURRENT_VERSION on load to run any registered migrations before deserialization.
  • items: Vec<T> — The ordered list of items as stored on disk.

pub struct PersistedListModel

Import: use teksilo::settings::PersistedListModel;

A reactive, Keyed-item list whose mutations persist to a single TOML file by merging ops, not by overwriting a whole-document snapshot.

#![allow(unused)]
fn main() {
pub struct PersistedListModel<T>
where
    T: Keyed + Clone + Serialize + DeserializeOwned + Send + 'static, { /* fields */ }
}

Methods

pub fn open( path: PathBuf, delay: Duration, migrator: Migrator<ListFile<T>>, ) -> Result<Self, SettingsFileError>

Open the file at path (running migrator, under an exclusive lock so a peer mid-write can't hand us a torn read), seed the model from its contents, and retain everything needed to enqueue op patches on every mutation.

delay is the debounce window for writes — unlike crate::SettingsFile, this type's writes are expected to be frequent (every add/touch/remove on a live MRU list), so the debounce is real and load-bearing here, not vestigial.

pub fn model(&self) -> &ListModel<T>

The underlying reactive list handle. Clone it to share with Repeater / ListView widgets for reading. See the module docs: mutating the returned handle directly does not persist — use this type's own mutation methods instead.

pub fn upsert_front(&self, item: T)

Insert item at the front, deduping by item.key() (removing any existing entry with the same key first). Updates the live model immediately and enqueues the matching ListOp::UpsertFront.

pub fn update_in_place(&self, item: T) -> bool

Replace the entry with item.key() in place (no reordering). Returns false (and does nothing) if no entry with that key exists locally. Enqueues ListOp::UpdateInPlace on success.

pub fn remove(&self, key: &T::Key) -> bool

Remove the entry with this key, if present locally. Returns whether anything was removed. Enqueues ListOp::Remove on success.

pub fn clear(&self)

Drop every entry, locally and on disk.

pub fn flush_now(&self) -> Result<(), SettingsFileError>

Flush any pending op(s) to disk immediately, bypassing the debounce window. Flushes the op queue — never a re-derived snapshot of the in-memory list, which is exactly the mechanism that used to let a cleanly-exiting process erase a peer's newly-added entry.

pub fn path(&self) -> &Path

The absolute path of the TOML file being written to.