braid-http 0.0.1 → 0.1.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.
@@ -136,7 +136,7 @@ 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 = {}) {
@@ -162,16 +162,30 @@ async function braid_fetch (url, params = {}) {
162
162
 
163
163
  // Prepare patches
164
164
  if (params.patches) {
165
- console.assert(Array.isArray(params.patches), 'Patches must be array')
166
165
  console.assert(!params.body, 'Cannot send both patches and body')
166
+ console.assert(typeof params.patches === 'object', 'Patches must be object or array')
167
+
168
+ // We accept a single patch as an array of one patch
169
+ if (!Array.isArray(params.patches))
170
+ params.patches = [params.patches]
171
+
172
+ // If just one patch, send it directly!
173
+ if (params.patches.length === 1) {
174
+ let patch = params.patches[0]
175
+ params.headers.set('Content-Range', `${patch.unit} ${patch.range}`)
176
+ params.headers.set('Content-Length', `${patch.content.length}`)
177
+ params.body = patch.content
178
+ }
167
179
 
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')
180
+ // Multiple patches get sent within a Patches: N block
181
+ else {
182
+ params.headers.set('Patches', params.patches.length)
183
+ params.body = (params.patches).map(patch => {
184
+ var length = `content-length: ${patch.content.length}`
185
+ var range = `content-range: ${patch.unit} ${patch.range}`
186
+ return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n`
187
+ }).join('\r\n')
188
+ }
175
189
  }
176
190
 
177
191
  // Wrap the AbortController with a new one that we control.
@@ -482,19 +496,51 @@ function parse_headers (input) {
482
496
  return { result: 'success', headers, input }
483
497
  }
484
498
 
499
+ // Content-range is of the form '<unit> <range>' e.g. 'json .index'
500
+ var content_range_regex = /(\S+) (.*)/
485
501
  function parse_body (state) {
502
+
486
503
  // Parse Body Snapshot
487
504
 
488
505
  var content_length = parseInt(state.headers['content-length'])
489
- if (content_length !== NaN) {
506
+ if (!isNaN(content_length)) {
507
+
508
+ // We've read a Content-Length, so we have a block to parse
490
509
  if (content_length > state.input.length) {
510
+ // But we haven't received the whole block yet
491
511
  state.result = 'waiting'
492
512
  return state
493
513
  }
494
514
 
515
+ // We have the whole block!
495
516
  var consumed_length = content_length + 2
496
517
  state.result = 'success'
497
- state.body = state.input.substring(0, content_length)
518
+
519
+ // If we have a content-range, then this is a patch
520
+ if (state.headers['content-range']) {
521
+ var match = state.headers['content-range'].match(content_range_regex)
522
+ if (!match)
523
+ return {
524
+ result: 'error',
525
+ message: 'cannot parse content-range',
526
+ range: state.headers['content-range']
527
+ }
528
+ state.patches = [{
529
+ unit: match[1],
530
+ range: match[2],
531
+ content: state.input.substring(0, content_length),
532
+
533
+ // Question: Perhaps we should include headers here, like we do for
534
+ // the Patches: N headers below?
535
+
536
+ // headers: state.headers
537
+ }]
538
+ }
539
+
540
+ // Otherwise, this is a snapshot body
541
+ else
542
+ state.body = state.input.substring(0, content_length)
543
+
498
544
  state.input = state.input.substring(consumed_length)
499
545
  return state
500
546
  }
@@ -535,7 +581,7 @@ function parse_body (state) {
535
581
  state.input = parsed.input
536
582
  }
537
583
 
538
- // Todo: support arbitrary patches, not just range-patch
584
+ // Todo: support custom patches, not just range-patch
539
585
 
540
586
  // Parse Range Patch format
541
587
  {
@@ -561,9 +607,7 @@ function parse_body (state) {
561
607
  return state
562
608
  }
563
609
 
564
- // Content-range is of the form '<unit> <range>' e.g. 'json .index'
565
-
566
- var match = last_patch.headers['content-range'].match(/(\S+) (.*)/)
610
+ var match = last_patch.headers['content-range'].match(content_range_regex)
567
611
  if (!match)
568
612
  return {
569
613
  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/index.js CHANGED
@@ -7,6 +7,6 @@ var client = require('./braid-http-client'),
7
7
 
8
8
  module.exports = {
9
9
  fetch: client.fetch,
10
- http: client.http,
10
+ http_client: client.http,
11
11
  http_server: server
12
12
  }
package/index.mjs CHANGED
@@ -8,8 +8,8 @@ import braid_client from './braid-http-client.js'
8
8
  import braid_server from './braid-http-server.js'
9
9
 
10
10
  var fetch = braid_client.fetch,
11
- http = braid_client.http,
11
+ http_client = braid_client.http,
12
12
  http_server = braid_server
13
13
 
14
- export { fetch, http, http_server }
15
- export default { fetch, http, http_server }
14
+ export { fetch, http_client, http_server }
15
+ export default { fetch, http_client, http_server }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-http",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
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:
@@ -19,12 +23,14 @@ Node.js:
19
23
  npm install braid-http
20
24
  ```
21
25
 
22
- Then in your node.js code:
23
-
24
26
  ```javascript
27
+ // Import with require()
25
28
  require('braid-http').fetch // A polyfill for require('node-fetch')
26
- require('braid-http').http // A polyfill for require('http') clients
29
+ require('braid-http').http_client // A polyfill for require('http') clients
27
30
  require('braid-http').http_server // A polyfill for require('http') servers
31
+
32
+ // Or as es6 module
33
+ import {fetch, http_client, http_server} from 'braid-http'
28
34
  ```
29
35
 
30
36
  ## Using it in Browsers
@@ -96,7 +102,8 @@ async function connect () {
96
102
  ```javascript
97
103
  async function connect () {
98
104
  try {
99
- 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) {
100
107
  // Updates might come in the form of patches:
101
108
  if (v.patches)
102
109
  chat = apply_patches(v.patches, chat)
@@ -201,10 +208,10 @@ require('http').createServer(app).listen(8583)
201
208
  // Use this line if necessary for self-signed certs
202
209
  // process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0
203
210
 
204
- var https = require('braid-http').http(require('https'))
211
+ var https = require('braid-http').http_client(require('https'))
205
212
  // or:
206
213
  // import braid_http from 'braid-http'
207
- // https = braid_http.http(require('https'))
214
+ // https = braid_http.http_client(require('https'))
208
215
 
209
216
  https.get(
210
217
  'https://braid.org/chat',