fix(stalker): retain abandoned auth fences

This commit is contained in:
4gray committed 2026-08-09 15:36:37 +02:00
1 parent 344182014f
commit bfbd252ea2
7 files changed
+176 -30

No files matched your search

@@ -209,6 +209,37 @@ describe('AppStalkerPlaylistConnectionEditorService', () => {
});
});
it('keeps an abandoned authentication fenced until it settles', async () => {
let settleAuthentication: () => void = () => undefined;
const abandonedAuthenticationSettled = new Promise<void>((resolve) => {
settleAuthentication = resolve;
});
discovery.discover.mockResolvedValue({
status: 'auth-rejected',
portalUrl: 'https://portal.example.com/server/load.php',
abandonedInFlight: true,
abandonedAuthenticationSettled,
});
await expect(service.resolveConnection(draft)).resolves.toMatchObject({
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.AUTH_REJECTED,
});
expect(stalkerSession.cancelEditDiscovery).not.toHaveBeenCalled();
expect(portalRepair.releasePlaylistEdit).not.toHaveBeenCalled();
settleAuthentication();
await abandonedAuthenticationSettled;
await Promise.resolve();
expect(stalkerSession.cancelEditDiscovery).toHaveBeenCalledWith(
expect.objectContaining({ playlistId: draft._id })
);
expect(portalRepair.releasePlaylistEdit).toHaveBeenCalledWith(
draft._id
);
});
it('returns a dedicated error when no endpoint can be reached', async () => {
discovery.discover.mockResolvedValue({ status: 'unreachable' });
@@ -136,7 +136,15 @@ export class AppStalkerPlaylistConnectionEditorService implements StalkerPlaylis
}
if (outcome.status === 'auth-rejected') {
this.releaseEditFence(playlist._id, fence);
if (outcome.abandonedInFlight) {
this.releaseEditFenceAfterAuthenticationSettles(
playlist._id,
fence,
outcome.abandonedAuthenticationSettled
);
} else {
this.releaseEditFence(playlist._id, fence);
}
return {
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.AUTH_REJECTED,
message: this.buildAuthErrorMessage(outcome.error),
@@ -230,6 +238,28 @@ export class AppStalkerPlaylistConnectionEditorService implements StalkerPlaylis
this.portalRepair.releasePlaylistEdit(playlistId);
}
private releaseEditFenceAfterAuthenticationSettles(
playlistId: string,
fence: StalkerEditFence,
authenticationSettled: Promise<void> | undefined
): void {
// Fail closed if a producer ever reports an abandoned request without
// its lifetime. Releasing here would let a fresh session authenticate
// while the old get_profile can still land and invalidate its token.
if (!authenticationSettled) {
return;
}
void authenticationSettled.then(() => {
// The dialog may have been closed or a later owner may already
// have retired this exact reservation. Never release a different
// edit's counted repair fence from this late continuation.
if (this.editFences.get(playlistId) === fence) {
this.releaseEditFence(playlistId, fence);
}
});
}
private buildAuthErrorMessage(error: unknown): string {
const portalError = asStalkerPortalError(error);
const headline = this.translate.instant(