Skip to content
Open
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
134 changes: 134 additions & 0 deletions doc/library-development/ADRs/broadcast-offline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Architecture Decision Record: Offline Broadcast

Title: Record: Offline Broadcast

## status

In progress

Updated on 15-06-2025

## contributors

- Main contributor(s): fmar

- Reviewer(s): fmar, leo, nogringo

- Final decision made by: fmar, leo, nogringo
Comment on lines +5 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the ADR metadata consistent.

In progress conflicts with Final decision made by, and the Updated on 15-06-2025 stamp looks stale for this PR. Please either keep this as a draft and remove the final-decision field, or mark it accepted with the correct date.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doc/library-development/ADRs/broadcast-offline.md` around lines 5 - 17, The
ADR metadata in the status section contains conflicting information: the status
field shows "In progress" while the "Final decision made by" field indicates the
decision has been finalized, and the "Updated on" date appears outdated. To
resolve this inconsistency, either change the status from "In progress" to
"Accepted" and update the date to the current date (15-06-2025 or later as
appropriate), or remove the "Final decision made by" field entirely and keep the
status as "In progress" to maintain the document as a draft. Choose the approach
based on whether this ADR's decision has actually been finalized or is still
under discussion.


## Context and Problem Statement

NDK does not remember events broadcasted when offline.
As a user, I want to be able to draft something in a remote location (e.g. plane) without thinking about Nostr relay constraints.


## Main Proposal

#### Requirements:
- relay specific
- gossip
- Consider done %
- different modes,
specific: relay broadcast must succeed
- eviction policy (keep in cache, delete old ones)
- Streamlined reject handling (retry, dismiss, paymentRequired, POW, blocked, etc.)

Comment on lines +27 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten the acceptance criteria and preserve auth-required retry behavior.

This section still reads like notes (todo, %, different modes) instead of implementable policy rules, and it omits auth-required, which packages/ndk/lib/domain_layer/usecases/relay_manager.dart#L469-L640 already treats as retryable rather than terminal. Please spell out the concrete thresholds and reject classes so the refactor matches the current broadcast flow.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 27-27: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doc/library-development/ADRs/broadcast-offline.md` around lines 27 - 35, The
Requirements section in the broadcast-offline ADR uses incomplete placeholder
language (like `todo`, `%`, `different modes`) instead of concrete,
implementable policy rules. Replace these vague entries with specific,
actionable requirements based on the actual behavior in relay_manager.dart
(particularly the reject handling logic in lines L469-L640). Specifically, add
`auth-required` to the list of retryable reject classes (since the relay_manager
already treats it as retryable rather than terminal), define explicit thresholds
and eviction policy rules instead of vague guidance, and enumerate the concrete
reject classes (such as retry, dismiss, paymentRequired, POW, blocked,
auth-required) with their respective handling behaviors to match the current
broadcast flow implementation.

- Gossip: % of relays is ok




#### Refactor the Broadcast use case into several concerns.

- cache with state information
```
PriorityBroadcast: 0
PolicyBroadcast: all ok
PolicyEviction: replace
event1: broadcast: {
relay 1 ok
relay 2 failed, timeout
}
```
- cache of deleted events
- Broadcast monitor (checks for broadcast state, delegates work, runs periodically)
- broadcast (stateless, broadcasts and returns results with comprehensive err handeling)

To model the constraints on each event, we are implementing a BroadcastPolicy, EvictionPolicy and priority broadcast.
Depending on the use case, an event must be accepted by one or multiple relays or just be accepted by a few. \
These policies are checked and executed by the broadcast monitor depending on the current conditions.\
\
Broadcast just executes a given policy/strategy given by the broadcast monitor and reports back.

#### BroadcastPolicys
- consider done %
- all ok
- todo

#### EvictionPolicys
- todo


## Consequences

The main difficulty is the remote signer setup.
As a workaround, we store unsigned events in cache (so they are queriable) and check with the broadcast monitor whether we can sign them before broadcasting.


#### Impact Radius

Accounts:
edit, set up acc

Blossom/Files:
publish blossom servers

Cashu:
Not impacted rn

Connectivity:
Not impacted

Domain verification:
Not impacted

Follows:
edit, setup

Gift Wrap
publish

Lists:
publish, edit

Metadata:
publish, edit

Nwc:
rpc method, offline zaps?

Relay sets
publish, edit

Requests:
Not impacted

User relay lists
publish, edit

Wallets
not impacted

Zaps
offline zap? (how to get invoice)



## Alternative proposals

Monolithc archetecture with a broadcast manager that encapsulates most of the retry logic attached to a state/cache class.

## Final Notes



Loading