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:
parent
ec9a6bca14
commit
048d437b0b
@ -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', () => {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user