mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(portals): reload Stalker categories only for a held destination
Review follow-ups (Greptile, Codex): resetCategories() reloaded the category resource, and the route session calls it on a portal switch before the destination is resolved and on teardown, so it asked the portal being left, and a failed destination lookup could let that answer repopulate the sidebar. resetCategories() now only clears; the session calls the new reloadCategories() after installing the destination, and only when a handoff had already put that playlist in the store (the owner, and so the resource params, did not change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
278245121c
commit
c73a3e92a5
7 files changed
+84
-3
No files matched your search
@@ -397,8 +397,12 @@ Category arrays belong to `categoryPlaylistKey`, not just the content type.
|
||||
Every category reader checks that owner; a portal switch clears all four
|
||||
section caches before loading the destination. Aborted or foreign-portal
|
||||
responses (including errors and radio fallbacks) cannot write into the active
|
||||
cache. Explicit category resets reload even when the playlist object remains
|
||||
unchanged. This contract applies to collection details as well as routed
|
||||
cache. `resetCategories()` only clears: the route session calls it on a
|
||||
portal switch before the destination is resolved and on teardown, where a
|
||||
request would go to the portal being left. When a handoff had already put
|
||||
the destination in the store, the owner does not change and the resource
|
||||
would not reload by itself, so the session calls `reloadCategories()` once
|
||||
it has installed that playlist. This contract applies to collection details as well as routed
|
||||
catalogs, because both use the root Stalker store.
|
||||
|
||||
Failure-handling rule:
|
||||
|
||||
@@ -93,6 +93,9 @@ During refactor:
|
||||
- `clearSelectedItem(): void`
|
||||
- `setCategories(type: 'vod' | 'series' | 'itv' | 'radio', categories: StalkerCategoryItem[]): void`
|
||||
- `resetCategories(): void`
|
||||
Clears only; it starts no request (called on portal switches and route teardown).
|
||||
- `reloadCategories(): void`
|
||||
Refetches the current portal's categories after a reset that keeps the owner.
|
||||
- `setItvChannels(channels: StalkerItvChannel[]): void`
|
||||
- `setRadioChannels(channels: StalkerItvChannel[]): void`
|
||||
- `setSearchPhrase(phrase: string): void`
|
||||
|
||||
@@ -139,6 +139,7 @@ describe('StalkerStore API compatibility smoke', () => {
|
||||
'clearSelectedItem',
|
||||
'setCategories',
|
||||
'resetCategories',
|
||||
'reloadCategories',
|
||||
'setItvChannels',
|
||||
'setSearchPhrase',
|
||||
'fetchVodSeriesEpisodes',
|
||||
|
||||
+16
-1
@@ -97,7 +97,21 @@ describe('Stalker category ownership', () => {
|
||||
void store.isCategoryResourceLoading();
|
||||
});
|
||||
|
||||
it('reloads categories when the active portal cache is explicitly reset', async () => {
|
||||
it('sends no request on a reset alone', async () => {
|
||||
request.mockResolvedValueOnce({
|
||||
js: [{ id: '1', title: 'Before reset' }],
|
||||
});
|
||||
store.selectPlaylist('a');
|
||||
await settle();
|
||||
request.mockClear();
|
||||
|
||||
store.resetCategories();
|
||||
await settle();
|
||||
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reloads categories when the active portal cache is explicitly reloaded after a reset', async () => {
|
||||
request.mockResolvedValueOnce({
|
||||
js: [{ id: '1', title: 'Before reset' }],
|
||||
});
|
||||
@@ -107,6 +121,7 @@ describe('Stalker category ownership', () => {
|
||||
js: [{ id: '1', title: 'After reset' }],
|
||||
});
|
||||
store.resetCategories();
|
||||
store.reloadCategories();
|
||||
await settle();
|
||||
expect(
|
||||
store.getCategoryResource().map((c) => c.category_name)
|
||||
|
||||
+9
@@ -1214,6 +1214,8 @@ export function withStalkerContent() {
|
||||
...buildCategoryPatch(type, categories),
|
||||
});
|
||||
},
|
||||
// Clears only: called on portal switches and route teardown,
|
||||
// where a reload would ask the portal being left.
|
||||
resetCategories() {
|
||||
patchState(store, {
|
||||
vodCategories: [],
|
||||
@@ -1222,6 +1224,13 @@ export function withStalkerContent() {
|
||||
radioCategories: [],
|
||||
categoryError: null,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Refetches the current portal's categories. For a reset that
|
||||
* keeps the owner: the resource's params do not change then,
|
||||
* so it would not load on its own.
|
||||
*/
|
||||
reloadCategories() {
|
||||
storeContext.categoryResource.reload();
|
||||
},
|
||||
setItvChannels(channels: StalkerItvChannel[]) {
|
||||
|
||||
@@ -71,6 +71,7 @@ describe('StalkerWorkspaceRouteSession', () => {
|
||||
const stalkerStore = {
|
||||
currentPlaylist,
|
||||
resetCategories: jest.fn(),
|
||||
reloadCategories: jest.fn(),
|
||||
setSelectedCategory: jest.fn(),
|
||||
clearSelectedItem: jest.fn(),
|
||||
setCurrentPlaylist: jest.fn(
|
||||
@@ -117,6 +118,7 @@ describe('StalkerWorkspaceRouteSession', () => {
|
||||
}));
|
||||
|
||||
stalkerStore.resetCategories.mockClear();
|
||||
stalkerStore.reloadCategories.mockClear();
|
||||
stalkerStore.setSelectedCategory.mockClear();
|
||||
stalkerStore.clearSelectedItem.mockClear();
|
||||
stalkerStore.setCurrentPlaylist.mockClear();
|
||||
@@ -169,6 +171,46 @@ describe('StalkerWorkspaceRouteSession', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// A switch must not ask the portal being left for categories: the new
|
||||
// owner's params make the resource load on their own.
|
||||
it('does not reload categories when the store switches portals', async () => {
|
||||
currentPlaylist.set(OTHER_PLAYLIST);
|
||||
|
||||
TestBed.inject(StalkerWorkspaceRouteSession);
|
||||
await flushEffects();
|
||||
|
||||
expect(stalkerStore.resetCategories).toHaveBeenCalled();
|
||||
expect(currentPlaylist()?._id).toBe(PLAYLIST_ID);
|
||||
expect(stalkerStore.reloadCategories).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// A handoff installed the destination in the store first: the reset
|
||||
// keeps the owner, so only an explicit reload brings the categories back.
|
||||
it('reloads the cleared categories once a held destination is installed', async () => {
|
||||
currentPlaylist.set(ACTIVE_PLAYLIST);
|
||||
|
||||
TestBed.inject(StalkerWorkspaceRouteSession);
|
||||
await flushEffects();
|
||||
|
||||
expect(stalkerStore.reloadCategories).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
stalkerStore.reloadCategories.mock.invocationCallOrder[0]
|
||||
).toBeGreaterThan(
|
||||
stalkerStore.setCurrentPlaylist.mock.invocationCallOrder[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('does not reload categories when the route is torn down', async () => {
|
||||
TestBed.inject(StalkerWorkspaceRouteSession);
|
||||
await flushEffects();
|
||||
stalkerStore.reloadCategories.mockClear();
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
expect(stalkerStore.resetCategories).toHaveBeenCalled();
|
||||
expect(stalkerStore.reloadCategories).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reconciles a revisited route when collection detail changed the shared store', async () => {
|
||||
const session = TestBed.inject(StalkerWorkspaceRouteSession);
|
||||
await flushEffects();
|
||||
|
||||
@@ -140,6 +140,10 @@ export class StalkerWorkspaceRouteSession {
|
||||
this.currentPlaylistId !== playlistId ||
|
||||
this.stalkerStore.currentPlaylist()?._id !== playlistId
|
||||
) {
|
||||
// A handoff can install the destination in the store before this
|
||||
// session does; the categories it held are cleared all the same.
|
||||
const storeHeldDestination =
|
||||
this.stalkerStore.currentPlaylist()?._id === playlistId;
|
||||
this.stalkerStore.resetCategories();
|
||||
this.stalkerStore.setSelectedCategory(null);
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
@@ -154,6 +158,9 @@ export class StalkerWorkspaceRouteSession {
|
||||
}
|
||||
await this.stalkerStore.setCurrentPlaylist(playlist);
|
||||
this.currentPlaylistId = playlistId;
|
||||
// The owner did not change, so the category resource keeps its
|
||||
// params and would not reload the cleared categories by itself.
|
||||
if (storeHeldDestination) this.stalkerStore.reloadCategories();
|
||||
}
|
||||
|
||||
if (generation !== this.syncGeneration || this.destroyRef.destroyed) {
|
||||
|
||||
Reference in new issue
Block a user