class SimplexTransferListener extends AbstractTransferListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(SimplexTransferListener.class);
    private static final int QUEUE_SIZE = 1024;
    private static final int BATCH_MAX_SIZE = 500;
    private final TransferListener delegate;
    private final int batchMaxSize;
    private final boolean blockOnLastEvent;
    private final ArrayBlockingQueue<Exchange> eventQueue;

    /**
     * Constructor that makes passed in delegate run on single thread, and will block on last event.
     */
    public SimplexTransferListener(TransferListener delegate) {
        this(delegate, QUEUE_SIZE, BATCH_MAX_SIZE, true);
    }

    /**
     * Constructor that may alter behaviour of this listener.
     *
     * @param delegate The delegate that should run on single thread.
     * @param queueSize The event queue size (default {@code 1024}).
     * @param batchMaxSize The maximum batch size delegate should receive (default {@code 500}).
     * @param blockOnLastEvent Should this listener block on last transfer end (completed or corrupted) block? (default {@code true}).
     */
    public SimplexTransferListener(
            TransferListener delegate, int queueSize, int batchMaxSize, boolean blockOnLastEvent) {
        this.delegate = requireNonNull(delegate);
        if (queueSize < 1 || batchMaxSize < 1) {
            throw new IllegalArgumentException("Queue and batch sizes must be greater than 1");
        }
        this.batchMaxSize = batchMaxSize;
        this.blockOnLastEvent = blockOnLastEvent;

        this.eventQueue = new ArrayBlockingQueue<>(queueSize);
        Thread updater = new Thread(this::feedConsumer);
        updater.setDaemon(true);
        updater.start();
    }

    public TransferListener getDelegate() {
        return delegate;
    }

    private void feedConsumer() {
        final ArrayList<Exchange> batch = new ArrayList<>(batchMaxSize);
        try {
            while (true) {
                batch.clear();
                if (eventQueue.drainTo(batch, BATCH_MAX_SIZE) == 0) {
                    batch.add(eventQueue.take());
                }
                demux(batch);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void demux(List<Exchange> exchanges) {<FILL_FUNCTION_BODY>}

    private void put(TransferEvent event, boolean last) {
        try {
            Exchange exchange;
            if (blockOnLastEvent && last) {
                exchange = new BlockingExchange(event);
            } else {
                exchange = new Exchange(event);
            }
            eventQueue.put(exchange);
            exchange.waitForProcessed();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private final ConcurrentHashMap<File, Boolean> ongoing = new ConcurrentHashMap<>();

    @Override
    public void transferInitiated(TransferEvent event) {
        ongoing.putIfAbsent(event.getResource().getFile(), Boolean.TRUE);
        put(event, false);
    }

    @Override
    public void transferStarted(TransferEvent event) throws TransferCancelledException {
        if (ongoing.get(event.getResource().getFile()) == Boolean.FALSE) {
            throw new TransferCancelledException();
        }
        put(event, false);
    }

    @Override
    public void transferProgressed(TransferEvent event) throws TransferCancelledException {
        if (ongoing.get(event.getResource().getFile()) == Boolean.FALSE) {
            throw new TransferCancelledException();
        }
        put(event, false);
    }

    @Override
    public void transferCorrupted(TransferEvent event) throws TransferCancelledException {
        if (ongoing.get(event.getResource().getFile()) == Boolean.FALSE) {
            throw new TransferCancelledException();
        }
        put(event, false);
    }

    @Override
    public void transferSucceeded(TransferEvent event) {
        ongoing.remove(event.getResource().getFile());
        put(event, ongoing.isEmpty());
    }

    @Override
    public void transferFailed(TransferEvent event) {
        ongoing.remove(event.getResource().getFile());
        put(event, ongoing.isEmpty());
    }

    private static class Exchange {
        private final TransferEvent event;

        private Exchange(TransferEvent event) {
            this.event = event;
        }

        public void process(Consumer<TransferEvent> consumer) {
            consumer.accept(event);
        }

        public void waitForProcessed() throws InterruptedException {
            // nothing, is async
        }
    }

    private static class BlockingExchange extends Exchange {
        private final CountDownLatch latch = new CountDownLatch(1);

        private BlockingExchange(TransferEvent event) {
            super(event);
        }

        @Override
        public void process(Consumer<TransferEvent> consumer) {
            super.process(consumer);
            latch.countDown();
        }

        @Override
        public void waitForProcessed() throws InterruptedException {
            latch.await();
        }
    }
}

        for (Exchange exchange : exchanges) {
            exchange.process(transferEvent -> {
                TransferEvent.EventType type = transferEvent.getType();
                try {
                    switch (type) {
                        case INITIATED:
                            delegate.transferInitiated(transferEvent);
                            break;
                        case STARTED:
                            delegate.transferStarted(transferEvent);
                            break;
                        case PROGRESSED:
                            delegate.transferProgressed(transferEvent);
                            break;
                        case CORRUPTED:
                            delegate.transferCorrupted(transferEvent);
                            break;
                        case SUCCEEDED:
                            delegate.transferSucceeded(transferEvent);
                            break;
                        case FAILED:
                            delegate.transferFailed(transferEvent);
                            break;
                        default:
                            LOGGER.warn("Invalid TransferEvent.EventType={}; ignoring it", type);
                    }
                } catch (TransferCancelledException e) {
                    ongoing.put(transferEvent.getResource().getFile(), Boolean.FALSE);
                }
            });
        }
    
<no_super_class>