braid-http 0.3.22 → 1.0.0

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,171 @@ 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
+ // Now we run the original fetch....
241
+ res = await normal_fetch(url, params)
242
+
243
+ // And customize the response with a couple methods for getting
244
+ // the braid subscription data:
245
+ res.subscribe = start_subscription
246
+ res.subscription = {[Symbol.asyncIterator]: iterator}
247
+
248
+ // Now we define the subscription function we just used:
249
+ function start_subscription (cb, error) {
250
+ subscription_cb = cb
251
+ subscription_error = error
252
+
253
+ if (!res.ok)
254
+ throw new Error('Request returned not ok status:', res.status)
255
+
256
+ if (res.bodyUsed)
257
+ // TODO: check if this needs a return
258
+ throw new Error('This response\'s body has already been read', res)
259
+
260
+ // Parse the streamed response
261
+ handle_fetch_stream(
262
+ res.body,
263
+
264
+ // Each time something happens, we'll either get a new
265
+ // version back, or an error.
266
+ (result, err) => {
267
+ if (!err)
268
+ // Yay! We got a new version! Tell the callback!
269
+ cb(result)
270
+ else {
271
+ // This error handling code runs if the connection
272
+ // closes, or if there is unparseable stuff in the
273
+ // streamed response.
274
+
275
+ // In any case, we want to be sure to abort the
276
+ // underlying fetch.
277
+ underlying_aborter.abort()
278
+
279
+ on_error(err)
280
+ }
281
+ },
282
+ !isTextContentType(res.headers.get('content-type'))
283
+ )
263
284
  }
264
- }
265
- )
266
- }
267
285
 
286
+ // And the iterator for use with "for async (...)"
287
+ function iterator () {
288
+ // We'll keep this state while our iterator runs
289
+ var initialized = false,
290
+ inbox = [],
291
+ resolve = null,
292
+ reject = null,
293
+ last_error = null
268
294
 
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
295
+ return {
296
+ async next() {
297
+ // If we got an error, throw it
298
+ if (last_error) throw last_error
299
+
300
+ // If we've already received a version, return it
301
+ if (inbox.length > 0)
302
+ return {done: false, value: inbox.shift()}
303
+
304
+ // Otherwise, let's set up a promise to resolve when we get the next item
305
+ var promise = new Promise((_resolve, _reject) => {
306
+ resolve = _resolve
307
+ reject = _reject
308
+ })
309
+
310
+ // Start the subscription, if we haven't already
311
+ if (!initialized) {
312
+ initialized = true
313
+
314
+ // The subscription will call whichever resolve and
315
+ // reject functions the current promise is waiting for
316
+ start_subscription(x => {
317
+ inbox.push(x)
318
+ resolve()
319
+ }, x => reject(x) )
320
+ }
321
+
322
+ // Now wait for the subscription to resolve or reject the promise.
323
+ await promise
324
+
325
+ // From here on out, we'll redirect the reject,
326
+ // since that promise is already done
327
+ reject = (err) => {last_error = err}
328
+
329
+ return {done: false, value: inbox.shift()}
330
+ }
331
+ }
332
+ }
276
333
 
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) )
334
+ if (params.retry) {
335
+ let give_up = res.status >= 400 && res.status < 600
336
+ switch (res.status) {
337
+ case 408: // Request Timeout
338
+ case 425: // Too Early
339
+ case 429: // Too Many Requests
340
+
341
+ case 502: // Bad Gateway
342
+ case 504: // Gateway Timeout
343
+ give_up = false;
344
+ }
345
+ if (give_up) return fail(new Error(`giving up because of http status: ${res.status}${(res.status === 401 || res.status === 403) ? ` (access denied)` : ''}`))
346
+ if (!res.ok) throw new Error(`status not ok: ${res.status}`)
297
347
  }
298
348
 
299
- // Now wait for the subscription to resolve or reject the promise.
300
- var result = await promise
349
+ if (subscription_cb) start_subscription(subscription_cb, subscription_error)
301
350
 
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}
351
+ done(res)
305
352
 
306
- return { done: false, value: result }
307
- }
353
+ params?.retry?.onRes?.(res)
354
+ waitTime = 10
355
+ } catch (e) { on_error(e) }
308
356
  }
309
- }
357
+ function on_error(e) {
358
+ if (!params.retry || (e.name === "AbortError")) {
359
+ subscription_error?.(e)
360
+ return fail(e)
361
+ }
310
362
 
311
- return res
363
+ console.log(`retrying in ${waitTime}ms: ${url} after error: ${e}`)
364
+ setTimeout(connect, waitTime)
365
+ waitTime = Math.min(waitTime * 2, 3000)
366
+ }
367
+ })
312
368
  }
313
369
 
314
370
  // Parse a stream of versions from the incoming bytes
315
- async function handle_fetch_stream (stream, cb) {
371
+ async function handle_fetch_stream (stream, cb, binary) {
316
372
  if (is_nodejs)
317
373
  stream = to_whatwg_stream(stream)
318
374
 
319
375
  // Set up a reader
320
376
  var reader = stream.getReader(),
321
- parser = subscription_parser(cb)
377
+ parser = subscription_parser(cb, binary)
322
378
 
323
379
  while (true) {
324
380
  var versions = []
@@ -350,7 +406,7 @@ async function handle_fetch_stream (stream, cb) {
350
406
  // Braid-HTTP Subscription Parser
351
407
  // ****************************
352
408
 
353
- var subscription_parser = (cb) => ({
409
+ var subscription_parser = (cb, binary) => ({
354
410
  // A parser keeps some parse state
355
411
  state: {input: []},
356
412
 
@@ -370,6 +426,9 @@ var subscription_parser = (cb) => ({
370
426
  // Try to parse an update
371
427
  try {
372
428
  this.state = parse_update (this.state)
429
+
430
+ // Parse UTF-8 if it isn't binary
431
+ if (!binary && this.state.body) this.state.body = (new TextDecoder('utf-8')).decode(this.state.body)
373
432
  } catch (e) {
374
433
  this.cb(null, e)
375
434
  return
@@ -541,8 +600,7 @@ function parse_body (state) {
541
600
  }
542
601
 
543
602
  // Otherwise, this is a snapshot body
544
- else
545
- state.body = (new TextDecoder('utf-8')).decode(new Uint8Array(state.input.slice(0, content_length)))
603
+ else state.body = new Uint8Array(state.input.slice(0, content_length))
546
604
 
547
605
  state.input = state.input.slice(content_length)
548
606
  return state
@@ -714,6 +772,29 @@ function extractHeader(input) {
714
772
  };
715
773
  }
716
774
 
775
+ function isTextContentType(contentType) {
776
+ if (!contentType) return false
777
+
778
+ contentType = contentType.toLowerCase().trim()
779
+
780
+ // Check if it starts with "text/"
781
+ if (contentType.startsWith("text/")) return true
782
+
783
+ // Initialize the Map if it doesn't exist yet
784
+ if (!isTextContentType.textApplicationTypes) {
785
+ isTextContentType.textApplicationTypes = new Map([
786
+ ["application/json", true],
787
+ ["application/xml", true],
788
+ ["application/javascript", true],
789
+ ["application/ecmascript", true],
790
+ ["application/x-www-form-urlencoded", true],
791
+ ])
792
+ }
793
+
794
+ // Use the cached map of text-based application types
795
+ return isTextContentType.textApplicationTypes.has(contentType)
796
+ }
797
+
717
798
  // ****************************
718
799
  // Exports
719
800
  // ****************************
@@ -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.0",
4
4
  "description": "An implementation of Braid-HTTP for Node.js and Browsers",
5
5
  "scripts": {
6
6
  "test": "node test/server.js"