private <REQ extends PlcRequest, RES extends PlcResponse> CompletableFuture<RES> send(
        REQ originalRequest,
        List<REQ> requests,
        Function<REQ, CompletableFuture<RES>> sender,
        Function<Map<REQ, SubResponse<RES>>, RES> responseProcessor) {
        // If this send has only one sub-request and this matches the original one, don't do any special handling
        // and just forward the request to the normal sending method.
        if ((requests.size() == 1) && (requests.get(0) == originalRequest)) {
            return sender.apply(requests.get(0));
        }
        // If at least one sub request is requested, split up each tag request into a separate sub-request
        // And have the reader process each one independently. After the last sub-request is finished,
        // Merge the results back together.
        if (requests.isEmpty()) {
            return CompletableFuture.completedFuture(responseProcessor.apply(Collections.emptyMap()));
        }

        // Create a new future which will be used to return the aggregated response back to the application.
        CompletableFuture<RES> parentFuture = new CompletableFuture<>();

        // Create one sub-request for every single tag and store the futures in a map.
        Map<REQ, CompletableFuture<RES>> subFutures = new HashMap<>();
        for (REQ subRequest : requests) {
            subFutures.put(subRequest, sender.apply(subRequest));
            // Potentially add a delay between sending messages.
            if((getMillisDelay() > 0) || (getNanosDelay() > 0)) {
                try {
                    Thread.sleep(getMillisDelay(), getNanosDelay());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        // As soon as all sub-futures are done, merge the individual responses back to one big response.
        CompletableFuture.allOf(subFutures.values().toArray(new CompletableFuture[0])).handle((aVoid, t) -> {
            if (t != null) {
                parentFuture.completeExceptionally(t);
            }
            Map<REQ, SubResponse<RES>> results = new HashMap<>();
            for (Map.Entry<REQ, CompletableFuture<RES>> subFutureEntry : subFutures.entrySet()) {
                REQ subRequest = subFutureEntry.getKey();
                CompletableFuture<RES> subFuture = subFutureEntry.getValue();
                try {
                    final RES subResponse = subFuture.get();
                    results.put(subRequest, new SubResponse<>(subResponse));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    results.put(subRequest, new SubResponse<>(new Exception("Something went wrong")));
                } catch (Exception e) {
                    results.put(subRequest, new SubResponse<>(new Exception("Something went wrong")));
                }
            }
            RES response = responseProcessor.apply(results);
            parentFuture.complete(response);
            return Void.TYPE;
        }).exceptionally(throwable -> {
            // TODO: If would be cool if we could still process all of the successful ones ...
            parentFuture.completeExceptionally(throwable);
            return null;
        });
        return parentFuture;
    }
If a large message is split up into multiple ones, then sending them out without a delay, might overwhelm
the PLC, here an optimizer can introduce a delay between them to avoid that.

@return number of nanoseconds between two requests being sent out (0-9999999).
plc4j/spi/src/main/java/org/apache/plc4x/java/spi/optimizer/BaseOptimizer.java