mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(playback): expose strict position persistence
This commit is contained in:
1 parent
ac12272db6
commit
7b6d576fef
8 files changed
+434
-29
No files matched your search
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
DestroyableInjector,
|
||||
Injector,
|
||||
runInInjectionContext,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
IXtreamDataSource,
|
||||
XTREAM_DATA_SOURCE,
|
||||
} from '@iptvnator/portal/xtream/data-access';
|
||||
import {
|
||||
PlaybackPositionRuntimeBridgeService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import type { PlaybackPositionData } from '@iptvnator/shared/interfaces';
|
||||
import { AppPortalPlaybackPositionsService } from './portal-playback-positions.service';
|
||||
|
||||
interface PersistenceMocks {
|
||||
clearDataSource: jest.Mock;
|
||||
clearRuntime: jest.Mock;
|
||||
saveDataSource: jest.Mock;
|
||||
saveRuntime: jest.Mock;
|
||||
}
|
||||
|
||||
interface PersistenceHarness {
|
||||
mocks: PersistenceMocks;
|
||||
service: AppPortalPlaybackPositionsService;
|
||||
}
|
||||
|
||||
const position: PlaybackPositionData = {
|
||||
contentXtreamId: 101,
|
||||
contentType: 'episode',
|
||||
playlistId: 'playlist-1',
|
||||
positionSeconds: 42,
|
||||
};
|
||||
|
||||
describe('AppPortalPlaybackPositionsService strict persistence', () => {
|
||||
let injector: DestroyableInjector;
|
||||
|
||||
function createHarness(
|
||||
supportsXtreamSqliteDataSource: boolean
|
||||
): PersistenceHarness {
|
||||
const mocks: PersistenceMocks = {
|
||||
clearDataSource: jest.fn().mockResolvedValue(undefined),
|
||||
clearRuntime: jest.fn().mockResolvedValue(undefined),
|
||||
saveDataSource: jest.fn().mockResolvedValue(undefined),
|
||||
saveRuntime: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const dataSource = {
|
||||
clearPlaybackPosition: mocks.clearDataSource,
|
||||
savePlaybackPosition: mocks.saveDataSource,
|
||||
} as unknown as IXtreamDataSource;
|
||||
injector = Injector.create({
|
||||
providers: [
|
||||
AppPortalPlaybackPositionsService,
|
||||
{ provide: XTREAM_DATA_SOURCE, useValue: dataSource },
|
||||
{
|
||||
provide: RuntimeCapabilitiesService,
|
||||
useValue: { supportsXtreamSqliteDataSource },
|
||||
},
|
||||
{
|
||||
provide: PlaybackPositionRuntimeBridgeService,
|
||||
useValue: {
|
||||
clearPlaybackPositionOrThrow: mocks.clearRuntime,
|
||||
savePlaybackPositionOrThrow: mocks.saveRuntime,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
mocks,
|
||||
service: runInInjectionContext(injector, () =>
|
||||
injector.get(AppPortalPlaybackPositionsService)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => injector.destroy());
|
||||
|
||||
describe.each([
|
||||
{
|
||||
dataSourceMock: (mocks: PersistenceMocks) =>
|
||||
mocks.saveDataSource,
|
||||
invoke: (service: AppPortalPlaybackPositionsService) =>
|
||||
service.savePlaybackPositionOrThrow(
|
||||
'playlist-1',
|
||||
position
|
||||
),
|
||||
runtimeMock: (mocks: PersistenceMocks) => mocks.saveRuntime,
|
||||
},
|
||||
{
|
||||
dataSourceMock: (mocks: PersistenceMocks) =>
|
||||
mocks.clearDataSource,
|
||||
invoke: (service: AppPortalPlaybackPositionsService) =>
|
||||
service.clearPlaybackPositionOrThrow(
|
||||
'playlist-1',
|
||||
101,
|
||||
'episode'
|
||||
),
|
||||
runtimeMock: (mocks: PersistenceMocks) => mocks.clearRuntime,
|
||||
},
|
||||
])('strict operation %#', (operation) => {
|
||||
it('propagates Electron bridge rejection', async () => {
|
||||
const { mocks, service } = createHarness(true);
|
||||
const error = new Error('Electron write failed');
|
||||
operation.runtimeMock(mocks).mockRejectedValue(error);
|
||||
|
||||
await expect(operation.invoke(service)).rejects.toBe(error);
|
||||
expect(operation.dataSourceMock(mocks)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('propagates PWA data-source rejection', async () => {
|
||||
const { mocks, service } = createHarness(false);
|
||||
const error = new Error('localStorage quota exceeded');
|
||||
operation.dataSourceMock(mocks).mockRejectedValue(error);
|
||||
|
||||
await expect(operation.invoke(service)).rejects.toBe(error);
|
||||
expect(operation.runtimeMock(mocks)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps a partial bridge on the selected PWA data source', async () => {
|
||||
const { mocks, service } = createHarness(false);
|
||||
|
||||
await expect(operation.invoke(service)).resolves.toBeUndefined();
|
||||
expect(operation.dataSourceMock(mocks)).toHaveBeenCalledTimes(1);
|
||||
expect(operation.runtimeMock(mocks)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
PORTAL_PLAYBACK_POSITIONS,
|
||||
PortalPlaybackPositions,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
PlaybackPositionRuntimeBridgeService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
PlaybackPositionData,
|
||||
XTREAM_DATA_SOURCE,
|
||||
@@ -15,6 +19,12 @@ export class AppPortalPlaybackPositionsService
|
||||
implements PortalPlaybackPositions
|
||||
{
|
||||
private readonly dataSource = inject(XTREAM_DATA_SOURCE);
|
||||
private readonly useRuntimePersistence = inject(
|
||||
RuntimeCapabilitiesService
|
||||
).supportsXtreamSqliteDataSource;
|
||||
private readonly runtimeBridge = inject(
|
||||
PlaybackPositionRuntimeBridgeService
|
||||
);
|
||||
|
||||
async savePlaybackPosition(
|
||||
playlistId: string,
|
||||
@@ -23,6 +33,21 @@ export class AppPortalPlaybackPositionsService
|
||||
await this.dataSource.savePlaybackPosition(playlistId, data);
|
||||
}
|
||||
|
||||
async savePlaybackPositionOrThrow(
|
||||
playlistId: string,
|
||||
data: PlaybackPositionData
|
||||
): Promise<void> {
|
||||
if (this.useRuntimePersistence) {
|
||||
await this.runtimeBridge.savePlaybackPositionOrThrow(
|
||||
playlistId,
|
||||
data
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource.savePlaybackPosition(playlistId, data);
|
||||
}
|
||||
|
||||
async getPlaybackPosition(
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
@@ -62,6 +87,27 @@ export class AppPortalPlaybackPositionsService
|
||||
contentType
|
||||
);
|
||||
}
|
||||
|
||||
async clearPlaybackPositionOrThrow(
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
contentType: 'vod' | 'episode'
|
||||
): Promise<void> {
|
||||
if (this.useRuntimePersistence) {
|
||||
await this.runtimeBridge.clearPlaybackPositionOrThrow(
|
||||
playlistId,
|
||||
contentXtreamId,
|
||||
contentType
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource.clearPlaybackPosition(
|
||||
playlistId,
|
||||
contentXtreamId,
|
||||
contentType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const providePortalPlaybackPositions = () => [
|
||||
|
||||
Reference in new issue
Block a user