chromium/ui/file_manager/file_manager/foreground/js/metadata/metadata_cache_set.ts

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {NativeEventTarget as EventTarget} from 'chrome://resources/ash/common/event_target.js';

import {entriesToURLs} from '../../../common/js/entry_utils.js';
import type {FilesAppEntry} from '../../../common/js/files_app_entry_types.js';

import {MetadataCacheItem} from './metadata_cache_item.js';
import type {MetadataItem} from './metadata_item.js';
import {type MetadataKey} from './metadata_item.js';
import {MetadataRequest} from './metadata_request.js';

/**
 * Custom event dispatched by the metadata cache set when results from metadata
 * provider are set on it.
 */
export class MetadataSetEvent extends Event {
  constructor(
      name: string, public entries: Array<Entry|FilesAppEntry>,
      public entriesMap: Map<string, Entry|FilesAppEntry>,
      public names: Set<string>) {
    super(name);
  }
}

export interface MetadataModelMap extends Record<string, MetadataSetEvent> {
  'update': MetadataSetEvent;
}

interface MetadataSetEventTarget {
  addEventListener<K extends keyof MetadataModelMap>(
      type: K, listener: (event: MetadataModelMap[K]) => void,
      options?: boolean|AddEventListenerOptions|undefined): void;
  addEventListener(
      type: string, callback: EventListenerOrEventListenerObject|null,
      options?: AddEventListenerOptions|boolean): void;
  removeEventListener<K extends keyof MetadataModelMap>(
      type: K, listener: (event: MetadataModelMap[K]) => void,
      options?: boolean|EventListenerOptions): void;
  removeEventListener(
      type: string, listener: EventListenerOrEventListenerObject|null,
      options?: boolean|EventListenerOptions): void;
}

class MetadataSetEventTarget extends EventTarget {}

/**
 * A collection of MetadataCacheItem objects. This class acts as a map from file
 * entry URLs to metadata items. You can store metadata for entries, you can
 * retrieve metadata for entries, clear the entire cache, or just selected
 * entries. In addition, you can generate MetadataRequests and start them (i.e.,
 * put them in the LOADING state).
 */
export class MetadataCacheSet extends MetadataSetEventTarget {
  private items_ = new Map<string, MetadataCacheItem>();
  private requestIdCounter_: number = 0;

  /**
   * Creates list of MetadataRequest based on the cache state.
   */
  createRequests(entries: Array<Entry|FilesAppEntry>, names: MetadataKey[]):
      MetadataRequest[] {
    const urls = entriesToURLs(entries);
    const requests = [];
    for (const [i, entry] of entries.entries()) {
      const item = this.items_.get(urls[i]!);
      const requestedNames = item ? item.createRequests(names) : names;
      if (requestedNames.length) {
        requests.push(new MetadataRequest(entry, requestedNames));
      }
    }
    return requests;
  }

  /**
   * Updates cache states to start the given requests.
   */
  startRequests(requestId: number, requests: MetadataRequest[]) {
    for (const request of requests) {
      const url = request.entry.toURL();
      let item = this.items_.get(url);
      if (!item) {
        item = new MetadataCacheItem();
        this.items_.set(url, item);
      }
      item.startRequests(requestId, request.names);
    }
  }

  /**
   * Stores results from MetadataProvider with the request ID.
   * @param requestId Request ID. If a newer operation has already been done,
   *     the results must be ignored.
   * @param names Property names that have been requested and updated.
   * @return Whether at least one result is stored or not.
   */
  storeProperties(
      requestId: number, entries: Array<Entry|FilesAppEntry>,
      results: MetadataItem[], names: MetadataKey[]): boolean {
    const changedEntries: Array<Entry|FilesAppEntry> = [];
    const urls = entriesToURLs(entries);
    const entriesMap = new Map<string, Entry|FilesAppEntry>();

    for (const [i, entry] of entries.entries()) {
      const url = urls[i]!;
      const item = this.items_.get(url);
      if (item && item.storeProperties(requestId, results[i]!)) {
        changedEntries.push(entry);
        entriesMap.set(url, entry);
      }
    }

    if (!changedEntries.length) {
      return false;
    }

    const event = new MetadataSetEvent(
        'update', changedEntries, entriesMap, new Set(names));
    this.dispatchEvent(event);
    return true;
  }

  /**
   * Obtains cached properties for entries and names.
   * Note that it returns invalidated properties also.
   * @param entries Entries.
   * @param names Property names.
   * @return metadata for the given entries.
   */
  get(entries: Array<Entry|FilesAppEntry>,
      names: MetadataKey[]): MetadataItem[] {
    const results = [];
    const urls = entriesToURLs(entries);
    for (let i = 0; i < entries.length; i++) {
      const item = this.items_.get(urls[i]!);
      results.push(item ? item.get(names) : {});
    }
    return results;
  }

  /**
   * Obtains cached properties for file URLs and names.
   * Note that it returns invalidated properties also.
   * @param urls File URLs.
   * @param names Property names.
   * @return metadata for the given entries.
   */
  getByUrls(urls: string[], names: MetadataKey[]): MetadataItem[] {
    const results = [];
    for (const url of urls) {
      const item = this.items_.get(url);
      results.push(item ? item.get(names) : {});
    }
    return results;
  }

  /**
   * Marks the caches of entries as invalidates and forces to reload at the next
   * time of startRequests. Optionally, takes an array of metadata names and
   * only invalidates those.
   * @param requestId Request ID of the invalidation request. This must
   *     be larger than other request ID passed to the set before.
   * @param [names]
   */
  invalidate(
      requestId: number, entries: Array<Entry|FilesAppEntry>,
      names?: MetadataKey[]) {
    const urls = entriesToURLs(entries);
    for (let i = 0; i < entries.length; i++) {
      const item = this.items_.get(urls[i]!);
      if (item) {
        item.invalidate(requestId, names);
      }
    }
  }

  /**
   * Clears the caches of entries.
   */
  clear(urls: string[]) {
    for (const url of urls) {
      this.items_.delete(url);
    }
  }

  /**
   * Clears all cache.
   */
  clearAll() {
    this.items_.clear();
  }

  /**
   * Creates snapshot of the cache for entries.
   * @return a cache with metadata for the given entries.
   */
  createSnapshot(entries: Array<Entry|FilesAppEntry>): MetadataCacheSet {
    const snapshot = new MetadataCacheSet();
    const items = snapshot.items_;
    const urls = entriesToURLs(entries);
    for (let i = 0; i < entries.length; i++) {
      const url = urls[i]!;
      const item = this.items_.get(url);
      if (item) {
        items.set(url, item.clone());
      }
    }
    return snapshot;
  }

  /**
   * Returns whether all the given properties are fulfilled.
   * @param entries Entries.
   * @param names Property names.
   */
  hasFreshCache(entries: Array<Entry|FilesAppEntry>, names: MetadataKey[]):
      boolean {
    if (!names.length) {
      return true;
    }
    const urls = entriesToURLs(entries);
    for (let i = 0; i < entries.length; i++) {
      const item = this.items_.get(urls[i]!);
      if (!(item && item.hasFreshCache(names))) {
        return false;
      }
    }
    return true;
  }

  /**
   * Generates a unique request ID every time when it is called.
   */
  generateRequestId(): number {
    return this.requestIdCounter_++;
  }
}