bolthole 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bolthole",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Reach any service from anywhere through a relay that can't read your traffic. Browser, Node, Deno, Bun and Workers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/mux.js CHANGED
@@ -3,7 +3,8 @@
3
3
  import { concat, enc, dec, u32, readU32, deferred } from './bytes.js';
4
4
 
5
5
  export const T = { OPEN: 1, ACCEPT: 2, DATA: 3, FIN: 4, RESET: 5, WINDOW: 6, PING: 7, PONG: 8, GOAWAY: 9 };
6
- export const INITIAL_WINDOW = 256 * 1024;
6
+ export const INITIAL_WINDOW = 4 * 1024 * 1024;
7
+ export const SESSION_WINDOW = 64 * 1024 * 1024;
7
8
  export const MAX_CHUNK = 32 * 1024;
8
9
  export const DEFAULT_MAX_FRAME = (1 << 20) + 5;
9
10
 
@@ -54,6 +55,11 @@ export class Session {
54
55
  this.lastRecv = Date.now();
55
56
  this.lastSend = Date.now();
56
57
  this.tail = Promise.resolve();
58
+ // Session-level flow control (spec §3.4), shared by all streams.
59
+ this.sendCredit = SESSION_WINDOW;
60
+ this.recvCredit = SESSION_WINDOW;
61
+ this.recvUnack = 0;
62
+ this._wake = deferred();
57
63
  this.readLoop();
58
64
  if (this.heartbeatMs > 0) {
59
65
  this.timer = setInterval(() => this.tick(), this.heartbeatMs / 4);
@@ -136,6 +142,17 @@ export class Session {
136
142
  }
137
143
  }
138
144
 
145
+ _release(n) {
146
+ if (n <= 0) return;
147
+ this.recvUnack += n;
148
+ if (this.recvUnack >= SESSION_WINDOW / 2) {
149
+ const credit = this.recvUnack;
150
+ this.recvUnack = 0;
151
+ this.recvCredit += credit;
152
+ this.send(T.WINDOW, 0, u32(credit)).catch(() => {});
153
+ }
154
+ }
155
+
139
156
  isPeerId(id) {
140
157
  return id !== 0 && (id % 2 === 1) !== this.dialer;
141
158
  }
@@ -177,8 +194,22 @@ export class Session {
177
194
  case T.FIN:
178
195
  case T.RESET:
179
196
  case T.WINDOW: {
197
+ if (f.type === T.WINDOW && f.id === 0) {
198
+ if (f.payload.length !== 4) throw new Error('bolthole: bad session window');
199
+ this.sendCredit += readU32(f.payload);
200
+ if (this.sendCredit > 2 ** 31) throw new Error('bolthole: session window overflow');
201
+ const w = this._wake;
202
+ this._wake = deferred();
203
+ w.resolve();
204
+ return;
205
+ }
206
+ if (f.type === T.DATA) {
207
+ if (f.payload.length > this.recvCredit) throw new Error('bolthole: session window exceeded');
208
+ this.recvCredit -= f.payload.length;
209
+ }
180
210
  const st = this.streams.get(f.id);
181
211
  if (!st) {
212
+ if (f.type === T.DATA) this._release(f.payload.length);
182
213
  if (f.type === T.ACCEPT || f.type === T.DATA) this.send(T.RESET, f.id, json({ code: 'unknown_stream' })).catch(() => {});
183
214
  return;
184
215
  }
@@ -203,6 +234,7 @@ export class Stream {
203
234
  this.recvFin = false;
204
235
  this.recvCredit = INITIAL_WINDOW;
205
236
  this.unacked = 0;
237
+ this.held = 0; // buffered bytes still charged to the session window
206
238
  this.sendCredit = INITIAL_WINDOW;
207
239
  this.localFin = false;
208
240
  this.isAccepted = false;
@@ -243,6 +275,9 @@ export class Stream {
243
275
  const chunk = this.buf.shift();
244
276
  this.bufLen -= chunk.length;
245
277
  this.unacked += chunk.length;
278
+ const release = Math.min(chunk.length, this.held);
279
+ this.held -= release;
280
+ this.session._release(release);
246
281
  if (this.unacked >= INITIAL_WINDOW / 2 && !this.recvFin && !this.error) {
247
282
  const credit = this.unacked;
248
283
  this.unacked = 0;
@@ -275,8 +310,14 @@ export class Stream {
275
310
  await this._wake.promise;
276
311
  continue;
277
312
  }
278
- const n = Math.min(data.length - off, MAX_CHUNK, this.sendCredit);
313
+ const s = this.session;
314
+ if (s.sendCredit <= 0) {
315
+ await Promise.race([this._wake.promise, s._wake.promise]);
316
+ continue;
317
+ }
318
+ const n = Math.min(data.length - off, MAX_CHUNK, this.sendCredit, s.sendCredit);
279
319
  this.sendCredit -= n;
320
+ s.sendCredit -= n;
280
321
  await this.session.send(T.DATA, this.id, data.subarray(off, off + n));
281
322
  off += n;
282
323
  }
@@ -308,6 +349,8 @@ export class Stream {
308
349
  if (!this.error && !this.finished) this.error = err;
309
350
  const was = this.finished;
310
351
  this.finished = true;
352
+ this.session._release(this.held);
353
+ this.held = 0;
311
354
  this._accept.reject(err);
312
355
  this._signal();
313
356
  if (!was) this._done.resolve();
@@ -336,9 +379,13 @@ export class Stream {
336
379
  }
337
380
  return;
338
381
  case T.DATA:
339
- if (this.recvFin || this.error) return;
340
- if (f.payload.length > this.recvCredit) return this.reset('flow_control', 'window exceeded');
382
+ if (this.recvFin || this.error) return this.session._release(f.payload.length);
383
+ if (f.payload.length > this.recvCredit) {
384
+ this.session._release(f.payload.length);
385
+ return this.reset('flow_control', 'window exceeded');
386
+ }
341
387
  this.recvCredit -= f.payload.length;
388
+ this.held += f.payload.length;
342
389
  this.buf.push(f.payload.slice());
343
390
  this.bufLen += f.payload.length;
344
391
  this._signal();
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Package version, in one place so every module agrees.
2
- export const VERSION = '0.1.1';
2
+ export const VERSION = '0.2.0';