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:
8 files changed
+125
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user