@dxos/network-manager 0.6.12-staging.e11e696 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-YOKKEU6T.mjs → chunk-XYSYUN63.mjs} +998 -1192
- package/dist/lib/browser/chunk-XYSYUN63.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +19 -10
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +27 -18
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-7ZWQLO5T.cjs → chunk-4YAYC7WN.cjs} +1166 -1233
- package/dist/lib/node/chunk-4YAYC7WN.cjs.map +7 -0
- package/dist/lib/node/index.cjs +37 -27
- package/dist/lib/node/index.cjs.map +2 -2
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +29 -20
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/signal/integration.test.d.ts +2 -0
- package/dist/types/src/signal/integration.test.d.ts.map +1 -0
- package/dist/types/src/signal/swarm-messenger.test.d.ts +2 -0
- package/dist/types/src/signal/swarm-messenger.test.d.ts.map +1 -0
- package/dist/types/src/swarm/connection.d.ts.map +1 -1
- package/dist/types/src/swarm/swarm.d.ts +1 -1
- package/dist/types/src/testing/test-builder.d.ts +2 -2
- package/dist/types/src/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/tests/basic-test-suite.d.ts.map +1 -1
- package/dist/types/src/tests/property-test-suite.d.ts.map +1 -1
- package/dist/types/src/tests/tcp-transport.test.d.ts +2 -0
- package/dist/types/src/tests/tcp-transport.test.d.ts.map +1 -0
- package/dist/types/src/tests/utils.d.ts.map +1 -1
- package/dist/types/src/transport/index.d.ts +5 -1
- package/dist/types/src/transport/index.d.ts.map +1 -1
- package/dist/types/src/transport/libdatachannel-transport.d.ts +42 -0
- package/dist/types/src/transport/libdatachannel-transport.d.ts.map +1 -0
- package/dist/types/src/transport/libdatachannel-transport.test.d.ts +2 -0
- package/dist/types/src/transport/libdatachannel-transport.test.d.ts.map +1 -0
- package/dist/types/src/transport/memory-transport.d.ts +2 -2
- package/dist/types/src/transport/memory-transport.d.ts.map +1 -1
- package/dist/types/src/transport/memory-transport.test.d.ts +2 -0
- package/dist/types/src/transport/memory-transport.test.d.ts.map +1 -0
- package/dist/types/src/transport/simplepeer-simple-peer.d.ts +2 -0
- package/dist/types/src/transport/simplepeer-simple-peer.d.ts.map +1 -0
- package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts +2 -0
- package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts.map +1 -0
- package/dist/types/src/transport/{webrtc/rtc-transport-proxy.d.ts → simplepeer-transport-proxy.d.ts} +12 -10
- package/dist/types/src/transport/simplepeer-transport-proxy.d.ts.map +1 -0
- package/dist/types/src/transport/{webrtc/rtc-transport-service.d.ts → simplepeer-transport-service.d.ts} +7 -9
- package/dist/types/src/transport/simplepeer-transport-service.d.ts.map +1 -0
- package/dist/types/src/transport/simplepeer-transport.d.ts +36 -0
- package/dist/types/src/transport/simplepeer-transport.d.ts.map +1 -0
- package/dist/types/src/transport/simplepeer-transport.test.d.ts +2 -0
- package/dist/types/src/transport/simplepeer-transport.test.d.ts.map +1 -0
- package/dist/types/src/transport/{tcp/tcp-transport.browser.d.ts → tcp-transport.browser.d.ts} +3 -3
- package/dist/types/src/transport/tcp-transport.browser.d.ts.map +1 -0
- package/dist/types/src/transport/{tcp/tcp-transport.d.ts → tcp-transport.d.ts} +3 -3
- package/dist/types/src/transport/tcp-transport.d.ts.map +1 -0
- package/dist/types/src/transport/transport.d.ts +6 -7
- package/dist/types/src/transport/transport.d.ts.map +1 -1
- package/dist/types/src/transport/webrtc.d.ts +6 -0
- package/dist/types/src/transport/webrtc.d.ts.map +1 -0
- package/package.json +30 -53
- package/src/globals.d.ts +7 -0
- package/src/signal/ice.test.ts +3 -1
- package/src/signal/{integration.node.test.ts → integration.test.ts} +15 -9
- package/src/signal/{swarm-messenger.node.test.ts → swarm-messenger.test.ts} +23 -13
- package/src/swarm/connection-limiter.test.ts +6 -3
- package/src/swarm/connection.test.ts +38 -63
- package/src/swarm/connection.ts +5 -5
- package/src/swarm/swarm.test.ts +11 -9
- package/src/swarm/swarm.ts +1 -1
- package/src/testing/test-builder.ts +28 -12
- package/src/tests/basic-test-suite.ts +33 -34
- package/src/tests/memory-transport.test.ts +42 -40
- package/src/tests/property-test-suite.ts +22 -21
- package/src/tests/tcp-transport.test.ts +67 -0
- package/src/tests/utils.ts +2 -3
- package/src/tests/webrtc-transport.test.ts +9 -9
- package/src/transport/index.ts +5 -1
- package/src/transport/libdatachannel-transport.test.ts +100 -0
- package/src/transport/libdatachannel-transport.ts +376 -0
- package/src/transport/memory-transport.test.ts +74 -0
- package/src/transport/memory-transport.ts +0 -2
- package/src/transport/simplepeer-simple-peer.ts +26 -0
- package/src/transport/simplepeer-transport-proxy-test.ts +181 -0
- package/src/transport/simplepeer-transport-proxy.ts +246 -0
- package/src/transport/simplepeer-transport-service.ts +160 -0
- package/src/transport/simplepeer-transport.test.ts +61 -0
- package/src/transport/simplepeer-transport.ts +250 -0
- package/src/transport/{tcp/tcp-transport.browser.ts → tcp-transport.browser.ts} +3 -7
- package/src/transport/{tcp/tcp-transport.ts → tcp-transport.ts} +1 -3
- package/src/transport/transport.ts +7 -8
- package/src/transport/webrtc.ts +15 -0
- package/src/typings.d.ts +2 -8
- package/dist/lib/browser/chunk-GW3YM55A.mjs +0 -14
- package/dist/lib/browser/chunk-GW3YM55A.mjs.map +0 -7
- package/dist/lib/browser/chunk-YOKKEU6T.mjs.map +0 -7
- package/dist/lib/browser/transport/tcp/index.mjs +0 -39
- package/dist/lib/browser/transport/tcp/index.mjs.map +0 -7
- package/dist/lib/node/chunk-7ZWQLO5T.cjs.map +0 -7
- package/dist/lib/node/transport/tcp/index.cjs +0 -191
- package/dist/lib/node/transport/tcp/index.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-4VO725JT.mjs +0 -4383
- package/dist/lib/node-esm/chunk-4VO725JT.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -50
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -280
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/transport/tcp/index.mjs +0 -159
- package/dist/lib/node-esm/transport/tcp/index.mjs.map +0 -7
- package/dist/types/src/signal/integration.node.test.d.ts +0 -2
- package/dist/types/src/signal/integration.node.test.d.ts.map +0 -1
- package/dist/types/src/signal/swarm-messenger.node.test.d.ts +0 -2
- package/dist/types/src/signal/swarm-messenger.node.test.d.ts.map +0 -1
- package/dist/types/src/tests/tcp-transport.node.test.d.ts +0 -2
- package/dist/types/src/tests/tcp-transport.node.test.d.ts.map +0 -1
- package/dist/types/src/transport/tcp/index.d.ts +0 -2
- package/dist/types/src/transport/tcp/index.d.ts.map +0 -1
- package/dist/types/src/transport/tcp/tcp-transport.browser.d.ts.map +0 -1
- package/dist/types/src/transport/tcp/tcp-transport.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/index.d.ts +0 -4
- package/dist/types/src/transport/webrtc/index.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts +0 -14
- package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts +0 -68
- package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts +0 -33
- package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts +0 -2
- package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts +0 -4
- package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport-proxy.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts +0 -2
- package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport-service.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts +0 -4
- package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts +0 -2
- package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/test-utils.d.ts +0 -5
- package/dist/types/src/transport/webrtc/test-utils.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc/utils.d.ts +0 -3
- package/dist/types/src/transport/webrtc/utils.d.ts.map +0 -1
- package/src/tests/tcp-transport.node.test.ts +0 -65
- package/src/transport/tcp/index.ts +0 -5
- package/src/transport/webrtc/index.ts +0 -7
- package/src/transport/webrtc/rtc-connection-factory.ts +0 -82
- package/src/transport/webrtc/rtc-peer-connection.ts +0 -472
- package/src/transport/webrtc/rtc-transport-channel.test.ts +0 -176
- package/src/transport/webrtc/rtc-transport-channel.ts +0 -195
- package/src/transport/webrtc/rtc-transport-factory.ts +0 -28
- package/src/transport/webrtc/rtc-transport-proxy.test.ts +0 -413
- package/src/transport/webrtc/rtc-transport-proxy.ts +0 -264
- package/src/transport/webrtc/rtc-transport-service.ts +0 -192
- package/src/transport/webrtc/rtc-transport-stats.ts +0 -67
- package/src/transport/webrtc/rtc-transport.test.ts +0 -198
- package/src/transport/webrtc/test-utils.ts +0 -22
- package/src/transport/webrtc/utils.ts +0 -36
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Duplex } from 'stream';
|
|
6
|
+
|
|
7
|
+
import { Event, Trigger, synchronized } from '@dxos/async';
|
|
8
|
+
import { ErrorStream } from '@dxos/debug';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
12
|
+
|
|
13
|
+
import { type Transport, type TransportFactory, type TransportOptions, type TransportStats } from './transport';
|
|
14
|
+
import { type IceProvider } from '../signal';
|
|
15
|
+
|
|
16
|
+
const DATACHANNEL_LABEL = 'dxos.mesh.transport';
|
|
17
|
+
const MAX_BUFFERED_AMOUNT = 64 * 1024;
|
|
18
|
+
|
|
19
|
+
// https://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size
|
|
20
|
+
const MAX_MESSAGE_SIZE = 64 * 1024;
|
|
21
|
+
|
|
22
|
+
export type LibDataChannelTransportOptions = TransportOptions & {
|
|
23
|
+
webrtcConfig?: RTCConfiguration;
|
|
24
|
+
iceProvider?: IceProvider;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const createLibDataChannelTransportFactory = (
|
|
28
|
+
webrtcConfig?: RTCConfiguration,
|
|
29
|
+
iceProvider?: IceProvider,
|
|
30
|
+
): TransportFactory => {
|
|
31
|
+
return {
|
|
32
|
+
createTransport: (options) =>
|
|
33
|
+
new LibDataChannelTransport({
|
|
34
|
+
...options,
|
|
35
|
+
webrtcConfig,
|
|
36
|
+
iceProvider,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Transport
|
|
43
|
+
*/
|
|
44
|
+
// TODO(burdon): Purpose (e.g., platform).
|
|
45
|
+
export class LibDataChannelTransport implements Transport {
|
|
46
|
+
private static _instanceCount = 0;
|
|
47
|
+
|
|
48
|
+
private _peer?: RTCPeerConnection;
|
|
49
|
+
private _channel!: RTCDataChannel;
|
|
50
|
+
private _stream!: Duplex;
|
|
51
|
+
|
|
52
|
+
private _closed = false;
|
|
53
|
+
private _connected = false;
|
|
54
|
+
|
|
55
|
+
private _writeCallback: (() => void) | null = null;
|
|
56
|
+
private readonly _readyForCandidates = new Trigger();
|
|
57
|
+
|
|
58
|
+
readonly closed = new Event();
|
|
59
|
+
readonly connected = new Event();
|
|
60
|
+
readonly errors = new ErrorStream();
|
|
61
|
+
|
|
62
|
+
constructor(private readonly _options: LibDataChannelTransportOptions) {}
|
|
63
|
+
|
|
64
|
+
get isOpen() {
|
|
65
|
+
return !!this._peer && !this._closed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async open() {
|
|
69
|
+
if (this._closed) {
|
|
70
|
+
// TODO(burdon): Make idempotent?
|
|
71
|
+
this.errors.raise(new Error('connection already closed'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// TODO(burdon): Move to factory?
|
|
75
|
+
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
|
76
|
+
const { RTCPeerConnection } = (await importESM('node-datachannel/polyfill'))
|
|
77
|
+
.default as typeof import('node-datachannel/polyfill');
|
|
78
|
+
|
|
79
|
+
const providedIceServers = await this._options.iceProvider?.getIceServers();
|
|
80
|
+
|
|
81
|
+
// workaround https://github.com/murat-dogan/node-datachannel/pull/207
|
|
82
|
+
if (!this._options.webrtcConfig) {
|
|
83
|
+
this._options.webrtcConfig = {};
|
|
84
|
+
}
|
|
85
|
+
this._options.webrtcConfig.iceServers = [
|
|
86
|
+
...(this._options.webrtcConfig.iceServers ?? []),
|
|
87
|
+
...(providedIceServers ?? []),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
this._peer = new RTCPeerConnection(this._options.webrtcConfig);
|
|
91
|
+
|
|
92
|
+
this._peer.onicecandidateerror = (event) => {
|
|
93
|
+
log.error('peer.onicecandidateerror', { event });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this._peer.onconnectionstatechange = (event) => {
|
|
97
|
+
log.debug('peer.onconnectionstatechange', {
|
|
98
|
+
event,
|
|
99
|
+
peerConnectionState: this._peer?.connectionState,
|
|
100
|
+
transportConnectionState: this._connected,
|
|
101
|
+
});
|
|
102
|
+
// TODO(nf): throw error if datachannel does not connect after some time?
|
|
103
|
+
// TODO(burdon): Restart ICE.
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
this._peer.onicecandidate = async (event) => {
|
|
107
|
+
log.debug('peer.onicecandidate', { event });
|
|
108
|
+
if (event.candidate) {
|
|
109
|
+
try {
|
|
110
|
+
await this._options.sendSignal({
|
|
111
|
+
payload: {
|
|
112
|
+
data: {
|
|
113
|
+
type: 'candidate',
|
|
114
|
+
candidate: {
|
|
115
|
+
candidate: event.candidate.candidate,
|
|
116
|
+
// These fields never seem to be not null, but connecting to Chrome doesn't work if they are.
|
|
117
|
+
sdpMLineIndex: event.candidate.sdpMLineIndex ?? 0,
|
|
118
|
+
sdpMid: event.candidate.sdpMid ?? 0,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
} catch (err) {
|
|
124
|
+
log.info('signaling error', { err });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (this._options.initiator) {
|
|
130
|
+
invariant(this._peer, 'not open');
|
|
131
|
+
// TODO(burdon): Deprecated negotiation pattern?
|
|
132
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
|
|
133
|
+
this._peer
|
|
134
|
+
.createOffer()
|
|
135
|
+
.then(async (offer) => {
|
|
136
|
+
if (this._closed) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this._peer?.connectionState !== 'connecting') {
|
|
141
|
+
log.error('peer not connecting', { peer: this._peer });
|
|
142
|
+
this.errors.raise(new Error('invalid state: peer is initiator, but other peer not in state connecting'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
log.debug('creating offer', { peer: this._peer, offer });
|
|
146
|
+
await this._peer!.setLocalDescription(offer);
|
|
147
|
+
await this._options.sendSignal({ payload: { data: { type: offer.type, sdp: offer.sdp } } });
|
|
148
|
+
})
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
this.errors.raise(err);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this._handleChannel(this._peer.createDataChannel(DATACHANNEL_LABEL));
|
|
154
|
+
|
|
155
|
+
log.debug('created data channel');
|
|
156
|
+
this._peer.ondatachannel = () => {
|
|
157
|
+
this.errors.raise(new Error('unexpected ondatachannel event for initiator'));
|
|
158
|
+
};
|
|
159
|
+
} else {
|
|
160
|
+
this._peer.ondatachannel = (event) => {
|
|
161
|
+
log.debug('peer.ondatachannel (non-initiator)', { event });
|
|
162
|
+
// TODO(nf): should the label contain some identifier?
|
|
163
|
+
if (event.channel.label !== DATACHANNEL_LABEL) {
|
|
164
|
+
this.errors.raise(new Error(`unexpected channel label ${event.channel.label}`));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._handleChannel(event.channel);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
LibDataChannelTransport._instanceCount++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async close() {
|
|
175
|
+
await this._close();
|
|
176
|
+
if (--LibDataChannelTransport._instanceCount === 0) {
|
|
177
|
+
(await importESM('node-datachannel')).cleanup();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@synchronized
|
|
182
|
+
private async _close() {
|
|
183
|
+
if (this._closed) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await this._disconnectStreams();
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
this._peer?.close();
|
|
190
|
+
} catch (err: any) {
|
|
191
|
+
this.errors.raise(err);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this._peer = undefined;
|
|
195
|
+
this._closed = true;
|
|
196
|
+
this.closed.emit();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handle data channel events.
|
|
201
|
+
*/
|
|
202
|
+
private _handleChannel(dataChannel: RTCDataChannel) {
|
|
203
|
+
this._channel = dataChannel;
|
|
204
|
+
|
|
205
|
+
this._channel.onopen = () => {
|
|
206
|
+
log.debug('channel.onopen');
|
|
207
|
+
const duplex = new Duplex({
|
|
208
|
+
read: () => {},
|
|
209
|
+
write: async (chunk, encoding, callback) => {
|
|
210
|
+
// TODO(nf): Wait to open.
|
|
211
|
+
if (chunk.length > MAX_MESSAGE_SIZE) {
|
|
212
|
+
this.errors.raise(new Error(`message too large: ${chunk.length} > ${MAX_MESSAGE_SIZE}`));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
dataChannel.send(chunk);
|
|
217
|
+
} catch (err: any) {
|
|
218
|
+
this.errors.raise(err);
|
|
219
|
+
await this._close();
|
|
220
|
+
}
|
|
221
|
+
if (this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
|
|
222
|
+
if (this._writeCallback !== null) {
|
|
223
|
+
log.error('consumer trying to write before we are ready for more data');
|
|
224
|
+
}
|
|
225
|
+
this._writeCallback = callback;
|
|
226
|
+
} else {
|
|
227
|
+
callback();
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
duplex.pipe(this._options.stream).pipe(duplex);
|
|
233
|
+
this._stream = duplex;
|
|
234
|
+
this._connected = true;
|
|
235
|
+
this.connected.emit();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
this._channel.onclose = async (err) => {
|
|
239
|
+
log.info('channel.onclose', { err });
|
|
240
|
+
await this._close();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
this._channel.onerror = async (err) => {
|
|
244
|
+
this.errors.raise(new Error('channel error: ' + err.toString()));
|
|
245
|
+
await this._close();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
this._channel.onbufferedamountlow = () => {
|
|
249
|
+
const cb = this._writeCallback;
|
|
250
|
+
this._writeCallback = null;
|
|
251
|
+
cb?.();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
this._channel.onmessage = (event) => {
|
|
255
|
+
let data = event.data;
|
|
256
|
+
if (data instanceof ArrayBuffer) {
|
|
257
|
+
data = Buffer.from(data);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this._stream.push(data);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async onSignal(signal: Signal) {
|
|
265
|
+
invariant(this._peer, 'not open');
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const data = signal.payload.data;
|
|
269
|
+
switch (data.type) {
|
|
270
|
+
case 'offer': {
|
|
271
|
+
if (this._peer.connectionState !== 'new') {
|
|
272
|
+
log.error('received offer but peer not in state new', { peer: this._peer });
|
|
273
|
+
this.errors.raise(new Error('invalid signalling state: received offer when peer is not in state new'));
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
await this._peer.setRemoteDescription({ type: data.type, sdp: data.sdp });
|
|
279
|
+
const answer = await this._peer.createAnswer();
|
|
280
|
+
await this._peer.setLocalDescription(answer);
|
|
281
|
+
await this._options.sendSignal({ payload: { data: { type: answer.type, sdp: answer.sdp } } });
|
|
282
|
+
this._readyForCandidates.wake();
|
|
283
|
+
} catch (err) {
|
|
284
|
+
log.error('cannot handle offer from signalling server', { err });
|
|
285
|
+
this.errors.raise(new Error('error handling offer'));
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'answer':
|
|
291
|
+
try {
|
|
292
|
+
await this._peer.setRemoteDescription({ type: data.type, sdp: data.sdp });
|
|
293
|
+
this._readyForCandidates.wake();
|
|
294
|
+
} catch (err) {
|
|
295
|
+
log.error('cannot handle answer from signalling server', { err });
|
|
296
|
+
this.errors.raise(new Error('error handling answer'));
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
|
|
300
|
+
case 'candidate':
|
|
301
|
+
await this._readyForCandidates.wait();
|
|
302
|
+
await this._peer.addIceCandidate({ candidate: data.candidate.candidate });
|
|
303
|
+
break;
|
|
304
|
+
|
|
305
|
+
default:
|
|
306
|
+
log.error('unhandled signal type', { type: data.type, signal });
|
|
307
|
+
this.errors.raise(new Error(`unhandled signal type ${data.type}`));
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
log.catch(err);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async getDetails(): Promise<string> {
|
|
315
|
+
const stats = await this._getStats();
|
|
316
|
+
const rc = stats?.remoteCandidate;
|
|
317
|
+
if (!rc) {
|
|
318
|
+
return 'unavailable';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (rc.candidateType === 'relay') {
|
|
322
|
+
return `${rc.ip}:${rc.port} relay for ${rc.relatedAddress}:${rc.relatedPort}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return `${rc.ip}:${rc.port} ${rc.candidateType}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async getStats(): Promise<TransportStats> {
|
|
329
|
+
const stats = await this._getStats();
|
|
330
|
+
if (!stats) {
|
|
331
|
+
return {
|
|
332
|
+
bytesSent: 0,
|
|
333
|
+
bytesReceived: 0,
|
|
334
|
+
packetsSent: 0,
|
|
335
|
+
packetsReceived: 0,
|
|
336
|
+
rawStats: {},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
bytesSent: stats.transport.bytesSent,
|
|
342
|
+
bytesReceived: stats.transport.bytesReceived,
|
|
343
|
+
packetsSent: 0,
|
|
344
|
+
packetsReceived: 0,
|
|
345
|
+
rawStats: stats.raw,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async _getStats(): Promise<any> {
|
|
350
|
+
invariant(this._peer, 'not open');
|
|
351
|
+
const stats = await this._peer.getStats();
|
|
352
|
+
const statsEntries = Array.from((stats as any).entries() as any[]);
|
|
353
|
+
const transport = statsEntries.filter((s) => s[1].type === 'transport')[0][1];
|
|
354
|
+
const candidatePair = statsEntries.filter((s: any) => s[0] === transport.selectedCandidatePairId);
|
|
355
|
+
let selectedCandidatePair: any;
|
|
356
|
+
let remoteCandidate: any;
|
|
357
|
+
if (candidatePair.length > 0) {
|
|
358
|
+
selectedCandidatePair = candidatePair[0][1];
|
|
359
|
+
remoteCandidate = statsEntries.filter((s: any) => s[0] === selectedCandidatePair.remoteCandidateId)[0][1];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
transport,
|
|
364
|
+
selectedCandidatePair,
|
|
365
|
+
remoteCandidate,
|
|
366
|
+
raw: Object.fromEntries(stats as any),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async _disconnectStreams() {
|
|
371
|
+
this._options.stream.unpipe?.(this._stream)?.unpipe?.(this._options.stream);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// eslint-disable-next-line no-new-func
|
|
376
|
+
const importESM = Function('path', 'return import(path)');
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { TestStream } from '@dxos/async';
|
|
6
|
+
import { PublicKey } from '@dxos/keys';
|
|
7
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
8
|
+
import { range } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
import { MemoryTransport } from './memory-transport';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Flaky test.
|
|
13
|
+
// Cannot log after tests are done. Did you forget to wait for something async in your test?
|
|
14
|
+
// Attempted to log "Ignoring unsupported ICE candidate.".
|
|
15
|
+
|
|
16
|
+
// TODO(burdon): Move to TestBuilder.
|
|
17
|
+
const createPair = async () => {
|
|
18
|
+
const topic = PublicKey.random();
|
|
19
|
+
const peer1Id = PublicKey.random();
|
|
20
|
+
const peer2Id = PublicKey.random();
|
|
21
|
+
|
|
22
|
+
const stream1 = new TestStream();
|
|
23
|
+
const connection1 = new MemoryTransport({
|
|
24
|
+
stream: stream1,
|
|
25
|
+
sendSignal: async (signal) => {
|
|
26
|
+
await connection2.onSignal(signal);
|
|
27
|
+
},
|
|
28
|
+
initiator: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterTest(() => connection1.close());
|
|
32
|
+
afterTest(() => connection1.errors.assertNoUnhandledErrors());
|
|
33
|
+
|
|
34
|
+
const stream2 = new TestStream();
|
|
35
|
+
const connection2 = new MemoryTransport({
|
|
36
|
+
stream: stream2,
|
|
37
|
+
sendSignal: async (signal) => {
|
|
38
|
+
await connection1.onSignal(signal);
|
|
39
|
+
},
|
|
40
|
+
initiator: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterTest(() => connection2.close());
|
|
44
|
+
afterTest(() => connection2.errors.assertNoUnhandledErrors());
|
|
45
|
+
|
|
46
|
+
await connection1.open();
|
|
47
|
+
await connection2.open();
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
connection1,
|
|
51
|
+
connection2,
|
|
52
|
+
stream1,
|
|
53
|
+
stream2,
|
|
54
|
+
peer1Id,
|
|
55
|
+
peer2Id,
|
|
56
|
+
topic,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
describe('MemoryTransport', () => {
|
|
61
|
+
test('establish connection and send data through with protocol', async () => {
|
|
62
|
+
const { stream1, stream2 } = await createPair();
|
|
63
|
+
await TestStream.assertConnectivity(stream1, stream2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('10 pairs of peers connecting at the same time', async () => {
|
|
67
|
+
await Promise.all(
|
|
68
|
+
range(10).map(async () => {
|
|
69
|
+
const { stream1, stream2 } = await createPair();
|
|
70
|
+
await TestStream.assertConnectivity(stream1, stream2);
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -123,7 +123,6 @@ export class MemoryTransport implements Transport {
|
|
|
123
123
|
this.errors.raise(err);
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
-
return this;
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
async close() {
|
|
@@ -156,7 +155,6 @@ export class MemoryTransport implements Transport {
|
|
|
156
155
|
|
|
157
156
|
this.closed.emit();
|
|
158
157
|
log('closed');
|
|
159
|
-
return this;
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
async onSignal({ payload }: Signal) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @dxos/test platform=nodejs
|
|
6
|
+
|
|
7
|
+
import wrtc from '@koush/wrtc';
|
|
8
|
+
import SimplePeerConstructor from 'simple-peer';
|
|
9
|
+
|
|
10
|
+
import { sleep } from '@dxos/async';
|
|
11
|
+
import { describe, test } from '@dxos/test';
|
|
12
|
+
|
|
13
|
+
describe('Node WebRTC and simple-peer', () => {
|
|
14
|
+
// Simplest test that reproduces SIGABRT (mac) and SIGSEGV (linux) in wrtc.
|
|
15
|
+
test
|
|
16
|
+
.skip('open and close', async () => {
|
|
17
|
+
const peer = new SimplePeerConstructor({
|
|
18
|
+
initiator: true,
|
|
19
|
+
wrtc,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await sleep(1);
|
|
23
|
+
await peer.destroy();
|
|
24
|
+
})
|
|
25
|
+
.timeout(3_000);
|
|
26
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @dxos/test platform=nodejs
|
|
6
|
+
|
|
7
|
+
import { TestStream } from '@dxos/async';
|
|
8
|
+
import { schema } from '@dxos/protocols/proto';
|
|
9
|
+
import { type BridgeService } from '@dxos/protocols/proto/dxos/mesh/bridge';
|
|
10
|
+
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
11
|
+
import { createLinkedPorts, createProtoRpcPeer, type ProtoRpcPeer } from '@dxos/rpc';
|
|
12
|
+
import { afterAll, afterTest, beforeAll, describe, test } from '@dxos/test';
|
|
13
|
+
|
|
14
|
+
import { SimplePeerTransportProxy } from './simplepeer-transport-proxy';
|
|
15
|
+
import { SimplePeerTransportService } from './simplepeer-transport-service';
|
|
16
|
+
|
|
17
|
+
describe('SimplePeerTransportProxy', () => {
|
|
18
|
+
const setupProxy = async ({
|
|
19
|
+
initiator = true,
|
|
20
|
+
stream = new TestStream(),
|
|
21
|
+
sendSignal = async () => {},
|
|
22
|
+
}: {
|
|
23
|
+
initiator?: boolean;
|
|
24
|
+
stream?: NodeJS.ReadWriteStream;
|
|
25
|
+
sendSignal?: (msg: Signal) => Promise<void>;
|
|
26
|
+
} = {}) => {
|
|
27
|
+
const [port1, port2] = createLinkedPorts();
|
|
28
|
+
|
|
29
|
+
// Starting BridgeService
|
|
30
|
+
const simplePeerTransportService: BridgeService = new SimplePeerTransportService();
|
|
31
|
+
|
|
32
|
+
// Starting BridgeService
|
|
33
|
+
const rpcService = createProtoRpcPeer({
|
|
34
|
+
requested: {},
|
|
35
|
+
exposed: {
|
|
36
|
+
BridgeService: schema.getService('dxos.mesh.bridge.BridgeService'),
|
|
37
|
+
},
|
|
38
|
+
handlers: { BridgeService: simplePeerTransportService },
|
|
39
|
+
port: port1,
|
|
40
|
+
noHandshake: true,
|
|
41
|
+
encodingOptions: {
|
|
42
|
+
preserveAny: true,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
await rpcService.open();
|
|
46
|
+
afterTest(() => rpcService.close());
|
|
47
|
+
|
|
48
|
+
// Starting RPC client
|
|
49
|
+
const rpcClient = createProtoRpcPeer({
|
|
50
|
+
requested: {
|
|
51
|
+
BridgeService: schema.getService('dxos.mesh.bridge.BridgeService'),
|
|
52
|
+
},
|
|
53
|
+
port: port2,
|
|
54
|
+
noHandshake: true,
|
|
55
|
+
encodingOptions: {
|
|
56
|
+
preserveAny: true,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
await rpcClient.open();
|
|
60
|
+
afterTest(() => rpcClient.close());
|
|
61
|
+
|
|
62
|
+
const simplePeerTransportProxy = new SimplePeerTransportProxy({
|
|
63
|
+
initiator,
|
|
64
|
+
stream,
|
|
65
|
+
sendSignal,
|
|
66
|
+
bridgeService: rpcClient.rpc.BridgeService,
|
|
67
|
+
});
|
|
68
|
+
await simplePeerTransportProxy.open();
|
|
69
|
+
afterTest(async () => await simplePeerTransportProxy.close());
|
|
70
|
+
|
|
71
|
+
return { simplePeerService: rpcService, SimplePeerTransportProxy: simplePeerTransportProxy };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// This doesn't clean up correctly and crashes with SIGSEGV / SIGABRT at the end. Probably an issue with wrtc package.
|
|
75
|
+
test('open and close', async () => {
|
|
76
|
+
const { SimplePeerTransportProxy: connection } = await setupProxy();
|
|
77
|
+
|
|
78
|
+
const wait = connection.closed.waitForCount(1);
|
|
79
|
+
await connection.close();
|
|
80
|
+
await wait;
|
|
81
|
+
}).timeout(1_000);
|
|
82
|
+
|
|
83
|
+
test('establish connection and send data through with protocol', async () => {
|
|
84
|
+
const stream1 = new TestStream();
|
|
85
|
+
const { SimplePeerTransportProxy: connection1 } = await setupProxy({
|
|
86
|
+
initiator: true,
|
|
87
|
+
stream: stream1,
|
|
88
|
+
sendSignal: async (signal) => {
|
|
89
|
+
await connection2.onSignal(signal);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
afterTest(() => connection1.errors.assertNoUnhandledErrors());
|
|
93
|
+
|
|
94
|
+
const stream2 = new TestStream();
|
|
95
|
+
const { SimplePeerTransportProxy: connection2 } = await setupProxy({
|
|
96
|
+
initiator: false,
|
|
97
|
+
stream: stream2,
|
|
98
|
+
sendSignal: async (signal) => {
|
|
99
|
+
await connection1.onSignal(signal);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
afterTest(() => connection2.errors.assertNoUnhandledErrors());
|
|
103
|
+
|
|
104
|
+
await TestStream.assertConnectivity(stream1, stream2);
|
|
105
|
+
}).timeout(2_000);
|
|
106
|
+
|
|
107
|
+
describe('Multiplexing', () => {
|
|
108
|
+
let service: any;
|
|
109
|
+
let rpcClient: ProtoRpcPeer<{ BridgeService: BridgeService }>;
|
|
110
|
+
|
|
111
|
+
beforeAll(async () => {
|
|
112
|
+
const [port1, port2] = createLinkedPorts();
|
|
113
|
+
|
|
114
|
+
const simplePeerTransportService: BridgeService = new SimplePeerTransportService();
|
|
115
|
+
service = createProtoRpcPeer({
|
|
116
|
+
exposed: {
|
|
117
|
+
BridgeService: schema.getService('dxos.mesh.bridge.BridgeService'),
|
|
118
|
+
},
|
|
119
|
+
handlers: { BridgeService: simplePeerTransportService },
|
|
120
|
+
port: port1,
|
|
121
|
+
noHandshake: true,
|
|
122
|
+
encodingOptions: {
|
|
123
|
+
preserveAny: true,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await service.open();
|
|
127
|
+
|
|
128
|
+
rpcClient = createProtoRpcPeer({
|
|
129
|
+
requested: {
|
|
130
|
+
BridgeService: schema.getService('dxos.mesh.bridge.BridgeService'),
|
|
131
|
+
},
|
|
132
|
+
port: port2,
|
|
133
|
+
noHandshake: true,
|
|
134
|
+
encodingOptions: {
|
|
135
|
+
preserveAny: true,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
await rpcClient.open();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterAll(async () => {
|
|
142
|
+
await service?.close();
|
|
143
|
+
await rpcClient?.close();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('establish connection and send data through with protocol', async () => {
|
|
147
|
+
const stream1 = new TestStream();
|
|
148
|
+
const proxy1 = new SimplePeerTransportProxy({
|
|
149
|
+
initiator: true,
|
|
150
|
+
stream: stream1,
|
|
151
|
+
sendSignal: async (signal) => {
|
|
152
|
+
await proxy2.onSignal(signal);
|
|
153
|
+
},
|
|
154
|
+
bridgeService: rpcClient.rpc.BridgeService,
|
|
155
|
+
});
|
|
156
|
+
afterTest(async () => {
|
|
157
|
+
proxy1.errors.assertNoUnhandledErrors();
|
|
158
|
+
await proxy1.close();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const stream2 = new TestStream();
|
|
162
|
+
const proxy2 = new SimplePeerTransportProxy({
|
|
163
|
+
initiator: false,
|
|
164
|
+
stream: stream2,
|
|
165
|
+
sendSignal: async (signal) => {
|
|
166
|
+
await proxy1.onSignal(signal);
|
|
167
|
+
},
|
|
168
|
+
bridgeService: rpcClient.rpc.BridgeService,
|
|
169
|
+
});
|
|
170
|
+
afterTest(async () => {
|
|
171
|
+
proxy2.errors.assertNoUnhandledErrors();
|
|
172
|
+
await proxy2.close();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await proxy1.open();
|
|
176
|
+
await proxy2.open();
|
|
177
|
+
|
|
178
|
+
await TestStream.assertConnectivity(stream1, stream2);
|
|
179
|
+
}).timeout(3_000);
|
|
180
|
+
});
|
|
181
|
+
});
|