/**
   * Handles a publish request.
   *
   * @param request The publish request to handle.
   * @return A completable future to be completed with the publish response.
   */
  @SuppressWarnings("unchecked")
  private CompletableFuture<PublishResponse> handlePublish(PublishRequest request) {
    if (request.session() != id)
      return Futures.exceptionalFuture(new UnknownSessionException("incorrect session ID"));

    if (request.previousVersion() != eventVersion) {
      return CompletableFuture.completedFuture(
          PublishResponse.builder()
              .withStatus(Response.Status.ERROR)
              .withError(RaftError.Type.INTERNAL_ERROR)
              .withVersion(eventVersion)
              .build());
    }

    eventVersion = request.eventVersion();

    List<CompletableFuture<Void>> futures = new ArrayList<>(request.events().size());
    for (Event<?> event : request.events()) {
      Listeners<Object> listeners = eventListeners.get(event.name());
      if (listeners != null) {
        futures.add(listeners.accept(event.message()));
      }
    }

    return CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[futures.size()]))
        .handleAsync(
            (result, error) -> {
              completeVersion = Math.max(completeVersion, request.eventVersion());
              return PublishResponse.builder()
                  .withStatus(Response.Status.OK)
                  .withVersion(eventVersion)
                  .build();
            },
            context.executor());
  }
  /** Recursively submits a query. */
  @SuppressWarnings("unchecked")
  private <T> CompletableFuture<T> submit(QueryRequest request, CompletableFuture<T> future) {
    if (!isOpen()) {
      future.completeExceptionally(new IllegalStateException("session not open"));
      return future;
    }

    long sequence = ++requestSequence;

    this.<QueryRequest, QueryResponse>request(request)
        .whenComplete(
            (response, error) -> {
              if (error == null) {
                // If the query consistency level is CAUSAL, we can simply complete queries in
                // sequential order.
                if (request.query().consistency() == Query.ConsistencyLevel.CAUSAL) {
                  sequenceResponse(
                      sequence,
                      () -> {
                        responseVersion = Math.max(responseVersion, response.version());
                        completeResponse(response, future);
                      });
                }
                // If the query consistency level is strong, the query must be executed
                // sequentially. In order to ensure responses
                // are received in a sequential manner, we compare the response version number with
                // the highest version for which
                // we've received a response and resubmit queries with output resulting from stale
                // (prior) versions.
                else {
                  sequenceResponse(
                      sequence,
                      () -> {
                        if (response.version() > 0 && response.version() < responseVersion) {
                          submit(request, future);
                        } else {
                          responseVersion = Math.max(responseVersion, response.version());
                          completeResponse(response, future);
                        }
                      });
                }
              } else {
                future.completeExceptionally(error);
              }
            });
    return future;
  }