commit 136b16ec1a1f6c30130fb1e10d913ebf0a4cae58
parent 97f2e0a3711deaccb8e7b316b4982e63d9ed1509
Author: triesap <tyson@radroots.org>
Date: Sat, 23 May 2026 10:42:11 +0000
listing: make app records newest-first
- read shared app records from newest change pages
- expose app-record list pagination metadata
- dedupe fallback listing identity by owner pubkey
- cover newer records behind older local work
Diffstat:
4 files changed, 146 insertions(+), 22 deletions(-)
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -2572,6 +2572,12 @@ pub struct ListingAppRecordListView {
pub state: String,
pub source: String,
pub count: usize,
+ pub limit: u32,
+ pub has_more: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub next_before_change_seq: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub next_before_seq: Option<i64>,
pub local_events_db: String,
pub records: Vec<ListingAppRecordSummaryView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -49,9 +49,9 @@ use crate::runtime::direct_relay::{
};
use crate::runtime::farm_config;
use crate::runtime::local_events::{
- append_local_work, append_signed_event, get_shared_record, list_shared_records,
- mark_signed_event_acknowledged, mark_signed_event_failed_for_publish_error,
- shared_local_events_db_path,
+ append_local_work, append_signed_event, get_shared_record, list_shared_records_before,
+ list_shared_records_latest, mark_signed_event_acknowledged,
+ mark_signed_event_failed_for_publish_error, shared_local_events_db_path,
};
use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority};
use crate::runtime::sync::{
@@ -669,8 +669,20 @@ pub fn list(config: &RuntimeConfig) -> Result<ListingListView, RuntimeError> {
pub fn app_record_list(config: &RuntimeConfig) -> Result<ListingAppRecordListView, RuntimeError> {
let database_path = shared_local_events_db_path(config)?;
- let records = current_app_record_entries(app_local_records(config)?)
- .into_iter()
+ let mut entries = current_app_record_entries(app_local_records(config)?);
+ let has_more = entries.len() > APP_RECORD_LIST_LIMIT as usize;
+ if has_more {
+ entries.truncate(APP_RECORD_LIST_LIMIT as usize);
+ }
+ let next_cursor = if has_more {
+ entries
+ .last()
+ .map(|entry| (entry.record.change_seq, entry.record.seq))
+ } else {
+ None
+ };
+ let records = entries
+ .iter()
.map(|entry| app_record_summary(&entry.record, entry.superseded_count))
.collect::<Vec<_>>();
let state = if records.is_empty() { "empty" } else { "ready" };
@@ -684,6 +696,10 @@ pub fn app_record_list(config: &RuntimeConfig) -> Result<ListingAppRecordListVie
state: state.to_owned(),
source: LISTING_APP_RECORD_SOURCE.to_owned(),
count: records.len(),
+ limit: APP_RECORD_LIST_LIMIT,
+ has_more,
+ next_before_change_seq: next_cursor.map(|(change_seq, _)| change_seq),
+ next_before_seq: next_cursor.map(|(_, seq)| seq),
local_events_db: database_path.display().to_string(),
records,
actions,
@@ -1163,17 +1179,46 @@ struct AppRecordListEntry {
}
fn app_local_records(config: &RuntimeConfig) -> Result<Vec<LocalEventRecord>, RuntimeError> {
- Ok(list_shared_records(config, APP_RECORD_LIST_LIMIT)?
- .into_iter()
- .filter(|record| {
- record.source_runtime == SourceRuntime::App
- && record.family == LocalRecordFamily::LocalWork
- && matches!(
- local_record_kind(record).as_deref(),
- Some("farm_config_v1" | DRAFT_KIND)
- )
- })
- .collect())
+ let mut app_records = Vec::new();
+ let mut before_cursor = None::<(i64, i64)>;
+ loop {
+ let shared_records = if let Some((before_change_seq, before_seq)) = before_cursor {
+ list_shared_records_before(
+ config,
+ before_change_seq,
+ before_seq,
+ APP_RECORD_LIST_LIMIT,
+ )?
+ } else {
+ list_shared_records_latest(config, APP_RECORD_LIST_LIMIT)?
+ };
+ let Some(next_cursor) = shared_records
+ .last()
+ .map(|record| (record.change_seq, record.seq))
+ else {
+ break;
+ };
+ let has_more = shared_records.len() == APP_RECORD_LIST_LIMIT as usize;
+ app_records.extend(
+ shared_records
+ .into_iter()
+ .filter(is_supported_app_local_record),
+ );
+ if !has_more {
+ break;
+ }
+ before_cursor = Some(next_cursor);
+ }
+ Ok(app_records)
+}
+
+fn is_supported_app_local_record(record: &LocalEventRecord) -> bool {
+ record.source_runtime == SourceRuntime::App
+ && record.family == LocalRecordFamily::LocalWork
+ && matches!(
+ local_record_kind(record).as_deref(),
+ Some("farm_config_v1" | DRAFT_KIND)
+ )
}
fn current_app_record_entries(mut records: Vec<LocalEventRecord>) -> Vec<AppRecordListEntry> {
@@ -1296,10 +1341,15 @@ fn app_record_current_key(record: &LocalEventRecord) -> String {
{
return format!("listing_addr:{listing_addr}");
}
- let (listing_id, _, farm_d_tag) = app_listing_display_parts(record);
- if let Some(listing_id) = listing_id {
- let farm_key = farm_d_tag.or(record.farm_id.clone()).unwrap_or_default();
- return format!("listing:{farm_key}:{listing_id}");
+ let (listing_id, _, _) = app_listing_display_parts(record);
+ if let (Some(owner_pubkey), Some(listing_id)) = (
+ record
+ .owner_pubkey
+ .as_deref()
+ .and_then(canonical_hex_pubkey),
+ listing_id.filter(|value| is_d_tag_base64url(value)),
+ ) {
+ return format!("listing_owner:{owner_pubkey}:{listing_id}");
}
}
Some("farm_config_v1") => {
@@ -1326,6 +1376,15 @@ fn app_record_current_key(record: &LocalEventRecord) -> String {
format!("record:{}", record.record_id)
}
+fn canonical_hex_pubkey(value: &str) -> Option<String> {
+ let trimmed = value.trim();
+ if trimmed.len() == 64 && trimmed.chars().all(|char| char.is_ascii_hexdigit()) {
+ Some(trimmed.to_ascii_lowercase())
+ } else {
+ None
+ }
+}
+
fn app_listing_display_parts(
record: &LocalEventRecord,
) -> (Option<String>, Option<String>, Option<String>) {
diff --git a/src/runtime/local_events.rs b/src/runtime/local_events.rs
@@ -165,7 +165,7 @@ pub fn shared_local_events_db_path(config: &RuntimeConfig) -> Result<PathBuf, Ru
Ok(shared_local_events_root(config)?.join(SHARED_LOCAL_EVENTS_DB_FILE))
}
-pub fn list_shared_records(
+pub fn list_shared_records_latest(
config: &RuntimeConfig,
limit: u32,
) -> Result<Vec<LocalEventRecord>, RuntimeError> {
@@ -175,7 +175,22 @@ pub fn list_shared_records(
}
let executor = SqliteExecutor::open(database_path)?;
let store = LocalEventsStore::new(executor);
- Ok(store.list_records_changed_after(0, limit)?)
+ Ok(store.list_records_changed_latest(limit)?)
+}
+
+pub fn list_shared_records_before(
+ config: &RuntimeConfig,
+ before_change_seq: i64,
+ before_seq: i64,
+ limit: u32,
+) -> Result<Vec<LocalEventRecord>, RuntimeError> {
+ let database_path = shared_local_events_db_path(config)?;
+ if !database_path.exists() {
+ return Ok(Vec::new());
+ }
+ let executor = SqliteExecutor::open(database_path)?;
+ let store = LocalEventsStore::new(executor);
+ Ok(store.list_records_changed_before(before_change_seq, before_seq, limit)?)
}
pub fn get_shared_record(
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -3686,6 +3686,8 @@ fn listing_app_records_list_current_records_and_blocks_stale_export() {
let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
assert_eq!(list["result"]["count"], 2);
+ assert_eq!(list["result"]["limit"], 500);
+ assert_eq!(list["result"]["has_more"], false);
let records = list["result"]["records"].as_array().expect("records");
assert!(
records
@@ -3732,6 +3734,48 @@ fn listing_app_records_list_current_records_and_blocks_stale_export() {
}
#[test]
+fn listing_app_records_list_includes_new_records_after_older_volume() {
+ let sandbox = RadrootsCliSandbox::new();
+ let account = sandbox.json_success(&["--format", "json", "account", "create"]);
+ let account_id = account["result"]["account"]["id"]
+ .as_str()
+ .expect("account id");
+ let signer = sandbox.json_success(&["--format", "json", "signer", "status", "get"]);
+ let seller_pubkey = signer["result"]["local"]["public_identity"]["public_key_hex"]
+ .as_str()
+ .expect("seller pubkey");
+ for index in 0..505 {
+ let farm_d_tag = format!("F{index:021}");
+ seed_app_farm_record(&sandbox, account_id, seller_pubkey, farm_d_tag.as_str());
+ }
+ let listing_d_tag = "AAAAAAAAAAAAAAAAAAAAAQ";
+ let current_record_id = seed_app_listing_record_variant(
+ &sandbox,
+ account_id,
+ Some(seller_pubkey),
+ "AAAAAAAAAAAAAAAAAAAAAw",
+ listing_d_tag,
+ "current",
+ "Newest App Eggs",
+ None,
+ );
+
+ let list = sandbox.json_success(&["--format", "json", "listing", "app", "list"]);
+ assert_eq!(list["result"]["limit"], 500);
+ assert_eq!(list["result"]["count"], 500);
+ assert_eq!(list["result"]["has_more"], true);
+ assert!(list["result"]["next_before_change_seq"].as_i64().is_some());
+ assert!(list["result"]["next_before_seq"].as_i64().is_some());
+ let records = list["result"]["records"].as_array().expect("records");
+ assert_eq!(records[0]["record_id"], current_record_id);
+ assert!(
+ records
+ .iter()
+ .any(|record| record["record_id"] == current_record_id)
+ );
+}
+
+#[test]
fn listing_app_records_mark_unresolved_pubkey_records_non_exportable() {
let sandbox = RadrootsCliSandbox::new();
let account_id = "acct_unresolved";