File size: 14,869 Bytes
d4b85c0
1
{"version":3,"sources":["../../src/Interceptor.ts","../../src/createRequestId.ts","../../src/utils/fetchUtils.ts"],"sourcesContent":["import { Logger } from '@open-draft/logger'\nimport { Emitter, Listener } from 'strict-event-emitter'\n\nexport type InterceptorEventMap = Record<string, any>\nexport type InterceptorSubscription = () => void\n\n/**\n * Request header name to detect when a single request\n * is being handled by nested interceptors (XHR -> ClientRequest).\n * Obscure by design to prevent collisions with user-defined headers.\n * Ideally, come up with the Interceptor-level mechanism for this.\n * @see https://github.com/mswjs/interceptors/issues/378\n */\nexport const INTERNAL_REQUEST_ID_HEADER_NAME =\n  'x-interceptors-internal-request-id'\n\nexport function getGlobalSymbol<V>(symbol: Symbol): V | undefined {\n  return (\n    // @ts-ignore https://github.com/Microsoft/TypeScript/issues/24587\n    globalThis[symbol] || undefined\n  )\n}\n\nfunction setGlobalSymbol(symbol: Symbol, value: any): void {\n  // @ts-ignore\n  globalThis[symbol] = value\n}\n\nexport function deleteGlobalSymbol(symbol: Symbol): void {\n  // @ts-ignore\n  delete globalThis[symbol]\n}\n\nexport enum InterceptorReadyState {\n  INACTIVE = 'INACTIVE',\n  APPLYING = 'APPLYING',\n  APPLIED = 'APPLIED',\n  DISPOSING = 'DISPOSING',\n  DISPOSED = 'DISPOSED',\n}\n\nexport type ExtractEventNames<Events extends Record<string, any>> =\n  Events extends Record<infer EventName, any> ? EventName : never\n\nexport class Interceptor<Events extends InterceptorEventMap> {\n  protected emitter: Emitter<Events>\n  protected subscriptions: Array<InterceptorSubscription>\n  protected logger: Logger\n\n  public readyState: InterceptorReadyState\n\n  constructor(private readonly symbol: symbol) {\n    this.readyState = InterceptorReadyState.INACTIVE\n\n    this.emitter = new Emitter()\n    this.subscriptions = []\n    this.logger = new Logger(symbol.description!)\n\n    // Do not limit the maximum number of listeners\n    // so not to limit the maximum amount of parallel events emitted.\n    this.emitter.setMaxListeners(0)\n\n    this.logger.info('constructing the interceptor...')\n  }\n\n  /**\n   * Determine if this interceptor can be applied\n   * in the current environment.\n   */\n  protected checkEnvironment(): boolean {\n    return true\n  }\n\n  /**\n   * Apply this interceptor to the current process.\n   * Returns an already running interceptor instance if it's present.\n   */\n  public apply(): void {\n    const logger = this.logger.extend('apply')\n    logger.info('applying the interceptor...')\n\n    if (this.readyState === InterceptorReadyState.APPLIED) {\n      logger.info('intercepted already applied!')\n      return\n    }\n\n    const shouldApply = this.checkEnvironment()\n\n    if (!shouldApply) {\n      logger.info('the interceptor cannot be applied in this environment!')\n      return\n    }\n\n    this.readyState = InterceptorReadyState.APPLYING\n\n    // Whenever applying a new interceptor, check if it hasn't been applied already.\n    // This enables to apply the same interceptor multiple times, for example from a different\n    // interceptor, only proxying events but keeping the stubs in a single place.\n    const runningInstance = this.getInstance()\n\n    if (runningInstance) {\n      logger.info('found a running instance, reusing...')\n\n      // Proxy any listeners you set on this instance to the running instance.\n      this.on = (event, listener) => {\n        logger.info('proxying the \"%s\" listener', event)\n\n        // Add listeners to the running instance so they appear\n        // at the top of the event listeners list and are executed first.\n        runningInstance.emitter.addListener(event, listener)\n\n        // Ensure that once this interceptor instance is disposed,\n        // it removes all listeners it has appended to the running interceptor instance.\n        this.subscriptions.push(() => {\n          runningInstance.emitter.removeListener(event, listener)\n          logger.info('removed proxied \"%s\" listener!', event)\n        })\n\n        return this\n      }\n\n      this.readyState = InterceptorReadyState.APPLIED\n\n      return\n    }\n\n    logger.info('no running instance found, setting up a new instance...')\n\n    // Setup the interceptor.\n    this.setup()\n\n    // Store the newly applied interceptor instance globally.\n    this.setInstance()\n\n    this.readyState = InterceptorReadyState.APPLIED\n  }\n\n  /**\n   * Setup the module augments and stubs necessary for this interceptor.\n   * This method is not run if there's a running interceptor instance\n   * to prevent instantiating an interceptor multiple times.\n   */\n  protected setup(): void {}\n\n  /**\n   * Listen to the interceptor's public events.\n   */\n  public on<EventName extends ExtractEventNames<Events>>(\n    event: EventName,\n    listener: Listener<Events[EventName]>\n  ): this {\n    const logger = this.logger.extend('on')\n\n    if (\n      this.readyState === InterceptorReadyState.DISPOSING ||\n      this.readyState === InterceptorReadyState.DISPOSED\n    ) {\n      logger.info('cannot listen to events, already disposed!')\n      return this\n    }\n\n    logger.info('adding \"%s\" event listener:', event, listener)\n\n    this.emitter.on(event, listener)\n    return this\n  }\n\n  public once<EventName extends ExtractEventNames<Events>>(\n    event: EventName,\n    listener: Listener<Events[EventName]>\n  ): this {\n    this.emitter.once(event, listener)\n    return this\n  }\n\n  public off<EventName extends ExtractEventNames<Events>>(\n    event: EventName,\n    listener: Listener<Events[EventName]>\n  ): this {\n    this.emitter.off(event, listener)\n    return this\n  }\n\n  public removeAllListeners<EventName extends ExtractEventNames<Events>>(\n    event?: EventName\n  ): this {\n    this.emitter.removeAllListeners(event)\n    return this\n  }\n\n  /**\n   * Disposes of any side-effects this interceptor has introduced.\n   */\n  public dispose(): void {\n    const logger = this.logger.extend('dispose')\n\n    if (this.readyState === InterceptorReadyState.DISPOSED) {\n      logger.info('cannot dispose, already disposed!')\n      return\n    }\n\n    logger.info('disposing the interceptor...')\n    this.readyState = InterceptorReadyState.DISPOSING\n\n    if (!this.getInstance()) {\n      logger.info('no interceptors running, skipping dispose...')\n      return\n    }\n\n    // Delete the global symbol as soon as possible,\n    // indicating that the interceptor is no longer running.\n    this.clearInstance()\n\n    logger.info('global symbol deleted:', getGlobalSymbol(this.symbol))\n\n    if (this.subscriptions.length > 0) {\n      logger.info('disposing of %d subscriptions...', this.subscriptions.length)\n\n      for (const dispose of this.subscriptions) {\n        dispose()\n      }\n\n      this.subscriptions = []\n\n      logger.info('disposed of all subscriptions!', this.subscriptions.length)\n    }\n\n    this.emitter.removeAllListeners()\n    logger.info('destroyed the listener!')\n\n    this.readyState = InterceptorReadyState.DISPOSED\n  }\n\n  private getInstance(): this | undefined {\n    const instance = getGlobalSymbol<this>(this.symbol)\n    this.logger.info('retrieved global instance:', instance?.constructor?.name)\n    return instance\n  }\n\n  private setInstance(): void {\n    setGlobalSymbol(this.symbol, this)\n    this.logger.info('set global instance!', this.symbol.description)\n  }\n\n  private clearInstance(): void {\n    deleteGlobalSymbol(this.symbol)\n    this.logger.info('cleared global instance!', this.symbol.description)\n  }\n}\n","/**\n * Generate a random ID string to represent a request.\n * @example\n * createRequestId()\n * // \"f774b6c9c600f\"\n */\nexport function createRequestId(): string {\n  return Math.random().toString(16).slice(2)\n}\n","export interface FetchResponseInit extends ResponseInit {\n  url?: string\n}\n\nexport class FetchResponse extends Response {\n  /**\n   * Response status codes for responses that cannot have body.\n   * @see https://fetch.spec.whatwg.org/#statuses\n   */\n  static readonly STATUS_CODES_WITHOUT_BODY = [101, 103, 204, 205, 304]\n\n  static readonly STATUS_CODES_WITH_REDIRECT = [301, 302, 303, 307, 308]\n\n  static isConfigurableStatusCode(status: number): boolean {\n    return status >= 200 && status <= 599\n  }\n\n  static isRedirectResponse(status: number): boolean {\n    return FetchResponse.STATUS_CODES_WITH_REDIRECT.includes(status)\n  }\n\n  /**\n   * Returns a boolean indicating whether the given response status\n   * code represents a response that can have a body.\n   */\n  static isResponseWithBody(status: number): boolean {\n    return !FetchResponse.STATUS_CODES_WITHOUT_BODY.includes(status)\n  }\n\n  static setUrl(url: string | undefined, response: Response): void {\n    if (!url) {\n      return\n    }\n\n    if (response.url != '') {\n      return\n    }\n\n    Object.defineProperty(response, 'url', {\n      value: url,\n      enumerable: true,\n      configurable: true,\n      writable: false,\n    })\n  }\n\n  /**\n   * Parses the given raw HTTP headers into a Fetch API `Headers` instance.\n   */\n  static parseRawHeaders(rawHeaders: Array<string>): Headers {\n    const headers = new Headers()\n    for (let line = 0; line < rawHeaders.length; line += 2) {\n      headers.append(rawHeaders[line], rawHeaders[line + 1])\n    }\n    return headers\n  }\n\n  constructor(body?: BodyInit | null, init: FetchResponseInit = {}) {\n    const status = init.status ?? 200\n    const safeStatus = FetchResponse.isConfigurableStatusCode(status)\n      ? status\n      : 200\n    const finalBody = FetchResponse.isResponseWithBody(status) ? body : null\n\n    super(finalBody, {\n      ...init,\n      status: safeStatus,\n    })\n\n    if (status !== safeStatus) {\n      /**\n       * @note Undici keeps an internal \"Symbol(state)\" that holds\n       * the actual value of response status. Update that in Node.js.\n       */\n      const stateSymbol = Object.getOwnPropertySymbols(this).find(\n        (symbol) => symbol.description === 'state'\n      )\n      if (stateSymbol) {\n        const state = Reflect.get(this, stateSymbol) as object\n        Reflect.set(state, 'status', status)\n      } else {\n        Object.defineProperty(this, 'status', {\n          value: status,\n          enumerable: true,\n          configurable: true,\n          writable: false,\n        })\n      }\n    }\n\n    FetchResponse.setUrl(init.url, this)\n  }\n}\n"],"mappings":"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","names":["InterceptorReadyState"]}