{"version":3,"file":"agent.mjs","names":[],"sources":["../src/agent.ts"],"sourcesContent":["/* -------------------------------------------------------------------\n\n                       ⚡ Storm Software - Stryke\n\n This code was released as part of the Stryke project. Stryke\n is maintained by Storm Software under the Apache-2.0 license, and is\n free for commercial and private use. For more information, please visit\n our licensing page at https://stormsoftware.com/licenses/projects/stryke.\n\n Website:                  https://stormsoftware.com\n Repository:               https://github.com/storm-software/stryke\n Documentation:            https://docs.stormsoftware.com/projects/stryke\n Contact:                  https://stormsoftware.com/contact\n\n SPDX-License-Identifier:  Apache-2.0\n\n ------------------------------------------------------------------- */\n\nimport { isBoolean, isString } from \"@stryke/type-checks\";\nimport * as http from \"node:http\";\nimport type { Agent as HttpsAgent } from \"node:https\";\nimport * as net from \"node:net\";\nimport type { Duplex } from \"node:stream\";\nimport type * as tls from \"node:tls\";\n\ninterface HttpConnectOpts extends net.TcpNetConnectOpts {\n  secureEndpoint: false;\n  protocol?: string;\n}\n\ninterface HttpsConnectOpts extends tls.ConnectionOptions {\n  secureEndpoint: true;\n  protocol?: string;\n  port: number;\n}\n\nexport type AgentConnectOpts = HttpConnectOpts | HttpsConnectOpts;\n\nconst SYMBOL_INTERNAL = Symbol(\"Agent\");\n\ninterface InternalState {\n  defaultPort?: number;\n  protocol?: string;\n  currentSocket?: Duplex;\n}\n\nexport abstract class Agent extends http.Agent {\n  private [SYMBOL_INTERNAL]: InternalState;\n\n  // Set by `http.Agent` - missing from `@types/node`\n  options!: Partial<net.TcpNetConnectOpts & tls.ConnectionOptions>;\n\n  keepAlive!: boolean;\n\n  constructor(opts?: http.AgentOptions) {\n    super(opts);\n    this[SYMBOL_INTERNAL] = {};\n  }\n\n  abstract connect(\n    req: http.ClientRequest,\n    options: AgentConnectOpts\n  ): Promise<Duplex | http.Agent> | Duplex | http.Agent;\n\n  /**\n   * Determine whether this is an `http` or `https` request.\n   */\n  isSecureEndpoint(options?: AgentConnectOpts): boolean {\n    if (options) {\n      // First check the `secureEndpoint` property explicitly, since this\n      // means that a parent `Agent` is \"passing through\" to this instance.\n      if (isBoolean((options as any).secureEndpoint)) {\n        return options.secureEndpoint;\n      }\n\n      // If no explicit `secure` endpoint, check if `protocol` property is\n      // set. This will usually be the case since using a full string URL\n      // or `URL` instance should be the most common usage.\n      if (isString(options.protocol)) {\n        return options.protocol === \"https:\";\n      }\n    }\n\n    // Finally, if no `protocol` property was set, then fall back to\n    // checking the stack trace of the current call stack, and try to\n    // detect the \"https\" module.\n    const { stack } = new Error(\" \");\n    if (!isString(stack)) {\n      return false;\n    }\n\n    return stack\n      .split(\"\\n\")\n      .some(l => l.includes(\"(https.js:\") || l.includes(\"node:https:\"));\n  }\n\n  // In order to support async signatures in `connect()` and Node's native\n  // connection pooling in `http.Agent`, the array of sockets for each origin\n  // has to be updated synchronously. This is so the length of the array is\n  // accurate when `addRequest()` is next called. We achieve this by creating a\n  // fake socket and adding it to `sockets[origin]` and incrementing\n  // `totalSocketCount`.\n  private incrementSockets(name: string) {\n    // If `maxSockets` and `maxTotalSockets` are both Infinity then there is no\n    // need to create a fake socket because Node.js native connection pooling\n    // will never be invoked.\n    if (this.maxSockets === Infinity && this.maxTotalSockets === Infinity) {\n      return null;\n    }\n    // All instances of `sockets` are expected TypeScript errors. The\n    // alternative is to add it as a private property of this class but that\n    // will break TypeScript sub-classing.\n\n    // @ts-expect-error `sockets` is readonly in `@types/node`\n    this.sockets[name] ??= [];\n\n    const fakeSocket = new net.Socket({ writable: false });\n    this.sockets[name].push(fakeSocket);\n    // @ts-expect-error `totalSocketCount` isn't defined in `@types/node`\n    this.totalSocketCount++;\n    return fakeSocket;\n  }\n\n  private decrementSockets(name: string, socket: null | net.Socket) {\n    if (!this.sockets[name] || socket === null) {\n      return;\n    }\n    const sockets = this.sockets[name];\n    const index = sockets.indexOf(socket);\n    if (index !== -1) {\n      sockets.splice(index, 1);\n      // @ts-expect-error  `totalSocketCount` isn't defined in `@types/node`\n      this.totalSocketCount--;\n      if (sockets.length === 0) {\n        // @ts-expect-error `sockets` is readonly in `@types/node`\n        delete this.sockets[name];\n      }\n    }\n  }\n\n  // In order to properly update the socket pool, we need to call `getName()` on\n  // the core `https.Agent` if it is a secureEndpoint.\n  override getName(options?: AgentConnectOpts): string {\n    const secureEndpoint = this.isSecureEndpoint(options);\n    if (secureEndpoint) {\n      // @ts-expect-error `getName()` isn't defined in `@types/node`\n      return HttpsAgent.prototype.getName.call(this, options);\n    }\n\n    return super.getName(options);\n  }\n\n  createSocket(\n    req: http.ClientRequest,\n    options: AgentConnectOpts,\n    cb: (err: Error | null, s?: Duplex) => void\n  ) {\n    const connectOpts = {\n      ...options,\n      secureEndpoint: this.isSecureEndpoint(options)\n    };\n    const name = this.getName(connectOpts);\n    const fakeSocket = this.incrementSockets(name);\n    Promise.resolve()\n      .then(async () => this.connect(req, connectOpts))\n      .then(\n        socket => {\n          this.decrementSockets(name, fakeSocket);\n          if (socket instanceof http.Agent) {\n            try {\n              // @ts-expect-error `addRequest()` isn't defined in `@types/node`\n              // eslint-disable-next-line ts/no-unsafe-call\n              return socket.addRequest(req, connectOpts);\n            } catch (err: unknown) {\n              return cb(err as Error);\n            }\n          }\n          this[SYMBOL_INTERNAL].currentSocket = socket;\n          // @ts-expect-error `createSocket()` isn't defined in `@types/node`\n          // eslint-disable-next-line ts/no-unsafe-call\n          super.createSocket(req, options, cb);\n        },\n        err => {\n          this.decrementSockets(name, fakeSocket);\n          cb(err);\n        }\n      );\n  }\n\n  override createConnection(): Duplex {\n    const socket = this[SYMBOL_INTERNAL].currentSocket;\n    this[SYMBOL_INTERNAL].currentSocket = undefined;\n    if (!socket) {\n      throw new Error(\"No socket was returned in the `connect()` function\");\n    }\n    return socket;\n  }\n\n  get defaultPort(): number {\n    return (\n      this[SYMBOL_INTERNAL].defaultPort ??\n      (this.protocol === \"https:\" ? 443 : 80)\n    );\n  }\n\n  set defaultPort(v: number) {\n    if (this[SYMBOL_INTERNAL]) {\n      this[SYMBOL_INTERNAL].defaultPort = v;\n    }\n  }\n\n  get protocol(): string {\n    return (\n      this[SYMBOL_INTERNAL].protocol ??\n      (this.isSecureEndpoint() ? \"https:\" : \"http:\")\n    );\n  }\n\n  set protocol(v: string) {\n    if (this[SYMBOL_INTERNAL]) {\n      this[SYMBOL_INTERNAL].protocol = v;\n    }\n  }\n}\n"],"mappings":";;;;;AAsCA,MAAM,kBAAkB,OAAO,QAAQ;AAQvC,IAAsB,QAAtB,cAAoC,KAAK,MAAM;CAC7C,CAAS;CAGT;CAEA;CAEA,YAAY,MAA0B;AACpC,QAAM,KAAK;AACX,OAAK,mBAAmB,EAAE;;;;;CAW5B,iBAAiB,SAAqC;AACpD,MAAI,SAAS;AAGX,OAAI,UAAW,QAAgB,eAAe,CAC5C,QAAO,QAAQ;AAMjB,OAAI,SAAS,QAAQ,SAAS,CAC5B,QAAO,QAAQ,aAAa;;EAOhC,MAAM,EAAE,0BAAU,IAAI,MAAM,IAAI;AAChC,MAAI,CAAC,SAAS,MAAM,CAClB,QAAO;AAGT,SAAO,MACJ,MAAM,KAAK,CACX,MAAK,MAAK,EAAE,SAAS,aAAa,IAAI,EAAE,SAAS,cAAc,CAAC;;CASrE,AAAQ,iBAAiB,MAAc;AAIrC,MAAI,KAAK,eAAe,YAAY,KAAK,oBAAoB,SAC3D,QAAO;AAOT,OAAK,QAAQ,UAAU,EAAE;EAEzB,MAAM,aAAa,IAAI,IAAI,OAAO,EAAE,UAAU,OAAO,CAAC;AACtD,OAAK,QAAQ,MAAM,KAAK,WAAW;AAEnC,OAAK;AACL,SAAO;;CAGT,AAAQ,iBAAiB,MAAc,QAA2B;AAChE,MAAI,CAAC,KAAK,QAAQ,SAAS,WAAW,KACpC;EAEF,MAAM,UAAU,KAAK,QAAQ;EAC7B,MAAM,QAAQ,QAAQ,QAAQ,OAAO;AACrC,MAAI,UAAU,IAAI;AAChB,WAAQ,OAAO,OAAO,EAAE;AAExB,QAAK;AACL,OAAI,QAAQ,WAAW,EAErB,QAAO,KAAK,QAAQ;;;CAO1B,AAAS,QAAQ,SAAoC;AAEnD,MADuB,KAAK,iBAAiB,QAC3B,CAEhB,QAAO,WAAW,UAAU,QAAQ,KAAK,MAAM,QAAQ;AAGzD,SAAO,MAAM,QAAQ,QAAQ;;CAG/B,aACE,KACA,SACA,IACA;EACA,MAAM,cAAc;GAClB,GAAG;GACH,gBAAgB,KAAK,iBAAiB,QAAQ;GAC/C;EACD,MAAM,OAAO,KAAK,QAAQ,YAAY;EACtC,MAAM,aAAa,KAAK,iBAAiB,KAAK;AAC9C,UAAQ,SAAS,CACd,KAAK,YAAY,KAAK,QAAQ,KAAK,YAAY,CAAC,CAChD,MACC,WAAU;AACR,QAAK,iBAAiB,MAAM,WAAW;AACvC,OAAI,kBAAkB,KAAK,MACzB,KAAI;AAGF,WAAO,OAAO,WAAW,KAAK,YAAY;YACnC,KAAc;AACrB,WAAO,GAAG,IAAa;;AAG3B,QAAK,iBAAiB,gBAAgB;AAGtC,SAAM,aAAa,KAAK,SAAS,GAAG;MAEtC,QAAO;AACL,QAAK,iBAAiB,MAAM,WAAW;AACvC,MAAG,IAAI;IAEV;;CAGL,AAAS,mBAA2B;EAClC,MAAM,SAAS,KAAK,iBAAiB;AACrC,OAAK,iBAAiB,gBAAgB;AACtC,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,qDAAqD;AAEvE,SAAO;;CAGT,IAAI,cAAsB;AACxB,SACE,KAAK,iBAAiB,gBACrB,KAAK,aAAa,WAAW,MAAM;;CAIxC,IAAI,YAAY,GAAW;AACzB,MAAI,KAAK,iBACP,MAAK,iBAAiB,cAAc;;CAIxC,IAAI,WAAmB;AACrB,SACE,KAAK,iBAAiB,aACrB,KAAK,kBAAkB,GAAG,WAAW;;CAI1C,IAAI,SAAS,GAAW;AACtB,MAAI,KAAK,iBACP,MAAK,iBAAiB,WAAW"}