smoldot 2.0.9 → 2.0.11

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.
@@ -68,7 +68,7 @@ export interface Connection {
68
68
  *
69
69
  * Must not be called after `closeSend` has been called.
70
70
  */
71
- send(data: Uint8Array, streamId?: number): void;
71
+ send(data: Array<Uint8Array>, streamId?: number): void;
72
72
  /**
73
73
  * Closes the writing side of the given stream of the given connection.
74
74
  *
@@ -140,7 +140,7 @@ export interface ConnectionConfig {
140
140
  *
141
141
  * This function must only be called for connections of type "multi-stream".
142
142
  */
143
- onStreamReset: (streamId: number) => void;
143
+ onStreamReset: (streamId: number, message: string) => void;
144
144
  /**
145
145
  * Callback called when some data sent using {@link Connection.send} has effectively been
146
146
  * written on the stream, meaning that some buffer space is now free.
@@ -153,10 +153,10 @@ function start(options, wasmModule, platformBindings) {
153
153
  throw new Error();
154
154
  state.instance.instance.streamWritableBytes(connectionId, numExtra, streamId);
155
155
  },
156
- onStreamReset(streamId) {
156
+ onStreamReset(streamId, message) {
157
157
  if (state.instance.status !== "ready")
158
158
  throw new Error();
159
- state.instance.instance.streamReset(connectionId, streamId);
159
+ state.instance.instance.streamReset(connectionId, streamId, message);
160
160
  },
161
161
  }));
162
162
  break;
@@ -69,7 +69,7 @@ export type Event = {
69
69
  ty: "stream-send";
70
70
  connectionId: number;
71
71
  streamId?: number;
72
- data: Uint8Array;
72
+ data: Array<Uint8Array>;
73
73
  } | {
74
74
  ty: "stream-send-close";
75
75
  connectionId: number;
@@ -111,7 +111,7 @@ export interface Instance {
111
111
  streamWritableBytes: (connectionId: number, numExtra: number, streamId?: number) => void;
112
112
  streamMessage: (connectionId: number, message: Uint8Array, streamId?: number) => void;
113
113
  streamOpened: (connectionId: number, streamId: number, direction: 'inbound' | 'outbound') => void;
114
- streamReset: (connectionId: number, streamId: number) => void;
114
+ streamReset: (connectionId: number, streamId: number, message: string) => void;
115
115
  }
116
116
  /**
117
117
  * Starts a new instance using the given configuration.
@@ -250,9 +250,15 @@ function startLocalInstance(config, wasmModule, eventCallback) {
250
250
  // that this function is called only when the connection is in an open state.
251
251
  stream_send: (connectionId, streamId, ptr, len) => {
252
252
  const instance = state.instance;
253
+ const mem = new Uint8Array(instance.exports.memory.buffer);
253
254
  ptr >>>= 0;
254
255
  len >>>= 0;
255
- const data = new Uint8Array(instance.exports.memory.buffer).slice(ptr, ptr + len);
256
+ const data = new Array();
257
+ for (let i = 0; i < len; ++i) {
258
+ const bufPtr = buffer.readUInt32LE(mem, ptr + 8 * i);
259
+ const bufLen = buffer.readUInt32LE(mem, ptr + 8 * i + 4);
260
+ data.push(mem.slice(bufPtr, bufPtr + bufLen));
261
+ }
256
262
  // TODO: docs says the streamId is provided only for multi-stream connections, but here it's always provided
257
263
  eventCallback({ ty: "stream-send", connectionId, streamId, data });
258
264
  },
@@ -438,10 +444,12 @@ function startLocalInstance(config, wasmModule, eventCallback) {
438
444
  return;
439
445
  state.instance.exports.connection_stream_opened(connectionId, streamId, direction === 'outbound' ? 1 : 0);
440
446
  },
441
- streamReset: (connectionId, streamId) => {
447
+ streamReset: (connectionId, streamId, message) => {
442
448
  if (!state.instance)
443
449
  return;
444
- state.instance.exports.stream_reset(connectionId, streamId);
450
+ state.bufferIndices[0] = new TextEncoder().encode(message);
451
+ state.instance.exports.stream_reset(connectionId, streamId, 0);
452
+ delete state.bufferIndices[0];
445
453
  },
446
454
  };
447
455
  });
@@ -196,9 +196,9 @@ function connectToInstanceServer(config) {
196
196
  const msg = { ty: "stream-writable-bytes", connectionId, numExtra, streamId };
197
197
  portToServer.postMessage(msg);
198
198
  },
199
- streamReset(connectionId, streamId) {
199
+ streamReset(connectionId, streamId, message) {
200
200
  state.connections.get(connectionId).delete(streamId);
201
- const msg = { ty: "stream-reset", connectionId, streamId };
201
+ const msg = { ty: "stream-reset", connectionId, streamId, message };
202
202
  portToServer.postMessage(msg);
203
203
  },
204
204
  };
@@ -333,7 +333,7 @@ function startInstanceServer(config, initPortToClient) {
333
333
  if (!state.connections.has(message.connectionId))
334
334
  return;
335
335
  // The stream might have been reset locally in the past.
336
- if (message.streamId && !state.connections.get(message.connectionId).has(message.streamId))
336
+ if (message.streamId !== undefined && !state.connections.get(message.connectionId).has(message.streamId))
337
337
  return;
338
338
  state.instance.streamMessage(message.connectionId, message.message, message.streamId);
339
339
  break;
@@ -351,7 +351,7 @@ function startInstanceServer(config, initPortToClient) {
351
351
  if (!state.connections.has(message.connectionId))
352
352
  return;
353
353
  // The stream might have been reset locally in the past.
354
- if (message.streamId && !state.connections.get(message.connectionId).has(message.streamId))
354
+ if (message.streamId !== undefined && !state.connections.get(message.connectionId).has(message.streamId))
355
355
  return;
356
356
  state.instance.streamWritableBytes(message.connectionId, message.numExtra, message.streamId);
357
357
  break;
@@ -361,10 +361,10 @@ function startInstanceServer(config, initPortToClient) {
361
361
  if (!state.connections.has(message.connectionId))
362
362
  return;
363
363
  // The stream might have been reset locally in the past.
364
- if (message.streamId && !state.connections.get(message.connectionId).has(message.streamId))
364
+ if (!state.connections.get(message.connectionId).has(message.streamId))
365
365
  return;
366
366
  state.connections.get(message.connectionId).delete(message.streamId);
367
- state.instance.streamReset(message.connectionId, message.streamId);
367
+ state.instance.streamReset(message.connectionId, message.streamId, message.message);
368
368
  break;
369
369
  }
370
370
  }
@@ -29,6 +29,19 @@ Object.defineProperty(exports, "QueueFullError", { enumerable: true, get: functi
29
29
  */
30
30
  function startWithBytecode(options) {
31
31
  options.forbidTcp = true;
32
+ // When in a secure context, browsers refuse to open non-secure WebSocket connections to
33
+ // non-localhost. There is an exception if the page is localhost, in which case all connections
34
+ // are allowed.
35
+ // Detecting this ahead of time is better for the overall health of the client, as it will
36
+ // avoid storing in memory addresses that it knows it can't connect to.
37
+ // The condition below is a hint, and false-positives or false-negatives are not fundamentally
38
+ // an issue.
39
+ if ((typeof isSecureContext === 'boolean' && isSecureContext) && typeof location !== undefined) {
40
+ const loc = location.toString();
41
+ if (loc.indexOf('localhost') !== -1 && loc.indexOf('127.0.0.1') !== -1 && loc.indexOf('::1') !== -1) {
42
+ options.forbidNonLocalWs = true;
43
+ }
44
+ }
32
45
  return (0, client_js_1.start)(options, options.bytecode, {
33
46
  performanceNow: () => {
34
47
  return performance.now();
@@ -142,12 +155,14 @@ function connect(config) {
142
155
  connection = null;
143
156
  },
144
157
  send: (data) => {
145
- connection.send(data);
146
158
  if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
147
159
  bufferedAmountCheck.nextTimeout = 10;
148
160
  setTimeout(checkBufferedAmount, 10);
149
161
  }
150
- bufferedAmountCheck.quenedUnreportedBytes += data.length;
162
+ for (const buffer of data) {
163
+ bufferedAmountCheck.quenedUnreportedBytes += buffer.length;
164
+ }
165
+ connection.send(new Blob(data));
151
166
  },
152
167
  closeSend: () => { throw new Error('Wrong connection type'); },
153
168
  openOutSubstream: () => { throw new Error('Wrong connection type'); }
@@ -155,56 +170,52 @@ function connect(config) {
155
170
  }
156
171
  else if (config.address.ty === "webrtc") {
157
172
  const { targetPort, ipVersion, targetIp, remoteTlsCertificateSha256 } = config.address;
158
- // TODO: detect localhost for Firefox? https://bugzilla.mozilla.org/show_bug.cgi?id=1659672
159
- // Note that `pc` can be the connection, but also null or undefined.
160
- // `undefined` means "certificate generation in progress", while `null` means "opening must
161
- // be cancelled".
162
- // While it would be better to use for example a string instead of `null`, using `null` lets
163
- // us use the `!` operator more easily and leads to more readable code.
164
- let pc = undefined;
165
- // Contains the data channels that are open and have been reported to smoldot.
166
- const dataChannels = new Map();
167
- // SHA256 hash of the DTLS certificate of the local node. Unknown as long as it hasn't been
168
- // generated.
169
- // TODO: could be merged with `pc` in one variable, and maybe even the other fields as well
170
- let localTlsCertificateSha256;
173
+ const state = {
174
+ pc: undefined,
175
+ dataChannels: new Map(),
176
+ nextStreamId: 0,
177
+ isFirstOutSubstream: true,
178
+ };
171
179
  // Kills all the JavaScript objects (the connection and all its substreams), ensuring that no
172
180
  // callback will be called again. Doesn't report anything to smoldot, as this should be done
173
181
  // by the caller.
174
182
  const killAllJs = () => {
175
183
  // The `RTCPeerConnection` is created pretty quickly. It is however still possible for
176
184
  // smoldot to cancel the opening, in which case `pc` will still be undefined.
177
- if (!pc) {
178
- console.assert(dataChannels.size === 0, "substreams exist while pc is undef");
179
- pc = null;
185
+ if (!state.pc) {
186
+ console.assert(state.dataChannels.size === 0, "substreams exist while pc is undef");
187
+ state.pc = null;
180
188
  return;
181
189
  }
182
- pc.onconnectionstatechange = null;
183
- pc.onnegotiationneeded = null;
184
- pc.ondatachannel = null;
185
- for (const channel of Array.from(dataChannels.values())) {
190
+ state.pc.onconnectionstatechange = null;
191
+ state.pc.onnegotiationneeded = null;
192
+ state.pc.ondatachannel = null;
193
+ for (const channel of Array.from(state.dataChannels.values())) {
186
194
  channel.channel.onopen = null;
187
195
  channel.channel.onerror = null;
188
196
  channel.channel.onclose = null;
189
197
  channel.channel.onbufferedamountlow = null;
190
198
  channel.channel.onmessage = null;
191
199
  }
192
- dataChannels.clear();
193
- pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
200
+ state.dataChannels.clear();
201
+ state.pc.close(); // Not necessarily necessary, but it doesn't hurt to do so.
194
202
  };
195
203
  // Function that configures a newly-opened channel and adds it to the map. Used for both
196
204
  // inbound and outbound substreams.
197
205
  const addChannel = (dataChannel, direction) => {
198
- const dataChannelId = dataChannel.id;
206
+ const streamId = state.nextStreamId;
207
+ state.nextStreamId += 1;
199
208
  dataChannel.binaryType = 'arraybuffer';
200
209
  let isOpen = { value: false };
201
210
  dataChannel.onopen = () => {
202
211
  console.assert(!isOpen.value, "substream opened twice");
203
212
  isOpen.value = true;
204
- config.onStreamOpened(dataChannelId, direction);
205
- config.onWritableBytes(65536, dataChannelId);
213
+ config.onStreamOpened(streamId, direction);
214
+ config.onWritableBytes(65536, streamId);
206
215
  };
207
- dataChannel.onerror = dataChannel.onclose = (_error) => {
216
+ dataChannel.onerror = dataChannel.onclose = (event) => {
217
+ // Note that Firefox doesn't support <https://developer.mozilla.org/en-US/docs/Web/API/RTCErrorEvent>.
218
+ const message = (event instanceof RTCErrorEvent) ? event.error.toString() : "RTCDataChannel closed";
208
219
  if (!isOpen.value) {
209
220
  // Substream wasn't opened yet and thus has failed to open. The API has no
210
221
  // mechanism to report substream openings failures. We could try opening it
@@ -212,24 +223,30 @@ function connect(config) {
212
223
  // entire connection.
213
224
  killAllJs();
214
225
  // Note that the event doesn't give any additional reason for the failure.
215
- config.onConnectionReset("data channel failed to open");
226
+ config.onConnectionReset("data channel failed to open: " + message);
216
227
  }
217
228
  else {
218
229
  // Substream was open and is now closed. Normal situation.
219
- config.onStreamReset(dataChannelId);
230
+ dataChannel.onopen = null;
231
+ dataChannel.onerror = null;
232
+ dataChannel.onclose = null;
233
+ dataChannel.onbufferedamountlow = null;
234
+ dataChannel.onmessage = null;
235
+ state.dataChannels.delete(streamId);
236
+ config.onStreamReset(streamId, message);
220
237
  }
221
238
  };
222
239
  dataChannel.onbufferedamountlow = () => {
223
- const channel = dataChannels.get(dataChannelId);
240
+ const channel = state.dataChannels.get(streamId);
224
241
  const val = channel.bufferedBytes;
225
242
  channel.bufferedBytes = 0;
226
- config.onWritableBytes(val, dataChannelId);
243
+ config.onWritableBytes(val, streamId);
227
244
  };
228
245
  dataChannel.onmessage = (m) => {
229
246
  // The `data` field is an `ArrayBuffer`.
230
- config.onMessage(new Uint8Array(m.data), dataChannelId);
247
+ config.onMessage(new Uint8Array(m.data), streamId);
231
248
  };
232
- dataChannels.set(dataChannelId, { channel: dataChannel, bufferedBytes: 0 });
249
+ state.dataChannels.set(streamId, { channel: dataChannel, bufferedBytes: 0 });
233
250
  };
234
251
  // It is possible for the browser to use multiple different certificates.
235
252
  // In order for our local certificate to be deterministic, we need to generate it manually and
@@ -237,10 +254,23 @@ function connect(config) {
237
254
  // According to <https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-generatecertificate>,
238
255
  // browsers are guaranteed to support `{ name: "ECDSA", namedCurve: "P-256" }`.
239
256
  RTCPeerConnection.generateCertificate({ name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" }).then((localCertificate) => __awaiter(this, void 0, void 0, function* () {
240
- if (pc === null)
257
+ if (state.pc === null)
241
258
  return;
259
+ // Due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1659672>, connections from
260
+ // Firefox to a localhost WebRTC server always fails. Since this bug has been opened
261
+ // for three years at the time of writing, it is unlikely to be fixed in the short
262
+ // term. In order to provider better user feedback, we straight up refuse connecting
263
+ // and stop the connection.
264
+ // Note that this is just a hint. Failing to detect this will lead to the WebRTC
265
+ // handshake timing out.
266
+ // TODO: eventually remove this if the Firefox bug is fixed
267
+ if ((targetIp == 'localhost' || targetIp == '127.0.0.1' || targetIp == '::1') && navigator.userAgent.indexOf('Firefox') !== -1) {
268
+ killAllJs();
269
+ config.onConnectionReset("Firefox can't connect to a localhost WebRTC server");
270
+ return;
271
+ }
242
272
  // Create a new WebRTC connection.
243
- pc = new RTCPeerConnection({ certificates: [localCertificate] });
273
+ state.pc = new RTCPeerConnection({ certificates: [localCertificate] });
244
274
  // We need to build the multihash corresponding to the local certificate.
245
275
  // While there exists a `RTCPeerConnection.getFingerprints` function, Firefox notably
246
276
  // doesn't support it.
@@ -263,7 +293,7 @@ function connect(config) {
263
293
  }
264
294
  }
265
295
  else {
266
- const localSdpOffer = yield pc.createOffer();
296
+ const localSdpOffer = yield state.pc.createOffer();
267
297
  // Note that this regex is not strict. The browser isn't a malicious actor, and the
268
298
  // objective of this regex is not to detect invalid input.
269
299
  const localSdpOfferFingerprintMatch = localSdpOffer.sdp.match(/a(\s*)=(\s*)fingerprint:(\s*)(sha|SHA)-256(\s*)(([a-fA-F0-9]{2}(:)*){32})/);
@@ -277,23 +307,23 @@ function connect(config) {
277
307
  config.onConnectionReset('Failed to obtain the browser certificate fingerprint');
278
308
  return;
279
309
  }
280
- localTlsCertificateSha256 = new Uint8Array(32);
310
+ let localTlsCertificateSha256 = new Uint8Array(32);
281
311
  localTlsCertificateSha256.set(localTlsCertificateHex.split(':').map((s) => parseInt(s, 16)), 0);
282
312
  // `onconnectionstatechange` is used to detect when the connection has closed or has failed
283
313
  // to open.
284
314
  // Note that smoldot will think that the connection is open even when it is still opening.
285
315
  // Therefore we don't care about events concerning the fact that the connection is now fully
286
316
  // open.
287
- pc.onconnectionstatechange = (_event) => {
288
- if (pc.connectionState == "closed" || pc.connectionState == "disconnected" || pc.connectionState == "failed") {
317
+ state.pc.onconnectionstatechange = (_event) => {
318
+ if (state.pc.connectionState == "closed" || state.pc.connectionState == "disconnected" || state.pc.connectionState == "failed") {
289
319
  killAllJs();
290
- config.onConnectionReset("WebRTC state transitioned to " + pc.connectionState);
320
+ config.onConnectionReset("WebRTC state transitioned to " + state.pc.connectionState);
291
321
  }
292
322
  };
293
- pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () {
323
+ state.pc.onnegotiationneeded = (_event) => __awaiter(this, void 0, void 0, function* () {
294
324
  var _a;
295
325
  // Create a new offer and set it as local description.
296
- let sdpOffer = (yield pc.createOffer()).sdp;
326
+ let sdpOffer = (yield state.pc.createOffer()).sdp;
297
327
  // We check that the locally-generated SDP offer has a data channel with the UDP
298
328
  // protocol. If that isn't the case, the connection will likely fail.
299
329
  if (sdpOffer.match(/^m=application(\s+)(\d+)(\s+)UDP\/DTLS\/SCTP(\s+)webrtc-datachannel$/m) === null) {
@@ -312,7 +342,7 @@ function connect(config) {
312
342
  const ufragPwd = "libp2p+webrtc+v1/" + browserGeneratedPwd;
313
343
  sdpOffer = sdpOffer.replace(/^a=ice-ufrag.*$/m, 'a=ice-ufrag:' + ufragPwd);
314
344
  sdpOffer = sdpOffer.replace(/^a=ice-pwd.*$/m, 'a=ice-pwd:' + ufragPwd);
315
- yield pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
345
+ yield state.pc.setLocalDescription({ type: 'offer', sdp: sdpOffer });
316
346
  // Transform certificate hash into fingerprint (upper-hex; each byte separated by ":").
317
347
  const fingerprint = Array.from(remoteTlsCertificateSha256).map((n) => ("0" + n.toString(16)).slice(-2).toUpperCase()).join(':');
318
348
  // Note that the trailing line feed is important, as otherwise Chrome
@@ -373,9 +403,9 @@ function connect(config) {
373
403
  // A transport address for a candidate that can be used for connectivity
374
404
  // checks (RFC8839).
375
405
  "a=candidate:1 1 UDP 1 " + targetIp + " " + String(targetPort) + " typ host" + "\n";
376
- yield pc.setRemoteDescription({ type: "answer", sdp: remoteSdp });
406
+ yield state.pc.setRemoteDescription({ type: "answer", sdp: remoteSdp });
377
407
  });
378
- pc.ondatachannel = ({ channel }) => {
408
+ state.pc.ondatachannel = ({ channel }) => {
379
409
  // TODO: is the substream maybe already open? according to the Internet it seems that no but it's unclear
380
410
  addChannel(channel, 'inbound');
381
411
  };
@@ -391,20 +421,22 @@ function connect(config) {
391
421
  killAllJs();
392
422
  }
393
423
  else {
394
- const channel = dataChannels.get(streamId);
424
+ const channel = state.dataChannels.get(streamId);
395
425
  channel.channel.onopen = null;
396
426
  channel.channel.onerror = null;
397
427
  channel.channel.onclose = null;
398
428
  channel.channel.onbufferedamountlow = null;
399
429
  channel.channel.onmessage = null;
400
430
  channel.channel.close();
401
- dataChannels.delete(streamId);
431
+ state.dataChannels.delete(streamId);
402
432
  }
403
433
  },
404
434
  send: (data, streamId) => {
405
- const channel = dataChannels.get(streamId);
406
- channel.channel.send(data);
407
- channel.bufferedBytes += data.length;
435
+ const channel = state.dataChannels.get(streamId);
436
+ for (const buffer of data) {
437
+ channel.bufferedBytes += buffer.length;
438
+ }
439
+ channel.channel.send(new Blob(data));
408
440
  },
409
441
  closeSend: () => { throw new Error('Wrong connection type'); },
410
442
  openOutSubstream: () => {
@@ -412,7 +444,10 @@ function connect(config) {
412
444
  // therefore `pc` is guaranteed to be non-null.
413
445
  // Note that the label passed to `createDataChannel` is required to be empty as
414
446
  // per the libp2p WebRTC specification.
415
- addChannel(pc.createDataChannel(""), 'outbound');
447
+ // TODO: adjusting the options based on the first substream is a bit hacky
448
+ const opts = state.isFirstOutSubstream ? { negotiated: true, id: 0 } : {};
449
+ state.isFirstOutSubstream = false;
450
+ addChannel(state.pc.createDataChannel("", opts), 'outbound');
416
451
  }
417
452
  };
418
453
  }
@@ -105,12 +105,14 @@ function connect(config) {
105
105
  // we thus absorb any exception thrown here.
106
106
  // See also <https://github.com/paritytech/smoldot/issues/2937>.
107
107
  try {
108
- socket.send(data);
109
108
  if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
110
109
  bufferedAmountCheck.nextTimeout = 10;
111
110
  setTimeout(checkBufferedAmount, 10);
112
111
  }
113
- bufferedAmountCheck.quenedUnreportedBytes += data.length;
112
+ for (const buffer of data) {
113
+ bufferedAmountCheck.quenedUnreportedBytes += buffer.length;
114
+ }
115
+ socket.send(new Blob(data));
114
116
  }
115
117
  catch (_error) { }
116
118
  },
@@ -172,31 +174,35 @@ function connect(config) {
172
174
  socket.inner.then((connec) => connec.close());
173
175
  },
174
176
  send: (data) => {
175
- let dataCopy = Uint8Array.from(data); // Deep copy of the data
177
+ let dataCopy = data.map((buf) => Uint8Array.from(buf)); // Deep copy of the data
176
178
  socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
177
- while (dataCopy.length > 0) {
178
- if (socket.destroyed || c === null)
179
- return c;
180
- let outcome;
181
- try {
182
- outcome = yield c.write(dataCopy);
183
- config.onWritableBytes(dataCopy.length);
184
- }
185
- catch (error) {
186
- // The type of `error` is unclear, but we assume that it implements `Error`
187
- outcome = error.toString();
188
- }
189
- if (typeof outcome !== 'number') {
190
- // The socket is reported closed, but `socket.destroyed` is still
191
- // `false` (see check above). As such, we must inform the inner layers.
192
- socket.destroyed = true;
193
- config.onConnectionReset(outcome);
194
- return c;
179
+ for (let buffer of dataCopy) {
180
+ while (buffer.length > 0) {
181
+ if (socket.destroyed || c === null)
182
+ return c;
183
+ let outcome;
184
+ try {
185
+ outcome = yield c.write(buffer);
186
+ config.onWritableBytes(buffer.length);
187
+ }
188
+ catch (error) {
189
+ // The type of `error` is unclear, but we assume that it
190
+ // implements `Error`
191
+ outcome = error.toString();
192
+ }
193
+ if (typeof outcome !== 'number') {
194
+ // The socket is reported closed, but `socket.destroyed` is still
195
+ // `false` (see check above). As such, we must inform the
196
+ // inner layers.
197
+ socket.destroyed = true;
198
+ config.onConnectionReset(outcome);
199
+ return c;
200
+ }
201
+ // Note that, contrary to `read`, it is possible for `outcome` to be 0.
202
+ // This happen if the write had to be interrupted, and the only thing
203
+ // we have to do is try writing again.
204
+ buffer = buffer.slice(outcome);
195
205
  }
196
- // Note that, contrary to `read`, it is possible for `outcome` to be 0.
197
- // This happen if the write had to be interrupted, and the only thing
198
- // we have to do is try writing again.
199
- dataCopy = dataCopy.slice(outcome);
200
206
  }
201
207
  return c;
202
208
  }));
@@ -106,12 +106,14 @@ function connect(config) {
106
106
  socket.close();
107
107
  },
108
108
  send: (data) => {
109
- socket.send(data);
110
109
  if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
111
110
  bufferedAmountCheck.nextTimeout = 10;
112
111
  setTimeout(checkBufferedAmount, 10);
113
112
  }
114
- bufferedAmountCheck.quenedUnreportedBytes += data.length;
113
+ for (const buffer of data) {
114
+ socket.send(buffer);
115
+ bufferedAmountCheck.quenedUnreportedBytes += buffer.length;
116
+ }
115
117
  },
116
118
  closeSend: () => { throw new Error('Wrong connection type'); },
117
119
  openOutSubstream: () => { throw new Error('Wrong connection type'); }
@@ -158,17 +160,19 @@ function connect(config) {
158
160
  socket.destroy();
159
161
  },
160
162
  send: (data) => {
161
- const dataLen = data.length;
162
- const allWritten = socket.write(data);
163
- if (allWritten) {
164
- setImmediate(() => {
165
- if (!socket.writable)
166
- return;
167
- config.onWritableBytes(dataLen);
168
- });
169
- }
170
- else {
171
- drainingBytes.num += dataLen;
163
+ for (const buffer of data) {
164
+ const bufferLen = buffer.length;
165
+ const allWritten = socket.write(buffer);
166
+ if (allWritten) {
167
+ setImmediate(() => {
168
+ if (!socket.writable)
169
+ return;
170
+ config.onWritableBytes(bufferLen);
171
+ });
172
+ }
173
+ else {
174
+ drainingBytes.num += bufferLen;
175
+ }
172
176
  }
173
177
  },
174
178
  closeSend: () => {