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