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

Working with unsaved edits

An editor needs to reuse work between keystrokes without replacing saved results on disk. Mark the edited observations as session inputs. Queries still use the same functions and keys; their input reads determine where results may be kept.

Lifetime follows the observations

Each field observation has one of three lifetimes:

LifetimeDerived work may survive
OperationOnly the operation that computed it.
SessionLater operations in this database owner, within a memory budget.
PersistentLater operations and, on a disk backend, reopening.

A query inherits the shortest lifetime it reads, including through child queries. If a saved project depends on one unsaved file, affected work becomes session work; unaffected saved queries remain durable.

Lifetime does not describe how often data changes. That is the separate durability classification.

Keep the saved view and override one field

The editor-session example stores text together with its retention policy:

extern crate ajika;
extern crate anyhow;
extern crate tempfile;
use std::{sync::Arc, time::Duration};

use ajika::{Database, Db, Inputs, Lifetime, Retention, Stamp};

#[derive(Clone)]
struct Text {
    contents: Arc<String>,
    lifetime: Lifetime,
}

impl Text {
    fn stamp(&self) -> ajika::Result<Stamp> {
        Ok(Stamp::of(self.contents.as_bytes()))
    }

    fn lifetime(&self) -> Lifetime {
        self.lifetime
    }
}

#[ajika::input(key = String)]
struct File {
    #[input(stamp = Text::stamp, lifetime = Text::lifetime)]
    text: Text,
}

#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
    Ok(File::at(path).text(db)?.contents.split_whitespace().count())
}

fn main() -> anyhow::Result<()> {
    let directory = tempfile::tempdir()?;
    let mut database = Database::open(
        directory.path().join("queries"),
        "editor-v1",
        Inputs::default(),
        (),
        Retention {
            max_entries: 10_000,
            max_bytes: 256 * 1024 * 1024,
            max_idle: Duration::from_secs(24 * 60 * 60),
        },
    )?;
    let file = File::at("notes.txt".into());
    database.update_inputs(|update| {
        file.set_text(
            update,
            Text {
                contents: Arc::new("hello world".into()),
                lifetime: Lifetime::Persistent,
            },
        )
    })?;
    database.run(|db| word_count(db, "notes.txt".into()))?;

    let saved = database.inputs().clone();
    let edited = Arc::new("hello new world".to_owned());
    let editor = saved.overlay().edit(|update| {
        file.set_text(
            update,
            Text {
                contents: edited.clone(),
                lifetime: Lifetime::Session,
            },
        )
    })?;
    database.replace_inputs(editor);
    assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 3);
    assert_eq!(database.statistics().bytes_written, 0);

    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 0);
    assert_eq!(database.statistics().decoded, 0);

    // The host has saved these exact bytes successfully.
    let saved = saved.edit(|update| {
        file.set_text(
            update,
            Text {
                contents: edited,
                lifetime: Lifetime::Persistent,
            },
        )
    })?;
    database.replace_inputs(saved);
    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 0);
    assert_eq!(database.statistics().session_queries, 0);
    Ok(())
}

The custom stamp identifies the text; the lifetime callback supplies its policy. Changing policy is still an input change even if the stamp stays equal.

After computing the saved file, the application creates a sparse input overlay:

extern crate ajika;
extern crate anyhow;
extern crate tempfile;
use std::{sync::Arc, time::Duration};

use ajika::{Database, Db, Inputs, Lifetime, Retention, Stamp};

#[derive(Clone)]
struct Text {
    contents: Arc<String>,
    lifetime: Lifetime,
}

impl Text {
    fn stamp(&self) -> ajika::Result<Stamp> {
        Ok(Stamp::of(self.contents.as_bytes()))
    }

    fn lifetime(&self) -> Lifetime {
        self.lifetime
    }
}

#[ajika::input(key = String)]
struct File {
    #[input(stamp = Text::stamp, lifetime = Text::lifetime)]
    text: Text,
}

#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
    Ok(File::at(path).text(db)?.contents.split_whitespace().count())
}

fn main() -> anyhow::Result<()> {
    let directory = tempfile::tempdir()?;
    let mut database = Database::open(
        directory.path().join("queries"),
        "editor-v1",
        Inputs::default(),
        (),
        Retention {
            max_entries: 10_000,
            max_bytes: 256 * 1024 * 1024,
            max_idle: Duration::from_secs(24 * 60 * 60),
        },
    )?;
    let file = File::at("notes.txt".into());
    database.update_inputs(|update| {
        file.set_text(
            update,
            Text {
                contents: Arc::new("hello world".into()),
                lifetime: Lifetime::Persistent,
            },
        )
    })?;
    database.run(|db| word_count(db, "notes.txt".into()))?;

    let saved = database.inputs().clone();
    let edited = Arc::new("hello new world".to_owned());
    let editor = saved.overlay().edit(|update| {
        file.set_text(
            update,
            Text {
                contents: edited.clone(),
                lifetime: Lifetime::Session,
            },
        )
    })?;
    database.replace_inputs(editor);
    assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 3);
    assert_eq!(database.statistics().bytes_written, 0);

    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 0);
    assert_eq!(database.statistics().decoded, 0);

    // The host has saved these exact bytes successfully.
    let saved = saved.edit(|update| {
        file.set_text(
            update,
            Text {
                contents: edited,
                lifetime: Lifetime::Persistent,
            },
        )
    })?;
    database.replace_inputs(saved);
    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 0);
    assert_eq!(database.statistics().session_queries, 0);
    Ok(())
}

The overlay refers to the saved base and stores only its overrides. It does not copy all saved sources. Missing fields can still be acquired through the base. The application retains the saved view so it can discard or save editor changes later.

The first operation computes the edited count. The second reuses it directly from the session store. Neither writes that edited payload to disk.

This is one installed input view and one set of latest session results per owner. It is not a facility for querying several historical revisions concurrently.

Saving can reuse the same computation

After the host successfully saves those exact bytes, it publishes the same content as persistent:

extern crate ajika;
extern crate anyhow;
extern crate tempfile;
use std::{sync::Arc, time::Duration};

use ajika::{Database, Db, Inputs, Lifetime, Retention, Stamp};

#[derive(Clone)]
struct Text {
    contents: Arc<String>,
    lifetime: Lifetime,
}

impl Text {
    fn stamp(&self) -> ajika::Result<Stamp> {
        Ok(Stamp::of(self.contents.as_bytes()))
    }

    fn lifetime(&self) -> Lifetime {
        self.lifetime
    }
}

#[ajika::input(key = String)]
struct File {
    #[input(stamp = Text::stamp, lifetime = Text::lifetime)]
    text: Text,
}

#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
    Ok(File::at(path).text(db)?.contents.split_whitespace().count())
}

fn main() -> anyhow::Result<()> {
    let directory = tempfile::tempdir()?;
    let mut database = Database::open(
        directory.path().join("queries"),
        "editor-v1",
        Inputs::default(),
        (),
        Retention {
            max_entries: 10_000,
            max_bytes: 256 * 1024 * 1024,
            max_idle: Duration::from_secs(24 * 60 * 60),
        },
    )?;
    let file = File::at("notes.txt".into());
    database.update_inputs(|update| {
        file.set_text(
            update,
            Text {
                contents: Arc::new("hello world".into()),
                lifetime: Lifetime::Persistent,
            },
        )
    })?;
    database.run(|db| word_count(db, "notes.txt".into()))?;

    let saved = database.inputs().clone();
    let edited = Arc::new("hello new world".to_owned());
    let editor = saved.overlay().edit(|update| {
        file.set_text(
            update,
            Text {
                contents: edited.clone(),
                lifetime: Lifetime::Session,
            },
        )
    })?;
    database.replace_inputs(editor);
    assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 3);
    assert_eq!(database.statistics().bytes_written, 0);

    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 0);
    assert_eq!(database.statistics().decoded, 0);

    // The host has saved these exact bytes successfully.
    let saved = saved.edit(|update| {
        file.set_text(
            update,
            Text {
                contents: edited,
                lifetime: Lifetime::Persistent,
            },
        )
    })?;
    database.replace_inputs(saved);
    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 0);
    assert_eq!(database.statistics().session_queries, 0);
    Ok(())
}

The text stamp matches. Ajika refreshes the derived lifetime and can promote the retained result to durable storage without executing the query again. This reuse depends on the session result still being retained; eviction may require computation.

If an edit is discarded, install the saved input view instead. That is a new revision, not a rewind of the runtime. Validation decides which saved answers match it.

Budget the session separately

Database::set_session_retention controls retained session records and changed payloads. The default budget is 16,384 records, 64 MiB of estimated encoded data, and five minutes since last use. These are cache limits, not a measurement of allocator memory.

The session store can refer back to an equal durable payload rather than keep a second decoded copy. Changed payloads remain typed values in memory. Transient query results still expire with the operation.

clear_session drops retained session query work. It does not close editor buffers or replace input overlays; the host owns those actions. Keeping many returned Arc values alive also keeps their allocations alive independently of these budgets.

Checkpointing explains how Ajika separates session overrides from the durable cache.