File size: 16,671 Bytes
d4b85c0
1
{"version":3,"sources":["../../src/RequestController.ts","../../src/InterceptorError.ts","../../src/utils/emitAsync.ts","../../src/utils/handleRequest.ts","../../src/utils/isPropertyAccessible.ts","../../src/utils/responseUtils.ts","../../src/utils/isNodeLikeError.ts"],"sourcesContent":["import { invariant } from 'outvariant'\nimport { DeferredPromise } from '@open-draft/deferred-promise'\nimport { InterceptorError } from './InterceptorError'\n\nconst kRequestHandled = Symbol('kRequestHandled')\nexport const kResponsePromise = Symbol('kResponsePromise')\n\nexport class RequestController {\n  /**\n   * Internal response promise.\n   * Available only for the library internals to grab the\n   * response instance provided by the developer.\n   * @note This promise cannot be rejected. It's either infinitely\n   * pending or resolved with whichever Response was passed to `respondWith()`.\n   */\n  [kResponsePromise]: DeferredPromise<Response | Error | undefined>;\n\n  /**\n   * Internal flag indicating if this request has been handled.\n   * @note The response promise becomes \"fulfilled\" on the next tick.\n   */\n  [kRequestHandled]: boolean\n\n  constructor(private request: Request) {\n    this[kRequestHandled] = false\n    this[kResponsePromise] = new DeferredPromise()\n  }\n\n  /**\n   * Respond to this request with the given `Response` instance.\n   * @example\n   * controller.respondWith(new Response())\n   * controller.respondWith(Response.json({ id }))\n   * controller.respondWith(Response.error())\n   */\n  public respondWith(response: Response): void {\n    invariant.as(\n      InterceptorError,\n      !this[kRequestHandled],\n      'Failed to respond to the \"%s %s\" request: the \"request\" event has already been handled.',\n      this.request.method,\n      this.request.url\n    )\n\n    this[kRequestHandled] = true\n    this[kResponsePromise].resolve(response)\n\n    /**\n     * @note The request conrtoller doesn't do anything\n     * apart from letting the interceptor await the response\n     * provided by the developer through the response promise.\n     * Each interceptor implements the actual respondWith/errorWith\n     * logic based on that interceptor's needs.\n     */\n  }\n\n  /**\n   * Error this request with the given error.\n   * @example\n   * controller.errorWith()\n   * controller.errorWith(new Error('Oops!'))\n   */\n  public errorWith(error?: Error): void {\n    invariant.as(\n      InterceptorError,\n      !this[kRequestHandled],\n      'Failed to error the \"%s %s\" request: the \"request\" event has already been handled.',\n      this.request.method,\n      this.request.url\n    )\n\n    this[kRequestHandled] = true\n\n    /**\n     * @note Resolve the response promise, not reject.\n     * This helps us differentiate between unhandled exceptions\n     * and intended errors (\"errorWith\") while waiting for the response.\n     */\n    this[kResponsePromise].resolve(error)\n  }\n}\n","export class InterceptorError extends Error {\n  constructor(message?: string) {\n    super(message)\n    this.name = 'InterceptorError'\n    Object.setPrototypeOf(this, InterceptorError.prototype)\n  }\n}\n","import { Emitter, EventMap } from 'strict-event-emitter'\n\n/**\n * Emits an event on the given emitter but executes\n * the listeners sequentially. This accounts for asynchronous\n * listeners (e.g. those having \"sleep\" and handling the request).\n */\nexport async function emitAsync<\n  Events extends EventMap,\n  EventName extends keyof Events\n>(\n  emitter: Emitter<Events>,\n  eventName: EventName,\n  ...data: Events[EventName]\n): Promise<void> {\n  const listners = emitter.listeners(eventName)\n\n  if (listners.length === 0) {\n    return\n  }\n\n  for (const listener of listners) {\n    await listener.apply(emitter, data)\n  }\n}\n","import type { Emitter } from 'strict-event-emitter'\nimport { DeferredPromise } from '@open-draft/deferred-promise'\nimport { until } from '@open-draft/until'\nimport type { HttpRequestEventMap } from '../glossary'\nimport { emitAsync } from './emitAsync'\nimport { kResponsePromise, RequestController } from '../RequestController'\nimport {\n  createServerErrorResponse,\n  isResponseError,\n  ResponseError,\n} from './responseUtils'\nimport { InterceptorError } from '../InterceptorError'\nimport { isNodeLikeError } from './isNodeLikeError'\n\ninterface HandleRequestOptions {\n  requestId: string\n  request: Request\n  emitter: Emitter<HttpRequestEventMap>\n  controller: RequestController\n\n  /**\n   * Called when the request has been handled\n   * with the given `Response` instance.\n   */\n  onResponse: (response: Response) => void | Promise<void>\n\n  /**\n   * Called when the request has been handled\n   * with the given `Response.error()` instance.\n   */\n  onRequestError: (response: ResponseError) => void\n\n  /**\n   * Called when an unhandled error happens during the\n   * request handling. This is never a thrown error/response.\n   */\n  onError: (error: unknown) => void\n}\n\n/**\n * @returns {Promise<boolean>} Indicates whether the request has been handled.\n */\nexport async function handleRequest(\n  options: HandleRequestOptions\n): Promise<boolean> {\n  const handleResponse = async (response: Response | Error) => {\n    if (response instanceof Error) {\n      options.onError(response)\n    }\n\n    // Handle \"Response.error()\" instances.\n    else if (isResponseError(response)) {\n      options.onRequestError(response)\n    } else {\n      await options.onResponse(response)\n    }\n\n    return true\n  }\n\n  const handleResponseError = async (error: unknown): Promise<boolean> => {\n    // Forward the special interceptor error instances\n    // to the developer. These must not be handled in any way.\n    if (error instanceof InterceptorError) {\n      throw result.error\n    }\n\n    // Support mocking Node.js-like errors.\n    if (isNodeLikeError(error)) {\n      options.onError(error)\n      return true\n    }\n\n    // Handle thrown responses.\n    if (error instanceof Response) {\n      return await handleResponse(error)\n    }\n\n    return false\n  }\n\n  // Add the last \"request\" listener to check if the request\n  // has been handled in any way. If it hasn't, resolve the\n  // response promise with undefined.\n  options.emitter.once('request', ({ requestId: pendingRequestId }) => {\n    if (pendingRequestId !== options.requestId) {\n      return\n    }\n\n    if (options.controller[kResponsePromise].state === 'pending') {\n      options.controller[kResponsePromise].resolve(undefined)\n    }\n  })\n\n  const requestAbortPromise = new DeferredPromise<void, unknown>()\n\n  /**\n   * @note `signal` is not always defined in React Native.\n   */\n  if (options.request.signal) {\n    if (options.request.signal.aborted) {\n      requestAbortPromise.reject(options.request.signal.reason)\n    } else {\n      options.request.signal.addEventListener(\n        'abort',\n        () => {\n          requestAbortPromise.reject(options.request.signal.reason)\n        },\n        { once: true }\n      )\n    }\n  }\n\n  const result = await until(async () => {\n    // Emit the \"request\" event and wait until all the listeners\n    // for that event are finished (e.g. async listeners awaited).\n    // By the end of this promise, the developer cannot affect the\n    // request anymore.\n    const requestListtenersPromise = emitAsync(options.emitter, 'request', {\n      requestId: options.requestId,\n      request: options.request,\n      controller: options.controller,\n    })\n\n    await Promise.race([\n      // Short-circuit the request handling promise if the request gets aborted.\n      requestAbortPromise,\n      requestListtenersPromise,\n      options.controller[kResponsePromise],\n    ])\n\n    // The response promise will settle immediately once\n    // the developer calls either \"respondWith\" or \"errorWith\".\n    const mockedResponse = await options.controller[kResponsePromise]\n    return mockedResponse\n  })\n\n  // Handle the request being aborted while waiting for the request listeners.\n  if (requestAbortPromise.state === 'rejected') {\n    options.onError(requestAbortPromise.rejectionReason)\n    return true\n  }\n\n  if (result.error) {\n    // Handle the error during the request listener execution.\n    // These can be thrown responses or request errors.\n    if (await handleResponseError(result.error)) {\n      return true\n    }\n\n    // If the developer has added \"unhandledException\" listeners,\n    // allow them to handle the error. They can translate it to a\n    // mocked response, network error, or forward it as-is.\n    if (options.emitter.listenerCount('unhandledException') > 0) {\n      // Create a new request controller just for the unhandled exception case.\n      // This is needed because the original controller might have been already\n      // interacted with (e.g. \"respondWith\" or \"errorWith\" called on it).\n      const unhandledExceptionController = new RequestController(\n        options.request\n      )\n\n      await emitAsync(options.emitter, 'unhandledException', {\n        error: result.error,\n        request: options.request,\n        requestId: options.requestId,\n        controller: unhandledExceptionController,\n      }).then(() => {\n        // If all the \"unhandledException\" listeners have finished\n        // but have not handled the response in any way, preemptively\n        // resolve the pending response promise from the new controller.\n        // This prevents it from hanging forever.\n        if (\n          unhandledExceptionController[kResponsePromise].state === 'pending'\n        ) {\n          unhandledExceptionController[kResponsePromise].resolve(undefined)\n        }\n      })\n\n      const nextResult = await until(\n        () => unhandledExceptionController[kResponsePromise]\n      )\n\n      /**\n       * @note Handle the result of the unhandled controller\n       * in the same way as the original request controller.\n       * The exception here is that thrown errors within the\n       * \"unhandledException\" event do NOT result in another\n       * emit of the same event. They are forwarded as-is.\n       */\n      if (nextResult.error) {\n        return handleResponseError(nextResult.error)\n      }\n\n      if (nextResult.data) {\n        return handleResponse(nextResult.data)\n      }\n    }\n\n    // Otherwise, coerce unhandled exceptions to a 500 Internal Server Error response.\n    options.onResponse(createServerErrorResponse(result.error))\n    return true\n  }\n\n  /**\n   * Handle a mocked Response instance.\n   * @note That this can also be an Error in case\n   * the developer called \"errorWith\". This differentiates\n   * unhandled exceptions from intended errors.\n   */\n  if (result.data) {\n    return handleResponse(result.data)\n  }\n\n  // In all other cases, consider the request unhandled.\n  // The interceptor must perform it as-is.\n  return false\n}\n","/**\n * A function that validates if property access is possible on an object\n * without throwing. It returns `true` if the property access is possible\n * and `false` otherwise.\n *\n * Environments like miniflare will throw on property access on certain objects\n * like Request and Response, for unimplemented properties.\n */\nexport function isPropertyAccessible<Obj extends Record<string, any>>(\n  obj: Obj,\n  key: keyof Obj\n) {\n  try {\n    obj[key]\n    return true\n  } catch {\n    return false\n  }\n}\n","import { isPropertyAccessible } from './isPropertyAccessible'\n\n/**\n * Creates a generic 500 Unhandled Exception response.\n */\nexport function createServerErrorResponse(body: unknown): Response {\n  return new Response(\n    JSON.stringify(\n      body instanceof Error\n        ? {\n            name: body.name,\n            message: body.message,\n            stack: body.stack,\n          }\n        : body\n    ),\n    {\n      status: 500,\n      statusText: 'Unhandled Exception',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n    }\n  )\n}\n\nexport type ResponseError = Response & { type: 'error' }\n\n/**\n * Check if the given response is a `Response.error()`.\n *\n * @note Some environments, like Miniflare (Cloudflare) do not\n * implement the \"Response.type\" property and throw on its access.\n * Safely check if we can access \"type\" on \"Response\" before continuing.\n * @see https://github.com/mswjs/msw/issues/1834\n */\nexport function isResponseError(response: Response): response is ResponseError {\n  return isPropertyAccessible(response, 'type') && response.type === 'error'\n}\n","export function isNodeLikeError(\n  error: unknown\n): error is NodeJS.ErrnoException {\n  if (error == null) {\n    return false\n  }\n\n  if (!(error instanceof Error)) {\n    return false\n  }\n\n  return 'code' in error && 'errno' in error\n}\n"],"mappings":"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","names":["DeferredPromise","DeferredPromise"]}