Technology
Normalised Cache
We use a normalised cache for our frontend to ensure the UI will always contain consistent data.
What this system does?
By normalising the data it's impossible to get "state tearing".
Each time useNodes
or cache.withNodes
is called all useCache
hooks will reexecute if they depend on a node that has changed.
This means the queries will always render the newest version of the model.
Terminology
CacheNode
: A node in the cache - this contains the data and can be identified by the model's name and unique ID within the data (eg. database primary key).Reference<T>
: A reference to a node in the cache - This contains the model's name and unique ID.
High level overview
We turn the data on the backend into a list of CacheNode
's and a list of Reference<T>
's and then return it to the frontend.
We insert the CacheNode
's into a global cache on the frontend and then use the Reference<T>
's to reconstruct the data by looking up the CacheNode
's.
When the cache changes (from another query, invalidation, etc), we can reconstruct all queries using their Reference<T>
's to reflect the updated data.
Rust usage
The Rust helpers are defined here and can be used like the following:
pub struct Demo {
id: String,
}
impl sd_cache::Model for Demo {
// The name + the ID *must* refer to a unique node.
// If your using an enum, the variant should show up in the ID (although this isn't possible right now)
fn name() -> &'static str {
"Demo"
}
}
let data: Vec<Demo> = vec![];
// We normalised the data but splitting it into a group of reference and a group of `CacheNode`'s.
let (nodes, items) = libraries.normalise(|i| i.id);
// `NormalisedResults` or `NormalisedResult` are optional wrapper types to hold a one or multiple items and their cache nodes.
// You don't have to use them, but they save declaring a bunch of identical structs.
//
// Alternatively add `nodes: Vec<CacheNode>` and `items: Vec<Reference<T>>` to your existing return type.
//
return sd_cache::NormalisedResults { nodes, items };
Typescript usage
The Typescript helpers are defined here.
Usage with React
We have helpers designed for easy usage within React's lifecycle.
const query = useLibraryQuery([...]);
// This will inject all the models into the cache
useNodes(query.data?.nodes);
// This will reconstruct the data from the cache
const data = useCache(query.data?.item);
console.log(data);
Vanilla JS
These API's are really useful for special cases. In general aim to use the React API's unless you have a good reason for these.
const cache = useNormalisedCache(); // Get the cache within the react context
// Pass `cache` outside React (Eg. `useEffect`, `onSuccess`, etc)
const data = ...;
// This will inject all the models into the cache
cache.withNodes(data.nodes)
// This will reconstruct the data from the cache
//
// *WARNING* This is not reactive. So any changes to the nodes will not be reflected.
// Using this is fine if you need to quickly check the data but don't hold onto it.
const data = useCache(query.data?.item);
console.log(data);
Design decisions
useNodes
and useCache
?
Why This was done to make the system super flexible with what data you can return from your backend.
For example the backend doesn't just have to return NormalisedResults
or NormalisedResult
, it could return:
pub struct AllTheData {
file_paths: Vec<Reference<FilePath>>,
locations: Vec<Reference<Location>>,
nodes: Vec<CacheNode>
}
and then on the frontend you could do the following:
const query = useQuery([...]);
useNodes(query.data?.nodes);
const locations = useCache(query.data?.locations);
const filePaths = useCache(query.data?.file_paths);
This is only possible because useNodes
and useCache
take in a specific key, instead of the whole data
object, so you can tell it where to look.
Known issues
Specta support
Expressing Reference<T>
in Specta is really hard so we surgically update it's type definition.
This is done using rspc::Router::sd_patch_types_dangerously
which is a method specific to our fork spacedrive/rspc.
Invalidation system integration
The initial implementation of this idea with an MVP. It works with the existing invalidation system like regular queries, but the invalidation system isn't aware of the normalised cache like a better implementation would be.