braid-http 0.0.2 → 0.1.2

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.
@@ -136,14 +136,16 @@ if (is_nodejs) {
136
136
  normal_fetch = window.fetch
137
137
  AbortController = window.AbortController
138
138
  Headers = window.Headers
139
- window.fetch = braid_fetch
139
+ // window.fetch = braid_fetch
140
140
  }
141
141
 
142
142
  async function braid_fetch (url, params = {}) {
143
+ params = {...params} // Copy params, because we'll mutate it
144
+
143
145
  // Initialize the headers object
144
146
  if (!params.headers)
145
147
  params.headers = new Headers()
146
- if (!(params.headers instanceof Headers))
148
+ else
147
149
  params.headers = new Headers(params.headers)
148
150
 
149
151
  // Always set the peer
@@ -162,16 +164,30 @@ async function braid_fetch (url, params = {}) {
162
164
 
163
165
  // Prepare patches
164
166
  if (params.patches) {
165
- console.assert(Array.isArray(params.patches), 'Patches must be array')
166
167
  console.assert(!params.body, 'Cannot send both patches and body')
168
+ console.assert(typeof params.patches === 'object', 'Patches must be object or array')
169
+
170
+ // We accept a single patch as an array of one patch
171
+ if (!Array.isArray(params.patches))
172
+ params.patches = [params.patches]
173
+
174
+ // If just one patch, send it directly!
175
+ if (params.patches.length === 1) {
176
+ let patch = params.patches[0]
177
+ params.headers.set('Content-Range', `${patch.unit} ${patch.range}`)
178
+ params.headers.set('Content-Length', `${patch.content.length}`)
179
+ params.body = patch.content
180
+ }
167
181
 
168
- params.patches = params.patches || []
169
- params.headers.set('patches', params.patches.length)
170
- params.body = (params.patches).map(patch => {
171
- var length = `content-length: ${patch.content.length}`
172
- var range = `content-range: ${patch.unit} ${patch.range}`
173
- return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n`
174
- }).join('\r\n')
182
+ // Multiple patches get sent within a Patches: N block
183
+ else {
184
+ params.headers.set('Patches', params.patches.length)
185
+ params.body = (params.patches).map(patch => {
186
+ var length = `content-length: ${patch.content.length}`
187
+ var range = `content-range: ${patch.unit} ${patch.range}`
188
+ return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n`
189
+ }).join('\r\n')
190
+ }
175
191
  }
176
192
 
177
193
  // Wrap the AbortController with a new one that we control.
@@ -482,19 +498,51 @@ function parse_headers (input) {
482
498
  return { result: 'success', headers, input }
483
499
  }
484
500
 
501
+ // Content-range is of the form '<unit> <range>' e.g. 'json .index'
502
+ var content_range_regex = /(\S+) (.*)/
485
503
  function parse_body (state) {
504
+
486
505
  // Parse Body Snapshot
487
506
 
488
507
  var content_length = parseInt(state.headers['content-length'])
489
- if (content_length !== NaN) {
508
+ if (!isNaN(content_length)) {
509
+
510
+ // We've read a Content-Length, so we have a block to parse
490
511
  if (content_length > state.input.length) {
512
+ // But we haven't received the whole block yet
491
513
  state.result = 'waiting'
492
514
  return state
493
515
  }
494
516
 
517
+ // We have the whole block!
495
518
  var consumed_length = content_length + 2
496
519
  state.result = 'success'
497
- state.body = state.input.substring(0, content_length)
520
+
521
+ // If we have a content-range, then this is a patch
522
+ if (state.headers['content-range']) {
523
+ var match = state.headers['content-range'].match(content_range_regex)
524
+ if (!match)
525
+ return {
526
+ result: 'error',
527
+ message: 'cannot parse content-range',
528
+ range: state.headers['content-range']
529
+ }
530
+ state.patches = [{
531
+ unit: match[1],
532
+ range: match[2],
533
+ content: state.input.substring(0, content_length),
534
+
535
+ // Question: Perhaps we should include headers here, like we do for
536
+ // the Patches: N headers below?
537
+
538
+ // headers: state.headers
539
+ }]
540
+ }
541
+
542
+ // Otherwise, this is a snapshot body
543
+ else
544
+ state.body = state.input.substring(0, content_length)
545
+
498
546
  state.input = state.input.substring(consumed_length)
499
547
  return state
500
548
  }
@@ -535,7 +583,7 @@ function parse_body (state) {
535
583
  state.input = parsed.input
536
584
  }
537
585
 
538
- // Todo: support arbitrary patches, not just range-patch
586
+ // Todo: support custom patches, not just range-patch
539
587
 
540
588
  // Parse Range Patch format
541
589
  {
@@ -561,9 +609,7 @@ function parse_body (state) {
561
609
  return state
562
610
  }
563
611
 
564
- // Content-range is of the form '<unit> <range>' e.g. 'json .index'
565
-
566
- var match = last_patch.headers['content-range'].match(/(\S+) (.*)/)
612
+ var match = last_patch.headers['content-range'].match(content_range_regex)
567
613
  if (!match)
568
614
  return {
569
615
  result: 'error',
@@ -1,31 +1,62 @@
1
1
  var assert = require('assert')
2
2
 
3
- // Write an array of patches into the pseudoheader format.
3
+ // Return a string of patches in pseudoheader format.
4
+ //
5
+ // The `patches` argument can be:
6
+ // - Array of patches
7
+ // - A single patch
8
+ //
9
+ // Multiple patches are generated like:
10
+ //
11
+ // Patches: n
12
+ //
13
+ // content-length: 21
14
+ // content-range: json .range
15
+ //
16
+ // {"some": "json object"}
17
+ //
18
+ // content-length: x
19
+ // ...
20
+ //
21
+ // A single patch is generated like:
22
+ //
23
+ // content-length: 21
24
+ // content-range: json .range
25
+ //
26
+ // {"some": "json object"}
27
+ //
4
28
  function generate_patches(res, patches) {
29
+
30
+ // `patches` must be an object or an array
31
+ assert(typeof patches === 'object')
32
+
33
+ // An array of one patch behaves like a single patch
34
+ if (!Array.isArray(patches))
35
+ var patches = [patches]
36
+
5
37
  for (let patch of patches) {
6
38
  assert(typeof patch.unit === 'string')
7
39
  assert(typeof patch.range === 'string')
8
40
  assert(typeof patch.content === 'string')
9
41
  }
10
42
 
11
- // This will return something like:
12
- // Patches: n
13
- //
14
- // content-length: 21
15
- // content-range: json .range
16
- //
17
- // {"some": "json object"}
18
- //
19
- // content-length: x
20
- // ...
21
- var result = `Patches: ${patches.length}\r\n`
22
- for (let patch of patches)
23
- result += `\r
24
- content-length: ${patch.content.length}\r
25
- content-range: ${patch.unit} ${patch.range}\r
43
+ // Build up the string as a result
44
+ var result = ''
45
+
46
+ // Add `Patches: N` header if we have multiple patches
47
+ if (patches.length > 1)
48
+ result += `Patches: ${patches.length}\r\n\r\n`
49
+
50
+ // Generate each patch
51
+ patches.forEach((patch, i) => {
52
+ if (i > 0)
53
+ result += '\r\n\r\n'
54
+
55
+ result += `Content-Length: ${patch.content.length}\r
56
+ Content-Range: ${patch.unit} ${patch.range}\r
26
57
  \r
27
- ${patch.content}\r
28
- `
58
+ ${patch.content}`
59
+ })
29
60
  return result
30
61
  }
31
62
 
@@ -33,81 +64,118 @@ ${patch.content}\r
33
64
  // This function reads num_patches in pseudoheader format from a
34
65
  // ReadableStream and then fires a callback when they're finished.
35
66
  function parse_patches (req, cb) {
36
- // Todo: make this work in the case where there is no Patches: header, but
37
- // Content-Range is still set, nonetheless.
38
-
39
- var num_patches = req.headers.patches,
40
- stream = req
41
-
42
- let patches = []
43
- let buffer = ""
44
- if (num_patches === 0)
45
- return cb(patches)
46
-
47
- stream.on('data', function parse (chunk) {
48
- // Merge the latest chunk into our buffer
49
- buffer = (buffer + chunk)
50
-
51
- // We might have an extra newline at the start. (mike: why?)
52
- buffer = buffer.trimStart()
53
-
54
- while (patches.length < num_patches) {
55
- // First parse the patch headers. It ends with a double-newline.
56
- // Let's see where that is.
57
- var headers_end = buffer.match(/(\r?\n)(\r?\n)/)
58
-
59
- // Give up if we don't have a set of headers yet.
60
- if (!headers_end)
61
- return
62
-
63
- // Now we know where things end
64
- var first_newline = headers_end[1],
65
- headers_length = headers_end.index + first_newline.length,
66
- blank_line = headers_end[2]
67
-
68
- // Now let's parse those headers.
69
- var headers = require('parse-headers')(
70
- buffer.substring(0, headers_length)
71
- )
72
-
73
- // We require `content-length` to declare the length of the patch.
74
- if (!('content-length' in headers)) {
75
- // Print a nice error if it's missing
76
- console.error('No content-length in', JSON.stringify(headers))
77
- process.exit(1)
78
- }
79
-
80
- var body_length = parseInt(headers['content-length'])
81
-
82
- // Give up if we don't have the full patch yet.
83
- if (buffer.length < headers_length + blank_line.length + body_length)
84
- return
85
-
86
- // XX Todo: support custom patch types beyond content-range.
67
+ var num_patches = req.headers.patches
87
68
 
88
- // Content-range is of the form '<unit> <range>' e.g. 'json .index'
89
- var [unit, range] = headers['content-range'].match(/(\S+) (.*)/).slice(1)
90
- var patch_content =
91
- buffer.substring(headers_length + blank_line.length,
92
- headers_length + blank_line.length + body_length)
69
+ // Parse a single patch from the request body
70
+ if (num_patches === undefined) {
93
71
 
94
- // We've got our patch!
95
- patches.push({unit, range, content: patch_content})
72
+ // We only support range patches right now, so there must be a
73
+ // Content-Range header.
74
+ assert(req.headers['content-range'], 'No patches to parse: need `Patches: N` or `Content-Range:` header in ' + JSON.stringify(req.headers))
96
75
 
97
- buffer = buffer.substring(headers_length + blank_line.length + body_length)
76
+ // Parse the Content-Range header
77
+ var match = req.headers['content-range'].match(/(\S+) (.*)/)
78
+ if (!match) {
79
+ console.error('Cannot parse Content-Range in', JSON.stringify(headers))
80
+ process.exit(1)
98
81
  }
82
+ var [unit, range] = match.slice(1)
83
+
84
+ // The contents of the patch is in the request body
85
+ var buffer = ''
86
+ // Read the body one chunk at a time
87
+ req.on('data', chunk => buffer = buffer + chunk)
88
+ // Then return it
89
+ req.on('end', () => {
90
+ patches = [{unit, range, content: buffer}]
91
+ cb(patches)
92
+ })
93
+ }
99
94
 
100
- // We got all the patches! Pause the stream and tell the callback!
101
- stream.pause()
102
- cb(patches)
103
- })
104
- stream.on('end', () => {
105
- // If the stream ends before we get everything, then return what we
106
- // did receive
107
- console.error('Stream ended!')
108
- if (patches.length !== num_patches)
109
- console.error(`Got an incomplete PUT: ${patches.length}/${num_patches} patches were received`)
110
- })
95
+ // Parse multiple patches within a Patches: N block
96
+ else {
97
+ num_patches = parseInt(num_patches)
98
+ let patches = []
99
+ let buffer = ""
100
+
101
+ // We check to send send patches each time we parse one. But if there
102
+ // are zero to parse, we will never check to send them.
103
+ if (num_patches === 0)
104
+ return cb([])
105
+
106
+ req.on('data', function parse (chunk) {
107
+
108
+ // Merge the latest chunk into our buffer
109
+ buffer = (buffer + chunk)
110
+
111
+ while (patches.length < num_patches) {
112
+ // We might have extra newlines at the start, because patches
113
+ // can be separated by arbitrary numbers of newlines
114
+ buffer = buffer.trimStart()
115
+
116
+ // First parse the patch headers. It ends with a double-newline.
117
+ // Let's see where that is.
118
+ var headers_end = buffer.match(/(\r?\n)(\r?\n)/)
119
+
120
+ // Give up if we don't have a set of headers yet.
121
+ if (!headers_end)
122
+ return
123
+
124
+ // Now we know where things end
125
+ var first_newline = headers_end[1],
126
+ headers_length = headers_end.index + first_newline.length,
127
+ blank_line = headers_end[2]
128
+
129
+ // Now let's parse those headers.
130
+ var headers = require('parse-headers')(
131
+ buffer.substring(0, headers_length)
132
+ )
133
+
134
+ // We require `content-length` to declare the length of the patch.
135
+ if (!('content-length' in headers)) {
136
+ // Print a nice error if it's missing
137
+ console.error('No content-length in', JSON.stringify(headers),
138
+ 'from', {buffer, headers_length})
139
+ process.exit(1)
140
+ }
141
+
142
+ var body_length = parseInt(headers['content-length'])
143
+
144
+ // Give up if we don't have the full patch yet.
145
+ if (buffer.length < headers_length + blank_line.length + body_length)
146
+ return
147
+
148
+ // XX Todo: support custom patch types beyond content-range.
149
+
150
+ // Content-range is of the form '<unit> <range>' e.g. 'json .index'
151
+ var match = headers['content-range'].match(/(\S+) (.*)/)
152
+ if (!match) {
153
+ console.error('Cannot parse Content-Range in', JSON.stringify(headers))
154
+ process.exit(1)
155
+ }
156
+ var [unit, range] = match.slice(1)
157
+ var patch_content =
158
+ buffer.substring(headers_length + blank_line.length,
159
+ headers_length + blank_line.length + body_length)
160
+
161
+ // We've got our patch!
162
+ patches.push({unit, range, content: patch_content})
163
+
164
+ buffer = buffer.substring(headers_length + blank_line.length + body_length)
165
+ }
166
+
167
+ // We got all the patches! Pause the stream and tell the callback!
168
+ req.pause()
169
+ cb(patches)
170
+ })
171
+ req.on('end', () => {
172
+ // If the stream ends before we get everything, then return what we
173
+ // did receive
174
+ console.error('Request stream ended!')
175
+ if (patches.length !== num_patches)
176
+ console.error(`Got an incomplete PUT: ${patches.length}/${num_patches} patches were received`)
177
+ })
178
+ }
111
179
  }
112
180
 
113
181
  function braidify (req, res, next) {
@@ -116,7 +184,6 @@ function braidify (req, res, next) {
116
184
  // First, declare that we support Patches and JSON ranges.
117
185
  res.setHeader('Range-Request-Allow-Methods', 'PATCH, PUT')
118
186
  res.setHeader('Range-Request-Allow-Units', 'json')
119
- res.setHeader("Patches", "OK")
120
187
 
121
188
  // Extract braid info from headers
122
189
  var version = req.headers.version && JSON.parse(req.headers.version),
@@ -163,6 +230,7 @@ function braidify (req, res, next) {
163
230
  res.statusCode = 209
164
231
  res.setHeader("subscribe", req.headers.subscribe)
165
232
  res.setHeader('cache-control', 'no-cache, no-transform')
233
+ res.setHeader('transfer-encoding', '')
166
234
 
167
235
  var connected = true
168
236
  function disconnected (x) {
@@ -198,7 +266,7 @@ function send_version(res, data, url, peer) {
198
266
  }
199
267
  function write_body (body) {
200
268
  if (res.isSubscription)
201
- res.write('\r\n' + body + '\r\n')
269
+ res.write('\r\n' + body)
202
270
  else
203
271
  res.write(body)
204
272
  }
@@ -211,16 +279,26 @@ function send_version(res, data, url, peer) {
211
279
  assert(typeof body === 'string')
212
280
  else {
213
281
  assert(patches !== undefined)
214
- patches.forEach(p => assert(typeof p.content === 'string'))
282
+ assert(patches !== null)
283
+ assert(typeof patches === 'object')
284
+ if (Array.isArray(patches))
285
+ patches.forEach(p => assert(typeof p.content === 'string'))
215
286
  }
287
+ assert(body || patches, 'Missing body or patches')
288
+ assert(!(body && patches), 'Cannot send both body and patches')
216
289
 
217
290
  // Write the headers or virtual headers
218
291
  for (var [header, value] of Object.entries(data)) {
292
+ header = header.toLowerCase()
293
+
219
294
  // Version and Parents get output in the Structured Headers format
220
- if (header === 'version')
295
+ if (header === 'version') {
296
+ header = 'Version' // Capitalize for prettiness
221
297
  value = JSON.stringify(value)
222
- else if (header === 'parents')
298
+ } else if (header === 'parents') {
299
+ header = 'Parents' // Capitalize for prettiness
223
300
  value = parents.map(JSON.stringify).join(", ")
301
+ }
224
302
 
225
303
  // We don't output patches or body yet
226
304
  else if (header === 'patches' || header == 'body')
@@ -230,20 +308,16 @@ function send_version(res, data, url, peer) {
230
308
  }
231
309
 
232
310
  // Write the patches or body
233
- if (Array.isArray(patches))
234
- res.write(generate_patches(res, patches)) // adds its own newline
235
- else if (typeof body === 'string') {
236
- set_header('content-length', body.length)
311
+ if (typeof body === 'string') {
312
+ set_header('Content-Length', body.length)
237
313
  write_body(body)
238
- } else {
239
- console.trace("Missing body or patches")
240
- process.exit()
241
- }
314
+ } else
315
+ res.write(generate_patches(res, patches))
242
316
 
243
317
  // Add a newline to prepare for the next version
244
318
  // See also https://github.com/braid-org/braid-spec/issues/73
245
319
  if (res.isSubscription) {
246
- var extra_newlines = 0
320
+ var extra_newlines = 1
247
321
  if (res.is_firefox)
248
322
  // Work around Firefox network buffering bug
249
323
  // See https://github.com/braid-org/braidjs/issues/15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-http",
3
- "version": "0.0.2",
3
+ "version": "0.1.2",
4
4
  "description": "An implementation of Braid-HTTP for Node.js and Browsers",
5
5
  "scripts": {
6
6
  "test": "node test/server.js"
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Braid-HTTP
2
2
 
3
- This polyfill library implements the [Braid-HTTP v03 protocol](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-braid-http-03.txt) in Javascript. It extends the existing browser `fetch()` API, and the nodejs `http` library, with the ability to speak Braid.
3
+ This polyfill library implements the [Braid-HTTP v03 protocol](https://github.com/braid-org/braid-spec/blob/master/draft-toomim-httpbis-braid-http-03.txt) in Javascript. It gives browsers a `braid_fetch()` drop-in replacement for the `fetch()` API, and gives nodejs an `http` plugin, allowing them to speak Braid in a simple way.
4
4
 
5
5
  Developed in [braid.org](https://braid.org).
6
6
 
@@ -11,6 +11,10 @@ Browsers:
11
11
 
12
12
  ```html
13
13
  <script src="https://unpkg.com/braid-http/braid-http-client.js"></script>
14
+ <script>
15
+ // To live on the cutting edge, you can now replace the browser's fetch() if desired:
16
+ // window.fetch = braid_fetch
17
+ </script>
14
18
  ```
15
19
 
16
20
  Node.js:
@@ -98,7 +102,8 @@ async function connect () {
98
102
  ```javascript
99
103
  async function connect () {
100
104
  try {
101
- for await (var v of fetch('/chat', {subscribe: true}).subscription) {
105
+ var subscription_iterator = fetch('/chat', {subscribe: true}).subscription
106
+ for await (var v of subscription_iterator) {
102
107
  // Updates might come in the form of patches:
103
108
  if (v.patches)
104
109
  chat = apply_patches(v.patches, chat)