diff --git a/interop/BrowserDockerfile b/interop/BrowserDockerfile index 5e3edc6981..30a41ad354 100644 --- a/interop/BrowserDockerfile +++ b/interop/BrowserDockerfile @@ -8,4 +8,8 @@ WORKDIR /app/interop ARG BROWSER=chromium ENV BROWSER=${BROWSER} +# hack to ensure the correct browser version is installed while building the +# container and not during the test run which slows everything down +RUN npx playwright-test --runner mocha --browser $BROWSER --grep do-not-match-anything + ENTRYPOINT npm test -- -t browser -- --browser $BROWSER diff --git a/interop/README.md b/interop/README.md index bc321dbd24..f6f44ba8e6 100644 --- a/interop/README.md +++ b/interop/README.md @@ -40,14 +40,14 @@ This must be repeated every time you make a change to the js-libp2p source code. ```console $ npm run build -$ docker build . -f ./interop/Dockerfile -t js-libp2p-node +$ docker build . -f ./interop/Dockerfile -t node-js-libp2p-head ``` #### Browsers ```console $ npm run build -$ docker build . -f ./interop/BrowserDockerfile -t js-libp2p-browsers +$ docker build . -f ./interop/BrowserDockerfile -t browsers-js-libp2p-head ``` ### Build another libp2p implementation @@ -93,13 +93,13 @@ $ docker run --name redis --rm -p 6379:6379 redis:7-alpine #### node.js ```console -$ docker run -e transport=tcp -e muxer=yamux -e security=noise -e is_dialer=true -e redis_addr=redis:6379 --link redis:redis js-libp2p-node +$ docker run -e transport=tcp -e muxer=yamux -e security=noise -e is_dialer=true -e redis_addr=redis:6379 --link redis:redis node-js-libp2p-head ``` #### Browsers ```console -$ docker run -e transport=webtransport -e muxer=yamux -e security=noise -e is_dialer=true -e redis_addr=redis:6379 --link redis:redis js-libp2p-browsers +$ docker run -e transport=webtransport -e muxer=yamux -e security=noise -e is_dialer=true -e redis_addr=redis:6379 --link redis:redis browsers-js-libp2p-head ``` ### Start another libp2p implementation @@ -110,6 +110,9 @@ $ docker run -e transport=webtransport -e muxer=yamux -e security=noise -e is_di ```console $ docker run -e transport=tcp -e muxer=yamux -e security=noise -e is_dialer=false -e redis_addr=redis:6379 --link redis:redis go-v0.29 + + +docker run -e transport=webrtc-direct -e is_dialer=false -e redis_addr=redis:6379 --link redis:redis go-v0.42 ``` # License diff --git a/packages/interface-compliance-tests/src/transport/index.ts b/packages/interface-compliance-tests/src/transport/index.ts index 00c3b260e4..2256bd4b58 100644 --- a/packages/interface-compliance-tests/src/transport/index.ts +++ b/packages/interface-compliance-tests/src/transport/index.ts @@ -167,7 +167,7 @@ export default (common: TestSetup): void => { const input = Uint8Array.from([0, 1, 2, 3, 4]) const output = await dialer.services.echo.echo(dialAddrs[0], input, { - signal: AbortSignal.timeout(5000) + signal: AbortSignal.timeout(5_000) }) expect(output.subarray()).to.equalBytes(input) @@ -179,12 +179,12 @@ export default (common: TestSetup): void => { const input = Uint8Array.from([0, 1, 2, 3, 4]) const output1 = await dialer.services.echo.echo(dialAddrs[0], input, { - signal: AbortSignal.timeout(5000) + signal: AbortSignal.timeout(5_000) }) expect(output1.subarray()).to.equalBytes(input) const output2 = await dialer.services.echo.echo(dialAddrs[1], input, { - signal: AbortSignal.timeout(5000), + signal: AbortSignal.timeout(5_000), force: true }) expect(output2.subarray()).to.equalBytes(input) @@ -194,10 +194,13 @@ export default (common: TestSetup): void => { ({ dialer, listener, dialAddrs } = await getSetup(common)) const conn = await dialer.dial(dialAddrs[0], { - signal: AbortSignal.timeout(5000) + signal: AbortSignal.timeout(5_000) + }) + + await conn.close({ + signal: AbortSignal.timeout(5_000) }) - await conn.close() expect(isValidTick(conn.timeline.close)).to.equal(true) }) @@ -247,7 +250,9 @@ export default (common: TestSetup): void => { }) } - expect(connection).to.have.property('streams').that.has.lengthOf(5) + expect( + connection.streams.filter(s => s.protocol === '/echo/1.0.0') + ).to.have.lengthOf(5) if (remoteConn != null) { await pWaitFor(() => remoteConn.streams.filter(s => s.protocol === '/echo/1.0.0').length === 5, { diff --git a/packages/protocol-identify/src/identify.ts b/packages/protocol-identify/src/identify.ts index fa81a2fc02..89936de0ae 100644 --- a/packages/protocol-identify/src/identify.ts +++ b/packages/protocol-identify/src/identify.ts @@ -51,6 +51,8 @@ export class Identify extends AbstractIdentify implements Startable, IdentifyInt } } + this.log('run identify on new connection %a', connection.remoteAddr) + try { stream = await connection.newStream(this.protocol, { ...options, @@ -62,10 +64,7 @@ export class Identify extends AbstractIdentify implements Startable, IdentifyInt maxDataLength: this.maxMessageSize }).pb(IdentifyMessage) - log('read response') const message = await pb.read(options) - - log('close write') await pb.unwrap().unwrap().close(options) return message @@ -147,6 +146,8 @@ export class Identify extends AbstractIdentify implements Startable, IdentifyInt async handleProtocol (stream: Stream, connection: Connection): Promise { const log = stream.log.newScope('identify') + log('responding to identify') + const signal = AbortSignal.timeout(this.timeout) setMaxListeners(Infinity, signal) diff --git a/packages/transport-webrtc/src/muxer.ts b/packages/transport-webrtc/src/muxer.ts index 1f61101d8d..21da1ac5f8 100644 --- a/packages/transport-webrtc/src/muxer.ts +++ b/packages/transport-webrtc/src/muxer.ts @@ -39,26 +39,44 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory { private readonly peerConnection: RTCPeerConnection private readonly metrics?: CounterGroup private readonly dataChannelOptions?: DataChannelOptions + private readonly earlyDataChannels: RTCDataChannel[] constructor (init: DataChannelMuxerFactoryInit) { + this.onEarlyDataChannel = this.onEarlyDataChannel.bind(this) + this.peerConnection = init.peerConnection this.metrics = init.metrics this.protocol = init.protocol ?? MUXER_PROTOCOL this.dataChannelOptions = init.dataChannelOptions ?? {} + this.peerConnection.addEventListener('datachannel', this.onEarlyDataChannel) + this.earlyDataChannels = [] + } + + private onEarlyDataChannel (evt: RTCDataChannelEvent): void { + this.earlyDataChannels.push(evt.channel) } createStreamMuxer (maConn: MultiaddrConnection): StreamMuxer { + this.peerConnection.removeEventListener('datachannel', this.onEarlyDataChannel) + return new DataChannelMuxer(maConn, { peerConnection: this.peerConnection, dataChannelOptions: this.dataChannelOptions, metrics: this.metrics, - protocol: this.protocol + protocol: this.protocol, + earlyDataChannels: this.earlyDataChannels }) } } export interface DataChannelMuxerInit extends DataChannelMuxerFactoryInit { protocol: string + + /** + * Incoming data channels that were opened by the remote before the peer + * connection was established + */ + earlyDataChannels: RTCDataChannel[] } export interface DataChannelMuxerComponents { @@ -89,27 +107,44 @@ export class DataChannelMuxer extends AbstractStreamMuxer implemen * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event} */ this.peerConnection.ondatachannel = ({ channel }) => { - this.log.trace('incoming %s datachannel with channel id %d, protocol %s and status %s', channel.protocol, channel.id, channel.protocol, channel.readyState) - - // 'init' channel is only used during connection establishment, it is - // closed by the initiator - if (channel.label === 'init') { - this.log.trace('closing init channel %d', channel.id) - channel.close() + this.onDataChannel(channel) + } + queueMicrotask(() => { + if (this.status !== 'open') { + init.earlyDataChannels.forEach(channel => { + channel.close() + }) return } - const stream = createStream({ - ...this.streamOptions, - ...this.dataChannelOptions, - channel, - direction: 'inbound', - log: this.log + init.earlyDataChannels.forEach(channel => { + this.onDataChannel(channel) }) + }) + } + + private onDataChannel (channel: RTCDataChannel): void { + this.log('incoming datachannel with channel id %d, protocol %s and status %s', channel.id, channel.protocol, channel.readyState) + + // 'init' channel is only used during connection establishment, it is + // closed by the initiator + if (channel.label === 'init') { + this.log.trace('closing init channel %d', channel.id) + channel.close() - this.onRemoteStream(stream) + return } + + const stream = createStream({ + ...this.streamOptions, + ...this.dataChannelOptions, + channel, + direction: 'inbound', + log: this.log + }) + + this.onRemoteStream(stream) } async onCreateStream (options?: CreateStreamOptions): Promise { diff --git a/packages/transport-webrtc/src/private-to-private/initiate-connection.ts b/packages/transport-webrtc/src/private-to-private/initiate-connection.ts index bcb3ca2f70..323a860946 100644 --- a/packages/transport-webrtc/src/private-to-private/initiate-connection.ts +++ b/packages/transport-webrtc/src/private-to-private/initiate-connection.ts @@ -32,7 +32,7 @@ export interface ConnectOptions extends LoggerOptions, ProgressOptions { +export async function initiateConnection ({ rtcConfiguration, dataChannel, signal, metrics, multiaddr: ma, connectionManager, transportManager, log, logger, onProgress }: ConnectOptions): Promise<{ remoteAddress: Multiaddr, peerConnection: globalThis.RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> { const { circuitAddress, targetPeer } = splitAddr(ma) metrics?.dialerEvents.increment({ open: true }) @@ -209,6 +209,7 @@ export async function initiateConnection ({ rtcConfiguration, dataChannel, signa return { remoteAddress: ma, + // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370 peerConnection, muxerFactory } diff --git a/packages/transport-webrtc/src/private-to-private/transport.ts b/packages/transport-webrtc/src/private-to-private/transport.ts index d58b9a6a2f..ee0cc470f0 100644 --- a/packages/transport-webrtc/src/private-to-private/transport.ts +++ b/packages/transport-webrtc/src/private-to-private/transport.ts @@ -229,6 +229,7 @@ export class WebRTCTransport implements Transport, Startable { }) const webRTCConn = toMultiaddrConnection({ + // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370 peerConnection, remoteAddr: remoteAddress, metrics: this.metrics?.listenerEvents, @@ -245,6 +246,7 @@ export class WebRTCTransport implements Transport, Startable { }) // close the connection on shut down + // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370 this._closeOnShutdown(peerConnection, webRTCConn) } catch (err: any) { this.log.error('incoming signaling error', err) @@ -255,7 +257,7 @@ export class WebRTCTransport implements Transport, Startable { } } - private _closeOnShutdown (pc: RTCPeerConnection, webRTCConn: MultiaddrConnection): void { + private _closeOnShutdown (pc: globalThis.RTCPeerConnection, webRTCConn: MultiaddrConnection): void { // close the connection on shut down const shutDownListener = (): void => { webRTCConn.close() diff --git a/packages/transport-webrtc/src/private-to-public/listener.ts b/packages/transport-webrtc/src/private-to-public/listener.ts index 7683f27b25..1111716014 100644 --- a/packages/transport-webrtc/src/private-to-public/listener.ts +++ b/packages/transport-webrtc/src/private-to-public/listener.ts @@ -27,6 +27,7 @@ export interface WebRTCDirectListenerComponents { keychain?: Keychain datastore: Datastore metrics?: Metrics + events?: CounterGroup } export interface WebRTCDirectListenerInit { @@ -184,7 +185,13 @@ export class WebRTCDirectListener extends TypedEventEmitter impl signal.throwIfAborted() // https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md#browser-to-public-server - peerConnection = await createDialerRTCPeerConnection('server', ufrag, this.init.rtcConfiguration, this.certificate) + const results = await createDialerRTCPeerConnection('server', ufrag, { + rtcConfiguration: this.init.rtcConfiguration, + certificate: this.certificate, + events: this.metrics?.listenerEvents, + dataChannel: this.init.dataChannel + }) + peerConnection = results.peerConnection this.connections.set(key, peerConnection) @@ -201,11 +208,10 @@ export class WebRTCDirectListener extends TypedEventEmitter impl }) try { - await connect(peerConnection, ufrag, { + await connect(peerConnection, results.muxerFactory, ufrag, { role: 'server', log: this.log, logger: this.components.logger, - metrics: this.components.metrics, events: this.metrics?.listenerEvents, signal, remoteAddr: multiaddr(`/ip${isIPv4(remoteHost) ? 4 : 6}/${remoteHost}/udp/${remotePort}/webrtc-direct`), diff --git a/packages/transport-webrtc/src/private-to-public/transport.ts b/packages/transport-webrtc/src/private-to-public/transport.ts index ae8c54f5a9..d63c8030a7 100644 --- a/packages/transport-webrtc/src/private-to-public/transport.ts +++ b/packages/transport-webrtc/src/private-to-public/transport.ts @@ -163,14 +163,19 @@ export class WebRTCDirectTransport implements Transport, Startable { const ufrag = genUfrag() // https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md#browser-to-public-server - const peerConnection = await createDialerRTCPeerConnection('client', ufrag, typeof this.init.rtcConfiguration === 'function' ? await this.init.rtcConfiguration() : this.init.rtcConfiguration ?? {}) + const { + peerConnection, + muxerFactory + } = await createDialerRTCPeerConnection('client', ufrag, { + rtcConfiguration: typeof this.init.rtcConfiguration === 'function' ? await this.init.rtcConfiguration() : this.init.rtcConfiguration ?? {}, + dataChannel: this.init.dataChannel + }) try { - return await connect(peerConnection, ufrag, { + return await connect(peerConnection, muxerFactory, ufrag, { role: 'client', log: this.log, logger: this.components.logger, - metrics: this.components.metrics, events: this.metrics?.dialerEvents, signal: options.signal, remoteAddr: ma, diff --git a/packages/transport-webrtc/src/private-to-public/utils/connect.ts b/packages/transport-webrtc/src/private-to-public/utils/connect.ts index 137e1dc37f..c76b54995d 100644 --- a/packages/transport-webrtc/src/private-to-public/utils/connect.ts +++ b/packages/transport-webrtc/src/private-to-public/utils/connect.ts @@ -9,13 +9,12 @@ import { generateNoisePrologue } from './generate-noise-prologue.js' import * as sdp from './sdp.js' import type { DirectRTCPeerConnection } from './get-rtcpeerconnection.js' import type { DataChannelOptions } from '../../index.js' -import type { ComponentLogger, Connection, CounterGroup, Logger, Metrics, PeerId, PrivateKey, Upgrader } from '@libp2p/interface' +import type { ComponentLogger, Connection, CounterGroup, Logger, PeerId, PrivateKey, Upgrader } from '@libp2p/interface' import type { Multiaddr } from '@multiformats/multiaddr' export interface ConnectOptions { log: Logger logger: ComponentLogger - metrics?: Metrics events?: CounterGroup remoteAddr: Multiaddr role: 'client' | 'server' @@ -37,9 +36,13 @@ export interface ServerOptions extends ConnectOptions { const CONNECTION_STATE_CHANGE_EVENT = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange' -export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: string, options: ClientOptions): Promise -export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: string, options: ServerOptions): Promise -export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: string, options: ConnectOptions): Promise { +function isServer (options: ClientOptions | ServerOptions, peerConnection: any): peerConnection is DirectRTCPeerConnection { + return options.role === 'server' +} + +export async function connect (peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ClientOptions): Promise +export async function connect (peerConnection: DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ServerOptions): Promise +export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ClientOptions | ServerOptions): Promise { // create data channel for running the noise handshake. Once the data // channel is opened, the listener will initiate the noise handshake. This // is used to confirm the identity of the peer. @@ -88,7 +91,7 @@ export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: s options.log.trace('%s handshake channel opened', options.role) - if (options.role === 'server') { + if (isServer(options, peerConnection)) { // now that the connection has been opened, add the remote's certhash to // it's multiaddr so we can complete the noise handshake const remoteFingerprint = peerConnection.remoteFingerprint()?.value ?? '' @@ -124,6 +127,7 @@ export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: s // Creating the connection before completion of the noise // handshake ensures that the stream opening callback is set up const maConn = toMultiaddrConnection({ + // @ts-expect-error types are broken peerConnection, remoteAddr: options.remoteAddr, metrics: options.events, @@ -149,13 +153,6 @@ export async function connect (peerConnection: DirectRTCPeerConnection, ufrag: s // Track opened peer connection options.events?.increment({ peer_connection: true }) - const muxerFactory = new DataChannelMuxerFactory({ - // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370 - peerConnection, - metrics: options.events, - dataChannelOptions: options.dataChannel - }) - if (options.role === 'client') { // For outbound connections, the remote is expected to start the noise // handshake. Therefore, we need to secure an inbound noise connection diff --git a/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.browser.ts b/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.browser.ts index cf3c8cac73..f3891e2f4c 100644 --- a/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.browser.ts +++ b/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.browser.ts @@ -1,4 +1,10 @@ -export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise), certificate?: RTCCertificate): Promise { +import { DataChannelMuxerFactory } from '../../muxer.ts' +import type { CreateDialerRTCPeerConnectionOptions } from './get-rtcpeerconnection.ts' + +export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, options: CreateDialerRTCPeerConnectionOptions = {}): Promise<{ peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> { + // @ts-expect-error options type is wrong + let certificate: RTCCertificate = options.certificate + if (certificate == null) { // ECDSA is preferred over RSA here. From our testing we find that P-256 elliptic // curve is supported by Pion, webrtc-rs, as well as Chromium (P-228 and P-384 @@ -13,10 +19,21 @@ export async function createDialerRTCPeerConnection (role: 'client' | 'server', }) } - const rtcConfig = typeof rtcConfiguration === 'function' ? await rtcConfiguration() : rtcConfiguration + const rtcConfig = typeof options.rtcConfiguration === 'function' ? await options.rtcConfiguration() : options.rtcConfiguration - return new RTCPeerConnection({ + const peerConnection = new RTCPeerConnection({ ...(rtcConfig ?? {}), certificates: [certificate] }) + + const muxerFactory = new DataChannelMuxerFactory({ + peerConnection, + metrics: options.events, + dataChannelOptions: options.dataChannel + }) + + return { + peerConnection, + muxerFactory + } } diff --git a/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.ts b/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.ts index a02c8c50e5..700d288e03 100644 --- a/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.ts +++ b/packages/transport-webrtc/src/private-to-public/utils/get-rtcpeerconnection.ts @@ -2,8 +2,10 @@ import { Crypto } from '@peculiar/webcrypto' import { PeerConnection } from 'node-datachannel' import { RTCPeerConnection } from 'node-datachannel/polyfill' import { DEFAULT_ICE_SERVERS, MAX_MESSAGE_SIZE } from '../../constants.js' +import { DataChannelMuxerFactory } from '../../muxer.ts' import { generateTransportCertificate } from './generate-certificates.js' -import type { TransportCertificate } from '../../index.js' +import type { DataChannelOptions, TransportCertificate } from '../../index.js' +import type { CounterGroup } from '@libp2p/interface' import type { CertificateFingerprint } from 'node-datachannel' const crypto = new Crypto() @@ -85,8 +87,17 @@ function mapIceServers (iceServers?: RTCIceServer[]): string[] { .flat() ?? [] } -export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise), certificate?: TransportCertificate): Promise { - if (certificate == null) { +export interface CreateDialerRTCPeerConnectionOptions { + rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise) + certificate?: TransportCertificate + events?: CounterGroup + dataChannel?: DataChannelOptions +} + +export async function createDialerRTCPeerConnection (role: 'client', ufrag: string, options?: CreateDialerRTCPeerConnectionOptions): Promise<{ peerConnection: globalThis.RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> +export async function createDialerRTCPeerConnection (role: 'server', ufrag: string, options?: CreateDialerRTCPeerConnectionOptions): Promise<{ peerConnection: DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> +export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, options: CreateDialerRTCPeerConnectionOptions = {}): Promise<{ peerConnection: globalThis.RTCPeerConnection | DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> { + if (options.certificate == null) { // ECDSA is preferred over RSA here. From our testing we find that P-256 // elliptic curve is supported by Pion, webrtc-rs, as well as Chromium // (P-228 and P-384 was not supported in Chromium). We use the same hash @@ -96,24 +107,36 @@ export async function createDialerRTCPeerConnection (role: 'client' | 'server', namedCurve: 'P-256' }, true, ['sign', 'verify']) - certificate = await generateTransportCertificate(keyPair, { + options.certificate = await generateTransportCertificate(keyPair, { days: 365 }) } - const rtcConfig = typeof rtcConfiguration === 'function' ? await rtcConfiguration() : rtcConfiguration + const rtcConfig = typeof options.rtcConfiguration === 'function' ? await options.rtcConfiguration() : options.rtcConfiguration - return new DirectRTCPeerConnection({ + const peerConnection = new DirectRTCPeerConnection({ ...rtcConfig, ufrag, peerConnection: new PeerConnection(`${role}-${Date.now()}`, { disableFingerprintVerification: true, disableAutoNegotiation: true, - certificatePemFile: certificate.pem, - keyPemFile: certificate.privateKey, + certificatePemFile: options.certificate.pem, + keyPemFile: options.certificate.privateKey, enableIceUdpMux: role === 'server', maxMessageSize: MAX_MESSAGE_SIZE, iceServers: mapIceServers(rtcConfig?.iceServers ?? DEFAULT_ICE_SERVERS.map(urls => ({ urls }))) }) }) + + const muxerFactory = new DataChannelMuxerFactory({ + // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370 + peerConnection, + metrics: options.events, + dataChannelOptions: options.dataChannel + }) + + return { + peerConnection, + muxerFactory + } } diff --git a/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts b/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts index 43170ec1f6..07c7765093 100644 --- a/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts +++ b/packages/transport-webrtc/src/rtcpeerconnection-to-conn.ts @@ -1,5 +1,4 @@ import { AbstractMultiaddrConnection } from '@libp2p/utils' -import type { RTCPeerConnection } from './webrtc/index.js' import type { AbortOptions, MultiaddrConnection } from '@libp2p/interface' import type { AbstractMultiaddrConnectionInit, SendResult } from '@libp2p/utils' import type { Uint8ArrayList } from 'uint8arraylist' diff --git a/packages/transport-webrtc/src/stream.ts b/packages/transport-webrtc/src/stream.ts index f55881a6eb..4eb6f650e8 100644 --- a/packages/transport-webrtc/src/stream.ts +++ b/packages/transport-webrtc/src/stream.ts @@ -88,23 +88,6 @@ export class WebRTCStream extends AbstractStream { } } - if (this.channel.readyState !== 'open') { - this.log('channel ready state is "%s" and not "open", waiting for "open" event before sending data', this.channel.readyState) - pEvent(this.channel, 'open', { - rejectionEvents: [ - 'close', - 'error' - ] - }) - .then(() => { - this.log('channel ready state is now "%s", dispatching drain', this.channel.readyState) - this.safeDispatchEvent('drain') - }) - .catch(err => { - this.abort(err.error ?? err) - }) - } - // pipe framed protobuf messages through a length prefixed decoder, and // surface data from the `Message.message` field through a source. Promise.resolve().then(async () => { @@ -124,6 +107,26 @@ export class WebRTCStream extends AbstractStream { } } this.addEventListener('close', cleanUpDatachannelOnClose) + + // chrome can receive message events before the open even is fired - calling + // code needs to attach message event listeners before these events occur + // but we need to wait before sending any data so this has to be done async + if (this.channel.readyState !== 'open') { + this.log('channel ready state is "%s" and not "open", waiting for "open" event before sending data', this.channel.readyState) + pEvent(this.channel, 'open', { + rejectionEvents: [ + 'close', + 'error' + ] + }) + .then(() => { + this.log('channel ready state is now "%s", dispatching drain', this.channel.readyState) + this.safeDispatchEvent('drain') + }) + .catch(err => { + this.abort(err.error ?? err) + }) + } } sendNewStream (): void { @@ -204,8 +207,13 @@ export class WebRTCStream extends AbstractStream { options?.signal?.throwIfAborted() this.receivedFinAck = Promise.withResolvers() + // wait for either: + // 1. the FIN_ACK to be received + // 2. the datachannel to close + // 3. timeout await Promise.any([ raceSignal(this.receivedFinAck.promise, options?.signal), + pEvent(this.channel, 'close'), new Promise(resolve => { AbortSignal.timeout(this.finAckTimeout) .addEventListener('abort', () => { diff --git a/packages/transport-webrtc/test/maconn.spec.ts b/packages/transport-webrtc/test/maconn.spec.ts index 81c3f8a970..6905f5ca47 100644 --- a/packages/transport-webrtc/test/maconn.spec.ts +++ b/packages/transport-webrtc/test/maconn.spec.ts @@ -18,6 +18,7 @@ describe('Multiaddr Connection', () => { reset: () => {} }) const maConn = toMultiaddrConnection({ + // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370 peerConnection, remoteAddr, metrics,