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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ jobs:
--no-default-features
--features postgres

- name: cargo check + test (sync_v2 — off by default, so unchecked otherwise)
# `crate::remote` (RFC-005) lives behind an off-by-default feature
# while its slices land, which means the step above never compiles
# a line of it. Without this step the whole tree would rot
# silently until the day it is switched on. Linux only: it adds no
# platform-specific code, and the Windows slot cannot run the app
# crate's test binary anyway (see below).
if: runner.os == 'Linux'
run: >-
cargo test
--manifest-path src-tauri/Cargo.toml
-p waveflow
--features sync_v2
--all-targets

- name: cargo test (full workspace)
if: runner.os == 'Linux'
run: cargo test --manifest-path src-tauri/Cargo.toml --workspace
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Inside `crates/app/src/`:

- **`commands/`** — one module per domain (`library`, `playlist`, `smart_playlists`, `track`, `browse`, `player`, `scan`, `edit`, `profile`, `analysis`, `deezer`, `similar`, `lyrics`, `stats`, `wrapped`, `maintenance`, `radio`, `duplicates`, `preferences`, `plugins`, `canvas`, …), all registered in `lib.rs::generate_handler![]`. CRUD delegates to `waveflow_core::repository::sqlite::*`; IPC + state + filesystem + emit glue stays in the command.
- **`audio/`** — 3-thread lock-free engine: `decoder.rs` (symphonia + rubato), `output.rs` (cpal callback on its own thread, SPSC `rtrb` ring), `state.rs` (`SharedPlayback` atomics), `analytics.rs`, `crossfade.rs`, `eq.rs`, `spectrum.rs`, `wasapi_exclusive.rs`. Topology: [`docs/architecture/audio.md`](docs/architecture/audio.md).
- **`dlna/`** (axum + SSDP, opt-in) · **`mpd/`** (TCP MPD protocol, opt-in) · **`media_controls.rs`** (souvlaki → SMTC / MPRIS / MediaRemote) · **`discord_presence.rs`** · **`queue.rs`** · **`player_actions.rs`** (shared control sequence) · **`sync/`** (outbound op emit) · **`backup.rs`** · **`db/`** (pool wiring + `migration_heal`).
- **`dlna/`** (axum + SSDP, opt-in) · **`mpd/`** (TCP MPD protocol, opt-in) · **`media_controls.rs`** (souvlaki → SMTC / MPRIS / MediaRemote) · **`discord_presence.rs`** · **`queue.rs`** · **`player_actions.rs`** (shared control sequence) · **`remote/`** (remote source + sync v2, feature `sync_v2`) · **`sync/`** (retired v1 protocol, feature `sync_v1` — both features off by default) · **`backup.rs`** · **`db/`** (pool wiring + `migration_heal`).
- **Scanner** — the orchestrator `scan_folder_inner` stays app-side (it emits `scan:progress`); every pure helper lives in `waveflow_core::scanner::{extract, upserts}`.
- **Database** — per-profile SQLite via sqlx + a global `app.db` for the profile list and app-wide settings. Migrations at `src-tauri/migrations/{app,profile}/`, compiled in via `sqlx::migrate!`. Layout: [`docs/architecture/storage.md`](docs/architecture/storage.md).

Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ Long-form design documents that lock in cross-cutting architectural decisions be
| ------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| [RFC-001 — WaveFlow Server](rfcs/RFC-001-waveflow-server.md) | Accepted | Server, web, auth, sync, streaming, Phase 1 delivery plan |
| [RFC-002 — Plugin SDK](rfcs/RFC-002-plugin-sdk.md) | Draft | WASM Component Model plugins for sources / metadata / UI, sideload distribution, desktop + server parity |
| [RFC-003 — Sync architecture v2](rfcs/RFC-003-sync-architecture.md) | Draft | Backfill, HLC ordering, per-entity CRDT conflict resolution, status UI. Supersedes RFC-001 §1.f. |
| [RFC-003 — Sync architecture v2](rfcs/RFC-003-sync-architecture.md) | Superseded by RFC-005 | Backfill, HLC ordering, per-entity CRDT conflict resolution. **Not** the server's RFC-003 — see [RFC-005](rfcs/RFC-005-remote-source-and-sync-v2.md#the-rfc-003-naming-trap). |
| [RFC-004 — Community-DB](rfcs/RFC-004-community-database.md) | Draft | Opt-in shared metadata pool (lyrics, bios, BPM, etc.), LRCLIB pattern. Schema + endpoints + privacy. |
| [RFC-005 — Remote source + sync v2](rfcs/RFC-005-remote-source-and-sync-v2.md) | Accepted | The server catalogue as a separate remote source, `MusicServer` / `SyncProvider` seam, PKCE, journal-based user-data sync. |

## Contributing

Expand Down
14 changes: 14 additions & 0 deletions docs/architecture/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ Default it into the overflow ("⋯") menu via [`MoreActionsMenu`](../../src/comp

Every outbound HTTP path (Deezer, Last.fm, similar, LRCLIB, the plugin registry) checks `offline::is_offline()` first and short-circuits to an empty payload or the cache. Persisted in `app_setting['network.offline_mode']`. **Treat new HTTP code paths the same way.**

### Remote user data never lands in the local tables

[RFC-005](../rfcs/RFC-005-remote-source-and-sync-v2.md). Synchronized state describes the **server's** playlists, favourites, ratings, history, queue and shares, and those reference the **server's** tracks — which have no local counterpart. Writing them into `playlist` / `liked_track` / `track.rating` would leave two options, both wrong: fabricate local track rows for content that only exists on the server, or silently drop every entry. The first corrupts the library, the second makes sync look broken while reporting success.

The projection therefore lives in its own `remote_*` tables and is **reconstructible**: dropping it and re-fetching `GET /api/v2/sync/snapshot` is always a valid recovery, and is what the apply path does when it meets a known event it cannot apply. `remote_mutation` is the one exception — it holds writes the server has not seen yet, so it must survive a projection reset.

Matching a local file to a server track is deliberately out of scope and needs its own RFC.

**Two RFCs are numbered 003.** The desktop's [RFC-003](../rfcs/RFC-003-sync-architecture.md) (hybrid logical clocks, superseded) has nothing to do with the server's RFC-003 (sync v2, accepted). Any instruction naming "RFC-003" must name the repository too, or it will be read as the wrong document. On the desktop side the accepted design is **RFC-005**.

### The three sections below describe the retired v1 protocol

They are accurate for `crate::sync` under the `sync_v1` feature, which is off by default and talks to a server generation that no longer exists. They stay until the v2 snapshot bootstrap is proven, because they are the only documented recovery path from a divergence. **Do not use them as a model for new work** — see the section above.

### Outbound `playlist + field: "tracks"` ops carry a snapshot map

Phase 1.j.b. Every command in [`commands/playlist.rs`](../../src-tauri/crates/app/src/commands/playlist.rs) that inserts tracks (`add_track_to_playlist`, `add_tracks_to_playlist`, `add_source_to_playlist`) calls [`sync::track_snapshots::build_snapshots(conn, &track_ids)`](../../src-tauri/crates/app/src/sync/track_snapshots.rs) inside the same SQLite transaction and folds the result into the outbound payload as `snapshots: { "<id_str>": { title, artist?, duration_ms? } }`.
Expand Down
12 changes: 11 additions & 1 deletion docs/rfcs/RFC-003-sync-architecture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# RFC-003 — Sync architecture v2

- **Status**: Draft
> **Superseded on 2026-08-10 by [RFC-005](RFC-005-remote-source-and-sync-v2.md).**
> The server it was designed against no longer exists: hybrid logical clocks,
> per-entity CRDT arbitration and digest reconciliation are all dropped in favour
> of a server-authoritative ordered journal. Kept for the problem statement
> below, which is still an accurate account of why the v1 protocol failed.
>
> **This is not the server's RFC-003.** `waveflow-server` has its own document
> with that number, describing the accepted v2 protocol. Any instruction
> mentioning "RFC-003" must name the repository.

- **Status**: Superseded by RFC-005
- **Date**: 2026-06-12
- **Authors**: @InstaZDLL
- **Supersedes**: RFC-001 §Phase 1.f sync (the practical parts — apply pipeline + ops log stay; semantics and protocol are redesigned).
Expand Down
Loading
Loading