Skip to content

Commit 96187aa

Browse files
committed
feat!: muxed streams as web streams
Refactors streams from duplex async iterables: ```js { source: Duplex<AsyncGenerator<Uint8Array, void, unknown>, Source<Uint8Array | Uint8ArrayList>, Promise<void> sink: (Source<Uint8Array | Uint8ArrayList>) => Promise<void> } ``` to `ReadableWriteablePair<Uint8Array>`s: ```js { readable: ReadableStream<Uint8Array> writable: WritableStream<Uint8Array> } ``` Since the close methods for web streams are asynchronous, this lets us close streams cleanly - that is, wait for any buffered data to be sent/consumed before closing the stream. We still need to be able abort a stream in an emergency, so streams have the following methods for graceful closing: ```js stream.readable.cancel(reason?: any): Promise<void> stream.writable.close(): Promise<void> // or stream.close(): Promise<void> ``` ..and for emergency closing: ```js stream.abort(err: Error): void ``` Connections and multiaddr connections have the same `close`/`abort` semantics, but are still Duplexes since making them web streams would mean we need to convert things like node streams (for tcp) to web streams which would just make things slower. Transports such as WebTransport and WebRTC already deal in web streams when multiplexing so these no longer need to be converted to Duplex streams so it's win-win. Fixes #1793
1 parent 6fdaa7d commit 96187aa

File tree

148 files changed

+6305
-2745
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+6305
-2745
lines changed

examples/protocol-and-stream-muxing/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ There is still one last feature, you can provide multiple protocols for the same
8282

8383
```JavaScript
8484
node2.handle(['/another-protocol/1.0.0', '/another-protocol/2.0.0'], ({ stream }) => {
85-
if (stream.stat.protocol === '/another-protocol/2.0.0') {
85+
if (stream.protocol === '/another-protocol/2.0.0') {
8686
// handle backwards compatibility
8787
}
8888

@@ -136,7 +136,7 @@ node2.handle(['/a', '/b'], ({ stream }) => {
136136
stream,
137137
async function (source) {
138138
for await (const msg of source) {
139-
console.log(`from: ${stream.stat.protocol}, msg: ${uint8ArrayToString(msg.subarray())}`)
139+
console.log(`from: ${stream.protocol}, msg: ${uint8ArrayToString(msg.subarray())}`)
140140
}
141141
}
142142
)

packages/interface-compliance-tests/package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@
9494
"test:firefox": "aegir test -t browser -- --browser firefox",
9595
"test:firefox-webworker": "aegir test -t webworker -- --browser firefox",
9696
"test:node": "aegir test -t node --cov",
97-
"test:electron-main": "aegir test -t electron-main"
97+
"test:electron-main": "aegir test -t electron-main",
98+
"generate": "protons src/stream-muxer/fixtures/pb/*.proto"
9899
},
99100
"dependencies": {
100101
"@libp2p/interface": "~0.0.1",
@@ -104,9 +105,9 @@
104105
"@libp2p/peer-collections": "^3.0.0",
105106
"@libp2p/peer-id": "^2.0.0",
106107
"@libp2p/peer-id-factory": "^2.0.0",
108+
"@libp2p/utils": "^3.0.12",
107109
"@multiformats/multiaddr": "^12.1.3",
108110
"abortable-iterator": "^5.0.1",
109-
"any-signal": "^4.1.1",
110111
"delay": "^6.0.0",
111112
"it-all": "^3.0.2",
112113
"it-drain": "^3.0.2",
@@ -121,13 +122,15 @@
121122
"p-defer": "^4.0.0",
122123
"p-limit": "^4.0.0",
123124
"p-wait-for": "^5.0.2",
125+
"protons-runtime": "^5.0.0",
124126
"sinon": "^15.1.2",
125127
"ts-sinon": "^2.0.2",
126128
"uint8arraylist": "^2.4.3",
127129
"uint8arrays": "^4.0.4"
128130
},
129131
"devDependencies": {
130-
"aegir": "^39.0.10"
132+
"aegir": "^39.0.10",
133+
"protons": "^7.0.2"
131134
},
132135
"typedoc": {
133136
"entryPoint": "./src/index.ts"

packages/interface-compliance-tests/src/connection-encryption/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function createMaConnPair (): [MultiaddrConnection, MultiaddrConnection]
1010
const output: MultiaddrConnection = {
1111
...duplex,
1212
close: async () => {},
13+
abort: () => {},
1314
remoteAddr: multiaddr('/ip4/127.0.0.1/tcp/4001'),
1415
timeline: {
1516
open: Date.now()

packages/interface-compliance-tests/src/connection/index.ts

+24-26
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,19 @@ export default (test: TestSetup<Connection>): void => {
2222
expect(connection.id).to.exist()
2323
expect(connection.remotePeer).to.exist()
2424
expect(connection.remoteAddr).to.exist()
25-
expect(connection.stat.status).to.equal('OPEN')
26-
expect(connection.stat.timeline.open).to.exist()
27-
expect(connection.stat.timeline.close).to.not.exist()
28-
expect(connection.stat.direction).to.exist()
25+
expect(connection.status).to.equal('OPEN')
26+
expect(connection.timeline.open).to.exist()
27+
expect(connection.timeline.close).to.not.exist()
28+
expect(connection.direction).to.exist()
2929
expect(connection.streams).to.eql([])
3030
expect(connection.tags).to.eql([])
3131
})
3232

3333
it('should get the metadata of an open connection', () => {
34-
const stat = connection.stat
35-
36-
expect(stat.status).to.equal('OPEN')
37-
expect(stat.direction).to.exist()
38-
expect(stat.timeline.open).to.exist()
39-
expect(stat.timeline.close).to.not.exist()
34+
expect(connection.status).to.equal('OPEN')
35+
expect(connection.direction).to.exist()
36+
expect(connection.timeline.open).to.exist()
37+
expect(connection.timeline.close).to.not.exist()
4038
})
4139

4240
it('should return an empty array of streams', () => {
@@ -51,7 +49,7 @@ export default (test: TestSetup<Connection>): void => {
5149
const protocolToUse = '/echo/0.0.1'
5250
const stream = await connection.newStream([protocolToUse])
5351

54-
expect(stream).to.have.nested.property('stat.protocol', protocolToUse)
52+
expect(stream).to.have.property('protocol', protocolToUse)
5553

5654
const connStreams = connection.streams
5755

@@ -79,19 +77,19 @@ export default (test: TestSetup<Connection>): void => {
7977
}, proxyHandler)
8078

8179
connection = await test.setup()
82-
connection.stat.timeline = timelineProxy
80+
connection.timeline = timelineProxy
8381
})
8482

8583
afterEach(async () => {
8684
await test.teardown()
8785
})
8886

8987
it('should be able to close the connection after being created', async () => {
90-
expect(connection.stat.timeline.close).to.not.exist()
88+
expect(connection.timeline.close).to.not.exist()
9189
await connection.close()
9290

93-
expect(connection.stat.timeline.close).to.exist()
94-
expect(connection.stat.status).to.equal('CLOSED')
91+
expect(connection.timeline.close).to.exist()
92+
expect(connection.status).to.equal('CLOSED')
9593
})
9694

9795
it('should be able to close the connection after opening a stream', async () => {
@@ -100,21 +98,21 @@ export default (test: TestSetup<Connection>): void => {
10098
await connection.newStream([protocol])
10199

102100
// Close connection
103-
expect(connection.stat.timeline.close).to.not.exist()
101+
expect(connection.timeline.close).to.not.exist()
104102
await connection.close()
105103

106-
expect(connection.stat.timeline.close).to.exist()
107-
expect(connection.stat.status).to.equal('CLOSED')
104+
expect(connection.timeline.close).to.exist()
105+
expect(connection.status).to.equal('CLOSED')
108106
})
109107

110108
it('should properly track streams', async () => {
111109
// Open stream
112110
const protocol = '/echo/0.0.1'
113111
const stream = await connection.newStream([protocol])
114-
expect(stream).to.have.nested.property('stat.protocol', protocol)
112+
expect(stream).to.have.property('protocol', protocol)
115113

116114
// Close stream
117-
stream.close()
115+
await stream.close()
118116

119117
expect(connection.streams.filter(s => s.id === stream.id)).to.be.empty()
120118
})
@@ -123,7 +121,7 @@ export default (test: TestSetup<Connection>): void => {
123121
// Open stream
124122
const protocol = '/echo/0.0.1'
125123
const stream = await connection.newStream(protocol)
126-
expect(stream).to.have.nested.property('stat.direction', 'outbound')
124+
expect(stream).to.have.property('direction', 'outbound')
127125
})
128126

129127
it.skip('should track inbound streams', async () => {
@@ -135,20 +133,20 @@ export default (test: TestSetup<Connection>): void => {
135133

136134
it('should support a proxy on the timeline', async () => {
137135
sinon.spy(proxyHandler, 'set')
138-
expect(connection.stat.timeline.close).to.not.exist()
136+
expect(connection.timeline.close).to.not.exist()
139137

140138
await connection.close()
141139
// @ts-expect-error - fails to infer callCount
142140
expect(proxyHandler.set.callCount).to.equal(1)
143141
// @ts-expect-error - fails to infer getCall
144142
const [obj, key, value] = proxyHandler.set.getCall(0).args
145-
expect(obj).to.eql(connection.stat.timeline)
143+
expect(obj).to.eql(connection.timeline)
146144
expect(key).to.equal('close')
147-
expect(value).to.be.a('number').that.equals(connection.stat.timeline.close)
145+
expect(value).to.be.a('number').that.equals(connection.timeline.close)
148146
})
149147

150148
it('should fail to create a new stream if the connection is closing', async () => {
151-
expect(connection.stat.timeline.close).to.not.exist()
149+
expect(connection.timeline.close).to.not.exist()
152150
const p = connection.close()
153151

154152
try {
@@ -165,7 +163,7 @@ export default (test: TestSetup<Connection>): void => {
165163
})
166164

167165
it('should fail to create a new stream if the connection is closed', async () => {
168-
expect(connection.stat.timeline.close).to.not.exist()
166+
expect(connection.timeline.close).to.not.exist()
169167
await connection.close()
170168

171169
try {

packages/interface-compliance-tests/src/mocks/connection.ts

+39-48
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as STATUS from '@libp2p/interface/connection/status'
1+
import * as Status from '@libp2p/interface/connection/status'
22
import { CodeError } from '@libp2p/interface/errors'
33
import { logger } from '@libp2p/logger'
44
import * as mss from '@libp2p/multistream-select'
@@ -9,13 +9,11 @@ import { mockMultiaddrConnection } from './multiaddr-connection.js'
99
import { mockMuxer } from './muxer.js'
1010
import { mockRegistrar } from './registrar.js'
1111
import type { AbortOptions } from '@libp2p/interface'
12-
import type { MultiaddrConnection, Connection, Stream, ConnectionStat, Direction } from '@libp2p/interface/connection'
12+
import type { MultiaddrConnection, Connection, Stream, Direction, ByteStream, ConnectionTimeline } from '@libp2p/interface/connection'
1313
import type { PeerId } from '@libp2p/interface/peer-id'
1414
import type { StreamMuxer, StreamMuxerFactory } from '@libp2p/interface/stream-muxer'
1515
import type { Registrar } from '@libp2p/interface-internal/registrar'
1616
import type { Multiaddr } from '@multiformats/multiaddr'
17-
import type { Duplex, Source } from 'it-stream-types'
18-
import type { Uint8ArrayList } from 'uint8arraylist'
1917

2018
const log = logger('libp2p:mock-connection')
2119

@@ -38,7 +36,10 @@ class MockConnection implements Connection {
3836
public remoteAddr: Multiaddr
3937
public remotePeer: PeerId
4038
public direction: Direction
41-
public stat: ConnectionStat
39+
public timeline: ConnectionTimeline
40+
public multiplexer?: string
41+
public encryption?: string
42+
public status: keyof typeof Status
4243
public streams: Stream[]
4344
public tags: string[]
4445

@@ -52,13 +53,10 @@ class MockConnection implements Connection {
5253
this.remoteAddr = remoteAddr
5354
this.remotePeer = remotePeer
5455
this.direction = direction
55-
this.stat = {
56-
status: STATUS.OPEN,
57-
direction,
58-
timeline: maConn.timeline,
59-
multiplexer: 'test-multiplexer',
60-
encryption: 'yes-yes-very-secure'
61-
}
56+
this.status = Status.OPEN
57+
this.timeline = maConn.timeline
58+
this.multiplexer = 'test-multiplexer'
59+
this.encryption = 'yes-yes-very-secure'
6260
this.streams = []
6361
this.tags = []
6462
this.muxer = muxer
@@ -74,30 +72,20 @@ class MockConnection implements Connection {
7472
throw new Error('protocols must have a length')
7573
}
7674

77-
if (this.stat.status !== STATUS.OPEN) {
75+
if (this.status !== Status.OPEN) {
7876
throw new CodeError('connection must be open to create streams', 'ERR_CONNECTION_CLOSED')
7977
}
8078

8179
const id = `${Math.random()}`
8280
const stream = await this.muxer.newStream(id)
83-
const result = await mss.select(stream, protocols, options)
84-
85-
const streamWithProtocol: Stream = {
86-
...stream,
87-
...result.stream,
88-
stat: {
89-
...stream.stat,
90-
direction: 'outbound',
91-
protocol: result.protocol
92-
}
93-
}
81+
const protocolStream = await mss.select(stream, protocols, options)
9482

95-
this.streams.push(streamWithProtocol)
83+
this.streams.push(protocolStream)
9684

97-
return streamWithProtocol
85+
return protocolStream
9886
}
9987

100-
addStream (stream: Stream): void {
88+
addStream (stream: any): void {
10189
this.streams.push(stream)
10290
}
10391

@@ -106,13 +94,23 @@ class MockConnection implements Connection {
10694
}
10795

10896
async close (): Promise<void> {
109-
this.stat.status = STATUS.CLOSING
97+
this.status = Status.CLOSING
98+
await Promise.all(
99+
this.streams.map(async s => s.close())
100+
)
110101
await this.maConn.close()
102+
this.status = Status.CLOSED
103+
this.timeline.close = Date.now()
104+
}
105+
106+
abort (err: Error): void {
107+
this.status = Status.CLOSING
111108
this.streams.forEach(s => {
112-
s.close()
109+
s.abort(err)
113110
})
114-
this.stat.status = STATUS.CLOSED
115-
this.stat.timeline.close = Date.now()
111+
this.maConn.abort(err)
112+
this.status = Status.CLOSED
113+
this.timeline.close = Date.now()
116114
}
117115
}
118116

@@ -134,15 +132,13 @@ export function mockConnection (maConn: MultiaddrConnection, opts: MockConnectio
134132
onIncomingStream: (muxedStream) => {
135133
try {
136134
mss.handle(muxedStream, registrar.getProtocols())
137-
.then(({ stream, protocol }) => {
138-
log('%s: incoming stream opened on %s', direction, protocol)
139-
muxedStream = { ...muxedStream, ...stream }
140-
muxedStream.stat.protocol = protocol
135+
.then(stream => {
136+
log('%s: incoming stream opened on %s', stream.direction, stream.protocol)
141137

142138
connection.addStream(muxedStream)
143-
const { handler } = registrar.getHandler(protocol)
139+
const { handler } = registrar.getHandler(stream.protocol)
144140

145-
handler({ connection, stream: muxedStream })
141+
handler({ connection, stream })
146142
}).catch(err => {
147143
log.error(err)
148144
})
@@ -170,20 +166,15 @@ export function mockConnection (maConn: MultiaddrConnection, opts: MockConnectio
170166
return connection
171167
}
172168

173-
export function mockStream (stream: Duplex<AsyncGenerator<Uint8ArrayList>, Source<Uint8ArrayList | Uint8Array>, Promise<void>>): Stream {
169+
export function mockStream (stream: ByteStream): Stream {
174170
return {
175171
...stream,
176-
close: () => {},
177-
closeRead: () => {},
178-
closeWrite: () => {},
172+
close: async () => {},
179173
abort: () => {},
180-
reset: () => {},
181-
stat: {
182-
direction: 'outbound',
183-
protocol: '/foo/1.0.0',
184-
timeline: {
185-
open: Date.now()
186-
}
174+
direction: 'outbound',
175+
protocol: '/foo/1.0.0',
176+
timeline: {
177+
open: Date.now()
187178
},
188179
metadata: {},
189180
id: `stream-${Date.now()}`

packages/interface-compliance-tests/src/mocks/multiaddr-connection.ts

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function mockMultiaddrConnection (source: Duplex<AsyncGenerator<Uint8Arra
1111
async close () {
1212

1313
},
14+
abort: () => {},
1415
timeline: {
1516
open: Date.now()
1617
},
@@ -44,6 +45,10 @@ export function mockMultiaddrConnPair (opts: MockMultiaddrConnPairOptions): { in
4445
close: async () => {
4546
outbound.timeline.close = Date.now()
4647
controller.abort()
48+
},
49+
abort: () => {
50+
outbound.timeline.close = Date.now()
51+
controller.abort()
4752
}
4853
}
4954

@@ -56,6 +61,10 @@ export function mockMultiaddrConnPair (opts: MockMultiaddrConnPairOptions): { in
5661
close: async () => {
5762
inbound.timeline.close = Date.now()
5863
controller.abort()
64+
},
65+
abort: () => {
66+
inbound.timeline.close = Date.now()
67+
controller.abort()
5968
}
6069
}
6170

0 commit comments

Comments
 (0)