feat(qconnect): advise or replay last transport intent after reconnect

Add a pure evaluateQconnectReconnectAdvisory helper that, on a
reconnecting->connected transition, replays a recently-issued idempotent intent
(play/pause/set_track) verbatim but only advises for next/previous to avoid a
double-skip; stale or non-reconnect transitions are no-ops. No invoke, fully
unit-testable.

Add the qconnect.lastActionMayNotHaveApplied i18n key to all five locales
(en/es/de/fr/pt) and emit a qconnect:resync_complete diagnostic from the backend
after the P1-8 post-reconnect resync so the frontend can run the advisory.

cc
This commit is contained in:
vicrodh committed 2026-06-01 04:16:24 -06:00
1 parent 6c48266acc
commit 0d5eb50889
8 files changed
+125

No files matched your search

+1
View File
@@ -1803,6 +1803,7 @@
},
"qconnect": {
"title": "Qobuz Connect",
"lastActionMayNotHaveApplied": "Die letzte Aktion wurde möglicherweise nicht angewendet. Bitte überprüfe den aktuellen Status.",
"statusConnected": "Verbunden",
"statusDisconnected": "Getrennt",
"turnOn": "Einschalten",
+1
View File
@@ -1803,6 +1803,7 @@
},
"qconnect": {
"title": "Qobuz Connect",
"lastActionMayNotHaveApplied": "The last action may not have applied. Please check the current state.",
"statusConnected": "Connected",
"statusDisconnected": "Disconnected",
"turnOn": "Turn On",
+1
View File
@@ -1803,6 +1803,7 @@
},
"qconnect": {
"title": "Qobuz Connect",
"lastActionMayNotHaveApplied": "Es posible que la última acción no se haya aplicado. Revisa el estado actual.",
"statusConnected": "Conectado",
"statusDisconnected": "Desconectado",
"turnOn": "Encender",
+1
View File
@@ -1803,6 +1803,7 @@
},
"qconnect": {
"title": "Qobuz Connect",
"lastActionMayNotHaveApplied": "La dernière action n'a peut-être pas été appliquée. Veuillez vérifier l'état actuel.",
"statusConnected": "Connecté",
"statusDisconnected": "Déconnecté",
"turnOn": "Activer",
+1
View File
@@ -1803,6 +1803,7 @@
},
"qconnect": {
"title": "Qobuz Connect",
"lastActionMayNotHaveApplied": "A última ação pode não ter sido aplicada. Verifique o estado atual.",
"statusConnected": "Conectado",
"statusDisconnected": "Desconectado",
"turnOn": "Ligar",
+44
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
evaluateQconnectPlaybackReportSkip,
evaluateQconnectReconnectAdvisory,
evaluateQconnectSessionPersistence,
isQconnectPeerRendererActive,
isQconnectRemoteModeActive,
@@ -246,3 +247,46 @@ describe('QConnect runtime state helpers', () => {
).toBeNull();
});
});
describe('evaluateQconnectReconnectAdvisory', () => {
const base = { prevState: 'reconnecting' as const, nextState: 'connected' as const, now: 1000 };
it('replays an idempotent play intent issued recently', () => {
const r = evaluateQconnectReconnectAdvisory({
...base,
lastIntent: { kind: 'play', issuedAt: 900 }
});
expect(r.shouldReplay).toBe(true);
expect(r.shouldToast).toBe(false);
});
it('only advises (no replay) for next, to avoid double-skip', () => {
const r = evaluateQconnectReconnectAdvisory({
...base,
lastIntent: { kind: 'next', issuedAt: 900 }
});
expect(r.shouldReplay).toBe(false);
expect(r.shouldToast).toBe(true);
expect(r.toastKey).toBe('qconnect.lastActionMayNotHaveApplied');
});
it('does nothing when the intent is stale', () => {
const r = evaluateQconnectReconnectAdvisory({
...base,
now: 100_000,
lastIntent: { kind: 'play', issuedAt: 900 }
});
expect(r.shouldReplay).toBe(false);
expect(r.shouldToast).toBe(false);
});
it('does nothing outside a reconnecting->connected transition', () => {
const r = evaluateQconnectReconnectAdvisory({
prevState: 'connected',
nextState: 'connected',
now: 1000,
lastIntent: { kind: 'play', issuedAt: 900 }
});
expect(r.shouldReplay).toBe(false);
});
});
+67
View File
@@ -334,3 +334,70 @@ export async function toggleQconnectConnection(toggleOn: boolean): Promise<void>
await invoke('v2_qconnect_connect', { options: null });
}
/**
* A user-issued remote transport intent we may want to replay after a
* reconnect (P1-11).
*/
export type QconnectTransportIntent = {
kind: 'play' | 'pause' | 'next' | 'previous' | 'set_track';
trackId?: number | null;
/** Epoch ms when the intent was issued. */
issuedAt: number;
};
export type QconnectReconnectAdvisory = {
shouldReplay: boolean;
shouldToast: boolean;
toastKey: string | null;
};
/**
* Pure decision helper (P1-11): on a `reconnecting -> connected` transition,
* decide whether to replay the last user transport intent or merely advise.
*
* - Idempotent intents (`play` / `pause` / `set_track`) issued recently are
* safe to replay verbatim.
* - `next` / `previous` are NOT replayed — the action may already have applied
* on the renderer before the drop, so replaying risks a double-skip; we only
* surface an advisory toast instead.
* - Stale intents (older than `staleAfterMs`) and non-reconnect transitions are
* no-ops.
*
* No side effects, no `invoke` — fully unit-testable.
*/
export function evaluateQconnectReconnectAdvisory(params: {
prevState: QconnectLifecycleState;
nextState: QconnectLifecycleState;
lastIntent: QconnectTransportIntent | null;
now: number;
staleAfterMs?: number;
}): QconnectReconnectAdvisory {
const { prevState, nextState, lastIntent, now } = params;
const staleAfterMs = params.staleAfterMs ?? 15_000;
const noop: QconnectReconnectAdvisory = {
shouldReplay: false,
shouldToast: false,
toastKey: null
};
const reconnected = prevState === 'reconnecting' && nextState === 'connected';
if (!reconnected || !lastIntent) {
return noop;
}
if (now - lastIntent.issuedAt > staleAfterMs) {
return noop;
}
const idempotent =
lastIntent.kind === 'play' ||
lastIntent.kind === 'pause' ||
lastIntent.kind === 'set_track';
if (idempotent) {
return { shouldReplay: true, shouldToast: false, toastKey: null };
}
// next/previous: replaying risks a double-skip -> only advise.
return {
shouldReplay: false,
shouldToast: true,
toastKey: 'qconnect.lastActionMayNotHaveApplied'
};
}