mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(stalker): merge late connection saves
This commit is contained in:
1 parent
ffae919cc8
commit
37575c8d88
10 files changed
+202
-29
No files matched your search
+27
-5
@@ -115,7 +115,9 @@ describe('PlaylistInfoComponent', () => {
|
||||
disableClose: false,
|
||||
};
|
||||
stalkerConnectionEditor = {
|
||||
applyResolvedConnection: jest.fn().mockResolvedValue(undefined),
|
||||
applyResolvedConnection: jest.fn(
|
||||
async (playlist: PlaylistMeta) => playlist
|
||||
),
|
||||
resolveConnection: jest.fn(
|
||||
async (updatedPlaylist: PlaylistMeta) => ({
|
||||
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.RESOLVED,
|
||||
@@ -841,6 +843,14 @@ describe('PlaylistInfoComponent', () => {
|
||||
stalkerSessionIdentity: 'new-fingerprint',
|
||||
},
|
||||
};
|
||||
const currentPlaylist = {
|
||||
...resolvedPlaylist,
|
||||
title: 'Newer title',
|
||||
epgUrls: ['https://new.example.com/epg.xml'],
|
||||
};
|
||||
stalkerConnectionEditor.applyResolvedConnection.mockResolvedValueOnce(
|
||||
currentPlaylist
|
||||
);
|
||||
let finishDiscovery:
|
||||
| ((value: {
|
||||
status: 'resolved';
|
||||
@@ -869,10 +879,12 @@ describe('PlaylistInfoComponent', () => {
|
||||
|
||||
expect(
|
||||
stalkerConnectionEditor.applyResolvedConnection
|
||||
).toHaveBeenCalledWith(resolvedPlaylist);
|
||||
).toHaveBeenCalledWith(resolvedPlaylist, {
|
||||
preserveCurrentMetadata: true,
|
||||
});
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: resolvedPlaylist,
|
||||
playlist: currentPlaylist,
|
||||
persist: false,
|
||||
})
|
||||
);
|
||||
@@ -891,6 +903,14 @@ describe('PlaylistInfoComponent', () => {
|
||||
stalkerSessionIdentity: 'new-fingerprint',
|
||||
},
|
||||
};
|
||||
const currentPlaylist = {
|
||||
...resolvedPlaylist,
|
||||
title: 'Newer title',
|
||||
epgUrls: ['https://new.example.com/epg.xml'],
|
||||
};
|
||||
stalkerConnectionEditor.applyResolvedConnection.mockResolvedValueOnce(
|
||||
currentPlaylist
|
||||
);
|
||||
let finishDiscovery:
|
||||
| ((value: {
|
||||
status: 'resolved';
|
||||
@@ -919,10 +939,12 @@ describe('PlaylistInfoComponent', () => {
|
||||
|
||||
expect(
|
||||
stalkerConnectionEditor.applyResolvedConnection
|
||||
).toHaveBeenCalledWith(resolvedPlaylist);
|
||||
).toHaveBeenCalledWith(resolvedPlaylist, {
|
||||
preserveCurrentMetadata: true,
|
||||
});
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
PlaylistActions.updatePlaylistMeta({
|
||||
playlist: resolvedPlaylist,
|
||||
playlist: currentPlaylist,
|
||||
persist: false,
|
||||
})
|
||||
);
|
||||
|
||||
+11
-3
@@ -397,6 +397,7 @@ export class PlaylistInfoComponent {
|
||||
}
|
||||
|
||||
let resolvedStalkerConnection = false;
|
||||
let preserveCurrentMetadata = false;
|
||||
let normalizedPlaylist: PlaylistMetaUpdate =
|
||||
this.normalizeStalkerPlaylistMeta(
|
||||
this.normalizeXtreamPlaylistMeta(submittedPlaylist)
|
||||
@@ -425,6 +426,8 @@ export class PlaylistInfoComponent {
|
||||
}
|
||||
normalizedPlaylist = result.playlist;
|
||||
resolvedStalkerConnection = true;
|
||||
preserveCurrentMetadata =
|
||||
this.dialogClosing || this.destroyRef.destroyed;
|
||||
} else {
|
||||
normalizedPlaylist = preserveStalkerConnection(
|
||||
this.playlist,
|
||||
@@ -443,9 +446,14 @@ export class PlaylistInfoComponent {
|
||||
}
|
||||
|
||||
if (resolvedStalkerConnection) {
|
||||
await this.stalkerConnectionEditor.applyResolvedConnection(
|
||||
normalizedPlaylist
|
||||
);
|
||||
normalizedPlaylist = preserveCurrentMetadata
|
||||
? await this.stalkerConnectionEditor.applyResolvedConnection(
|
||||
normalizedPlaylist,
|
||||
{ preserveCurrentMetadata: true }
|
||||
)
|
||||
: await this.stalkerConnectionEditor.applyResolvedConnection(
|
||||
normalizedPlaylist
|
||||
);
|
||||
}
|
||||
|
||||
// Resolved Stalker edits cross an awaited, atomic persistence
|
||||
|
||||
+9
-1
@@ -34,12 +34,20 @@ interface StalkerPlaylistConnectionResultMap {
|
||||
export type StalkerPlaylistConnectionResult =
|
||||
StalkerPlaylistConnectionResultMap[keyof StalkerPlaylistConnectionResultMap];
|
||||
|
||||
export interface StalkerResolvedConnectionApplyOptions {
|
||||
/** Merge only connection/session fields into the current persisted row. */
|
||||
preserveCurrentMetadata?: boolean;
|
||||
}
|
||||
|
||||
export interface StalkerPlaylistConnectionEditor {
|
||||
resolveConnection(
|
||||
playlist: PlaylistMeta
|
||||
): Promise<StalkerPlaylistConnectionResult>;
|
||||
/** Atomically persists a resolved edit, then synchronizes in-run state. */
|
||||
applyResolvedConnection(playlist: PlaylistMetaUpdate): Promise<void>;
|
||||
applyResolvedConnection(
|
||||
playlist: PlaylistMetaUpdate,
|
||||
options?: StalkerResolvedConnectionApplyOptions
|
||||
): Promise<PlaylistMetaUpdate>;
|
||||
}
|
||||
|
||||
export const STALKER_PLAYLIST_CONNECTION_EDITOR =
|
||||
|
||||
@@ -23,11 +23,15 @@ describe('Stalker edited-session coordination', () => {
|
||||
let service: StalkerSessionService;
|
||||
let getPlaylistById: jest.Mock;
|
||||
let updatePlaylistMeta: jest.Mock;
|
||||
let transformPlaylistMeta: jest.Mock;
|
||||
let updateStalkerSession: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
getPlaylistById = jest.fn(() => of(oldPlaylist));
|
||||
updatePlaylistMeta = jest.fn(() => of(oldPlaylist));
|
||||
transformPlaylistMeta = jest.fn((_id, transform) =>
|
||||
of(transform(oldPlaylist))
|
||||
);
|
||||
updateStalkerSession = jest.fn(() => of(oldPlaylist));
|
||||
sendIpcEvent = jest.fn();
|
||||
TestBed.configureTestingModule({
|
||||
@@ -42,6 +46,7 @@ describe('Stalker edited-session coordination', () => {
|
||||
useValue: {
|
||||
getPlaylistById,
|
||||
updatePlaylistMeta,
|
||||
transformPlaylistMeta,
|
||||
updateStalkerSession,
|
||||
},
|
||||
},
|
||||
@@ -318,6 +323,50 @@ describe('Stalker edited-session coordination', () => {
|
||||
expect(updateStalkerSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('atomically merges only connection fields into a newer row after navigation', async () => {
|
||||
const currentPlaylist = {
|
||||
...oldPlaylist,
|
||||
title: 'Newer title',
|
||||
epgUrls: ['https://new.example.com/epg.xml'],
|
||||
} as Playlist;
|
||||
transformPlaylistMeta.mockImplementationOnce((_id, transform) =>
|
||||
of(transform(currentPlaylist))
|
||||
);
|
||||
const editedPlaylist = {
|
||||
...oldPlaylist,
|
||||
title: 'Stale form title',
|
||||
portalUrl: 'https://new.example.com/server/load.php',
|
||||
username: 'subscriber',
|
||||
stalkerToken: 'NEW_TOKEN',
|
||||
stalkerWatchdogTimeout: 90,
|
||||
stalkerTimeslot: 5,
|
||||
} as Playlist;
|
||||
const fence = await service.beginEditDiscovery(editedPlaylist);
|
||||
|
||||
const persisted = await service.replaceSessionAfterEdit(
|
||||
editedPlaylist,
|
||||
fence,
|
||||
{ preserveCurrentMetadata: true }
|
||||
);
|
||||
|
||||
expect(updatePlaylistMeta).not.toHaveBeenCalled();
|
||||
expect(transformPlaylistMeta).toHaveBeenCalledWith(
|
||||
oldPlaylist._id,
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(persisted).toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'Newer title',
|
||||
epgUrls: ['https://new.example.com/epg.xml'],
|
||||
portalUrl: 'https://new.example.com/server/load.php',
|
||||
username: 'subscriber',
|
||||
stalkerToken: 'NEW_TOKEN',
|
||||
stalkerWatchdogTimeout: 90,
|
||||
stalkerTimeslot: 5,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not adopt a resolved full session when its atomic write fails', async () => {
|
||||
service.adoptDiscoveredSimplePortal(oldPlaylist);
|
||||
updatePlaylistMeta.mockReturnValueOnce(
|
||||
|
||||
@@ -77,8 +77,7 @@ export class StalkerEditedSessionCoordinator {
|
||||
if (stalkerSessionFingerprint(playlist) !== sessionFingerprint) {
|
||||
throw new Error('Stale Stalker playlist configuration');
|
||||
}
|
||||
const authoritative =
|
||||
this.authoritativeConfigurations.get(playlistId);
|
||||
const authoritative = this.authoritativeConfigurations.get(playlistId);
|
||||
if (
|
||||
authoritative &&
|
||||
authoritative !==
|
||||
@@ -121,7 +120,11 @@ export class StalkerEditedSessionCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
replace(playlist: Playlist, fence?: StalkerEditFence): Promise<Playlist> {
|
||||
replace(
|
||||
playlist: Playlist,
|
||||
fence?: StalkerEditFence,
|
||||
options: { preserveCurrentMetadata?: boolean } = {}
|
||||
): Promise<Playlist> {
|
||||
const playlistId = playlist._id;
|
||||
const sessionFingerprint = stalkerSessionFingerprint(playlist);
|
||||
const configurationFingerprint = stalkerConfigurationFingerprint(
|
||||
@@ -152,7 +155,8 @@ export class StalkerEditedSessionCoordinator {
|
||||
playlist,
|
||||
sessionFingerprint,
|
||||
configurationFingerprint,
|
||||
owner
|
||||
owner,
|
||||
options
|
||||
)
|
||||
);
|
||||
this.replacements.set(playlistId, replacement);
|
||||
@@ -173,7 +177,8 @@ export class StalkerEditedSessionCoordinator {
|
||||
playlist: Playlist,
|
||||
sessionFingerprint: string,
|
||||
configurationFingerprint: string,
|
||||
owner: symbol
|
||||
owner: symbol,
|
||||
options: { preserveCurrentMetadata?: boolean }
|
||||
): Promise<Playlist> {
|
||||
const playlistId = playlist._id;
|
||||
this.assertFenceCurrent(
|
||||
@@ -208,10 +213,18 @@ export class StalkerEditedSessionCoordinator {
|
||||
}
|
||||
|
||||
const persistedPlaylist = await firstValueFrom(
|
||||
playlists.updatePlaylistMeta({
|
||||
...playlist,
|
||||
stalkerSessionPatch: sessionPatch,
|
||||
} as PlaylistMetaUpdate)
|
||||
options.preserveCurrentMetadata
|
||||
? playlists.transformPlaylistMeta(playlistId, (current) =>
|
||||
mergeResolvedStalkerConnection(
|
||||
current,
|
||||
playlist,
|
||||
sessionPatch
|
||||
)
|
||||
)
|
||||
: playlists.updatePlaylistMeta({
|
||||
...playlist,
|
||||
stalkerSessionPatch: sessionPatch,
|
||||
} as PlaylistMetaUpdate)
|
||||
);
|
||||
if (!persistedPlaylist) {
|
||||
throw new Error('Resolved Stalker playlist could not be persisted');
|
||||
@@ -292,6 +305,32 @@ export class StalkerEditedSessionCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies remote connection authority without replaying stale form metadata. */
|
||||
function mergeResolvedStalkerConnection(
|
||||
current: Playlist,
|
||||
resolved: Playlist,
|
||||
sessionPatch: PlaylistMetaUpdate['stalkerSessionPatch']
|
||||
): Playlist {
|
||||
return {
|
||||
...current,
|
||||
portalUrl: resolved.portalUrl,
|
||||
isFullStalkerPortal: resolved.isFullStalkerPortal,
|
||||
macAddress: resolved.macAddress,
|
||||
username: resolved.username,
|
||||
password: resolved.password,
|
||||
stalkerSerialNumber: resolved.stalkerSerialNumber,
|
||||
stalkerDeviceId1: resolved.stalkerDeviceId1,
|
||||
stalkerDeviceId2: resolved.stalkerDeviceId2,
|
||||
stalkerSignature1: resolved.stalkerSignature1,
|
||||
stalkerSignature2: resolved.stalkerSignature2,
|
||||
stalkerToken: sessionPatch?.stalkerToken,
|
||||
stalkerSessionIdentity: sessionPatch?.stalkerSessionIdentity,
|
||||
stalkerWatchdogTimeout: sessionPatch?.stalkerWatchdogTimeout,
|
||||
stalkerTimeslot: sessionPatch?.stalkerTimeslot,
|
||||
stalkerAccountInfo: sessionPatch?.stalkerAccountInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/** In-run Edit authority also owns the observed full/simple routing mode. */
|
||||
function stalkerConfigurationFingerprint(
|
||||
playlist: Playlist,
|
||||
|
||||
@@ -286,9 +286,10 @@ export class StalkerSessionService {
|
||||
*/
|
||||
replaceSessionAfterEdit(
|
||||
playlist: Playlist,
|
||||
fence?: StalkerEditFence
|
||||
fence?: StalkerEditFence,
|
||||
options: { preserveCurrentMetadata?: boolean } = {}
|
||||
): Promise<Playlist> {
|
||||
return this.editedSessions.replace(playlist, fence);
|
||||
return this.editedSessions.replace(playlist, fence, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user