braid-http
Advanced tools
Comparing version 0.0.2 to 0.1.1
@@ -139,3 +139,3 @@ var peer = Math.random().toString(36).substr(2) | ||
Headers = window.Headers | ||
window.fetch = braid_fetch | ||
// window.fetch = braid_fetch | ||
} | ||
@@ -166,12 +166,26 @@ | ||
if (params.patches) { | ||
console.assert(Array.isArray(params.patches), 'Patches must be array') | ||
console.assert(!params.body, 'Cannot send both patches and body') | ||
console.assert(typeof params.patches === 'object', 'Patches must be object or array') | ||
params.patches = params.patches || [] | ||
params.headers.set('patches', params.patches.length) | ||
params.body = (params.patches).map(patch => { | ||
var length = `content-length: ${patch.content.length}` | ||
var range = `content-range: ${patch.unit} ${patch.range}` | ||
return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n` | ||
}).join('\r\n') | ||
// We accept a single patch as an array of one patch | ||
if (!Array.isArray(params.patches)) | ||
params.patches = [params.patches] | ||
// If just one patch, send it directly! | ||
if (params.patches.length === 1) { | ||
let patch = params.patches[0] | ||
params.headers.set('Content-Range', `${patch.unit} ${patch.range}`) | ||
params.headers.set('Content-Length', `${patch.content.length}`) | ||
params.body = patch.content | ||
} | ||
// Multiple patches get sent within a Patches: N block | ||
else { | ||
params.headers.set('Patches', params.patches.length) | ||
params.body = (params.patches).map(patch => { | ||
var length = `content-length: ${patch.content.length}` | ||
var range = `content-range: ${patch.unit} ${patch.range}` | ||
return `${length}\r\n${range}\r\n\r\n${patch.content}\r\n` | ||
}).join('\r\n') | ||
} | ||
} | ||
@@ -487,8 +501,14 @@ | ||
// Content-range is of the form '<unit> <range>' e.g. 'json .index' | ||
var content_range_regex = /(\S+) (.*)/ | ||
function parse_body (state) { | ||
// Parse Body Snapshot | ||
var content_length = parseInt(state.headers['content-length']) | ||
if (content_length !== NaN) { | ||
if (!isNaN(content_length)) { | ||
// We've read a Content-Length, so we have a block to parse | ||
if (content_length > state.input.length) { | ||
// But we haven't received the whole block yet | ||
state.result = 'waiting' | ||
@@ -498,5 +518,31 @@ return state | ||
// We have the whole block! | ||
var consumed_length = content_length + 2 | ||
state.result = 'success' | ||
state.body = state.input.substring(0, content_length) | ||
// If we have a content-range, then this is a patch | ||
if (state.headers['content-range']) { | ||
var match = state.headers['content-range'].match(content_range_regex) | ||
if (!match) | ||
return { | ||
result: 'error', | ||
message: 'cannot parse content-range', | ||
range: state.headers['content-range'] | ||
} | ||
state.patches = [{ | ||
unit: match[1], | ||
range: match[2], | ||
content: state.input.substring(0, content_length), | ||
// Question: Perhaps we should include headers here, like we do for | ||
// the Patches: N headers below? | ||
// headers: state.headers | ||
}] | ||
} | ||
// Otherwise, this is a snapshot body | ||
else | ||
state.body = state.input.substring(0, content_length) | ||
state.input = state.input.substring(consumed_length) | ||
@@ -542,3 +588,3 @@ return state | ||
// Todo: support arbitrary patches, not just range-patch | ||
// Todo: support custom patches, not just range-patch | ||
@@ -569,5 +615,3 @@ // Parse Range Patch format | ||
// Content-range is of the form '<unit> <range>' e.g. 'json .index' | ||
var match = last_patch.headers['content-range'].match(/(\S+) (.*)/) | ||
var match = last_patch.headers['content-range'].match(content_range_regex) | ||
if (!match) | ||
@@ -574,0 +618,0 @@ return { |
var assert = require('assert') | ||
// Write an array of patches into the pseudoheader format. | ||
// Return a string of patches in pseudoheader format. | ||
// | ||
// The `patches` argument can be: | ||
// - Array of patches | ||
// - A single patch | ||
// | ||
// Multiple patches are generated like: | ||
// | ||
// Patches: n | ||
// | ||
// content-length: 21 | ||
// content-range: json .range | ||
// | ||
// {"some": "json object"} | ||
// | ||
// content-length: x | ||
// ... | ||
// | ||
// A single patch is generated like: | ||
// | ||
// content-length: 21 | ||
// content-range: json .range | ||
// | ||
// {"some": "json object"} | ||
// | ||
function generate_patches(res, patches) { | ||
// `patches` must be an object or an array | ||
assert(typeof patches === 'object') | ||
// An array of one patch behaves like a single patch | ||
if (!Array.isArray(patches)) | ||
var patches = [patches] | ||
for (let patch of patches) { | ||
@@ -11,20 +43,19 @@ assert(typeof patch.unit === 'string') | ||
// This will return something like: | ||
// Patches: n | ||
// | ||
// content-length: 21 | ||
// content-range: json .range | ||
// | ||
// {"some": "json object"} | ||
// | ||
// content-length: x | ||
// ... | ||
var result = `Patches: ${patches.length}\r\n` | ||
for (let patch of patches) | ||
result += `\r | ||
content-length: ${patch.content.length}\r | ||
content-range: ${patch.unit} ${patch.range}\r | ||
// Build up the string as a result | ||
var result = '' | ||
// Add `Patches: N` header if we have multiple patches | ||
if (patches.length > 1) | ||
result += `Patches: ${patches.length}\r\n\r\n` | ||
// Generate each patch | ||
patches.forEach((patch, i) => { | ||
if (i > 0) | ||
result += '\r\n\r\n' | ||
result += `Content-Length: ${patch.content.length}\r | ||
Content-Range: ${patch.unit} ${patch.range}\r | ||
\r | ||
${patch.content}\r | ||
` | ||
${patch.content}` | ||
}) | ||
return result | ||
@@ -37,77 +68,114 @@ } | ||
function parse_patches (req, cb) { | ||
// Todo: make this work in the case where there is no Patches: header, but | ||
// Content-Range is still set, nonetheless. | ||
var num_patches = req.headers.patches | ||
var num_patches = req.headers.patches, | ||
stream = req | ||
// Parse a single patch from the request body | ||
if (num_patches === undefined) { | ||
let patches = [] | ||
let buffer = "" | ||
if (num_patches === 0) | ||
return cb(patches) | ||
// We only support range patches right now, so there must be a | ||
// Content-Range header. | ||
assert(req.headers['content-range'], 'No patches to parse: need `Patches: N` or `Content-Range:` header in ' + JSON.stringify(req.headers)) | ||
stream.on('data', function parse (chunk) { | ||
// Merge the latest chunk into our buffer | ||
buffer = (buffer + chunk) | ||
// Parse the Content-Range header | ||
var match = req.headers['content-range'].match(/(\S+) (.*)/) | ||
if (!match) { | ||
console.error('Cannot parse Content-Range in', JSON.stringify(headers)) | ||
process.exit(1) | ||
} | ||
var [unit, range] = match.slice(1) | ||
// We might have an extra newline at the start. (mike: why?) | ||
buffer = buffer.trimStart() | ||
// The contents of the patch is in the request body | ||
var buffer = '' | ||
// Read the body one chunk at a time | ||
req.on('data', chunk => buffer = buffer + chunk) | ||
// Then return it | ||
req.on('end', () => { | ||
patches = [{unit, range, content: buffer}] | ||
cb(patches) | ||
}) | ||
} | ||
while (patches.length < num_patches) { | ||
// First parse the patch headers. It ends with a double-newline. | ||
// Let's see where that is. | ||
var headers_end = buffer.match(/(\r?\n)(\r?\n)/) | ||
// Parse multiple patches within a Patches: N block | ||
else { | ||
num_patches = parseInt(num_patches) | ||
let patches = [] | ||
let buffer = "" | ||
// Give up if we don't have a set of headers yet. | ||
if (!headers_end) | ||
return | ||
// We check to send send patches each time we parse one. But if there | ||
// are zero to parse, we will never check to send them. | ||
if (num_patches === 0) | ||
return cb([]) | ||
// Now we know where things end | ||
var first_newline = headers_end[1], | ||
headers_length = headers_end.index + first_newline.length, | ||
blank_line = headers_end[2] | ||
req.on('data', function parse (chunk) { | ||
// Now let's parse those headers. | ||
var headers = require('parse-headers')( | ||
buffer.substring(0, headers_length) | ||
) | ||
// Merge the latest chunk into our buffer | ||
buffer = (buffer + chunk) | ||
// We require `content-length` to declare the length of the patch. | ||
if (!('content-length' in headers)) { | ||
// Print a nice error if it's missing | ||
console.error('No content-length in', JSON.stringify(headers)) | ||
process.exit(1) | ||
} | ||
while (patches.length < num_patches) { | ||
// We might have extra newlines at the start, because patches | ||
// can be separated by arbitrary numbers of newlines | ||
buffer = buffer.trimStart() | ||
var body_length = parseInt(headers['content-length']) | ||
// First parse the patch headers. It ends with a double-newline. | ||
// Let's see where that is. | ||
var headers_end = buffer.match(/(\r?\n)(\r?\n)/) | ||
// Give up if we don't have the full patch yet. | ||
if (buffer.length < headers_length + blank_line.length + body_length) | ||
return | ||
// Give up if we don't have a set of headers yet. | ||
if (!headers_end) | ||
return | ||
// XX Todo: support custom patch types beyond content-range. | ||
// Now we know where things end | ||
var first_newline = headers_end[1], | ||
headers_length = headers_end.index + first_newline.length, | ||
blank_line = headers_end[2] | ||
// Content-range is of the form '<unit> <range>' e.g. 'json .index' | ||
var [unit, range] = headers['content-range'].match(/(\S+) (.*)/).slice(1) | ||
var patch_content = | ||
buffer.substring(headers_length + blank_line.length, | ||
headers_length + blank_line.length + body_length) | ||
// Now let's parse those headers. | ||
var headers = require('parse-headers')( | ||
buffer.substring(0, headers_length) | ||
) | ||
// We've got our patch! | ||
patches.push({unit, range, content: patch_content}) | ||
// We require `content-length` to declare the length of the patch. | ||
if (!('content-length' in headers)) { | ||
// Print a nice error if it's missing | ||
console.error('No content-length in', JSON.stringify(headers), | ||
'from', {buffer, headers_length}) | ||
process.exit(1) | ||
} | ||
buffer = buffer.substring(headers_length + blank_line.length + body_length) | ||
} | ||
var body_length = parseInt(headers['content-length']) | ||
// We got all the patches! Pause the stream and tell the callback! | ||
stream.pause() | ||
cb(patches) | ||
}) | ||
stream.on('end', () => { | ||
// If the stream ends before we get everything, then return what we | ||
// did receive | ||
console.error('Stream ended!') | ||
if (patches.length !== num_patches) | ||
console.error(`Got an incomplete PUT: ${patches.length}/${num_patches} patches were received`) | ||
}) | ||
// Give up if we don't have the full patch yet. | ||
if (buffer.length < headers_length + blank_line.length + body_length) | ||
return | ||
// XX Todo: support custom patch types beyond content-range. | ||
// Content-range is of the form '<unit> <range>' e.g. 'json .index' | ||
var match = headers['content-range'].match(/(\S+) (.*)/) | ||
if (!match) { | ||
console.error('Cannot parse Content-Range in', JSON.stringify(headers)) | ||
process.exit(1) | ||
} | ||
var [unit, range] = match.slice(1) | ||
var patch_content = | ||
buffer.substring(headers_length + blank_line.length, | ||
headers_length + blank_line.length + body_length) | ||
// We've got our patch! | ||
patches.push({unit, range, content: patch_content}) | ||
buffer = buffer.substring(headers_length + blank_line.length + body_length) | ||
} | ||
// We got all the patches! Pause the stream and tell the callback! | ||
req.pause() | ||
cb(patches) | ||
}) | ||
req.on('end', () => { | ||
// If the stream ends before we get everything, then return what we | ||
// did receive | ||
console.error('Request stream ended!') | ||
if (patches.length !== num_patches) | ||
console.error(`Got an incomplete PUT: ${patches.length}/${num_patches} patches were received`) | ||
}) | ||
} | ||
} | ||
@@ -121,3 +189,2 @@ | ||
res.setHeader('Range-Request-Allow-Units', 'json') | ||
res.setHeader("Patches", "OK") | ||
@@ -169,2 +236,3 @@ // Extract braid info from headers | ||
res.setHeader('cache-control', 'no-cache, no-transform') | ||
res.setHeader('transfer-encoding', '') | ||
@@ -205,3 +273,3 @@ var connected = true | ||
if (res.isSubscription) | ||
res.write('\r\n' + body + '\r\n') | ||
res.write('\r\n' + body) | ||
else | ||
@@ -219,12 +287,22 @@ res.write(body) | ||
assert(patches !== undefined) | ||
patches.forEach(p => assert(typeof p.content === 'string')) | ||
assert(patches !== null) | ||
assert(typeof patches === 'object') | ||
if (Array.isArray(patches)) | ||
patches.forEach(p => assert(typeof p.content === 'string')) | ||
} | ||
assert(body || patches, 'Missing body or patches') | ||
assert(!(body && patches), 'Cannot send both body and patches') | ||
// Write the headers or virtual headers | ||
for (var [header, value] of Object.entries(data)) { | ||
header = header.toLowerCase() | ||
// Version and Parents get output in the Structured Headers format | ||
if (header === 'version') | ||
if (header === 'version') { | ||
header = 'Version' // Capitalize for prettiness | ||
value = JSON.stringify(value) | ||
else if (header === 'parents') | ||
} else if (header === 'parents') { | ||
header = 'Parents' // Capitalize for prettiness | ||
value = parents.map(JSON.stringify).join(", ") | ||
} | ||
@@ -239,11 +317,7 @@ // We don't output patches or body yet | ||
// Write the patches or body | ||
if (Array.isArray(patches)) | ||
res.write(generate_patches(res, patches)) // adds its own newline | ||
else if (typeof body === 'string') { | ||
set_header('content-length', body.length) | ||
if (typeof body === 'string') { | ||
set_header('Content-Length', body.length) | ||
write_body(body) | ||
} else { | ||
console.trace("Missing body or patches") | ||
process.exit() | ||
} | ||
} else | ||
res.write(generate_patches(res, patches)) | ||
@@ -253,3 +327,3 @@ // Add a newline to prepare for the next version | ||
if (res.isSubscription) { | ||
var extra_newlines = 0 | ||
var extra_newlines = 1 | ||
if (res.is_firefox) | ||
@@ -256,0 +330,0 @@ // Work around Firefox network buffering bug |
{ | ||
"name": "braid-http", | ||
"version": "0.0.2", | ||
"version": "0.1.1", | ||
"description": "An implementation of Braid-HTTP for Node.js and Browsers", | ||
@@ -5,0 +5,0 @@ "scripts": { |
# Braid-HTTP | ||
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. | ||
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. | ||
@@ -14,2 +14,6 @@ Developed in [braid.org](https://braid.org). | ||
<script src="https://unpkg.com/braid-http/braid-http-client.js"></script> | ||
<script> | ||
// To live on the cutting edge, you can now replace the browser's fetch() if desired: | ||
// window.fetch = braid_fetch | ||
</script> | ||
``` | ||
@@ -102,3 +106,4 @@ | ||
try { | ||
for await (var v of fetch('/chat', {subscribe: true}).subscription) { | ||
var subscription_iterator = fetch('/chat', {subscribe: true}).subscription | ||
for await (var v of subscription_iterator) { | ||
// Updates might come in the form of patches: | ||
@@ -105,0 +110,0 @@ if (v.patches) |
41164
818
277