All files / packages/core/src/requestPool requestPoolManager.ts

77.94% Statements 53/68
54.83% Branches 17/31
68.42% Functions 13/19
77.61% Lines 52/67

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324                                                                                                                                                                                                16x 2x   2x           2x 2x   2x           2x                                   3x                                                                             109x   109x             109x 2x       109x     109x 109x 109x                               2x 2x 6x 6x 10x                                             327x 327x   327x 436x 436x 327x 109x 109x 109x   109x 109x 109x               1308x 436x 436x 109x 109x       327x       109x     109x     109x       109x         109x       109x 109x 109x                       436x 436x   679x   436x                             1x        
import RequestType from '../enums/RequestType';
import { IImage } from '../types';
import { uuidv4 } from '../utilities';
 
type AdditionalDetails = {
  imageId?: string;
  volumeId?: string;
};
 
type RequestDetailsInterface = {
  requestFn: () => Promise<IImage | void>;
  type: RequestType;
  additionalDetails: AdditionalDetails;
};
 
type RequestPool = {
  [name in RequestType]: { [key: number]: RequestDetailsInterface[] };
};
 
/**
 * RequestPool manager class is a base class that manages the request pools.
 * It is used imageRetrievalPoolManager, and imageLoadPoolManager to retrieve and load images.
 * Previously requestPoolManager was used to manage the retrieval and loading and decoding
 * of the images in a way that new requests were sent after the image was both loaded and decoded
 * which was not performant since it was waiting for the image to be loaded and decoded before
 * sending the next request which is a network request and can be done in parallel.
 * Now, we use separate imageRetrievalPoolManager and imageLoadPoolManager
 * to improve performance and both are extending the RequestPoolManager class which
 * is a basic queueing pool.
 *
 * A new requestPool can be created by instantiating a new RequestPoolManager class.
 *
 * ```javascript
 * const requestPoolManager = new RequestPoolManager()
 * ```
 *
 * ## ImageLoadPoolManager
 *
 * You can use the imageLoadPoolManager to load images, by providing a `requestFn`
 * that returns a promise for the image. You can provide a `type` to specify the type of
 * request (interaction, thumbnail, prefetch), and you can provide additional details
 * that will be passed to the requestFn. Below is an example of a requestFn that loads
 * an image from an imageId:
 *
 * ```javascript
 *
 * const priority = -5
 * const requestType = RequestType.Interaction
 * const additionalDetails = { imageId }
 * const options = {
 *   targetBuffer: {
 *     type: 'Float32Array',
 *     offset: null,
 *     length: null,
 *   },
 *   preScale: {
 *      enabled: true,
 *    },
 * }
 *
 * imageLoadPoolManager.addRequest(
 *   loadAndCacheImage(imageId, options).then(() => { // set on viewport}),
 *   requestType,
 *   additionalDetails,
 *   priority
 * )
 * ```
 * ### ImageRetrievalPoolManager
 * You don't need to directly use the imageRetrievalPoolManager to load images
 * since the imageLoadPoolManager will automatically use it for retrieval. However,
 * maximum number of concurrent requests can be set by calling `setMaxConcurrentRequests`.
 */
class RequestPoolManager {
  private id: string;
  private awake: boolean;
  private requestPool: RequestPool;
  private numRequests = {
    interaction: 0,
    thumbnail: 0,
    prefetch: 0,
  };
  /* maximum number of requests of each type. */
  public maxNumRequests: {
    interaction: number;
    thumbnail: number;
    prefetch: number;
  };
  /* A public property that is used to set the delay between requests. */
  public grabDelay: number;
  private timeoutHandle: number;
 
  /**
   * By default a request pool containing three priority groups, one for each
   * of the request types, is created. Maximum number of requests of each type
   * is set to 6.
   */
  constructor(id?: string) {
    this.id = id ? id : uuidv4();
 
    this.requestPool = {
      interaction: { 0: [] },
      thumbnail: { 0: [] },
      prefetch: { 0: [] },
    };
 
    this.grabDelay = 5;
    this.awake = false;
 
    this.numRequests = {
      interaction: 0,
      thumbnail: 0,
      prefetch: 0,
    };
 
    this.maxNumRequests = {
      interaction: 6,
      thumbnail: 6,
      prefetch: 5,
    };
  }
 
  /**
   * This function sets the maximum number of requests for a given request type.
   * @param type - The type of request you want to set the max number
   * of requests for it can be either of interaction, prefetch, or thumbnail.
   * @param maxNumRequests - The maximum number of requests that can be
   * made at a time.
   */
  public setMaxSimultaneousRequests(
    type: RequestType,
    maxNumRequests: number
  ): void {
    this.maxNumRequests[type] = maxNumRequests;
  }
 
  /**
   * It returns the maximum number of requests of a given type that can be made
   * @param type - The type of request.
   * @returns The maximum number of requests of a given type.
   */
  public getMaxSimultaneousRequests(type: RequestType): number {
    return this.maxNumRequests[type];
  }
 
  /**
   * Stops further fetching of the requests, all the ongoing requests will still
   * be retrieved
   */
  public destroy(): void {
    if (this.timeoutHandle) {
      window.clearTimeout(this.timeoutHandle);
    }
  }
 
  /**
   * Adds the requests to the pool of requests.
   *
   * @param requestFn - A function that returns a promise which resolves in the image
   * @param type - Priority category, it can be either of interaction, prefetch,
   * or thumbnail.
   * @param additionalDetails - Additional details that requests can contain.
   * For instance the volumeId for the volume requests
   * @param priority - Priority number for each category of requests. Its default
   * value is priority 0. The lower the priority number, the higher the priority number
   *
   */
  public addRequest(
    requestFn: () => Promise<IImage | void>,
    type: RequestType,
    additionalDetails: Record<string, unknown>,
    priority = 0
  ): void {
    // Describe the request
    const requestDetails: RequestDetailsInterface = {
      requestFn,
      type,
      additionalDetails,
    };
 
    // Check if the priority group exists on the request type
    if (this.requestPool[type][priority] === undefined) {
      this.requestPool[type][priority] = [];
    }
 
    // Adding the request to the correct priority group of the request type
    this.requestPool[type][priority].push(requestDetails);
 
    // Wake up
    Eif (!this.awake) {
      this.awake = true;
      this.startGrabbing();
    } else if (type === RequestType.Interaction) {
      // Todo: this is a hack for interaction right now, we should separate
      // the grabbing from the adding requests
      this.startGrabbing();
    }
  }
 
  /**
   * Filter the requestPoolManager's pool of request based on the result of
   * provided filter function. The provided filter function needs to return false or true
   *
   * @param filterFunction - The filter function for filtering of the requests to keep
   */
  public filterRequests(
    filterFunction: (requestDetails: RequestDetailsInterface) => boolean
  ): void {
    Object.keys(this.requestPool).forEach((type: string) => {
      const requestType = this.requestPool[type];
      Object.keys(requestType).forEach((priority) => {
        requestType[priority] = requestType[priority].filter(
          (requestDetails: RequestDetailsInterface) => {
            return filterFunction(requestDetails);
          }
        );
      });
    });
  }
 
  /**
   * Clears the requests specific to the provided type. For instance, the
   * pool of requests of type 'interaction' can be cleared via this function.
   *
   *
   * @param type - category of the request (either interaction, prefetch or thumbnail)
   */
  public clearRequestStack(type: string): void {
    if (!this.requestPool[type]) {
      throw new Error(`No category for the type ${type} found`);
    }
    this.requestPool[type] = { 0: [] };
  }
 
  private sendRequests(type) {
    const requestsToSend = this.maxNumRequests[type] - this.numRequests[type];
 
    for (let i = 0; i < requestsToSend; i++) {
      const requestDetails = this.getNextRequest(type);
      if (requestDetails === null) {
        return false;
      } else Eif (requestDetails) {
        this.numRequests[type]++;
        this.awake = true;
 
        requestDetails.requestFn().finally(() => {
          this.numRequests[type]--;
          this.startAgain();
        });
      }
    }
 
    return true;
  }
 
  private getNextRequest(type): RequestDetailsInterface | null {
    const interactionPriorities = this.getSortedPriorityGroups(type);
    for (const priority of interactionPriorities) {
      Eif (this.requestPool[type][priority].length) {
        return this.requestPool[type][priority].shift();
      }
    }
 
    return null;
  }
 
  protected startGrabbing(): void {
    const hasRemainingInteractionRequests = this.sendRequests(
      RequestType.Interaction
    );
    const hasRemainingThumbnailRequests = this.sendRequests(
      RequestType.Thumbnail
    );
    const hasRemainingPrefetchRequests = this.sendRequests(
      RequestType.Prefetch
    );
 
    Eif (
      !hasRemainingInteractionRequests &&
      !hasRemainingThumbnailRequests &&
      !hasRemainingPrefetchRequests
    ) {
      this.awake = false;
    }
  }
 
  protected startAgain(): void {
    Eif (!this.awake) {
      return;
    }
 
    if (this.grabDelay !== undefined) {
      this.timeoutHandle = window.setTimeout(() => {
        this.startGrabbing();
      }, this.grabDelay);
    } else {
      this.startGrabbing();
    }
  }
 
  protected getSortedPriorityGroups(type: string): Array<number> {
    const priorities = Object.keys(this.requestPool[type])
      .map(Number)
      .filter((priority) => this.requestPool[type][priority].length)
      .sort();
    return priorities;
  }
 
  /**
   * Returns the request pool containing different categories, their priority and
   * the added request details.
   *
   * @returns the request pool which contains different categories, their priority and
   * the added request details
   */
  getRequestPool(): RequestPool {
    return this.requestPool;
  }
}
 
const requestPoolManager = new RequestPoolManager();
 
export { RequestPoolManager };
export default requestPoolManager;