@libp2p/interface-mocks
Version:
Mock implementations of several libp2p interfaces
341 lines • 12.4 kB
JavaScript
import { CodeError } from '@libp2p/interfaces/errors';
import { logger } from '@libp2p/logger';
import { abortableSource } from 'abortable-iterator';
import { anySignal } from 'any-signal';
import map from 'it-map';
import * as ndjson from 'it-ndjson';
import { pipe } from 'it-pipe';
import { pushable } from 'it-pushable';
import { Uint8ArrayList } from 'uint8arraylist';
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
let muxers = 0;
let streams = 0;
const MAX_MESSAGE_SIZE = 1024 * 1024;
class MuxedStream {
id;
input;
stream;
type;
sinkEnded;
sourceEnded;
abortController;
resetController;
closeController;
log;
constructor(init) {
const { id, type, push, onEnd } = init;
this.log = logger(`libp2p:mock-muxer:stream:${id}:${type}`);
this.id = id;
this.type = type;
this.abortController = new AbortController();
this.resetController = new AbortController();
this.closeController = new AbortController();
this.sourceEnded = false;
this.sinkEnded = false;
let endErr;
const onSourceEnd = (err) => {
if (this.sourceEnded) {
return;
}
this.log('onSourceEnd sink ended? %s', this.sinkEnded);
this.sourceEnded = true;
if (err != null && endErr == null) {
endErr = err;
}
if (this.sinkEnded) {
this.stream.stat.timeline.close = Date.now();
if (onEnd != null) {
onEnd(endErr);
}
}
};
const onSinkEnd = (err) => {
if (this.sinkEnded) {
return;
}
this.log('onSinkEnd source ended? %s', this.sourceEnded);
this.sinkEnded = true;
if (err != null && endErr == null) {
endErr = err;
}
if (this.sourceEnded) {
this.stream.stat.timeline.close = Date.now();
if (onEnd != null) {
onEnd(endErr);
}
}
};
this.input = pushable({
onEnd: onSourceEnd
});
this.stream = {
id,
sink: async (source) => {
if (this.sinkEnded) {
throw new CodeError('stream closed for writing', 'ERR_SINK_ENDED');
}
const signal = anySignal([
this.abortController.signal,
this.resetController.signal,
this.closeController.signal
]);
source = abortableSource(source, signal);
try {
if (this.type === 'initiator') {
// If initiator, open a new stream
const createMsg = {
id: this.id,
type: 'create',
direction: this.type
};
push.push(createMsg);
}
const list = new Uint8ArrayList();
for await (const chunk of source) {
list.append(chunk);
while (list.length > 0) {
const available = Math.min(list.length, MAX_MESSAGE_SIZE);
const dataMsg = {
id,
type: 'data',
chunk: uint8ArrayToString(list.subarray(0, available), 'base64pad'),
direction: this.type
};
push.push(dataMsg);
list.consume(available);
}
}
}
catch (err) {
if (err.type === 'aborted' && err.message === 'The operation was aborted') {
if (this.closeController.signal.aborted) {
return;
}
if (this.resetController.signal.aborted) {
err.message = 'stream reset';
err.code = 'ERR_STREAM_RESET';
}
if (this.abortController.signal.aborted) {
err.message = 'stream aborted';
err.code = 'ERR_STREAM_ABORT';
}
}
// Send no more data if this stream was remotely reset
if (err.code !== 'ERR_STREAM_RESET') {
const resetMsg = {
id,
type: 'reset',
direction: this.type
};
push.push(resetMsg);
}
this.log('sink erred', err);
this.input.end(err);
onSinkEnd(err);
return;
}
finally {
signal.clear();
}
this.log('sink ended');
onSinkEnd();
const closeMsg = {
id,
type: 'close',
direction: this.type
};
push.push(closeMsg);
},
source: this.input,
// Close for reading
close: () => {
this.stream.closeRead();
this.stream.closeWrite();
},
closeRead: () => {
this.input.end();
},
closeWrite: () => {
this.closeController.abort();
const closeMsg = {
id,
type: 'close',
direction: this.type
};
push.push(closeMsg);
onSinkEnd();
},
// Close for reading and writing (local error)
abort: (err) => {
// End the source with the passed error
this.input.end(err);
this.abortController.abort();
onSinkEnd(err);
},
// Close immediately for reading and writing (remote error)
reset: () => {
const err = new CodeError('stream reset', 'ERR_STREAM_RESET');
this.resetController.abort();
this.input.end(err);
onSinkEnd(err);
},
stat: {
direction: type === 'initiator' ? 'outbound' : 'inbound',
timeline: {
open: Date.now()
}
},
metadata: {}
};
}
}
class MockMuxer {
source;
input;
streamInput;
name;
protocol = '/mock-muxer/1.0.0';
closeController;
registryInitiatorStreams;
registryRecipientStreams;
options;
log;
constructor(init) {
this.name = `muxer:${muxers++}`;
this.log = logger(`libp2p:mock-muxer:${this.name}`);
this.registryInitiatorStreams = new Map();
this.registryRecipientStreams = new Map();
this.log('create muxer');
this.options = init ?? { direction: 'inbound' };
this.closeController = new AbortController();
// receives data from the muxer at the other end of the stream
this.source = this.input = pushable({
onEnd: (err) => {
this.close(err);
}
});
// receives messages from all of the muxed streams
this.streamInput = pushable({
objectMode: true
});
}
// receive incoming messages
async sink(source) {
try {
await pipe(abortableSource(source, this.closeController.signal), (source) => map(source, buf => uint8ArrayToString(buf.subarray())), (ndjson.parse), async (source) => {
for await (const message of source) {
this.log.trace('-> %s %s %s', message.type, message.direction, message.id);
this.handleMessage(message);
}
});
this.log('muxed stream ended');
this.input.end();
}
catch (err) {
this.log('muxed stream errored', err);
this.input.end(err);
}
}
handleMessage(message) {
let muxedStream;
const registry = message.direction === 'initiator' ? this.registryRecipientStreams : this.registryInitiatorStreams;
if (message.type === 'create') {
if (registry.has(message.id)) {
throw new Error(`Already had stream for ${message.id}`);
}
muxedStream = this.createStream(message.id, 'recipient');
registry.set(muxedStream.stream.id, muxedStream);
if (this.options.onIncomingStream != null) {
this.options.onIncomingStream(muxedStream.stream);
}
}
muxedStream = registry.get(message.id);
if (muxedStream == null) {
this.log.error(`No stream found for ${message.id}`);
return;
}
if (message.type === 'data') {
muxedStream.input.push(new Uint8ArrayList(uint8ArrayFromString(message.chunk, 'base64pad')));
}
else if (message.type === 'reset') {
this.log('-> reset stream %s %s', muxedStream.type, muxedStream.stream.id);
muxedStream.stream.reset();
}
else if (message.type === 'close') {
this.log('-> closing stream %s %s', muxedStream.type, muxedStream.stream.id);
muxedStream.stream.closeRead();
}
}
get streams() {
return Array.from(this.registryRecipientStreams.values())
.concat(Array.from(this.registryInitiatorStreams.values()))
.map(({ stream }) => stream);
}
newStream(name) {
if (this.closeController.signal.aborted) {
throw new Error('Muxer already closed');
}
this.log('newStream %s', name);
const storedStream = this.createStream(name, 'initiator');
this.registryInitiatorStreams.set(storedStream.stream.id, storedStream);
return storedStream.stream;
}
createStream(name, type = 'initiator') {
const id = name ?? `${this.name}:stream:${streams++}`;
this.log('createStream %s %s', type, id);
const muxedStream = new MuxedStream({
id,
type,
push: this.streamInput,
onEnd: () => {
this.log('stream ended %s %s', type, id);
if (type === 'initiator') {
this.registryInitiatorStreams.delete(id);
}
else {
this.registryRecipientStreams.delete(id);
}
if (this.options.onStreamEnd != null) {
this.options.onStreamEnd(muxedStream.stream);
}
}
});
return muxedStream;
}
close(err) {
if (this.closeController.signal.aborted)
return;
this.log('closing muxed streams');
if (err == null) {
this.streams.forEach(s => {
s.close();
});
}
else {
this.streams.forEach(s => {
s.abort(err);
});
}
this.closeController.abort();
this.input.end(err);
}
}
class MockMuxerFactory {
protocol = '/mock-muxer/1.0.0';
createStreamMuxer(init) {
const mockMuxer = new MockMuxer(init);
void Promise.resolve().then(async () => {
void pipe(mockMuxer.streamInput, ndjson.stringify, (source) => map(source, str => new Uint8ArrayList(uint8ArrayFromString(str))), async (source) => {
for await (const buf of source) {
mockMuxer.input.push(buf.subarray());
}
});
});
return mockMuxer;
}
}
export function mockMuxer() {
return new MockMuxerFactory();
}
//# sourceMappingURL=muxer.js.map