UNPKG

@libp2p/interface-mocks

Version:
341 lines 12.4 kB
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