mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
fix(sources): own Undo timer, per-connection health history, race-free perf waits
Local review round 2 (Codex + Greptile): - The delete committed whenever the shared snack bar was dismissed, so another message (a refresh finishing elsewhere) cut the four-second Undo window short and deleted the source early. The window is now the service's own timer; only the toast's close button (commitNow) or a second delete commits sooner. - The health history was keyed by playlist ID only, so an edited address or account inherited the old connection's result, failing run included. Entries now keep a 32-bit FNV-1a fingerprint of the source health key (no URL or credential); a result for another connection starts over and a stale entry no longer shows. - The M3U refresh-cancellation probe still queried the removed row's .cancel-btn, .busy-state__message and .action-spinner, so its cancel never fired; it now uses the source row's selectors (smoke run passes, cancelEffect=true). - The Xtream delete benchmark waited to catch the "Removing…" row coming back before it left, which a fast delete could finish before the wait began. The driver now arms a delete-playlist operation window before the commit click and ends on its completed event and the absent row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
366745f16d
commit
123bc170e9
20 files changed
+403
-97
No files matched your search
@@ -84,6 +84,24 @@ export function sourceHealthKey(p: Partial<PlaylistMeta>): string {
|
||||
p.stalkerSignature2,
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* A 32-bit FNV-1a fingerprint of a source health key: enough to tell that a
|
||||
* source now points somewhere else, without storing where (or with which
|
||||
* credentials). Kept with the persisted health history.
|
||||
*/
|
||||
export function fingerprintSourceHealthKey(key: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let index = 0; index < key.length; index += 1) {
|
||||
hash ^= key.charCodeAt(index);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
return (hash >>> 0).toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
export function sourceHealthFingerprint(p: Partial<PlaylistMeta>): string {
|
||||
return fingerprintSourceHealthKey(sourceHealthKey(p));
|
||||
}
|
||||
|
||||
export function accountHealth(
|
||||
status: unknown,
|
||||
expiration?: number,
|
||||
|
||||
@@ -73,11 +73,37 @@ describe('nextSourceHealthHistory', () => {
|
||||
});
|
||||
|
||||
it('keeps the cause of a failure, never of a success', () => {
|
||||
const failed = nextSourceHealthHistory(undefined, 'bad', 100, 'auth');
|
||||
const failed = nextSourceHealthHistory(undefined, 'bad', 100, {
|
||||
reason: 'auth',
|
||||
});
|
||||
expect(failed?.reason).toBe('auth');
|
||||
expect(nextSourceHealthHistory(failed, 'ok', 200, 'available')).toEqual(
|
||||
{ state: 'ok', checkedAt: 200, lastSuccessAt: 200 }
|
||||
);
|
||||
expect(
|
||||
nextSourceHealthHistory(failed, 'ok', 200, { reason: 'available' })
|
||||
).toEqual({ state: 'ok', checkedAt: 200, lastSuccessAt: 200 });
|
||||
});
|
||||
|
||||
it('starts over for an edited connection, keeps the run for the same one', () => {
|
||||
const failed = nextSourceHealthHistory(undefined, 'bad', 100, {
|
||||
connection: 'old',
|
||||
});
|
||||
expect(
|
||||
nextSourceHealthHistory(failed, 'bad', 900, { connection: 'old' })
|
||||
?.failingSince
|
||||
).toBe(100);
|
||||
expect(
|
||||
nextSourceHealthHistory(failed, 'bad', 900, { connection: 'new' })
|
||||
).toEqual({
|
||||
state: 'bad',
|
||||
checkedAt: 900,
|
||||
failingSince: 900,
|
||||
connection: 'new',
|
||||
});
|
||||
// An inconclusive check of the new connection keeps nothing old.
|
||||
expect(
|
||||
nextSourceHealthHistory(failed, 'unknown', 900, {
|
||||
connection: 'new',
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ends the run when the source answers again', () => {
|
||||
|
||||
@@ -106,6 +106,12 @@ export interface SourceHealthHistoryEntry {
|
||||
lastSuccessAt?: number;
|
||||
/** The cause of the last failed check; absent while `ok`. */
|
||||
reason?: SourceHealthReason;
|
||||
/**
|
||||
* Fingerprint of the connection that was checked
|
||||
* (`sourceHealthFingerprint`): a result for an edited address or account
|
||||
* is not this source's any more.
|
||||
*/
|
||||
connection?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,16 +121,23 @@ export interface SourceHealthHistoryEntry {
|
||||
* every later failed check.
|
||||
*/
|
||||
export function nextSourceHealthHistory(
|
||||
previous: SourceHealthHistoryEntry | undefined,
|
||||
kept: SourceHealthHistoryEntry | undefined,
|
||||
reachability: SourceReachability,
|
||||
checkedAt: number,
|
||||
reason?: SourceHealthReason
|
||||
detail: { reason?: SourceHealthReason; connection?: string } = {}
|
||||
): SourceHealthHistoryEntry | undefined {
|
||||
const { reason, connection } = detail;
|
||||
// A check of another connection starts the history over.
|
||||
const previous =
|
||||
kept?.connection && connection && kept.connection !== connection
|
||||
? undefined
|
||||
: kept;
|
||||
if (!isConclusiveReachability(reachability)) {
|
||||
return previous;
|
||||
}
|
||||
const checked = connection ? { connection } : {};
|
||||
if (reachability === 'ok') {
|
||||
return { state: 'ok', checkedAt, lastSuccessAt: checkedAt };
|
||||
return { state: 'ok', checkedAt, lastSuccessAt: checkedAt, ...checked };
|
||||
}
|
||||
const wasFailing = previous !== undefined && previous.state !== 'ok';
|
||||
return {
|
||||
@@ -137,5 +150,6 @@ export function nextSourceHealthHistory(
|
||||
? { lastSuccessAt: previous.lastSuccessAt }
|
||||
: {}),
|
||||
...(reason ? { reason } : {}),
|
||||
...checked,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user