mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
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:
1 parent
31899d7b58
commit
7d0e8f69ca
14 files changed
+191
-167
No files matched your search
+5
-6
@@ -21,7 +21,10 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { SourceStatusService } from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
SourceStatusService,
|
||||
sourceStatusBucket,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
countSourcesByType,
|
||||
orderFailingFirst,
|
||||
@@ -37,11 +40,7 @@ import {
|
||||
SortService,
|
||||
SourceActivityService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
PlaylistMeta,
|
||||
PlaylistSourceKind,
|
||||
sourceStatusBucket,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { PlaylistMeta, PlaylistSourceKind } from '@iptvnator/shared/interfaces';
|
||||
import { PlaylistRefreshActionService } from '../playlist-refresh-action.service';
|
||||
import { PlaylistInfoComponent } from '../recent-playlists/playlist-info/playlist-info.component';
|
||||
import { SourceRemovalService } from '../source-removal/source-removal.service';
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import {
|
||||
keepSourcesChecked,
|
||||
SourceStatusService,
|
||||
sourceStatusBucket,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
filterSources,
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
SortService,
|
||||
SourceActivityService,
|
||||
} from '@iptvnator/services';
|
||||
import { PlaylistMeta, sourceStatusBucket } from '@iptvnator/shared/interfaces';
|
||||
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
import type { PlaylistType } from '../add-playlist-menu/playlist-type';
|
||||
import { PlaylistRefreshActionService } from '../playlist-refresh-action.service';
|
||||
import { SourceRemovalService } from '../source-removal/source-removal.service';
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './lib/source-health.service';
|
||||
export * from './lib/source-health-history.service';
|
||||
export * from './lib/source-status.service';
|
||||
export * from './lib/source-status-checks';
|
||||
export { sourceStatusBucket } from './lib/source-reachability';
|
||||
|
||||
export * from './lib/source-cleanup.service';
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { SourceHealthEvidenceService } from '@iptvnator/services';
|
||||
import {
|
||||
isSourceHealthReason,
|
||||
nextSourceHealthHistory,
|
||||
SourceHealthHistoryEntry,
|
||||
SourceHealthSnapshot,
|
||||
sourceReachabilityFromHealth,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { isSourceHealthReason } from './source-health-identity';
|
||||
import {
|
||||
nextSourceHealthHistory,
|
||||
sourceReachabilityFromHealth,
|
||||
} from './source-reachability';
|
||||
|
||||
export const SOURCE_HEALTH_HISTORY_STORAGE_KEY = 'source-health:history:v1';
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
sourceHealthKey,
|
||||
type PlaylistMeta,
|
||||
type SourceHealthReason,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
fingerprintSourceHealthKey,
|
||||
PlaylistMeta,
|
||||
SourceHealthSnapshot,
|
||||
SourceHealthResult,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
sourceHealthUnknown,
|
||||
sourceHealthError,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { fingerprintSourceHealthKey } from './source-health-identity';
|
||||
import { SourceHealthHistoryService } from './source-health-history.service';
|
||||
import { SourceHealthProbesService } from './source-health-probes.service';
|
||||
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,116 @@
|
||||
import type {
|
||||
ConclusiveSourceReachability,
|
||||
SourceHealthHistoryEntry,
|
||||
SourceHealthReason,
|
||||
SourceHealthSnapshot,
|
||||
SourceReachability,
|
||||
SourceStatusBucket,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
// Runtime helpers live here, beside the lazily loaded services that use
|
||||
// them: in `@iptvnator/shared/interfaces` they rode into a chunk the app
|
||||
// preloads at startup.
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
import {
|
||||
PlaylistMeta,
|
||||
SourceHealthHistoryEntry,
|
||||
sourceHealthFingerprint,
|
||||
SourceHealthSnapshot,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { sourceHealthFingerprint } from './source-health-identity';
|
||||
import { SourceHealthHistoryService } from './source-health-history.service';
|
||||
import { SourceHealthService } from './source-health.service';
|
||||
import { SourceStatusService } from './source-status.service';
|
||||
|
||||
@@ -5,17 +5,19 @@ import {
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
isConclusiveReachability,
|
||||
PlaylistMeta,
|
||||
SourceHealthReason,
|
||||
sourceHealthFingerprint,
|
||||
sourceHealthType,
|
||||
sourceReachabilityFromHealth,
|
||||
sourceReachabilityFromPortalStatus,
|
||||
SourceStatus,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { sourceHealthFingerprint } from './source-health-identity';
|
||||
import { SourceHealthHistoryService } from './source-health-history.service';
|
||||
import { SourceHealthService } from './source-health.service';
|
||||
import {
|
||||
isConclusiveReachability,
|
||||
sourceReachabilityFromHealth,
|
||||
sourceReachabilityFromPortalStatus,
|
||||
} from './source-reachability';
|
||||
|
||||
type XtreamAccount = PlaylistMeta & {
|
||||
serverUrl: string;
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
}
|
||||
+5
-5
@@ -10,7 +10,10 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { selectAllPlaylistsMeta } from '@iptvnator/m3u-state';
|
||||
import { SourceStatusService } from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
SourceStatusService,
|
||||
sourceStatusBucket,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import { SourceRemovalService } from '@iptvnator/playlist/shared/ui';
|
||||
import {
|
||||
countSourcesByStatus,
|
||||
@@ -20,10 +23,7 @@ import {
|
||||
SourcesTypeFilter,
|
||||
SourcesViewStateService,
|
||||
} from '@iptvnator/playlist/shared/util';
|
||||
import {
|
||||
SOURCE_TYPE_ICONS,
|
||||
sourceStatusBucket,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { SOURCE_TYPE_ICONS } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
WORKSPACE_SHELL_ACTIONS,
|
||||
WorkspaceShellContextDrawerService,
|
||||
|
||||
+4
-2
@@ -18,7 +18,10 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { map } from 'rxjs';
|
||||
import { PlaylistActions, selectAllPlaylistsMeta } from '@iptvnator/m3u-state';
|
||||
import { SourceStatusService } from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
SourceStatusService,
|
||||
sourceStatusBucket,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
isLiveExternalPlayerSession,
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
@@ -43,7 +46,6 @@ import {
|
||||
import {
|
||||
PlaylistUpdateState,
|
||||
sourceHealthType,
|
||||
sourceStatusBucket,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
MenuItemRadioCheckDirective,
|
||||
|
||||
Reference in new issue
Block a user