braid-http 1.1.1 → 1.3.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.
@@ -189,18 +189,31 @@ async function braid_fetch (url, params = {}) {
189
189
  if (params.patches.length === 1) {
190
190
  let patch = params.patches[0]
191
191
  params.headers.set('Content-Range', `${patch.unit} ${patch.range}`)
192
- params.headers.set('Content-Length', `${(new TextEncoder().encode(patch.content)).length}`)
192
+
193
+ if (typeof patch.content === 'string')
194
+ patch.content = new TextEncoder().encode(patch.content)
195
+
193
196
  params.body = patch.content
194
197
  }
195
198
 
196
199
  // Multiple patches get sent within a Patches: N block
197
200
  else {
198
201
  params.headers.set('Patches', params.patches.length)
199
- params.body = (params.patches).map(patch => {
200
- var length = `content-length: ${(new TextEncoder().encode(patch.content)).length}`
202
+ let bufs = []
203
+ let te = new TextEncoder()
204
+ for (let patch of params.patches) {
205
+ if (bufs.length) bufs.push(te.encode(`\r\n`))
206
+
207
+ if (typeof patch.content === 'string')
208
+ patch.content = te.encode(patch.content)
209
+
210
+ var length = `content-length: ${get_binary_length(patch.content)}`
201
211
  var range = `content-range: ${patch.unit} ${patch.range}`
202
- return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n`
203
- }).join('\r\n')
212
+ bufs.push(te.encode(`${length}\r\n${range}\r\n\r\n`))
213
+ bufs.push(patch.content)
214
+ bufs.push(te.encode(`\r\n`))
215
+ }
216
+ params.body = new Blob(bufs)
204
217
  }
205
218
  }
206
219
 
@@ -306,10 +319,13 @@ async function braid_fetch (url, params = {}) {
306
319
  // Each time something happens, we'll either get a new
307
320
  // version back, or an error.
308
321
  (result, err) => {
309
- if (!err)
322
+ if (!err) {
323
+ // check whether we aborted
324
+ if (original_signal?.aborted) throw new DOMException('already aborted', 'AbortError')
325
+
310
326
  // Yay! We got a new version! Tell the callback!
311
327
  cb(result)
312
- else
328
+ } else
313
329
  // This error handling code runs if the connection
314
330
  // closes, or if there is unparseable stuff in the
315
331
  // streamed response.
@@ -486,7 +502,18 @@ var subscription_parser = (cb) => ({
486
502
  }
487
503
  })
488
504
 
489
- this.cb(update)
505
+ for (let p of update.patches ?? []) {
506
+ Object.defineProperty(p, 'content_text', {
507
+ get: () => new TextDecoder('utf-8').decode(p.content)
508
+ })
509
+ }
510
+
511
+ try {
512
+ this.cb(update)
513
+ } catch (e) {
514
+ this.cb(null, e)
515
+ return
516
+ }
490
517
 
491
518
  // Reset the parser for the next version!
492
519
  this.state = {input: this.state.input}
@@ -551,6 +578,15 @@ function parse_headers (input) {
551
578
  var h = extractHeader(input)
552
579
  if (!h) return {result: 'waiting'}
553
580
 
581
+ // Skip "HTTP 104 Multiresponse"
582
+ if (h.header_string.startsWith('HTTP 104')) {
583
+ h = extractHeader(h.remaining_bytes)
584
+ if (!h) return {result: 'waiting'}
585
+ }
586
+
587
+ // Skip "HTTP 200 OK"
588
+ h.header_string = h.header_string.replace(/^HTTP 200.*\r?\n/, '')
589
+
554
590
  var headers_source = h.header_string
555
591
  var headers_length = headers_source.length
556
592
 
@@ -629,7 +665,7 @@ function parse_body (state) {
629
665
  state.patches = [{
630
666
  unit: match.unit,
631
667
  range: match.range,
632
- content: (new TextDecoder('utf-8')).decode(new Uint8Array(state.input.slice(0, content_length))),
668
+ content: new Uint8Array(state.input.slice(0, content_length)),
633
669
 
634
670
  // Question: Perhaps we should include headers here, like we do for
635
671
  // the Patches: N headers below?
@@ -715,7 +751,7 @@ function parse_body (state) {
715
751
 
716
752
  last_patch.unit = match.unit
717
753
  last_patch.range = match.range
718
- last_patch.content = (new TextDecoder('utf-8')).decode(new Uint8Array(state.input.slice(0, content_length)))
754
+ last_patch.content = new Uint8Array(state.input.slice(0, content_length))
719
755
  last_patch.extra_headers = extra_headers(last_patch.headers)
720
756
  delete last_patch.headers // We only keep the extra headers ^^
721
757
 
@@ -811,6 +847,12 @@ function extractHeader(input) {
811
847
  };
812
848
  }
813
849
 
850
+ function get_binary_length(x) {
851
+ return x instanceof ArrayBuffer ? x.byteLength :
852
+ x instanceof Uint8Array ? x.length :
853
+ x instanceof Blob ? x.size : undefined
854
+ }
855
+
814
856
  // ****************************
815
857
  // Exports
816
858
  // ****************************
@@ -1,6 +1,6 @@
1
1
  var assert = require('assert')
2
2
 
3
- // Return a string of patches in pseudoheader format.
3
+ // Writes patches in pseudoheader format.
4
4
  //
5
5
  // The `patches` argument can be:
6
6
  // - Array of patches
@@ -25,9 +25,7 @@ var assert = require('assert')
25
25
  //
26
26
  // {"some": "json object"}
27
27
  //
28
- function generate_patches(res, patches) {
29
- var result = ''
30
-
28
+ function write_patches(res, patches) {
31
29
  // `patches` must be a patch object or an array of patch objects
32
30
  // - Object: {unit, range, content}
33
31
  // - Array: [{unit, range, content}, ...]
@@ -38,7 +36,7 @@ function generate_patches(res, patches) {
38
36
  if (Array.isArray(patches)) {
39
37
 
40
38
  // Add `Patches: N` header if array
41
- result += `Patches: ${patches.length}\r\n\r\n`
39
+ res.write(`Patches: ${patches.length}\r\n\r\n`)
42
40
  } else
43
41
  // Else, we'll out put a single patch
44
42
  patches = [patches]
@@ -47,29 +45,35 @@ function generate_patches(res, patches) {
47
45
  patches.forEach((patch, i) => {
48
46
  assert(typeof patch.unit === 'string')
49
47
  assert(typeof patch.range === 'string')
50
- assert(typeof patch.content === 'string')
48
+
49
+ if (typeof patch.content === 'string')
50
+ patch.content = new TextEncoder().encode(patch.content)
51
51
 
52
52
  if (i > 0)
53
- result += '\r\n\r\n'
53
+ res.write('\r\n\r\n')
54
54
 
55
55
  let extra_headers = Object.fromEntries(Object.entries(patch).filter(([k, v]) => k != 'unit' && k != 'range' && k != 'content'))
56
56
 
57
- result += `Content-Length: ${(new TextEncoder().encode(patch.content)).length}\r
57
+ res.write(`Content-Length: ${get_binary_length(patch.content)}\r
58
58
  Content-Range: ${patch.unit} ${patch.range}\r
59
59
  ${Object.entries(extra_headers).map(([k, v]) => `${k}: ${v}\r\n`).join('')}\r
60
- ${patch.content}`
60
+ `)
61
+ write_binary(res, patch.content)
61
62
  })
62
- return result
63
63
  }
64
64
 
65
65
 
66
66
  // Deprecated method for legacy support
67
67
  function parse_patches (req, cb) {
68
68
  parse_update(req, update => {
69
- if (typeof update.body === 'string')
69
+ if (update.body != null) {
70
70
  // Return body as an "everything" patch
71
- cb([{unit: 'everything', range: '', content: update.body}])
72
- else
71
+ let patch = {unit: 'everything', range: '', content: update.body}
72
+ Object.defineProperty(patch, 'content_text', {
73
+ get: () => new TextDecoder('utf-8').decode(patch.content)
74
+ })
75
+ cb([patch])
76
+ } else
73
77
  cb(update.patches)
74
78
  })
75
79
  }
@@ -80,16 +84,20 @@ function parse_update (req, cb) {
80
84
  var num_patches = req.headers.patches
81
85
 
82
86
  if (!num_patches && !req.headers['content-range']) {
83
- var body = ''
84
- req.on('data', chunk => {body += chunk.toString()})
87
+ var buffer = []
88
+ req.on('data', chunk => buffer.push(chunk))
85
89
  req.on('end', () => {
86
- cb({ body, patches: undefined })
90
+ let body = new Uint8Array(Buffer.concat(buffer))
91
+ let update = { body, patches: undefined }
92
+ Object.defineProperty(update, 'body_text', {
93
+ get: () => new TextDecoder('utf-8').decode(update.body)
94
+ })
95
+ cb(update)
87
96
  })
88
97
  }
89
98
 
90
99
  // Parse a single patch, lacking Patches: N
91
100
  else if (num_patches === undefined && req.headers['content-range']) {
92
-
93
101
  // We only support range patches right now, so there must be a
94
102
  // Content-Range header.
95
103
  assert(req.headers['content-range'], 'No patches to parse: need `Patches: N` or `Content-Range:` header in ' + JSON.stringify(req.headers))
@@ -104,8 +112,11 @@ function parse_update (req, cb) {
104
112
  req.on('data', chunk => buffer.push(chunk))
105
113
  // Then return it
106
114
  req.on('end', () => {
107
- patches = [{unit, range, content: Buffer.concat(buffer).toString('utf8')}]
108
- cb({ patches, body: undefined })
115
+ let patch = {unit, range, content: new Uint8Array(Buffer.concat(buffer))}
116
+ Object.defineProperty(patch, 'content_text', {
117
+ get: () => new TextDecoder('utf-8').decode(patch.content)
118
+ })
119
+ cb({ patches: [patch], body: undefined })
109
120
  })
110
121
  }
111
122
 
@@ -136,7 +147,7 @@ function parse_update (req, cb) {
136
147
  if (!('content-length' in headers)) {
137
148
  // Print a nice error if it's missing
138
149
  console.error('No content-length in', JSON.stringify(headers),
139
- 'from', {buffer})
150
+ 'from', new TextDecoder().decode(new Uint8Array(buffer)), {buffer})
140
151
  process.exit(1)
141
152
  }
142
153
 
@@ -150,10 +161,14 @@ function parse_update (req, cb) {
150
161
 
151
162
  // Content-range is of the form '<unit> <range>' e.g. 'json .index'
152
163
  var [unit, range] = parse_content_range(headers['content-range'])
153
- var patch_content = new TextDecoder('utf-8').decode(new Uint8Array(h.remaining_bytes.slice(0, body_length)))
164
+ var patch_content = new Uint8Array(h.remaining_bytes.slice(0, body_length))
154
165
 
155
166
  // We've got our patch!
156
- patches.push({unit, range, content: patch_content})
167
+ let patch = {unit, range, content: patch_content}
168
+ Object.defineProperty(patch, 'content_text', {
169
+ get: () => new TextDecoder('utf-8').decode(patch.content)
170
+ })
171
+ patches.push(patch)
157
172
 
158
173
  buffer = h.remaining_bytes.slice(body_length)
159
174
  }
@@ -215,7 +230,7 @@ function braidify (req, res, next) {
215
230
  (done, err) => parse_patches(
216
231
  req,
217
232
  (patches) => done(patches.map(
218
- p => ({...p, content: JSON.parse(p.content)})
233
+ p => ({...p, content: JSON.parse(p.content_text)})
219
234
  ))
220
235
  )
221
236
  )
@@ -297,7 +312,7 @@ function braidify (req, res, next) {
297
312
  next && next()
298
313
  }
299
314
 
300
- function send_update(res, data, url, peer) {
315
+ async function send_update(res, data, url, peer) {
301
316
  var {version, parents, patches, patch, body} = data
302
317
 
303
318
  function set_header (key, val) {
@@ -308,7 +323,7 @@ function send_update(res, data, url, peer) {
308
323
  }
309
324
  function write_body (body) {
310
325
  if (res.isSubscription) res.write('\r\n')
311
- res.write(body)
326
+ write_binary(res, body)
312
327
  }
313
328
 
314
329
  // console.log('sending version', {url, peer, version, parents, patches, body,
@@ -316,14 +331,10 @@ function send_update(res, data, url, peer) {
316
331
 
317
332
  // Validate that the body and patches are strings,
318
333
  // or in the case of body, it could be binary
319
- if (body !== undefined)
320
- assert(typeof body === 'string' ||
321
- body instanceof ArrayBuffer ||
322
- body instanceof Uint8Array ||
323
- body instanceof Blob ||
324
- body instanceof Buffer
325
- )
326
- else {
334
+ if (body !== undefined) {
335
+ assert(typeof body === 'string' || get_binary_length(body) != null)
336
+ if (body instanceof Blob) body = await body.arrayBuffer()
337
+ } else {
327
338
  // Only one of patch or patches can be set
328
339
  assert(!(patch && patches))
329
340
  assert((patch || patches) !== undefined)
@@ -339,23 +350,31 @@ function send_update(res, data, url, peer) {
339
350
 
340
351
  // Now `patches` will be an array of patches or a single patch object.
341
352
  //
342
- // This distinction is used in generate_patches() to determine whether
353
+ // This distinction is used in write_patches() to determine whether
343
354
  // to inline a single patch in the update body vs. writing out a
344
355
  // Patches: N block.
345
356
  assert(typeof patches === 'object')
346
- if (Array.isArray(patches))
347
- patches.forEach(p => {
348
- assert('unit' in p)
349
- assert('range' in p)
350
- assert('content' in p)
351
- assert(typeof p.content === 'string')
352
- })
357
+ for (let p of Array.isArray(patches) ? patches : [patch]) {
358
+ assert('unit' in p)
359
+ assert('range' in p)
360
+ assert('content' in p)
361
+ assert(typeof p.content === 'string' || get_binary_length(p.content) != null)
362
+ if (p.content instanceof Blob) p.content = await p.content.arrayBuffer()
363
+ }
353
364
  }
354
365
 
355
366
  var body_exists = body || body === ''
356
367
  assert(body_exists || patches, 'Missing body or patches')
357
368
  assert(!(body_exists && patches), 'Cannot send both body and patches')
358
369
 
370
+ // Write the beginning of a new multiresponse response
371
+ if (!res.wrote_104_multiresponse) {
372
+ res.write(`HTTP 104 Multiresponse\r\n\r\n`)
373
+ res.wrote_104_multiresponse = true
374
+ }
375
+
376
+ res.write(`HTTP 200 OK\r\n`)
377
+
359
378
  // Write the headers or virtual headers
360
379
  for (var [header, value] of Object.entries(data)) {
361
380
  header = header.toLowerCase()
@@ -384,14 +403,10 @@ function send_update(res, data, url, peer) {
384
403
  // Write the patches or body
385
404
  if (body_exists) {
386
405
  let x = typeof body === 'string' ? new TextEncoder().encode(body) : body
387
- set_header('Content-Length',
388
- x instanceof ArrayBuffer ? x.byteLength :
389
- x instanceof Uint8Array ? x.length :
390
- x instanceof Blob ? x.size :
391
- x instanceof Buffer ? x.length : null)
406
+ set_header('Content-Length', get_binary_length(x))
392
407
  write_body(x)
393
408
  } else
394
- res.write(generate_patches(res, patches))
409
+ write_patches(res, patches)
395
410
 
396
411
  // Add a newline to prepare for the next version
397
412
  // See also https://github.com/braid-org/braid-spec/issues/73
@@ -461,4 +476,16 @@ function extractHeader(input) {
461
476
  };
462
477
  }
463
478
 
479
+ function get_binary_length(x) {
480
+ return x instanceof ArrayBuffer ? x.byteLength :
481
+ x instanceof Uint8Array ? x.length :
482
+ x instanceof Blob ? x.size :
483
+ x instanceof Buffer ? x.length : undefined
484
+ }
485
+
486
+ function write_binary(res, body) {
487
+ if (body instanceof ArrayBuffer) body = new Uint8Array(body)
488
+ res.write(body)
489
+ }
490
+
464
491
  module.exports = braidify
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-http",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "An implementation of Braid-HTTP for Node.js and Browsers",
5
5
  "scripts": {
6
6
  "test": "node test/server.js"