mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	perf(web): batch asset store changes (#7974)
* perf(web): batch asset store changes * update external calls to assetstore
This commit is contained in:
		
							parent
							
								
									a3dfa27a53
								
							
						
					
					
						commit
						5a6b71dda3
					
				@ -169,12 +169,12 @@
 | 
			
		||||
      case AssetAction.UNARCHIVE:
 | 
			
		||||
      case AssetAction.FAVORITE:
 | 
			
		||||
      case AssetAction.UNFAVORITE: {
 | 
			
		||||
        assetStore.updateAsset(asset);
 | 
			
		||||
        assetStore.updateAssets([asset]);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      case AssetAction.ADD: {
 | 
			
		||||
        assetStore.addAsset(asset);
 | 
			
		||||
        assetStore.addAssets([asset]);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -46,22 +46,22 @@ const THUMBNAIL_HEIGHT = 235;
 | 
			
		||||
 | 
			
		||||
interface AddAsset {
 | 
			
		||||
  type: 'add';
 | 
			
		||||
  value: AssetResponseDto;
 | 
			
		||||
  values: AssetResponseDto[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface UpdateAsset {
 | 
			
		||||
  type: 'update';
 | 
			
		||||
  value: AssetResponseDto;
 | 
			
		||||
  values: AssetResponseDto[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DeleteAsset {
 | 
			
		||||
  type: 'delete';
 | 
			
		||||
  value: string;
 | 
			
		||||
  values: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TrashAssets {
 | 
			
		||||
  type: 'trash';
 | 
			
		||||
  value: string[];
 | 
			
		||||
  values: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const photoViewer = writable<HTMLImageElement | null>(null);
 | 
			
		||||
@ -102,16 +102,16 @@ export class AssetStore {
 | 
			
		||||
  connect() {
 | 
			
		||||
    this.unsubscribers.push(
 | 
			
		||||
      websocketEvents.on('on_upload_success', (asset) => {
 | 
			
		||||
        this.addPendingChanges({ type: 'add', value: asset });
 | 
			
		||||
        this.addPendingChanges({ type: 'add', values: [asset] });
 | 
			
		||||
      }),
 | 
			
		||||
      websocketEvents.on('on_asset_trash', (ids) => {
 | 
			
		||||
        this.addPendingChanges({ type: 'trash', value: ids });
 | 
			
		||||
        this.addPendingChanges({ type: 'trash', values: ids });
 | 
			
		||||
      }),
 | 
			
		||||
      websocketEvents.on('on_asset_update', (asset) => {
 | 
			
		||||
        this.addPendingChanges({ type: 'update', value: asset });
 | 
			
		||||
        this.addPendingChanges({ type: 'update', values: [asset] });
 | 
			
		||||
      }),
 | 
			
		||||
      websocketEvents.on('on_asset_delete', (id: string) => {
 | 
			
		||||
        this.addPendingChanges({ type: 'delete', value: id });
 | 
			
		||||
        this.addPendingChanges({ type: 'delete', values: [id] });
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@ -122,28 +122,55 @@ export class AssetStore {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getPendingChangeBatches() {
 | 
			
		||||
    const batches: PendingChange[] = [];
 | 
			
		||||
    let batch: PendingChange | undefined;
 | 
			
		||||
 | 
			
		||||
    for (const { type, values: _values } of this.pendingChanges) {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
      const values = _values as any[];
 | 
			
		||||
 | 
			
		||||
      if (batch && batch.type !== type) {
 | 
			
		||||
        batches.push(batch);
 | 
			
		||||
        batch = undefined;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (batch) {
 | 
			
		||||
        batch.values.push(...values);
 | 
			
		||||
      } else {
 | 
			
		||||
        batch = { type, values };
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (batch) {
 | 
			
		||||
      batches.push(batch);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return batches;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  processPendingChanges = throttle(() => {
 | 
			
		||||
    for (const { type, value } of this.pendingChanges) {
 | 
			
		||||
    for (const { type, values } of this.getPendingChangeBatches()) {
 | 
			
		||||
      switch (type) {
 | 
			
		||||
        case 'add': {
 | 
			
		||||
          this.addAsset(value);
 | 
			
		||||
          this.addAssets(values);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case 'update': {
 | 
			
		||||
          this.updateAsset(value);
 | 
			
		||||
          this.updateAssets(values);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case 'trash': {
 | 
			
		||||
          if (!this.options.isTrashed) {
 | 
			
		||||
            this.removeAssets(value);
 | 
			
		||||
            this.removeAssets(values);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case 'delete': {
 | 
			
		||||
          this.removeAssets([value]);
 | 
			
		||||
          this.removeAssets(values);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -151,7 +178,7 @@ export class AssetStore {
 | 
			
		||||
 | 
			
		||||
    this.pendingChanges = [];
 | 
			
		||||
    this.emit(true);
 | 
			
		||||
  }, 10_000);
 | 
			
		||||
  }, 2500);
 | 
			
		||||
 | 
			
		||||
  async init(viewport: Viewport) {
 | 
			
		||||
    this.initialized = false;
 | 
			
		||||
@ -277,56 +304,73 @@ export class AssetStore {
 | 
			
		||||
    return scrollTimeline ? delta : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addAsset(asset: AssetResponseDto): void {
 | 
			
		||||
    if (
 | 
			
		||||
      this.assetToBucket[asset.id] ||
 | 
			
		||||
      this.options.userId ||
 | 
			
		||||
      this.options.personId ||
 | 
			
		||||
      this.options.albumId ||
 | 
			
		||||
      isMismatched(this.options.isArchived, asset.isArchived) ||
 | 
			
		||||
      isMismatched(this.options.isFavorite, asset.isFavorite)
 | 
			
		||||
    ) {
 | 
			
		||||
      // If asset is already in the bucket we don't need to recalculate
 | 
			
		||||
      // asset store containers
 | 
			
		||||
      this.updateAsset(asset);
 | 
			
		||||
      return;
 | 
			
		||||
  addAssets(assets: AssetResponseDto[]) {
 | 
			
		||||
    const assetsToUpdate: AssetResponseDto[] = [];
 | 
			
		||||
    const assetsToAdd: AssetResponseDto[] = [];
 | 
			
		||||
 | 
			
		||||
    for (const asset of assets) {
 | 
			
		||||
      if (
 | 
			
		||||
        this.assetToBucket[asset.id] ||
 | 
			
		||||
        this.options.userId ||
 | 
			
		||||
        this.options.personId ||
 | 
			
		||||
        this.options.albumId ||
 | 
			
		||||
        isMismatched(this.options.isArchived, asset.isArchived) ||
 | 
			
		||||
        isMismatched(this.options.isFavorite, asset.isFavorite)
 | 
			
		||||
      ) {
 | 
			
		||||
        // If asset is already in the bucket we don't need to recalculate
 | 
			
		||||
        // asset store containers
 | 
			
		||||
        assetsToUpdate.push(asset);
 | 
			
		||||
      } else {
 | 
			
		||||
        assetsToAdd.push(asset);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.addAssetToBucket(asset);
 | 
			
		||||
    this.updateAssets(assetsToUpdate);
 | 
			
		||||
    this.addAssetsToBuckets(assetsToAdd);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private addAssetToBucket(asset: AssetResponseDto) {
 | 
			
		||||
    const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString();
 | 
			
		||||
    let bucket = this.getBucketByDate(timeBucket);
 | 
			
		||||
  private addAssetsToBuckets(assets: AssetResponseDto[]) {
 | 
			
		||||
    if (assets.length === 0) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const updatedBuckets = new Set<AssetBucket>();
 | 
			
		||||
 | 
			
		||||
    if (!bucket) {
 | 
			
		||||
      bucket = {
 | 
			
		||||
        bucketDate: timeBucket,
 | 
			
		||||
        bucketHeight: THUMBNAIL_HEIGHT,
 | 
			
		||||
        bucketCount: 0,
 | 
			
		||||
        assets: [],
 | 
			
		||||
        cancelToken: null,
 | 
			
		||||
        position: BucketPosition.Unknown,
 | 
			
		||||
      };
 | 
			
		||||
    for (const asset of assets) {
 | 
			
		||||
      const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString();
 | 
			
		||||
      let bucket = this.getBucketByDate(timeBucket);
 | 
			
		||||
 | 
			
		||||
      this.buckets.push(bucket);
 | 
			
		||||
      this.buckets = this.buckets.sort((a, b) => {
 | 
			
		||||
        const aDate = DateTime.fromISO(a.bucketDate).toUTC();
 | 
			
		||||
        const bDate = DateTime.fromISO(b.bucketDate).toUTC();
 | 
			
		||||
      if (!bucket) {
 | 
			
		||||
        bucket = {
 | 
			
		||||
          bucketDate: timeBucket,
 | 
			
		||||
          bucketHeight: THUMBNAIL_HEIGHT,
 | 
			
		||||
          bucketCount: 0,
 | 
			
		||||
          assets: [],
 | 
			
		||||
          cancelToken: null,
 | 
			
		||||
          position: BucketPosition.Unknown,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.buckets.push(bucket);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      bucket.assets.push(asset);
 | 
			
		||||
      this.assets.push(asset);
 | 
			
		||||
      updatedBuckets.add(bucket);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.buckets = this.buckets.sort((a, b) => {
 | 
			
		||||
      const aDate = DateTime.fromISO(a.bucketDate).toUTC();
 | 
			
		||||
      const bDate = DateTime.fromISO(b.bucketDate).toUTC();
 | 
			
		||||
      return bDate.diff(aDate).milliseconds;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (const bucket of updatedBuckets) {
 | 
			
		||||
      bucket.assets.sort((a, b) => {
 | 
			
		||||
        const aDate = DateTime.fromISO(a.fileCreatedAt).toUTC();
 | 
			
		||||
        const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC();
 | 
			
		||||
        return bDate.diff(aDate).milliseconds;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    bucket.assets.push(asset);
 | 
			
		||||
    bucket.assets.sort((a, b) => {
 | 
			
		||||
      const aDate = DateTime.fromISO(a.fileCreatedAt).toUTC();
 | 
			
		||||
      const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC();
 | 
			
		||||
      return bDate.diff(aDate).milliseconds;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // If we added an asset to the store, we need to recalculate
 | 
			
		||||
    // asset store containers
 | 
			
		||||
    this.assets.push(asset);
 | 
			
		||||
    this.emit(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -358,21 +402,29 @@ export class AssetStore {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateAsset(_asset: AssetResponseDto) {
 | 
			
		||||
    const asset = this.assets.find((asset) => asset.id === _asset.id);
 | 
			
		||||
    if (!asset) {
 | 
			
		||||
  updateAssets(assets: AssetResponseDto[]) {
 | 
			
		||||
    if (assets.length === 0) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const assetsToReculculate: AssetResponseDto[] = [];
 | 
			
		||||
 | 
			
		||||
    const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt;
 | 
			
		||||
    if (recalculate) {
 | 
			
		||||
      this.removeAssets([asset.id]);
 | 
			
		||||
      this.addAssetToBucket(_asset);
 | 
			
		||||
      return;
 | 
			
		||||
    for (const _asset of assets) {
 | 
			
		||||
      const asset = this.assets.find((asset) => asset.id === _asset.id);
 | 
			
		||||
      if (!asset) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt;
 | 
			
		||||
      Object.assign(asset, _asset);
 | 
			
		||||
 | 
			
		||||
      if (recalculate) {
 | 
			
		||||
        assetsToReculculate.push(asset);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Object.assign(asset, _asset);
 | 
			
		||||
    this.emit(recalculate);
 | 
			
		||||
    this.removeAssets(assetsToReculculate.map((asset) => asset.id));
 | 
			
		||||
    this.addAssetsToBuckets(assetsToReculculate);
 | 
			
		||||
    this.emit(assetsToReculculate.length > 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeAssets(ids: string[]) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user