Files
Shifted/node_modules/@mswjs/interceptors/lib/node/fetchUtils-CoU35g3M.mjs
2026-02-10 01:14:19 +00:00

359 lines
11 KiB
JavaScript

import { Logger } from "@open-draft/logger";
import { Emitter } from "strict-event-emitter";
import { DeferredPromise } from "@open-draft/deferred-promise";
import { invariant } from "outvariant";
//#region src/Interceptor.ts
/**
* Request header name to detect when a single request
* is being handled by nested interceptors (XHR -> ClientRequest).
* Obscure by design to prevent collisions with user-defined headers.
* Ideally, come up with the Interceptor-level mechanism for this.
* @see https://github.com/mswjs/interceptors/issues/378
*/
const INTERNAL_REQUEST_ID_HEADER_NAME = "x-interceptors-internal-request-id";
function getGlobalSymbol(symbol) {
return globalThis[symbol] || void 0;
}
function setGlobalSymbol(symbol, value) {
globalThis[symbol] = value;
}
function deleteGlobalSymbol(symbol) {
delete globalThis[symbol];
}
let InterceptorReadyState = /* @__PURE__ */ function(InterceptorReadyState$1) {
InterceptorReadyState$1["INACTIVE"] = "INACTIVE";
InterceptorReadyState$1["APPLYING"] = "APPLYING";
InterceptorReadyState$1["APPLIED"] = "APPLIED";
InterceptorReadyState$1["DISPOSING"] = "DISPOSING";
InterceptorReadyState$1["DISPOSED"] = "DISPOSED";
return InterceptorReadyState$1;
}({});
var Interceptor = class {
constructor(symbol) {
this.symbol = symbol;
this.readyState = InterceptorReadyState.INACTIVE;
this.emitter = new Emitter();
this.subscriptions = [];
this.logger = new Logger(symbol.description);
this.emitter.setMaxListeners(0);
this.logger.info("constructing the interceptor...");
}
/**
* Determine if this interceptor can be applied
* in the current environment.
*/
checkEnvironment() {
return true;
}
/**
* Apply this interceptor to the current process.
* Returns an already running interceptor instance if it's present.
*/
apply() {
const logger = this.logger.extend("apply");
logger.info("applying the interceptor...");
if (this.readyState === InterceptorReadyState.APPLIED) {
logger.info("intercepted already applied!");
return;
}
if (!this.checkEnvironment()) {
logger.info("the interceptor cannot be applied in this environment!");
return;
}
this.readyState = InterceptorReadyState.APPLYING;
const runningInstance = this.getInstance();
if (runningInstance) {
logger.info("found a running instance, reusing...");
this.on = (event, listener) => {
logger.info("proxying the \"%s\" listener", event);
runningInstance.emitter.addListener(event, listener);
this.subscriptions.push(() => {
runningInstance.emitter.removeListener(event, listener);
logger.info("removed proxied \"%s\" listener!", event);
});
return this;
};
this.readyState = InterceptorReadyState.APPLIED;
return;
}
logger.info("no running instance found, setting up a new instance...");
this.setup();
this.setInstance();
this.readyState = InterceptorReadyState.APPLIED;
}
/**
* Setup the module augments and stubs necessary for this interceptor.
* This method is not run if there's a running interceptor instance
* to prevent instantiating an interceptor multiple times.
*/
setup() {}
/**
* Listen to the interceptor's public events.
*/
on(event, listener) {
const logger = this.logger.extend("on");
if (this.readyState === InterceptorReadyState.DISPOSING || this.readyState === InterceptorReadyState.DISPOSED) {
logger.info("cannot listen to events, already disposed!");
return this;
}
logger.info("adding \"%s\" event listener:", event, listener);
this.emitter.on(event, listener);
return this;
}
once(event, listener) {
this.emitter.once(event, listener);
return this;
}
off(event, listener) {
this.emitter.off(event, listener);
return this;
}
removeAllListeners(event) {
this.emitter.removeAllListeners(event);
return this;
}
/**
* Disposes of any side-effects this interceptor has introduced.
*/
dispose() {
const logger = this.logger.extend("dispose");
if (this.readyState === InterceptorReadyState.DISPOSED) {
logger.info("cannot dispose, already disposed!");
return;
}
logger.info("disposing the interceptor...");
this.readyState = InterceptorReadyState.DISPOSING;
if (!this.getInstance()) {
logger.info("no interceptors running, skipping dispose...");
return;
}
this.clearInstance();
logger.info("global symbol deleted:", getGlobalSymbol(this.symbol));
if (this.subscriptions.length > 0) {
logger.info("disposing of %d subscriptions...", this.subscriptions.length);
for (const dispose of this.subscriptions) dispose();
this.subscriptions = [];
logger.info("disposed of all subscriptions!", this.subscriptions.length);
}
this.emitter.removeAllListeners();
logger.info("destroyed the listener!");
this.readyState = InterceptorReadyState.DISPOSED;
}
getInstance() {
const instance = getGlobalSymbol(this.symbol);
this.logger.info("retrieved global instance:", instance?.constructor?.name);
return instance;
}
setInstance() {
setGlobalSymbol(this.symbol, this);
this.logger.info("set global instance!", this.symbol.description);
}
clearInstance() {
deleteGlobalSymbol(this.symbol);
this.logger.info("cleared global instance!", this.symbol.description);
}
};
//#endregion
//#region src/InterceptorError.ts
var InterceptorError = class InterceptorError extends Error {
constructor(message) {
super(message);
this.name = "InterceptorError";
Object.setPrototypeOf(this, InterceptorError.prototype);
}
};
//#endregion
//#region src/RequestController.ts
var RequestController = class RequestController {
static {
this.PENDING = 0;
}
static {
this.PASSTHROUGH = 1;
}
static {
this.RESPONSE = 2;
}
static {
this.ERROR = 3;
}
constructor(request, source) {
this.request = request;
this.source = source;
this.readyState = RequestController.PENDING;
this.handled = new DeferredPromise();
}
get #handled() {
return this.handled;
}
/**
* Perform this request as-is.
*/
async passthrough() {
invariant.as(InterceptorError, this.readyState === RequestController.PENDING, "Failed to passthrough the \"%s %s\" request: the request has already been handled", this.request.method, this.request.url);
this.readyState = RequestController.PASSTHROUGH;
await this.source.passthrough();
this.#handled.resolve();
}
/**
* Respond to this request with the given `Response` instance.
*
* @example
* controller.respondWith(new Response())
* controller.respondWith(Response.json({ id }))
* controller.respondWith(Response.error())
*/
respondWith(response) {
invariant.as(InterceptorError, this.readyState === RequestController.PENDING, "Failed to respond to the \"%s %s\" request with \"%d %s\": the request has already been handled (%d)", this.request.method, this.request.url, response.status, response.statusText || "OK", this.readyState);
this.readyState = RequestController.RESPONSE;
this.#handled.resolve();
/**
* @note Although `source.respondWith()` is potentially asynchronous,
* do NOT await it for backward-compatibility. Awaiting it will short-circuit
* the request listener invocation as soon as a listener responds to a request.
* Ideally, that's what we want, but that's not what we promise the user.
*/
this.source.respondWith(response);
}
/**
* Error this request with the given reason.
*
* @example
* controller.errorWith()
* controller.errorWith(new Error('Oops!'))
* controller.errorWith({ message: 'Oops!'})
*/
errorWith(reason) {
invariant.as(InterceptorError, this.readyState === RequestController.PENDING, "Failed to error the \"%s %s\" request with \"%s\": the request has already been handled (%d)", this.request.method, this.request.url, reason?.toString(), this.readyState);
this.readyState = RequestController.ERROR;
this.source.errorWith(reason);
this.#handled.resolve();
}
};
//#endregion
//#region src/createRequestId.ts
/**
* Generate a random ID string to represent a request.
* @example
* createRequestId()
* // "f774b6c9c600f"
*/
function createRequestId() {
return Math.random().toString(16).slice(2);
}
//#endregion
//#region src/utils/canParseUrl.ts
/**
* Returns a boolean indicating whether the given URL string
* can be parsed into a `URL` instance.
* A substitute for `URL.canParse()` for Node.js 18.
*/
function canParseUrl(url) {
try {
new URL(url);
return true;
} catch (_error) {
return false;
}
}
//#endregion
//#region src/utils/getValueBySymbol.ts
/**
* Returns the value behind the symbol with the given name.
*/
function getValueBySymbol(symbolName, source) {
const symbol = Object.getOwnPropertySymbols(source).find((symbol$1) => {
return symbol$1.description === symbolName;
});
if (symbol) return Reflect.get(source, symbol);
}
//#endregion
//#region src/utils/fetchUtils.ts
var FetchResponse = class FetchResponse extends Response {
static {
this.STATUS_CODES_WITHOUT_BODY = [
101,
103,
204,
205,
304
];
}
static {
this.STATUS_CODES_WITH_REDIRECT = [
301,
302,
303,
307,
308
];
}
static isConfigurableStatusCode(status) {
return status >= 200 && status <= 599;
}
static isRedirectResponse(status) {
return FetchResponse.STATUS_CODES_WITH_REDIRECT.includes(status);
}
/**
* Returns a boolean indicating whether the given response status
* code represents a response that can have a body.
*/
static isResponseWithBody(status) {
return !FetchResponse.STATUS_CODES_WITHOUT_BODY.includes(status);
}
static setUrl(url, response) {
if (!url || url === "about:" || !canParseUrl(url)) return;
const state = getValueBySymbol("state", response);
if (state) state.urlList.push(new URL(url));
else Object.defineProperty(response, "url", {
value: url,
enumerable: true,
configurable: true,
writable: false
});
}
/**
* Parses the given raw HTTP headers into a Fetch API `Headers` instance.
*/
static parseRawHeaders(rawHeaders) {
const headers = new Headers();
for (let line = 0; line < rawHeaders.length; line += 2) headers.append(rawHeaders[line], rawHeaders[line + 1]);
return headers;
}
constructor(body, init = {}) {
const status = init.status ?? 200;
const safeStatus = FetchResponse.isConfigurableStatusCode(status) ? status : 200;
const finalBody = FetchResponse.isResponseWithBody(status) ? body : null;
super(finalBody, {
status: safeStatus,
statusText: init.statusText,
headers: init.headers
});
if (status !== safeStatus) {
/**
* @note Undici keeps an internal "Symbol(state)" that holds
* the actual value of response status. Update that in Node.js.
*/
const state = getValueBySymbol("state", this);
if (state) state.status = status;
else Object.defineProperty(this, "status", {
value: status,
enumerable: true,
configurable: true,
writable: false
});
}
FetchResponse.setUrl(init.url, this);
}
};
//#endregion
export { InterceptorError as a, InterceptorReadyState as c, RequestController as i, deleteGlobalSymbol as l, canParseUrl as n, INTERNAL_REQUEST_ID_HEADER_NAME as o, createRequestId as r, Interceptor as s, FetchResponse as t, getGlobalSymbol as u };
//# sourceMappingURL=fetchUtils-CoU35g3M.mjs.map