{"version":3,"file":"await-response.mjs","names":[],"sources":["../../src/await/await-response.ts"],"sourcesContent":["import type { SingleConnection } from \"~/client/connection\";\nimport type { ClientEvents } from \"~/client/interface\";\nimport type { IRCMessage } from \"~/message/irc/irc-message\";\nimport { TimeoutError } from \"./timeout-error\";\nimport { ConnectionError, MessageError } from \"~/client/errors\";\nimport { setDefaults } from \"~/utils/set-defaults\";\n\nexport type Condition = (message: IRCMessage) => boolean;\nexport type NoResponseAction = \"success\" | \"failure\";\n\nexport interface AwaitConfig {\n  /**\n   * If this condition evaluates to true on any incoming message, the promise is resolved with the message\n   * that matched.\n   */\n  success?: Condition;\n\n  /**\n   * If this condition evaluates to true on any incoming message, the promise is rejected with an\n   * error specifying the cause message.\n   */\n  failure?: Condition;\n\n  /**\n   * If neither the success or failure condition match on any message within\n   * this period (after connection, {@link noResponseAction} is taken.\n   */\n  timeout?: number;\n\n  /**\n   * Action to take after\n   *   - a timeout occurs or\n   *   - a response awaited later than this response is resolved or rejected\n   *     (and given that since the server processes commands\n   *     and sends their responses strictly sequentially) this response would\n   *     never be fulfilled because the server is done processing this command\n   *\n   *     E.g. the client issues <code>JOIN #a,#b,#c</code> to the server,\n   *     and receives the responses for <code>a</code> and <code>c</code>,\n   *     in that order. In that case, the response for <code>b</code> can be\n   *     rejected the moment the response for <code>c</code> is received.\n   */\n  noResponseAction?: NoResponseAction;\n\n  /**\n   * Function to create custom error type given optional message and\n   * cause error.\n   *\n   * @param message Optional message\n   * @param cause Optional cause\n   */\n  errorType: (message: string, cause?: Error) => Error;\n\n  /**\n   * Custom error message to pass to the {@link errorType} function\n   * as the message, preferably about what kind of response to what\n   * input variables was awaited (e.g. channel name)\n   */\n  errorMessage: string;\n}\n\nconst configDefaults = {\n  success: () => false,\n  failure: () => false,\n  timeout: 2000,\n  noResponseAction: \"failure\",\n};\n\nexport class ResponseAwaiter {\n  public readonly promise: Promise<IRCMessage | undefined>;\n\n  private readonly unsubscribers: (() => void)[] = [];\n  private readonly conn: SingleConnection;\n  private readonly config: Required<AwaitConfig>;\n  private resolvePromise!: (message: IRCMessage | undefined) => void;\n  private rejectPromise!: (reason: Error) => void;\n\n  public constructor(conn: SingleConnection, config: AwaitConfig) {\n    this.conn = conn;\n    this.config = setDefaults(config, configDefaults);\n\n    this.promise = new Promise((resolve, reject) => {\n      this.resolvePromise = resolve;\n      this.rejectPromise = reject;\n    });\n\n    this.subscribeTo(\"close\", this.onConnectionClosed.bind(this));\n    this.joinPendingResponsesQueue();\n  }\n\n  /**\n   * Called when this response awaiter is inserted to the head of\n   * the queue or moves to the queue head after a previous\n   * response awaiter was rejected or resolved.\n   */\n  public movedToQueueHead(): void {\n    if (this.conn.connected || this.conn.ready) {\n      this.beginTimeout();\n    } else {\n      const listener = this.beginTimeout.bind(this);\n      this.conn.once(\"connect\", listener);\n      this.unsubscribers.push(() =>\n        this.conn.removeListener(\"connect\", listener),\n      );\n    }\n  }\n\n  /**\n   * Called by a later awaiter indicating that this awaiter was still\n   * in the queue while the later awaiter matched a response.\n   */\n  public outpaced(): void {\n    this.onNoResponse(\n      \"A response to a command issued later than this command was received\",\n    );\n  }\n\n  private unsubscribe(): void {\n    for (const function_ of this.unsubscribers) function_();\n  }\n\n  private resolve(message?: IRCMessage): void {\n    this.unsubscribe();\n    this.resolvePromise(message);\n  }\n\n  private reject(cause: Error): void {\n    this.unsubscribe();\n    const errorWithCause = this.config.errorType(\n      this.config.errorMessage,\n      cause,\n    );\n    process.nextTick(() => this.conn.emitError(errorWithCause, true));\n    this.rejectPromise(errorWithCause);\n  }\n\n  private onNoResponse(reason: string): void {\n    if (this.config.noResponseAction === \"failure\") {\n      this.reject(new TimeoutError(reason));\n    } else {\n      this.resolve();\n    }\n  }\n\n  private beginTimeout(): void {\n    const registeredTimeout = setTimeout(() => {\n      const reason = `Timed out after waiting for response for ${this.config.timeout} milliseconds`;\n      this.onNoResponse(reason);\n    }, this.config.timeout);\n\n    this.unsubscribers.push(() => {\n      clearTimeout(registeredTimeout);\n    });\n  }\n\n  private joinPendingResponsesQueue(): void {\n    const ourIndex = this.conn.pendingResponses.push(this) - 1;\n    if (ourIndex === 0) {\n      this.movedToQueueHead();\n    } // else: we are behind another awaiter\n    // which will notify us via #movedToQueueHead() that we should\n    // begin the timeout\n\n    this.unsubscribers.push(() => {\n      const selfPosition = this.conn.pendingResponses.indexOf(this);\n\n      if (selfPosition === -1) {\n        // we are not in the queue anymore (e.g. sliced off by other\n        // awaiter)\n        return;\n      }\n\n      // remove all awaiters, leading up to ourself\n      const removedAwaiters = this.conn.pendingResponses.splice(\n        0,\n        selfPosition + 1,\n      );\n\n      // remove ourself\n      removedAwaiters.pop();\n\n      // notify the other awaiters they were outpaced\n      for (const awaiter of removedAwaiters) awaiter.outpaced();\n\n      // notify the new queue head to begin its timeout\n      const newQueueHead = this.conn.pendingResponses[0];\n      if (newQueueHead != null) {\n        newQueueHead.movedToQueueHead();\n      }\n    });\n  }\n\n  private onConnectionClosed(cause?: Error): void {\n    if (cause == null) {\n      this.reject(new ConnectionError(\"Connection closed with no error\"));\n    } else {\n      this.reject(new ConnectionError(\"Connection closed due to error\", cause));\n    }\n  }\n\n  // returns true if something matched, preventing \"later\" matchers from\n  // running against that message\n  public onConnectionMessage(message: IRCMessage): boolean {\n    if (this.config.failure(message)) {\n      this.reject(\n        new MessageError(`Bad response message: ${message.rawSource}`),\n      );\n      return true;\n    } else if (this.config.success(message)) {\n      this.resolve(message);\n      return true;\n    }\n    return false;\n  }\n\n  private subscribeTo<T extends keyof ClientEvents>(\n    eventName: T,\n    handler: (...args: ClientEvents[T]) => unknown,\n  ): void {\n    handler = handler.bind(this);\n    this.conn.on(eventName, handler);\n    this.unsubscribers.push(() => this.conn.removeListener(eventName, handler));\n  }\n}\n\nexport async function awaitResponse<\n  // eslint-disable-next-line ts/no-unnecessary-type-parameters -- not detected correctly\n  TMessage extends IRCMessage = IRCMessage,\n  const TNoResponseAction extends NoResponseAction | undefined = undefined,\n>(\n  conn: SingleConnection,\n  config: AwaitConfig & {\n    noResponseAction?: TNoResponseAction;\n    success?: ((message: IRCMessage) => message is TMessage) | Condition;\n  },\n) {\n  return new ResponseAwaiter(conn, config).promise as Promise<\n    TNoResponseAction extends \"success\" ? TMessage | undefined : TMessage\n  >;\n}\n"],"mappings":";;;;AA6DA,MAAM,iBAAiB;CACrB,eAAe;CACf,eAAe;CACf,SAAS;CACT,kBAAkB;CACnB;AAED,IAAa,kBAAb,MAA6B;CAC3B;CAEA,gBAAiD,EAAE;CACnD;CACA;CACA;CACA;CAEA,YAAmB,MAAwB,QAAqB;AAC9D,OAAK,OAAO;AACZ,OAAK,SAAS,YAAY,QAAQ,eAAe;AAEjD,OAAK,UAAU,IAAI,SAAS,SAAS,WAAW;AAC9C,QAAK,iBAAiB;AACtB,QAAK,gBAAgB;IACrB;AAEF,OAAK,YAAY,SAAS,KAAK,mBAAmB,KAAK,KAAK,CAAC;AAC7D,OAAK,2BAA2B;;;;;;;CAQlC,mBAAgC;AAC9B,MAAI,KAAK,KAAK,aAAa,KAAK,KAAK,MACnC,MAAK,cAAc;OACd;GACL,MAAM,WAAW,KAAK,aAAa,KAAK,KAAK;AAC7C,QAAK,KAAK,KAAK,WAAW,SAAS;AACnC,QAAK,cAAc,WACjB,KAAK,KAAK,eAAe,WAAW,SAAS,CAC9C;;;;;;;CAQL,WAAwB;AACtB,OAAK,aACH,sEACD;;CAGH,cAA4B;AAC1B,OAAK,MAAM,aAAa,KAAK,cAAe,YAAW;;CAGzD,QAAgB,SAA4B;AAC1C,OAAK,aAAa;AAClB,OAAK,eAAe,QAAQ;;CAG9B,OAAe,OAAoB;AACjC,OAAK,aAAa;EAClB,MAAM,iBAAiB,KAAK,OAAO,UACjC,KAAK,OAAO,cACZ,MACD;AACD,UAAQ,eAAe,KAAK,KAAK,UAAU,gBAAgB,KAAK,CAAC;AACjE,OAAK,cAAc,eAAe;;CAGpC,aAAqB,QAAsB;AACzC,MAAI,KAAK,OAAO,qBAAqB,UACnC,MAAK,OAAO,IAAI,aAAa,OAAO,CAAC;MAErC,MAAK,SAAS;;CAIlB,eAA6B;EAC3B,MAAM,oBAAoB,iBAAiB;GACzC,MAAM,SAAS,4CAA4C,KAAK,OAAO,QAAQ;AAC/E,QAAK,aAAa,OAAO;KACxB,KAAK,OAAO,QAAQ;AAEvB,OAAK,cAAc,WAAW;AAC5B,gBAAa,kBAAkB;IAC/B;;CAGJ,4BAA0C;AAExC,MADiB,KAAK,KAAK,iBAAiB,KAAK,KAAK,GAAG,MACxC,EACf,MAAK,kBAAkB;AAKzB,OAAK,cAAc,WAAW;GAC5B,MAAM,eAAe,KAAK,KAAK,iBAAiB,QAAQ,KAAK;AAE7D,OAAI,iBAAiB,GAGnB;GAIF,MAAM,kBAAkB,KAAK,KAAK,iBAAiB,OACjD,GACA,eAAe,EAChB;AAGD,mBAAgB,KAAK;AAGrB,QAAK,MAAM,WAAW,gBAAiB,SAAQ,UAAU;GAGzD,MAAM,eAAe,KAAK,KAAK,iBAAiB;AAChD,OAAI,gBAAgB,KAClB,cAAa,kBAAkB;IAEjC;;CAGJ,mBAA2B,OAAqB;AAC9C,MAAI,SAAS,KACX,MAAK,OAAO,IAAI,gBAAgB,kCAAkC,CAAC;MAEnE,MAAK,OAAO,IAAI,gBAAgB,kCAAkC,MAAM,CAAC;;CAM7E,oBAA2B,SAA8B;AACvD,MAAI,KAAK,OAAO,QAAQ,QAAQ,EAAE;AAChC,QAAK,OACH,IAAI,aAAa,yBAAyB,QAAQ,YAAY,CAC/D;AACD,UAAO;aACE,KAAK,OAAO,QAAQ,QAAQ,EAAE;AACvC,QAAK,QAAQ,QAAQ;AACrB,UAAO;;AAET,SAAO;;CAGT,YACE,WACA,SACM;AACN,YAAU,QAAQ,KAAK,KAAK;AAC5B,OAAK,KAAK,GAAG,WAAW,QAAQ;AAChC,OAAK,cAAc,WAAW,KAAK,KAAK,eAAe,WAAW,QAAQ,CAAC;;;AAI/E,eAAsB,cAKpB,MACA,QAIA;AACA,QAAO,IAAI,gBAAgB,MAAM,OAAO,CAAC"}