Content addressable datastore. Allows you to configure a node to store and stream large blobs of data, and retrieve them from any swactor-connected node. Co-authored-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com> Co-committed-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
13 KiB
Datastore Actor Reference
Overview
The datastore is built from four actors within the swactor runtime. DatastoreNode is the public facade — all external requests (HTTP API, network protocol) enter through it and are routed to two long-lived worker actors: BlobStoreActor (content-addressed chunk/manifest I/O) and MetadataActor (object index, DHT replication, GC). A fourth actor, TransferActor, is spawned ephemerally for each remote download and self-terminates on completion or failure.
┌─────────────────────────┐
│ store_node (main) │
│ spawns all 3 long-lived │
│ actors, drives ticks │
└────┬──────┬──────┬───────┘
│ │ │
spawn │ │ │ spawn
┌──────────────┘ │ └──────────────┐
▼ │ spawn ▼
┌───────────────────┐ │ ┌───────────────────┐
│ BlobStoreActor │ │ │ MetadataActor │
│ (chunks, manifests)│ │ │ (index, DHT, GC) │
└─────────▲─────────┘ │ └──▲────────┬───────┘
│ ▼ │ │
│ ┌───────────────────┐ │ │
│ │ DatastoreNode │────┘ │
│ │ (facade/router) │─────────────┘
└────────────│ │
└────────┬──────────┘
│ spawns (per download)
▼
┌───────────────────┐
│ TransferActor │
│ (ephemeral) │
└───────────────────┘
Arrows: ──▶ sends messages to
Actors
DatastoreNode
| Role | Top-level coordinator/facade. Accepts user-facing commands and incoming network protocol messages, delegates all work to BlobStoreActor and MetadataActor. |
| Source | crates/datastore/src/actors/datastore_node.rs |
| Spawned by | store_node binary (crates/datastore/src/bin/store_node.rs:188) |
| Lifecycle | Long-lived — runs for the lifetime of the process |
Inbound messages (DatastoreNodeMsg — 11 variants):
User-facing commands:
Put { data, name, tags, reply_to }— chunk a blob, write chunks + manifest toBlobStoreActor, register inMetadataActorGet { content_hash, reply_to }— retrieve object metadata + manifest viaMetadataActorDelete { content_hash, reply_to }— remove object viaMetadataActorList { name_filter, all, reply_to }— list objects (local or swarm-wide) viaMetadataActorStatus { reply_to }— return this node'sNodeIdReadChunk { hash, reply_to }— read a single chunk viaBlobStoreActor
Protocol routing (incoming network messages):
IncomingGetChunk— forwards toBlobStoreActor::ReadChunkIncomingGetManifest— forwards toBlobStoreActor::ReadManifestIncomingStoreObject— forwards toMetadataActor::HandleStoreObjectIncomingFindObject— forwards toMetadataActor::HandleFindObjectIncomingListObjects— forwards toMetadataActor::ListLocal
Key outbound messages:
BlobStoreMsg::WriteChunk,WriteManifest,ReadChunk,ReadManifest— toBlobStoreActorMetadataMsg::PutObject,GetObject,DeleteObject,ListLocal,ListSwarm,HandleStoreObject,HandleFindObject— toMetadataActorDatastoreResponse::NodeStatus— directly to caller forStatus
BlobStoreActor
| Role | Content-addressed storage for chunks and manifests. All I/O goes through a pluggable StorageBackend (filesystem or in-memory). |
| Source | crates/datastore/src/actors/blob_store.rs |
| Spawned by | store_node binary (store_node.rs:178) |
| Lifecycle | Long-lived — runs for the lifetime of the process |
Inbound messages (BlobStoreMsg — 8 variants):
Chunk operations:
WriteChunk { hash, data, reply_to }— persist a chunk, replyChunkStoredReadChunk { hash, reply_to }— read a chunk, replyChunkOkorNotFoundDeleteChunk { hash }— remove a chunk (fire-and-forget)HasChunk { hash, reply_to }— existence check, replyBoolListChunks { reply_to }— list all chunk hashes, replyChunkListGcUnreferenced { referenced }— delete chunks not in the referenced set (fire-and-forget)
Manifest operations:
WriteManifest { manifest, reply_to }— persist a manifest, replyManifestStoredReadManifest { hash, reply_to }— read a manifest, replyManifestOkorNotFound
Key outbound messages:
DatastoreResponsevariants (ChunkStored,ChunkOk,ManifestStored,ManifestOk,NotFound,Error,Bool,ChunkList) — always back to thereply_toaddress
MetadataActor
| Role | Object metadata index. Maintains a HashMap<ContentHash, ObjectEntry> and a manifest cache. Handles DHT-style find/store operations, epidemic dissemination of entries to peers, and periodic garbage collection. |
| Source | crates/datastore/src/actors/metadata.rs |
| Spawned by | store_node binary (store_node.rs:185) |
| Lifecycle | Long-lived — runs for the lifetime of the process |
Inbound messages (MetadataMsg — 11 variants):
Object operations:
PutObject { entry, manifest, reply_to }— store metadata + manifest locally, enqueue for dissemination, replyPutOkGetObject { content_hash, reply_to }— local lookup, replyGetOkorNotFoundDeleteObject { content_hash, reply_to }— remove from local index, replyDeleteOkorNotFoundListLocal { name_filter, reply_to }— list local entries with optional name filter, replyListOkListSwarm { name_filter, reply_to }— swarm-wide list (currently delegates toListLocal)
DHT protocol:
HandleFindObject { from, content_hash, reply_to }— answer an incoming FIND_VALUE from a peerHandleStoreObject { entry, manifest }— accept an incoming STORE from a peer (fire-and-forget)
Peer management:
SetPeers { peers }— update the list of peerMetadataActoraddresses for dissemination
Periodic ticks (driven by the store_node main loop):
DisseminateTick— send pending entries to all known peersGcTick— collect referenced chunks from all manifests, sendBlobStoreMsg::GcUnreferencedtoBlobStoreActor
Key outbound messages:
DatastoreResponsevariants (PutOk,GetOk,DeleteOk,ListOk,NotFound,Error) — to callerMetadataMsg::HandleStoreObject— to peerMetadataActoraddresses during disseminationBlobStoreMsg::GcUnreferenced— to localBlobStoreActorduring GC
TransferActor
| Role | Manages a single object download from a remote node. Tracks pending/received chunks, forwards received data to the local BlobStoreActor, and reports completion or failure to the original requester. |
| Source | crates/datastore/src/actors/transfer.rs |
| Spawned by | DatastoreNode (one per remote download) |
| Lifecycle | Ephemeral — self-terminates via ctx.stop_self() on completion, failure, or cancel |
Inbound messages (TransferMsg — 4 variants):
StartDownload { manifest, source_node, reply_to }— initialize the download with a manifest and sourceChunkReceived { hash, data }— a chunk arrived from the remote nodeChunkFailed { hash, reason }— a chunk fetch failed (retries up tomax_retries, then fails the whole transfer)Cancel— abort the transfer immediately
Key outbound messages:
BlobStoreMsg::WriteChunk— to localBlobStoreActorfor each received chunkDatastoreResponse::TransferComplete— toreply_towhen all chunks receivedDatastoreResponse::TransferFailed— toreply_towhen retries are exhausted
Message Reference
All message types are defined in crates/datastore/src/messages.rs.
Intra-node actor messages
| Enum | Variants | Handled by |
|---|---|---|
DatastoreNodeMsg |
11 (6 user-facing + 5 protocol routing) | DatastoreNode |
BlobStoreMsg |
8 (5 chunk ops + 1 GC + 2 manifest ops) | BlobStoreActor |
MetadataMsg |
11 (5 object ops + 2 DHT + 1 peer mgmt + 2 ticks) | MetadataActor |
TransferMsg |
4 (start + chunk received + chunk failed + cancel) | TransferActor |
Shared response enum
DatastoreResponse — 15 variants used as the return type for all four actors:
| Variant | Meaning |
|---|---|
PutOk { content_hash } |
Object stored successfully |
GetOk { entry, manifest } |
Object found |
DeleteOk { content_hash } |
Object deleted |
ListOk { entries } |
List result |
ChunkOk { hash, data } |
Chunk data retrieved |
ChunkStored { hash } |
Chunk written to storage |
ManifestStored { hash } |
Manifest written to storage |
ManifestOk { manifest } |
Manifest retrieved |
TransferComplete { content_hash } |
All chunks downloaded |
TransferFailed { reason } |
Transfer failed |
NodeStatus { node_id } |
Node identity |
NotFound |
Resource not found |
Error { reason } |
Generic error |
Bool(bool) |
Boolean result (e.g. HasChunk) |
ChunkList { hashes } |
List of chunk hashes |
Inter-node wire messages (NetworkMessage)
| Struct | Direction | Purpose |
|---|---|---|
GetChunkRequest |
requester → holder | Fetch a chunk by hash |
GetChunkResponse |
holder → requester | Return chunk data (or None) |
StoreObjectRequest |
origin → DHT peer | Kademlia STORE for object metadata |
FindObjectRequest |
requester → DHT peer | Kademlia FIND_VALUE for object metadata |
FindObjectResponse |
DHT peer → requester | Return Found(entry) or Closer(nodes) |
GetManifestRequest |
requester → holder | Fetch a manifest by content hash |
GetManifestResponse |
holder → requester | Return manifest (or None) |
ListObjectsRequest |
requester → peer | List objects with optional name filter |
ListObjectsResponse |
peer → requester | Return matching entries |
Wire messages are distinguished from intra-node messages by implementing the NetworkMessage trait with a stable type_tag() string. They are serialized with serde for transport over iroh/QUIC.
Key Flows
Put (store a blob)
Client → DatastoreNode::Put
→ chunk_blob() splits data into chunks
→ BlobStoreActor::WriteChunk (for each chunk, fire-and-forget)
→ BlobStoreActor::WriteManifest
→ MetadataActor::PutObject
→ stores entry + manifest locally
→ enqueues for dissemination
→ replies DatastoreResponse::PutOk
Get (retrieve metadata)
Client → DatastoreNode::Get
→ MetadataActor::GetObject
→ local index lookup
→ replies DatastoreResponse::GetOk (or NotFound)
Data (reassemble from chunks)
See streaming.md for the full transfer protocol. In summary:
API server → DatastoreNode::Get → MetadataActor (local miss)
→ iterate peers:
→ FindObjectRequest (wire) → peer MetadataActor
→ GetManifestRequest (wire) → peer BlobStoreActor
→ GetChunkRequest (wire) → peer BlobStoreActor (per chunk)
→ BlobStoreActor::WriteChunk (store locally)
→ BlobStoreActor::WriteManifest
→ MetadataActor::PutObject
→ reassemble_blob() → verify blake3 → respond
Dissemination (epidemic replication)
store_node main loop (every disseminate_interval ticks)
→ MetadataActor::DisseminateTick
→ take_pending() selects entries with remaining budget
→ for each peer: MetadataActor::HandleStoreObject
→ peer inserts if absent, re-enqueues for further dissemination
Budget per entry = Λ * ceil(log2(cluster_size)) (SWIM-style, Λ=3).
GC (garbage collection)
store_node main loop (every gc_interval ticks)
→ MetadataActor::GcTick
→ scans all manifests → builds referenced chunk set
→ BlobStoreActor::GcUnreferenced { referenced }
→ deletes any chunk not in the referenced set
Related
- streaming.md — chunking, transfer protocol, reassembly, and progress tracking