braid-http 1.3.15 → 1.3.17
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 +350 -31
- package/braid-http-server.js +147 -3
- package/package.json +1 -1
package/braid-http-client.js
CHANGED
|
@@ -64,16 +64,19 @@ function braidify_http (http) {
|
|
|
64
64
|
on_update = f
|
|
65
65
|
|
|
66
66
|
// And set up a subscription parser
|
|
67
|
-
var parser = subscription_parser((update, error) => {
|
|
67
|
+
var parser = subscription_parser(async (update, error) => {
|
|
68
68
|
if (!error)
|
|
69
|
-
on_update && on_update(update)
|
|
69
|
+
on_update && (await on_update(update))
|
|
70
70
|
else
|
|
71
71
|
on_error && on_error(error)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
// That will run each time we get new data
|
|
75
|
+
var chain = Promise.resolve()
|
|
75
76
|
res.orig_on('data', (chunk) => {
|
|
76
|
-
|
|
77
|
+
chain = chain.then(async () => {
|
|
78
|
+
await parser.read(chunk)
|
|
79
|
+
})
|
|
77
80
|
})
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -228,30 +231,61 @@ async function braid_fetch (url, params = {}) {
|
|
|
228
231
|
var subscription_error = null
|
|
229
232
|
var cb_running = false
|
|
230
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
|
+
|
|
231
253
|
return await new Promise((done, fail) => {
|
|
232
254
|
connect()
|
|
233
255
|
async function connect() {
|
|
256
|
+
// we direct all error paths here so we can make centralized retry decisions
|
|
234
257
|
let on_error = e => {
|
|
235
258
|
on_error = () => {}
|
|
236
259
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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?.()
|
|
242
282
|
subscription_error?.(e)
|
|
243
283
|
return fail(e)
|
|
244
284
|
}
|
|
245
|
-
|
|
246
|
-
underlying_aborter.abort()
|
|
247
|
-
|
|
248
|
-
console.log(`retrying in ${waitTime}s: ${url} after error: ${e}`)
|
|
249
|
-
setTimeout(connect, waitTime * 1000)
|
|
250
|
-
waitTime = Math.min(waitTime + 1, 3)
|
|
251
285
|
}
|
|
252
286
|
|
|
253
287
|
try {
|
|
254
|
-
if (original_signal?.aborted) throw
|
|
288
|
+
if (original_signal?.aborted) throw create_abort_error('already aborted')
|
|
255
289
|
|
|
256
290
|
// We need a fresh underlying abort controller each time we connect
|
|
257
291
|
underlying_aborter = new AbortController()
|
|
@@ -270,7 +304,34 @@ async function braid_fetch (url, params = {}) {
|
|
|
270
304
|
params.onFetch?.(url, params)
|
|
271
305
|
|
|
272
306
|
// Now we run the original fetch....
|
|
273
|
-
|
|
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
|
+
}
|
|
274
335
|
|
|
275
336
|
// And customize the response with a couple methods for getting
|
|
276
337
|
// the braid subscription data:
|
|
@@ -312,14 +373,14 @@ async function braid_fetch (url, params = {}) {
|
|
|
312
373
|
|
|
313
374
|
// Each time something happens, we'll either get a new
|
|
314
375
|
// version back, or an error.
|
|
315
|
-
(result, err) => {
|
|
376
|
+
async (result, err) => {
|
|
316
377
|
if (!err) {
|
|
317
378
|
// check whether we aborted
|
|
318
|
-
if (original_signal?.aborted) throw
|
|
379
|
+
if (original_signal?.aborted) throw create_abort_error('already aborted')
|
|
319
380
|
|
|
320
381
|
// Yay! We got a new version! Tell the callback!
|
|
321
382
|
cb_running = true
|
|
322
|
-
cb(result)
|
|
383
|
+
await cb(result)
|
|
323
384
|
cb_running = false
|
|
324
385
|
} else
|
|
325
386
|
// This error handling code runs if the connection
|
|
@@ -426,21 +487,23 @@ async function handle_fetch_stream (stream, cb, on_bytes) {
|
|
|
426
487
|
var {done, value} = await reader.read()
|
|
427
488
|
}
|
|
428
489
|
catch (e) {
|
|
429
|
-
cb(null, e)
|
|
490
|
+
await cb(null, e)
|
|
430
491
|
return
|
|
431
492
|
}
|
|
432
493
|
|
|
433
494
|
// Check if this connection has been closed!
|
|
434
495
|
if (done) {
|
|
435
496
|
console.debug("Connection closed.")
|
|
436
|
-
cb(null, 'Connection closed')
|
|
497
|
+
await cb(null, 'Connection closed')
|
|
437
498
|
return
|
|
438
499
|
}
|
|
439
500
|
|
|
440
501
|
on_bytes?.(value)
|
|
441
502
|
|
|
442
503
|
// Tell the parser to process some more stream
|
|
443
|
-
parser.read(value)
|
|
504
|
+
await parser.read(value)
|
|
505
|
+
if (parser.state.result === 'error')
|
|
506
|
+
return await cb(null, new Error(parser.state.message))
|
|
444
507
|
}
|
|
445
508
|
}
|
|
446
509
|
|
|
@@ -459,7 +522,7 @@ var subscription_parser = (cb) => ({
|
|
|
459
522
|
|
|
460
523
|
// You give it new input information as soon as you get it, and it will
|
|
461
524
|
// report back with new versions as soon as it finds them.
|
|
462
|
-
read (input) {
|
|
525
|
+
async read (input) {
|
|
463
526
|
|
|
464
527
|
// Store the new input!
|
|
465
528
|
for (let x of input) this.state.input.push(x)
|
|
@@ -471,7 +534,7 @@ var subscription_parser = (cb) => ({
|
|
|
471
534
|
try {
|
|
472
535
|
this.state = parse_update (this.state)
|
|
473
536
|
} catch (e) {
|
|
474
|
-
this.cb(null, e)
|
|
537
|
+
await this.cb(null, e)
|
|
475
538
|
return
|
|
476
539
|
}
|
|
477
540
|
|
|
@@ -502,20 +565,20 @@ var subscription_parser = (cb) => ({
|
|
|
502
565
|
})
|
|
503
566
|
}
|
|
504
567
|
|
|
568
|
+
// Reset the parser for the next version!
|
|
569
|
+
this.state = {input: this.state.input}
|
|
570
|
+
|
|
505
571
|
try {
|
|
506
|
-
this.cb(update)
|
|
572
|
+
await this.cb(update)
|
|
507
573
|
} catch (e) {
|
|
508
|
-
this.cb(null, e)
|
|
574
|
+
await this.cb(null, e)
|
|
509
575
|
return
|
|
510
576
|
}
|
|
511
|
-
|
|
512
|
-
// Reset the parser for the next version!
|
|
513
|
-
this.state = {input: this.state.input}
|
|
514
577
|
}
|
|
515
578
|
|
|
516
579
|
// Or maybe there's an error to report upstream
|
|
517
580
|
else if (this.state.result === 'error') {
|
|
518
|
-
this.cb(null, this.state.message)
|
|
581
|
+
await this.cb(null, this.state.message)
|
|
519
582
|
return
|
|
520
583
|
}
|
|
521
584
|
|
|
@@ -585,7 +648,8 @@ function parse_headers (input) {
|
|
|
585
648
|
}
|
|
586
649
|
|
|
587
650
|
// Extract the header string
|
|
588
|
-
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)
|
|
589
653
|
|
|
590
654
|
// Convert "HTTP 200 OK" to a :status: 200 header
|
|
591
655
|
headers_source = headers_source.replace(/^HTTP\/?\d*\.?\d* (\d\d\d).*\r?\n/,
|
|
@@ -776,6 +840,255 @@ function parse_body (state) {
|
|
|
776
840
|
}
|
|
777
841
|
}
|
|
778
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
|
+
var r = await braid_fetch(`${origin}/${multiplexer}`, {method: 'MULTIPLEX', retry: true})
|
|
863
|
+
} catch (e) {
|
|
864
|
+
// fallback to normal fetch if multiplexed connection fails
|
|
865
|
+
console.error(`Could not establish multiplexed connection.\nGot error: ${e}.\nFalling back to normal connection.`)
|
|
866
|
+
return (url, params) => {
|
|
867
|
+
params.headers.delete('multiplexer')
|
|
868
|
+
return normal_fetch(url, params)
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// parse the multiplexed stream,
|
|
873
|
+
// and send messages to the appropriate streams
|
|
874
|
+
var streams = new Map()
|
|
875
|
+
var mux_error = null
|
|
876
|
+
parse_multiplex_stream(r.body.getReader(), (stream, bytes) => {
|
|
877
|
+
streams.get(stream)?.(bytes)
|
|
878
|
+
}, e => {
|
|
879
|
+
// the multiplexer stream has died.. let everyone know..
|
|
880
|
+
mux_error = e
|
|
881
|
+
for (var f of streams.values()) f()
|
|
882
|
+
delete multiplex_fetch.multiplexers[multiplexer]
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
// return a "fetch" for this multiplexer
|
|
886
|
+
return async (url, params) => {
|
|
887
|
+
// extract stream id from the header
|
|
888
|
+
var stream = params.headers.get('multiplexer').split('/')[2]
|
|
889
|
+
|
|
890
|
+
// setup a way to receive incoming data from the multiplexer
|
|
891
|
+
var buffers = []
|
|
892
|
+
var bytes_available = () => {}
|
|
893
|
+
var stream_error = null
|
|
894
|
+
|
|
895
|
+
// this utility calls the callback whenever new data is available to process
|
|
896
|
+
async function process_buffers(cb) {
|
|
897
|
+
while (true) {
|
|
898
|
+
// wait for data if none is available
|
|
899
|
+
if (!mux_error && !stream_error && !buffers.length)
|
|
900
|
+
await new Promise(done => bytes_available = done)
|
|
901
|
+
if (mux_error || stream_error) throw (mux_error || stream_error)
|
|
902
|
+
|
|
903
|
+
// process the data
|
|
904
|
+
let ret = cb()
|
|
905
|
+
if (ret) return ret
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// tell the multiplexer to send bytes for this stream to us
|
|
910
|
+
streams.set(stream, bytes => {
|
|
911
|
+
if (!bytes) {
|
|
912
|
+
streams.delete(stream)
|
|
913
|
+
buffers.push(bytes)
|
|
914
|
+
} else if (!mux_error) buffers.push(bytes)
|
|
915
|
+
bytes_available()
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
// prepare a function that we'll call to cleanly tear things down
|
|
919
|
+
var unset = async e => {
|
|
920
|
+
unset = () => {}
|
|
921
|
+
streams.delete(stream)
|
|
922
|
+
stream_error = e
|
|
923
|
+
bytes_available()
|
|
924
|
+
try {
|
|
925
|
+
await braid_fetch(`${origin}${params.headers.get('multiplexer')}`, {method: 'MULTIPLEX', retry: true})
|
|
926
|
+
} catch (e) {
|
|
927
|
+
console.error(`Could not cancel multiplexed connection:`, e)
|
|
928
|
+
throw e
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// do the underlying fetch
|
|
933
|
+
try {
|
|
934
|
+
var res = await normal_fetch(url, params)
|
|
935
|
+
if (res.status !== 293) throw new Error('Could not establish multiplexed stream ' + params.headers.get('multiplexer') + ' got status: ' + res.status)
|
|
936
|
+
|
|
937
|
+
// we want to present the illusion that the connection is still open,
|
|
938
|
+
// and therefor closable with "abort",
|
|
939
|
+
// so we handle the abort ourselves to close the multiplexed stream
|
|
940
|
+
params.signal?.addEventListener('abort', () =>
|
|
941
|
+
unset(create_abort_error('stream aborted')))
|
|
942
|
+
|
|
943
|
+
// first, we need to listen for the headers..
|
|
944
|
+
var headers_buffer = new Uint8Array()
|
|
945
|
+
var parsed_headers = await process_buffers(() => {
|
|
946
|
+
// check if the stream has been closed
|
|
947
|
+
var stream_ended = !buffers[buffers.length - 1]
|
|
948
|
+
if (stream_ended) buffers.pop()
|
|
949
|
+
|
|
950
|
+
// aggregate all the new buffers into our big headers_buffer
|
|
951
|
+
headers_buffer = concat_buffers([headers_buffer, ...buffers])
|
|
952
|
+
buffers = []
|
|
953
|
+
|
|
954
|
+
// and if the stream had ended, put that information back
|
|
955
|
+
if (stream_ended) buffers.push(null)
|
|
956
|
+
|
|
957
|
+
// try parsing what we got so far as headers..
|
|
958
|
+
var x = parse_headers(headers_buffer)
|
|
959
|
+
|
|
960
|
+
// how did it go?
|
|
961
|
+
if (x.result === 'error') {
|
|
962
|
+
// if we got an error, give up
|
|
963
|
+
console.log(`headers_buffer: ` + new TextDecoder().decode(headers_buffer))
|
|
964
|
+
throw new Error('error parsing headers')
|
|
965
|
+
} else if (x.result === 'waiting') {
|
|
966
|
+
if (stream_ended) throw new Error('Multiplexed stream ended before headers received.')
|
|
967
|
+
} else return x
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
// put the bytes left over from the header back
|
|
971
|
+
if (parsed_headers.input.length) buffers.unshift(parsed_headers.input)
|
|
972
|
+
|
|
973
|
+
// these headers will also have the status,
|
|
974
|
+
// but we want to present the status in a more usual way below
|
|
975
|
+
var status = parsed_headers.headers[':status']
|
|
976
|
+
delete parsed_headers.headers[':status']
|
|
977
|
+
|
|
978
|
+
// create our own fake response object,
|
|
979
|
+
// to mimik fetch's response object,
|
|
980
|
+
// feeding the user our stream data from the multiplexer
|
|
981
|
+
var res = new Response(new ReadableStream({
|
|
982
|
+
async start(controller) {
|
|
983
|
+
try {
|
|
984
|
+
await process_buffers(() => {
|
|
985
|
+
var b = buffers.shift()
|
|
986
|
+
if (!b) return true
|
|
987
|
+
controller.enqueue(b)
|
|
988
|
+
})
|
|
989
|
+
} finally { controller.close() }
|
|
990
|
+
}
|
|
991
|
+
}), {
|
|
992
|
+
status,
|
|
993
|
+
headers: parsed_headers.headers
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
// add a convenience property for the user to know if
|
|
997
|
+
// this response is being multiplexed
|
|
998
|
+
res.multiplexer = params.headers.get('multiplexer')
|
|
999
|
+
|
|
1000
|
+
// return the fake response object
|
|
1001
|
+
return res
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
// if we had an error, be sure to unregister ourselves
|
|
1004
|
+
await unset(e)
|
|
1005
|
+
throw e
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
})()
|
|
1009
|
+
|
|
1010
|
+
// call the special fetch function for the multiplexer
|
|
1011
|
+
return await (await multiplex_fetch.multiplexers[multiplexer])(url, params)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// waits on reader for chunks like: 123 bytes for stream ABC\r\n..123 bytes..
|
|
1015
|
+
// which would trigger cb("ABC", bytes)
|
|
1016
|
+
async function parse_multiplex_stream(reader, cb, on_error) {
|
|
1017
|
+
try {
|
|
1018
|
+
var buffers = [new Uint8Array(0)]
|
|
1019
|
+
var buffers_size = 0
|
|
1020
|
+
var chunk_size = null
|
|
1021
|
+
var stream_id = null
|
|
1022
|
+
var header_length = 0
|
|
1023
|
+
var header_started = false
|
|
1024
|
+
|
|
1025
|
+
while (true) {
|
|
1026
|
+
var { done, value } = await reader.read()
|
|
1027
|
+
if (done) throw new Error('multiplex stream ended unexpectedly')
|
|
1028
|
+
buffers.push(value)
|
|
1029
|
+
buffers_size += value.length
|
|
1030
|
+
|
|
1031
|
+
while (true) {
|
|
1032
|
+
if (chunk_size === null && buffers_size) {
|
|
1033
|
+
if (buffers.length > 1) buffers = [concat_buffers(buffers)]
|
|
1034
|
+
|
|
1035
|
+
var headerComplete = false
|
|
1036
|
+
while (buffers[0].length > header_length) {
|
|
1037
|
+
const byte = buffers[0][header_length]
|
|
1038
|
+
header_length++
|
|
1039
|
+
|
|
1040
|
+
if (byte !== 13 && byte !== 10) header_started = true
|
|
1041
|
+
if (header_started && byte === 10) {
|
|
1042
|
+
headerComplete = true
|
|
1043
|
+
break
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (headerComplete) {
|
|
1047
|
+
var headerStr = new TextDecoder().decode(buffers[0].slice(0, header_length))
|
|
1048
|
+
var m = headerStr.match(/^[\r\n]*((\d+) bytes for|close) stream ([A-Za-z0-9_-]+)\r\n$/)
|
|
1049
|
+
if (!m) throw new Error('invalid multiplex header')
|
|
1050
|
+
stream_id = m[3]
|
|
1051
|
+
|
|
1052
|
+
buffers[0] = buffers[0].slice(header_length)
|
|
1053
|
+
buffers_size -= header_length
|
|
1054
|
+
|
|
1055
|
+
if (m[1] === 'close') {
|
|
1056
|
+
cb(stream_id)
|
|
1057
|
+
break
|
|
1058
|
+
} else chunk_size = 1 * m[2]
|
|
1059
|
+
} else break
|
|
1060
|
+
} else if (chunk_size !== null && buffers_size >= chunk_size) {
|
|
1061
|
+
if (buffers.length > 1) buffers = [concat_buffers(buffers)]
|
|
1062
|
+
|
|
1063
|
+
var chunk = buffers[0].slice(0, chunk_size)
|
|
1064
|
+
buffers[0] = buffers[0].slice(chunk_size)
|
|
1065
|
+
buffers_size -= chunk_size
|
|
1066
|
+
|
|
1067
|
+
// console.log(`stream_id: ${stream_id}, ${new TextDecoder().decode(chunk)}`)
|
|
1068
|
+
|
|
1069
|
+
cb(stream_id, chunk)
|
|
1070
|
+
|
|
1071
|
+
chunk_size = null
|
|
1072
|
+
stream_id = null
|
|
1073
|
+
header_length = 0
|
|
1074
|
+
header_started = false
|
|
1075
|
+
} else break
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
} catch (e) { on_error(e) }
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// concatenates an array of Uint8Array's, into a single one
|
|
1082
|
+
function concat_buffers(buffers) {
|
|
1083
|
+
const x = new Uint8Array(buffers.reduce((a, b) => a + b.length, 0))
|
|
1084
|
+
let offset = 0
|
|
1085
|
+
for (const b of buffers) {
|
|
1086
|
+
x.set(b, offset)
|
|
1087
|
+
offset += b.length
|
|
1088
|
+
}
|
|
1089
|
+
return x
|
|
1090
|
+
}
|
|
1091
|
+
|
|
779
1092
|
// The "extra_headers" field is returned to the client on any *update* or
|
|
780
1093
|
// *patch* to include any headers that we've received, but don't have braid
|
|
781
1094
|
// semantics for.
|
|
@@ -817,6 +1130,12 @@ function ascii_ify(s) {
|
|
|
817
1130
|
return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
|
|
818
1131
|
}
|
|
819
1132
|
|
|
1133
|
+
function create_abort_error(msg) {
|
|
1134
|
+
var e = new Error(msg)
|
|
1135
|
+
e.name = 'AbortError'
|
|
1136
|
+
return e
|
|
1137
|
+
}
|
|
1138
|
+
|
|
820
1139
|
// ****************************
|
|
821
1140
|
// Exports
|
|
822
1141
|
// ****************************
|
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,151 @@ 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
|
+
// parse the multiplexer id and stream id from the url
|
|
248
|
+
var [multiplexer, stream] = req.url.slice(1).split('/')
|
|
249
|
+
|
|
250
|
+
// if there's just a multiplexer, then we're creating a multiplexer..
|
|
251
|
+
if (!stream) {
|
|
252
|
+
// maintain a Map of all the multiplexers
|
|
253
|
+
if (!braidify.multiplexers) braidify.multiplexers = new Map()
|
|
254
|
+
braidify.multiplexers.set(multiplexer, {streams: new Map(), res})
|
|
255
|
+
|
|
256
|
+
// when the response closes,
|
|
257
|
+
// let everyone know the multiplexer has died
|
|
258
|
+
res.on('close', () => {
|
|
259
|
+
for (var f of braidify.multiplexers.get(multiplexer).streams.values()) f()
|
|
260
|
+
braidify.multiplexers.delete(multiplexer)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// keep the connection open,
|
|
264
|
+
// so people can send multiplexed data to it
|
|
265
|
+
res.writeHead(200, {
|
|
266
|
+
'Cache-Control': 'no-cache',
|
|
267
|
+
'X-Accel-Buffering': 'no',
|
|
268
|
+
...req.httpVersion !== '2.0' && {'Connection': 'keep-alive'}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// but write something.. won't interfere with stream,
|
|
272
|
+
// and helps flush the headers
|
|
273
|
+
return res.write(`\r\n`)
|
|
274
|
+
} else {
|
|
275
|
+
// in this case, we're closing the given stream
|
|
276
|
+
var m = braidify.multiplexers?.get(multiplexer)
|
|
277
|
+
|
|
278
|
+
// if the multiplexer doesn't exist, send an error
|
|
279
|
+
if (!m) {
|
|
280
|
+
var msg = `multiplexer ${multiplexer} does not exist`
|
|
281
|
+
res.writeHead(400, {'Content-Type': 'text/plain'})
|
|
282
|
+
res.end(msg)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// remove this stream, and notify it
|
|
287
|
+
let s = m.streams.get(stream)
|
|
288
|
+
if (s) {
|
|
289
|
+
s()
|
|
290
|
+
m.streams.delete(stream)
|
|
291
|
+
} else m.streams.set(stream, 'abort')
|
|
292
|
+
|
|
293
|
+
// let the requester know we succeeded
|
|
294
|
+
res.writeHead(200, {})
|
|
295
|
+
return res.end(``)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// a multiplexer header means the user wants to send the
|
|
300
|
+
// results of this request to the provided multiplexer,
|
|
301
|
+
// tagged with the given stream id
|
|
302
|
+
if (braidify.use_multiplexing && req.headers.multiplexer) {
|
|
303
|
+
// parse the multiplexer id and stream id from the url
|
|
304
|
+
var [multiplexer, stream] = req.headers.multiplexer.slice(1).split('/')
|
|
305
|
+
|
|
306
|
+
var end_things = (msg) => {
|
|
307
|
+
res.statusCode = 400
|
|
308
|
+
res.end(msg)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// find the multiplexer object (contains a response object)
|
|
312
|
+
var m = braidify.multiplexers?.get(multiplexer)
|
|
313
|
+
if (!m) return end_things(`multiplexer ${multiplexer} does not exist`)
|
|
314
|
+
|
|
315
|
+
// special case: check that this stream isn't already aborted
|
|
316
|
+
if (m.streams.get(stream) === 'abort') {
|
|
317
|
+
m.streams.delete(stream)
|
|
318
|
+
return end_things(`multiplexer stream ${req.headers.multiplexer} already aborted`)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// let the requester know we've multiplexed their response
|
|
322
|
+
res.writeHead(293, {multiplexer: req.headers.multiplexer})
|
|
323
|
+
res.end('Ok.')
|
|
324
|
+
|
|
325
|
+
// and now set things up so that future use of the
|
|
326
|
+
// response object forwards stuff into the multiplexer
|
|
327
|
+
|
|
328
|
+
// first we create a kind of fake socket
|
|
329
|
+
class MultiplexedWritable extends require('stream').Writable {
|
|
330
|
+
constructor(multiplexer, stream) {
|
|
331
|
+
super()
|
|
332
|
+
this.multiplexer = multiplexer
|
|
333
|
+
this.stream = stream
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_write(chunk, encoding, callback) {
|
|
337
|
+
var len = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk, encoding)
|
|
338
|
+
this.multiplexer.res.write(`${len} bytes for stream ${this.stream}\r\n`)
|
|
339
|
+
this.multiplexer.res.write(chunk, encoding, callback)
|
|
340
|
+
|
|
341
|
+
// console.log(`wrote:`)
|
|
342
|
+
// console.log(`${len} bytes for stream /${this.stream}\r\n`)
|
|
343
|
+
// if (Buffer.isBuffer(chunk)) console.log(new TextDecoder().decode(chunk))
|
|
344
|
+
// else console.log('STRING?: ' + chunk)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
var mw = new MultiplexedWritable(m, stream)
|
|
348
|
+
|
|
349
|
+
// then we create a fake server response,
|
|
350
|
+
// that pipes data to our fake socket
|
|
351
|
+
var res2 = new (require('http').ServerResponse)({})
|
|
352
|
+
res2.useChunkedEncodingByDefault = false
|
|
353
|
+
res2.assignSocket(mw)
|
|
354
|
+
|
|
355
|
+
// register a handler for when the multiplexer closes,
|
|
356
|
+
// to close our fake response stream
|
|
357
|
+
m.streams.set(stream, () => res2.destroy())
|
|
358
|
+
|
|
359
|
+
// when our fake response is done,
|
|
360
|
+
// we want to send a special message to the multiplexer saying so
|
|
361
|
+
res2.on('finish', () => m.res.write(`close stream ${stream}\r\n`))
|
|
362
|
+
|
|
363
|
+
// we want access to "res" to be forwarded to our fake "res2",
|
|
364
|
+
// so that it goes into the multiplexer
|
|
365
|
+
function* get_props(obj) {
|
|
366
|
+
do {
|
|
367
|
+
for (var x of Object.getOwnPropertyNames(obj)) yield x
|
|
368
|
+
} while (obj = Object.getPrototypeOf(obj))
|
|
369
|
+
}
|
|
370
|
+
for (let key of get_props(res)) {
|
|
371
|
+
if (key === '_events' || key === 'emit') continue
|
|
372
|
+
if (res2[key] === undefined) continue
|
|
373
|
+
var value = res[key]
|
|
374
|
+
if (typeof value === 'function') {
|
|
375
|
+
res[key] = res2[key].bind(res2)
|
|
376
|
+
} else {
|
|
377
|
+
+((key) => {
|
|
378
|
+
Object.defineProperty(res, key, {
|
|
379
|
+
get: () => res2[key],
|
|
380
|
+
set: x => res2[key] = x
|
|
381
|
+
})
|
|
382
|
+
})(key)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// this is provided so code can know if the response has been multiplexed
|
|
387
|
+
res.multiplexer = res2
|
|
388
|
+
}
|
|
389
|
+
|
|
246
390
|
// Add the braidly request/response helper methods
|
|
247
391
|
res.sendUpdate = (stuff) => send_update(res, stuff, req.url, peer)
|
|
248
392
|
res.sendVersion = res.sendUpdate
|
|
@@ -275,7 +419,7 @@ function braidify (req, res, next) {
|
|
|
275
419
|
|
|
276
420
|
// We have a subscription!
|
|
277
421
|
res.statusCode = 209
|
|
278
|
-
res.setHeader("subscribe", req.headers.subscribe)
|
|
422
|
+
res.setHeader("subscribe", req.headers.subscribe ?? 'true')
|
|
279
423
|
res.setHeader('cache-control', 'no-cache, no-transform')
|
|
280
424
|
|
|
281
425
|
|