About Ajika
Ajika is a library that lets your application to remember computations and reuse them when inputs do not change. Moreover, it lets you to store these computations to the disk, allowing you to offload computations from memory when they are not needed, load them on demand, and reuse computations across the application restarts.
In other words, Ajika is a persistent incrementality library.
Ajika is heavily inspired by Salsa, with a different focus. Salsa is generally oriented to provide maximum efficiency, and is memory-first. Ajika is the opposite: it accepts a performance hit (often not very big in practice, especially for heavy computations) to offload data from memory and load it from disk on demand. It might be a good choice if you want to use Salsa approach but reduce the memory usage.
You can also keep reusable work in RAM with a memory database. For frequent unsaved edits, a session overlay can reuse results in memory while preserving saved answers on disk. The query functions stay the same in each case; the following chapters show how to choose where their results live.
LLM usage disclaimer
This book was written with use of LLMs, but it has plenty a human touch. I’ve spend hours on this text to make sure that it’s something I am comfortable reading myself, so, I hope, it will be a good read for you too.
Regardless, I am very open for feedback, so if some parts (or even the book as a whole) do not feel good, please open an issue and I’ll work on it.
Eventually I do plan to rewrite the documentation myself from scratch, but it’s a very big amount of work and there are only so many hours in a day.
About this book
This book tries to teach you Ajika through a prism of a very naive query in a text editor. The request is very stupid: how many words are there in the document.
In real world, the queries are typically much heavier, when your task is implementing an LSP server, a compiler, or a build system. The editor we’re discussing is, obviously, not a project where Ajika shines (because with such simple queries the persistence overhead will dominate, and the memory usage is low anyways), but a project that is useful for understanding basic concepts.
Regardless, let’s think how we would implement such a request.
From functions to queries
In ordinary Rust, we could write:
struct File {
text: String,
}
fn words(file: &File) -> Vec<String> {
file.text.split_whitespace().map(str::to_owned).collect()
}
fn word_count(file: &File) -> usize {
words(file).len()
}
Each call to word_count runs words again, even if the text hasn’t changed.
With Ajika, we can describe the text as an input and these functions as queries:
extern crate ajika;
extern crate anyhow;
use std::{sync::Arc, time::Duration};
use ajika::{Database, Db, Inputs, Retention};
#[ajika::input(key = String)]
struct File {
#[input(arc)]
text: String,
}
#[ajika::query]
fn words(db: &Db<'_>, path: String) -> ajika::Result<Vec<String>> {
let text = File::at(path).text(db)?;
Ok(text.split_whitespace().map(str::to_owned).collect())
}
#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
Ok(words(db, path)?.len())
}
// This convenience query reuses the words query's allocation.
#[ajika::query(forward)]
fn note_words(db: &Db<'_>) -> ajika::Result<Arc<Vec<String>>> {
words(db, "notes.txt".into())
}
fn main() -> anyhow::Result<()> {
let limits = Retention {
max_entries: 10_000,
max_bytes: 256 * 1024 * 1024,
max_idle: Duration::from_secs(24 * 60 * 60),
};
let mut database = Database::memory(Inputs::default(), (), limits);
let file = File::at("notes.txt".into());
database.update_inputs(|update| file.set_text(update, "hello world".to_owned()))?;
let count = database.run(|db| word_count(db, "notes.txt".into()))?;
assert_eq!(*count, 2);
assert_eq!(database.statistics().executed, 2);
database.run(|db| word_count(db, "notes.txt".into()))?;
assert_eq!(database.statistics().executed, 0);
database.update_inputs(|update| file.set_text(update, "hello world".to_owned()))?;
database.run(|db| word_count(db, "notes.txt".into()))?;
assert_eq!(database.statistics().executed, 1); // Only words ran.
database.update_inputs(|update| file.set_text(update, "hello new world".to_owned()))?;
let count = database.run(|db| word_count(db, "notes.txt".into()))?;
assert_eq!(*count, 3);
assert_eq!(database.statistics().executed, 2);
database.run(|db| {
let words = words(db, "notes.txt".into())?;
assert!(Arc::ptr_eq(&words, ¬e_words(db)?));
Ok(())
})?;
Ok(())
}
The function bodies are still ordinary Rust. The extra declarations let Ajika remember which inputs and other query answers each function used.
Suppose we change hello world to hello world, widening the gap to four
spaces. Ajika reruns words because the text changed, but its answer is still
["hello", "world"]. Since word_count depends on that vector, Ajika can reuse
the count without running its function again. Counting words is cheap, but the
same boundary could protect an expensive analysis from unnecessary work.
How to read this book
The tutorial turns the queries above into a runnable program. We’ll supply text, edit it, reuse results after reopening, and separate words from their source locations. Later chapters cover interning, loading inputs on demand, unsaved edits, parallel work, and memory and storage budgets.
The conceptual reference explains the choices behind the public interface: what belongs in an input or a query key, what callers depend on, how to organize a request, and what to keep between requests. Read it when designing your own integration, or return to individual chapters as those questions arise. It does not require following the tutorial.
The internal walkthrough follows query calls through the implementation. It introduces the objects after explaining the problem they solve, then traces validation, publication, and storage.
Where the boundary lies
Ajika manages derived work. Your application still decides what counts as an input, when those inputs form a coherent view, and when a result is fit to publish to a user. Source files, configuration, and editor buffers remain application concerns.
Ajika’s disk store is a disposable cache. It is not the authoritative copy of those inputs, a general database for user data, or a way to query arbitrary historical revisions. A corrupted or incompatible cache can be discarded and rebuilt.
The project is under active development. APIs and cache formats can change.
About this book
This book is obviously inspired by the Salsa book.