ilp-protocol-stream
Version:
Interledger Transport Protocol for sending multiple streams of money and data over ILP.
406 lines • 16.4 kB
JavaScript
"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