chromium/ui/file_manager/file_manager/foreground/js/metadata/metadata_cache_item.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 {assert} from 'chrome://resources/js/assert.js';

import {MetadataItem, type MetadataKey} from './metadata_item.js';

/**
 * Cache of metadata for a FileEntry.
 */
export class MetadataCacheItem {
  /**
   * Map of property name and MetadataCacheItemProperty.
   */
  private readonly properties_:
      Partial<Record<MetadataKey, MetadataCacheItemProperty>> = {};

  /**
   * Creates requested names that need to be loaded.
   * @return Property names that need to be loaded.
   */
  createRequests(names: MetadataKey[]): MetadataKey[] {
    const loadRequested: MetadataKey[] = [];
    for (const name of names) {
      assert(!/Error$/.test(name));
      // Check if the property needs to be updated.
      const property = this.properties_[name];
      if (property &&
          property.state !== MetadataCacheItemPropertyState.INVALIDATED) {
        continue;
      }
      loadRequested.push(name);
    }
    return loadRequested;
  }

  /**
   * Marks the given properties as loading.
   */
  startRequests(requestId: number, names: MetadataKey[]) {
    for (const name of names) {
      assert(!/Error$/.test(name));
      if (!this.properties_[name]) {
        this.properties_[name] = new MetadataCacheItemProperty();
      }
      this.properties_[name]!.requestId = requestId;
      this.properties_[name]!.state = MetadataCacheItemPropertyState.LOADING;
    }
  }

  /**
   * Feeds the result of startRequests.
   * @param requestId Request ID passed when calling startRequests.
   * @param typedObject Map of property name and value.
   * @return Whether at least one property is updated or not.
   */
  storeProperties(requestId: number, typedObject: MetadataItem): boolean {
    let changed = false;
    for (const name in typedObject) {
      if (/.Error$/.test(name) && typedObject[name as MetadataKey]) {
        typedObject[name.substr(0, name.length - 5) as MetadataKey] = undefined;
      }
    }
    for (const _name in typedObject) {
      if (/.Error$/.test(_name)) {
        continue;
      }
      const name = _name as MetadataKey;
      if (!this.properties_[name]) {
        this.properties_[name] = new MetadataCacheItemProperty();
      }
      if (requestId < this.properties_[name]!.requestId ||
          this.properties_[name]!.state ===
              MetadataCacheItemPropertyState.FULFILLED) {
        continue;
      }
      changed = true;
      this.properties_[name]!.requestId = requestId;
      this.properties_[name]!.value = typedObject[name as MetadataKey];
      const errorKey = `${name}Error` as const;
      this.properties_[name]!.error =
          typedObject[errorKey as (typeof errorKey) & MetadataKey];
      this.properties_[name]!.state = MetadataCacheItemPropertyState.FULFILLED;
    }
    return changed;
  }

  /**
   * Marks the caches of all properties in the item as invalidates and forces to
   * reload at the next time of startRequests. Optionally, takes an array of
   * names and only invalidates those.
   * @param requestId Request ID of the invalidation request. This must
   *     be larger than other requests ID passed to the item before.
   */
  invalidate(requestId: number, names?: MetadataKey[]): void {
    const namesToInvalidate = names ?
        names.filter(n => this.properties_[n]) :
        Object.keys(this.properties_) as MetadataKey[];
    for (const name of namesToInvalidate) {
      assert(this.properties_[name]!.requestId < requestId);
      this.properties_[name]!.requestId = requestId;
      this.properties_[name]!.state =
          MetadataCacheItemPropertyState.INVALIDATED;
    }
  }

  /**
   * Obtains property for entries and names.
   * Note that it returns invalidated properties also.
   */
  get(names: MetadataKey[]): MetadataItem {
    const result = new MetadataItem();
    for (const name of names) {
      assert(!/Error$/.test(name));
      if (this.properties_[name]) {
        // `undefined` is the intersection of all possible properties of
        // MetadataItem.
        result[name] = this.properties_[name]!.value as undefined;
        const errorKey = `${name}Error` as const;
        // TODO(TS): check ... if (!(errorKey in result)){
        result[errorKey as (typeof errorKey) & MetadataKey] =
            this.properties_[name]!.error;
      }
    }
    return result;
  }

  /**
   * Creates deep copy of the item.
   */
  clone(): MetadataCacheItem {
    const clonedItem = new MetadataCacheItem();
    for (const name in this.properties_) {
      const property = this.properties_[name as MetadataKey]!;
      const newItemProperties = new MetadataCacheItemProperty();
      newItemProperties.value = property.value;
      newItemProperties.error = property.error;
      newItemProperties.requestId = property.requestId;
      newItemProperties.state = property.state;
      clonedItem.properties_[name as MetadataKey] = newItemProperties;
    }
    return clonedItem;
  }

  /**
   * Returns whether all the given properties are fulfilled.
   * @param names Property names.
   */
  hasFreshCache(names: MetadataKey[]): boolean {
    for (const name of names) {
      const property = this.properties_[name];
      if (!(property &&
            property.state === MetadataCacheItemPropertyState.FULFILLED)) {
        return false;
      }
    }
    return true;
  }
}

enum MetadataCacheItemPropertyState {
  INVALIDATED = 'invalidated',
  LOADING = 'loading',
  FULFILLED = 'fulfilled',
}

/**
 * Cache of metadata for a property.
 */
class MetadataCacheItemProperty {
  /**
   * Cached value of property.
   */
  value: unknown = null;

  error: undefined|Error = undefined;

  /**
   * Last request ID.
   */
  requestId: number = -1;

  /**
   * Cache state of the property.
   */
  state = MetadataCacheItemPropertyState.INVALIDATED;
}