1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-22 01:47:08 +02:00

fix(web): prevent duplicate time bucket loads (#8091)

This commit is contained in:
Michel Heusschen 2024-03-20 20:40:41 +01:00 committed by GitHub
parent ec9a6bca14
commit 048d437b0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 20 deletions

View File

@ -1,4 +1,5 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { AbortError } from '$lib/utils';
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk'; import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
import { assetFactory } from '@test-data/factories/asset-factory'; import { assetFactory } from '@test-data/factories/asset-factory';
import { AssetStore, BucketPosition } from './assets.store'; import { AssetStore, BucketPosition } from './assets.store';
@ -62,7 +63,15 @@ describe('AssetStore', () => {
{ count: 1, timeBucket: '2024-01-03T00:00:00.000Z' }, { count: 1, timeBucket: '2024-01-03T00:00:00.000Z' },
{ count: 3, timeBucket: '2024-01-01T00:00:00.000Z' }, { count: 3, timeBucket: '2024-01-01T00:00:00.000Z' },
]); ]);
sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket])); sdkMock.getTimeBucket.mockImplementation(async ({ timeBucket }, { signal } = {}) => {
// Allow request to be aborted
await new Promise((resolve) => setTimeout(resolve, 0));
if (signal?.aborted) {
throw new AbortError();
}
return bucketAssets[timeBucket];
});
await assetStore.init({ width: 0, height: 0 }); await assetStore.init({ width: 0, height: 0 });
}); });
@ -87,17 +96,39 @@ describe('AssetStore', () => {
}); });
it('cancels bucket loading', async () => { it('cancels bucket loading', async () => {
const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
const loadPromise = assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown);
const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z');
expect(bucket).not.toBeNull(); const loadPromise = assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown);
const abortSpy = vi.spyOn(bucket!.cancelToken!, 'abort');
assetStore.cancelBucket(bucket!); assetStore.cancelBucket(bucket!);
expect(abortSpy).toBeCalledTimes(1); expect(abortSpy).toBeCalledTimes(1);
await loadPromise; await loadPromise;
expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(0); expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(0);
}); });
it('prevents loading buckets multiple times', async () => {
await Promise.all([
assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown),
assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown),
]);
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown);
expect(sdkMock.getTimeBucket).toBeCalledTimes(1);
});
it('allows loading a canceled bucket', async () => {
const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z');
const loadPromise = assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown);
assetStore.cancelBucket(bucket!);
await loadPromise;
expect(bucket?.assets.length).toEqual(0);
await assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown);
expect(bucket!.assets.length).toEqual(3);
});
}); });
describe('addAssets', () => { describe('addAssets', () => {

View File

@ -229,7 +229,6 @@ export class AssetStore {
} }
async loadBucket(bucketDate: string, position: BucketPosition): Promise<void> { async loadBucket(bucketDate: string, position: BucketPosition): Promise<void> {
try {
const bucket = this.getBucketByDate(bucketDate); const bucket = this.getBucketByDate(bucketDate);
if (!bucket) { if (!bucket) {
return; return;
@ -237,13 +236,14 @@ export class AssetStore {
bucket.position = position; bucket.position = position;
if (bucket.assets.length > 0) { if (bucket.cancelToken || bucket.assets.length > 0) {
this.emit(false); this.emit(false);
return; return;
} }
bucket.cancelToken = new AbortController(); bucket.cancelToken = new AbortController();
try {
const assets = await getTimeBucket( const assets = await getTimeBucket(
{ {
...this.options, ...this.options,
@ -278,6 +278,8 @@ export class AssetStore {
this.emit(true); this.emit(true);
} catch (error) { } catch (error) {
handleError(error, 'Failed to load assets'); handleError(error, 'Failed to load assets');
} finally {
bucket.cancelToken = null;
} }
} }

View File

@ -27,7 +27,7 @@ interface UploadRequestOptions {
onUploadProgress?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void; onUploadProgress?: (event: ProgressEvent<XMLHttpRequestEventTarget>) => void;
} }
class AbortError extends Error { export class AbortError extends Error {
name = 'AbortError'; name = 'AbortError';
} }