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.
- package/braid-http-client.js +172 -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,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 =
|
|
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
|
-
|
|
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
|
+
// 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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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) )
|
|
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
|
-
|
|
300
|
-
var result = await promise
|
|
349
|
+
if (subscription_cb) start_subscription(subscription_cb, subscription_error)
|
|
301
350
|
|
|
302
|
-
|
|
303
|
-
resolve = (new_version) => inbox.push(new_version)
|
|
304
|
-
reject = (err) => {throw err}
|
|
351
|
+
done(res)
|
|
305
352
|
|
|
306
|
-
|
|
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
|
-
|
|
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
|
// ****************************
|
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
|
|