{"version":3,"file":"session.cjs","names":["EventEmitter","toUint8Array","concatArrayBuffer","WS_STATES","startSessionMessage: LiveV2StartSessionMessage","message: unknown"],"sources":["../../../src/v2/live/session.ts"],"sourcesContent":["import { EventEmitter } from 'eventemitter3'\nimport { concatArrayBuffer, toUint8Array } from '../../helpers.js'\nimport { HttpClient } from '../../network/httpClient.js'\nimport { WebSocketClient, WebSocketSession, WS_STATES } from '../../network/wsClient.js'\nimport type {\n  LiveV2InitRequest,\n  LiveV2InitResponse,\n  LiveV2StartSessionMessage,\n  LiveV2WebSocketMessage,\n} from './generated-types.js'\nimport { LiveV2SessionStatus } from './types.js'\n\ntype EventMap = {\n  started: [message: LiveV2InitResponse]\n  connecting: [message: { attempt: number }]\n  connected: [message: { attempt: number }]\n  ending: [message: { code: number; reason?: string }]\n  ended: [message: { code: number; reason?: string }]\n  message: [message: LiveV2WebSocketMessage]\n  error: [error: Error]\n}\n\nexport class LiveV2Session {\n  private sessionOptions: LiveV2InitRequest\n  private httpClient: HttpClient\n  private webSocketClient: WebSocketClient\n\n  private abortController: AbortController = new AbortController()\n  private initSessionPromise: Promise<LiveV2InitResponse>\n  private initSessionResponse: LiveV2InitResponse | null = null\n  private webSocketSession: WebSocketSession | null = null\n\n  private eventEmitter: EventEmitter | null = new EventEmitter()\n  private bytesSent = 0\n  private audioBuffer: Uint8Array | null = null\n\n  private _status: LiveV2SessionStatus = 'starting'\n\n  constructor({\n    options,\n    httpClient,\n    webSocketClient,\n  }: {\n    options: LiveV2InitRequest\n    httpClient: HttpClient\n    webSocketClient: WebSocketClient\n  }) {\n    this.sessionOptions = options\n    this.httpClient = httpClient\n    this.webSocketClient = webSocketClient\n    this.abortController = new AbortController()\n\n    this.initSessionPromise = this.initSession()\n    this.startSession()\n  }\n\n  /**\n   * Get the session id. The promise is resolved when the session is started.\n   * @returns the session id\n   */\n  async getSessionId(): Promise<string> {\n    const { id } = await this.initSessionPromise\n    return id\n  }\n\n  /**\n   * The session id or null if the session is not started yet.\n   */\n  get sessionId(): string | null {\n    return this.initSessionResponse?.id ?? null\n  }\n\n  /**\n   * The current status of the session.\n   * - `starting`: the session is not started yet\n   * - `started`: the session is started but not connected to the websocket\n   * - `connecting`: the session is connecting to the websocket. If the connection is lost and it's retryable, the session will reconnect and the status will be `connecting` again.\n   * - `connected`: the session is connected to the websocket.\n   * - `ending`: the session is ending because of a user action or an error. In this status, sendAudio and stop are no-op.\n   * - `ended`: the session is ended. In this status, sendAudio and stop are no-op and listeners are removed.\n   */\n  get status(): LiveV2SessionStatus {\n    return this._status\n  }\n\n  /**\n   * Send an audio chunk to the current live session.\n   * If not connected, the audio will be buffered and sent when the session is connected.\n   *\n   * @param audio the audio chunk to send\n   */\n  sendAudio(audio: ArrayBufferLike | Buffer<ArrayBufferLike> | ArrayLike<number>): void {\n    if (this._status === 'ended') {\n      // throw new Error(`The session stopped, you can no longer send audio.`)\n      return\n    }\n    if (this._status === 'ending') {\n      // throw new Error(`The session is stopping, you can no longer send audio.`)\n      return\n    }\n\n    // TODO check if it has a wav header.\n    // If it does, check with config and remove it\n\n    const audioArray = toUint8Array(audio)\n    this.audioBuffer = concatArrayBuffer(this.audioBuffer, audioArray)\n\n    if (this.webSocketSession?.readyState === WS_STATES.OPEN) {\n      this.webSocketSession?.send(audioArray)\n    }\n  }\n\n  /**\n   * Stop the recording.\n   * The session will process the remaining audio and emit the `ended` event when the post-processing is done.\n   */\n  stopRecording(): void {\n    this.doStop(1000)\n  }\n\n  /**\n   * Force the end of the session without waiting for the post-processing to be done.\n   * `ending` and `ended` events are emitted, pending requests/connections are cancelled and listeners are removed.\n   */\n  endSession(): void {\n    this.doDestroy(1000, 'Session ended by user')\n  }\n\n  private async initSession(): Promise<LiveV2InitResponse> {\n    try {\n      return await this.httpClient.post<LiveV2InitResponse>(`/v2/live`, {\n        signal: this.abortController.signal,\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          ...this.sessionOptions,\n          messages_config: {\n            ...this.sessionOptions.messages_config,\n            // Force ack reception for resume\n            receive_acknowledgments: true,\n          },\n        } satisfies LiveV2InitRequest),\n      })\n    } catch (error) {\n      if (!this.abortController.signal.aborted) {\n        this.emit(\n          'error',\n          error instanceof Error\n            ? error\n            : new Error(`Error creating session: ${error}`, { cause: error })\n        )\n        this.doDestroy(1006, `Couldn't start a new session`)\n      }\n      throw error\n    }\n  }\n\n  private async startSession(): Promise<void> {\n    const session = await this.initSessionPromise\n    this.initSessionResponse = session\n\n    if (this.abortController.signal.aborted) {\n      return\n    }\n\n    if (this._status === 'starting') {\n      this._status = 'started'\n      this.emit('started', session)\n    }\n\n    if (this.sessionOptions.messages_config?.receive_lifecycle_events) {\n      const startSessionMessage: LiveV2StartSessionMessage = {\n        type: 'start_session',\n        session_id: session.id,\n        created_at: session.created_at,\n      }\n      this.emit('message', startSessionMessage)\n    }\n\n    this.connectToWebSocket(session)\n  }\n\n  private connectToWebSocket(session: LiveV2InitResponse): void {\n    if (this.abortController.signal.aborted) {\n      return\n    }\n\n    // Create a WebSocket session and bridge its events\n    const webSocketSession = this.webSocketClient.createSession(session.url)\n    this.abortController.signal.addEventListener('abort', () => {\n      this.webSocketSession = null\n      webSocketSession.onconnecting = null\n      webSocketSession.onopen = null\n      webSocketSession.onmessage = null\n      webSocketSession.onclose = null\n      webSocketSession.onerror = null\n      webSocketSession.close(1001, 'Aborted')\n    })\n\n    this.webSocketSession = webSocketSession\n\n    this.webSocketSession.onconnecting = ({ connection }) => {\n      this._status = 'connecting'\n      this.emit('connecting', { attempt: connection })\n    }\n\n    this.webSocketSession.onopen = ({ connection }) => {\n      if (this.audioBuffer?.byteLength) {\n        webSocketSession.send(this.audioBuffer)\n      }\n      if (this.status === 'ending') {\n        webSocketSession.send(JSON.stringify({ type: 'stop_recording' }))\n        return\n      }\n      this._status = 'connected'\n      this.emit('connected', { attempt: connection })\n    }\n\n    this.webSocketSession.onmessage = (event) => {\n      const message = this.parseMessage(event.data.toString())\n\n      // We forced the acknowledgment reception for resume so we must not emit them if user don't want them\n      if (\n        !this.sessionOptions.messages_config ||\n        this.sessionOptions.messages_config.receive_acknowledgments ||\n        !('acknowledged' in message)\n      ) {\n        this.emit('message', message)\n      }\n\n      if (message.type === 'audio_chunk') {\n        if (message.acknowledged && message.data) {\n          const byteEnd = message.data.byte_range[1]\n          const slice = this.audioBuffer?.slice(byteEnd - this.bytesSent)\n          this.audioBuffer = slice?.byteLength ? slice : null\n          this.bytesSent = byteEnd\n        }\n      }\n    }\n\n    this.webSocketSession.onclose = ({ code, reason }) => {\n      this.doDestroy(code, reason)\n    }\n\n    this.webSocketSession.onerror = (error) => {\n      this.emit('error', error)\n    }\n  }\n\n  private parseMessage(data: string): LiveV2WebSocketMessage {\n    let message: unknown\n    try {\n      message = JSON.parse(data)\n    } catch (err) {\n      throw new Error(`Invalid message received: ${data}`, { cause: err })\n    }\n    if (!message || typeof message !== 'object' || !('type' in message)) {\n      throw new Error(`Invalid message received: ${data}`)\n    }\n    return message as LiveV2WebSocketMessage\n  }\n\n  private doStop(code = 1006, reason?: string): void {\n    if (this._status === 'ended') {\n      // no-op\n      return\n    }\n    if (this._status === 'ending') {\n      // no-op\n      return\n    }\n\n    this._status = 'ending'\n    this.emit('ending', { code, reason })\n\n    if (this.webSocketSession?.readyState === WS_STATES.OPEN) {\n      this.webSocketSession?.send(JSON.stringify({ type: 'stop_recording' }))\n    }\n  }\n\n  private doDestroy(code = 1006, reason?: string) {\n    if (this._status === 'ended') {\n      return\n    }\n\n    // Ending the session\n    this.doStop(code, reason)\n\n    // Session ended\n    this._status = 'ended'\n    this.emit('ended', { code, reason })\n\n    // Cancel pending connection\n    this.abortController.abort()\n\n    this.audioBuffer = null\n\n    this.removeAllListeners()\n    this.eventEmitter = null\n  }\n\n  // #### Listeners ####\n\n  on(type: 'started', cb: (...args: EventMap['started']) => void): void\n  on(type: 'connecting', cb: (...args: EventMap['connecting']) => void): void\n  on(type: 'connected', cb: (...args: EventMap['connected']) => void): void\n  on(type: 'ending', cb: (...args: EventMap['ending']) => void): void\n  on(type: 'ended', cb: (...args: EventMap['ended']) => void): void\n  on(type: 'message', cb: (...args: EventMap['message']) => void): void\n  on(type: 'error', cb: (...args: EventMap['error']) => void): void\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- overload, cannot avoid any\n  on(type: keyof EventMap, cb: (event: any) => void): void {\n    this.eventEmitter?.on(type, cb)\n  }\n\n  once(type: 'started', cb: (...args: EventMap['started']) => void): void\n  once(type: 'connecting', cb: (...args: EventMap['connecting']) => void): void\n  once(type: 'connected', cb: (...args: EventMap['connected']) => void): void\n  once(type: 'ending', cb: (...args: EventMap['ending']) => void): void\n  once(type: 'ended', cb: (...args: EventMap['ended']) => void): void\n  once(type: 'message', cb: (...args: EventMap['message']) => void): void\n  once(type: 'error', cb: (...args: EventMap['error']) => void): void\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- overload, cannot avoid any\n  once(type: keyof EventMap, cb: (event: any) => void): void {\n    this.eventEmitter?.once(type, cb)\n  }\n\n  off(type: 'started', cb?: (...args: EventMap['started']) => void): void\n  off(type: 'connecting', cb?: (...args: EventMap['connecting']) => void): void\n  off(type: 'connected', cb?: (...args: EventMap['connected']) => void): void\n  off(type: 'ending', cb?: (...args: EventMap['ending']) => void): void\n  off(type: 'ended', cb?: (...args: EventMap['ended']) => void): void\n  off(type: 'message', cb?: (...args: EventMap['message']) => void): void\n  off(type: 'error', cb?: (...args: EventMap['error']) => void): void\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- overload, cannot avoid any\n  off(type: keyof EventMap, cb?: (event: any) => void): void {\n    this.eventEmitter?.off(type, cb)\n  }\n\n  addListener(type: 'started', cb: (...args: EventMap['started']) => void): void\n  addListener(type: 'connecting', cb: (...args: EventMap['connecting']) => void): void\n  addListener(type: 'connected', cb: (...args: EventMap['connected']) => void): void\n  addListener(type: 'ending', cb: (...args: EventMap['ending']) => void): void\n  addListener(type: 'ended', cb: (...args: EventMap['ended']) => void): void\n  addListener(type: 'message', cb: (...args: EventMap['message']) => void): void\n  addListener(type: 'error', cb: (...args: EventMap['error']) => void): void\n  addListener(\n    type: keyof EventMap,\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- overload, cannot avoid any\n    cb: (event: any) => void\n  ): void {\n    this.eventEmitter?.addListener(type, cb)\n  }\n\n  removeListener(type: 'started', cb?: (...args: EventMap['started']) => void): void\n  removeListener(type: 'connecting', cb?: (...args: EventMap['connecting']) => void): void\n  removeListener(type: 'connected', cb?: (...args: EventMap['connected']) => void): void\n  removeListener(type: 'ending', cb?: (...args: EventMap['ending']) => void): void\n  removeListener(type: 'ended', cb?: (...args: EventMap['ended']) => void): void\n  removeListener(type: 'message', cb?: (...args: EventMap['message']) => void): void\n  removeListener(type: 'error', cb?: (...args: EventMap['error']) => void): void\n  removeListener(\n    type: keyof EventMap,\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- overload, cannot avoid any\n    cb?: (event: any) => void\n  ): void {\n    this.eventEmitter?.removeListener(type, cb)\n  }\n\n  removeAllListeners(type?: keyof EventMap): void {\n    this.eventEmitter?.removeAllListeners(type)\n  }\n\n  private emit(type: 'started', ...args: EventMap['started']): void\n  private emit(type: 'connecting', ...args: EventMap['connecting']): void\n  private emit(type: 'connected', ...args: EventMap['connected']): void\n  private emit(type: 'ending', ...args: EventMap['ending']): void\n  private emit(type: 'ended', ...args: EventMap['ended']): void\n  private emit(type: 'message', ...args: EventMap['message']): void\n  private emit(type: 'error', ...args: EventMap['error']): void\n  private emit(\n    type: keyof EventMap,\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- overload, cannot avoid any\n    ...args: any[]\n  ): void {\n    this.eventEmitter?.emit(type, ...args)\n  }\n}\n"],"mappings":"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"}