import { closeSource } from '@libp2p/utils/close-source';
import * as lp from 'it-length-prefixed';
import { pipe } from 'it-pipe';
import { pushable } from 'it-pushable';
import { TypedEventEmitter } from 'main-event';
import { Uint8ArrayList } from 'uint8arraylist';
/**
 * Thin wrapper around a peer's inbound / outbound pubsub streams
 */
export class PeerStreams extends TypedEventEmitter {
    id;
    protocol;
    /**
     * Write stream - it's preferable to use the write method
     */
    outboundStream;
    /**
     * Read stream
     */
    inboundStream;
    /**
     * The raw outbound stream, as retrieved from conn.newStream
     */
    _rawOutboundStream;
    /**
     * The raw inbound stream, as retrieved from the callback from libp2p.handle
     */
    _rawInboundStream;
    /**
     * An AbortController for controlled shutdown of the inbound stream
     */
    _inboundAbortController;
    closed;
    log;
    constructor(components, init) {
        super();
        this.log = components.logger.forComponent('libp2p-pubsub:peer-streams');
        this.id = init.id;
        this.protocol = init.protocol;
        this._inboundAbortController = new AbortController();
        this.closed = false;
    }
    /**
     * Do we have a connection to read from?
     */
    get isReadable() {
        return Boolean(this.inboundStream);
    }
    /**
     * Do we have a connection to write on?
     */
    get isWritable() {
        return Boolean(this.outboundStream);
    }
    /**
     * Send a message to this peer.
     * Throws if there is no `stream` to write to available.
     */
    write(data) {
        if (this.outboundStream == null) {
            const id = this.id.toString();
            throw new Error('No writable connection to ' + id);
        }
        this.outboundStream.push(data instanceof Uint8Array ? new Uint8ArrayList(data) : data);
    }
    /**
     * Attach a raw inbound stream and setup a read stream
     */
    attachInboundStream(stream, decoderOptions) {
        const abortListener = () => {
            closeSource(stream.source, this.log);
        };
        this._inboundAbortController.signal.addEventListener('abort', abortListener, {
            once: true
        });
        // Create and attach a new inbound stream
        // The inbound stream is:
        // - abortable, set to only return on abort, rather than throw
        // - transformed with length-prefix transform
        this._rawInboundStream = stream;
        this.inboundStream = pipe(this._rawInboundStream, (source) => lp.decode(source, decoderOptions));
        this.dispatchEvent(new CustomEvent('stream:inbound'));
        return this.inboundStream;
    }
    /**
     * Attach a raw outbound stream and setup a write stream
     */
    async attachOutboundStream(stream) {
        // If an outbound stream already exists, gently close it
        const _prevStream = this.outboundStream;
        if (this.outboundStream != null) {
            // End the stream without emitting a close event
            this.outboundStream.end();
        }
        this._rawOutboundStream = stream;
        this.outboundStream = pushable({
            onEnd: (shouldEmit) => {
                // close writable side of the stream if it exists
                this._rawOutboundStream?.closeWrite()
                    .catch(err => {
                    this.log('error closing outbound stream', err);
                });
                this._rawOutboundStream = undefined;
                this.outboundStream = undefined;
                if (shouldEmit != null) {
                    this.dispatchEvent(new CustomEvent('close'));
                }
            }
        });
        pipe(this.outboundStream, (source) => lp.encode(source), this._rawOutboundStream).catch((err) => {
            this.log.error(err);
        });
        // Only emit if the connection is new
        if (_prevStream == null) {
            this.dispatchEvent(new CustomEvent('stream:outbound'));
        }
        return this.outboundStream;
    }
    /**
     * Closes the open connection to peer
     */
    close() {
        if (this.closed) {
            return;
        }
        this.closed = true;
        // End the outbound stream
        if (this.outboundStream != null) {
            this.outboundStream.end();
        }
        // End the inbound stream
        if (this.inboundStream != null) {
            this._inboundAbortController.abort();
        }
        this._rawOutboundStream = undefined;
        this.outboundStream = undefined;
        this._rawInboundStream = undefined;
        this.inboundStream = undefined;
        this.dispatchEvent(new CustomEvent('close'));
    }
}
//# sourceMappingURL=peer-streams.js.map