The renderer-engine seam: a 16-method trait both frontends implement with
zero-cost forwards to their QbzCore/Player, so the hard-won renderer
orchestration (echo-seek rejection, cursor align, queue materialize, shuffle
deferral, load dedup window) can move UP into qconnect-app and be written once
against the trait instead of re-derived per frontend (slice 6, Phase T 1-2).
- Adds qbz-player as a qconnect-app dep, for PlaybackState (read by the echo
gate + should_reload; it is not in qbz-models and not re-exported). qbz-player
does NOT depend on qconnect-app -> no dependency cycle.
- The two protected bit-perfect seams (play_data / play_streaming_dynamic) are
reached ONLY through the high-level start_track_stream method; BufferWriter +
reqwest + the detached feeder spawn stay impl-side and never cross the crate
boundary. play_data / play_streaming_dynamic are never modified.
No impls, no logic moved yet -- pure foundation. qconnect-app 38 green, src-tauri
cargo check green.
cc
Move the spawned transport event-loop body out of the Tauri adapter into the
shared QconnectApp::run_session_loop, so both frontends inherit takeover
arbitration / deferred renderer-join / reconnect resync / Lagged re-ask. The
adapter-coupled seams go through a new SessionLoopHost trait:
- update_lifecycle -- Tauri gates on a live runtime + dedups, emits via sink
- bootstrap_after_reconnect
- deferred_renderer_join -- R2: the CoreBridge track-duration read stays adapter-side
- on_reconnect_exhausted -- R3: Exhausted + last_error + runtime=None teardown +
the raw qconnect:status_changed emit; returns whether the loop should break
- on_loop_error -- the raw qconnect:error channel
TauriSessionLoopHost implements it over the existing service handles; connect()
is reduced to: build host -> spawn(app.run_session_loop(host, idle_retry)). The
diagnostic/lifecycle/resync emits still route through self.sink, so the Svelte
wire surface is byte-identical. hex_preview + queue_payload_track_preview + the
re-ask budget const relocate with the loop; the dead Tauri copies are removed.
R3 (QconnectRuntime/JoinHandle ownership, #358 latch, disconnect teardown) stays
adapter-side.
qconnect-app 38 green, src-tauri qconnect 42 green (the byte-identity fence).
cc
Relocate the transport-loop helpers out of the Tauri adapter into QconnectApp
methods + qconnect-app free items, so the shared session loop (slice 5.2) and a
future Slint adapter use one definition:
- QconnectApp::capture_session_state_takeover_input (locks self.sync)
- QconnectApp::clear_pending_if_matches (locks self.state)
- QconnectApp::send_set_active_renderer -- inline {"renderer_id": i32} payload,
byte-identical to the prior QconnectSetActiveRendererRequest serialization
- QconnectApp::ask_for_active_renderer_state / QconnectApp::ask_for_queue_state
- SessionStateTakeoverInput struct + queue_payload_track_preview (pure free fn)
Deleted the service.rs free fns (capture_session_state_takeover_input,
send_set_active_renderer_via_app, ask_for_active_renderer_state,
ask_for_queue_state, queue_payload_track_preview, clear_pending_if_matches) and
the SessionStateTakeoverInput struct; rewired every call site (the loop +
deferred_renderer_join + the reconnect/lagged paths) to the methods. The request
structs serialize identically to the inline json!, so the wire is unchanged.
qconnect-app 38 green, src-tauri qconnect 42 green.
cc
Relocate the session-management apply + renderer-liveness logic into
qconnect-app so both the Tauri and the future Slint adapter share it:
- apply_session_management_event(message_type, payload, identity)
-> SessionApplyOutcome: the locked critical section, byte-identical,
now locking self.sync. Local identity is injected as data (LocalIdentity)
so the crate stays frontend-agnostic (refresh_local_renderer_id takes it
instead of resolving device-info itself). The CoreBridge post-lock work is
RETURNED via the outcome rather than run inline -- materialization stays
adapter-side (renderer-engine seam, slice 6). Early returns yield a default
outcome, exactly as the prior `return;` skipped the post-lock block.
- arm_renderer_watchdog / run_renderer_watchdog: the generation-guarded 12s
liveness task, modeled on watch_pending_action_timeout (tokio::spawn + sleep
+ re-lock + 4-field epoch revalidation).
- freeze_active_renderer_projection: cached playing_state -> UNKNOWN, then
emit the freeze event through the sink (dedicated channel + blanket event
preserved).
- ensure_session_renderer_state + sync_session_renderer_active_flags moved to
qconnect-app (pure mutators over the relocated accumulator).
Adds 5 tests (SESSION_STATE outcome, watchdog arm/fire-after-12s/epoch-disarm,
ACTIVE_DISCONNECTED freeze) using start_paused + advance. The Tauri sink keeps
its own live copy this commit; slice 2+4.5 rewires it to delegate and deletes
the duplicate. qconnect-app 38 green, src-tauri qconnect 42 green.
cc
Move the cross-submodule remote-sync accumulator into the new
frontend-agnostic qconnect_app::sync_state module. All field types are
now frontend-agnostic (primitives / qconnect-core / qconnect-app), so
the struct moves cleanly with pub fields; the Tauri qconnect module
re-exports it so existing super::QconnectRemoteSyncState references and
the Arc<Mutex<>> creation/threading in service.rs compile unchanged.
It stays ONE struct under ONE lock by design: capture_session_state_takeover_input
reads session topology together with last_renderer_playing_state, and the
materialize path reads last_renderer_* with last_applied_queue_state, all
atomically. Splitting them would tear the takeover decision, so the whole
accumulator relocates as a unit (single-lock locking is byte-identical).
Two test-only re-exports (find_unique_renderer_id, QconnectSessionRendererState)
are gated #[cfg(test)] since their non-test consumers moved to qconnect-app.
qconnect-app + src-tauri qconnect tests green (33 / 42), no new warnings.
cc
Relocate the pure session mutators (refresh_local_renderer_id,
is_peer/local_renderer_active, find_unique_renderer_id,
normalize_active_renderer_id, renderer_allows_remote_volume) and the
QconnectFileAudioQualitySnapshot type into qconnect-app::session.
refresh_local_renderer_id now takes an injected LocalIdentity (device
uuid + device-info) so qconnect-app stays free of the persistence /
transport crates (ADR-006). A thin Tauri wrapper resolves the identity
from default_qconnect_device_info() and delegates, keeping the three
event_sink call sites and the existing tests unchanged. The eager
device-info build is idempotent and side-effect-free, so renderer-id
resolution is byte-identical.
qconnect-app + src-tauri qconnect tests green (33 / 42).
cc
Move QconnectSessionState, QconnectRendererInfo, and
QconnectSessionRendererState from the Tauri adapter into the
frontend-agnostic qconnect-app::session module, re-exporting them from
the Tauri qconnect module so all existing super:: references and the
command surface compile unchanged. Field visibility widened to pub for
cross-crate access; serialized shape is unchanged.
First step of hoisting the QConnect session/liveness layer up so both
the Tauri adapter and a future Slint adapter share one session model.
qconnect crates + src-tauri qconnect tests green.
cc
Add five QconnectAppEvent variants (RendererUnreachable, RendererDisconnected,
ResyncComplete, LifecycleChanged, Diagnostic) and a Tauri mapper so the
session/liveness/diagnostic signals flow through the QconnectEventSink trait
instead of raw app_handle.emit calls. Move QconnectLifecycleState into
qconnect-app (pure data); Tauri types.rs re-exports it for the command surface.
No logic moves: the session logic still lives Tauri-side but now calls
sink.on_event(variant) at each rewired emit site. The sink mapper turns each
variant back into the exact same qconnect:* channel + payload, so the Svelte
wire surface is byte-identical. Each dedicated channel still fires exactly once
per occurrence (raw emits are replaced, not duplicated). The blanket
qconnect:event emit now also carries the new variants, which is additive and
ignored by the Svelte listener (pattern-matches known keys only).
Rewired emit sites:
- event_sink.rs freeze helper now takes/emits the freeze variant
(RendererUnreachable/RendererDisconnected); the mapper emits the dedicated
channel. State mutation precedes on_event and the variant arms only emit, so
there is no re-entrancy/recursion into the watchdog/session dispatch.
- service.rs resync-complete -> ResyncComplete; inbound_queue_event/cloud_error/
max_reconnect -> Diagnostic; update_lifecycle_state_if_running -> LifecycleChanged.
Tests: add qconnect-app unit tests asserting the new variants flow through a
sink and that QconnectLifecycleState serializes snake_case (matching the wire
payload). crates green (qconnect-app 33 passed); src-tauri cargo test qconnect
42 passed.
cc
Move the frontend-agnostic pure functions and enums out of the Tauri
qconnect adapter into the shared crates, with zero behavior change. The
Tauri side keeps thin re-exports so all existing call sites compile
unchanged.
Into qconnect-app (new `session` module):
- should_arm_renderer_watchdog
- compute_connection_state + ServerActiveState + ConnectionDecision
- deferred_join_reason, should_reask_queue_state
- RendererStatus + from_wire
- quality_from_max_audio_quality
- QCONNECT_RENDERER_LOST_TIMEOUT_MS + JoinSession reason consts
Into qconnect-core (next to evaluate_remote_queue_admission):
- validate_track_origins_for_admission (pure over TrackOrigin); the
Tauri command wrapper stays thin and only maps wire origins to core
origins.
The 6 pure-fn unit tests move into the destination crates' test
modules. R5 honored: compute_connection_state matrix cells are relocated
verbatim, not changed.
cc
Pure enum + decision function for QConnect auto-connect-on-startup.
Persistence and CLI parsing stay in the Tauri adapter; this crate
remains UI-agnostic.
cc
Add missing SRVR_CTRL session management message types (81-87, 97-101)
that were completely absent from the protobuf definitions. Without these,
prost silently discarded all session topology messages from the server.
- Add 12 new proto structs for session/renderer/volume/quality events
- Add decoder branches and map functions for all new message types
- Add SessionManagementEvent variant to bypass queue reducer
- Add v2_qconnect_report_playback_state and v2_qconnect_report_volume
- Add comprehensive logging for inbound message diagnostics