Ankurah
(The root of all cosmic projections of state)
A distributed state-management framework that enables real-time data synchronization across multiple nodes with built-in observability.
Beta
It works! But exercise caution in production
Key Features
Schema-First Design
#[derive(Model, Debug, Serialize, Deserialize)]
pub struct Album {
#[active_type(YrsString)]
pub name: String,
pub artist: String,
pub year: i32,
}
(YrsString is default backend for String, LWW otherwise)
Live Queries
Rust
// Using selection! macro with ctx.query()
let q: LiveQuery<AlbumView> = ctx.query(selection!("year > 1985"))?;
TypeScript
let albumsQuery: AlbumLiveQuery | undefined;
function queryAlbums(): AlbumLiveQuery {
albumsQuery ??= Album.query(ctx(), "year > 1985");
return albumsQuery;
}
The Model macro generates Rust views and browser-facing query types.
React Support
interface Props {
albums: AlbumLiveQuery;
}
/* Bind a React observer to the component. */
const AlbumList = signalObserver(({ albums }: Props) => {
return (
<ul>
{/* Reading items registers this render as a live-query observer. */}
{albums.items.map((album) => (
<li key={album.id.to_base64()}>{album.name}</li>
))}
</ul>
);
});
Reading items inside signalObserver registers the component.
Flexible Storage
Postgres
let storage = Postgres::open(uri).await?;
SQLite
let storage = SqliteStorageEngine::open("ankurah.sqlite").await?;
Sled
let storage = SledStorageEngine::new()?;
IndexedDB
let storage = IndexedDBStorageEngine::open("myapp").await?;
Generated Interfaces
export class Album {
static query(
context: Context,
selection: string,
...substitution_values: any
): AlbumLiveQuery;
static create(
transaction: Transaction,
album: Album
): Promise<AlbumView>;
}
Batteries Included
Common building blocks, ready to compose.
Storage engines
- Postgres
- SQLite
- Sled
- IndexedDB
Connectors & policy
- WebSocket server
- Native WebSocket client
- Browser/WASM WebSocket client
- Iroh (coming soon; #341)
- JWT
Quick Example
Server
let storage = SledStorageEngine::with_path(storage_dir)?;
let node = Node::new_durable(Arc::new(storage), PermissiveAgent::new());
node.system.wait_loaded().await;
if node.system.root().is_none() {
node.system.create().await?;
}
let mut server = WebsocketServer::new(node);
println!("Running server...");
server.run("127.0.0.1:9797").await?;
Rust Client
let storage = SledStorageEngine::new_test()?;
let node = Node::new(Arc::new(storage), PermissiveAgent::new());
let _client = WebsocketClient::new(node.clone(), "ws://localhost:9797").await?;
node.system.wait_system_ready().await;
// Create album
let ctx = node.context(ankurah::policy::DEFAULT_CONTEXT)?;
let trx = ctx.begin();
trx.create(&Album { name: "Parade".into(), artist: "Prince".into(), year: 1986 }).await?;
trx.commit().await?;
Wasm Client
let storage = IndexedDBStorageEngine::open("myapp").await?;
let node = Node::new(Arc::new(storage), PermissiveAgent::new());
let client = WebsocketClient::new(node.clone(), server_url)?;
node.system.wait_system_ready().await;
let context = node.context(DEFAULT_CONTEXT)?;
CONTEXT.with(|slot| slot.replace(Some(context)));
CLIENT.with(|slot| slot.replace(Some(client)));
React Component
interface Props {
albums: AlbumLiveQuery;
}
/* Bind a React observer to the component. */
const AlbumList = signalObserver(({ albums }: Props) => {
return (
<ul>
{/* Reading items registers this render as a live-query observer. */}
{albums.items.map((album) => (
<li key={album.id.to_base64()}>{album.name}</li>
))}
</ul>
);
});
More Examples →
Ready to Start?
Get up and running quickly with our React + Sled template
cargo generate https://github.com/ankurah/react-template