mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(settings): drop parental-lock Xtream reloads once the playlist is switched
A reload issued for playlist A no longer publishes into the shared Xtream store after the user opened playlist B: the store's reloads guard on the playlist they read for, and the enforcement apply retires its search refresh and selection checks on a playlist switch as on a newer lock version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
501e196456
commit
451b373b1d
6 files changed
+94
-6
No files matched your search
@@ -313,6 +313,34 @@ describe('ParentalLockEnforcementService', () => {
|
||||
expect(guard?.()).toBe(false);
|
||||
});
|
||||
|
||||
it('abandons the reloads and checks once the Xtream playlist is switched', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/vod/7';
|
||||
parentalLock.active.set(true);
|
||||
xtreamStore.selectedCategoryId.set(7);
|
||||
parentalLock.isXtreamCategoryLocked.mockReturnValue(true);
|
||||
let guard: () => boolean = () => true;
|
||||
(xtreamStore.reloadCategories as jest.Mock).mockImplementationOnce(
|
||||
async (shouldPublish: () => boolean) => {
|
||||
guard = shouldPublish;
|
||||
xtreamStore.playlistId.set('xtream-2');
|
||||
router.url = '/workspace/xtreams/xtream-2/vod';
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
await service.applyXtream(parentalLock.version());
|
||||
expect(guard()).toBe(false);
|
||||
expect(xtreamStore.refreshSearchResults).not.toHaveBeenCalled();
|
||||
expect(
|
||||
xtreamDataSource.getAllCategories
|
||||
).not.toHaveBeenCalled();
|
||||
expect(xtreamStore.setSelectedCategory).not.toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
xtreamStore.playlistId.set('xtream-1');
|
||||
}
|
||||
});
|
||||
|
||||
it('re-runs the stored in-portal search after the reload', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/search';
|
||||
|
||||
|
||||
@@ -132,8 +132,12 @@ export class ParentalLockEnforcementService {
|
||||
if (!playlistId) {
|
||||
return;
|
||||
}
|
||||
// A playlist switch retires the apply as a newer lock version
|
||||
// does: the store now holds another portal, whose own load reads
|
||||
// under the current lock state.
|
||||
const shouldPublish = (): boolean =>
|
||||
this.parentalLock.version() === version;
|
||||
this.parentalLock.version() === version &&
|
||||
this.xtreamStore.playlistId?.() === playlistId;
|
||||
await this.xtreamStore.reloadCategories(shouldPublish);
|
||||
await this.xtreamStore.reloadCachedContent(shouldPublish);
|
||||
if (!shouldPublish()) {
|
||||
|
||||
@@ -189,7 +189,11 @@ on either side.
|
||||
(retiring a search still in flight, which was issued under the previous
|
||||
lock state) before the filtered reads refill them; both reloads take a publish guard
|
||||
answered before every state patch, so a read issued under an older lock
|
||||
version is dropped instead of published. The post-reload checks of the
|
||||
version is dropped instead of published. The guard also turns false once
|
||||
another Xtream playlist is open (the store is a singleton), and the store
|
||||
itself refuses to publish a reload into a playlist it was not read for;
|
||||
the post-reload search refresh and selection checks are skipped then
|
||||
too. The post-reload checks of the
|
||||
selected category and item decide by the LOCK STORE through the
|
||||
unfiltered category rows (`IXtreamDataSource.getAllCategories`), not by
|
||||
absence from the reloaded list, which also omits categories the user
|
||||
|
||||
+31
@@ -243,4 +243,35 @@ describe('withContent parental-lock reloads', () => {
|
||||
series: 'ready',
|
||||
});
|
||||
});
|
||||
|
||||
it('drops reloads whose playlist was switched away while they read', async () => {
|
||||
dataSource.getCategories.mockResolvedValue([{ category_id: 'a' }]);
|
||||
dataSource.getContent.mockResolvedValue([{ xtream_id: 1 }]);
|
||||
await store.initializeContent();
|
||||
|
||||
const categories = createDeferred<unknown[]>();
|
||||
const content = createDeferred<unknown[]>();
|
||||
dataSource.getCategories.mockReturnValue(categories.promise);
|
||||
dataSource.getContent.mockImplementation(
|
||||
(_playlistId: string, _credentials: unknown, type: ContentType) =>
|
||||
type === 'live'
|
||||
? content.promise
|
||||
: Promise.reject(new Error('db'))
|
||||
);
|
||||
const reloadingCategories = store.reloadCategories();
|
||||
const reloadingContent = store.reloadCachedContent();
|
||||
store.switchPlaylist('playlist-b');
|
||||
categories.resolve([{ category_id: 'stale' }]);
|
||||
content.resolve([{ xtream_id: 99 }]);
|
||||
await reloadingCategories;
|
||||
await reloadingContent;
|
||||
|
||||
expect(store.liveCategories()).toEqual([{ category_id: 'a' }]);
|
||||
expect(store.liveStreams()).toEqual([{ xtream_id: 1 }]);
|
||||
expect(store.contentLoadStateByType()).toEqual({
|
||||
live: 'ready',
|
||||
vod: 'ready',
|
||||
series: 'ready',
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
@@ -114,6 +114,10 @@ export function createContentTestStore(
|
||||
patchState(store, { portalStatus: status });
|
||||
return status;
|
||||
},
|
||||
/** Simulates the user opening another playlist. */
|
||||
switchPlaylist(playlistId: string): void {
|
||||
patchState(store, { playlistId });
|
||||
},
|
||||
})),
|
||||
withContent()
|
||||
);
|
||||
|
||||
@@ -259,6 +259,15 @@ export function withContent() {
|
||||
Promise<void>
|
||||
>();
|
||||
|
||||
// A reload publishes only while its playlist is still the open
|
||||
// one: the store is shared, and rows read for playlist A must
|
||||
// not land in it after the user switched to playlist B.
|
||||
const publishWhileCurrent =
|
||||
(playlistId: string, shouldPublish: () => boolean) =>
|
||||
(): boolean =>
|
||||
getPortalStore().playlistId?.() === playlistId &&
|
||||
shouldPublish();
|
||||
|
||||
const getCachedHydrationKey = (
|
||||
playlistId: string,
|
||||
scope?: XtreamCachedContentScope | null
|
||||
@@ -1547,10 +1556,14 @@ export function withContent() {
|
||||
* moved on while it was in flight).
|
||||
*/
|
||||
async reloadCategories(
|
||||
shouldPublish: () => boolean = () => true
|
||||
shouldPublishArg: () => boolean = () => true
|
||||
): Promise<void> {
|
||||
const ctx = getCredentialsFromStore();
|
||||
if (!ctx) return;
|
||||
const shouldPublish = publishWhileCurrent(
|
||||
ctx.playlistId,
|
||||
shouldPublishArg
|
||||
);
|
||||
|
||||
try {
|
||||
const [live, vod, series] = await Promise.all([
|
||||
@@ -1601,7 +1614,7 @@ export function withContent() {
|
||||
* so the in-memory catalog must be rebuilt from them.
|
||||
*/
|
||||
async reloadCachedContent(
|
||||
shouldPublish: () => boolean = () => true
|
||||
shouldPublishArg: () => boolean = () => true
|
||||
): Promise<void> {
|
||||
const ctx = getCredentialsFromStore();
|
||||
if (!ctx) {
|
||||
@@ -1612,9 +1625,13 @@ export function withContent() {
|
||||
// read under the previous lock state: withhold those
|
||||
// and reload once it has settled.
|
||||
reloadAfterInitialization = true;
|
||||
deferredPublishGuard = shouldPublish;
|
||||
deferredPublishGuard = shouldPublishArg;
|
||||
return;
|
||||
}
|
||||
const shouldPublish = publishWhileCurrent(
|
||||
ctx.playlistId,
|
||||
shouldPublishArg
|
||||
);
|
||||
const loadStates = store.contentLoadStateByType();
|
||||
// Each type on its own: one failing read must neither
|
||||
// skip the remaining types nor keep its own rows, which
|
||||
@@ -1673,7 +1690,7 @@ export function withContent() {
|
||||
failed.push('series');
|
||||
}
|
||||
}
|
||||
if (failed.length > 0) {
|
||||
if (failed.length > 0 && shouldPublish()) {
|
||||
const next = { ...store.contentLoadStateByType() };
|
||||
for (const type of failed) {
|
||||
next[type] = 'idle';
|
||||
|
||||
Reference in new issue
Block a user