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:
| Lifetime | Derived work may survive |
|---|---|
Operation | Only the operation that computed it. |
Session | Later operations in this database owner, within a memory budget. |
Persistent | Later 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.