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.
@@ -63,12 +63,33 @@ export interface Connection {
63
63
  /**
64
64
  * Queues data to be sent on the given connection.
65
65
  *
66
- * The connection must currently be in the `Open` state.
66
+ * The connection and stream must currently be in the `Open` state.
67
+ *
68
+ * The number of bytes most never exceed the number of "writable bytes" of the stream.
69
+ * `onWritableBytes` can be used in order to notify that more writable bytes are available.
67
70
  *
68
71
  * The `streamId` must be provided if and only if the connection is of type "multi-stream".
69
72
  * It indicates which substream to send the data on.
73
+ *
74
+ * Must not be called after `closeSend` has been called.
70
75
  */
71
76
  send(data: Uint8Array, streamId?: number): void;
77
+ /**
78
+ * Closes the writing side of the given stream of the given connection.
79
+ *
80
+ * Never called for connection types where this isn't possible to implement (i.e. WebSocket
81
+ * and WebRTC at the moment).
82
+ *
83
+ * The connection and stream must currently be in the `Open` state.
84
+ *
85
+ * Implicitly sets the "writable bytes" of the stream to zero.
86
+ *
87
+ * The `streamId` must be provided if and only if the connection is of type "multi-stream".
88
+ * It indicates which substream to send the data on.
89
+ *
90
+ * Must only be called once per stream.
91
+ */
92
+ closeSend(streamId?: number): void;
72
93
  /**
73
94
  * Start opening an additional outbound substream on the given connection.
74
95
  *
@@ -104,6 +125,8 @@ export interface ConnectionConfig {
104
125
  onOpen: (info: {
105
126
  type: 'single-stream';
106
127
  handshake: 'multistream-select-noise-yamux';
128
+ initialWritableBytes: number;
129
+ writeClosable: boolean;
107
130
  } | {
108
131
  type: 'multi-stream';
109
132
  handshake: 'webrtc';
@@ -121,7 +144,7 @@ export interface ConnectionConfig {
121
144
  *
122
145
  * This function must only be called for connections of type "multi-stream".
123
146
  */
124
- onStreamOpened: (streamId: number, direction: 'inbound' | 'outbound') => void;
147
+ onStreamOpened: (streamId: number, direction: 'inbound' | 'outbound', initialWritableBytes: number) => void;
125
148
  /**
126
149
  * Callback called when a stream transitions to the `Reset` state.
127
150
  *
@@ -130,6 +153,17 @@ export interface ConnectionConfig {
130
153
  * This function must only be called for connections of type "multi-stream".
131
154
  */
132
155
  onStreamReset: (streamId: number) => void;
156
+ /**
157
+ * Callback called when more data can be written on the stream.
158
+ *
159
+ * Can only happen while the connection is in the `Open` state.
160
+ *
161
+ * This callback must not be called after `closeSend` has been called.
162
+ *
163
+ * The `streamId` parameter must be provided if and only if the connection is of type
164
+ * "multi-stream".
165
+ */
166
+ onWritableBytes: (numExtra: number, streamId?: number) => void;
133
167
  /**
134
168
  * Callback called when a message sent by the remote has been received.
135
169
  *
@@ -97,7 +97,7 @@ function default_1(config) {
97
97
  // In browsers, `setTimeout` works as expected when `ms` equals 0. However, NodeJS
98
98
  // requires a minimum of 1 millisecond (if `0` is passed, it is automatically replaced
99
99
  // with `1`) and wants you to use `setImmediate` instead.
100
- if (ms == 0 && typeof setImmediate === "function") {
100
+ if (ms < 1 && typeof setImmediate === "function") {
101
101
  setImmediate(() => {
102
102
  if (killedTracked.killed)
103
103
  return;
@@ -140,7 +140,7 @@ function default_1(config) {
140
140
  try {
141
141
  switch (info.type) {
142
142
  case 'single-stream': {
143
- instance.exports.connection_open_single_stream(connectionId, 0);
143
+ instance.exports.connection_open_single_stream(connectionId, 0, info.initialWritableBytes, info.writeClosable ? 1 : 0);
144
144
  break;
145
145
  }
146
146
  case 'multi-stream': {
@@ -168,6 +168,14 @@ function default_1(config) {
168
168
  }
169
169
  catch (_error) { }
170
170
  },
171
+ onWritableBytes: (numExtra, streamId) => {
172
+ if (killedTracked.killed)
173
+ return;
174
+ try {
175
+ instance.exports.stream_writable_bytes(connectionId, streamId || 0, numExtra);
176
+ }
177
+ catch (_error) { }
178
+ },
171
179
  onMessage: (message, streamId) => {
172
180
  if (killedTracked.killed)
173
181
  return;
@@ -178,11 +186,11 @@ function default_1(config) {
178
186
  }
179
187
  catch (_error) { }
180
188
  },
181
- onStreamOpened: (streamId, direction) => {
189
+ onStreamOpened: (streamId, direction, initialWritableBytes) => {
182
190
  if (killedTracked.killed)
183
191
  return;
184
192
  try {
185
- instance.exports.connection_stream_opened(connectionId, streamId, direction === 'outbound' ? 1 : 0);
193
+ instance.exports.connection_stream_opened(connectionId, streamId, direction === 'outbound' ? 1 : 0, initialWritableBytes);
186
194
  }
187
195
  catch (_error) { }
188
196
  },
@@ -244,6 +252,12 @@ function default_1(config) {
244
252
  const connection = connections[connectionId];
245
253
  connection.send(data, streamId); // TODO: docs says the streamId is provided only for multi-stream connections, but here it's always provided
246
254
  },
255
+ stream_send_close: (connectionId, streamId) => {
256
+ if (killedTracked.killed)
257
+ return;
258
+ const connection = connections[connectionId];
259
+ connection.closeSend(streamId); // TODO: docs says the streamId is provided only for multi-stream connections, but here it's always provided
260
+ },
247
261
  current_task_entered: (ptr, len) => {
248
262
  if (killedTracked.killed)
249
263
  return;
@@ -19,10 +19,11 @@ export interface SmoldotWasmExports extends WebAssembly.Exports {
19
19
  json_rpc_responses_peek: (chainId: number) => number;
20
20
  json_rpc_responses_pop: (chainId: number) => void;
21
21
  timer_finished: (timerId: number) => void;
22
- connection_open_single_stream: (connectionId: number, handshakeTy: number) => void;
22
+ connection_open_single_stream: (connectionId: number, handshakeTy: number, initialWritableBytes: number, writeClosable: number) => void;
23
23
  connection_open_multi_stream: (connectionId: number, handshakeTyPtr: number, handshakeTyLen: number) => void;
24
+ stream_writable_bytes: (connectionId: number, streamId: number, numBytes: number) => void;
24
25
  stream_message: (connectionId: number, streamId: number, ptr: number, len: number) => void;
25
- connection_stream_opened: (connectionId: number, streamId: number, outbound: number) => void;
26
+ connection_stream_opened: (connectionId: number, streamId: number, outbound: number, initialWritableBytes: number) => void;
26
27
  connection_reset: (connectionId: number, ptr: number, len: number) => void;
27
28
  stream_reset: (connectionId: number, streamId: number) => void;
28
29
  }
@@ -26,17 +26,23 @@ export interface Client {
26
26
  /**
27
27
  * Connects to a chain.
28
28
  *
29
- * Throws an exception if the chain specification isn't valid, or if the chain specification
30
- * concerns a parachain but no corresponding relay chain can be found.
29
+ * After you've called this function, the client will verify whether the chain specification is
30
+ * valid. Once this is done, the `Promise` returned by this function will yield a
31
+ * {@link Chain} that can be used to interact with that chain. Only after the `Promise` has
32
+ * yielded will the client actually start establishing networking connections to the chain.
33
+ *
34
+ * The `Promise` throws an exception if the chain specification isn't valid, or if the chain
35
+ * specification concerns a parachain but no corresponding relay chain can be found.
31
36
  *
32
37
  * Smoldot will automatically de-duplicate chains if multiple identical chains are added, in
33
38
  * order to save resources. In other words, it is not a problem to call `addChain` multiple
34
- * times with the same chain specifications and obtain multiple `Chain`.
39
+ * times with the same chain specifications and obtain multiple {@link Chain} objects.
35
40
  * When the same client is used for multiple different purposes, you are in fact strongly
36
41
  * encouraged to trust smoldot and not attempt to de-duplicate chains yourself, as determining
37
42
  * whether two chains are identical is complicated and might have security implications.
38
43
  *
39
- * Smoldot tries to distribute CPU resources equally between all active `Chain` objects.
44
+ * Smoldot tries to distribute CPU resources equally between all active {@link Chain} objects
45
+ * of the same client.
40
46
  *
41
47
  * @param options Configuration of the chain to add.
42
48
  *
@@ -48,6 +54,9 @@ export interface Client {
48
54
  /**
49
55
  * Terminates the client.
50
56
  *
57
+ * This implicitly calls {@link Chain.remove} on all the chains associated with this client,
58
+ * then shuts down the client itself.
59
+ *
51
60
  * Afterwards, trying to use the client or any of its chains again will lead to an exception
52
61
  * being thrown.
53
62
  *
@@ -70,10 +79,15 @@ export interface Chain {
70
79
  * Be aware that some requests will cause notifications to be sent back using the same callback
71
80
  * as the responses.
72
81
  *
73
- * A {@link MalformedJsonRpcError} is thrown if the request isn't a valid JSON-RPC request or
74
- * if the request is unreasonably large (64 MiB at the time of writing of this comment).
75
- * If, however, the request is a valid JSON-RPC request but that concerns an unknown method, a
76
- * error response is properly generated.
82
+ * A {@link MalformedJsonRpcError} is thrown if the request isn't a valid JSON-RPC request
83
+ * (for example if it is not valid JSON) or if the request is unreasonably large (64 MiB at the
84
+ * time of writing of this comment).
85
+ * If, however, the request is a valid JSON-RPC request but that concerns an unknown method, or
86
+ * if for example some parameters are missing, an error response is properly generated and
87
+ * yielded through the JSON-RPC callback.
88
+ * In other words, a {@link MalformedJsonRpcError} is thrown in situations where something
89
+ * is *so wrong* with the request that it is not possible for smoldot to send back an error
90
+ * through the JSON-RPC callback.
77
91
  *
78
92
  * Two JSON-RPC APIs are supported by smoldot:
79
93
  *
@@ -92,14 +106,17 @@ export interface Chain {
92
106
  /**
93
107
  * Waits for a JSON-RPC response or notification to be generated.
94
108
  *
95
- * If this function is called multiple times "simultaneously" (generating multiple different
96
- * `Promise`s), each `Promise` will return a different JSON-RPC response or notification.
97
- *
98
109
  * Each chain contains a buffer of the responses waiting to be sent out. Calling this function
99
110
  * pulls one element from the buffer. If this function is called at a slower rate than responses
100
- * are generated, then buffer will eventually become full, at which point calling
111
+ * are generated, then the buffer will eventually become full, at which point calling
101
112
  * {@link Chain.sendJsonRpc} will throw an exception.
102
113
  *
114
+ * If this function is called multiple times "simultaneously" (generating multiple different
115
+ * `Promise`s), each `Promise` will return a different JSON-RPC response or notification. In
116
+ * that situation, there is no guarantee in the ordering in which the responses or notifications
117
+ * are yielded. Calling this function multiple times "simultaneously" is in general a niche
118
+ * corner case that you are encouraged to avoid.
119
+ *
103
120
  * @throws {@link AlreadyDestroyedError} If the chain has been removed or the client has been terminated.
104
121
  * @throws {@link JsonRpcDisabledError} If the JSON-RPC system was disabled in the options of the chain.
105
122
  * @throws {@link CrashError} If the background client has crashed.
@@ -108,14 +125,15 @@ export interface Chain {
108
125
  /**
109
126
  * Disconnects from the blockchain.
110
127
  *
111
- * The JSON-RPC callback will no longer be called.
128
+ * The JSON-RPC callback will no longer be called. This is the case immediately after this
129
+ * function is called. Any on-going JSON-RPC request is instantaneously aborted.
112
130
  *
113
131
  * Trying to use the chain again will lead to an exception being thrown.
114
132
  *
115
133
  * If this chain is a relay chain, then all parachains that use it will continue to work. Smoldot
116
134
  * automatically keeps alive all relay chains that have an active parachains. There is no need
117
- * to track parachains and relaychains, or to destroy them in the correct order, as this is
118
- * handled automatically.
135
+ * to track parachains and relay chains, or to destroy them in the correct order, as this is
136
+ * handled automatically internally.
119
137
  *
120
138
  * @throws {@link AlreadyDestroyedError} If the chain has already been removed or the client has been terminated.
121
139
  * @throws {@link CrashError} If the background client has crashed.
@@ -140,10 +158,10 @@ export interface ClientOptions {
140
158
  */
141
159
  logCallback?: LogCallback;
142
160
  /**
143
- * The client will never call the callback with a value of `level` superior to this value.
161
+ * The client will never call the log callback with a value of `level` superior to this value.
144
162
  * Defaults to 3.
145
163
  *
146
- * While this filtering could be done directly by the `logCallback`, passing a maximum log level
164
+ * While this filtering could be done manually in the `logCallback`, passing a maximum log level
147
165
  * leads to better performances as the client doesn't even need to generate a `message` when it
148
166
  * knows that this message isn't interesting.
149
167
  */
@@ -226,32 +244,47 @@ export interface AddChainOptions {
226
244
  * `<client> build-spec --raw > spec.json`. Only "raw" chain specifications are supported by
227
245
  * smoldot at the moment.
228
246
  *
229
- * If the chain specification contains a `relay_chain` field, then smoldot will try to match
230
- * the value in `relay_chain` with the value in `id` of the chains in `potentialRelayChains`.
247
+ * If the chain specification contains a `relayChain` field, then smoldot will try to match
248
+ * the value in `relayChain` with the value in `id` of the chains in
249
+ * {@link AddChainOptions.potentialRelayChains}.
231
250
  */
232
251
  chainSpec: string;
233
252
  /**
234
- * Content of the database of this chain. Can be obtained by using the
235
- * `chainHead_unstable_finalizedDatabase` JSON-RPC function.
253
+ * Content of the database of this chain.
254
+ *
255
+ * The content of the database can be obtained by using the
256
+ * `chainHead_unstable_finalizedDatabase` JSON-RPC function. This undocumented JSON-RPC function
257
+ * accepts one parameter of type `number` indicating an upper limit to the size of the database.
258
+ * The content of the database is always a UTF-8 string whose content is at the discretion of
259
+ * the smoldot implementation.
236
260
  *
237
261
  * Smoldot reserves the right to change its database format, making previous databases
238
- * incompatible. For this reason, no error is generated if the content of the database is invalid
239
- * and/or can't be decoded.
262
+ * incompatible. For this reason, no error is generated if the content of the database is
263
+ * invalid and/or can't be decoded.
264
+ *
265
+ * Providing a database can considerably improve the time it takes for smoldot to be fully
266
+ * synchronized with a chain by reducing the amount of data that it has to download.
267
+ * Furthermore, the database also contains a list of nodes that smoldot can use in order to
268
+ * reduce the load that is being put on the bootnodes.
240
269
  *
241
270
  * Important: please note that using a malicious database content can lead to a security
242
271
  * vulnerability. This database content is considered by smoldot as trusted input. It is the
243
272
  * responsibility of the API user to make sure that the value passed in this field comes from
244
273
  * the same source of trust as the chain specification that was used when retrieving this
245
- * database content.
274
+ * database content. In other words, if you load this database content for example from the disk
275
+ * or from the browser's local storage, be absolutely certain that no malicious program has
276
+ * modified the content of that file or local storage.
246
277
  */
247
278
  databaseContent?: string;
248
279
  /**
249
280
  * If `chainSpec` concerns a parachain, contains the list of chains whose `id` smoldot will try
250
- * to match with the parachain's `relay_chain`.
281
+ * to match with the parachain's `relayChain`.
251
282
  * Defaults to `[]`.
252
283
  *
253
- * Must contain exactly the objects that were returned by previous calls to `addChain`. The
254
- * library uses a `WeakMap` in its implementation in order to identify chains.
284
+ * Must contain exactly the {@link Chain} objects that were returned by previous calls to
285
+ * `addChain`. The library uses a `WeakMap` in its implementation in order to identify chains.
286
+ *
287
+ * # Explanation and usage
255
288
  *
256
289
  * The primary way smoldot determines which relay chain is associated to a parachain is by
257
290
  * inspecting the chain specification of that parachain (i.e. the `chainSpec` field).
@@ -260,7 +293,7 @@ export interface AddChainOptions {
260
293
  * applications: multiple applications could add mutiple different chains with the same `id`,
261
294
  * creating an ambiguity, or an application could register malicious chains with small variations
262
295
  * of a popular chain's `id` and try to benefit from a typo in a legitimate application's
263
- * `relay_chain`.
296
+ * `relayChain`.
264
297
  *
265
298
  * These problems can be solved by using this parameter to segregate multiple different uses of
266
299
  * the same client. To use it, pass the list of all chains that the same application has
@@ -40,6 +40,8 @@ export function start(options) {
40
40
  return Promise.resolve(inflate(classicDecode(input)));
41
41
  },
42
42
  registerShouldPeriodicallyYield: (callback) => {
43
+ if (typeof document === 'undefined') // We might be in a web worker.
44
+ return [false, () => { }];
43
45
  const wrappedCallback = () => callback(document.visibilityState === 'visible');
44
46
  document.addEventListener('visibilitychange', wrappedCallback);
45
47
  return [document.visibilityState === 'visible', () => { document.removeEventListener('visibilitychange', wrappedCallback); }];
@@ -68,9 +70,8 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
68
70
  // Attempt to parse the multiaddress.
69
71
  // TODO: remove support for `/wss` in a long time (https://github.com/paritytech/smoldot/issues/1940)
70
72
  const wsParsed = config.address.match(/^\/(ip4|ip6|dns4|dns6|dns)\/(.*?)\/tcp\/(.*?)\/(ws|wss|tls\/ws)$/);
71
- const webRTCParsed = config.address.match(/^\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc\/certhash\/(.*?)$/);
73
+ const webRTCParsed = config.address.match(/^\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc-direct\/certhash\/(.*?)$/);
72
74
  if (wsParsed != null) {
73
- let connection;
74
75
  const proto = (wsParsed[4] == 'ws') ? 'ws' : 'wss';
75
76
  if ((proto == 'ws' && forbidWs) ||
76
77
  (proto == 'ws' && wsParsed[2] != 'localhost' && wsParsed[2] != '127.0.0.1' && forbidNonLocalWs) ||
@@ -80,10 +81,36 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
80
81
  const url = (wsParsed[1] == 'ip6') ?
81
82
  (proto + "://[" + wsParsed[2] + "]:" + wsParsed[3]) :
82
83
  (proto + "://" + wsParsed[2] + ":" + wsParsed[3]);
83
- connection = new WebSocket(url);
84
+ const connection = new WebSocket(url);
84
85
  connection.binaryType = 'arraybuffer';
86
+ const bufferedAmountCheck = { quenedUnreportedBytes: 0, nextTimeout: 10 };
87
+ const checkBufferedAmount = () => {
88
+ if (connection.readyState != 1)
89
+ return;
90
+ // Note that we might expect `bufferedAmount` to always be <= the sum of the lengths
91
+ // of all the data that has been sent, but that might not be the case. For this
92
+ // reason, we use `bufferedAmount` as a hint rather than a correct value.
93
+ const bufferedAmount = connection.bufferedAmount;
94
+ let wasSent = bufferedAmountCheck.quenedUnreportedBytes - bufferedAmount;
95
+ if (wasSent < 0)
96
+ wasSent = 0;
97
+ bufferedAmountCheck.quenedUnreportedBytes -= wasSent;
98
+ if (bufferedAmountCheck.quenedUnreportedBytes != 0) {
99
+ setTimeout(checkBufferedAmount, bufferedAmountCheck.nextTimeout);
100
+ bufferedAmountCheck.nextTimeout *= 2;
101
+ if (bufferedAmountCheck.nextTimeout > 500)
102
+ bufferedAmountCheck.nextTimeout = 500;
103
+ }
104
+ // Note: it is important to call `onWritableBytes` at the very end, as it might
105
+ // trigger a call to `send`.
106
+ if (wasSent != 0)
107
+ config.onWritableBytes(wasSent);
108
+ };
85
109
  connection.onopen = () => {
86
- config.onOpen({ type: 'single-stream', handshake: 'multistream-select-noise-yamux' });
110
+ config.onOpen({
111
+ type: 'single-stream', handshake: 'multistream-select-noise-yamux',
112
+ initialWritableBytes: 1024 * 1024, writeClosable: false,
113
+ });
87
114
  };
88
115
  connection.onclose = (event) => {
89
116
  const message = "Error code " + event.code + (!!event.reason ? (": " + event.reason) : "");
@@ -102,7 +129,13 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
102
129
  },
103
130
  send: (data) => {
104
131
  connection.send(data);
132
+ if (bufferedAmountCheck.quenedUnreportedBytes == 0) {
133
+ bufferedAmountCheck.nextTimeout = 10;
134
+ setTimeout(checkBufferedAmount, 10);
135
+ }
136
+ bufferedAmountCheck.quenedUnreportedBytes += data.length;
105
137
  },
138
+ closeSend: () => { throw new Error('Wrong connection type'); },
106
139
  openOutSubstream: () => { throw new Error('Wrong connection type'); }
107
140
  };
108
141
  }
@@ -152,16 +185,18 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
152
185
  pc.onnegotiationneeded = null;
153
186
  pc.ondatachannel = null;
154
187
  for (const channel of Array.from(dataChannels.values())) {
155
- channel.onopen = null;
156
- channel.onerror = null;
157
- channel.onclose = null;
158
- channel.onmessage = null;
188
+ channel.channel.onopen = null;
189
+ channel.channel.onerror = null;
190
+ channel.channel.onclose = null;
191
+ channel.channel.onbufferedamountlow = null;
192
+ channel.channel.onmessage = null;
159
193
  }
160
194
  dataChannels.clear();
161
195
  if (handshakeDataChannel) {
162
196
  handshakeDataChannel.onopen = null;
163
197
  handshakeDataChannel.onerror = null;
164
198
  handshakeDataChannel.onclose = null;
199
+ handshakeDataChannel.onbufferedamountlow = null;
165
200
  handshakeDataChannel.onmessage = null;
166
201
  }
167
202
  handshakeDataChannel = undefined;
@@ -190,7 +225,7 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
190
225
  }
191
226
  else {
192
227
  console.assert(direction !== 'outbound' || !handshakeDataChannel, "handshakeDataChannel still defined");
193
- config.onStreamOpened(dataChannelId, direction);
228
+ config.onStreamOpened(dataChannelId, direction, 65536);
194
229
  }
195
230
  };
196
231
  dataChannel.onerror = dataChannel.onclose = (_error) => {
@@ -212,6 +247,7 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
212
247
  handshakeDataChannel.onopen = null;
213
248
  handshakeDataChannel.onerror = null;
214
249
  handshakeDataChannel.onclose = null;
250
+ handshakeDataChannel.onbufferedamountlow = null;
215
251
  handshakeDataChannel.onmessage = null;
216
252
  handshakeDataChannel = undefined;
217
253
  }
@@ -228,12 +264,18 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
228
264
  config.onStreamReset(dataChannelId);
229
265
  }
230
266
  };
267
+ dataChannel.onbufferedamountlow = () => {
268
+ const channel = dataChannels.get(dataChannelId);
269
+ const val = channel.bufferedBytes;
270
+ channel.bufferedBytes = 0;
271
+ config.onWritableBytes(val, dataChannelId);
272
+ };
231
273
  dataChannel.onmessage = (m) => {
232
274
  // The `data` field is an `ArrayBuffer`.
233
275
  config.onMessage(new Uint8Array(m.data), dataChannelId);
234
276
  };
235
277
  if (direction !== 'first-outbound')
236
- dataChannels.set(dataChannelId, dataChannel);
278
+ dataChannels.set(dataChannelId, { channel: dataChannel, bufferedBytes: 0 });
237
279
  else
238
280
  handshakeDataChannel = dataChannel;
239
281
  };
@@ -375,7 +417,8 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
375
417
  // (UDP or TCP)
376
418
  "a=sctp-port:5000" + "\n" +
377
419
  // The maximum SCTP user message size (in bytes) (RFC8841)
378
- "a=max-message-size:16384" + "\n" + // TODO: should this be part of the spec?
420
+ // Setting this field is part of the libp2p spec.
421
+ "a=max-message-size:16384" + "\n" +
379
422
  // A transport address for a candidate that can be used for connectivity
380
423
  // checks (RFC8839).
381
424
  "a=candidate:1 1 UDP 1 " + targetIp + " " + targetPort + " typ host" + "\n";
@@ -404,17 +447,21 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
404
447
  }
405
448
  else {
406
449
  const channel = dataChannels.get(streamId);
407
- channel.onopen = null;
408
- channel.onerror = null;
409
- channel.onclose = null;
410
- channel.onmessage = null;
411
- channel.close();
450
+ channel.channel.onopen = null;
451
+ channel.channel.onerror = null;
452
+ channel.channel.onclose = null;
453
+ channel.channel.onbufferedamountlow = null;
454
+ channel.channel.onmessage = null;
455
+ channel.channel.close();
412
456
  dataChannels.delete(streamId);
413
457
  }
414
458
  },
415
459
  send: (data, streamId) => {
416
- dataChannels.get(streamId).send(data);
460
+ const channel = dataChannels.get(streamId);
461
+ channel.channel.send(data);
462
+ channel.bufferedBytes += data.length;
417
463
  },
464
+ closeSend: () => { throw new Error('Wrong connection type'); },
418
465
  openOutSubstream: () => {
419
466
  // `openOutSubstream` can only be called after we have called `config.onOpen`, therefore
420
467
  // `pc` is guaranteed to be non-null.
@@ -426,8 +473,8 @@ function connect(config, forbidWs, forbidNonLocalWs, forbidWss, forbidWebRTC) {
426
473
  // We need to check again if `handshakeDataChannel` is still defined, as the
427
474
  // connection might have been closed.
428
475
  if (handshakeDataChannel) {
429
- config.onStreamOpened(handshakeDataChannel.id, 'outbound');
430
- dataChannels.set(handshakeDataChannel.id, handshakeDataChannel);
476
+ config.onStreamOpened(handshakeDataChannel.id, 'outbound', 1024 * 1024);
477
+ dataChannels.set(handshakeDataChannel.id, { channel: handshakeDataChannel, bufferedBytes: 0 });
431
478
  handshakeDataChannel = undefined;
432
479
  }
433
480
  }))();