async connect() {
    const mainProgress = progress.get('main')
    logger.debug('Connecting to IoT endpoint')

    const endpoint = await this.getIotEndpoint()

    const {
      accessKeyId,
      secretAccessKey: secretKey,
      sessionToken,
    } = await this.provider.getCredentials()

    const device = new iot.device({
      protocol: 'wss',
      host: endpoint,
      accessKeyId,
      secretKey,
      sessionToken,
      autoResubscribe: true,
      offlineQueueing: true,
      baseReconnectTimeMs: 1000,
      maximumReconnectTimeMs: 1000,
      minimumConnectionTimeMs: 1000,
      keepalive: 1,
    })

    device.on('error', (e) => {
      logger.debug('IoT connection error', e)
    })

    device.on('offline', (e) => {
      mainProgress.notice('Reconnecting')
    })

    device.on('connect', (e) => {
      if (!this.heartbeatInterval) {
        this.heartbeatInterval = setInterval(() => {
          device.publish(
            this.getTopicId('_heartbeat'),
            JSON.stringify({ connected: true }),
            {
              qos: 1,
            },
          )
        }, 1000)
      }

      logger.success(`Connected (Ctrl+C to cancel)`)
      mainProgress.remove()
    })

    // Each function has a seperate topic we need to subscribe to
    const functionNames = this.serverless.service.getAllFunctions()

    for (const functionName of functionNames) {
      device.subscribe(this.getTopicId(`${functionName}/request`), {
        qos: 1,
      })
    }

    /**
     * We listen for messages on the function's invocation topic.
     * Messages include the event, environment, and context for the function invocation.
     */
    device.on('message', async (topic, buffer) => {
      /**
       * parse the topicId to get the function name as set in the yaml file
       * so we can get the function configuration
       */
      const { functionName } = this.parseTopicId(topic)

      /**
       * If _heartbeat is set as the function name, this is just
       * the heartbeat message we publish from the local machine
       * to check if the connection is still alive.
       * Just ignore it.
       */
      if (functionName === '_heartbeat') {
        return
      }

      const { event, environment, context } = JSON.parse(buffer.toString())

      const invocationColorFn = stringToSafeColor(context.awsRequestId)

      this.logFunctionEvent(
        functionName,
        event,
        this.options.detailed,
        invocationColorFn,
      )

      const functionConfig = this.serverless.service.getFunction(functionName)
      const runtime = this.provider.getRuntime(functionConfig.runtime)

      // Spawn the Dev Invoke command to build changes with esbuild
      // await this.serverless.pluginManager.spawn('dev-invoke');

      let serviceAbsolutePath = this.serverless.serviceDir

      // If a build plugin was used then we need to set the serviceAbsolutePath to the build directory
      if (
        this.serverless.builtFunctions &&
        this.serverless.builtFunctions.has(functionName)
      ) {
        serviceAbsolutePath = path.join(
          this.serverless.config.serviceDir,
          '.serverless',
          'build',
        )
      }

      /**
       * We create a new instance of the LocalLambda class to invoke the function locally.
       * We need to know what the original runtime of the user function is to run the correct wrapper
       * We also need the handler to know which file to import and which function to call
       * We also set the environment and context to be passed to the function
       */
      const localLambda = new LocalLambda({
        serviceAbsolutePath,
        handler: functionConfig.handler,
        runtime,
        environment,
        invocationColorFn,
      })

      // set the timeout settings to be used by the getRemainingTimeInMillis function
      context.timeout = functionConfig.timeout || 6

      const startTime = performance.now()

      /**
       * Invoke the function locally and pass the event and context.
       * The context passed does not include context functions like .done, .succeed, .fail,
       * because we can't stream them over WebSockets. Those functions will be added by the wrapper later.
       * This function waits until the function execution is complete and returns the response.
       * The response includes any error that the function threw.
       */
      const response = await localLambda.invoke(event, context)

      const endTime = performance.now()

      const timeoutInMs = Math.round(context.timeout * 1000)
      const executionTimeInMs = Math.round(endTime - startTime)

      if (executionTimeInMs > timeoutInMs) {
        log.blankLine()
        log.warning(
          `Local invocation of function "${functionName}" took ${executionTimeInMs}ms, which exceeds the configured timeout of ${timeoutInMs}ms. Consider increasing the timeout, or optimizing your function.`,
        )
        log.blankLine()
      }

      this.logFunctionResponse(
        functionName,
        response.response,
        this.options.detailed,
        invocationColorFn,
      )

      // attach the requestId that corresponds to this response
      response.requestId = context.awsRequestId

      // Publish the result back to the function
      device.publish(
        this.getTopicId(`${functionName}/response`),
        JSON.stringify(response),
        {
          qos: 1,
        },
      )
    })

    /**
     * Exit the process when the user presses Ctrl+C
     */
    process.on('SIGINT', () => {
      mainProgress.remove()
      logger.blankLine()
      logger.blankLine()
      logger.warning(
        `Don't forget to run "serverless deploy" immediately upon closing Dev Mode to restore your original code and remove Dev Mode's instrumentation or your functions will not work!`,
      )
      logger.blankLine()
      process.exit(0)
    })
  }
Connects to the IoT endpoint over websockets and listens for lambda events.
The method subscribes to all function invocation topics and listens for incoming events.
When an event is received, the method invokes the corresponding function locally, waits for the result,
and publishes the result back to the IoT endpoint for the lambda function to use as a response to the invocation.

@returns {Promise<void>} This is a long-running method, so it does not return a value.