-
Notifications
You must be signed in to change notification settings - Fork 30
feat: close read and write streams #122
Changes from 10 commits
9ca7344
971146e
737f68d
d811a6d
272eb3d
272ead1
b2b4429
20c6c6b
4dff9c1
5c17ed6
8ae9668
eb071f0
5afe432
8375912
cee482a
17ff66d
b56b437
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,9 +8,16 @@ const BufferList = require('bl/BufferList') | |
const errCode = require('err-code') | ||
const { MAX_MSG_SIZE } = require('./restrict-size') | ||
const { InitiatorMessageTypes, ReceiverMessageTypes } = require('./message-types') | ||
const pDefer = require('p-defer') | ||
|
||
const ERR_MPLEX_STREAM_RESET = 'ERR_MPLEX_STREAM_RESET' | ||
const ERR_MPLEX_STREAM_ABORT = 'ERR_MPLEX_STREAM_ABORT' | ||
const MPLEX_WRITE_STREAM_CLOSED = 'MPLEX_WRITE_STREAM_CLOSED' | ||
|
||
/** | ||
* @typedef {import('libp2p-interfaces/src/stream-muxer/types').MuxedStream} MuxedStream | ||
* @typedef {import('libp2p-interfaces/src/stream-muxer/types').Sink} Sink | ||
*/ | ||
|
||
/** | ||
* @param {object} options | ||
|
@@ -20,18 +27,21 @@ const ERR_MPLEX_STREAM_ABORT = 'ERR_MPLEX_STREAM_ABORT' | |
* @param {function(Error)} [options.onEnd] - Called whenever the stream ends | ||
* @param {string} [options.type] - One of ['initiator','receiver']. Defaults to 'initiator' | ||
* @param {number} [options.maxMsgSize] - Max size of an mplex message in bytes. Writes > size are automatically split. Defaults to 1MB | ||
* @returns {*} A muxed stream | ||
* @returns {MuxedStream} A muxed stream | ||
*/ | ||
module.exports = ({ id, name, send, onEnd = () => {}, type = 'initiator', maxMsgSize = MAX_MSG_SIZE }) => { | ||
const abortController = new AbortController() | ||
const resetController = new AbortController() | ||
const writeCloseController = new AbortController() | ||
const Types = type === 'initiator' ? InitiatorMessageTypes : ReceiverMessageTypes | ||
const externalId = type === 'initiator' ? (`i${id}`) : `r${id}` | ||
|
||
name = String(name == null ? id : name) | ||
|
||
let sourceEnded = false | ||
let sinkEnded = false | ||
let sinkInProgress = false | ||
let sinkClosedDefer | ||
let endErr | ||
|
||
const onSourceEnd = err => { | ||
|
@@ -54,11 +64,38 @@ module.exports = ({ id, name, send, onEnd = () => {}, type = 'initiator', maxMsg | |
stream.timeline.close = Date.now() | ||
onEnd(endErr) | ||
} | ||
if (sinkClosedDefer) sinkClosedDefer.resolve() | ||
} | ||
|
||
const _send = (message) => { | ||
if (!sinkEnded) { | ||
send(message) | ||
} | ||
} | ||
|
||
/** @type {MuxedStream} */ | ||
const stream = { | ||
// Close for both Reading and Writing | ||
close: () => Promise.all([ | ||
stream.closeRead(), | ||
stream.closeWrite() | ||
]), | ||
// Close for reading | ||
close: () => stream.source.end(), | ||
closeRead: () => stream.source.end(), | ||
// Close for writing | ||
closeWrite: () => { | ||
if (sinkEnded) { | ||
return | ||
} | ||
|
||
if (sinkInProgress) { | ||
sinkClosedDefer = pDefer() | ||
writeCloseController.abort() | ||
return sinkClosedDefer.promise | ||
} | ||
|
||
return stream.sink([]) | ||
}, | ||
// Close for reading and writing (local error) | ||
abort: err => { | ||
log('%s stream %s abort', type, name, err) | ||
|
@@ -75,41 +112,49 @@ module.exports = ({ id, name, send, onEnd = () => {}, type = 'initiator', maxMsg | |
onSinkEnd(err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problems seems here, both in reset and abort. They call Removing stream.source.end(err)
return onSinkEnd(err) Without the previous change, this was not problematic somehow. I think it is related to something now happening in a different event loop. What do you think to be the best approach here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not entirely sure I follow. Both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see, I guess it's because in I'll see tomorrow if I can clean this up a bit, but at least it works now. |
||
}, | ||
sink: async source => { | ||
if (sinkInProgress) { | ||
throw errCode(new Error('the sink was already opened'), 'ERR_SINK_ALREADY_OPENED') | ||
} | ||
|
||
sinkInProgress = true | ||
source = abortable(source, [ | ||
{ signal: abortController.signal, options: { abortMessage: 'stream aborted', abortCode: ERR_MPLEX_STREAM_ABORT } }, | ||
{ signal: resetController.signal, options: { abortMessage: 'stream reset', abortCode: ERR_MPLEX_STREAM_RESET } } | ||
{ signal: resetController.signal, options: { abortMessage: 'stream reset', abortCode: ERR_MPLEX_STREAM_RESET } }, | ||
{ signal: writeCloseController.signal, options: { abortMessage: 'write stream closed', abortCode: MPLEX_WRITE_STREAM_CLOSED } } | ||
]) | ||
|
||
if (type === 'initiator') { // If initiator, open a new stream | ||
send({ id, type: Types.NEW_STREAM, data: name }) | ||
_send({ id, type: Types.NEW_STREAM, data: name }) | ||
} | ||
|
||
try { | ||
for await (let data of source) { | ||
while (data.length) { | ||
if (data.length <= maxMsgSize) { | ||
send({ id, type: Types.MESSAGE, data }) | ||
_send({ id, type: Types.MESSAGE, data }) | ||
break | ||
} | ||
data = BufferList.isBufferList(data) ? data : new BufferList(data) | ||
send({ id, type: Types.MESSAGE, data: data.shallowSlice(0, maxMsgSize) }) | ||
_send({ id, type: Types.MESSAGE, data: data.shallowSlice(0, maxMsgSize) }) | ||
data.consume(maxMsgSize) | ||
} | ||
} | ||
} catch (err) { | ||
// Send no more data if this stream was remotely reset | ||
if (err.code === ERR_MPLEX_STREAM_RESET) { | ||
log('%s stream %s reset', type, name) | ||
} else { | ||
log('%s stream %s error', type, name, err) | ||
send({ id, type: Types.RESET }) | ||
} | ||
if (err.code !== MPLEX_WRITE_STREAM_CLOSED) { | ||
// Send no more data if this stream was remotely reset | ||
if (err.code === ERR_MPLEX_STREAM_RESET) { | ||
log('%s stream %s reset', type, name) | ||
} else { | ||
log('%s stream %s error', type, name, err) | ||
_send({ id, type: Types.RESET }) | ||
} | ||
|
||
stream.source.end(err) | ||
return onSinkEnd(err) | ||
stream.source.end(err) | ||
return onSinkEnd(err) | ||
} | ||
} | ||
|
||
send({ id, type: Types.CLOSE }) | ||
_send({ id, type: Types.CLOSE }) | ||
onSinkEnd() | ||
}, | ||
source: pushable(onSourceEnd), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm starting to wrap my head around these streams, and I have one question regarding the current code in relation to the spec: does it make sense to create a new stream right before closing it here? Does the spec require this, or could we simply replace it with
onSinkEnd()
and prohibit a thesink
function from being called ifsinkEnded
?I don't see anything indicating that there needs to be a stream in both directions in https://github.com/libp2p/specs/tree/master/mplex#opening-a-new-stream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the advantage of doing so is that we inform the other party that we will not write to it anymore and only expect read and their side will also be close for reading