Skip to content
Draft
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
78 changes: 78 additions & 0 deletions docs/rfcs/0013-favourites-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: "Favourites API"
owner: "@filippovecchiato"
---

# RFC 0013 — Favourites API

## Summary

Products can query, add, and remove bookmarked apps from the host's local product catalogue. The host exposes a subscription for the bookmarked-product list and two mutations for adding/removing entries.

## Motivation

Hosts maintain a local catalogue of products the user has bookmarked, but this data is inaccessible to products. Discovery surfaces like Browse cannot show which apps are already bookmarked or let the user add new ones without this API.

## Detailed Design

### Data Model

```rust
struct FavouriteProduct {
product_id: String,
source: FavouriteProductSource,
created_at: u64,
updated_at: u64,
}

enum FavouriteProductSource {
Remote,
Local,
}
```

`product_id` is a DotNS identifier. `source` distinguishes on-chain registry discoveries (`Remote`) from sideloaded entries (`Local`). Timestamps are Unix seconds.

### API

Three methods on a `Favourites` trait:

**`subscribe`** — streams the full bookmarked-product list on each change. Hosts MAY debounce.

**`add`** — upserts a product with `source: Remote`, setting `created_at` on first add and `updated_at` on every call. Returns the resulting record.

**`forget`** — removes the product from the catalogue.

### Error Handling

Subscription errors and mutation errors use `CallError` with a domain enum:

```rust
enum HostFavouritesSubscribeError {
Unknown { reason: String },
}

enum HostFavouritesAddError {
Unknown { reason: String },
}

enum HostFavouritesForgetError {
NotFound,
Unknown { reason: String },
}
```

Permission denial and unsupported-host cases are handled by `CallError::Denied` and `CallError::Unsupported`.

### Permission Model

The host SHOULD prompt the user before granting a product access to the favourites catalogue. The host MAY grant implicit access to Browse (the built-in discovery product) without prompting.

### Host Behaviour

Favourites are local to the host instance. Cross-host sync is out of scope.

## Drawbacks

- **Full-list delivery.** No pagination or filtered subscriptions. Acceptable for typical catalogue sizes (tens to low hundreds).
- **Browse coupling.** Implicit privilege for Browse assumes a well-known product identity.
73 changes: 73 additions & 0 deletions rust/crates/truapi/src/api/favourites.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Unified [`Favourites`] trait.

use crate::versioned::favourites::{
HostFavouritesAddError, HostFavouritesAddRequest, HostFavouritesAddResponse,
HostFavouritesForgetError, HostFavouritesForgetRequest, HostFavouritesSubscribeError,
HostFavouritesSubscribeItem,
};
use crate::wire;
use crate::{CallContext, CallError, Subscription};

/// Bookmarked-product catalogue methods.
pub trait Favourites: Send + Sync {
/// Subscribe to the user's bookmarked products.
///
/// ```ts
/// import { firstValueFrom, from } from "rxjs";
///
/// const favourites = await firstValueFrom(
/// from(truapi.favourites.subscribe()),
/// );
/// console.log("favourites:", favourites);
/// ```
#[wire(start_id = 164)]
async fn subscribe(
&self,
_cx: &CallContext,
) -> Result<
Subscription<HostFavouritesSubscribeItem>,
CallError<HostFavouritesSubscribeError>,
> {
Err(CallError::unavailable())
}

/// Add a product to the favourites catalogue.
///
/// ```ts
/// const result = await truapi.favourites.add({
/// productId: "some-product.dot",
/// });
/// result.match(
/// (value) => console.log("added:", value),
/// (error) => console.error("add failed:", error),
/// );
/// ```
#[wire(request_id = 168)]
async fn add(
&self,
_cx: &CallContext,
_request: HostFavouritesAddRequest,
) -> Result<HostFavouritesAddResponse, CallError<HostFavouritesAddError>> {
Err(CallError::unavailable())
}

/// Remove a product from the favourites catalogue.
///
/// ```ts
/// const result = await truapi.favourites.forget({
/// productId: "some-product.dot",
/// });
/// result.match(
/// () => console.log("forgotten"),
/// (error) => console.error("forget failed:", error),
/// );
/// ```
#[wire(request_id = 170)]
async fn forget(
&self,
_cx: &CallContext,
_request: HostFavouritesForgetRequest,
) -> Result<(), CallError<HostFavouritesForgetError>> {
Err(CallError::unavailable())
}
}
4 changes: 4 additions & 0 deletions rust/crates/truapi/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod chain;
pub mod chat;
pub mod coin_payment;
pub mod entropy;
pub mod favourites;
pub mod local_storage;
pub mod notifications;
pub mod payment;
Expand All @@ -21,6 +22,7 @@ pub use chain::Chain;
pub use chat::Chat;
pub use coin_payment::CoinPayment;
pub use entropy::Entropy;
pub use favourites::Favourites;
pub use local_storage::LocalStorage;
pub use notifications::Notifications;
pub use payment::Payment;
Expand All @@ -39,6 +41,7 @@ pub trait TrUApi:
+ Chat
+ CoinPayment
+ Entropy
+ Favourites
+ LocalStorage
+ Notifications
+ Payment
Expand All @@ -60,6 +63,7 @@ impl<T> TrUApi for T where
+ Chat
+ CoinPayment
+ Entropy
+ Favourites
+ LocalStorage
+ Notifications
+ Payment
Expand Down
70 changes: 70 additions & 0 deletions rust/crates/truapi/src/v01/favourites.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use parity_scale_codec::{Decode, Encode};

/// How a product entered the favourites catalogue.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub enum FavouriteProductSource {
/// Discovered via the on-chain registry.
Remote,
/// Sideloaded or manually added.
Local,
}

/// A bookmarked product in the host's local catalogue.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct FavouriteProduct {
/// DotNS identifier of the product.
pub product_id: String,
/// How the product was added.
pub source: FavouriteProductSource,
/// Unix timestamp (seconds) when first bookmarked.
pub created_at: u64,
/// Unix timestamp (seconds) of the most recent update.
pub updated_at: u64,
}

/// Request to add a product to the favourites catalogue.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct HostFavouritesAddRequest {
/// DotNS identifier of the product to add.
pub product_id: String,
}

/// Response after adding a product to favourites.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct HostFavouritesAddResponse {
/// The resulting catalogue entry.
pub product: FavouriteProduct,
}

/// Request to remove a product from the favourites catalogue.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct HostFavouritesForgetRequest {
/// DotNS identifier of the product to remove.
pub product_id: String,
}

/// Error from [`crate::api::Favourites::subscribe`].
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub enum HostFavouritesSubscribeError {
/// Catch-all.
Unknown { reason: String },
}

/// Error from [`crate::api::Favourites::add`].
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub enum HostFavouritesAddError {
/// Catch-all.
Unknown { reason: String },
}

/// Error from [`crate::api::Favourites::forget`].
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub enum HostFavouritesForgetError {
/// The product was not in the catalogue.
NotFound,
/// Catch-all.
Unknown { reason: String },
}

/// Item pushed to favourites subscribers: the full list of bookmarked products.
pub type HostFavouritesSubscribeItem = Vec<FavouriteProduct>;
2 changes: 2 additions & 0 deletions rust/crates/truapi/src/v01/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod chat;
mod coin_payment;
mod common;
mod entropy;
mod favourites;
mod local_storage;
mod notifications;
mod payment;
Expand All @@ -24,6 +25,7 @@ pub use chat::*;
pub use coin_payment::*;
pub use common::*;
pub use entropy::*;
pub use favourites::*;
pub use local_storage::*;
pub use notifications::*;
pub use payment::*;
Expand Down
31 changes: 31 additions & 0 deletions rust/crates/truapi/src/versioned/favourites.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Versioned wrappers for [`Favourites`](crate::api::Favourites) methods.

use crate::v01;

truapi_macros::versioned_type! {
pub enum HostFavouritesSubscribeItem { V1 => v01::HostFavouritesSubscribeItem }
}

truapi_macros::versioned_type! {
pub enum HostFavouritesSubscribeError { V1 => v01::HostFavouritesSubscribeError }
}

truapi_macros::versioned_type! {
pub enum HostFavouritesAddRequest { V1 => v01::HostFavouritesAddRequest }
}

truapi_macros::versioned_type! {
pub enum HostFavouritesAddResponse { V1 => v01::HostFavouritesAddResponse }
}

truapi_macros::versioned_type! {
pub enum HostFavouritesAddError { V1 => v01::HostFavouritesAddError }
}

truapi_macros::versioned_type! {
pub enum HostFavouritesForgetRequest { V1 => v01::HostFavouritesForgetRequest }
}

truapi_macros::versioned_type! {
pub enum HostFavouritesForgetError { V1 => v01::HostFavouritesForgetError }
}
1 change: 1 addition & 0 deletions rust/crates/truapi/src/versioned/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub mod chain;
pub mod chat;
pub mod coin_payment;
pub mod entropy;
pub mod favourites;
pub mod local_storage;
pub mod notifications;
pub mod payment;
Expand Down
Loading