Skip to content

feat(sync): track sync emit from scanner + duplicates (phase 4.d.0.3) - #206

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/4-d-0-3-track-sync-emit
Jun 8, 2026
Merged

feat(sync): track sync emit from scanner + duplicates (phase 4.d.0.3)#206
InstaZDLL merged 2 commits into
mainfrom
feat/4-d-0-3-track-sync-emit

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jun 8, 2026

Copy link
Copy Markdown
Owner

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

  • `entity: "track"`, `entity_id: <file_path>` — per-library natural identity.
  • `payload.library_canonical_id` — tenant scope, resolved inside the same tx via `sync::canonical::ensure_local_library`.
  • `payload.file_hash` — BLAKE3, rides as a field for the server's liked_track / rating joins.
  • `payload.file_modified` — lets a peer device sharing 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`, 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 (`delete_tracks`): emit a `track + delete` op per row inside the same tx as the DELETE.
  • Library rescan + import_paths: inherit the scanner's emit.

Every command calls `state.drain.notify()` post-commit — matches the convention every other sync-emitting command follows.

What's NOT emitted

  • `is_available = 0` (file vanished mid-scan): no emit. The track resurfaces on re-scan if the file reappears.
  • Cascade-driven deletes (library removal): no per-track emit. The server's library apply pipeline cascades. Folder removal is a known gap — flagged as a follow-up.

CR pre-push findings applied

  • H2: skip-fast-path now emits when multi-artist normalisation fires.
  • H3: `state.drain.notify()` added to all 4 enqueueing commands.
  • M1: `file_modified` added to the wire.
  • M2: `added_at` preserved on re-emit (extended the existing track SELECT to fetch it).

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

  • `cargo check --manifest-path src-tauri/Cargo.toml --workspace --all-targets` clean
  • `cargo fmt --manifest-path src-tauri/Cargo.toml --all --check` clean
  • `cargo test -p waveflow --lib sync::track_emit::` — 2 unit tests pass (payload builder + null handling).
  • Smoke test: scan a folder with sync enabled → verify `sync_pending_op` rows show up with `entity = 'track'` and the right payload shape.
  • Smoke test: edit a tag → re-scan → verify the re-emit has `added_at` preserved from the original row.
  • Smoke test: duplicate-delete a track → verify a `track + delete` op lands in `sync_pending_op`.

Summary by CodeRabbit

  • New Features

    • Renforcement de la synchronisation des pistes : envoi fiable des insertions et suppressions de pistes au serveur, préservation de la date d'ajout lors de ré-émissions et gestion correcte des artistes multiples.
  • Chores

    • Notifications du mécanisme de vidage (drain) déclenchées plus tôt pour accélérer l'envoi des opérations.
    • Ajustement du lancement des tâches pour compatibilité runtime.
  • Documentation

    • Ajout d'une règle décrivant le format et le comportement des opérations de synchronisation.
  • Tests

    • Tests ajoutés pour valider la structure des payloads envoyés.

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>
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets type: feat New feature size: l 200-500 lines labels Jun 8, 2026
@InstaZDLL InstaZDLL self-assigned this Jun 8, 2026
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 061bf0fa-d883-4ffd-83aa-1d582edd428f

📥 Commits

Reviewing files that changed from the base of the PR and between 94e4fd9 and 7925e64.

📒 Files selected for processing (2)
  • src-tauri/crates/app/src/sync/drain.rs
  • src-tauri/crates/app/src/sync/ws.rs

📝 Walkthrough

Walkthrough

Le PR ajoute un module track_emit qui construit et enfile les payloads JSON d'opérations track (insert/delete) dans l'outbox transactionnelle, intègre ces émissions au scanner et à la suppression de pistes, et ajoute des notifications state.drain.notify() pour accélérer l'envoi.

Changes

Track sync emit – Phase 4.d.0.3

Layer / File(s) Summary
Wire et sérialisation du payload
src-tauri/crates/app/src/sync/track_emit.rs, src-tauri/crates/app/src/sync/mod.rs
TrackInsertWire<'a> capture les champs de piste (obligatoires et optionnels audio/album), build_track_insert_payload sérialise en JSON avec library_canonical_id injecté. Module exporté publiquement.
Opérations outbox transactionnelles
src-tauri/crates/app/src/sync/track_emit.rs
emit_track_insert_in_tx et emit_track_delete_in_tx enfilent les ops dans la table outbox au sein d'une transaction SQLite. Tests valident sérialisation complète et optionnels en null.
Intégration scanner – émission sur scan
src-tauri/crates/app/src/commands/scan.rs
Récupère added_at pour les réémissions, ajoute booléen multi_artist_renormalised pour détecter renormalisation, crée helper emit_track_insert_from_extracted convertissant ExtractedFile en TrackInsertWire, émet sur renormalisation/update/nouveau, signale drain après.
Intégration deletion – émission et drain
src-tauri/crates/app/src/commands/duplicates.rs
delete_tracks récupère library_id/file_path avant suppression, émet via emit_track_delete_in_tx dans la même transaction, signale drain post-commit pour accélérer envoi.
Signaux drain après opérations bibliothèque
src-tauri/crates/app/src/commands/library.rs
Ajoute state.drain.notify() après rescan_library et import_paths pour réveiller immédiatement le drain face aux pistes importées/rescannées.
Spécification du protocole track sync
CLAUDE.md
Documente règle "Track sync emit – Phase 4.d.0.3" : payload shape, identité via entity_id = file_path, ré-émission avec added_at préservé, multi_artist_renormalised skip-fast-path, règles delete, comportement is_available, convention drain post-commit.
Adaptation runtime Tauri pour tâches async
src-tauri/crates/app/src/sync/drain.rs, src-tauri/crates/app/src/sync/ws.rs
Remplacement de tokio::spawn par tauri::async_runtime::spawn dans les spawns du drain et du WS, avec commentaires expliquant la contrainte du hook setup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#196: Lié — implémentation de la tâche sync::drain consommant l'outbox, qui sera alimentée par les ops track émises ici.

Suggested labels

size: xl

"Le scanner chante et prépare le flux,
Un payload JSON, clair et sans repli,
Le drain se réveille, pousse le flux à bout,
Les pistes voyagent, alignées en un coup,
Transaction ferme, le serveur attend, tranquille."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit la convention Conventional Commits avec un scope kebab-case approprié et décrit précisément la principale modification : l'ajout de l'émission des opérations de pistes de synchronisation depuis le scanner et les duplicates.
Description check ✅ Passed La description est complète et détaillée : elle explique le contexte (phase 4.d.0.3), la forme du payload, les sites d'émission, les cas non émis, les retours de la revue et un plan de test exhaustif avec résultats positifs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/4-d-0-3-track-sync-emit

Comment @coderabbitai help to get the list of available commands and usage tips.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets size: l 200-500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant