UNPKG

ilp-protocol-stream

Version:

Interledger Transport Protocol for sending multiple streams of money and data over ILP.

406 lines 16.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DataAndMoneyStream = void 0; const ilp_logger_1 = __importDefault(require("ilp-logger")); const long_1 = __importDefault(require("long")); const stream_1 = require("stream"); const data_queue_1 = require("./util/data-queue"); const data_offset_sorter_1 = require("./util/data-offset-sorter"); const long_2 = require("./util/long"); const DEFAULT_TIMEOUT = 60000; const MAX_REMOTE_RECEIVE = long_1.default.MAX_UNSIGNED_VALUE; class DataAndMoneyStream extends stream_1.Duplex { constructor(opts) { super({ allowHalfOpen: false }); this.id = opts.id; this.isServer = opts.isServer; this.log = (0, ilp_logger_1.default)(`ilp-protocol-stream:${this.isServer ? 'Server' : 'Client'}:Connection:${opts.connectionId}:Stream:${this.id}`); this.log.info('new stream created'); this._totalSent = long_1.default.UZERO; this._totalReceived = long_1.default.UZERO; this._sendMax = long_1.default.UZERO; this._receiveMax = long_1.default.UZERO; this._outgoingHeldAmount = long_1.default.UZERO; this._sentEnd = false; this._remoteSentEnd = false; this._closed = false; this.holds = {}; this._incomingData = new data_offset_sorter_1.OffsetSorter(); this._outgoingData = new data_queue_1.DataQueue(); this._outgoingDataToRetry = []; this.outgoingOffset = 0; this._remoteClosed = false; this._remoteReceived = long_1.default.UZERO; this._remoteReceiveMax = MAX_REMOTE_RECEIVE; this._remoteMaxOffset = 16384; this.emittedEnd = false; this.emittedClose = false; this.once('end', () => { this.emittedEnd = true; }); this.once('close', () => { this.emittedClose = true; }); } get totalSent() { return this._totalSent.toString(); } get totalReceived() { return this._totalReceived.toString(); } get sendMax() { return this._sendMax.toString(); } get receiveMax() { return this._receiveMax.toString(); } get receipt() { return this._receipt; } get closed() { return this._closed; } isOpen() { return !this.closed; } setSendMax(limit) { if (this.closed) { throw new Error('Stream already closed'); } else if (typeof limit === 'number' && !isFinite(limit)) { throw new Error('sendMax must be finite'); } const sendMax = (0, long_2.longFromValue)(limit, true); if (this._totalSent.greaterThan(sendMax)) { this.log.debug('cannot set sendMax to %s because we have already sent: %s', sendMax, this._totalSent); throw new Error(`Cannot set sendMax lower than the totalSent`); } this.log.debug('setting sendMax to %s', sendMax); this._sendMax = sendMax; this.emit('_maybe_start_send_loop'); } setReceiveMax(limit) { if (this.closed) { throw new Error('Stream already closed'); } const receiveMax = (0, long_2.longFromValue)(limit, true); if (this._totalReceived.greaterThan(receiveMax)) { this.log.debug('cannot set receiveMax to %s because we have already received: %s', receiveMax, this._totalReceived); throw new Error('Cannot set receiveMax lower than the totalReceived'); } if (this._receiveMax.greaterThan(receiveMax)) { this.log.debug('cannot set receiveMax to %s because the current limit is: %s', receiveMax, this._receiveMax); throw new Error('Cannot decrease the receiveMax'); } this.log.debug('setting receiveMax to %s', receiveMax); this._receiveMax = receiveMax; this.emit('_maybe_start_send_loop'); } async sendTotal(_limit, opts) { const limit = (0, long_2.longFromValue)(_limit, true); const timeout = (opts && opts.timeout) || DEFAULT_TIMEOUT; if (this._totalSent.greaterThanOrEqual(limit)) { this.log.debug('already sent %s, not sending any more', this._totalSent); return Promise.resolve(); } this.setSendMax(limit); await new Promise((resolve, reject) => { const outgoingHandler = () => { if (this._totalSent.greaterThanOrEqual(limit)) { cleanup(); resolve(); } }; const endHandler = () => { setTimeout(cleanup); if (this._totalSent.greaterThanOrEqual(limit)) { resolve(); } else { this.log.debug('Stream was closed before the desired amount was sent (target: %s, totalSent: %s)', limit, this._totalSent); reject(new Error(`Stream was closed before the desired amount was sent (target: ${limit}, totalSent: ${this._totalSent})`)); } }; const errorHandler = (err) => { this.log.debug('error waiting for stream to stabilize:', err); cleanup(); reject(new Error(`Stream encountered an error before the desired amount was sent (target: ${limit}, totalSent: ${this._totalSent}): ${err}`)); }; const timer = setTimeout(() => { cleanup(); reject(new Error(`Timed out before the desired amount was sent (target: ${limit}, totalSent: ${this._totalSent})`)); }, timeout); const cleanup = () => { clearTimeout(timer); this.removeListener('outgoing_money', outgoingHandler); this.removeListener('error', errorHandler); this.removeListener('end', endHandler); }; this.on('outgoing_money', outgoingHandler); this.on('error', errorHandler); this.on('end', endHandler); }); } async receiveTotal(_limit, opts) { const limit = (0, long_2.longFromValue)(_limit, true); const timeout = (opts && opts.timeout) || DEFAULT_TIMEOUT; if (this._totalReceived.greaterThanOrEqual(limit)) { this.log.debug('already received %s, not waiting for more', this._totalReceived); return Promise.resolve(); } this.setReceiveMax(limit); await new Promise((resolve, reject) => { const moneyHandler = () => { if (this._totalReceived.greaterThanOrEqual(limit)) { cleanup(); resolve(); } }; const endHandler = () => { setTimeout(cleanup); if (this._totalReceived.greaterThanOrEqual(limit)) { resolve(); } else { this.log.debug('Stream was closed before the desired amount was received (target: %s, totalReceived: %s)', limit, this._totalReceived); reject(new Error(`Stream was closed before the desired amount was received (target: ${limit}, totalReceived: ${this._totalReceived})`)); } }; const errorHandler = (err) => { this.log.debug('error waiting for stream to stabilize:', err); cleanup(); reject(new Error(`Stream encountered an error before the desired amount was received (target: ${limit}, totalReceived: ${this._totalReceived}): ${err}`)); }; const timer = setTimeout(() => { cleanup(); reject(new Error(`Timed out before the desired amount was received (target: ${limit}, totalReceived: ${this._totalReceived})`)); }, timeout); const cleanup = () => { clearTimeout(timer); this.removeListener('money', moneyHandler); this.removeListener('error', errorHandler); this.removeListener('end', endHandler); }; this.on('money', moneyHandler); this.on('error', errorHandler); this.on('end', endHandler); }); } _getAmountStreamCanReceive() { if (this._receiveMax.lessThan(this._totalReceived)) { return long_1.default.UZERO; } return (0, long_2.checkedSubtract)(this._receiveMax, this._totalReceived).difference; } _addToIncoming(amount, prepare) { this._totalReceived = (0, long_2.checkedAdd)(this._totalReceived, amount).sum; this.log.trace('received %s (totalReceived: %s)', amount, this._totalReceived); this.emit('money', amount.toString(), prepare); } _getAmountAvailableToSend() { if (this.closed) { return long_1.default.UZERO; } const amountAvailable = (0, long_2.checkedSubtract)((0, long_2.checkedSubtract)(this._sendMax, this._totalSent).difference, this._outgoingHeldAmount).difference; return amountAvailable; } _holdOutgoing(holdId, maxAmount) { const amountAvailable = this._getAmountAvailableToSend(); const amountToHold = maxAmount ? (0, long_2.minLong)(amountAvailable, maxAmount) : amountAvailable; if (amountToHold.greaterThan(0)) { this._outgoingHeldAmount = this._outgoingHeldAmount.add(amountToHold); this.holds[holdId] = amountToHold; this.log.trace('holding outgoing balance. holdId: %s, amount: %s', holdId, amountToHold); } return amountToHold; } _executeHold(holdId) { if (!this.holds[holdId]) { return; } const amount = this.holds[holdId]; this._outgoingHeldAmount = this._outgoingHeldAmount.subtract(amount); this._totalSent = this._totalSent.add(amount); delete this.holds[holdId]; this.log.trace('executed holdId: %s for: %s', holdId, amount); this.emit('outgoing_money', amount.toString()); if (this._totalSent.greaterThanOrEqual(this._sendMax)) { this.log.debug('outgoing total sent'); this.emit('outgoing_total_sent'); } } _cancelHold(holdId) { if (!this.holds[holdId]) { return; } const amount = this.holds[holdId]; this.log.trace('cancelled holdId: %s for: %s', holdId, amount); this._outgoingHeldAmount = this._outgoingHeldAmount.subtract(amount); delete this.holds[holdId]; } _final(callback) { this.log.info('stream is closing'); const finish = (err) => { if (err) { this.log.debug('error waiting for money to be sent:', err); } this.log.info('stream ended'); this._closed = true; setTimeout(() => { if (!this.emittedEnd) { this.emittedEnd = true; this.safeEmit('end'); } if (!this.emittedClose) { this.emittedClose = true; this.safeEmit('close'); } }); callback(err); }; if (this._remoteSentEnd || this._sendMax.lessThanOrEqual(this._totalSent)) { finish(); } else { this.log.info('waiting to finish sending money before ending stream'); new Promise((resolve, reject) => { this.once('outgoing_total_sent', resolve); this.once('_send_loop_finished', resolve); this.once('error', (error) => reject(error)); }) .then(() => finish()) .catch(finish); } } _destroy(error, callback) { this.log.error('destroying stream because of error:', error); this._closed = true; if (error) { this._errorMessage = error.message; } setTimeout(() => { if (!this.emittedEnd) { this.emittedEnd = true; this.safeEmit('end'); } if (!this.emittedClose) { this.emittedClose = true; this.safeEmit('close'); } }); callback(error); } _write(chunk, encoding, callback) { this.log.trace('%d bytes written to the outgoing data queue', chunk.length); this._outgoingData.push(chunk, callback); this.emit('_maybe_start_send_loop'); } _writev(chunks, callback) { for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; this.log.trace('%d bytes written to the outgoing data queue', chunk.chunk.length); if (i === chunks.length - 1) { this._outgoingData.push(chunk.chunk, callback); } else { this._outgoingData.push(chunk.chunk); } } this.emit('_maybe_start_send_loop'); } _read(size) { const data = this._incomingData.read(); if (!data) { if (this['readableFlowing'] !== true) { setTimeout(() => this.emit('_maybe_start_send_loop')); } return; } this.push(data); if (data.length < size) { this._read(size - data.length); } } _hasDataToSend() { return !this._outgoingData.isEmpty() || this._outgoingDataToRetry.length > 0; } _getAvailableDataToSend(size) { if (this._outgoingDataToRetry.length > 0) { const toSend = this._outgoingDataToRetry[0]; if (toSend.data.length > size) { const data = toSend.data.slice(0, size); const offset = toSend.offset; toSend.data = toSend.data.slice(size); toSend.offset = toSend.offset + size; return { data, offset }; } else { this._outgoingDataToRetry.shift(); return toSend; } } const maxBytes = Math.min(size, this._remoteMaxOffset - this.outgoingOffset); const offset = this.outgoingOffset; const data = this._outgoingData.read(maxBytes); if (data && data.length > 0) { this.outgoingOffset += data.length; this.log.trace('%d bytes taken from the outgoing data queue', data.length); } return { data, offset }; } _resendOutgoingData(data, offset) { this.log.trace('re-queuing %d bytes of data starting at offset %d', data.length, offset); this._outgoingDataToRetry.push({ data, offset }); } _isDataBlocked() { if (this._remoteMaxOffset < this.outgoingOffset + this._outgoingData.byteLength()) { return this.outgoingOffset + this._outgoingData.byteLength(); } } _getOutgoingOffsets() { return { current: this.outgoingOffset, max: this.outgoingOffset + this._outgoingData.byteLength(), }; } _getIncomingOffsets() { return { max: this._incomingData.maxOffset, current: this._incomingData.readOffset, maxAcceptable: this._incomingData.readOffset + this.readableHighWaterMark - this.readableLength, }; } _pushIncomingData(data, offset) { this._incomingData.push(data, offset); this._read(this.readableHighWaterMark - this.readableLength); } _remoteEnded(err) { this.log.info('remote closed stream'); this._remoteSentEnd = true; this._remoteClosed = true; if (err) { this.destroy(err); } else { this.push(null); this.end(); } } _setReceipt(receipt) { this._receipt = receipt; } safeEmit(...args) { const event = args[0]; try { this.emit(...args); } catch (err) { this.log.debug('error in %s handler: %s', event, err); } } } exports.DataAndMoneyStream = DataAndMoneyStream; //# sourceMappingURL=stream.js.map