Files
furumi_tui/ARCHITECTURE.md
T

349 lines
15 KiB
Markdown
Raw Normal View History

2026-07-26 02:21:16 +03:00
# Furumi architecture
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Furumi is an autonomous music player that can cooperate with other Furumi
players. The architecture starts from one constraint: **a node must remain a
complete and useful player when every other node is unavailable**.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Networking therefore extends a local player instead of becoming a prerequisite
for it. There is no control plane, account service, canonical catalog, or
server-owned source of truth.
## Architectural goals
The design optimizes for five properties:
1. **Local autonomy** — importing, browsing, playback, playlists, likes, and
history work entirely on one device.
2. **No central failure domain** — discovery, catalog exchange, streaming, and
synchronization do not depend on a Furumi-operated service.
3. **Offline tolerance** — trusted devices may change state independently and
reconcile after reconnecting.
4. **Incremental federation** — one node is complete; every additional node
increases availability and the amount of discoverable music.
5. **Explicit trust boundaries** — personal-device replication and wider
music federation are different protocols with different authority.
These goals are more important than maintaining a globally identical view of
the network. Furumi prefers useful local progress and eventual reconciliation
over distributed consensus.
## The node model
Every running Furumi instance contains the same four capabilities:
2026-06-10 16:11:09 +01:00
2026-07-26 02:07:07 +03:00
```text
2026-07-26 02:21:16 +03:00
┌─────────────────────────────────────────────────────────┐
│ Furumi node │
│ │
│ Local library ── Playback engine ── TUI / media keys │
│ │ │ │
│ ├── Trusted-device replication │
│ │ │
│ └── Federation: discovery, catalog, audio │
└─────────────────────────────────────────────────────────┘
2026-06-10 16:11:09 +01:00
```
2026-07-26 02:21:16 +03:00
The local SQLite library is authoritative for that node. Network data is
merged into local views or materialized as pending remote content; it does not
replace the local database with a remote database abstraction.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
This keeps the core behavior predictable:
- disconnecting never makes the local collection unavailable;
- downloaded content can become ordinary local content;
- a peer disappearing reduces availability but does not invalidate local
state;
- nodes may join and leave without electing a leader.
## Two network layers
Furumi deliberately separates **trusted-device synchronization** from
**federated music exchange**.
### Trusted-device synchronization
This layer connects devices owned or trusted by the same user. It carries
personal state such as:
- likes and playlist operations;
- device membership and revocation;
- acknowledgements and synchronization progress;
- playback state, commands, and handoff information.
Pairing establishes the trust relationship. After that, changes are replicated
through an append-only operation log and applied to materialized local tables.
### Music federation
Federation connects independent libraries. It provides:
- decentralized discovery through the DHT;
- automatic publication of searchable local catalog metadata;
- richer artist and release catalogs fetched from peers;
- direct audio transfer when a selected track is not available locally.
Federation does not grant another peer authority over personal playlists,
likes, or device membership. A node may participate in federation without
joining another user's trusted-device group.
Keeping these layers separate prevents discovery convenience from silently
becoming a synchronization trust decision.
2026-07-28 18:35:59 +01:00
### Federation Jam control
Jam is a third, deliberately narrow authority boundary. A host creates an
opaque `frid://j/...` runtime capability and remains the only node producing
audio. Other TUI peers use a dedicated Jam ALPN to submit the same portable
playback commands used by connected-device control and receive the host's
playback snapshot. They receive queue metadata, not audio.
Jam never exchanges trusted membership, likes, playlists, or listening
history. Volume remains local. Commands carry unique IDs and are retried until
the host acknowledges them, while inactive participants expire from the
runtime session. Regenerating the capability or restarting the host invalidates
the previous link.
2026-07-26 02:21:16 +03:00
## Discovery and direct communication
Furumi separates finding content from transferring it.
2026-06-10 16:11:09 +01:00
2026-07-26 02:07:07 +03:00
```text
2026-07-26 02:21:16 +03:00
discovery plane
Local catalog ───────> DHT <─────── Other catalogs
│ peer + content identity
v
direct P2P connection
├── catalog protocol
├── audio protocol
└── device-sync protocol
2026-06-10 16:11:09 +01:00
```
2026-07-26 02:21:16 +03:00
The DHT is the distributed index. Nodes publish compact searchable
descriptions of their local library and query the network without contacting a
central search service.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Once a peer is known, communication moves to direct P2P streams provided by
iroh through `music-dht`. Furumi defines separate application protocols for
catalog requests, audio transfer, and trusted-device synchronization. This
keeps discovery traffic small and lets large or private exchanges happen only
between the participating peers.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Relay-assisted connectivity may help peers establish a route, but relays do
not become catalog authorities or application-state owners.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
## Identity and content resolution
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Network operations refer to content independently of any one library row.
Content identifiers allow a node to ask:
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
1. Is this track already present in my local library?
2. Does one of my trusted devices have it?
3. Which federation peers currently advertise it?
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Resolution follows that order conceptually: prefer a ready local source, reuse
known content where possible, and fetch from a peer only when necessary.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
A federated track can initially exist as a lightweight pending item in a queue
or playlist. When playback reaches it, Furumi resolves an available peer,
starts the transfer, and either uses the cache or imports the result into the
local library. The rest of the player continues to work with the same
`TrackItem` model, so local and remote availability do not require separate
playback systems.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
## Offline-first synchronization
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Trusted devices do not share a live database connection. Each device records
operations locally and exchanges them when connectivity returns.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
The synchronization model combines:
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
- immutable operation identifiers for deduplication;
- hybrid logical timestamps for deterministic last-writer decisions;
- materialized tables for fast UI queries;
- tombstones so deletion survives offline replicas;
- per-peer acknowledgements to determine when old tombstones can be compacted;
- snapshots to repair a peer that missed older operations.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
This is eventual consistency scoped to a trusted device group. A temporarily
offline laptop can modify playlists, another device can continue playback, and
both can later converge without a permanently available coordinator.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Membership changes use the same replicated model. Revocation is state that
must propagate and converge, not an ephemeral server-side session flag.
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
## Playback across devices
2026-06-10 16:11:09 +01:00
2026-07-26 02:21:16 +03:00
Playback has one logical state but remains physically local to the device
producing audio.
2026-07-26 02:07:07 +03:00
2026-07-26 02:21:16 +03:00
The synchronized state describes the queue, current item, position, pause
state, volume, shuffle/repeat mode, and the active playback owner. Commands are
targeted and deduplicated. Handoff transfers intent and position; the receiving
device resolves the track against its own library or federation sources before
starting its audio engine.
2026-07-26 02:07:07 +03:00
2026-07-26 02:21:16 +03:00
Active-device leases and idle timing prevent stale snapshots from immediately
taking control after a device reconnects. This provides practical coordination
without introducing a central playback arbiter.
## Local application architecture
Inside one node, Furumi uses an event-driven state machine:
```text
terminal / player / database / network / OS media events
v
AppEvent
input → Action
v
update(AppState)
optional Effect
v
asynchronous runtime work
└──────────> AppEvent
```
`AppState` is the single source of truth for the interface. The update layer
performs deterministic state transitions and requests effects; it does not
perform blocking I/O. SQLite access, imports, artwork decoding, DHT queries,
peer transfers, and audio preparation run through runtime services and report
their results as events.
This structure gives the TUI three important properties:
- rendering is a pure projection of current state;
- input behavior can be tested without starting audio or networking;
- slow peers and large imports cannot block terminal interaction.
The audio engine is similarly isolated. The application owns the logical
queue, while `player::Controller` owns rodio playback and receives explicit
commands. Prefetching prepares the next source before the current item ends.
2026-07-26 02:41:08 +03:00
## Scripted visualizations
Visualizations are an extension boundary rather than hard-coded rendering
paths. Rust owns audio sampling, script execution, validation, and terminal
drawing; Rhai scripts own the visual composition.
```text
rodio source
v
audio analyzer ──> normalized features + scope samples
v
Rhai render(input)
v
validated draw commands
v
ratatui frame buffer
```
The player analyzer derives a bounded, renderer-independent input model:
energy, bass, mid, treble, beat strength, waveform samples, playback progress,
volume, pause state, track metadata, time, and terminal dimensions. Each
script implements `render(input)` and returns declarative commands such as
clear, cell, line, rectangle, trace, and text. Scripts never receive the
ratatui frame or audio engine directly.
This command boundary is intentional:
- scripts remain independent of Rust UI internals;
- the host validates command shapes, colors, coordinates, and arrays;
- drawing is clipped to the current terminal area;
- script failures become an in-UI visualizer error instead of corrupting the
terminal or stopping playback.
Rhai files live in the user's visualization directory. The runtime discovers
them dynamically, compiles the selected script, caches its AST, and recompiles
it when the file modification time changes. A visualization can therefore be
created or edited while Furumi is running without rebuilding or restarting the
application. Bundled scripts use the same path and contract as user scripts,
so built-in and custom visualizations exercise the same runtime.
The Rhai engine is configured as a sandboxed computation environment. Module
loading through `import` and `export` is disabled, no filesystem or network API
is exposed to scripts, and execution is bounded by limits on operations, call
depth, variables, functions, expression depth, and collection/string sizes.
Only the input map, Rhai language primitives, and a small set of mathematical
helpers are available.
The sandbox protects responsiveness and keeps visualization code in its
intended role: transforming current audio features into drawing commands. It
is not a plugin mechanism for accessing the library, network, or player
controls.
2026-07-26 02:21:16 +03:00
## Persistence boundaries
Furumi stores different kinds of state according to their lifetime:
| State | Storage | Role |
| --- | --- | --- |
| Library, playlists, likes, history | SQLite | Durable local source of truth |
| Device operation log and replicas | SQLite | Offline synchronization |
| Federation catalog cache | SQLite/cache | Faster network browsing |
| Audio and artwork cache | Filesystem cache | Reusable fetched data |
| Settings, keymap, identity | Platform config/data dirs | Node configuration |
| Queue and playback snapshots | Application/device sync state | Continuity and handoff |
Caches are replaceable. The local library and device operation log are durable.
This distinction lets maintenance and recovery code discard derived network
data without risking the user's collection.
## Failure model
Expected failures are treated as ordinary state:
- a DHT query may return partial results;
- an advertised peer may be offline by the time a track is requested;
- a transfer may stop and be retried through another source;
- trusted devices may reconnect with overlapping changes;
- cached metadata may be stale;
- an audio device may disappear during playback.
The architecture avoids converting these cases into global failure. Search can
show partial data, synchronization can resume, and playback resolution can try
another source. Errors return to the application as events so the UI can expose
them without terminating the node.
## Main implementation boundaries
The source tree follows the architectural responsibilities:
- `library/` owns the local catalog and import pipeline;
- `player/` owns audio playback and analysis;
- `federation/` owns DHT-facing search, peer catalogs, and audio exchange;
- `devices.rs` owns trusted-device replication and playback coordination;
- `app/` owns state transitions and runtime orchestration;
- `ui/` renders the TUI;
- `media.rs` integrates platform media controls;
- `visualizer.rs` hosts programmable Rhai visualizations.
Dependencies should continue to point inward toward typed models and explicit
events. UI code should not own network tasks, network protocols should not
mutate UI state directly, and the local library should not depend on the
presence of federation.
## Architectural invariants
Future changes should preserve these rules:
1. A node must start and play its local library without network access.
2. No central Furumi service may become required for discovery, playback, or
trusted-device synchronization.
3. Federation membership must not imply personal-device trust.
4. Remote state must be merged or cached locally, never treated as an always
available database.
5. Network and storage work must remain outside the interactive UI path.
6. More peers should improve availability; losing peers should only reduce
remote capabilities.