braid-http 1.3.103 → 1.3.105

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/README.md CHANGED
@@ -210,7 +210,9 @@ require('http').createServer(
210
210
  ).listen(9935)
211
211
  ```
212
212
 
213
- You can also use `braidify` within a request handler like this:
213
+ If you are working from a library, or from code that does not have access to
214
+ the root of the HTTP handler or `next` in `(req, res, next)`, you can also
215
+ call `braidify` inline:
214
216
 
215
217
  ```javascript
216
218
  require('http').createServer(
@@ -223,8 +225,9 @@ require('http').createServer(
223
225
  ).listen(9935)
224
226
  ```
225
227
 
226
- The `is_multiplexer` test in this form is only necessary if multiplexing is
227
- enabled.
228
+ This works, but the inline form [leaks the multiplexing
229
+ abstraction](#inline-braidifyreq-res-leaks-the-abstraction) in three minor
230
+ ways.
228
231
 
229
232
  ### Example Nodejs server with Express
230
233
 
@@ -337,71 +340,111 @@ fetch('https://localhost:3009/chat',
337
340
  )
338
341
  ```
339
342
 
340
- ## Configuring Multiplexing
341
-
342
- You shouldn't need to, but can, configure which requests the library will
343
- [multiplex](https://braid.org/protocol/multiplexing). You can configure
344
- multiplexing on both the client and the server. They both need multiplexing
345
- enabled for it to happen.
346
-
347
- ### Client
343
+ ## Testing
348
344
 
349
- A client can globally disable multiplexing on `braid_fetch()` with:
345
+ Run all tests from the command line:
350
346
 
351
- ```javascript
352
- braid_fetch.enable_multiplex = false
347
+ ```
348
+ npm test
353
349
  ```
354
350
 
355
- It can enable multiplexing for all GET requests with:
351
+ Run tests in a browser (auto-opens):
356
352
 
357
- ```javascript
358
- braid_fetch.enable_multiplex = true
353
+ ```
354
+ npm run test:browser
359
355
  ```
360
356
 
361
- It can also set it to multiplex after `N` connections to an origin with:
357
+ You can also filter tests by name:
362
358
 
363
- ```javascript
364
- braid_fetch.enable_multiplex = {after: N}
359
+ ```
360
+ node test/test.js --filter="version"
365
361
  ```
366
362
 
367
- The default value is `{after: 1}`.
363
+ ## Multiplexing
368
364
 
369
- A client can override this global setting per-request by passing the same
370
- value into `braid_fetch(url, {multiplex: <value>})`, such as with:
365
+ This library automatically
366
+ [multiplexes](https://braid.org/protocol/multiplexing) subscriptions behind
367
+ the scenes to overcome web browsers' 6-connection limit (with HTTP/1) and
368
+ 100-connection limit (with HTTP/2). When you setup a server's `braidify` in
369
+ the recommended ways, you don't need to know it's happening — the abstraction
370
+ is completely transparent.
371
371
 
372
- ```javascript
373
- braid_fetch('/example', {multiplex: true, subscription: true})
374
- braid_fetch('/example', {multiplex: false, subscription: true})
375
- // or
376
- braid_fetch('/example', {multiplex: {after: 1}, subscription: true})
372
+ ```
373
+ // Recommendation #1: Wrapping the entire request handler
374
+ require('http').createServer(
375
+ braidify((req, res) => {
376
+ ...
377
+ })
378
+ )
377
379
  ```
378
380
 
379
- ### Server
380
-
381
- Configure mutliplexing with:
381
+ ```
382
+ // Recommendation #2: As middleware
383
+ var app = require('express')()
384
+ app.use(braidify)
385
+ ```
382
386
 
383
- ```javascript
384
- var braidify = require('braid-http').http-server
385
- nbraidify.enable_multiplex = true // or false
387
+ ```
388
+ // Recommendation #3: With braidify(req, res, next)
389
+ // (Equivalent to the middleware form.)
390
+ ...
391
+ braidify(req, res, next)
392
+ ...
386
393
  ```
387
394
 
388
- ## Testing
389
395
 
390
- Run all tests from the command line:
396
+ ### Inline `braidify(req, res)` leaks the abstraction
397
+
398
+ If you are using braidify from within a library, or in another context without
399
+ access to the entire request handler, or a `next()` method, then you can use
400
+ the inline form:
391
401
 
392
402
  ```
393
- npm test
403
+ (req, res) => {
404
+ ...
405
+ braidify(req, res); if (req.is_multiplexer) return
406
+ ...
407
+ })
394
408
  ```
395
409
 
396
- Run tests in a browser (auto-opens):
410
+ Just know that there are three abstraction leaks when using this form:
397
411
 
398
- ```
399
- npm run test:browser
400
- ```
412
+ 1. You must add `if (req.is_multiplexer) return` after
413
+ the `braidify(req, res)` call.
414
+ 2. The library will disable the [buffering
415
+ optimization](https://braid.org/protocol/multiplexing#buffering-optimization)
416
+ on optimistic multiplexer creation. This buffering prevents two minor
417
+ inconveniences that occur on about ~15% of page loads:
418
+ 1. One round trip of additional latency on the first subscription to a host
419
+ 2. A harmless `424` error in the javascript console, which can be safely
420
+ ignored:
421
+ ![424 error in browser console](https://braid.org/files/424-error.png)
401
422
 
402
- You can also filter tests by name:
423
+ The buffering works like this: when the client connects to a new host, it
424
+ sends a POST to create the multiplexer and GETs to subscribe — all in
425
+ parallel. Sometimes a GET arrives before the POST. With the recommended
426
+ forms, the server briefly buffers the GET (70ms, event-driven) until the
427
+ POST lands, then processes it normally. Without `next`, the server can't
428
+ re-run the handler, so it returns 424 immediately and the client retries.
429
+
430
+ ### Configuring multiplexing
431
+
432
+ You can tune multiplexing on the client, per-request or globally:
433
+
434
+ ```javascript
435
+ braid_fetch('/a', {multiplex: true}) // force on for this request
436
+ braid_fetch('/a', {multiplex: false}) // force off for this request
403
437
 
438
+ braid_fetch.enable_multiplex = true // on for all GETs
439
+ braid_fetch.enable_multiplex = false // off globally
440
+ braid_fetch.enable_multiplex = {after: 1} // on after N connections (default)
404
441
  ```
405
- node test/test.js --filter="version"
442
+
443
+ And on the server:
444
+
445
+ ```javascript
446
+ braidify.enable_multiplex = true // default; set false to disable
447
+ braidify.multiplex_wait = 10 // ms; timeout for the buffering optimization (default 10)
448
+ // set to 0 to disable buffering
406
449
  ```
407
450
 
@@ -1143,7 +1143,7 @@ async function create_multiplexer(origin, mux_key, params, mux_params, attempt)
1143
1143
  })()
1144
1144
 
1145
1145
  // return a "fetch" for this multiplexer
1146
- return async (url, params, mux_params, attempt) => {
1146
+ var f = async (url, params, mux_params, attempt) => {
1147
1147
 
1148
1148
  // if we already know the multiplexer is not working,
1149
1149
  // then fallback to normal fetch
@@ -1349,6 +1349,8 @@ async function create_multiplexer(origin, mux_key, params, mux_params, attempt)
1349
1349
  throw (e === 'retry' && e) || mux_error || e
1350
1350
  }
1351
1351
  }
1352
+ f.cleanup = () => cleanup_multiplexer(new Error('manual cleanup'))
1353
+ return f
1352
1354
  }
1353
1355
 
1354
1356
  // waits on reader for chunks like: 123 bytes for request ABC\r\n..123 bytes..
@@ -255,6 +255,39 @@ function warn_braidify_dupe (req) {
255
255
  }
256
256
  }
257
257
 
258
+ // Like setTimeout, but can be aborted in a batch (via batch_id) and calls
259
+ // on_abort on each timeout when aborted, instead of on_timeout.
260
+ function abortable_set_timeout(batch_id, on_timeout, on_abort, timeout_ms) {
261
+ if (!braidify.pending_timeouts)
262
+ braidify.pending_timeouts = new Map()
263
+
264
+ var timers = braidify.pending_timeouts.get(batch_id)
265
+ if (!timers) {
266
+ timers = new Set()
267
+ braidify.pending_timeouts.set(batch_id, timers)
268
+ }
269
+
270
+ var timer = { on_abort: on_abort }
271
+ timer.timeout = setTimeout(function() {
272
+ timers.delete(timer)
273
+ if (!timers.size)
274
+ braidify.pending_timeouts.delete(batch_id)
275
+ on_timeout()
276
+ }, timeout_ms)
277
+
278
+ timers.add(timer)
279
+ }
280
+ // Aborts an abortable_timeout created above.
281
+ function abort_timeouts(batch_id) {
282
+ var timers = braidify.pending_timeouts?.get(batch_id)
283
+ if (!timers) return
284
+ braidify.pending_timeouts.delete(batch_id)
285
+ for (var t of timers) {
286
+ clearTimeout(t.timeout)
287
+ t.on_abort()
288
+ }
289
+ }
290
+
258
291
 
259
292
  // The main server function!
260
293
  function braidify (req, res, next) {
@@ -266,10 +299,16 @@ function braidify (req, res, next) {
266
299
 
267
300
 
268
301
  // Guard against double-braidification.
269
- if (req._braidified) {
302
+ if (req._braidified && !req.reprocess_me) {
303
+ // If this was already braidified, then print a warning
270
304
  warn_braidify_dupe(req)
305
+ // and stop braidifying it any further
271
306
  return next?.()
272
307
  }
308
+ // But some potential 424 responses get delayed and reprocessed.
309
+ // So let's clear the reprocess_me flag on those, since we're doing it.
310
+ delete req.reprocess_me
311
+
273
312
 
274
313
  req._braidified = braidify_version
275
314
 
@@ -321,30 +360,30 @@ function braidify (req, res, next) {
321
360
  }
322
361
 
323
362
  // parse the multiplexer id and request id from the url
324
- var [multiplexer, request] = req.url.split('/').slice(req.method === 'MULTIPLEX' ? 1 : 3)
363
+ var [multiplexer_id, request_id] = req.url.split('/').slice(req.method === 'MULTIPLEX' ? 1 : 3)
325
364
 
326
365
  // if there's just a multiplexer, then we're creating a multiplexer..
327
- if (!request) {
366
+ if (!request_id) {
328
367
  // maintain a Map of all the multiplexers
329
368
  if (!braidify.multiplexers) braidify.multiplexers = new Map()
330
369
 
331
370
  // if this multiplexer already exists, respond with an error
332
- if (braidify.multiplexers.has(multiplexer)) {
371
+ if (braidify.multiplexers.has(multiplexer_id)) {
333
372
  res.writeHead(409, 'Conflict', {'Content-Type': 'application/json'})
334
373
  return res.end(JSON.stringify({
335
374
  error: 'Multiplexer already exists',
336
- details: `Cannot create duplicate multiplexer with ID '${multiplexer}'`
375
+ details: `Cannot create duplicate multiplexer with ID '${multiplexer_id}'`
337
376
  }))
338
377
  }
339
378
 
340
- braidify.multiplexers.set(multiplexer, {requests: new Map(), res})
379
+ braidify.multiplexers.set(multiplexer_id, {requests: new Map(), res})
341
380
 
342
381
  // Clean up multiplexer on error or close
343
382
  function cleanup() {
344
- var m = braidify.multiplexers.get(multiplexer)
345
- if (!m) return
346
- for (var f of m.requests.values()) f()
347
- braidify.multiplexers.delete(multiplexer)
383
+ var multiplexer = braidify.multiplexers.get(multiplexer_id)
384
+ if (!multiplexer) return
385
+ for (var f of multiplexer.requests.values()) f()
386
+ braidify.multiplexers.delete(multiplexer_id)
348
387
  }
349
388
  res.on('error', cleanup)
350
389
  res.on('close', cleanup)
@@ -361,27 +400,33 @@ function braidify (req, res, next) {
361
400
 
362
401
  // but write something.. won't interfere with multiplexer,
363
402
  // and helps flush the headers
364
- return res.write(`\r\n`)
403
+ res.write(`\r\n`)
404
+
405
+ // Notify any requests that arrived before this multiplexer
406
+ // was created. Must happen after writeHead so the POST's
407
+ // response is ready before waiters write through it.
408
+ abort_timeouts(multiplexer_id)
409
+ return
365
410
  } else {
366
411
  // in this case, we're closing the given request
367
412
 
368
413
  // if the multiplexer doesn't exist, send an error
369
- var m = braidify.multiplexers?.get(multiplexer)
370
- if (!m) {
371
- res.writeHead(404, 'Multiplexer no exist', {'Bad-Multiplexer': multiplexer})
372
- return res.end(`multiplexer ${multiplexer} does not exist`)
414
+ var multiplexer = braidify.multiplexers?.get(multiplexer_id)
415
+ if (!multiplexer) {
416
+ res.writeHead(404, 'Multiplexer no exist', {'Bad-Multiplexer': multiplexer_id})
417
+ return res.end(`multiplexer ${multiplexer_id} does not exist`)
373
418
  }
374
419
 
375
420
  // if the request doesn't exist, send an error
376
- let s = m.requests.get(request)
377
- if (!s) {
378
- res.writeHead(404, 'Multiplexed request not found', {'Bad-Request': request})
379
- return res.end(`request ${request} does not exist`)
421
+ let request_finisher = multiplexer.requests.get(request_id)
422
+ if (!request_finisher) {
423
+ res.writeHead(404, 'Multiplexed request not found', {'Bad-Request': request_id})
424
+ return res.end(`request ${request_id} does not exist`)
380
425
  }
381
426
 
382
427
  // remove this request, and notify it
383
- m.requests.delete(request)
384
- s()
428
+ multiplexer.requests.delete(request_id)
429
+ request_finisher()
385
430
 
386
431
  // let the requester know we succeeded
387
432
  res.writeHead(200, 'OK', { 'Multiplex-Version': multiplex_version })
@@ -397,21 +442,44 @@ function braidify (req, res, next) {
397
442
  req.headers['multiplex-version'] === multiplex_version) {
398
443
 
399
444
  // parse the multiplexer id and request id from the header
400
- var [multiplexer, request] = req.headers['multiplex-through'].split('/').slice(3)
445
+ var [multiplexer_id, request_id] = req.headers['multiplex-through'].split('/').slice(3)
401
446
 
402
447
  // find the multiplexer object (contains a response object)
403
- var m = braidify.multiplexers?.get(multiplexer)
404
- if (!m) {
405
- // free cors to multiplexer errors
448
+ var multiplexer = braidify.multiplexers?.get(multiplexer_id)
449
+ if (!multiplexer) {
450
+ if (braidify.multiplex_wait && next) {
451
+ // Wait for the multiplexer to be created.
452
+ // Handles the race where Multiplex-Through arrives
453
+ // before the POST that creates the multiplexer.
454
+ abortable_set_timeout(multiplexer_id,
455
+ function give_up () {
456
+ // Timed out — send 424
457
+ free_cors(res)
458
+ req.is_multiplexer = res.is_multiplexer = true
459
+ res.writeHead(424, 'Multiplexer not found',
460
+ {'Bad-Multiplexer': multiplexer_id})
461
+ res.end('multiplexer ' + multiplexer_id
462
+ + ' does not exist')
463
+ },
464
+ function ready_for_mux () {
465
+ // Multiplexer appeared — re-process the request
466
+ req.reprocess_me = true
467
+ braidify(req, res, next)
468
+ },
469
+ braidify.multiplex_wait)
470
+ return
471
+ }
472
+
406
473
  free_cors(res)
407
-
408
474
  req.is_multiplexer = res.is_multiplexer = true
409
- res.writeHead(424, 'Multiplexer no exist', {'Bad-Multiplexer': multiplexer})
410
- return res.end(`multiplexer ${multiplexer} does not exist`)
475
+ res.writeHead(424, 'Multiplexer not found',
476
+ {'Bad-Multiplexer': multiplexer_id})
477
+ return res.end('multiplexer ' + multiplexer_id
478
+ + ' does not exist')
411
479
  }
412
480
 
413
481
  // if this request-id already exists, respond with an error
414
- if (m.requests.has(request)) {
482
+ if (multiplexer.requests.has(request_id)) {
415
483
  // free cors to multiplexer errors
416
484
  free_cors(res)
417
485
 
@@ -420,13 +488,13 @@ function braidify (req, res, next) {
420
488
  return res.end(JSON.stringify({
421
489
  error: 'Request already multiplexed',
422
490
  details: `Cannot multiplex request with duplicate ID '`
423
- + request + `' for multiplexer '` + multiplexer + `'`
491
+ + request_id + `' for multiplexer '` + multiplexer_id + `'`
424
492
  }))
425
493
  }
426
494
 
427
- m.res.write(`start response ${request}\r\n`)
495
+ multiplexer.res.write(`start response ${request_id}\r\n`)
428
496
 
429
- // let the requester know we've multiplexed their response
497
+ // let the requester know we've multiplexed his response
430
498
  var og_stream = res.stream
431
499
  var og_socket = res.socket
432
500
  var og_res_end = () => {
@@ -472,10 +540,10 @@ function braidify (req, res, next) {
472
540
 
473
541
  // first we create a kind of fake socket
474
542
  class MultiplexedWritable extends require('stream').Writable {
475
- constructor(multiplexer, request) {
543
+ constructor(multiplexer, request_id) {
476
544
  super()
477
545
  this.multiplexer = multiplexer
478
- this.request = request
546
+ this.request_id = request_id
479
547
  }
480
548
 
481
549
  _write(chunk, encoding, callback) {
@@ -483,14 +551,14 @@ function braidify (req, res, next) {
483
551
 
484
552
  try {
485
553
  var len = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk, encoding)
486
- this.multiplexer.res.write(`${len} bytes for response ${this.request}\r\n`)
554
+ this.multiplexer.res.write(`${len} bytes for response ${this.request_id}\r\n`)
487
555
  this.multiplexer.res.write(chunk, encoding, callback)
488
556
  } catch (e) {
489
557
  callback(e)
490
558
  }
491
559
  }
492
560
  }
493
- var mw = new MultiplexedWritable(m, request)
561
+ var mw = new MultiplexedWritable(multiplexer, request_id)
494
562
  mw.on('error', () => {}) // EPIPE when client disconnects mid-stream
495
563
 
496
564
  // then we create a fake server response,
@@ -501,14 +569,14 @@ function braidify (req, res, next) {
501
569
 
502
570
  // register a handler for when the multiplexer closes,
503
571
  // to close our fake response
504
- m.requests.set(request, () => {
572
+ multiplexer.requests.set(request_id, () => {
505
573
  og_res_end?.()
506
574
  res2.destroy()
507
575
  })
508
576
 
509
577
  // when our fake response is done,
510
578
  // we want to send a special message to the multiplexer saying so
511
- res2.on('finish', () => m.res.write(`close response ${request}\r\n`))
579
+ res2.on('finish', () => multiplexer.res.write(`close response ${request_id}\r\n`))
512
580
 
513
581
  // copy over any headers which have already been set on res to res2
514
582
  for (let x of Object.entries(res.getHeaders()))
@@ -551,7 +619,7 @@ function braidify (req, res, next) {
551
619
  }
552
620
 
553
621
  // this is provided so code can know if the response has been multiplexed
554
- res.multiplexer = m.res
622
+ res.multiplexer = multiplexer.res
555
623
  }
556
624
 
557
625
  // Add the braidly request/response helper methods
@@ -803,6 +871,8 @@ function free_cors(res) {
803
871
  res.setHeader("Access-Control-Expose-Headers", "*")
804
872
  }
805
873
 
874
+ braidify.multiplex_wait = 10 // ms; set to 0 or false to disable
875
+
806
876
  module.exports = {
807
877
  braidify,
808
878
  free_cors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-http",
3
- "version": "1.3.103",
3
+ "version": "1.3.105",
4
4
  "description": "An implementation of Braid-HTTP for Node.js and Browsers",
5
5
  "scripts": {
6
6
  "test": "node test/test.js",