braid-http 1.3.16 → 1.3.18

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.
@@ -231,30 +231,61 @@ async function braid_fetch (url, params = {}) {
231
231
  var subscription_error = null
232
232
  var cb_running = false
233
233
 
234
+ // Multiplexing book-keeping;
235
+ // basically, if the user tries to make two or more subscriptions to the same origin,
236
+ // then we want to multiplex
237
+ var subscription_counts_on_close = null
238
+ if (params.headers.has('subscribe')) {
239
+ var origin = url[0] === '/' ? location.origin : new URL(url).origin
240
+ if (!braid_fetch.subscription_counts)
241
+ braid_fetch.subscription_counts = {}
242
+ braid_fetch.subscription_counts[origin] =
243
+ (braid_fetch.subscription_counts[origin] ?? 0) + 1
244
+
245
+ subscription_counts_on_close = () => {
246
+ subscription_counts_on_close = null
247
+ braid_fetch.subscription_counts[origin]--
248
+ if (!braid_fetch.subscription_counts[origin])
249
+ delete braid_fetch.subscription_counts[origin]
250
+ }
251
+ }
252
+
234
253
  return await new Promise((done, fail) => {
235
254
  connect()
236
255
  async function connect() {
256
+ // we direct all error paths here so we can make centralized retry decisions
237
257
  let on_error = e => {
238
258
  on_error = () => {}
239
259
 
240
- if (!params.retry ||
241
- e.name === "AbortError" ||
242
- e.startsWith?.('Parse error in headers') ||
243
- cb_running) {
244
-
260
+ // The fetch is probably down already, but there are some other errors that could have happened,
261
+ // and in those cases, we want to make sure to close the fetch
262
+ underlying_aborter?.abort()
263
+
264
+ // see if we should retry..
265
+ var retry = params.retry && // only try to reconnect if the user has chosen to
266
+ e.name !== "AbortError" && // don't retry if the user has chosen to abort
267
+ !e.startsWith?.('Parse error in headers') && // in this case, the server is spewing garbage, so reconnecting might be bad
268
+ !cb_running // if an error is thrown in the callback, then it may not be good to reconnect, and generate more errors
269
+
270
+ if (retry && !original_signal?.aborted) {
271
+ // retry after some time..
272
+ console.log(`retrying in ${waitTime}s: ${url} after error: ${e}`)
273
+ setTimeout(connect, waitTime * 1000)
274
+ waitTime = Math.min(waitTime + 1, 3)
275
+ } else {
276
+ // if we would have retried except that original_signal?.aborted,
277
+ // then we want to return that as the error..
278
+ if (retry && original_signal?.aborted) e = create_abort_error('already aborted')
279
+
280
+ // let people know things are shutting down..
281
+ subscription_counts_on_close?.()
245
282
  subscription_error?.(e)
246
283
  return fail(e)
247
284
  }
248
-
249
- underlying_aborter.abort()
250
-
251
- console.log(`retrying in ${waitTime}s: ${url} after error: ${e}`)
252
- setTimeout(connect, waitTime * 1000)
253
- waitTime = Math.min(waitTime + 1, 3)
254
285
  }
255
286
 
256
287
  try {
257
- if (original_signal?.aborted) throw new DOMException('already aborted', 'AbortError')
288
+ if (original_signal?.aborted) throw create_abort_error('already aborted')
258
289
 
259
290
  // We need a fresh underlying abort controller each time we connect
260
291
  underlying_aborter = new AbortController()
@@ -273,7 +304,34 @@ async function braid_fetch (url, params = {}) {
273
304
  params.onFetch?.(url, params)
274
305
 
275
306
  // Now we run the original fetch....
276
- res = await normal_fetch(url, params)
307
+
308
+ // try multiplexing if either of these is true:
309
+ // - they explicitly want multiplexing
310
+ // - this is not the first subscription to the same origin
311
+ if (braid_fetch.use_multiplexing &&
312
+ (params.headers.has('multiplexer') ||
313
+ (params.headers.has('subscribe') &&
314
+ braid_fetch.subscription_counts?.[origin] > 1))) {
315
+
316
+ // invent a new multiplexer and stream id
317
+ // if not provided
318
+ if (!params.headers.has('multiplexer')) {
319
+ // we want to keep the same multiplexer id for each origin
320
+ if (!braid_fetch.multiplexers)
321
+ braid_fetch.multiplexers = {}
322
+ if (!braid_fetch.multiplexers[origin])
323
+ braid_fetch.multiplexers[origin] =
324
+ Math.random().toString(36).slice(2)
325
+
326
+ // the stream id is different each time
327
+ var stream = Math.random().toString(36).slice(2)
328
+ params.headers.set('multiplexer',
329
+ `/${braid_fetch.multiplexers[origin]}/${stream}`)
330
+ }
331
+ res = await multiplex_fetch(url, params)
332
+ } else {
333
+ res = await normal_fetch(url, params)
334
+ }
277
335
 
278
336
  // And customize the response with a couple methods for getting
279
337
  // the braid subscription data:
@@ -318,7 +376,7 @@ async function braid_fetch (url, params = {}) {
318
376
  async (result, err) => {
319
377
  if (!err) {
320
378
  // check whether we aborted
321
- if (original_signal?.aborted) throw new DOMException('already aborted', 'AbortError')
379
+ if (original_signal?.aborted) throw create_abort_error('already aborted')
322
380
 
323
381
  // Yay! We got a new version! Tell the callback!
324
382
  cb_running = true
@@ -444,6 +502,8 @@ async function handle_fetch_stream (stream, cb, on_bytes) {
444
502
 
445
503
  // Tell the parser to process some more stream
446
504
  await parser.read(value)
505
+ if (parser.state.result === 'error')
506
+ return await cb(null, new Error(parser.state.message))
447
507
  }
448
508
  }
449
509
 
@@ -505,15 +565,15 @@ var subscription_parser = (cb) => ({
505
565
  })
506
566
  }
507
567
 
568
+ // Reset the parser for the next version!
569
+ this.state = {input: this.state.input}
570
+
508
571
  try {
509
572
  await this.cb(update)
510
573
  } catch (e) {
511
574
  await this.cb(null, e)
512
575
  return
513
576
  }
514
-
515
- // Reset the parser for the next version!
516
- this.state = {input: this.state.input}
517
577
  }
518
578
 
519
579
  // Or maybe there's an error to report upstream
@@ -588,7 +648,8 @@ function parse_headers (input) {
588
648
  }
589
649
 
590
650
  // Extract the header string
591
- var headers_source = input.slice(start, end).map(x => String.fromCharCode(x)).join('')
651
+ var headers_source = input.slice(start, end)
652
+ headers_source = Array.isArray(headers_source) ? headers_source.map(x => String.fromCharCode(x)).join('') : new TextDecoder().decode(headers_source)
592
653
 
593
654
  // Convert "HTTP 200 OK" to a :status: 200 header
594
655
  headers_source = headers_source.replace(/^HTTP\/?\d*\.?\d* (\d\d\d).*\r?\n/,
@@ -779,6 +840,261 @@ function parse_body (state) {
779
840
  }
780
841
  }
781
842
 
843
+ // multiplex_fetch provides a fetch-like experience for HTTP requests
844
+ // where the result is actually being sent over a separate multiplexed connection.
845
+ //
846
+ // This function assumes a header in params called 'multiplexer' with a value
847
+ // that looks like /multiplexer_id/stream_id. It creates a multiplexer if it
848
+ // doesn't exist already, then performs a fetch providing the multiplexer header.
849
+ // This tells the server to send the results to the given multiplexer.
850
+ //
851
+ async function multiplex_fetch(url, params) {
852
+ // extract multiplexer id from the header
853
+ var multiplexer = params.headers.get('multiplexer').split('/')[1]
854
+
855
+ // create a new multiplexer if it doesn't exist
856
+ if (!multiplex_fetch.multiplexers) multiplex_fetch.multiplexers = {}
857
+ if (!multiplex_fetch.multiplexers[multiplexer]) multiplex_fetch.multiplexers[multiplexer] = (async () => {
858
+ var origin = url[0] === '/' ? location.origin : new URL(url).origin
859
+
860
+ // attempt to establish a multiplexed connection
861
+ try {
862
+ if (braid_fetch.use_multiplexing === 'USE GET')
863
+ var r = await braid_fetch(`${origin}/MULTIPLEX/${multiplexer}`, {retry: true})
864
+ else
865
+ var r = await braid_fetch(`${origin}/${multiplexer}`, {method: 'MULTIPLEX', retry: true})
866
+ } catch (e) {
867
+ // fallback to normal fetch if multiplexed connection fails
868
+ console.error(`Could not establish multiplexed connection.\nGot error: ${e}.\nFalling back to normal connection.`)
869
+ return (url, params) => {
870
+ params.headers.delete('multiplexer')
871
+ return normal_fetch(url, params)
872
+ }
873
+ }
874
+
875
+ // parse the multiplexed stream,
876
+ // and send messages to the appropriate streams
877
+ var streams = new Map()
878
+ var mux_error = null
879
+ parse_multiplex_stream(r.body.getReader(), (stream, bytes) => {
880
+ streams.get(stream)?.(bytes)
881
+ }, e => {
882
+ // the multiplexer stream has died.. let everyone know..
883
+ mux_error = e
884
+ for (var f of streams.values()) f()
885
+ delete multiplex_fetch.multiplexers[multiplexer]
886
+ })
887
+
888
+ // return a "fetch" for this multiplexer
889
+ return async (url, params) => {
890
+ // extract stream id from the header
891
+ var stream = params.headers.get('multiplexer').split('/')[2]
892
+
893
+ // setup a way to receive incoming data from the multiplexer
894
+ var buffers = []
895
+ var bytes_available = () => {}
896
+ var stream_error = null
897
+
898
+ // this utility calls the callback whenever new data is available to process
899
+ async function process_buffers(cb) {
900
+ while (true) {
901
+ // wait for data if none is available
902
+ if (!mux_error && !stream_error && !buffers.length)
903
+ await new Promise(done => bytes_available = done)
904
+ if (mux_error || stream_error) throw (mux_error || stream_error)
905
+
906
+ // process the data
907
+ let ret = cb()
908
+ if (ret) return ret
909
+ }
910
+ }
911
+
912
+ // tell the multiplexer to send bytes for this stream to us
913
+ streams.set(stream, bytes => {
914
+ if (!bytes) {
915
+ streams.delete(stream)
916
+ buffers.push(bytes)
917
+ } else if (!mux_error) buffers.push(bytes)
918
+ bytes_available()
919
+ })
920
+
921
+ // prepare a function that we'll call to cleanly tear things down
922
+ var unset = async e => {
923
+ unset = () => {}
924
+ streams.delete(stream)
925
+ stream_error = e
926
+ bytes_available()
927
+ try {
928
+ if (braid_fetch.use_multiplexing === 'USE GET')
929
+ await braid_fetch(`${origin}/MULTIPLEX${params.headers.get('multiplexer')}`, {retry: true})
930
+ else
931
+ await braid_fetch(`${origin}${params.headers.get('multiplexer')}`, {method: 'MULTIPLEX', retry: true})
932
+ } catch (e) {
933
+ console.error(`Could not cancel multiplexed connection:`, e)
934
+ throw e
935
+ }
936
+ }
937
+
938
+ // do the underlying fetch
939
+ try {
940
+ var res = await normal_fetch(url, params)
941
+ if (res.status !== 293) throw new Error('Could not establish multiplexed stream ' + params.headers.get('multiplexer') + ' got status: ' + res.status)
942
+
943
+ // we want to present the illusion that the connection is still open,
944
+ // and therefor closable with "abort",
945
+ // so we handle the abort ourselves to close the multiplexed stream
946
+ params.signal?.addEventListener('abort', () =>
947
+ unset(create_abort_error('stream aborted')))
948
+
949
+ // first, we need to listen for the headers..
950
+ var headers_buffer = new Uint8Array()
951
+ var parsed_headers = await process_buffers(() => {
952
+ // check if the stream has been closed
953
+ var stream_ended = !buffers[buffers.length - 1]
954
+ if (stream_ended) buffers.pop()
955
+
956
+ // aggregate all the new buffers into our big headers_buffer
957
+ headers_buffer = concat_buffers([headers_buffer, ...buffers])
958
+ buffers = []
959
+
960
+ // and if the stream had ended, put that information back
961
+ if (stream_ended) buffers.push(null)
962
+
963
+ // try parsing what we got so far as headers..
964
+ var x = parse_headers(headers_buffer)
965
+
966
+ // how did it go?
967
+ if (x.result === 'error') {
968
+ // if we got an error, give up
969
+ console.log(`headers_buffer: ` + new TextDecoder().decode(headers_buffer))
970
+ throw new Error('error parsing headers')
971
+ } else if (x.result === 'waiting') {
972
+ if (stream_ended) throw new Error('Multiplexed stream ended before headers received.')
973
+ } else return x
974
+ })
975
+
976
+ // put the bytes left over from the header back
977
+ if (parsed_headers.input.length) buffers.unshift(parsed_headers.input)
978
+
979
+ // these headers will also have the status,
980
+ // but we want to present the status in a more usual way below
981
+ var status = parsed_headers.headers[':status']
982
+ delete parsed_headers.headers[':status']
983
+
984
+ // create our own fake response object,
985
+ // to mimik fetch's response object,
986
+ // feeding the user our stream data from the multiplexer
987
+ var res = new Response(new ReadableStream({
988
+ async start(controller) {
989
+ try {
990
+ await process_buffers(() => {
991
+ var b = buffers.shift()
992
+ if (!b) return true
993
+ controller.enqueue(b)
994
+ })
995
+ } finally { controller.close() }
996
+ }
997
+ }), {
998
+ status,
999
+ headers: parsed_headers.headers
1000
+ })
1001
+
1002
+ // add a convenience property for the user to know if
1003
+ // this response is being multiplexed
1004
+ res.multiplexer = params.headers.get('multiplexer')
1005
+
1006
+ // return the fake response object
1007
+ return res
1008
+ } catch (e) {
1009
+ // if we had an error, be sure to unregister ourselves
1010
+ await unset(e)
1011
+ throw e
1012
+ }
1013
+ }
1014
+ })()
1015
+
1016
+ // call the special fetch function for the multiplexer
1017
+ return await (await multiplex_fetch.multiplexers[multiplexer])(url, params)
1018
+ }
1019
+
1020
+ // waits on reader for chunks like: 123 bytes for stream ABC\r\n..123 bytes..
1021
+ // which would trigger cb("ABC", bytes)
1022
+ async function parse_multiplex_stream(reader, cb, on_error) {
1023
+ try {
1024
+ var buffers = [new Uint8Array(0)]
1025
+ var buffers_size = 0
1026
+ var chunk_size = null
1027
+ var stream_id = null
1028
+ var header_length = 0
1029
+ var header_started = false
1030
+
1031
+ while (true) {
1032
+ var { done, value } = await reader.read()
1033
+ if (done) throw new Error('multiplex stream ended unexpectedly')
1034
+ buffers.push(value)
1035
+ buffers_size += value.length
1036
+
1037
+ while (true) {
1038
+ if (chunk_size === null && buffers_size) {
1039
+ if (buffers.length > 1) buffers = [concat_buffers(buffers)]
1040
+
1041
+ var headerComplete = false
1042
+ while (buffers[0].length > header_length) {
1043
+ const byte = buffers[0][header_length]
1044
+ header_length++
1045
+
1046
+ if (byte !== 13 && byte !== 10) header_started = true
1047
+ if (header_started && byte === 10) {
1048
+ headerComplete = true
1049
+ break
1050
+ }
1051
+ }
1052
+ if (headerComplete) {
1053
+ var headerStr = new TextDecoder().decode(buffers[0].slice(0, header_length))
1054
+ var m = headerStr.match(/^[\r\n]*((\d+) bytes for|close) stream ([A-Za-z0-9_-]+)\r\n$/)
1055
+ if (!m) throw new Error('invalid multiplex header')
1056
+ stream_id = m[3]
1057
+
1058
+ buffers[0] = buffers[0].slice(header_length)
1059
+ buffers_size -= header_length
1060
+
1061
+ if (m[1] === 'close') {
1062
+ cb(stream_id)
1063
+ break
1064
+ } else chunk_size = 1 * m[2]
1065
+ } else break
1066
+ } else if (chunk_size !== null && buffers_size >= chunk_size) {
1067
+ if (buffers.length > 1) buffers = [concat_buffers(buffers)]
1068
+
1069
+ var chunk = buffers[0].slice(0, chunk_size)
1070
+ buffers[0] = buffers[0].slice(chunk_size)
1071
+ buffers_size -= chunk_size
1072
+
1073
+ // console.log(`stream_id: ${stream_id}, ${new TextDecoder().decode(chunk)}`)
1074
+
1075
+ cb(stream_id, chunk)
1076
+
1077
+ chunk_size = null
1078
+ stream_id = null
1079
+ header_length = 0
1080
+ header_started = false
1081
+ } else break
1082
+ }
1083
+ }
1084
+ } catch (e) { on_error(e) }
1085
+ }
1086
+
1087
+ // concatenates an array of Uint8Array's, into a single one
1088
+ function concat_buffers(buffers) {
1089
+ const x = new Uint8Array(buffers.reduce((a, b) => a + b.length, 0))
1090
+ let offset = 0
1091
+ for (const b of buffers) {
1092
+ x.set(b, offset)
1093
+ offset += b.length
1094
+ }
1095
+ return x
1096
+ }
1097
+
782
1098
  // The "extra_headers" field is returned to the client on any *update* or
783
1099
  // *patch* to include any headers that we've received, but don't have braid
784
1100
  // semantics for.
@@ -820,6 +1136,12 @@ function ascii_ify(s) {
820
1136
  return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
821
1137
  }
822
1138
 
1139
+ function create_abort_error(msg) {
1140
+ var e = new Error(msg)
1141
+ e.name = 'AbortError'
1142
+ return e
1143
+ }
1144
+
823
1145
  // ****************************
824
1146
  // Exports
825
1147
  // ****************************
@@ -230,8 +230,7 @@ function braidify (req, res, next) {
230
230
  // Extract braid info from headers
231
231
  var version = ('version' in req.headers) && JSON.parse('['+req.headers.version+']'),
232
232
  parents = ('parents' in req.headers) && JSON.parse('['+req.headers.parents+']'),
233
- peer = req.headers['peer'],
234
- url = req.url.substr(1)
233
+ peer = req.headers['peer']
235
234
 
236
235
  // Parse the subscribe header
237
236
  var subscribe = req.headers.subscribe
@@ -243,6 +242,152 @@ function braidify (req, res, next) {
243
242
  req.parents = parents
244
243
  req.subscribe = subscribe
245
244
 
245
+ // Multiplexer stuff
246
+ if ((braidify.use_multiplexing && req.method === 'MULTIPLEX') ||
247
+ (braidify.use_multiplexing === 'USE GET' && req.url.startsWith('/MULTIPLEX/'))) {
248
+ // parse the multiplexer id and stream id from the url
249
+ var [multiplexer, stream] = req.url.slice(1).replace(/^MULTIPLEX\//, '').split('/')
250
+
251
+ // if there's just a multiplexer, then we're creating a multiplexer..
252
+ if (!stream) {
253
+ // maintain a Map of all the multiplexers
254
+ if (!braidify.multiplexers) braidify.multiplexers = new Map()
255
+ braidify.multiplexers.set(multiplexer, {streams: new Map(), res})
256
+
257
+ // when the response closes,
258
+ // let everyone know the multiplexer has died
259
+ res.on('close', () => {
260
+ for (var f of braidify.multiplexers.get(multiplexer).streams.values()) f()
261
+ braidify.multiplexers.delete(multiplexer)
262
+ })
263
+
264
+ // keep the connection open,
265
+ // so people can send multiplexed data to it
266
+ res.writeHead(200, {
267
+ 'Cache-Control': 'no-cache',
268
+ 'X-Accel-Buffering': 'no',
269
+ ...req.httpVersion !== '2.0' && {'Connection': 'keep-alive'}
270
+ })
271
+
272
+ // but write something.. won't interfere with stream,
273
+ // and helps flush the headers
274
+ return res.write(`\r\n`)
275
+ } else {
276
+ // in this case, we're closing the given stream
277
+ var m = braidify.multiplexers?.get(multiplexer)
278
+
279
+ // if the multiplexer doesn't exist, send an error
280
+ if (!m) {
281
+ var msg = `multiplexer ${multiplexer} does not exist`
282
+ res.writeHead(400, {'Content-Type': 'text/plain'})
283
+ res.end(msg)
284
+ return
285
+ }
286
+
287
+ // remove this stream, and notify it
288
+ let s = m.streams.get(stream)
289
+ if (s) {
290
+ s()
291
+ m.streams.delete(stream)
292
+ } else m.streams.set(stream, 'abort')
293
+
294
+ // let the requester know we succeeded
295
+ res.writeHead(200, {})
296
+ return res.end(``)
297
+ }
298
+ }
299
+
300
+ // a multiplexer header means the user wants to send the
301
+ // results of this request to the provided multiplexer,
302
+ // tagged with the given stream id
303
+ if (braidify.use_multiplexing && req.headers.multiplexer) {
304
+ // parse the multiplexer id and stream id from the url
305
+ var [multiplexer, stream] = req.headers.multiplexer.slice(1).split('/')
306
+
307
+ var end_things = (msg) => {
308
+ res.statusCode = 400
309
+ res.end(msg)
310
+ }
311
+
312
+ // find the multiplexer object (contains a response object)
313
+ var m = braidify.multiplexers?.get(multiplexer)
314
+ if (!m) return end_things(`multiplexer ${multiplexer} does not exist`)
315
+
316
+ // special case: check that this stream isn't already aborted
317
+ if (m.streams.get(stream) === 'abort') {
318
+ m.streams.delete(stream)
319
+ return end_things(`multiplexer stream ${req.headers.multiplexer} already aborted`)
320
+ }
321
+
322
+ // let the requester know we've multiplexed their response
323
+ res.writeHead(293, {multiplexer: req.headers.multiplexer})
324
+ res.end('Ok.')
325
+
326
+ // and now set things up so that future use of the
327
+ // response object forwards stuff into the multiplexer
328
+
329
+ // first we create a kind of fake socket
330
+ class MultiplexedWritable extends require('stream').Writable {
331
+ constructor(multiplexer, stream) {
332
+ super()
333
+ this.multiplexer = multiplexer
334
+ this.stream = stream
335
+ }
336
+
337
+ _write(chunk, encoding, callback) {
338
+ var len = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk, encoding)
339
+ this.multiplexer.res.write(`${len} bytes for stream ${this.stream}\r\n`)
340
+ this.multiplexer.res.write(chunk, encoding, callback)
341
+
342
+ // console.log(`wrote:`)
343
+ // console.log(`${len} bytes for stream /${this.stream}\r\n`)
344
+ // if (Buffer.isBuffer(chunk)) console.log(new TextDecoder().decode(chunk))
345
+ // else console.log('STRING?: ' + chunk)
346
+ }
347
+ }
348
+ var mw = new MultiplexedWritable(m, stream)
349
+
350
+ // then we create a fake server response,
351
+ // that pipes data to our fake socket
352
+ var res2 = new (require('http').ServerResponse)({})
353
+ res2.useChunkedEncodingByDefault = false
354
+ res2.assignSocket(mw)
355
+
356
+ // register a handler for when the multiplexer closes,
357
+ // to close our fake response stream
358
+ m.streams.set(stream, () => res2.destroy())
359
+
360
+ // when our fake response is done,
361
+ // we want to send a special message to the multiplexer saying so
362
+ res2.on('finish', () => m.res.write(`close stream ${stream}\r\n`))
363
+
364
+ // we want access to "res" to be forwarded to our fake "res2",
365
+ // so that it goes into the multiplexer
366
+ function* get_props(obj) {
367
+ do {
368
+ for (var x of Object.getOwnPropertyNames(obj)) yield x
369
+ } while (obj = Object.getPrototypeOf(obj))
370
+ }
371
+ for (let key of get_props(res)) {
372
+ if (key === '_events' || key === 'emit') continue
373
+ if (res2[key] === undefined) continue
374
+ var value = res[key]
375
+ if (typeof value === 'function') {
376
+ res[key] = res2[key].bind(res2)
377
+ } else {
378
+ +((key) => {
379
+ Object.defineProperty(res, key, {
380
+ get: () => res2[key],
381
+ set: x => res2[key] = x
382
+ })
383
+ })(key)
384
+ }
385
+ }
386
+
387
+ // this is provided so code can know if the response has been multiplexed
388
+ res.multiplexer = res2
389
+ }
390
+
246
391
  // Add the braidly request/response helper methods
247
392
  res.sendUpdate = (stuff) => send_update(res, stuff, req.url, peer)
248
393
  res.sendVersion = res.sendUpdate
@@ -275,7 +420,7 @@ function braidify (req, res, next) {
275
420
 
276
421
  // We have a subscription!
277
422
  res.statusCode = 209
278
- res.setHeader("subscribe", req.headers.subscribe)
423
+ res.setHeader("subscribe", req.headers.subscribe ?? 'true')
279
424
  res.setHeader('cache-control', 'no-cache, no-transform')
280
425
 
281
426
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-http",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
4
4
  "description": "An implementation of Braid-HTTP for Node.js and Browsers",
5
5
  "scripts": {
6
6
  "test": "node test/server.js"