feat(sync): track sync emit from scanner + duplicates (phase 4.d.0.3) - #206
Conversation
Closes the desktop side of the 4.d.0 sprint chain — the scanner
now pushes `entity: "track"` ops to the server for every new +
re-emit track, mirroring the wire shape that landed on
waveflow-server in 4.d.0.2.
== Wire shape ==
- `entity: "track"`, `entity_id: <file_path>` — per-library
natural identity, matches the server's ON CONFLICT key.
- `payload.library_canonical_id` — tenant scope, resolved via
`sync::canonical::ensure_local_library` inside the same tx.
- `payload.file_hash` — BLAKE3, rides as a field for the
server's liked_track / rating joins.
- `payload.file_modified` — lets a peer device that shares the
same drive skip its slow re-extract on the next scan.
- `payload.added_at` — the row's ORIGINAL import timestamp on
every re-emit (brand-new branch uses `now`, the update + skip
branches read the existing value). Prevents re-emits from
re-bumping the peer's "Recently added" order.
- Full audio metadata + the album/artist plumbing
(`album_title?`, `album_artist_name?`, `is_compilation?`,
`artists?: [String]` — desktop's `";"`-split list).
== Emit sites ==
- Scanner `commands/scan.rs`:
- "Brand-new track" branch: emit with `added_at = now`.
- "Existing track update" branch: emit with `added_at =
existing_added_at`.
- Skip-fast-path branch: emit ONLY when multi-artist
re-normalisation fired (peer devices would otherwise miss
the comma-joined → `";"`-split rewrite).
- Duplicates UI `commands/duplicates.rs::delete_tracks`: emit a
`track + delete` op per row inside the same tx as the DELETE
FROM track.
- Library `commands/library.rs::rescan_library` + `import_paths`:
inherit the scanner's emit via `scan_folder_inner`.
== Drain wake-up ==
Every command that enqueues track ops calls
`state.drain.notify()` post-commit (`scan_folder`,
`rescan_library`, `import_paths`, `delete_tracks`) — matches the
existing playlist/library convention, drain is edge-triggered.
== What's NOT emitted ==
- `is_available = 0` (file vanished mid-scan): no emit. The
track resurfaces on the next scan if the file reappears — a
delete-then-insert pair would just churn the apply pipeline.
- Cascade-driven deletes (library / library_folder removal): no
per-track emit. The server's library apply pipeline cascades
its own tracks when the parent op lands. (Follow-up: folder
removal via `remove_folder_from_library` — there's no
`library_folder` entity server-side, so a follow-up PR should
emit per-track deletes there.)
== CR pre-push findings applied ==
- **H2**: skip-fast-path now emits when multi-artist
normalisation fires (otherwise peer devices miss the
re-link).
- **H3**: `state.drain.notify()` added to `scan_folder`,
`rescan_library`, `import_paths`, and `delete_tracks` —
matches the convention every other CRUD command in this
crate follows.
- **M1**: `file_modified` added to the wire so peer-device
rescans skip the slow path.
- **M2**: `added_at` preserved on re-emit by extending the
scanner's `SELECT id, file_modified, file_hash` to also
fetch `added_at` from the existing row.
Skipped:
- **H1**: re-analysed against pre-PR — the existing batch was
already all-or-nothing via a single tx + `?`, no behavioural
change introduced by adding the emit `?`. False alarm.
- **H4** (first-scan storm follow-ups), **M3** (folder removal
gap), **M4** (helper placement TODO), **L1** (integration
test), **L2/L3** (doc polish): deferred to follow-up.
== Tests ==
- 2 unit tests in `sync::track_emit` cover the payload builder
shape (every field present) + null handling for absent
optionals (`album_title`, `track_number`, `codec`, ...).
- Build: `cargo check --workspace --all-targets` + `cargo fmt
--check` clean locally.
Signed-off-by: InstaZDLL <github.105mh@8shield.net>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughLe PR ajoute un module ChangesTrack sync emit – Phase 4.d.0.3
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Bare `tokio::spawn` panics with "there is no reactor running, must be called from the context of a Tokio 1.x runtime" when called from Tauri 2's `setup` callback (the callback runs synchronously without an ambient tokio runtime, even though Tauri uses tokio internally). `tauri::async_runtime::spawn` resolves to the runtime Tauri configures and is the supported entry point from sync hooks. Both `sync::drain::spawn` and `sync::ws::spawn` are called from `lib.rs::run`'s `setup` closure, so both hit the panic at app launch. Pre-existing latent bug — surfaced now because a fresh rebuild of the workspace went through the spawn path. Quick swap of the spawn primitive resolves it without touching the task body or the wake handle. Signed-off-by: InstaZDLL <github.105mh@8shield.net>
Summary
Third PR of the 4.d.0 sprint chain — closes the desktop side. The scanner now pushes `entity: "track"` ops to the server for every new + re-emit track, mirroring the wire shape that landed in waveflow-server #35 (phase 4.d.0.2).
Wire shape
Emit sites
Every command calls `state.drain.notify()` post-commit — matches the convention every other sync-emitting command follows.
What's NOT emitted
CR pre-push findings applied
Skipped (with rationale in commit body): H1 (false alarm — pre-PR was already all-or-nothing), H4 / M3 / M4 / L* (deferred to follow-up).
Test plan
Summary by CodeRabbit
New Features
Chores
Documentation
Tests