Skip to content

Commit dd7b329

Browse files
authored
fix: allow importing @libp2p/tcp in browsers (#2682)
When importing `@libp2p/tcp`, Instead of failing at compile time, fail at runtime to make it easier to write isomorphic js.
1 parent 10610a5 commit dd7b329

File tree

6 files changed

+330
-201
lines changed

6 files changed

+330
-201
lines changed

packages/transport-tcp/.aegir.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
/** @type {import('aegir').PartialOptions} */
23
export default {
34
build: {
45
config: {

packages/transport-tcp/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
"doc-check": "aegir doc-check",
5353
"build": "aegir build",
5454
"test": "aegir test -t node -t electron-main",
55+
"test:chrome": "aegir test -t browser -f ./dist/test/browser.js --cov",
56+
"test:chrome-webworker": "aegir test -t webworker -f ./dist/test/browser.js",
57+
"test:firefox": "aegir test -t browser -f ./dist/test/browser.js -- --browser firefox",
58+
"test:firefox-webworker": "aegir test -t webworker -f ./dist/test/browser.js -- --browser firefox",
5559
"test:node": "aegir test -t node --cov",
5660
"test:electron-main": "aegir test -t electron-main"
5761
},
@@ -72,7 +76,11 @@
7276
"it-pipe": "^3.0.1",
7377
"p-defer": "^4.0.1",
7478
"sinon": "^18.0.0",
75-
"uint8arrays": "^5.1.0"
79+
"uint8arrays": "^5.1.0",
80+
"wherearewe": "^2.0.1"
81+
},
82+
"browser": {
83+
"./dist/src/tcp.js": "./dist/src/tcp.browser.js"
7684
},
7785
"sideEffects": false
7886
}

packages/transport-tcp/src/index.ts

Lines changed: 4 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,10 @@
2727
* ```
2828
*/
2929

30-
import net from 'net'
31-
import { AbortError, CodeError, serviceCapabilities, transportSymbol } from '@libp2p/interface'
32-
import * as mafmt from '@multiformats/mafmt'
33-
import { CustomProgressEvent } from 'progress-events'
34-
import { CODE_CIRCUIT, CODE_P2P, CODE_UNIX } from './constants.js'
35-
import { type CloseServerOnMaxConnectionsOpts, TCPListener } from './listener.js'
36-
import { toMultiaddrConnection } from './socket-to-conn.js'
37-
import { multiaddrToNetConfig } from './utils.js'
38-
import type { ComponentLogger, Logger, Connection, CounterGroup, Metrics, CreateListenerOptions, DialTransportOptions, Transport, Listener, OutboundConnectionUpgradeEvents } from '@libp2p/interface'
39-
import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
40-
import type { Socket, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net'
30+
import { TCP } from './tcp.js'
31+
import type { CloseServerOnMaxConnectionsOpts } from './listener.js'
32+
import type { ComponentLogger, CounterGroup, Metrics, CreateListenerOptions, DialTransportOptions, Transport, OutboundConnectionUpgradeEvents } from '@libp2p/interface'
33+
import type { AbortOptions } from '@multiformats/multiaddr'
4134
import type { ProgressEvent } from 'progress-events'
4235

4336
export interface TCPOptions {
@@ -131,195 +124,6 @@ export interface TCPMetrics {
131124
dialerEvents: CounterGroup
132125
}
133126

134-
class TCP implements Transport<TCPDialEvents> {
135-
private readonly opts: TCPOptions
136-
private readonly metrics?: TCPMetrics
137-
private readonly components: TCPComponents
138-
private readonly log: Logger
139-
140-
constructor (components: TCPComponents, options: TCPOptions = {}) {
141-
this.log = components.logger.forComponent('libp2p:tcp')
142-
this.opts = options
143-
this.components = components
144-
145-
if (components.metrics != null) {
146-
this.metrics = {
147-
dialerEvents: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', {
148-
label: 'event',
149-
help: 'Total count of TCP dialer events by type'
150-
})
151-
}
152-
}
153-
}
154-
155-
readonly [transportSymbol] = true
156-
157-
readonly [Symbol.toStringTag] = '@libp2p/tcp'
158-
159-
readonly [serviceCapabilities]: string[] = [
160-
'@libp2p/transport'
161-
]
162-
163-
async dial (ma: Multiaddr, options: TCPDialOptions): Promise<Connection> {
164-
options.keepAlive = options.keepAlive ?? true
165-
options.noDelay = options.noDelay ?? true
166-
167-
// options.signal destroys the socket before 'connect' event
168-
const socket = await this._connect(ma, options)
169-
170-
// Avoid uncaught errors caused by unstable connections
171-
socket.on('error', err => {
172-
this.log('socket error', err)
173-
})
174-
175-
const maConn = toMultiaddrConnection(socket, {
176-
remoteAddr: ma,
177-
socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout,
178-
socketCloseTimeout: this.opts.socketCloseTimeout,
179-
metrics: this.metrics?.dialerEvents,
180-
logger: this.components.logger
181-
})
182-
183-
const onAbort = (): void => {
184-
maConn.close().catch(err => {
185-
this.log.error('Error closing maConn after abort', err)
186-
})
187-
}
188-
options.signal?.addEventListener('abort', onAbort, { once: true })
189-
190-
this.log('new outbound connection %s', maConn.remoteAddr)
191-
const conn = await options.upgrader.upgradeOutbound(maConn)
192-
this.log('outbound connection %s upgraded', maConn.remoteAddr)
193-
194-
options.signal?.removeEventListener('abort', onAbort)
195-
196-
if (options.signal?.aborted === true) {
197-
conn.close().catch(err => {
198-
this.log.error('Error closing conn after abort', err)
199-
})
200-
201-
throw new AbortError()
202-
}
203-
204-
return conn
205-
}
206-
207-
async _connect (ma: Multiaddr, options: TCPDialOptions): Promise<Socket> {
208-
options.signal?.throwIfAborted()
209-
options.onProgress?.(new CustomProgressEvent('tcp:open-connection'))
210-
211-
return new Promise<Socket>((resolve, reject) => {
212-
const start = Date.now()
213-
const cOpts = multiaddrToNetConfig(ma, {
214-
...(this.opts.dialOpts ?? {}),
215-
...options
216-
}) as (IpcSocketConnectOpts & TcpSocketConnectOpts)
217-
218-
this.log('dialing %a', ma)
219-
const rawSocket = net.connect(cOpts)
220-
221-
const onError = (err: Error): void => {
222-
const cOptsStr = cOpts.path ?? `${cOpts.host ?? ''}:${cOpts.port}`
223-
err.message = `connection error ${cOptsStr}: ${err.message}`
224-
this.metrics?.dialerEvents.increment({ error: true })
225-
226-
done(err)
227-
}
228-
229-
const onTimeout = (): void => {
230-
this.log('connection timeout %a', ma)
231-
this.metrics?.dialerEvents.increment({ timeout: true })
232-
233-
const err = new CodeError(`connection timeout after ${Date.now() - start}ms`, 'ERR_CONNECT_TIMEOUT')
234-
// Note: this will result in onError() being called
235-
rawSocket.emit('error', err)
236-
}
237-
238-
const onConnect = (): void => {
239-
this.log('connection opened %a', ma)
240-
this.metrics?.dialerEvents.increment({ connect: true })
241-
done()
242-
}
243-
244-
const onAbort = (): void => {
245-
this.log('connection aborted %a', ma)
246-
this.metrics?.dialerEvents.increment({ abort: true })
247-
rawSocket.destroy()
248-
done(new AbortError())
249-
}
250-
251-
const done = (err?: Error): void => {
252-
rawSocket.removeListener('error', onError)
253-
rawSocket.removeListener('timeout', onTimeout)
254-
rawSocket.removeListener('connect', onConnect)
255-
256-
if (options.signal != null) {
257-
options.signal.removeEventListener('abort', onAbort)
258-
}
259-
260-
if (err != null) {
261-
reject(err); return
262-
}
263-
264-
resolve(rawSocket)
265-
}
266-
267-
rawSocket.on('error', onError)
268-
rawSocket.on('timeout', onTimeout)
269-
rawSocket.on('connect', onConnect)
270-
271-
if (options.signal != null) {
272-
options.signal.addEventListener('abort', onAbort)
273-
}
274-
})
275-
}
276-
277-
/**
278-
* Creates a TCP listener. The provided `handler` function will be called
279-
* anytime a new incoming Connection has been successfully upgraded via
280-
* `upgrader.upgradeInbound`.
281-
*/
282-
createListener (options: TCPCreateListenerOptions): Listener {
283-
return new TCPListener({
284-
...(this.opts.listenOpts ?? {}),
285-
...options,
286-
maxConnections: this.opts.maxConnections,
287-
backlog: this.opts.backlog,
288-
closeServerOnMaxConnections: this.opts.closeServerOnMaxConnections,
289-
socketInactivityTimeout: this.opts.inboundSocketInactivityTimeout,
290-
socketCloseTimeout: this.opts.socketCloseTimeout,
291-
metrics: this.components.metrics,
292-
logger: this.components.logger
293-
})
294-
}
295-
296-
/**
297-
* Takes a list of `Multiaddr`s and returns only valid TCP addresses
298-
*/
299-
listenFilter (multiaddrs: Multiaddr[]): Multiaddr[] {
300-
multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs]
301-
302-
return multiaddrs.filter(ma => {
303-
if (ma.protoCodes().includes(CODE_CIRCUIT)) {
304-
return false
305-
}
306-
307-
if (ma.protoCodes().includes(CODE_UNIX)) {
308-
return true
309-
}
310-
311-
return mafmt.TCP.matches(ma.decapsulateCode(CODE_P2P))
312-
})
313-
}
314-
315-
/**
316-
* Filter check for all Multiaddrs that this transport can dial
317-
*/
318-
dialFilter (multiaddrs: Multiaddr[]): Multiaddr[] {
319-
return this.listenFilter(multiaddrs)
320-
}
321-
}
322-
323127
export function tcp (init: TCPOptions = {}): (components: TCPComponents) => Transport {
324128
return (components: TCPComponents) => {
325129
return new TCP(components, init)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @packageDocumentation
3+
*
4+
* A [libp2p transport](https://docs.libp2p.io/concepts/transports/overview/) based on the TCP networking stack.
5+
*
6+
* @example
7+
*
8+
* ```TypeScript
9+
* import { createLibp2p } from 'libp2p'
10+
* import { tcp } from '@libp2p/tcp'
11+
* import { multiaddr } from '@multiformats/multiaddr'
12+
*
13+
* const node = await createLibp2p({
14+
* transports: [
15+
* tcp()
16+
* ]
17+
* })
18+
*
19+
* const ma = multiaddr('/ip4/123.123.123.123/tcp/1234')
20+
*
21+
* // dial a TCP connection, timing out after 10 seconds
22+
* const connection = await node.dial(ma, {
23+
* signal: AbortSignal.timeout(10_000)
24+
* })
25+
*
26+
* // use connection...
27+
* ```
28+
*/
29+
30+
import { serviceCapabilities, transportSymbol } from '@libp2p/interface'
31+
import type { TCPComponents, TCPDialEvents, TCPMetrics, TCPOptions } from './index.js'
32+
import type { Logger, Connection, Transport, Listener } from '@libp2p/interface'
33+
import type { Multiaddr } from '@multiformats/multiaddr'
34+
35+
export class TCP implements Transport<TCPDialEvents> {
36+
private readonly opts: TCPOptions
37+
private readonly metrics?: TCPMetrics
38+
private readonly components: TCPComponents
39+
private readonly log: Logger
40+
41+
constructor () {
42+
throw new Error('TCP connections are not possible in browsers')
43+
}
44+
45+
readonly [transportSymbol] = true
46+
47+
readonly [Symbol.toStringTag] = '@libp2p/tcp'
48+
49+
readonly [serviceCapabilities]: string[] = [
50+
'@libp2p/transport'
51+
]
52+
53+
async dial (): Promise<Connection> {
54+
throw new Error('TCP connections are not possible in browsers')
55+
}
56+
57+
createListener (): Listener {
58+
throw new Error('TCP connections are not possible in browsers')
59+
}
60+
61+
listenFilter (): Multiaddr[] {
62+
return []
63+
}
64+
65+
dialFilter (): Multiaddr[] {
66+
return []
67+
}
68+
}

0 commit comments

Comments
 (0)