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.
@@ -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 = new AbortController()
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
- // Now we run the original fetch....
222
- var res = await normal_fetch(url, params)
223
-
224
- // And customize the response with a couple methods for getting
225
- // the braid subscription data:
226
- res.subscribe = start_subscription
227
- res.subscription = {[Symbol.asyncIterator]: iterator}
228
-
229
-
230
- // Now we define the subscription function we just used:
231
- function start_subscription (cb, error) {
232
- if (!res.ok)
233
- throw new Error('Request returned not ok status:', res.status)
234
-
235
- if (res.bodyUsed)
236
- // TODO: check if this needs a return
237
- throw new Error('This response\'s body has already been read', res)
238
-
239
- // Parse the streamed response
240
- handle_fetch_stream(
241
- res.body,
242
-
243
- // Each time something happens, we'll either get a new
244
- // version back, or an error.
245
- (result, err) => {
246
- if (!err)
247
- // Yay! We got a new version! Tell the callback!
248
- cb(result)
249
- else {
250
- // This error handling code runs if the connection
251
- // closes, or if there is unparseable stuff in the
252
- // streamed response.
253
-
254
- // In any case, we want to be sure to abort the
255
- // underlying fetch.
256
- underlying_aborter.abort()
257
-
258
- // Then send the error upstream.
259
- if (error)
260
- error(err)
261
- else
262
- throw 'Unhandled network error in subscription'
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
- // And the iterator for use with "for async (...)"
270
- function iterator () {
271
- // We'll keep this state while our iterator runs
272
- var initialized = false,
273
- inbox = [],
274
- resolve = null,
275
- reject = null
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
- return {
278
- async next() {
279
- // If we've already received a version, return it
280
- if (inbox.length > 0)
281
- return {done: false, value: inbox.shift()}
282
-
283
- // Otherwise, let's set up a promise to resolve when we get the next item
284
- var promise = new Promise((_resolve, _reject) => {
285
- resolve = _resolve
286
- reject = _reject
287
- })
288
-
289
- // Start the subscription, if we haven't already
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
- // Now wait for the subscription to resolve or reject the promise.
300
- var result = await promise
355
+ if (subscription_cb) start_subscription(subscription_cb, subscription_error)
301
356
 
302
- // Anything we get from here out we should add to the inbox
303
- resolve = (new_version) => inbox.push(new_version)
304
- reject = (err) => {throw err}
357
+ done(res)
305
358
 
306
- return { done: false, value: result }
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
- return res
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
  // ****************************
@@ -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
- res.write('\r\n' + body)
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 (typeof body === 'string') {
366
- set_header('Content-Length', (new TextEncoder().encode(body)).length)
367
- write_body(body)
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-http",
3
- "version": "0.3.22",
3
+ "version": "1.0.1",
4
4
  "description": "An implementation of Braid-HTTP for Node.js and Browsers",
5
5
  "scripts": {
6
6
  "test": "node test/server.js"