mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
perf(portals): fast-fail requests to portal hosts that stopped answering (#1421)
This commit is contained in:
1 parent
2d7811eb5f
commit
e3f72f7dce
50 files changed
+3073
-174
No files matched your search
@@ -1,6 +1,7 @@
|
||||
export * from './lib/catalog-title-match.service';
|
||||
export * from './lib/cross-portal-similar.service';
|
||||
export * from './lib/data.service';
|
||||
export * from './lib/host-connectivity-reset';
|
||||
export * from './lib/database-electron.service';
|
||||
export * from './lib/downloads.service';
|
||||
export * from './lib/playback-position-runtime-bridge.service';
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { CONNECTIVITY_GUARD_RESET } from '@iptvnator/shared/interfaces';
|
||||
import { resetHostConnectivityGuard } from './host-connectivity-reset';
|
||||
|
||||
describe('resetHostConnectivityGuard', () => {
|
||||
let sendIpcEvent: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
sendIpcEvent = jest.fn().mockResolvedValue({ success: true });
|
||||
});
|
||||
|
||||
it('asks the main process to forget the failures recorded for the host', async () => {
|
||||
await resetHostConnectivityGuard(
|
||||
{ sendIpcEvent },
|
||||
'http://portal.example:8080'
|
||||
);
|
||||
|
||||
expect(sendIpcEvent).toHaveBeenCalledWith(CONNECTIVITY_GUARD_RESET, {
|
||||
url: 'http://portal.example:8080',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([undefined, null, ''])(
|
||||
'sends nothing when there is no URL to reset (%p)',
|
||||
async (url) => {
|
||||
// A playlist can be missing its address entirely; there is no host
|
||||
// to clear then, and an empty reset would be a pointless round-trip.
|
||||
await resetHostConnectivityGuard({ sendIpcEvent }, url);
|
||||
|
||||
expect(sendIpcEvent).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it('never propagates a transport failure to the caller', async () => {
|
||||
// The guard only ever delays a request, so a failed reset must not
|
||||
// block the retry, discovery run or status check that asked for it.
|
||||
sendIpcEvent.mockRejectedValue(new Error('IPC unavailable'));
|
||||
|
||||
await expect(
|
||||
resetHostConnectivityGuard({ sendIpcEvent }, 'http://x.example')
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('tolerates the PWA no-op, where the channel is unknown', async () => {
|
||||
// PwaService.sendIpcEvent returns undefined synchronously for channels
|
||||
// it does not implement; nothing there records per-host failures yet.
|
||||
sendIpcEvent.mockReturnValue(undefined);
|
||||
|
||||
await expect(
|
||||
resetHostConnectivityGuard({ sendIpcEvent }, 'http://x.example')
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CONNECTIVITY_GUARD_RESET } from '@iptvnator/shared/interfaces';
|
||||
import type { DataService } from './data.service';
|
||||
|
||||
/**
|
||||
* Asks the main process to forget the connection failures it recorded for the
|
||||
* host `url` points at, so the next request contacts it for real instead of
|
||||
* being fast-failed by the connectivity guard.
|
||||
*
|
||||
* Send this whenever the user asked for a real attempt (portal retry, "test
|
||||
* connection") or handed over an address that may now point somewhere else
|
||||
* (import, edited connection, lazy repair).
|
||||
*
|
||||
* Always best effort: the guard only ever delays a request, so a failure here
|
||||
* must never block the action that asked for it. In the PWA the channel is
|
||||
* unknown and `sendIpcEvent` no-ops, which is correct — nothing there records
|
||||
* per-host failures yet.
|
||||
*/
|
||||
export async function resetHostConnectivityGuard(
|
||||
dataService: Pick<DataService, 'sendIpcEvent'>,
|
||||
url: string | null | undefined
|
||||
): Promise<void> {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.resolve(
|
||||
dataService.sendIpcEvent(CONNECTIVITY_GUARD_RESET, { url })
|
||||
);
|
||||
} catch {
|
||||
// Deliberately silent: the caller's own request reports the real state.
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createEnvironmentInjector,
|
||||
runInInjectionContext,
|
||||
} from '@angular/core';
|
||||
import { CONNECTIVITY_GUARD_RESET } from '@iptvnator/shared/interfaces';
|
||||
import { DataService } from './data.service';
|
||||
import { PortalStatusService } from './portal-status.service';
|
||||
|
||||
@@ -252,4 +253,56 @@ describe('PortalStatusService', () => {
|
||||
service.checkPortalStatus('https://example.com', 'user', 'pass')
|
||||
).resolves.toBe('active');
|
||||
});
|
||||
|
||||
describe('host connectivity guard', () => {
|
||||
beforeEach(() => {
|
||||
dataService.sendIpcEvent.mockResolvedValue({
|
||||
payload: { user_info: { auth: 1, status: 'Active' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the guard first when the user asked to test the connection', async () => {
|
||||
// Otherwise "Test Connection" on a panel that just came back would
|
||||
// report it unavailable without contacting it at all.
|
||||
await service.checkPortalStatusDetails(
|
||||
'http://example.com',
|
||||
'user',
|
||||
'pass',
|
||||
{ skipCache: true }
|
||||
);
|
||||
|
||||
expect(dataService.sendIpcEvent.mock.calls[0]).toEqual([
|
||||
CONNECTIVITY_GUARD_RESET,
|
||||
{ url: 'http://example.com' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves the guard alone for passive background checks', async () => {
|
||||
await service.checkPortalStatusDetails(
|
||||
'http://example.com',
|
||||
'user',
|
||||
'pass'
|
||||
);
|
||||
|
||||
expect(dataService.sendIpcEvent).not.toHaveBeenCalledWith(
|
||||
CONNECTIVITY_GUARD_RESET,
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('still runs the check when clearing the guard fails', async () => {
|
||||
dataService.sendIpcEvent.mockRejectedValueOnce(
|
||||
new Error('IPC unavailable')
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.checkPortalStatusDetails(
|
||||
'http://example.com',
|
||||
'user',
|
||||
'pass',
|
||||
{ skipCache: true }
|
||||
)
|
||||
).resolves.toMatchObject({ status: 'active' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,13 +6,10 @@ import {
|
||||
XtreamPortalStatusResponseLike,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { DataService } from './data.service';
|
||||
import { resetHostConnectivityGuard } from './host-connectivity-reset';
|
||||
|
||||
export type PortalStatus =
|
||||
| 'active'
|
||||
| 'inactive'
|
||||
| 'expired'
|
||||
| 'unavailable'
|
||||
| 'checking';
|
||||
'active' | 'inactive' | 'expired' | 'unavailable' | 'checking';
|
||||
|
||||
/**
|
||||
* Status plus the parsed account expiration from the same round-trip the
|
||||
@@ -135,6 +132,14 @@ export class PortalStatusService {
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
} else {
|
||||
// Skipping the cache means the user asked to test this portal
|
||||
// right now, so the main process must forget any connection
|
||||
// failures it recorded for the host and contact it for real.
|
||||
await resetHostConnectivityGuard(
|
||||
this.dataService,
|
||||
connection.serverUrl
|
||||
);
|
||||
}
|
||||
|
||||
const request = this.fetchPortalStatus(
|
||||
|
||||
Reference in new issue
Block a user