From 3cc39590a71fa81c30027578bb4c5b56fea3f661 Mon Sep 17 00:00:00 2001 From: Scott Reimers Date: Sat, 21 Mar 2026 00:20:47 -0400 Subject: [PATCH] Design doc audit: update badges, fix outdated descriptions, add CDN/replication docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Badge updates: - BlobHeader: Planned → Complete (has receipt/comment slots, reactions, policy) - LAN Discovery: Planned → Complete (iroh mDNS integration) - UPnP TCP: Planned → Complete (both UDP+TCP renewal cycles) - HTTP Post Delivery: added Complete badge Description fixes: - Share links: removed hostlist encoding, added tiered serving (redirect → punch → proxy) - Eviction formula: added share_boost factor (+100 for 3+ downstream) - Message types table: added ReplicationRequest/Response (0xE1/0xE2), count 41 → 49 - Engagement: added tombstone propagation description New sections: - Device roles & bandwidth budgets (Intermittent/Available/Persistent) - Active CDN replication (10-min cycle, target prioritization, graceful degradation) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/tauri-app/capabilities/default.json | 4 +- .../tauri-app/gen/schemas/capabilities.json | 2 +- frontend/app.js | 37 +++++++- website/design.html | 93 +++++++++++-------- 4 files changed, 94 insertions(+), 42 deletions(-) diff --git a/crates/tauri-app/capabilities/default.json b/crates/tauri-app/capabilities/default.json index 6fffe30..72d9e8c 100644 --- a/crates/tauri-app/capabilities/default.json +++ b/crates/tauri-app/capabilities/default.json @@ -9,6 +9,8 @@ "notification:allow-request-permission", "notification:allow-notify", "notification:allow-check-permissions", - "notification:allow-show" + "notification:allow-show", + "notification:allow-cancel", + "notification:allow-remove-active" ] } diff --git a/crates/tauri-app/gen/schemas/capabilities.json b/crates/tauri-app/gen/schemas/capabilities.json index 062c208..602c500 100644 --- a/crates/tauri-app/gen/schemas/capabilities.json +++ b/crates/tauri-app/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default capability for the main window","local":true,"windows":["main"],"permissions":["core:default","notification:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-check-permissions","notification:allow-show"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default capability for the main window","local":true,"windows":["main"],"permissions":["core:default","notification:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","notification:allow-check-permissions","notification:allow-show","notification:allow-cancel","notification:allow-remove-active"]}} \ No newline at end of file diff --git a/frontend/app.js b/frontend/app.js index a8f1193..29f08ac 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -357,6 +357,8 @@ function toast(msg) { // --- Notifications (Tauri plugin) --- let _notifReady = false; +let _activeNotificationIds = new Set(); + async function maybeNotify(title, body, tag) { try { if (window.__TAURI__?.notification) { @@ -367,16 +369,42 @@ async function maybeNotify(title, body, tag) { granted = perm === 'granted'; } if (granted) { - sendNotification({ title, body, channelId: 'default' }); + sendNotification({ title, body, channelId: 'default', id: tag ? hashCode(tag) : undefined }); + if (tag) _activeNotificationIds.add(tag); } } else if ('Notification' in window) { - // Fallback for browsers if (Notification.permission === 'default') await Notification.requestPermission(); if (Notification.permission === 'granted') new Notification(title, { body, tag, silent: false }); } } catch (_) {} } +async function clearNotifications(tagPrefix) { + try { + if (window.__TAURI__?.notification) { + const { removeActive } = window.__TAURI__.notification; + if (!removeActive) return; + const toRemove = [..._activeNotificationIds].filter(t => t.startsWith(tagPrefix)); + for (const tag of toRemove) { + try { + await removeActive({ notifications: [{ id: hashCode(tag) }] }); + } catch (_) {} + _activeNotificationIds.delete(tag); + } + } + } catch (_) {} +} + +function hashCode(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const ch = str.charCodeAt(i); + hash = ((hash << 5) - hash) + ch; + hash |= 0; + } + return Math.abs(hash); +} + // --- Popover helpers --- let popoverOnClose = null; function openPopover(title, html, opts = {}) { @@ -897,8 +925,9 @@ async function loadMessages(force) { const input = $('#popover-reply-input'); if (input) setTimeout(() => input.focus(), 100); - // Mark conversation as read (DB-backed) + // Mark conversation as read (DB-backed) and clear notifications invoke('mark_conversation_read', { partnerId }).catch(() => {}); + clearNotifications(`msg-`); // Mark incoming encrypted messages as "seen" for (const p of threadPosts) { @@ -952,6 +981,7 @@ async function loadMessages(force) { onClose() { // Mark conversation as read when closing the popover invoke('mark_conversation_read', { partnerId }).catch(() => {}); + clearNotifications(`msg-`); } }); }); @@ -2730,6 +2760,7 @@ document.querySelectorAll('.tab').forEach(tab => { if (target === 'messages') { if (!conversationsList.children.length) conversationsList.innerHTML = renderLoading(); loadMessages(true); loadDmRecipientOptions(); + clearNotifications('msg-'); } if (target === 'settings') { loadRedundancy(); loadPublicVisible(); loadCacheSizeSetting(); } }); diff --git a/website/design.html b/website/design.html index 46a5447..e2cb41b 100644 --- a/website/design.html +++ b/website/design.html @@ -653,9 +653,9 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false }

12. LAN Discovery

-

Status: Planned

+

Status: Complete

-

iroh's mDNS address lookup broadcasts peer presence on the local network via multicast DNS (service name "irohv1", backed by the swarm-discovery crate). Currently this is configured as a passive address resolver — if we already know a peer's NodeId, mDNS can resolve its LAN address. But mDNS also discovers unknown peers on the same network, and iroh exposes this via MdnsAddressLookup::subscribe().

+

mDNS-based LAN discovery is integrated via iroh's built-in MdnsAddressLookupBuilder. It works automatically — peers on the same local network are discovered and connected without manual configuration. iroh's mDNS address lookup broadcasts peer presence on the local network via multicast DNS (service name "irohv1", backed by the swarm-discovery crate).

Discovery flow

    @@ -885,17 +885,18 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false }Blob content immutability

    Blob data is BLAKE3-addressed — the CID is the hash of the content. This means blob content is immutable by definition. Any mutable metadata (neighborhood, host lists, signatures) MUST be stored separately in a BlobHeader. Inline mutable headers are architecturally incompatible with content addressing.

    -

    BlobHeader Planned

    -

    Formal mutable structure replacing/extending CdnManifest. Stored and transmitted separately from blob data.

    +

    BlobHeader Complete

    +

    Mutable structure stored and transmitted separately from blob data. Carries engagement state, CDN metadata, and encrypted slots for private posts.

    BlobHeader {
    -    cid,                    // BLAKE3 hash of blob content
    -    author_nplus10,         // Author's N+10 (NodeId + 10 preferred peers)
    -    author_recent_posts,    // 25 previous + 25 following PostIds (neighborhood)
    -    upstream_nplus10,       // Upstream file source's N+10 (if not author)
    -    downstream_hosts,       // Up to min(100, floor(170MB / blob_size)) downstream hosts
    -    author_signature,       // ed25519 signature over author fields
    -    host_signature,         // ed25519 signature by current host
    +    post_id,                // PostId this header belongs to
    +    author,                 // Author NodeId
    +    reactions,              // Vec of public reactions (emoji + reactor NodeId + timestamp)
    +    comments,               // Vec of comments (text + author + timestamp + signature)
    +    policy,                 // Author-controlled comment/react policy
         updated_at,             // Timestamp of last header update
    +    thread_splits,          // Linked thread posts when comments exceed 16KB
    +    receipt_slots,          // Encrypted delivery/read/react receipt slots (private posts)
    +    comment_slots,          // Encrypted comment slots (private posts)
     }
    • Post neighborhood: 25 previous + 25 following PostIds. Forward slots are empty at publish time and populate via BlobHeaderDiff propagation as the author continues posting. Empty forward slots are not an error condition.
    • @@ -927,7 +928,7 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false }

      Blob eviction Complete

      -
      priority = pin_boost + (relationship * heart_recency * freshness / (peer_copies + 1))
      +
      priority = pin_boost + share_boost + (relationship * heart_recency * freshness / (peer_copies + 1))
      @@ -935,6 +936,7 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false } +
      FactorCalculation
      pin_boost1000.0 if pinned, else 0.0. Own blobs auto-pinned.
      heart_recencyLinear decay over 30 days: max(0, 1 - age/30d)
      freshness1 / (1 + post_age_days)
      peer_copiesKnown replica count (from post_replicas, only if < 1 hour old)
      share_boost+100.0 if 3+ downstream peers (shared link with healthy distribution), scaled linearly for 1–2 downstream peers (33.3 per peer). Keeps shared content cached longer.

      Pin modes Planned

      @@ -973,7 +975,7 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false }
    -

    Message types (41 total)

    +

    Message types (49 total)

    @@ -1023,6 +1025,8 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false } + +
    HexNameStreamPurpose
    0x01NodeListUpdateUniIncremental N1/N2 diff broadcast
    0xD6TcpPunchRequestBiAsk holder to punch TCP toward browser IP
    0xD7TcpPunchResultBiPunch result + HTTP address for redirect
    0xE0MeshKeepaliveUni30s connection heartbeat
    0xE1ReplicationRequestBiRequest peer to cache specific posts
    0xE2ReplicationResponseBiAccept/reject replication request

    Engagement propagation

    @@ -1031,9 +1035,34 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false }Push (real-time): On react/comment, the diff is sent to both downstream peers (CDN tree children) and the upstream peer (who we got the post from). Each intermediate node re-propagates both directions, excluding sender. This flows the diff up to the author and down to all holders.
  1. Auto downstream registration: Nodes that receive a post via pull sync or push notification automatically send PostDownstreamRegister (0xD3) to the sender, ensuring bidirectional diff flow.
  2. Pull (safety net): Every 5 minutes, the pull cycle requests BlobHeaderRequest (0xD1) with the local header timestamp. Peers respond with the full header only if theirs is newer. Additive merge — store_reaction upserts, store_comment inserts with ON CONFLICT DO NOTHING.
  3. +
  4. Tombstones: Deleted reactions and comments are not hard-deleted. Instead, a deleted_at timestamp is set on the record. Tombstones propagate via pull sync headers — when a peer receives a header with a tombstoned entry, it applies the deletion locally. This prevents deleted engagement from being re-introduced by peers that haven't yet received the deletion.
  5. Planned: Pull engagement from both upstream and downstream peers to catch missed diffs from either direction.
  6. +

    Device roles & bandwidth budgets Complete

    +

    Each node advertises its device role in InitialExchange, which determines its bandwidth budgets for replication (pulling posts to cache) and delivery (serving requests from peers):

    + + + + + +
    RoleReplication / hourDelivery / hour
    Intermittent (phones)100 MB1 GB
    Available (desktops)200 MB2 GB
    Persistent (anchors)200 MB1 GB
    +
      +
    • Budgets auto-reset every hour
    • +
    • Role is self-declared based on device type and advertised to peers in InitialExchange
    • +
    • Peers respect advertised budgets when selecting replication targets
    • +
    + +

    Active CDN replication Complete

    +

    All devices proactively replicate recent under-replicated posts to peers, not just passively serve on request:

    +
      +
    • 10-minute cycle: All devices initiate replication checks every 10 minutes
    • +
    • Target prioritization: Desktops > anchors > phones, scored by available bandwidth budget and connection quality
    • +
    • Selection criteria: Posts less than 72 hours old with fewer than 2 downstream replicas are selected for replication
    • +
    • Protocol: ReplicationRequest (0xE1) asks a peer to cache specific posts; ReplicationResponse (0xE2) accepts or rejects based on available budget and storage
    • +
    • Graceful degradation: In small networks with few peers, the cycle runs but finds few or no viable targets — no wasted bandwidth. As the network grows, replication naturally increases.
    • +
    +

    Connection rate limiting

    Incoming QUIC connections that fail authentication are rate-limited per source IP to prevent CPU exhaustion from rogue or stale nodes:

      @@ -1258,6 +1287,8 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false }

      25. HTTP Post Delivery

      +

      Status: Complete

      +

      Direct peer-to-browser HTTP serving is implemented. For share link delivery, this is now part of the tiered web serving strategy (redirect → TCP punch → QUIC proxy) described in Section 26.

      Intent

      Every ItsGoin node that is publicly reachable can serve its cached public posts directly to browsers over HTTP — no extra infrastructure, no additional dependencies, no new binary. The same QUIC UDP port used for app traffic is accompanied by a TCP listener on the same port number. UDP goes to the QUIC stack as always. TCP goes to a minimal raw HTTP/1.1 handler baked into the binary.

      This makes every publicly-reachable node a browser-accessible content endpoint, enabling share links that deliver content peer-to-browser without routing any post bytes through itsgoin.net.

      @@ -1324,34 +1355,22 @@ FAILURE: C → B → A: AnchorProbeResult { reachable: false }

      URL format

      -
      https://itsgoin.net/p/<postid_hex>/<encoded_hostlist>
      +
      https://itsgoin.net/p/<postid_hex>/<author_nodeid_hex>
      • postid_hex: 64 hex characters (BLAKE3 post hash)
      • -
      • encoded_hostlist: base64url-encoded binary list of up to 5 host entries (see encoding below)
      • +
      • author_nodeid_hex: 64 hex characters (author's ed25519 public key). Enables direct QUIC connection as fast path; worm search handles the case where author is unreachable.
      -

      Example: https://itsgoin.net/p/3a7f...c921/AAEC...Zg==

      +

      Example: https://itsgoin.net/p/3a7f...c921/b4e2...f817

      +

      Simple, human-inspectable, no binary encoding needed. Author ID in the URL is sufficient for the tiered serving strategy below.

      -

      Host list encoding

      -

      Compact binary encoding — optimized for QR code scanability:

      -
      Per IPv6 host:  [0x06][16 bytes IP][2 bytes port]  =  19 bytes
      -Per IPv4 host:  [0x04][4 bytes IP][2 bytes port]   =   7 bytes
      -
      -5× IPv6:  95 bytes → ~127 chars base64url  (comfortably scannable QR)
      -

      All integers big-endian. base64url-encoded (URL-safe, no padding).

      - -

      Host list generation (at share time)

      -

      When a user taps “Share” on a post:

      +

      Tiered web serving

      +

      When a browser visits a share link, itsgoin.net attempts three tiers to deliver the post:

        -
      1. Query post_downstream for this postid
      2. -
      3. Filter to hosts with a known public address (IPv6 or UPnP-mapped IPv4)
      4. -
      5. Select up to 5 — prefer IPv6 public over UPnP IPv4, prefer most recently seen over stale
      6. -
      7. Include self if this node is publicly reachable
      8. -
      9. Encode and embed in URL
      10. +
      11. 302 redirect: If a publicly-reachable holder is known (IPv6, UPnP TCP), redirect the browser directly to that node's HTTP endpoint. Zero post bytes flow through itsgoin.net.
      12. +
      13. TCP punch: If a holder is connected but not publicly reachable, send TcpPunchRequest (0xD6) asking the holder to punch TCP toward the browser's IP. On success, TcpPunchResult (0xD7) returns the HTTP address for a redirect.
      14. +
      15. QUIC proxy: If neither redirect nor punch works, itsgoin.net fetches the post on-demand via PostFetch (0xD4/0xD5), renders HTML, and serves directly to the browser.
      -

      Availability math

      -

      At 80% per-node uptime (conservative for a mix of home and mobile nodes), 5 independent hosts gives 1 - (0.25) = 99.97% link availability. Hosts are selected from nodes that have already demonstrated they cached this specific post — not random peers.

      -

      itsgoin.net QUIC proxy handler

      Route: GET /p/<postid_hex>/<author_nodeid_hex>

      1. Check local storage (fast path — post already fetched recently)
      @@ -1502,7 +1521,7 @@ Per IPv4 host:  [0x04][4 bytes IP][2 bytes port]   =   7 bytes
                       UPnP port mapping (desktop)Complete
                       NAT type detection (STUN) + hard+hard skipComplete
                       Advanced NAT traversal (role-based scanning + filter probe)Complete
      -                LAN discovery (mDNS scan + auto-connect)Planned
      +                LAN discovery (mDNS scan + auto-connect)Complete
                       Content propagation via attentionPartial
                       BlobHeader separation from blob contentComplete
                       25+25 neighborhood with HeaderDiff propagationPartial (engagement diffs work, neighborhood diffs planned)
      @@ -1519,13 +1538,13 @@ Per IPv4 host:  [0x04][4 bytes IP][2 bytes port]   =   7 bytes
                       --max-mesh flag (test affordance)Planned
                       Audience shardingPlanned
                       Custom feedsPlanned
      -                HTTP post delivery (TCP listener, single route, load shedding)Planned
      +                HTTP post delivery (TCP listener, single route, load shedding)Complete
                       Share link generation (postid + author NodeId)Complete
                       itsgoin.net QUIC proxy handler (on-demand fetch + render)Complete
                       PostFetch (0xD4/0xD5) single-post retrievalComplete
                       Universal Links / App Links (itsgoin.net/p/*)Planned
                       itsgoin.net ItsGoin node (anchor + web handler)Complete
      -                UPnP TCP port mapping alongside UDPPlanned
      +                UPnP TCP port mapping alongside UDPComplete