Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Develop tier — the ndn umbrella

The Develop tier is what an application reaches for when it wants to fetch a Data by name or serve one and treat the forwarder as opaque. Everything below is a re-export of ndn-app, ndn-packet, and ndn-security from the ndn-rs-prelude crate (library name ndn).

Start with Node. For most apps the one type to learn is Node — a single handle that exposes every pattern (fetch / serve / object / publish / subscribe / query) over one forwarder connection. The per-pattern types below (Consumer, Producer, …) are the building blocks Node delegates to, reachable via node.connection() when you need them.

Package vs library: Cargo.toml carries ndn-rs-prelude = "0.1"; imports read use ndn::Consumer;. The split is recorded in crates/ndn-rs-prelude/Cargo.toml.

Inventory

graph LR
    A[Application] -->|imports| U(ndn umbrella)
    U --> P[Consumer / Producer / Responder]
    U --> S[Subscriber / Queryable / Query]
    U --> K[KeyChain / SigningInfo / TrustPolicy]
    U --> T[Connection / IpcConnection / InProcConnection]
    U --> X[Name / Interest / Data / *Builder]
Re-exportSourceWhat it does
Consumerndn_app::Consumer (crates/ndn-app/src/consumer.rs)Express Interests; fetch a single Data or a segmented object.
Producerndn_app::Producer (crates/ndn-app/src/producer.rs)Register a prefix; serve Data on demand.
Responderndn_app::Responder (crates/ndn-app/src/responder.rs)Callback-style producer (one closure → one Data).
Subscriber, SubscriberConfig, Samplendn_app::subscriberSVS-style multi-publisher stream subscription.
Queryable, Queryndn_app::queryableRequest/reply primitive (one Interest → one Data).
KeyChainndn_security::KeyChainIdentity / key / cert management; entry point for signing.
SigningInfo, SignerSelectionndn_security::{SigningInfo, SignerSelection}“Sign me with X” descriptor.
ValidationPolicy, TrustPolicyndn_security::{ValidationPolicy, TrustPolicy}Trust-decision contracts.
Connection, IpcConnection, InProcConnectionndn_app::*Trait + concrete connections; unifies external forwarder and embedded engine.
Name, NameComponent, Interest, Data, NackReasonndn_packet::*Decoded packets.
InterestBuilder, DataBuilderndn_packet::encode::*Builder-style packet construction.
AppErrorndn_app::error::AppErrorSingle error type at the Develop tier boundary.

Consumer

use ndn::prelude::*;
use ndn::Consumer;

async fn run() -> Result<(), ndn::AppError> {
let mut consumer = Consumer::connect("/tmp/ndn-fwd.sock").await?;
let data = consumer.fetch("/example/hello").await?;
println!("got {} bytes", data.content().len());
Ok(()) }
  • fetch(name) expresses one Interest, returns one Data.
  • fetch_object(name) performs RDR discovery (<name>/32=metadata) and reassembles segmented Data. Segments are fetched pipelined (a sliding window of in-flight Interests, retransmitting on a stall), so throughput is window / RTT × chunk rather than one round-trip per segment.
  • object(name) returns a fluent builder — object(name).verify(v).hint([..]) .progress(cb).to_file(&file) streams each verified segment to a file at its byte offset as it arrives, so an arbitrarily large object is received with flat memory; .fetch() reassembles in memory, .stream(..) hands each segment to a callback. See The Node cookbook. (The old fetch_object_* methods are deprecated in favour of this builder.)
  • fetch_on(face_id, name) pins the Interest to a face via NextHopFaceId — useful for measurement or multipath tests.
  • The Consumer applies the configured ValidationPolicy to every returned Data before handing it back.

Producer

use ndn::prelude::*;

async fn run() -> Result<(), Box<dyn std::error::Error>> {
let keychain = KeyChain::ephemeral("/example")?;
// connect registers the prefix; with_signer makes published Data signed.
let producer = Producer::connect("/tmp/ndn-fwd.sock", "/example")
    .await?
    .with_signer(keychain.signer()?);
producer
    .publish_object("/example/hello".parse()?, b"hi".to_vec().into(), 0)
    .await?;
Ok(()) }
  • publish_object(name, content, chunk_size) segments and serves the object — signing each segment with the configured signer, else DigestSha256. chunk_size == 0 uses the default.
  • publish_object_from_file(name, file, size, chunk_size) serves a file-backed object: segments are read from the file on demand (positioned reads), so an arbitrarily large file is published without ever loading it into memory.
  • connect(socket, prefix) registers the prefix; re-publishing the same name replaces the content (FreshnessPeriod governs the cache).

Responder

A Responder is the reply handle passed to Producer::serve — a closure-style producer for when each Interest needs a dynamic reply. With a signer configured, respond signs the reply.

use ndn::prelude::*;

async fn run() -> Result<(), Box<dyn std::error::Error>> {
let keychain = KeyChain::ephemeral("/example")?;
let producer = Producer::connect("/tmp/ndn-fwd.sock", "/example/time")
    .await?
    .with_signer(keychain.signer()?);
producer.serve(|interest, responder| async move {
    let now = format!("{:?}", std::time::SystemTime::now());
    // `respond` signs with the configured signer.
    responder.respond((*interest.name).clone(), now.into_bytes()).await.ok();
}).await?;
Ok(()) }

Subscriber

A Subscriber joins a multi-publisher stream (SVS pub/sub shape). Each peer publishes under its own name; Subscriber reassembles a total order and yields Sample items.

use ndn::prelude::*;
use ndn::Subscriber;

async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut sub = Subscriber::connect("/tmp/ndn-fwd.sock", "/svs/chatroom").await?;
while let Some(sample) = sub.recv().await {
    println!("{}: {:?}", sample.publisher, sample.payload);
}
Ok(()) }

Note: in v0.1.0 the Subscriber is read-only. Publishing into a sync group from Develop-tier code is filed for v0.1.x.

Connection

Connection is the trait the Develop types accept; two concrete implementations cover the typical deployments.

TypeWhere the engine livesTypical use
IpcConnectionExternal ndn-fwd over Unix socketProduction apps on Linux/macOS.
InProcConnectionEmbedded ForwarderEngine in the same processTests, mobile, browser.

Embedded engine

use ndn::prelude::*;
use ndn::{Consumer, InProcConnection};
use ndn_engine::{EngineBuilder, EngineConfig};
use ndn_face::local::InProcFace;
use ndn_transport::FaceId;

async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (face, handle) = InProcFace::new(FaceId(1), 64);
let (_engine, _shutdown) = EngineBuilder::new(EngineConfig::default())
    .face(face)
    .build()
    .await?;
let mut consumer = Consumer::new(InProcConnection::from_handle(handle));
let _ = consumer.fetch("/example/hello").await?;
Ok(()) }

EngineBuilder is in crates/ndn-engine/. The umbrella does not re-export it: the Develop tier treats the engine as opaque, and embedding it is an Extend-tier or test-time concern.

KeyChain

use ndn::prelude::*;

fn run() -> Result<(), Box<dyn std::error::Error>> {
// A KeyChain is one identity; `ephemeral` / `open_or_create` generate its key
// and self-signed cert.
let keychain = KeyChain::ephemeral("/alice")?;
// Sign a Data with that key → ready-to-send wire bytes.
let wire = keychain.sign_data(DataBuilder::new("/alice/notes/1", b"hi"))?;
Ok(()) }

Persistence backends: SQLite-backed PIB on native targets; IndexedDB PIB on wasm32. See Identity and keys.

Wasm target

The umbrella compiles for wasm32-unknown-unknown but exports a smaller surface: Name, Interest, Data, InterestBuilder, DataBuilder, SigningInfo, TrustPolicy. Consumer, Producer, KeyChain, and the connection types stay native-only because ndn-app pulls the full Tokio runtime.

Browser callers build the engine in-page with ndn_engine::WasmEngineBuilder and drive the Producer shape from ndn-engine directly. The split is intentional and documented in the prelude crate’s top-level docs.

Cross-platform parity

The endpoint API keeps the same names and arguments on every target, so application logic ports unchanged. The execution model is the only divergence — and it is forced by the platform, not chosen:

Axisnative / browserembedded (ndn-embedded)
Suspensionasync fn … .awaitnb::Result poll (WouldBlock)
Resultowned Data / Bytesborrowed &[u8] into an MTU buffer
Sizingheapconst-generic buffers, no alloc
OwnershipArc<Engine>, &self&mut Forwarder<…>

The packet, name, and signing primitives (Name, Interest, Data, Ed25519 sign/verify) are the same crates on all three targets, so only the seam above changes. ndn-embedded exposes Consumer::fetch(name) and Producer::serve(handler) — the same verbs as this tier, returning nb::Result rather than a future:

use ndn_embedded::{Consumer, Producer};

// native:   let data = consumer.fetch("/ndn/sensor/temp").await?;
// embedded:  poll the same call (here driven to completion with nb::block!)
let mut c: Consumer<_> = Consumer::new(&mut face, seed);
let data: &[u8] = nb::block!(c.fetch("/ndn/sensor/temp"))?;

// native:   producer.serve(|interest, responder| async move { … }).await?;
// embedded:  one Interest per poll, from your event loop
let mut p: Producer<_> = Producer::new(&mut face, "/app");
let _ = p.serve(|_name, out| {
    out[..2].copy_from_slice(b"on");
    Some(2) // content length, or None to decline
});

What this tier does not expose

The Develop tier deliberately omits:

  • Direct ForwarderEngine access (PIT/FIB/CS tables) — that’s the Instrument tier.
  • Strategy, RoutingProtocol, Face, LinkService traits — that’s the Extend tier.
  • Per-crate error enums (ConfigError, TrustError, etc.) — they collapse into AppError at this boundary.

See also