mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
fix(playback): re-evaluate the wake lock after a rejection masked a state change
In the PWA path a hidden-visible round-trip (or pause/resume) while wakeLock.request() was pending got swallowed by the in-flight guard; if that request then rejected, only the flag was cleared and a continuously playing visible video sat without a wake lock until the next unrelated event. State changes arriving mid-flight now queue one re-evaluation on rejection; permanent denials still don't loop because nothing queues a retry without a fresh interleaved change. Review finding by Codex. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
64f150c91c
commit
919bfabf72
2 files changed
+54
-3
No files matched your search
@@ -248,6 +248,38 @@ describe('PlaybackKeepAwakeService', () => {
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('re-evaluates after a rejection that masked a state change', async () => {
|
||||
let rejectRequest: ((reason: Error) => void) | undefined;
|
||||
request.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectRequest = reject;
|
||||
})
|
||||
);
|
||||
|
||||
video.dispatchEvent(new Event('playing'));
|
||||
// Hidden→visible round-trip while request() is still pending:
|
||||
// both sync() calls are swallowed by the in-flight guard, and
|
||||
// the pending request rejects because of the hidden moment.
|
||||
setVisibility('hidden');
|
||||
setVisibility('visible');
|
||||
rejectRequest?.(new Error('document was hidden'));
|
||||
await flush();
|
||||
|
||||
// The masked state change must trigger a fresh request — the
|
||||
// video is still playing in a visible document.
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not retry a plain rejection with no interleaved change', async () => {
|
||||
request.mockRejectedValueOnce(new Error('denied'));
|
||||
|
||||
video.dispatchEvent(new Event('playing'));
|
||||
await flush();
|
||||
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('survives a denied wake lock request', async () => {
|
||||
request.mockRejectedValueOnce(new Error('denied'));
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export class PlaybackKeepAwakeService {
|
||||
private lastSentToBridge: boolean | null = null;
|
||||
private wakeLock: WakeLockSentinelLike | null = null;
|
||||
private wakeLockRequestInFlight = false;
|
||||
private wakeLockRetryQueued = false;
|
||||
|
||||
private readonly onPlaying = (event: Event) => {
|
||||
const target = event.target;
|
||||
@@ -165,7 +166,15 @@ export class PlaybackKeepAwakeService {
|
||||
}
|
||||
|
||||
private acquireWakeLock(): void {
|
||||
if (this.wakeLock || this.wakeLockRequestInFlight) {
|
||||
if (this.wakeLock) {
|
||||
return;
|
||||
}
|
||||
if (this.wakeLockRequestInFlight) {
|
||||
// A state change arrived while request() is still pending (e.g.
|
||||
// hidden→visible round-trip). The pending request may reject
|
||||
// because of the moment it was processed in, so remember to
|
||||
// re-evaluate once it settles instead of dropping this signal.
|
||||
this.wakeLockRetryQueued = true;
|
||||
return;
|
||||
}
|
||||
const wakeLock = (navigator as WakeLockNavigator).wakeLock;
|
||||
@@ -177,6 +186,9 @@ export class PlaybackKeepAwakeService {
|
||||
.request('screen')
|
||||
.then((sentinel) => {
|
||||
this.wakeLockRequestInFlight = false;
|
||||
// The resolve path re-reads current state below, which
|
||||
// covers whatever change queued the retry.
|
||||
this.wakeLockRetryQueued = false;
|
||||
// The browser releases the sentinel on its own when the page
|
||||
// hides; forget it so the next sync() can re-request.
|
||||
sentinel.addEventListener?.('release', () => {
|
||||
@@ -191,9 +203,16 @@ export class PlaybackKeepAwakeService {
|
||||
this.wakeLock = sentinel;
|
||||
})
|
||||
.catch(() => {
|
||||
// Denied (battery saver, hidden document, …) — a later
|
||||
// `playing` or visibility event retries via sync().
|
||||
this.wakeLockRequestInFlight = false;
|
||||
if (this.wakeLockRetryQueued) {
|
||||
// A state change was masked by the in-flight request —
|
||||
// re-evaluate now. Permanent denials don't loop: without
|
||||
// a fresh interleaved change nothing queues a retry.
|
||||
this.wakeLockRetryQueued = false;
|
||||
this.sync();
|
||||
}
|
||||
// Otherwise: denied (battery saver, hidden document, …) — a
|
||||
// later `playing` or visibility event retries via sync().
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user