chromium/ui/file_manager/image_loader/image_loader_client.ts

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

import {LruCache} from 'chrome://file-manager/common/js/lru_cache.js';

import {cacheKey, type CacheValue, createCancel, type LoadImageRequest, LoadImageResponse, LoadImageResponseStatus} from './load_image_request.js';

let instance: ImageLoaderClient|null = null;

/**
 * Client used to connect to the remote ImageLoader extension. Client class runs
 * in the extension, where the client.js is included (eg. Files app).
 * It sends remote requests using IPC to the ImageLoader class and forwards
 * its responses.
 *
 * Implements cache, which is stored in the calling extension.
 */
export class ImageLoaderClient {
  private lastTaskId_: number = 0;
  /**
   * LRU cache for images.
   */
  private cache_ = new LruCache<CacheValue>(CACHE_MEMORY_LIMIT);

  /**
   * Returns a singleton instance.
   */
  static getInstance(): ImageLoaderClient {
    if (!instance) {
      instance = new ImageLoaderClient();
    }
    return instance;
  }

  /**
   * Sends a message to the Image Loader extension.
   * @param request The image request.
   * @param callback Response handling callback. The response is passed as a
   *     hash array.
   */
  private static sendMessage_(
      request: LoadImageRequest, callback?: (r: LoadImageResponse) => void) {
    chrome.runtime.sendMessage(EXTENSION_ID, request, {}, callback);
  }

  /**
   * Loads and resizes and image.
   *
   * @param callback Response handling callback.
   * @return Remote task id or null if loaded from cache.
   */
  load(request: LoadImageRequest, callback: (r: LoadImageResponse) => void):
      null|number {
    // Replace the client origin with the image loader extension origin.
    request.url = request.url ?? '';
    request.url = request.url.replace(CLIENT_URL_REGEX, IMAGE_LOADER_URL);
    request.url = request.url.replace(CLIENT_SWA_REGEX, IMAGE_LOADER_URL);

    // Try to load from cache, if available.
    const key = cacheKey(request);
    if (key) {
      if (request.cache) {
        // Load from cache.
        let cachedValue: CacheValue|null = this.cache_.get(key);
        // Check if the image in cache is up to date. If not, then remove it.
        // It relies on comparing `null` equals to `undefined`.
        // eslint-disable-next-line eqeqeq
        if (cachedValue && cachedValue.timestamp != request.timestamp) {
          this.cache_.remove(key);
          cachedValue = null;
        }
        if (cachedValue && cachedValue.data && cachedValue.width &&
            cachedValue.height) {
          callback(
              new LoadImageResponse(LoadImageResponseStatus.SUCCESS, null, {
                width: cachedValue.width,
                height: cachedValue.height,
                ifd: cachedValue.ifd,
                data: cachedValue.data,
              }));
          return null;
        }
      } else {
        // Remove from cache.
        this.cache_.remove(key);
      }
    }

    // Not available in cache, performing a request to a remote extension.
    this.lastTaskId_++;
    request.taskId = this.lastTaskId_;

    ImageLoaderClient.sendMessage_(request, (resultData) => {
      if (chrome.runtime.lastError) {
        console.warn(chrome.runtime.lastError.message);
        callback(new LoadImageResponse(
            LoadImageResponseStatus.ERROR, request.taskId!));
        return;
      }
      const result = resultData;
      // Save to cache.
      if (key && request.cache) {
        const value: CacheValue|null =
            LoadImageResponse.cacheValue(result, request.timestamp);
        if (value) {
          this.cache_.put(key, value, value.data.length);
        }
      }
      callback(result);
    });
    return request.taskId;
  }

  /**
   * Cancels the request. Note the original callback may still be invoked if
   * this message doesn't reach the ImageLoader before it starts processing.
   * @param taskId Task id returned by ImageLoaderClient.load().
   */
  cancel(taskId: number) {
    ImageLoaderClient.sendMessage_(createCancel(taskId), (_result) => {});
  }

  // Helper functions.

  /**
   * Loads and resizes and image.
   *
   * @param image Image node to load the requested picture into.
   * @param onSuccess Callback for success.
   * @param onError Callback for failure.
   * @return Remote task id or null if loaded from cache.
   */
  static loadToImage(
      request: LoadImageRequest, image: HTMLImageElement,
      onSuccess: VoidCallback, onError: VoidCallback): null|number {
    const callback = (result: LoadImageResponse) => {
      if (!result || result.status === LoadImageResponseStatus.ERROR) {
        onError();
        return;
      }
      image.src = result.data!;
      onSuccess();
    };

    return ImageLoaderClient.getInstance().load(request, callback);
  }
}

/**
 * Image loader's extension id.
 */
const EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp';

/**
 * Image loader client extension request URL matcher.
 */
const CLIENT_URL_REGEX = /filesystem:chrome-extension:\/\/[a-z]+/;

/**
 * Image loader client chrome://file-manager request URL matcher.
 */
const CLIENT_SWA_REGEX = /filesystem:chrome:\/\/file-manager/;

/**
 * All client request URL match CLIENT_URL_REGEX and all are
 * rewritten: the client extension id part of the request URL is replaced with
 * the image loader extension id.
 */
const IMAGE_LOADER_URL = 'filesystem:chrome-extension://' + EXTENSION_ID;

/**
 * Memory limit for images data in bytes.
 */
const CACHE_MEMORY_LIMIT = 20 * 1024 * 1024;  // 20 MB.