diff --git a/server/src/at_record_server/context.gleam b/server/src/at_record_server/context.gleam index 287295f..745cd12 100644 --- a/server/src/at_record_server/context.gleam +++ b/server/src/at_record_server/context.gleam @@ -3,6 +3,7 @@ //// (host string, client, store) never crosses into a domain module; only //// capabilities do. +import at_record_server/catalog/source as catalog_source import at_record_server/catalog_deps import at_record_server/catalog_index import at_record_server/discogs_client @@ -44,6 +45,9 @@ pub type Context { catalog: catalog_deps.Deps, known_users: known_users.Store, catalog_index: catalog_index.Store, + /// The resolution layer's read port: candidate release rows (chain edges + /// included) plus a public adoption tally. See `catalog/source`. + variant_source: catalog_source.Source, oauth: Config, ) } diff --git a/server/src/at_record_server/wiring.gleam b/server/src/at_record_server/wiring.gleam index fc3a4fa..ef42cb0 100644 --- a/server/src/at_record_server/wiring.gleam +++ b/server/src/at_record_server/wiring.gleam @@ -5,16 +5,25 @@ import at_record/gen/catalog/edit as catalog_edit import at_record/gen/catalog/release as catalog_release import at_record/gen/client as generated_client +import at_record/gen/defs +import at_record_server/browse.{type BrowseRow} +import at_record_server/catalog/source as catalog_source import at_record_server/catalog_deps +import at_record_server/catalog_index import at_record_server/context.{type Context, Atproto, Context, Discogs, Web} import at_record_server/musicbrainz_client import at_record_server/promotion import atproto/constellation import atproto/xrpc.{type Client} import gleam/dynamic/decode -import gleam/option.{None} +import gleam/list +import gleam/option.{None, Some} import gleam/result +/// Backlink source for adoption: shelf.entry rows whose `release` field +/// points at the release uri. +const shelf_entry_release_source = "dev.mokkenstorm.crate.shelf.entry:release.uri" + pub fn context( client client: Client, static_directory static_directory: String, @@ -27,6 +36,7 @@ pub fn context( catalog_index catalog_index, oauth oauth, ) -> Context { + let catalog = catalog_deps(client, constellation_host, resolver) Context( web: Web(static_directory:, base_url:), atproto: Atproto(client:, resolver:), @@ -35,13 +45,82 @@ pub fn context( creds: discogs_creds, send: text_sender(client), ), - catalog: catalog_deps(client, constellation_host, resolver), + catalog:, known_users:, catalog_index:, + variant_source: variant_source(catalog, catalog_index), oauth:, ) } +/// The resolution layer's read port, built over what's already wired: the +/// live `catalog_index` for candidate rows, plus a per-row `fetch_release` +/// call to decode the chain edges `BrowseRow` doesn't carry yet, and a +/// Constellation backlink tally for adoption. +/// +/// Swap point: once `feat/catalog-indexer` lands (BrowseRow gaining +/// `supersedes`/`based_on` and a maintained adoption count), `releases` +/// becomes a direct map over `catalog_index.list()` with no per-row +/// `fetch_release` round-trip, and `adoption_count` can read the indexed +/// count instead of calling Constellation live. +fn variant_source( + catalog: catalog_deps.Deps, + catalog_index: catalog_index.Store, +) -> catalog_source.Source { + catalog_source.Source( + releases: fn() { catalog_index.list() |> list_variant_rows(catalog, _) }, + adoption_count: fn(uri) { + case catalog.backlinks(uri, shelf_entry_release_source) { + Ok(page) -> page.total + Error(_) -> 0 + } + }, + ) +} + +fn list_variant_rows( + catalog: catalog_deps.Deps, + rows: List(BrowseRow), +) -> List(catalog_source.VariantRow) { + rows |> list.map(to_variant_row(catalog, _)) +} + +fn to_variant_row( + catalog: catalog_deps.Deps, + row: BrowseRow, +) -> catalog_source.VariantRow { + let #(supersedes, based_on) = case catalog.fetch_release(row.uri) { + Some(#(_cid, value)) -> #( + value.supersedes |> option.map(ref_uri), + value.based_on |> option.map(ref_uri), + ) + None -> #(None, None) + } + catalog_source.VariantRow( + uri: row.uri, + cid: row.cid, + title: row.title, + artist_display: row.artist_display, + genres: row.genres, + styles: row.styles, + released: row.released, + country: row.country, + cover: row.cover, + thumb_url: row.thumb_url, + discogs_id: row.discogs_id, + created_at: row.created_at, + publisher_did: row.publisher_did, + publisher_handle: row.publisher_handle, + publisher_pds: row.publisher_pds, + supersedes:, + based_on:, + ) +} + +fn ref_uri(ref: defs.CatalogRef) -> String { + ref.uri +} + fn text_sender(client: Client) { fn(r) { case xrpc.send_text(client, r) {