Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions desktop/src-tauri/src/bench/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ pub(crate) struct Item {
/// next one posts without the p tag. A new Drop starts false.
#[serde(default)]
pub quiet_repost: bool,
/// Its card left the relay (retired for a refresh, deleted by hand) and
/// Its card is gone (retired for a refresh, deleted by hand) and
/// has not posted again: it keeps its `MAX_CARDS` place till it does.
#[serde(default)]
pub repost: bool,
Expand Down Expand Up @@ -1013,8 +1013,8 @@ pub(crate) fn render_card(item: &Item, hhmm: &str) -> String {
out
}

/// A stale card kept for what is on it (publish (b)), from its text as
/// posted: closed, it no longer says how to answer, and says where to now.
/// An old post (`replaced`), from what it shows: closed, it no longer says how
/// to answer, and says where to now.
pub(crate) fn render_closed(posted: &str) -> String {
let open = [
format!(" {TAP}"),
Expand Down
10 changes: 6 additions & 4 deletions desktop/src-tauri/src/bench/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,17 @@ pub(crate) struct BenchState {
/// Each bound channel's join or admin check: cards go only where it passed.
channels: BTreeMap<String, channels::ChannelCheck>,
/// Engaged or owner-told cards a new question left behind, and stale ones
/// closed for what is on them: on the relay for good, never adopted again
/// by recover (which would retire them as stale).
/// retired: on the relay for good, never adopted again by recover (which
/// would retire them as stale). 64 bytes each, a few a day at 4 Nov 2026.
kept: Vec<String>,
/// Each kept card posted in a thread, as (its root, its created_at): it
/// still ends the root-reply window of an older card there.
/// Each kept card in a thread with anything on it, as (its root, its
/// created_at): it still ends the root-reply window of an older card there.
kept_at: Vec<(String, u64)>,
/// Old posts owed an edit or a reply or watched for a tap; since when their read fails.
replaced: Vec<replaced::Replaced>,
replaced_unread: Option<u64>,
/// Receipts still to become old posts (`replaced::backfill`); None: unread.
receipts_todo: Option<Vec<String>>,
/// Live cards whose owner's page was cut this tick, or every item while
/// roles.json does not read: their answers are unread, so nothing
/// expires, restamps, routes, retires or replaces them.
Expand Down
113 changes: 36 additions & 77 deletions desktop/src-tauri/src/bench/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ use std::sync::{Mutex, PoisonError};

use super::channels::{card_channel, channel_ready, joined, reads_replies};
use super::{
item, live_card, local_hhmm, next_ts, outbox, post_as_writer, query, query_writer, same_drop,
save_both, spend, submit, BenchState, Ctx, Outcome, Posted, BOARD_MAX_AGE_SECS, MAX_CARDS,
RECOVER_LIMIT,
item, live_card, local_hhmm, next_ts, outbox, post_as_writer, query, same_drop, save_both,
spend, submit, BenchState, Ctx, Outcome, Posted, BOARD_MAX_AGE_SECS, MAX_CARDS, RECOVER_LIMIT,
};
use crate::events;

Expand Down Expand Up @@ -337,55 +336,29 @@ fn df_answer(parts: [&str; 3]) -> Result<nostr::Tag, String> {
nostr::Tag::parse(["df-answer", n, by, id]).map_err(|e| e.to_string())
}

/// Whether a recovered card has anything on it but the writer's own seeds:
/// a tap, a reply, a route notice, or a Recorded or receipt edit. A cut page
/// holds more than the four seeds, so it counts too.
async fn touched(
/// What a post has on it but the writer's own seeds: a tap, a reply, a route
/// notice, or a Recorded, receipt or closing edit. A cut page holds more than
/// the four seeds, so it counts too.
pub(super) async fn on_post(
ctx: &Ctx<'_>,
s: &BenchState,
ch: &str,
card: &str,
now: u64,
) -> Result<bool, String> {
) -> Result<Vec<nostr::Event>, String> {
let filter = serde_json::json!({
"kinds": [7, 9, 40003], "#h": [ch], "#e": [card], "limit": RECOVER_LIMIT,
});
let writer = ctx.writer.public_key();
Ok(query(ctx, s, filter, now).await?.iter().any(|e| {
let mut on = query(ctx, s, filter, now).await?;
on.retain(|e| {
let on_card = e
.tags
.iter()
.any(|t| matches!(t.as_slice(), [k, v, ..] if k == "e" && v == card));
on_card && !(e.kind.as_u16() == 7 && e.pubkey == writer)
}))
}

/// A stale card kept for what is on it (`touched`), for good: edited closed
/// from its own text as posted (`replaced::old_post`), its thread root (its
/// parent) ending older cards' root-reply windows there, and watched for a tap
/// (`replaced`). One gone from the relay is only kept.
async fn close(
ctx: &Ctx<'_>,
s: &mut BenchState,
id: &str,
ch: &str,
card: &Posted,
now: u64,
) -> Result<(), String> {
let filter = serde_json::json!({ "ids": [card.event_id], "kinds": [9], "#h": [ch] });
if let Some(e) = query_writer(ctx, s, filter, now).await?.first() {
let text = super::replaced::old_post(s, id, &e.content);
let text = text + &item::thread_line(ch, &card.event_id);
match edit(ctx, s, ch, &card.event_id, &text, now).await {
Err(err) if !err.contains("not found") => return Err(err),
_ => {}
}
let root = item::parent(e).map(|r| (r.to_string(), card.created_at));
s.kept_at.extend(root);
super::replaced::closed(s, id, ch, card, item::parent(e), now);
}
s.kept.push(card.event_id.clone());
Ok(())
});
Ok(on)
}

/// A thread the relay will not take: its root is gone, in another channel or
Expand All @@ -398,8 +371,9 @@ fn bad_thread(e: &str) -> bool {

/// Cards, seeds, then the board, at most `budget` posts. A card is posted
/// once per Drop and card format; it is edited to record an answer and, once
/// closed out, to become a receipt. Only a card nobody engaged with is ever
/// deleted. Every post made once a card's own id is known links its thread.
/// stale or closed out, to become an old post (`replaced`), so only a recovered
/// orphan nobody touched is ever deleted. Every post made once a card's own id
/// is known links its thread.
/// An owner's notice that fails to go again lands in `failed`, for
/// lastError, and the rest goes on.
#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -497,30 +471,23 @@ pub(super) async fn publish(
}
}

// (a2b) A close-out keeps the card: edited to the receipt, with the
// answer and this drop as its Outcome. A later close-out edits it again.
// (a2b) A close-out keeps the card, owed its edit to the receipt
// (`replaced`), with the answer and this drop as its Outcome. A later
// close-out edits it again.
for id in s.items.keys().cloned().collect::<Vec<_>>() {
let item = &s.items[&id];
let (Some(_), Some(card)) = (&item.request, &item.card) else {
continue;
};
let hash = item::card_hash(item);
if card.hash == hash {
// A channel not ready takes no edit (`replaced`): the hash stays behind,
// so the close-out is picked up once it is ready.
if card.hash == hash || !channel_ready(s, item) {
continue;
}
if !spend(budget, 1) {
return Ok(());
}
let (event_id, ch) = (card.event_id.clone(), card_channel(s, item));
let text = item::render_card(item, hhmm) + &item::thread_line(&ch, &event_id);
match edit(ctx, s, &ch, &event_id, &text, now).await {
Ok(_) => {
let i = s.items.get_mut(&id).expect("receipt id");
i.card.as_mut().expect("receipt card").hash = hash;
}
Err(e) if e.contains("not found") => forget_card(ctx, s, &id),
Err(e) => return Err(e),
}
super::replaced::receipt(s, &id, now);
let i = s.items.get_mut(&id).expect("receipt id");
i.card.as_mut().expect("receipt card").hash = hash;
}

// (a3) Tell the writer's route target of a settled answer change, and of
Expand Down Expand Up @@ -704,12 +671,13 @@ pub(super) async fn publish(
s.items.get_mut(&id).expect("routed id").routed_reply = Some(r.event_id.clone());
}

// (b) Delete cards that are stale (another Drop, card format or stamp) or
// (b) Retire cards that are stale (another Drop, card format or stamp) or
// no longer asked (superseded, expired, no longer pending), never for
// MAX_CARDS, unless someone answered or replied on them or they are a
// receipt: those stay on the relay for good, and so, closed, does one
// with anything else on it but its seeds. A card in a channel that is
// not ready, or `held`, waits: its taps are read once it is again.
// receipt: those keep their place. A retired card stays on the relay for
// good, kept from recover and owed its edit closed (`replaced`), so no
// failure here fails the tick. A card in a channel that is not ready, or
// `held`, waits: its taps are read once it is again.
let stale: Vec<(String, String, Posted)> = s
.items
.values()
Expand All @@ -722,36 +690,27 @@ pub(super) async fn publish(
})
.collect();
for (id, ch, card) in stale {
// One post per card, its lookups riding on it: the delete, or the edit
// that closes it.
if !spend(budget, 1) {
return Ok(());
}
// A reply or tap the item never read or no longer holds (free text on
// an unscoped card with no route, or one asked again) still makes the
// card a record: it stays, as `carry_card` keeps one, but closed,
// since nothing reads it any more.
if touched(ctx, s, &ch, &card.event_id, now).await? {
close(ctx, s, &id, &ch, &card, now).await?;
} else {
delete(ctx, s, &ch, &card.event_id, now).await?;
}
super::replaced::retired(s, &id, &ch, &card, now);
s.kept.push(card.event_id);
forget_card(ctx, s, &id);
}

// (c) Post missing cards, adopting a recovered orphan when its hash matches.
// (c) Post missing cards, adopting a recovered orphan when its hash matches
// and it was not edited closed: that post says a tap there doesn't count.
for id in &wanted {
if s.items[id].card.is_some() {
continue;
}
let ch = card_channel(s, &s.items[id]);
if let Some(orphan) = s.orphan_cards.get(id).cloned() {
if orphan.hash == item::card_hash(&s.items[id]) {
let on = on_post(ctx, s, &ch, &orphan.event_id, now).await?;
let closed = on.iter().any(super::replaced::closes);
if orphan.hash == item::card_hash(&s.items[id]) && !closed {
s.items.get_mut(id).expect("wanted id").card = s.orphan_cards.remove(id);
continue;
}
// An answered, replied or receipt card stays on the relay for good.
if touched(ctx, s, &ch, &orphan.event_id, now).await? {
if !on.is_empty() {
s.kept.push(orphan.event_id.clone());
} else {
if !spend(budget, 1) {
Expand Down
Loading
Loading