perf(sources): keep source-health helpers off the startup path

Master's #1931 lowered the initial-bytes baseline, and this branch then
sat 535 bytes over its slack. Most of its growth was runtime helpers in
`@iptvnator/shared/interfaces` (reachability, health history, reason and
fingerprint helpers) that only the lazily loaded source services call:
the bundler put them in a chunk the app preloads. They now live in
`@iptvnator/portal/shared/data-access` beside those services (types stay
in interfaces); initialBytes 1,350,438 → 1,349,048, 855 under the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-14 04:02:40 +02:00
1 parent 31899d7b58
commit 7d0e8f69ca
14 files changed
+191 -167

No files matched your search

@@ -16,27 +16,6 @@ export type SourceHealthReason =
| 'invalid'
| 'unknown'
| 'cancelled';
const SOURCE_HEALTH_REASONS: ReadonlySet<string> = new Set<SourceHealthReason>([
'available',
'disabled',
'expired',
'auth',
'http',
'timeout',
'network',
'paused',
'invalid',
'unknown',
'cancelled',
]);
/** Whether stored data names a reason this build can describe. */
export function isSourceHealthReason(
value: unknown
): value is SourceHealthReason {
return typeof value === 'string' && SOURCE_HEALTH_REASONS.has(value);
}
export interface SourceHealthResult {
state: SourceHealthState;
reason: SourceHealthReason;
@@ -84,24 +63,6 @@ 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,
@@ -1,135 +0,0 @@
import {
nextSourceHealthHistory,
sourceReachabilityFromHealth,
sourceReachabilityFromPortalStatus,
sourceStatusBucket,
} from './source-reachability';
describe('source reachability', () => {
it.each([
[{ state: 'active', reason: 'available' }, 'ok'],
[{ state: 'expired', reason: 'expired' }, 'expired'],
[{ state: 'inactive', reason: 'disabled' }, 'bad'],
[{ state: 'inactive', reason: 'auth' }, 'bad'],
[{ state: 'checking', reason: 'unknown' }, 'checking'],
[{ state: 'unavailable', reason: 'network' }, 'bad'],
[{ state: 'unavailable', reason: 'timeout' }, 'bad'],
[{ state: 'unavailable', reason: 'http' }, 'bad'],
[{ state: 'unavailable', reason: 'invalid' }, 'bad'],
// Checks that never reached the source say nothing about it.
[{ state: 'unavailable', reason: 'paused' }, 'unknown'],
[{ state: 'unavailable', reason: 'cancelled' }, 'unknown'],
[{ state: 'unavailable', reason: 'unknown' }, 'unknown'],
[{ state: 'unknown', reason: 'unknown' }, 'unknown'],
] as const)('maps %o to %s', (snapshot, expected) => {
expect(sourceReachabilityFromHealth(snapshot)).toBe(expected);
});
it('treats a source without a snapshot as never checked', () => {
expect(sourceReachabilityFromHealth(undefined)).toBe('unknown');
});
it.each([
['active', 'ok'],
['expired', 'expired'],
['inactive', 'bad'],
['unavailable', 'bad'],
['checking', 'checking'],
] as const)('maps the portal status %s to %s', (status, expected) => {
expect(sourceReachabilityFromPortalStatus(status)).toBe(expected);
});
it('files expired accounts with the sources that do not respond', () => {
expect(sourceStatusBucket('ok')).toBe('ok');
expect(sourceStatusBucket('bad')).toBe('bad');
expect(sourceStatusBucket('expired')).toBe('bad');
expect(sourceStatusBucket('checking')).toBeNull();
expect(sourceStatusBucket('unknown')).toBeNull();
});
});
describe('nextSourceHealthHistory', () => {
it('starts a failing run at the first failed check', () => {
expect(nextSourceHealthHistory(undefined, 'bad', 100)).toEqual({
state: 'bad',
checkedAt: 100,
failingSince: 100,
});
});
it('keeps the start of the run across later failures', () => {
const first = nextSourceHealthHistory(
{ state: 'ok', checkedAt: 50, lastSuccessAt: 50 },
'bad',
100
);
const second = nextSourceHealthHistory(first, 'expired', 400);
expect(second).toEqual({
state: 'expired',
checkedAt: 400,
failingSince: 100,
lastSuccessAt: 50,
});
});
it('keeps the cause of a failure, never of a success', () => {
const failed = nextSourceHealthHistory(undefined, 'bad', 100, {
reason: 'auth',
});
expect(failed?.reason).toBe('auth');
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', () => {
expect(
nextSourceHealthHistory(
{ state: 'bad', checkedAt: 100, failingSince: 10 },
'ok',
500
)
).toEqual({ state: 'ok', checkedAt: 500, lastSuccessAt: 500 });
});
it('keeps the previous entry for an inconclusive check', () => {
const previous = {
state: 'bad',
checkedAt: 100,
failingSince: 10,
} as const;
expect(nextSourceHealthHistory(previous, 'unknown', 900)).toBe(
previous
);
expect(nextSourceHealthHistory(previous, 'checking', 900)).toBe(
previous
);
expect(
nextSourceHealthHistory(undefined, 'unknown', 900)
).toBeUndefined();
});
});
@@ -1,4 +1,4 @@
import type { SourceHealthReason, SourceHealthSnapshot } from './source-health';
import type { SourceHealthReason } from './source-health';
/**
* What a source list shows about one network source, folded from the six
@@ -33,70 +33,6 @@ export interface SourceStatus {
reason?: SourceHealthReason;
}
/**
* Health reasons that mean the source itself did not answer properly. A
* paused, cancelled or unknown check says nothing about the source.
*/
const UNREACHABLE_REASONS = new Set(['network', 'timeout', 'http', 'invalid']);
export function sourceReachabilityFromHealth(
snapshot: Pick<SourceHealthSnapshot, 'state' | 'reason'> | undefined
): SourceReachability {
switch (snapshot?.state) {
case 'active':
return 'ok';
case 'expired':
return 'expired';
case 'inactive':
return 'bad';
case 'checking':
return 'checking';
case 'unavailable':
return UNREACHABLE_REASONS.has(snapshot.reason) ? 'bad' : 'unknown';
default:
return 'unknown';
}
}
/** The browser build's Xtream portal status, by the same rules. */
export function sourceReachabilityFromPortalStatus(
status: 'active' | 'inactive' | 'expired' | 'unavailable' | 'checking'
): SourceReachability {
switch (status) {
case 'active':
return 'ok';
case 'expired':
return 'expired';
case 'checking':
return 'checking';
default:
return 'bad';
}
}
export function isConclusiveReachability(
reachability: SourceReachability
): reachability is ConclusiveSourceReachability {
return (
reachability === 'ok' ||
reachability === 'bad' ||
reachability === 'expired'
);
}
/**
* Expired accounts count as not responding: the source answers, but the
* account cannot play anything, which is what the filter is for.
*/
export function sourceStatusBucket(
reachability: SourceReachability
): SourceStatusBucket | null {
if (reachability === 'ok') {
return 'ok';
}
return reachability === 'bad' || reachability === 'expired' ? 'bad' : null;
}
/** The last conclusive result kept for a source across restarts. */
export interface SourceHealthHistoryEntry {
state: ConclusiveSourceReachability;
@@ -113,43 +49,3 @@ export interface SourceHealthHistoryEntry {
*/
connection?: string;
}
/**
* Folds one check into the kept history. An inconclusive check keeps the
* previous entry; a failure keeps the start of the current failing run, so
* a source that has not answered for three days still says three days after
* every later failed check.
*/
export function nextSourceHealthHistory(
kept: SourceHealthHistoryEntry | undefined,
reachability: SourceReachability,
checkedAt: number,
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, ...checked };
}
const wasFailing = previous !== undefined && previous.state !== 'ok';
return {
state: reachability,
checkedAt,
failingSince: wasFailing
? (previous.failingSince ?? previous.checkedAt)
: checkedAt,
...(previous?.lastSuccessAt !== undefined
? { lastSuccessAt: previous.lastSuccessAt }
: {}),
...(reason ? { reason } : {}),
...checked,
};
}