braid-http 1.3.107 → 1.3.109
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 +17 -5
- package/braid-http-server.js +110 -100
- package/package.json +1 -1
package/braid-http-client.js
CHANGED
|
@@ -197,6 +197,7 @@ async function braid_fetch (url, params = {}) {
|
|
|
197
197
|
// Multiple patches get sent within a Patches: N block
|
|
198
198
|
else {
|
|
199
199
|
params.headers.set('Patches', params.patches.length)
|
|
200
|
+
params.headers.set('Content-Type', 'message/http-patches')
|
|
200
201
|
let bufs = []
|
|
201
202
|
let te = new TextEncoder()
|
|
202
203
|
for (let patch of params.patches) {
|
|
@@ -324,13 +325,18 @@ async function braid_fetch (url, params = {}) {
|
|
|
324
325
|
if (!is_nodejs && params.headers.has('subscribe')) {
|
|
325
326
|
var nav_entry = performance?.getEntriesByType?.('navigation')?.[0]
|
|
326
327
|
if (nav_entry?.type === 'back_forward'
|
|
327
|
-
&& document.readyState !== 'complete')
|
|
328
|
+
&& document.readyState !== 'complete') {
|
|
329
|
+
|
|
330
|
+
// ...we wait until the page has loaded to send this fetch,
|
|
331
|
+
// because Chrome's SKIP_CACHE_VALIDATION policy goes away
|
|
332
|
+
// once the page loads.
|
|
328
333
|
|
|
329
|
-
// ...we just wait until the page has loaded to send this fetch
|
|
330
334
|
await new Promise(r => window.addEventListener('load', r))
|
|
331
335
|
|
|
332
|
-
//
|
|
333
|
-
//
|
|
336
|
+
// In practice, waiting for 'load' alone isn't enough;
|
|
337
|
+
// we also need this setTimeout(0) for it to work reliably.
|
|
338
|
+
await new Promise(done => setTimeout(done, 0))
|
|
339
|
+
}
|
|
334
340
|
}
|
|
335
341
|
|
|
336
342
|
// Braid-Chrome needs the following special (undocumented)
|
|
@@ -796,6 +802,12 @@ function parse_headers (input, check_for_encoding_blocks, dont_parse_special_hea
|
|
|
796
802
|
headers.patches = JSON.parse(headers.patches)
|
|
797
803
|
}
|
|
798
804
|
|
|
805
|
+
// If we have Patches: N, verify that we have the right content-type set
|
|
806
|
+
if ('patches' in headers
|
|
807
|
+
&& headers['content-type'] !== 'message/http-patches')
|
|
808
|
+
console.warn('braid-http: update with Patches: ' + headers.patches
|
|
809
|
+
+ ' is missing Content-Type: message/http-patches. Has Content-Type: ' + JSON.stringify(headers['content-type']))
|
|
810
|
+
|
|
799
811
|
// Update the input
|
|
800
812
|
input = input.slice(end)
|
|
801
813
|
|
|
@@ -959,7 +971,7 @@ function extra_headers (headers) {
|
|
|
959
971
|
|
|
960
972
|
// Remove the non-extra parts
|
|
961
973
|
var known_headers = ['version', 'parents', 'patches',
|
|
962
|
-
'content-length', 'content-range', ':status']
|
|
974
|
+
'content-length', 'content-range', 'content-type', ':status']
|
|
963
975
|
for (var i = 0; i < known_headers.length; i++)
|
|
964
976
|
delete result[known_headers[i]]
|
|
965
977
|
|
package/braid-http-server.js
CHANGED
|
@@ -38,8 +38,8 @@ function write_patches (res, patches) {
|
|
|
38
38
|
|
|
39
39
|
// An array of one patch behaves like a single patch
|
|
40
40
|
if (Array.isArray(patches)) {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Add `Patches: N` and `Content-Type: message/http-patches' if array
|
|
42
|
+
res.write('Content-Type: message/http-patches\r\n')
|
|
43
43
|
res.write(`Patches: ${patches.length}\r\n\r\n`)
|
|
44
44
|
} else
|
|
45
45
|
// Else, we'll out put a single patch
|
|
@@ -94,123 +94,128 @@ function parse_patches (req, cb) {
|
|
|
94
94
|
|
|
95
95
|
// This function reads an update (either a set of patches, or a body) from a
|
|
96
96
|
// ReadableStream and then fires a callback when finished.
|
|
97
|
+
//
|
|
98
|
+
// If req.already_buffered_body is set (Buffer, Uint8Array, or string), it
|
|
99
|
+
// will be used instead of reading from the request stream. This supports
|
|
100
|
+
// HTTP frameworks (like Fastify, Express with body-parser) that consume the
|
|
101
|
+
// request body before the handler runs.
|
|
97
102
|
function parse_update (req, cb) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
if (req.already_buffered_body != null) {
|
|
104
|
+
var buf = req.already_buffered_body
|
|
105
|
+
if (typeof buf === 'string') buf = new TextEncoder().encode(buf)
|
|
106
|
+
parse_update_from_bytes(new Uint8Array(buf), req.headers, cb)
|
|
107
|
+
} else {
|
|
108
|
+
var chunks = []
|
|
109
|
+
req.on('data', chunk => chunks.push(chunk))
|
|
110
|
+
req.on('end', () =>
|
|
111
|
+
parse_update_from_bytes(new Uint8Array(Buffer.concat(chunks)), req.headers, cb))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse a complete body buffer into an update (body snapshot or patches).
|
|
116
|
+
function parse_update_from_bytes (bytes, headers, cb) {
|
|
117
|
+
var num_patches = headers.patches
|
|
118
|
+
|
|
119
|
+
// Full body snapshot (no patches, no content-range)
|
|
120
|
+
if (!num_patches && !headers['content-range']) {
|
|
121
|
+
let update = { body: bytes, patches: undefined }
|
|
122
|
+
Object.defineProperty(update, 'body_text', {
|
|
123
|
+
get: () => new TextDecoder('utf-8').decode(update.body)
|
|
110
124
|
})
|
|
125
|
+
return cb(update)
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
// Parse a single patch, lacking Patches: N
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
assert(
|
|
129
|
+
// We only support range patches right now, so there must be a
|
|
130
|
+
// Content-Range header.
|
|
131
|
+
if (num_patches === undefined && headers['content-range']) {
|
|
132
|
+
assert(headers['content-range'], 'No patches to parse: need `Patches: N` or `Content-Range:` header in ' + JSON.stringify(headers))
|
|
118
133
|
|
|
119
134
|
// Parse the Content-Range header
|
|
120
135
|
// Content-range is of the form '<unit> <range>' e.g. 'json .index'
|
|
121
|
-
var [unit, range] = parse_content_range(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Read the body one chunk at a time
|
|
126
|
-
req.on('data', chunk => buffer.push(chunk))
|
|
127
|
-
// Then return it
|
|
128
|
-
req.on('end', () => {
|
|
129
|
-
let patch = {unit, range, content: new Uint8Array(Buffer.concat(buffer))}
|
|
130
|
-
Object.defineProperty(patch, 'content_text', {
|
|
131
|
-
get: () => new TextDecoder('utf-8').decode(patch.content)
|
|
132
|
-
})
|
|
133
|
-
cb({ patches: [patch], body: undefined })
|
|
136
|
+
var [unit, range] = parse_content_range(headers['content-range'])
|
|
137
|
+
let patch = {unit, range, content: bytes}
|
|
138
|
+
Object.defineProperty(patch, 'content_text', {
|
|
139
|
+
get: () => new TextDecoder('utf-8').decode(patch.content)
|
|
134
140
|
})
|
|
141
|
+
return cb({ patches: [patch], body: undefined })
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
// Parse multiple patches within a Patches: N block
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
145
|
+
num_patches = parseInt(num_patches)
|
|
146
|
+
|
|
147
|
+
// We check to send patches each time we parse one. But if there
|
|
148
|
+
// are zero to parse, we will never check to send them.
|
|
149
|
+
if (num_patches === 0)
|
|
150
|
+
return cb({ patches: [], body: undefined })
|
|
151
|
+
|
|
152
|
+
var patches = []
|
|
153
|
+
var buffer = Array.from(bytes)
|
|
154
|
+
|
|
155
|
+
while (patches.length < num_patches) {
|
|
156
|
+
// Find the start of the headers (skip leading CR/LF)
|
|
157
|
+
let headers_start = 0
|
|
158
|
+
while (buffer[headers_start] === 13 || buffer[headers_start] === 10)
|
|
159
|
+
headers_start++
|
|
160
|
+
if (headers_start === buffer.length)
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
// Look for the double-newline at the end of the headers.
|
|
164
|
+
let headers_end = headers_start
|
|
165
|
+
while (++headers_end) {
|
|
166
|
+
if (headers_end > buffer.length)
|
|
167
|
+
break
|
|
168
|
+
if (buffer[headers_end - 1] === 10
|
|
169
|
+
&& (buffer[headers_end - 2] === 10
|
|
170
|
+
|| (buffer[headers_end - 2] === 13
|
|
171
|
+
&& buffer[headers_end - 3] === 10)))
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
if (headers_end > buffer.length)
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
// Extract the header string
|
|
178
|
+
var headers_source = buffer.slice(headers_start, headers_end)
|
|
179
|
+
.map(x => String.fromCharCode(x)).join('')
|
|
180
|
+
|
|
181
|
+
// Now let's parse those headers.
|
|
182
|
+
var patch_headers = require('parse-headers')(headers_source)
|
|
183
|
+
|
|
184
|
+
// We require `content-length` to declare the length of the patch.
|
|
185
|
+
if (!('content-length' in patch_headers)) {
|
|
186
|
+
// Print a nice error if it's missing
|
|
187
|
+
console.error('No content-length in', JSON.stringify(patch_headers),
|
|
188
|
+
'from', new TextDecoder().decode(new Uint8Array(buffer)),
|
|
189
|
+
{buffer})
|
|
190
|
+
process.exit(1)
|
|
191
|
+
}
|
|
179
192
|
|
|
180
|
-
|
|
193
|
+
var body_length = parseInt(patch_headers['content-length'])
|
|
181
194
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return
|
|
195
|
+
// Give up if we don't have the full patch yet.
|
|
196
|
+
if (buffer.length - headers_end < body_length) break
|
|
185
197
|
|
|
186
|
-
|
|
198
|
+
// XX Todo: support custom patch types beyond content-range "Range Patches".
|
|
187
199
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
// Content-range is of the form '<unit> <range>' e.g. 'json .index'
|
|
201
|
+
var [unit, range] = parse_content_range(patch_headers['content-range'])
|
|
202
|
+
var patch_content = new Uint8Array(buffer.slice(headers_end,
|
|
203
|
+
headers_end + body_length))
|
|
204
|
+
|
|
205
|
+
// We've got our patch!
|
|
206
|
+
let patch = {unit, range, content: patch_content}
|
|
207
|
+
Object.defineProperty(patch, 'content_text', {
|
|
208
|
+
get: () => new TextDecoder('utf-8').decode(patch.content)
|
|
209
|
+
})
|
|
210
|
+
patches.push(patch)
|
|
191
211
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Object.defineProperty(patch, 'content_text', {
|
|
195
|
-
get: () => new TextDecoder('utf-8').decode(patch.content)
|
|
196
|
-
})
|
|
197
|
-
patches.push(patch)
|
|
212
|
+
buffer = buffer.slice(headers_end + body_length)
|
|
213
|
+
}
|
|
198
214
|
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
if (patches.length !== num_patches)
|
|
216
|
+
console.error(`Got an incomplete PUT: ${patches.length}/${num_patches} patches were received`)
|
|
201
217
|
|
|
202
|
-
|
|
203
|
-
req.pause()
|
|
204
|
-
cb({ patches, body: undefined })
|
|
205
|
-
})
|
|
206
|
-
req.on('end', () => {
|
|
207
|
-
// If the stream ends before we get everything, then return what we
|
|
208
|
-
// did receive
|
|
209
|
-
console.error('Request stream ended!')
|
|
210
|
-
if (patches.length !== num_patches)
|
|
211
|
-
console.error(`Got an incomplete PUT: ${patches.length}/${num_patches} patches were received`)
|
|
212
|
-
})
|
|
213
|
-
}
|
|
218
|
+
cb({ patches, body: undefined })
|
|
214
219
|
}
|
|
215
220
|
|
|
216
221
|
function parse_content_range (range_string) {
|
|
@@ -321,6 +326,11 @@ function braidify (req, res, next) {
|
|
|
321
326
|
res.setHeader('Range-Request-Allow-Methods', 'PATCH, PUT')
|
|
322
327
|
res.setHeader('Range-Request-Allow-Units', 'json')
|
|
323
328
|
|
|
329
|
+
// All requests explicitly Vary on Version, Parents, and Subscribe
|
|
330
|
+
res.appendHeader('Vary', 'Version')
|
|
331
|
+
res.appendHeader('Vary', 'Parents')
|
|
332
|
+
res.appendHeader('Vary', 'Subscribe')
|
|
333
|
+
|
|
324
334
|
// Extract braid info from headers
|
|
325
335
|
var version = ('version' in req.headers) && JSON.parse('['+req.headers.version+']'),
|
|
326
336
|
parents = ('parents' in req.headers) && JSON.parse('['+req.headers.parents+']'),
|