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.
- package/braid-http-client.js +340 -18
- package/braid-http-server.js +148 -3
- package/package.json +1 -1
package/braid-http-client.js
CHANGED
|
@@ -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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
// ****************************
|
package/braid-http-server.js
CHANGED
|
@@ -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
|
|