smoldot 0.7.13 → 1.0.1

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.
@@ -99,7 +99,6 @@ function trustedBase64Decode(base64) {
99
99
  * @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
100
100
  */
101
101
  function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
102
- let connection;
103
102
  // Attempt to parse the multiaddress.
104
103
  // TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
105
104
  const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
@@ -114,24 +113,72 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
114
113
  const url = (wsParsed[1] == 'ip6') ?
115
114
  (proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
116
115
  (proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
117
- connection = {
118
- ty: 'websocket',
119
- socket: new WebSocket(url)
116
+ const socket = new WebSocket(url);
117
+ socket.binaryType = 'arraybuffer';
118
+ const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
119
+ const checkBufferedAmount = () => {
120
+ if (socket.readyState != 1)
121
+ return;
122
+ // Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
123
+ // of all the data that has been sent, but that might not be the case. For this
124
+ // reason, we use `bufferedAmount` as a hint rather than a correct value.
125
+ const bufferedAmount = socket.bufferedAmount;
126
+ let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
127
+ if (wasSent < 0)
128
+ wasSent = 0;
129
+ bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
130
+ if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
131
+ setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
132
+ bufferedAmountCheck.nextTimeout *= 2;
133
+ if (bufferedAmountCheck.nextTimeout > 500)
134
+ bufferedAmountCheck.nextTimeout = 500;
135
+ }
136
+ // Note: it is important to call `onWritableBytes` at the very end, as it might
137
+ // trigger a call to `send`.
138
+ if (wasSent != 0)
139
+ config.onWritableBytes(wasSent);
120
140
  };
121
- connection.socket.binaryType = 'arraybuffer';
122
- connection.socket.onopen = () => {
123
- config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux' });
141
+ socket.onopen = () => {
142
+ config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false });
124
143
  };
125
- connection.socket.onclose = (event) => {
144
+ socket.onclose = (event) => {
126
145
  const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
127
146
  config.onConnectionReset(message);
128
147
  };
129
- connection.socket.onmessage = (msg) => {
148
+ socket.onmessage = (msg) => {
130
149
  config.onMessage(new Uint8Array(msg.data));
131
150
  };
151
+ return {
152
+ reset: () => {
153
+ // We can't set these fields to null because the TypeScript definitions don't
154
+ // allow it, but we can set them to dummy values.
155
+ socket.onopen = () => { };
156
+ socket.onclose = () => { };
157
+ socket.onmessage = () => { };
158
+ socket.onerror = () => { };
159
+ socket.close();
160
+ },
161
+ send: (data) => {
162
+ // The WebSocket library that we use seems to spontaneously transition connections
163
+ // to the "closed" state but not call the `onclosed` callback immediately. Calling
164
+ // `send` on that object throws an exception. In order to avoid panicking smoldot,
165
+ // we thus absorb any exception thrown here.
166
+ // See also <https://github.com/paritytech/smoldot/issues/2937>.
167
+ try {
168
+ socket.send(data);
169
+ if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
170
+ bufferedAmountCheck.nextTimeout = 10;
171
+ setTimeout(checkBufferedAmount, 10);
172
+ }
173
+ bufferedAmountCheck.quenedUnreportedBytes += data.length;
174
+ }
175
+ catch (_error) { }
176
+ },
177
+ closeSend: () => { throw new Error('Wrong connection type'); },
178
+ openOutSubstream: () => { throw new Error('Wrong connection type'); }
179
+ };
132
180
  }
133
181
  else if (tcpParsed != null) {
134
- // `net` module will be missing when we're not in NodeJS.
135
182
  if (forbidTcp) {
136
183
  throw new ConnectionError('TCP connections not available');
137
184
  }
@@ -140,21 +187,24 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
140
187
  inner: Deno.connect({
141
188
  hostname: tcpParsed[2],
142
189
  port: parseInt(tcpParsed[3], 10),
190
+ }).catch((error) => {
191
+ socket.destroyed = true;
192
+ config.onConnectionReset(error.toString());
193
+ return null;
143
194
  })
144
195
  };
145
- connection = { ty: 'tcp', socket };
146
196
  socket.inner = socket.inner.then((established) => {
147
197
  // TODO: at the time of writing of this comment, `setNoDelay` is still unstable
148
198
  //established.setNoDelay();
149
199
  if (socket.destroyed)
150
200
  return established;
151
- config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux' });
201
+ config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: true });
152
202
  // Spawns an asynchronous task that continuously reads from the socket.
153
203
  // Every time data is read, the task re-executes itself in order to continue reading.
154
204
  // The task ends automatically if an EOF or error is detected, which should also happen
155
205
  // if the user calls `close()`.
156
206
  const read = (readBuffer) => __awaiter(this, void 0, void 0, function* () {
157
- if (socket.destroyed)
207
+ if (socket.destroyed || established === null)
158
208
  return;
159
209
  let outcome = null;
160
210
  try {
@@ -180,53 +230,21 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
180
230
  read(new Uint8Array(1024));
181
231
  return established;
182
232
  });
183
- }
184
- else {
185
- throw new ConnectionError('Unrecognized multiaddr format');
186
- }
187
- return {
188
- reset: () => {
189
- if (connection.ty == 'websocket') {
190
- // WebSocket
191
- // We can't set these fields to null because the TypeScript definitions don't
192
- // allow it, but we can set them to dummy values.
193
- connection.socket.onopen = () => { };
194
- connection.socket.onclose = () => { };
195
- connection.socket.onmessage = () => { };
196
- connection.socket.onerror = () => { };
197
- connection.socket.close();
198
- }
199
- else {
200
- // TCP
201
- connection.socket.destroyed = true;
202
- connection.socket.inner.then((connec) => connec.close());
203
- }
204
- },
205
- send: (data) => {
206
- if (connection.ty == 'websocket') {
207
- // WebSocket
208
- // The WebSocket library that we use seems to spontaneously transition connections
209
- // to the "closed" state but not call the `onclosed` callback immediately. Calling
210
- // `send` on that object throws an exception. In order to avoid panicking smoldot,
211
- // we thus absorb any exception thrown here.
212
- // See also <https://github.com/paritytech/smoldot/issues/2937>.
213
- try {
214
- connection.socket.send(data);
215
- }
216
- catch (_error) { }
217
- }
218
- else {
219
- // TCP
220
- // TODO: at the moment, sending data doesn't have any back-pressure mechanism; as such, we just buffer data indefinitely
233
+ return {
234
+ reset: () => {
235
+ socket.destroyed = true;
236
+ socket.inner.then((connec) => connec.close());
237
+ },
238
+ send: (data) => {
221
239
  let dataCopy = Uint8Array.from(data); // Deep copy of the data
222
- const socket = connection.socket;
223
- connection.socket.inner = connection.socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
240
+ socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
224
241
  while (dataCopy.length > 0) {
225
- if (socket.destroyed)
242
+ if (socket.destroyed || c === null)
226
243
  return c;
227
244
  let outcome;
228
245
  try {
229
246
  outcome = yield c.write(dataCopy);
247
+ config.onWritableBytes(dataCopy.length);
230
248
  }
231
249
  catch (error) {
232
250
  // The type of `error` is unclear, but we assume that it implements `Error`
@@ -246,8 +264,17 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
246
264
  }
247
265
  return c;
248
266
  }));
249
- }
250
- },
251
- openOutSubstream: () => { throw new Error('Wrong connection type'); }
252
- };
267
+ },
268
+ closeSend: () => {
269
+ socket.inner = socket.inner.then((c) => __awaiter(this, void 0, void 0, function* () {
270
+ yield (c === null || c === void 0 ? void 0 : c.closeWrite());
271
+ return c;
272
+ }));
273
+ },
274
+ openOutSubstream: () => { throw new Error('Wrong connection type'); }
275
+ };
276
+ }
277
+ else {
278
+ throw new ConnectionError('Unrecognized multiaddr format');
279
+ }
253
280
  }
@@ -42,7 +42,7 @@ export function start(options) {
42
42
  return performance.now();
43
43
  },
44
44
  getRandomValues: (buffer) => {
45
- if (buffer.length >= 65536)
45
+ if (buffer.length >= 1024 * 1024)
46
46
  throw new Error('getRandomValues buffer too large');
47
47
  randomFillSync(buffer);
48
48
  },
@@ -58,7 +58,6 @@ export function start(options) {
58
58
  * @throws {@link ConnectionError} If the multiaddress couldn't be parsed or contains an invalid protocol.
59
59
  */
60
60
  function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
61
- let connection;
62
61
  // Attempt to parse the multiaddress.
63
62
  // TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
64
63
  const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
@@ -75,8 +74,33 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
75
74
  (proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
76
75
  const socket = new WebSocket(url);
77
76
  socket.binaryType = 'arraybuffer';
77
+ const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
78
+ const checkBufferedAmount = () => {
79
+ if (socket.readyState != 1)
80
+ return;
81
+ // Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
82
+ // of all the data that has been sent, but that seems to not be the case. It is
83
+ // unclear whether this is intended or a bug, but is is likely that `bufferedAmount`
84
+ // also includes WebSocket headers. For this reason, we use `bufferedAmount` as a hint
85
+ // rather than a correct value.
86
+ const bufferedAmount = socket.bufferedAmount;
87
+ let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
88
+ if (wasSent < 0)
89
+ wasSent = 0;
90
+ bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
91
+ if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
92
+ setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
93
+ bufferedAmountCheck.nextTimeout *= 2;
94
+ if (bufferedAmountCheck.nextTimeout > 500)
95
+ bufferedAmountCheck.nextTimeout = 500;
96
+ }
97
+ // Note: it is important to call `onWritableBytes` at the very end, as it might
98
+ // trigger a call to `send`.
99
+ if (wasSent != 0)
100
+ config.onWritableBytes(wasSent);
101
+ };
78
102
  socket.onopen = () => {
79
- config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux' });
103
+ config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux', initialWritableBytes: 1024 * 1024, writeClosable: false });
80
104
  };
81
105
  socket.onclose = (event) => {
82
106
  const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
@@ -96,7 +120,27 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
96
120
  socket.onmessage = (msg) => {
97
121
  config.onMessage(new Uint8Array(msg.data));
98
122
  };
99
- connection = { ty: 'websocket', socket };
123
+ return {
124
+ reset: () => {
125
+ // We can't set these fields to null because the TypeScript definitions don't
126
+ // allow it, but we can set them to dummy values.
127
+ socket.onopen = () => { };
128
+ socket.onclose = () => { };
129
+ socket.onmessage = () => { };
130
+ socket.onerror = () => { };
131
+ socket.close();
132
+ },
133
+ send: (data) => {
134
+ socket.send(data);
135
+ if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
136
+ bufferedAmountCheck.nextTimeout = 10;
137
+ setTimeout(checkBufferedAmount, 10);
138
+ }
139
+ bufferedAmountCheck.quenedUnreportedBytes += data.length;
140
+ },
141
+ closeSend: () => { throw new Error('Wrong connection type'); },
142
+ openOutSubstream: () => { throw new Error('Wrong connection type'); }
143
+ };
100
144
  }
101
145
  else if (tcpParsed != null) {
102
146
  // `net` module will be missing when we're not in NodeJS.
@@ -107,14 +151,18 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
107
151
  host: tcpParsed[2],
108
152
  port: parseInt(tcpParsed[3], 10),
109
153
  });
110
- connection = { ty: 'tcp', socket };
111
- connection.socket.setNoDelay();
112
- connection.socket.on('connect', () => {
154
+ // Number of bytes queued using `socket.write` and where `write` has returned false.
155
+ const drainingBytes = { num: 0 };
156
+ socket.setNoDelay();
157
+ socket.on('connect', () => {
113
158
  if (socket.destroyed)
114
159
  return;
115
- config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux' });
160
+ config.onOpen({
161
+ type: 'single-stream', handshake: 'multistream-select-noise-yamux',
162
+ initialWritableBytes: socket.writableHighWaterMark, writeClosable: true
163
+ });
116
164
  });
117
- connection.socket.on('close', (hasError) => {
165
+ socket.on('close', (hasError) => {
118
166
  if (socket.destroyed)
119
167
  return;
120
168
  // NodeJS doesn't provide a reason why the closing happened, but only
@@ -122,43 +170,46 @@ function connect(config, forbidTcp, forbidWs, forbidNonLocalWs, forbidWss) {
122
170
  const message = hasError ? "Error" : "Closed gracefully";
123
171
  config.onConnectionReset(message);
124
172
  });
125
- connection.socket.on('error', () => { });
126
- connection.socket.on('data', (message) => {
173
+ socket.on('error', () => { });
174
+ socket.on('data', (message) => {
127
175
  if (socket.destroyed)
128
176
  return;
129
177
  config.onMessage(new Uint8Array(message.buffer));
130
178
  });
179
+ socket.on('drain', () => {
180
+ // The bytes queued using `socket.write` and where `write` has returned false have now
181
+ // been sent. Notify the API that it can write more data.
182
+ if (socket.destroyed)
183
+ return;
184
+ const val = drainingBytes.num;
185
+ drainingBytes.num = 0;
186
+ config.onWritableBytes(val);
187
+ });
188
+ return {
189
+ reset: () => {
190
+ socket.destroy();
191
+ },
192
+ send: (data) => {
193
+ const dataLen = data.length;
194
+ const allWritten = socket.write(data);
195
+ if (allWritten) {
196
+ setImmediate(() => {
197
+ if (!socket.writable)
198
+ return;
199
+ config.onWritableBytes(dataLen);
200
+ });
201
+ }
202
+ else {
203
+ drainingBytes.num += dataLen;
204
+ }
205
+ },
206
+ closeSend: () => {
207
+ socket.end();
208
+ },
209
+ openOutSubstream: () => { throw new Error('Wrong connection type'); }
210
+ };
131
211
  }
132
212
  else {
133
213
  throw new ConnectionError('Unrecognized multiaddr format');
134
214
  }
135
- return {
136
- reset: () => {
137
- if (connection.ty == 'websocket') {
138
- // WebSocket
139
- // We can't set these fields to null because the TypeScript definitions don't
140
- // allow it, but we can set them to dummy values.
141
- connection.socket.onopen = () => { };
142
- connection.socket.onclose = () => { };
143
- connection.socket.onmessage = () => { };
144
- connection.socket.onerror = () => { };
145
- connection.socket.close();
146
- }
147
- else {
148
- // TCP
149
- connection.socket.destroy();
150
- }
151
- },
152
- send: (data) => {
153
- if (connection.ty == 'websocket') {
154
- // WebSocket
155
- connection.socket.send(data);
156
- }
157
- else {
158
- // TCP
159
- connection.socket.write(data);
160
- }
161
- },
162
- openOutSubstream: () => { throw new Error('Wrong connection type'); }
163
- };
164
215
  }