nmp-threading 1.0.0-rc.1

Reply-convention-agnostic timeline grouping algorithm. Owns ThreadPointer / ParentResolver / ModulePolicy / TimelineBlock / Grouper, consumed by per-NIP wrapper view modules (NIP-10 in nmp-nip01). No app nouns, no kind semantics.
Documentation
use flatbuffers::{FlatBufferBuilder, WIPOffset};

use crate::{ThreadEdge, ThreadPointer, ThreadingSnapshot, TimelineBlock};

use super::fb;
use super::THREADING_GRAPH_SCHEMA_VERSION;

#[must_use]
pub fn encode_threading_snapshot(snapshot: &ThreadingSnapshot) -> Vec<u8> {
    let mut builder = FlatBufferBuilder::new();

    let edges: Vec<WIPOffset<fb::ThreadEdge<'_>>> = snapshot
        .edges
        .iter()
        .map(|edge| encode_edge(&mut builder, edge))
        .collect();
    let edges = builder.create_vector(&edges);

    let blocks: Vec<WIPOffset<fb::TimelineBlockEntry<'_>>> = snapshot
        .blocks
        .iter()
        .map(|block| encode_block(&mut builder, block))
        .collect();
    let blocks = builder.create_vector(&blocks);

    let pending: Vec<WIPOffset<fb::BlockEventId<'_>>> = snapshot
        .pending_ancestor_ids
        .iter()
        .map(|id| encode_id(&mut builder, id))
        .collect();
    let pending = builder.create_vector(&pending);

    let root = fb::ThreadingSnapshot::create(
        &mut builder,
        &fb::ThreadingSnapshotArgs {
            schema_version: THREADING_GRAPH_SCHEMA_VERSION,
            edges: Some(edges),
            blocks: Some(blocks),
            pending_ancestor_ids: Some(pending),
        },
    );
    fb::finish_threading_snapshot_buffer(&mut builder, root);
    builder.finished_data().to_vec()
}

fn encode_edge<'bldr>(
    builder: &mut FlatBufferBuilder<'bldr>,
    edge: &ThreadEdge,
) -> WIPOffset<fb::ThreadEdge<'bldr>> {
    let event_id = builder.create_string(&edge.event_id);
    let author_pubkey = builder.create_string(&edge.author_pubkey);
    let parent = edge.parent.as_ref().map(|p| encode_pointer(builder, p));
    let root = edge.root.as_ref().map(|p| encode_pointer(builder, p));
    let parent_author_pubkey = edge
        .parent_author_pubkey
        .as_ref()
        .map(|p| builder.create_string(p));
    fb::ThreadEdge::create(
        builder,
        &fb::ThreadEdgeArgs {
            event_id: Some(event_id),
            author_pubkey: Some(author_pubkey),
            kind: edge.kind,
            created_at: edge.created_at,
            parent,
            root,
            parent_author_pubkey,
        },
    )
}

fn encode_block<'bldr>(
    builder: &mut FlatBufferBuilder<'bldr>,
    block: &TimelineBlock,
) -> WIPOffset<fb::TimelineBlockEntry<'bldr>> {
    match block {
        TimelineBlock::Standalone { id, root } => {
            let standalone_id = builder.create_string(id);
            let standalone_root = root.as_ref().map(|p| encode_pointer(builder, p));
            fb::TimelineBlockEntry::create(
                builder,
                &fb::TimelineBlockEntryArgs {
                    kind: fb::TimelineBlockKind::Standalone,
                    standalone_id: Some(standalone_id),
                    standalone_root,
                    module_event_ids: None,
                    module_has_gap: false,
                    module_root: None,
                },
            )
        }
        TimelineBlock::Module {
            events,
            has_gap,
            root,
        } => {
            let ids: Vec<WIPOffset<fb::BlockEventId<'_>>> =
                events.iter().map(|id| encode_id(builder, id)).collect();
            let module_event_ids = builder.create_vector(&ids);
            let module_root = root.as_ref().map(|p| encode_pointer(builder, p));
            fb::TimelineBlockEntry::create(
                builder,
                &fb::TimelineBlockEntryArgs {
                    kind: fb::TimelineBlockKind::Module,
                    standalone_id: None,
                    standalone_root: None,
                    module_event_ids: Some(module_event_ids),
                    module_has_gap: *has_gap,
                    module_root,
                },
            )
        }
    }
}

fn encode_pointer<'bldr>(
    builder: &mut FlatBufferBuilder<'bldr>,
    pointer: &ThreadPointer,
) -> WIPOffset<fb::ThreadPointer<'bldr>> {
    match pointer {
        ThreadPointer::Event { id, relay, kind } => {
            let id = builder.create_string(id);
            let relay = relay.as_ref().map(|r| builder.create_string(r));
            fb::ThreadPointer::create(
                builder,
                &fb::ThreadPointerArgs {
                    kind: fb::ThreadPointerKind::Event,
                    id: Some(id),
                    coord: None,
                    uri: None,
                    relay,
                    has_kind_num: kind.is_some(),
                    kind_num: kind.unwrap_or_default(),
                },
            )
        }
        ThreadPointer::Address { coord, relay, kind } => {
            let coord = builder.create_string(coord);
            let relay = relay.as_ref().map(|r| builder.create_string(r));
            fb::ThreadPointer::create(
                builder,
                &fb::ThreadPointerArgs {
                    kind: fb::ThreadPointerKind::Address,
                    id: None,
                    coord: Some(coord),
                    uri: None,
                    relay,
                    has_kind_num: kind.is_some(),
                    kind_num: kind.unwrap_or_default(),
                },
            )
        }
        ThreadPointer::External { uri } => {
            let uri = builder.create_string(uri);
            fb::ThreadPointer::create(
                builder,
                &fb::ThreadPointerArgs {
                    kind: fb::ThreadPointerKind::External,
                    id: None,
                    coord: None,
                    uri: Some(uri),
                    relay: None,
                    has_kind_num: false,
                    kind_num: 0,
                },
            )
        }
    }
}

fn encode_id<'bldr>(
    builder: &mut FlatBufferBuilder<'bldr>,
    id: &str,
) -> WIPOffset<fb::BlockEventId<'bldr>> {
    let id = builder.create_string(id);
    fb::BlockEventId::create(builder, &fb::BlockEventIdArgs { id: Some(id) })
}