braid-http 0.3.22 → 1.0.1
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 +180 -91
- package/braid-http-server.js +18 -9
- package/package.json +1 -1
package/braid-http-client.js
CHANGED
|
@@ -154,8 +154,8 @@ async function braid_fetch (url, params = {}) {
|
|
|
154
154
|
console.assert(Array.isArray(params.version),
|
|
155
155
|
'fetch(): `version` must be an array')
|
|
156
156
|
if (params.parents)
|
|
157
|
-
console.assert(Array.isArray(params.parents),
|
|
158
|
-
'fetch(): `parents` must be an array')
|
|
157
|
+
console.assert(Array.isArray(params.parents) || (typeof params.parents === 'function'),
|
|
158
|
+
'fetch(): `parents` must be an array or function')
|
|
159
159
|
|
|
160
160
|
// // Always set the peer
|
|
161
161
|
// params.headers.set('peer', peer)
|
|
@@ -163,7 +163,7 @@ async function braid_fetch (url, params = {}) {
|
|
|
163
163
|
// We provide some shortcuts for Braid params
|
|
164
164
|
if (params.version)
|
|
165
165
|
params.headers.set('version', params.version.map(JSON.stringify).join(', '))
|
|
166
|
-
if (params.parents)
|
|
166
|
+
if (Array.isArray(params.parents))
|
|
167
167
|
params.headers.set('parents', params.parents.map(JSON.stringify).join(', '))
|
|
168
168
|
if (params.subscribe)
|
|
169
169
|
params.headers.set('subscribe', 'true')
|
|
@@ -210,115 +210,177 @@ async function braid_fetch (url, params = {}) {
|
|
|
210
210
|
// `controller` to abort the fetch itself.
|
|
211
211
|
|
|
212
212
|
var original_signal = params.signal
|
|
213
|
-
var underlying_aborter =
|
|
214
|
-
params.signal = underlying_aborter.signal
|
|
213
|
+
var underlying_aborter = null
|
|
215
214
|
if (original_signal)
|
|
216
215
|
original_signal.addEventListener(
|
|
217
216
|
'abort',
|
|
218
217
|
() => underlying_aborter.abort()
|
|
219
218
|
)
|
|
220
219
|
|
|
221
|
-
|
|
222
|
-
var res =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
220
|
+
var waitTime = 10
|
|
221
|
+
var res = null
|
|
222
|
+
var subscription_cb = null
|
|
223
|
+
var subscription_error = null
|
|
224
|
+
|
|
225
|
+
return await new Promise((done, fail) => {
|
|
226
|
+
connect()
|
|
227
|
+
async function connect() {
|
|
228
|
+
try {
|
|
229
|
+
if (original_signal?.aborted) return fail(new Error('abort'))
|
|
230
|
+
|
|
231
|
+
// We need a fresh underlying abort controller each time we connect
|
|
232
|
+
underlying_aborter = new AbortController()
|
|
233
|
+
params.signal = underlying_aborter.signal
|
|
234
|
+
|
|
235
|
+
// If parents is a function,
|
|
236
|
+
// call it now to get the latest parents
|
|
237
|
+
if (typeof params.parents === 'function')
|
|
238
|
+
params.headers.set('parents', params.parents().map(JSON.stringify).join(', '))
|
|
239
|
+
|
|
240
|
+
// undocumented feature used by braid-chrome
|
|
241
|
+
// to see the fetch args as they are right before it is actually called,
|
|
242
|
+
// to display them for the user in the dev panel
|
|
243
|
+
params.onFetch?.(url, params)
|
|
244
|
+
|
|
245
|
+
// Now we run the original fetch....
|
|
246
|
+
res = await normal_fetch(url, params)
|
|
247
|
+
|
|
248
|
+
// And customize the response with a couple methods for getting
|
|
249
|
+
// the braid subscription data:
|
|
250
|
+
res.subscribe = start_subscription
|
|
251
|
+
res.subscription = {[Symbol.asyncIterator]: iterator}
|
|
252
|
+
|
|
253
|
+
// Now we define the subscription function we just used:
|
|
254
|
+
function start_subscription (cb, error) {
|
|
255
|
+
subscription_cb = cb
|
|
256
|
+
subscription_error = error
|
|
257
|
+
|
|
258
|
+
if (!res.ok)
|
|
259
|
+
throw new Error('Request returned not ok status:', res.status)
|
|
260
|
+
|
|
261
|
+
if (res.bodyUsed)
|
|
262
|
+
// TODO: check if this needs a return
|
|
263
|
+
throw new Error('This response\'s body has already been read', res)
|
|
264
|
+
|
|
265
|
+
// Parse the streamed response
|
|
266
|
+
handle_fetch_stream(
|
|
267
|
+
res.body,
|
|
268
|
+
|
|
269
|
+
// Each time something happens, we'll either get a new
|
|
270
|
+
// version back, or an error.
|
|
271
|
+
(result, err) => {
|
|
272
|
+
if (!err)
|
|
273
|
+
// Yay! We got a new version! Tell the callback!
|
|
274
|
+
cb(result)
|
|
275
|
+
else {
|
|
276
|
+
// This error handling code runs if the connection
|
|
277
|
+
// closes, or if there is unparseable stuff in the
|
|
278
|
+
// streamed response.
|
|
279
|
+
|
|
280
|
+
// In any case, we want to be sure to abort the
|
|
281
|
+
// underlying fetch.
|
|
282
|
+
underlying_aborter.abort()
|
|
283
|
+
|
|
284
|
+
on_error(err)
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
!isTextContentType(res.headers.get('content-type')),
|
|
288
|
+
params.onBytes
|
|
289
|
+
)
|
|
263
290
|
}
|
|
264
|
-
}
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
291
|
|
|
292
|
+
// And the iterator for use with "for async (...)"
|
|
293
|
+
function iterator () {
|
|
294
|
+
// We'll keep this state while our iterator runs
|
|
295
|
+
var initialized = false,
|
|
296
|
+
inbox = [],
|
|
297
|
+
resolve = null,
|
|
298
|
+
reject = null,
|
|
299
|
+
last_error = null
|
|
268
300
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
301
|
+
return {
|
|
302
|
+
async next() {
|
|
303
|
+
// If we got an error, throw it
|
|
304
|
+
if (last_error) throw last_error
|
|
305
|
+
|
|
306
|
+
// If we've already received a version, return it
|
|
307
|
+
if (inbox.length > 0)
|
|
308
|
+
return {done: false, value: inbox.shift()}
|
|
309
|
+
|
|
310
|
+
// Otherwise, let's set up a promise to resolve when we get the next item
|
|
311
|
+
var promise = new Promise((_resolve, _reject) => {
|
|
312
|
+
resolve = _resolve
|
|
313
|
+
reject = _reject
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// Start the subscription, if we haven't already
|
|
317
|
+
if (!initialized) {
|
|
318
|
+
initialized = true
|
|
319
|
+
|
|
320
|
+
// The subscription will call whichever resolve and
|
|
321
|
+
// reject functions the current promise is waiting for
|
|
322
|
+
start_subscription(x => {
|
|
323
|
+
inbox.push(x)
|
|
324
|
+
resolve()
|
|
325
|
+
}, x => reject(x) )
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Now wait for the subscription to resolve or reject the promise.
|
|
329
|
+
await promise
|
|
330
|
+
|
|
331
|
+
// From here on out, we'll redirect the reject,
|
|
332
|
+
// since that promise is already done
|
|
333
|
+
reject = (err) => {last_error = err}
|
|
334
|
+
|
|
335
|
+
return {done: false, value: inbox.shift()}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
276
339
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (!initialized) {
|
|
291
|
-
initialized = true
|
|
292
|
-
|
|
293
|
-
// The subscription will call whichever resolve and
|
|
294
|
-
// reject functions the current promise is waiting for
|
|
295
|
-
start_subscription(x => resolve(x),
|
|
296
|
-
x => reject(x) )
|
|
340
|
+
if (params.retry) {
|
|
341
|
+
let give_up = res.status >= 400 && res.status < 600
|
|
342
|
+
switch (res.status) {
|
|
343
|
+
case 408: // Request Timeout
|
|
344
|
+
case 425: // Too Early
|
|
345
|
+
case 429: // Too Many Requests
|
|
346
|
+
|
|
347
|
+
case 502: // Bad Gateway
|
|
348
|
+
case 504: // Gateway Timeout
|
|
349
|
+
give_up = false;
|
|
350
|
+
}
|
|
351
|
+
if (give_up) return fail(new Error(`giving up because of http status: ${res.status}${(res.status === 401 || res.status === 403) ? ` (access denied)` : ''}`))
|
|
352
|
+
if (!res.ok) throw new Error(`status not ok: ${res.status}`)
|
|
297
353
|
}
|
|
298
354
|
|
|
299
|
-
|
|
300
|
-
var result = await promise
|
|
355
|
+
if (subscription_cb) start_subscription(subscription_cb, subscription_error)
|
|
301
356
|
|
|
302
|
-
|
|
303
|
-
resolve = (new_version) => inbox.push(new_version)
|
|
304
|
-
reject = (err) => {throw err}
|
|
357
|
+
done(res)
|
|
305
358
|
|
|
306
|
-
|
|
307
|
-
|
|
359
|
+
params?.retry?.onRes?.(res)
|
|
360
|
+
waitTime = 10
|
|
361
|
+
} catch (e) { on_error(e) }
|
|
308
362
|
}
|
|
309
|
-
|
|
363
|
+
function on_error(e) {
|
|
364
|
+
if (!params.retry || (e.name === "AbortError")) {
|
|
365
|
+
subscription_error?.(e)
|
|
366
|
+
return fail(e)
|
|
367
|
+
}
|
|
310
368
|
|
|
311
|
-
|
|
369
|
+
console.log(`retrying in ${waitTime}ms: ${url} after error: ${e}`)
|
|
370
|
+
setTimeout(connect, waitTime)
|
|
371
|
+
waitTime = Math.min(waitTime * 2, 3000)
|
|
372
|
+
}
|
|
373
|
+
})
|
|
312
374
|
}
|
|
313
375
|
|
|
314
376
|
// Parse a stream of versions from the incoming bytes
|
|
315
|
-
async function handle_fetch_stream (stream, cb) {
|
|
377
|
+
async function handle_fetch_stream (stream, cb, binary, on_bytes) {
|
|
316
378
|
if (is_nodejs)
|
|
317
379
|
stream = to_whatwg_stream(stream)
|
|
318
380
|
|
|
319
381
|
// Set up a reader
|
|
320
382
|
var reader = stream.getReader(),
|
|
321
|
-
parser = subscription_parser(cb)
|
|
383
|
+
parser = subscription_parser(cb, binary)
|
|
322
384
|
|
|
323
385
|
while (true) {
|
|
324
386
|
var versions = []
|
|
@@ -339,6 +401,8 @@ async function handle_fetch_stream (stream, cb) {
|
|
|
339
401
|
return
|
|
340
402
|
}
|
|
341
403
|
|
|
404
|
+
on_bytes?.(value)
|
|
405
|
+
|
|
342
406
|
// Tell the parser to process some more stream
|
|
343
407
|
parser.read(value)
|
|
344
408
|
}
|
|
@@ -350,7 +414,7 @@ async function handle_fetch_stream (stream, cb) {
|
|
|
350
414
|
// Braid-HTTP Subscription Parser
|
|
351
415
|
// ****************************
|
|
352
416
|
|
|
353
|
-
var subscription_parser = (cb) => ({
|
|
417
|
+
var subscription_parser = (cb, binary) => ({
|
|
354
418
|
// A parser keeps some parse state
|
|
355
419
|
state: {input: []},
|
|
356
420
|
|
|
@@ -370,6 +434,9 @@ var subscription_parser = (cb) => ({
|
|
|
370
434
|
// Try to parse an update
|
|
371
435
|
try {
|
|
372
436
|
this.state = parse_update (this.state)
|
|
437
|
+
|
|
438
|
+
// Parse UTF-8 if it isn't binary
|
|
439
|
+
if (!binary && this.state.body) this.state.body = (new TextDecoder('utf-8')).decode(this.state.body)
|
|
373
440
|
} catch (e) {
|
|
374
441
|
this.cb(null, e)
|
|
375
442
|
return
|
|
@@ -541,8 +608,7 @@ function parse_body (state) {
|
|
|
541
608
|
}
|
|
542
609
|
|
|
543
610
|
// Otherwise, this is a snapshot body
|
|
544
|
-
else
|
|
545
|
-
state.body = (new TextDecoder('utf-8')).decode(new Uint8Array(state.input.slice(0, content_length)))
|
|
611
|
+
else state.body = new Uint8Array(state.input.slice(0, content_length))
|
|
546
612
|
|
|
547
613
|
state.input = state.input.slice(content_length)
|
|
548
614
|
return state
|
|
@@ -714,6 +780,29 @@ function extractHeader(input) {
|
|
|
714
780
|
};
|
|
715
781
|
}
|
|
716
782
|
|
|
783
|
+
function isTextContentType(contentType) {
|
|
784
|
+
if (!contentType) return false
|
|
785
|
+
|
|
786
|
+
contentType = contentType.toLowerCase().trim()
|
|
787
|
+
|
|
788
|
+
// Check if it starts with "text/"
|
|
789
|
+
if (contentType.startsWith("text/")) return true
|
|
790
|
+
|
|
791
|
+
// Initialize the Map if it doesn't exist yet
|
|
792
|
+
if (!isTextContentType.textApplicationTypes) {
|
|
793
|
+
isTextContentType.textApplicationTypes = new Map([
|
|
794
|
+
["application/json", true],
|
|
795
|
+
["application/xml", true],
|
|
796
|
+
["application/javascript", true],
|
|
797
|
+
["application/ecmascript", true],
|
|
798
|
+
["application/x-www-form-urlencoded", true],
|
|
799
|
+
])
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Use the cached map of text-based application types
|
|
803
|
+
return isTextContentType.textApplicationTypes.has(contentType)
|
|
804
|
+
}
|
|
805
|
+
|
|
717
806
|
// ****************************
|
|
718
807
|
// Exports
|
|
719
808
|
// ****************************
|
package/braid-http-server.js
CHANGED
|
@@ -291,18 +291,22 @@ function send_update(res, data, url, peer) {
|
|
|
291
291
|
res.setHeader(key, val)
|
|
292
292
|
}
|
|
293
293
|
function write_body (body) {
|
|
294
|
-
if (res.isSubscription)
|
|
295
|
-
|
|
296
|
-
else
|
|
297
|
-
res.write(body)
|
|
294
|
+
if (res.isSubscription) res.write('\r\n')
|
|
295
|
+
res.write(body)
|
|
298
296
|
}
|
|
299
297
|
|
|
300
298
|
// console.log('sending version', {url, peer, version, parents, patches, body,
|
|
301
299
|
// subscription: res.isSubscription})
|
|
302
300
|
|
|
303
|
-
// Validate that the body and patches are strings
|
|
301
|
+
// Validate that the body and patches are strings,
|
|
302
|
+
// or in the case of body, it could be binary
|
|
304
303
|
if (body !== undefined)
|
|
305
|
-
assert(typeof body === 'string'
|
|
304
|
+
assert(typeof body === 'string' ||
|
|
305
|
+
body instanceof ArrayBuffer ||
|
|
306
|
+
body instanceof Uint8Array ||
|
|
307
|
+
body instanceof Blob ||
|
|
308
|
+
body instanceof Buffer
|
|
309
|
+
)
|
|
306
310
|
else {
|
|
307
311
|
// Only one of patch or patches can be set
|
|
308
312
|
assert(!(patch && patches))
|
|
@@ -362,9 +366,14 @@ function send_update(res, data, url, peer) {
|
|
|
362
366
|
}
|
|
363
367
|
|
|
364
368
|
// Write the patches or body
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
|
|
369
|
+
if (body_exists) {
|
|
370
|
+
let x = typeof body === 'string' ? new TextEncoder().encode(body) : body
|
|
371
|
+
set_header('Content-Length',
|
|
372
|
+
x instanceof ArrayBuffer ? x.byteLength :
|
|
373
|
+
x instanceof Uint8Array ? x.length :
|
|
374
|
+
x instanceof Blob ? x.size :
|
|
375
|
+
x instanceof Buffer ? x.length : null)
|
|
376
|
+
write_body(x)
|
|
368
377
|
} else
|
|
369
378
|
res.write(generate_patches(res, patches))
|
|
370
379
|
|