braid-blob 0.0.13 → 0.0.15

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/index.js CHANGED
@@ -1,203 +1,244 @@
1
-
2
1
  var {http_server: braidify, free_cors} = require('braid-http'),
3
2
  fs = require('fs'),
4
3
  path = require('path')
5
4
 
6
- var braid_blob = {
7
- db_folder: './braid-blob-db',
8
- meta_folder: './braid-blob-meta',
9
- cache: {}
10
- }
5
+ function create_braid_blob() {
6
+ var braid_blob = {
7
+ db_folder: './braid-blob-db',
8
+ cache: {},
9
+ key_to_subs: {},
10
+ peer: null // we'll try to load this from a file, if not set by the user
11
+ }
11
12
 
12
- var key_to_subs = {}
13
+ braid_blob.init = async () => {
14
+ braid_blob.init = () => {}
13
15
 
14
- braid_blob.serve = async (req, res, options = {}) => {
15
- if (!options.key) options.key = decodeURIComponent(req.url.split('?')[0])
16
+ await fs.promises.mkdir(`${braid_blob.db_folder}/blob`, { recursive: true })
17
+ await fs.promises.mkdir(`${braid_blob.db_folder}/meta`, { recursive: true })
16
18
 
17
- braidify(req, res)
18
- if (res.is_multiplexer) return
19
+ // establish a peer id
20
+ if (!braid_blob.peer)
21
+ try {
22
+ braid_blob.peer = await fs.promises.readFile(`${braid_blob.db_folder}/peer.txt`, 'utf8')
23
+ } catch (e) {}
24
+ if (!braid_blob.peer)
25
+ braid_blob.peer = Math.random().toString(36).slice(2)
26
+ await fs.promises.writeFile(`${braid_blob.db_folder}/peer.txt`, braid_blob.peer)
27
+ }
19
28
 
20
- // Handle OPTIONS request
21
- if (req.method === 'OPTIONS') return res.end();
29
+ braid_blob.serve = async (req, res, options = {}) => {
30
+ await braid_blob.init()
22
31
 
23
- // consume PUT body
24
- var body = req.method === 'PUT' && await slurp(req)
32
+ if (!options.key) options.key = decodeURIComponent(req.url.split('?')[0])
25
33
 
26
- await within_fiber(options.key, async () => {
27
- const filename = `${braid_blob.db_folder}/${encode_filename(options.key)}`
28
- const metaname = `${braid_blob.meta_folder}/${encode_filename(options.key)}`
29
-
30
- // Read the meta file
31
- var meta = {}
32
- try {
33
- meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
34
- } catch (e) {}
35
- var our_v = meta.version
36
-
37
- if (req.method === 'GET') {
38
- // Handle GET request for binary files
39
-
40
- if (our_v == null) {
41
- res.statusCode = 404
42
- res.setHeader('Content-Type', 'text/plain')
43
- return res.end('File Not Found')
44
- }
34
+ braidify(req, res)
35
+ if (res.is_multiplexer) return
45
36
 
46
- if (meta.content_type && req.headers.accept &&
47
- !isAcceptable(meta.content_type, req.headers.accept)) {
48
- res.statusCode = 406
49
- res.setHeader('Content-Type', 'text/plain')
50
- return res.end(`Content-Type of ${meta.content_type} not in Accept: ${req.headers.accept}`)
51
- }
37
+ // Handle OPTIONS request
38
+ if (req.method === 'OPTIONS') return res.end();
52
39
 
53
- // Set Version header;
54
- // but if this is a subscription,
55
- // then we set Current-Version instead
56
- res.setHeader((req.subscribe ? 'Current-' : '') + 'Version', `"${our_v}"`)
57
-
58
- // Set Content-Type
59
- if (meta.content_type)
60
- res.setHeader('Content-Type', meta.content_type)
61
-
62
- if (!req.subscribe)
63
- return res.end(await fs.promises.readFile(filename))
64
-
65
- if (!res.hasHeader("editable"))
66
- res.setHeader("Editable", "true")
67
-
68
- // Start a subscription for future updates.
69
- if (!key_to_subs[options.key]) key_to_subs[options.key] = new Map()
70
- var peer = req.peer || Math.random().toString(36).slice(2)
71
- key_to_subs[options.key].set(peer, res)
72
-
73
- res.startSubscription({ onClose: () => {
74
- key_to_subs[options.key].delete(peer)
75
- if (!key_to_subs[options.key].size)
76
- delete key_to_subs[options.key]
77
- }})
78
-
79
-
80
- // Send an immediate update when:
81
- if (!req.parents || // 1) They have no version history
82
- // (need full sync)
83
- !req.parents.length || // 2) Or their version is the empty set
84
- our_v > 1*req.parents[0] // 3) Or our version is newer
85
- )
86
- return res.sendUpdate({
87
- version: our_v != null ? ['' + our_v] : [],
88
- body: our_v != null ? await fs.promises.readFile(filename) : ''
89
- })
90
- else res.write('\n\n') // get the node http code to send headers
91
- } else if (req.method === 'PUT') {
92
- // Handle PUT request to update binary files
93
-
94
- // Ensure directory exists
95
- await fs.promises.mkdir(path.dirname(filename), { recursive: true })
96
- await fs.promises.mkdir(path.dirname(metaname), { recursive: true })
97
-
98
- var their_v =
99
- !req.version ?
100
- // we'll give them a version in this case
101
- Math.max(our_v != null ? our_v + 1 : 0, Date.now()) :
102
- !req.version.length ?
103
- null :
104
- 1*req.version[0]
105
-
106
- if (their_v != null &&
107
- (our_v == null || their_v > our_v)) {
108
-
109
- // Write the file
110
- await fs.promises.writeFile(filename, body)
111
-
112
- // Write the meta file
113
- meta.version = their_v
114
- if (req.headers['content-type'])
115
- meta.content_type = req.headers['content-type']
116
- await fs.promises.writeFile(metaname, JSON.stringify(meta))
117
-
118
- // Notify all subscriptions of the update
119
- // (except the peer which made the PUT request itself)
120
- if (key_to_subs[options.key])
121
- for (var [peer, sub] of key_to_subs[options.key].entries())
122
- if (peer !== req.peer)
123
- sub.sendUpdate({ body, version: ['' + their_v] })
124
-
125
- res.setHeader("Version", `"${their_v}"`)
126
- } else {
127
- res.setHeader("Version", our_v != null ? `"${our_v}"` : '')
128
- }
129
- res.end('')
130
- } else if (req.method === 'DELETE') {
131
- try {
132
- await fs.promises.unlink(filename)
133
- } catch (e) {}
40
+ // consume PUT body
41
+ var body = req.method === 'PUT' && await slurp(req)
42
+
43
+ await within_fiber(options.key, async () => {
44
+ const filename = `${braid_blob.db_folder}/blob/${encode_filename(options.key)}`
45
+ const metaname = `${braid_blob.db_folder}/meta/${encode_filename(options.key)}`
46
+
47
+ // Read the meta file
48
+ var meta = {}
134
49
  try {
135
- await fs.promises.unlink(metaname)
50
+ meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
136
51
  } catch (e) {}
137
- res.statusCode = 204 // No Content
138
- res.end('')
139
- }
140
- })
141
- }
142
52
 
143
- function encode_filename(filename) {
144
- // Swap all "!" and "/" characters
145
- let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
53
+ if (req.method === 'GET') {
54
+ // Handle GET request for binary files
55
+
56
+ if (meta.event == null) {
57
+ res.statusCode = 404
58
+ res.setHeader('Content-Type', 'text/plain')
59
+ return res.end('File Not Found')
60
+ }
61
+
62
+ if (meta.content_type && req.headers.accept &&
63
+ !isAcceptable(meta.content_type, req.headers.accept)) {
64
+ res.statusCode = 406
65
+ res.setHeader('Content-Type', 'text/plain')
66
+ return res.end(`Content-Type of ${meta.content_type} not in Accept: ${req.headers.accept}`)
67
+ }
68
+
69
+ // Set Version header;
70
+ // but if this is a subscription,
71
+ // then we set Current-Version instead
72
+ res.setHeader((req.subscribe ? 'Current-' : '') + 'Version',
73
+ JSON.stringify(meta.event))
74
+
75
+ // Set Content-Type
76
+ if (meta.content_type)
77
+ res.setHeader('Content-Type', meta.content_type)
78
+
79
+ if (!req.subscribe)
80
+ return res.end(await fs.promises.readFile(filename))
81
+
82
+ if (!res.hasHeader("editable"))
83
+ res.setHeader("Editable", "true")
84
+
85
+ // Start a subscription for future updates.
86
+ if (!braid_blob.key_to_subs[options.key])
87
+ braid_blob.key_to_subs[options.key] = new Map()
88
+ var peer = req.peer || Math.random().toString(36).slice(2)
89
+ braid_blob.key_to_subs[options.key].set(peer, res)
90
+
91
+ res.startSubscription({ onClose: () => {
92
+ braid_blob.key_to_subs[options.key].delete(peer)
93
+ if (!braid_blob.key_to_subs[options.key].size)
94
+ delete braid_blob.key_to_subs[options.key]
95
+ }})
96
+
97
+ // Send an immediate update when:
98
+ if (!req.parents || // 1) They want everything,
99
+ !req.parents.length || // 2) Or everything past the empty set,
100
+ compare_events(meta.event, req.parents[0]) > 0
101
+ // 3) Or what we have is newer
102
+ )
103
+ return res.sendUpdate({
104
+ version: [meta.event],
105
+ 'Merge-Type': 'lww',
106
+ body: await fs.promises.readFile(filename)
107
+ })
108
+ else res.write('\n\n') // get the node http code to send headers
109
+ } else if (req.method === 'PUT') {
110
+ // Handle PUT request to update binary files
111
+
112
+ var their_e =
113
+ !req.version ?
114
+ // we'll give them a event id in this case
115
+ `${braid_blob.peer}-${Math.max(Date.now(),
116
+ meta.event ? 1*get_event_seq(meta.event) + 1 : -Infinity)}` :
117
+ !req.version.length ?
118
+ null :
119
+ req.version[0]
120
+
121
+ if (their_e != null &&
122
+ (meta.event == null ||
123
+ compare_events(their_e, meta.event) > 0)) {
124
+ meta.event = their_e
125
+
126
+ // Write the file
127
+ await fs.promises.writeFile(filename, body)
128
+
129
+ // Write the meta file
130
+ if (req.headers['content-type'])
131
+ meta.content_type = req.headers['content-type']
132
+ await fs.promises.writeFile(metaname, JSON.stringify(meta))
133
+
134
+ // Notify all subscriptions of the update
135
+ // (except the peer which made the PUT request itself)
136
+ if (braid_blob.key_to_subs[options.key])
137
+ for (var [peer, sub] of braid_blob.key_to_subs[options.key].entries())
138
+ if (peer !== req.peer)
139
+ sub.sendUpdate({
140
+ version: [meta.event],
141
+ 'Merge-Type': 'lww',
142
+ body
143
+ })
144
+ }
145
+ res.setHeader("Version", meta.event != null ? JSON.stringify(meta.event) : '')
146
+ res.end('')
147
+ } else if (req.method === 'DELETE') {
148
+ try {
149
+ await fs.promises.unlink(filename)
150
+ } catch (e) {}
151
+ try {
152
+ await fs.promises.unlink(metaname)
153
+ } catch (e) {}
154
+ res.statusCode = 204 // No Content
155
+ res.end('')
156
+ }
157
+ })
158
+ }
146
159
 
147
- // Encode the filename using encodeURIComponent()
148
- let encoded = encodeURIComponent(swapped)
160
+ function compare_events(a, b) {
161
+ var a_num = get_event_seq(a)
162
+ var b_num = get_event_seq(b)
149
163
 
150
- return encoded
151
- }
164
+ var c = a_num.length - b_num.length
165
+ if (c) return c
152
166
 
153
- function within_fiber(id, func) {
154
- if (!within_fiber.chains) within_fiber.chains = {}
155
- var prev = within_fiber.chains[id] || Promise.resolve()
156
- var curr = prev.then(async () => {
157
- try {
158
- return await func()
159
- } finally {
160
- if (within_fiber.chains[id] === curr)
161
- delete within_fiber.chains[id]
162
- }
163
- })
164
- return within_fiber.chains[id] = curr
165
- }
167
+ var c = a_num.localeCompare(b_num)
168
+ if (c) return c
166
169
 
167
- async function slurp(req) {
168
- return await new Promise(done => {
169
- var chunks = []
170
- req.on('data', chunk => chunks.push(chunk))
171
- req.on('end', () => done(Buffer.concat(chunks)))
172
- })
173
- }
170
+ return a.localeCompare(b)
171
+ }
172
+
173
+ function get_event_seq(e) {
174
+ for (let i = e.length - 1; i >= 0; i--)
175
+ if (e[i] === '-') return e.slice(i + 1)
176
+ return e
177
+ }
178
+
179
+ function encode_filename(filename) {
180
+ // Swap all "!" and "/" characters
181
+ let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
182
+
183
+ // Encode the filename using encodeURIComponent()
184
+ let encoded = encodeURIComponent(swapped)
185
+
186
+ return encoded
187
+ }
188
+
189
+ function within_fiber(id, func) {
190
+ if (!within_fiber.chains) within_fiber.chains = {}
191
+ var prev = within_fiber.chains[id] || Promise.resolve()
192
+ var curr = prev.then(async () => {
193
+ try {
194
+ return await func()
195
+ } finally {
196
+ if (within_fiber.chains[id] === curr)
197
+ delete within_fiber.chains[id]
198
+ }
199
+ })
200
+ return within_fiber.chains[id] = curr
201
+ }
174
202
 
175
- function isAcceptable(contentType, acceptHeader) {
176
- // If no Accept header or Accept is */*, accept everything
177
- if (!acceptHeader || acceptHeader === '*/*') return true;
178
-
179
- // Parse the Accept header into individual media types
180
- const acceptTypes = acceptHeader.split(',').map(type => type.trim());
181
-
182
- for (const acceptType of acceptTypes) {
183
- // Remove quality values (e.g., "text/html;q=0.9" -> "text/html")
184
- const cleanAcceptType = acceptType.split(';')[0].trim();
203
+ async function slurp(req) {
204
+ return await new Promise(done => {
205
+ var chunks = []
206
+ req.on('data', chunk => chunks.push(chunk))
207
+ req.on('end', () => done(Buffer.concat(chunks)))
208
+ })
209
+ }
210
+
211
+ function isAcceptable(contentType, acceptHeader) {
212
+ // If no Accept header or Accept is */*, accept everything
213
+ if (!acceptHeader || acceptHeader === '*/*') return true;
185
214
 
186
- // Exact match
187
- if (cleanAcceptType === contentType) return true;
215
+ // Parse the Accept header into individual media types
216
+ const acceptTypes = acceptHeader.split(',').map(type => type.trim());
188
217
 
189
- // Wildcard subtype match (e.g., "image/*" matches "image/png")
190
- if (cleanAcceptType.endsWith('/*')) {
191
- const acceptMain = cleanAcceptType.slice(0, -2);
192
- const contentMain = contentType.split('/')[0];
193
- if (acceptMain === contentMain) return true;
218
+ for (const acceptType of acceptTypes) {
219
+ // Remove quality values (e.g., "text/html;q=0.9" -> "text/html")
220
+ const cleanAcceptType = acceptType.split(';')[0].trim();
221
+
222
+ // Exact match
223
+ if (cleanAcceptType === contentType) return true;
224
+
225
+ // Wildcard subtype match (e.g., "image/*" matches "image/png")
226
+ if (cleanAcceptType.endsWith('/*')) {
227
+ const acceptMain = cleanAcceptType.slice(0, -2);
228
+ const contentMain = contentType.split('/')[0];
229
+ if (acceptMain === contentMain) return true;
230
+ }
231
+
232
+ // Full wildcard
233
+ if (cleanAcceptType === '*/*') return true;
194
234
  }
195
235
 
196
- // Full wildcard
197
- if (cleanAcceptType === '*/*') return true;
236
+ return false;
198
237
  }
199
-
200
- return false;
238
+
239
+ braid_blob.create_braid_blob = create_braid_blob
240
+
241
+ return braid_blob
201
242
  }
202
243
 
203
- module.exports = braid_blob
244
+ module.exports = create_braid_blob()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
package/test/server.js ADDED
@@ -0,0 +1,43 @@
1
+
2
+ var port = process.argv[2] || 8889
3
+
4
+ var braid_blob = require(`${__dirname}/../index.js`)
5
+ var {free_cors} = require("braid-http")
6
+ braid_blob.db_folder = `${__dirname}/test_db_folder`
7
+
8
+ var server = require("http").createServer(async (req, res) => {
9
+ console.log(`${req.method} ${req.url}`)
10
+
11
+ // Free the CORS
12
+ free_cors(res)
13
+ if (req.method === 'OPTIONS') return
14
+
15
+ if (req.url.startsWith('/eval')) {
16
+ var body = await new Promise(done => {
17
+ var chunks = []
18
+ req.on('data', chunk => chunks.push(chunk))
19
+ req.on('end', () => done(Buffer.concat(chunks)))
20
+ })
21
+ try {
22
+ eval('' + body)
23
+ } catch (error) {
24
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
25
+ res.end(`Error: ${error.message}`)
26
+ }
27
+ return
28
+ }
29
+
30
+ if (req.url.startsWith('/test.html')) {
31
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" })
32
+ require("fs").createReadStream(`${__dirname}/test.html`).pipe(res)
33
+ return
34
+ }
35
+
36
+ // Now serve the collaborative text!
37
+ braid_blob.serve(req, res)
38
+ })
39
+
40
+ // only listen on 'localhost' for security
41
+ server.listen(port, 'localhost', () => {
42
+ console.log(`serving: http://localhost:${port}/test.html`)
43
+ })
package/test/test.html ADDED
@@ -0,0 +1,754 @@
1
+ <style>
2
+ body {
3
+ font-family: Arial, sans-serif;
4
+ max-width: 800px;
5
+ margin: 0 auto;
6
+ padding: 10px;
7
+ }
8
+ .test {
9
+ margin-bottom: 3px;
10
+ padding: 3px;
11
+ }
12
+ .running {
13
+ background-color: #fffde7;
14
+ }
15
+ .passed {
16
+ background-color: #e8f5e9;
17
+ }
18
+ .failed {
19
+ background-color: #ffebee;
20
+ }
21
+ #summaryContainer {
22
+ display: flex;
23
+ flex-wrap: wrap;
24
+ gap: 5px;
25
+ margin-bottom: 20px;
26
+ }
27
+ .summaryBox {
28
+ width: 25px;
29
+ height: 25px;
30
+ border: 1px solid #ddd;
31
+ }
32
+ </style>
33
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
34
+ <div id="summaryContainer"></div>
35
+ <div id="testContainer"></div>
36
+ <script type=module>
37
+
38
+ let delay = 0
39
+
40
+ function createTestDiv(testName) {
41
+ const div = document.createElement("div")
42
+ div.className = "test running"
43
+ div.innerHTML = `<span style="font-weight:bold">${testName}: </span><span class="result">Running...</span>`
44
+ testContainer.appendChild(div)
45
+ return div
46
+ }
47
+
48
+ function updateTestResult(div, passed, message, got, expected) {
49
+ div.className = `test ${passed ? "passed" : "failed"}`
50
+
51
+ if (passed) {
52
+ div.querySelector(".result").textContent = message
53
+ div.querySelector(".result").style.fontSize = message.length > 400 ? 'xx-small' : message.length > 100 ? 'small' : ''
54
+ } else {
55
+ div.querySelector(".result").innerHTML = `${message}<br><strong>Got:</strong> ${got}<br><strong>Expected:</strong> ${expected}`
56
+ }
57
+ }
58
+
59
+ function createSummaryBox() {
60
+ var summaryContainer = document.getElementById('summaryContainer')
61
+ const box = document.createElement('div');
62
+ box.className = 'summaryBox running';
63
+ summaryContainer.appendChild(box);
64
+ return box;
65
+ }
66
+
67
+ function updateSummaryBox(box, passed) {
68
+ box.className = `summaryBox ${passed ? 'passed' : passed === false ? 'failed' : 'other'}`;
69
+ }
70
+
71
+ async function runTest(testName, testFunction, expectedResult) {
72
+ delay += 70
73
+
74
+ await new Promise(done => setTimeout(done, delay))
75
+ const div = createTestDiv(testName)
76
+ const summaryBox = createSummaryBox()
77
+ try {
78
+ let x = await testFunction()
79
+ if (x == expectedResult) {
80
+ updateTestResult(div, true, x)
81
+ updateSummaryBox(summaryBox, true)
82
+ } else {
83
+ updateTestResult(div, false, "Mismatch:", x, expectedResult)
84
+ updateSummaryBox(summaryBox, false)
85
+ }
86
+ } catch (error) {
87
+ updateTestResult(div, false, "Error:", error.message || error, expectedResult)
88
+ updateSummaryBox(summaryBox, false)
89
+ }
90
+ }
91
+
92
+ runTest(
93
+ "test that peer.txt gets initialized on a fresh run",
94
+ async () => {
95
+ var r1 = await braid_fetch(`/eval`, {
96
+ method: 'POST',
97
+ body: `void (async () => {
98
+ var db_name = 'test-db-' + Math.random().toString(36).slice(2)
99
+
100
+ var new_bb = braid_blob.create_braid_blob()
101
+ new_bb.db_folder = __dirname + '/' + db_name
102
+
103
+ try {
104
+ await new_bb.serve({}, {})
105
+ } catch (e) {}
106
+
107
+ await require('fs').promises.rm(new_bb.db_folder,
108
+ { recursive: true, force: true })
109
+
110
+ res.end(new_bb.peer)
111
+
112
+ })()`
113
+ })
114
+ return '' + ((await r1.text()).length > 5)
115
+ },
116
+ 'true'
117
+ )
118
+
119
+ runTest(
120
+ "test that peer is same the second time we run from same db folder",
121
+ async () => {
122
+ var r1 = await braid_fetch(`/eval`, {
123
+ method: 'POST',
124
+ body: `void (async () => {
125
+ var db = __dirname + '/test-db-' + Math.random().toString(36).slice(2)
126
+
127
+ var bb1 = braid_blob.create_braid_blob()
128
+ bb1.db_folder = db
129
+
130
+ try {
131
+ await bb1.serve({}, {})
132
+ } catch (e) {}
133
+
134
+ var bb2 = braid_blob.create_braid_blob()
135
+ bb2.db_folder = db
136
+
137
+ try {
138
+ await bb2.serve({}, {})
139
+ } catch (e) {}
140
+
141
+ await require('fs').promises.rm(db, { recursive: true, force: true })
142
+
143
+ res.end('' + (bb1.peer === bb2.peer))
144
+ })()`
145
+ })
146
+ return await r1.text()
147
+ },
148
+ 'true'
149
+ )
150
+
151
+ runTest(
152
+ "test that we can set the peer of a braid_blob object",
153
+ async () => {
154
+ var r1 = await braid_fetch(`/eval`, {
155
+ method: 'POST',
156
+ body: `void (async () => {
157
+ var db = __dirname + '/test-db-' + Math.random().toString(36).slice(2)
158
+
159
+ var bb1 = braid_blob.create_braid_blob()
160
+ bb1.db_folder = db
161
+ bb1.peer = 'test_peer'
162
+
163
+ try {
164
+ await bb1.serve({}, {})
165
+ } catch (e) {}
166
+
167
+ await require('fs').promises.rm(db, { recursive: true, force: true })
168
+
169
+ res.end(bb1.peer)
170
+ })()`
171
+ })
172
+ return await r1.text()
173
+ },
174
+ 'test_peer'
175
+ )
176
+
177
+ runTest(
178
+ "test that PUTing with shorter event id doesn't do anything.",
179
+ async () => {
180
+ var key = 'test-' + Math.random().toString(36).slice(2)
181
+
182
+ var r = await braid_fetch(`/${key}`, {
183
+ method: 'PUT',
184
+ version: ['11'],
185
+ body: 'xyz'
186
+ })
187
+ if (!r.ok) throw 'got: ' + r.statusCode
188
+
189
+ var r = await braid_fetch(`/${key}`, {
190
+ method: 'PUT',
191
+ version: ['9'],
192
+ body: 'abc'
193
+ })
194
+ if (!r.ok) throw 'got: ' + r.statusCode
195
+
196
+ var r = await braid_fetch(`/${key}`)
197
+ return await r.text()
198
+ },
199
+ 'xyz'
200
+ )
201
+
202
+ runTest(
203
+ "test that we ignore stuff after the ? in a url",
204
+ async () => {
205
+ var key = 'test-' + Math.random().toString(36).slice(2)
206
+
207
+ var r = await braid_fetch(`/${key}?blah`, {
208
+ method: 'PUT',
209
+ version: ['11'],
210
+ body: 'yo!'
211
+ })
212
+ if (!r.ok) throw 'got: ' + r.statusCode
213
+
214
+ var r = await braid_fetch(`/${key}`)
215
+ return await r.text()
216
+ },
217
+ 'yo!'
218
+ )
219
+
220
+ runTest(
221
+ "test that we ignore stuff after the # in a url",
222
+ async () => {
223
+ var key = 'test-' + Math.random().toString(36).slice(2)
224
+
225
+ var r = await braid_fetch(`/${key}#blah?bloop`, {
226
+ method: 'PUT',
227
+ version: ['11'],
228
+ body: 'hi!'
229
+ })
230
+ if (!r.ok) throw 'got: ' + r.statusCode
231
+
232
+ var r = await braid_fetch(`/${key}`)
233
+ return await r.text()
234
+ },
235
+ 'hi!'
236
+ )
237
+
238
+ runTest(
239
+ "test send an update to another peer",
240
+ async () => {
241
+ var key = 'test-' + Math.random().toString(36).slice(2)
242
+
243
+ var r = await braid_fetch(`/${key}`, {
244
+ method: 'PUT',
245
+ headers: {'Content-Type': 'text/plain'},
246
+ version: ['1'],
247
+ body: 'xyz'
248
+ })
249
+ if (!r.ok) throw 'got: ' + r.statusCode
250
+
251
+ var a = new AbortController()
252
+ var r = await braid_fetch(`/${key}`, {
253
+ signal: a.signal,
254
+ subscribe: true,
255
+ peer: key
256
+ })
257
+
258
+ var p = new Promise(done => {
259
+ r.subscribe(update => {
260
+ if (update.version?.[0] !== '2') return
261
+ done(update.body_text)
262
+ a.abort()
263
+ })
264
+ })
265
+
266
+ var r = await braid_fetch(`/${key}`, {
267
+ method: 'PUT',
268
+ headers: {'Content-Type': 'text/plain'},
269
+ version: ['2'],
270
+ body: 'abc'
271
+ })
272
+ if (!r.ok) throw 'got: ' + r.statusCode
273
+
274
+ return await p
275
+ },
276
+ 'abc'
277
+ )
278
+
279
+ runTest(
280
+ "test having multiple subs",
281
+ async () => {
282
+ var key = 'test-' + Math.random().toString(36).slice(2)
283
+ var key2 = 'test2-' + Math.random().toString(36).slice(2)
284
+
285
+ var r = await braid_fetch(`/${key}`, {
286
+ method: 'PUT',
287
+ headers: {'Content-Type': 'text/plain'},
288
+ version: ['1'],
289
+ body: 'xyz'
290
+ })
291
+ if (!r.ok) throw 'got: ' + r.statusCode
292
+
293
+ var a = new AbortController()
294
+ var r = await braid_fetch(`/${key}`, {
295
+ signal: a.signal,
296
+ subscribe: true,
297
+ peer: key
298
+ })
299
+
300
+ var p = new Promise(done => {
301
+ r.subscribe(update => {
302
+ if (update.version?.[0] !== '2') return
303
+ done(update.body_text)
304
+ })
305
+ })
306
+
307
+ var r = await braid_fetch(`/${key2}`, {
308
+ method: 'PUT',
309
+ headers: {'Content-Type': 'text/plain'},
310
+ version: ['1'],
311
+ body: 'xyz2'
312
+ })
313
+ if (!r.ok) throw 'got: ' + r.statusCode
314
+
315
+ var a = new AbortController()
316
+ var r = await braid_fetch(`/${key2}`, {
317
+ signal: a.signal,
318
+ subscribe: true,
319
+ peer: key2
320
+ })
321
+
322
+ var p2 = new Promise(done => {
323
+ r.subscribe(update => {
324
+ if (update.version?.[0] !== '2') return
325
+ done(update.body_text)
326
+ })
327
+ })
328
+
329
+ var r = await braid_fetch(`/${key}`, {
330
+ method: 'PUT',
331
+ headers: {'Content-Type': 'text/plain'},
332
+ version: ['2'],
333
+ body: 'abc'
334
+ })
335
+ if (!r.ok) throw 'got: ' + r.statusCode
336
+
337
+ var r = await braid_fetch(`/${key2}`, {
338
+ method: 'PUT',
339
+ headers: {'Content-Type': 'text/plain'},
340
+ version: ['2'],
341
+ body: 'abc2'
342
+ })
343
+ if (!r.ok) throw 'got: ' + r.statusCode
344
+
345
+ var ret = await Promise.all([p, p2])
346
+ a.abort()
347
+ return 'got: ' + ret
348
+
349
+ },
350
+ 'got: abc,abc2'
351
+ )
352
+
353
+ runTest(
354
+ "test getting a 406",
355
+ async () => {
356
+ var key = 'test-' + Math.random().toString(36).slice(2)
357
+
358
+ var r = await braid_fetch(`/${key}`, {
359
+ method: 'PUT',
360
+ headers: {'Content-Type': 'text/plain'},
361
+ version: ['1'],
362
+ parents: [],
363
+ body: 'xyz'
364
+ })
365
+ if (!r.ok) throw 'got: ' + r.statusCode
366
+
367
+ var r = await braid_fetch(`/${key}`, {
368
+ headers: {Accept: 'text/html'}
369
+ })
370
+ return r.status + ' ' + await r.text()
371
+ },
372
+ '406 Content-Type of text/plain not in Accept: text/html'
373
+ )
374
+
375
+ runTest(
376
+ "test deleting something",
377
+ async () => {
378
+ var key = 'test-' + Math.random().toString(36).slice(2)
379
+
380
+ var r = await braid_fetch(`/${key}`, {
381
+ method: 'PUT',
382
+ version: ['1'],
383
+ parents: [],
384
+ body: 'xyz'
385
+ })
386
+ if (!r.ok) throw 'got: ' + r.statusCode
387
+
388
+ var r = await braid_fetch(`/${key}`, {
389
+ method: 'DELETE',
390
+ })
391
+ if (!r.ok) throw 'got: ' + r.statusCode
392
+
393
+ var r = await braid_fetch(`/${key}`)
394
+ return r.status
395
+ },
396
+ '404'
397
+ )
398
+
399
+ runTest(
400
+ "test deleting something that doesn't exist",
401
+ async () => {
402
+ var key = 'test-' + Math.random().toString(36).slice(2)
403
+
404
+ var r = await braid_fetch(`/${key}`, {
405
+ method: 'DELETE',
406
+ })
407
+ if (!r.ok) throw 'got: ' + r.statusCode
408
+
409
+ return r.status
410
+ },
411
+ '204'
412
+ )
413
+
414
+ runTest(
415
+ "test that subscribe returns current-version header",
416
+ async () => {
417
+ var key = 'test-' + Math.random().toString(36).slice(2)
418
+
419
+ var r = await braid_fetch(`/${key}`, {
420
+ method: 'PUT',
421
+ version: ['1'],
422
+ parents: [],
423
+ body: 'xyz'
424
+ })
425
+ if (!r.ok) throw 'got: ' + r.statusCode
426
+
427
+ var a = new AbortController()
428
+ var r = await braid_fetch(`/${key}`, {
429
+ signal: a.signal,
430
+ subscribe: true
431
+ })
432
+ a.abort()
433
+ return r.headers.get('current-version')
434
+ },
435
+ '"1"'
436
+ )
437
+
438
+ runTest(
439
+ "test that subscribe returns version as string-number in array",
440
+ async () => {
441
+ var key = 'test-' + Math.random().toString(36).slice(2)
442
+
443
+ var r = await braid_fetch(`/${key}`, {
444
+ method: 'PUT',
445
+ version: ['1'],
446
+ parents: [],
447
+ body: 'xyz'
448
+ })
449
+ if (!r.ok) throw 'got: ' + r.statusCode
450
+
451
+ var a = new AbortController()
452
+ var r = await braid_fetch(`/${key}`, {
453
+ signal: a.signal,
454
+ subscribe: true
455
+ })
456
+
457
+ var x = await new Promise(done => {
458
+ r.subscribe(update => {
459
+ done(update.version)
460
+ })
461
+ })
462
+
463
+ a.abort()
464
+ return JSON.stringify(x)
465
+ },
466
+ '["1"]'
467
+ )
468
+
469
+ runTest(
470
+ "test that subscribe's update's versions are string-number in array",
471
+ async () => {
472
+ var key = 'test-' + Math.random().toString(36).slice(2)
473
+
474
+ var r = await braid_fetch(`/${key}`, {
475
+ method: 'PUT',
476
+ version: ['4'],
477
+ body: 'xyz'
478
+ })
479
+ if (!r.ok) throw 'got: ' + r.statusCode
480
+
481
+ var a = new AbortController()
482
+ var r = await braid_fetch(`/${key}`, {
483
+ signal: a.signal,
484
+ subscribe: true,
485
+ parents: ['0']
486
+ })
487
+
488
+ var p = new Promise(done => {
489
+ r.subscribe(update => {
490
+ done(update.version)
491
+ })
492
+ })
493
+
494
+ var ret = await p
495
+ a.abort()
496
+ return JSON.stringify(ret)
497
+ },
498
+ '["4"]'
499
+ )
500
+
501
+ runTest(
502
+ "test that non-subscribe returns version header",
503
+ async () => {
504
+ var key = 'test-' + Math.random().toString(36).slice(2)
505
+
506
+ var r = await braid_fetch(`/${key}`, {
507
+ method: 'PUT',
508
+ version: ['2'],
509
+ parents: [],
510
+ body: 'xyz'
511
+ })
512
+ if (!r.ok) throw 'got: ' + r.statusCode
513
+
514
+ var r = await braid_fetch(`/${key}`)
515
+ return r.headers.get('version')
516
+ },
517
+ '"2"'
518
+ )
519
+
520
+ runTest(
521
+ "test that PUTing at version [] doesn't do anything.",
522
+ async () => {
523
+ var key = 'test-' + Math.random().toString(36).slice(2)
524
+
525
+ var r = await braid_fetch(`/${key}`, {
526
+ method: 'PUT',
527
+ version: [],
528
+ parents: [],
529
+ body: 'xyz'
530
+ })
531
+ if (!r.ok) throw 'got: ' + r.statusCode
532
+
533
+ var r = await braid_fetch(`/${key}`)
534
+ return r.status
535
+ },
536
+ '404'
537
+ )
538
+
539
+ runTest(
540
+ "test that subscribe sends no version if parents is big enough.",
541
+ async () => {
542
+ var key = 'test-' + Math.random().toString(36).slice(2)
543
+
544
+ var r = await braid_fetch(`/${key}`, {
545
+ method: 'PUT',
546
+ version: ['3'],
547
+ parents: [],
548
+ body: 'xyz'
549
+ })
550
+ if (!r.ok) throw 'got: ' + r.statusCode
551
+
552
+ var a = new AbortController()
553
+ var r = await braid_fetch(`/${key}`, {
554
+ signal: a.signal,
555
+ subscribe: true,
556
+ parents: ['3']
557
+ })
558
+
559
+ var received_update = false
560
+ var promise_a = new Promise(done => {
561
+ r.subscribe(async (update) => {
562
+ received_update = true
563
+ done()
564
+ })
565
+ })
566
+
567
+ var promise_b = new Promise(done => setTimeout(done, 300))
568
+
569
+ await Promise.race([promise_a, promise_b])
570
+ a.abort()
571
+
572
+ return '' + received_update
573
+ },
574
+ 'false'
575
+ )
576
+
577
+ runTest(
578
+ "test that subscribe sends 404 if there is no file.",
579
+ async () => {
580
+ var key = 'test-' + Math.random().toString(36).slice(2)
581
+
582
+ var r = await braid_fetch(`/${key}`, {
583
+ subscribe: true,
584
+ })
585
+ return r.status
586
+ },
587
+ '404'
588
+ )
589
+
590
+ runTest(
591
+ "test that we get 404 when file doesn't exist, on GET without subscribe.",
592
+ async () => {
593
+ var key = 'test-' + Math.random().toString(36).slice(2)
594
+
595
+ var r = await braid_fetch(`/${key}`)
596
+
597
+ return `${r.status}`
598
+ },
599
+ '404'
600
+ )
601
+
602
+ runTest(
603
+ "test second subscription to same key",
604
+ async () => {
605
+ var key = 'test-' + Math.random().toString(36).slice(2)
606
+
607
+ var r = await braid_fetch(`/${key}`, {
608
+ method: 'PUT',
609
+ version: ['3'],
610
+ parents: [],
611
+ body: 'xyz'
612
+ })
613
+ if (!r.ok) throw 'got: ' + r.statusCode
614
+
615
+ var a = new AbortController()
616
+ var r = await braid_fetch(`/${key}`, {
617
+ signal: a.signal,
618
+ subscribe: true,
619
+ parents: ['3']
620
+ })
621
+
622
+ var a2 = new AbortController()
623
+ var r2 = await braid_fetch(`/${key}`, {
624
+ signal: a2.signal,
625
+ subscribe: true,
626
+ parents: ['2']
627
+ })
628
+
629
+ var body = await new Promise(done => {
630
+ r2.subscribe((update) => done(update.body_text))
631
+ })
632
+
633
+ a.abort()
634
+ a2.abort()
635
+ return body
636
+ },
637
+ 'xyz'
638
+ )
639
+
640
+ runTest(
641
+ "test PUTing when server already has blob",
642
+ async () => {
643
+ var key = 'test-' + Math.random().toString(36).slice(2)
644
+
645
+ var r = await braid_fetch(`/${key}`, {
646
+ method: 'PUT',
647
+ version: ['3'],
648
+ parents: [],
649
+ body: 'xyz'
650
+ })
651
+ if (!r.ok) throw 'got: ' + r.statusCode
652
+
653
+ var r = await braid_fetch(`/${key}`, {
654
+ method: 'PUT',
655
+ version: ['4'],
656
+ parents: [],
657
+ body: 'XYZ'
658
+ })
659
+ if (!r.ok) throw 'got: ' + r.statusCode
660
+
661
+ return await (await braid_fetch(`/${key}`)).text()
662
+ },
663
+ 'XYZ'
664
+ )
665
+
666
+ runTest(
667
+ "test PUTing when server has newer version",
668
+ async () => {
669
+ var key = 'test-' + Math.random().toString(36).slice(2)
670
+
671
+ var r = await braid_fetch(`/${key}`, {
672
+ method: 'PUT',
673
+ version: ['3'],
674
+ parents: [],
675
+ body: 'xyz'
676
+ })
677
+ if (!r.ok) throw 'got: ' + r.statusCode
678
+
679
+ var r = await braid_fetch(`/${key}`, {
680
+ method: 'PUT',
681
+ version: ['2'],
682
+ parents: [],
683
+ body: 'XYZ'
684
+ })
685
+ if (!r.ok) throw 'got: ' + r.statusCode
686
+
687
+ return r.headers.get('version')
688
+ },
689
+ '"3"'
690
+ )
691
+
692
+ runTest(
693
+ "test that version we get back is the version we set",
694
+ async () => {
695
+ var key = 'test-' + Math.random().toString(36).slice(2)
696
+
697
+ var r = await braid_fetch(`/${key}`, {
698
+ method: 'PUT',
699
+ version: ['1760077018883'],
700
+ parents: [],
701
+ body: 'xyz'
702
+ })
703
+ if (!r.ok) throw 'got: ' + r.statusCode
704
+
705
+ var r = await braid_fetch(`/${key}`)
706
+ return r.headers.get('version')
707
+ },
708
+ '"1760077018883"'
709
+ )
710
+
711
+ runTest(
712
+ "test that subscribe gets back editable:true.",
713
+ async () => {
714
+ var key = 'test-' + Math.random().toString(36).slice(2)
715
+
716
+ var r = await braid_fetch(`/${key}`, {
717
+ method: 'PUT',
718
+ body: 'xyz'
719
+ })
720
+ if (!r.ok) throw 'got: ' + r.statusCode
721
+
722
+ var a = new AbortController()
723
+ var r = await braid_fetch(`/${key}`, {
724
+ signal: a.signal,
725
+ subscribe: true,
726
+ parents: ['3']
727
+ })
728
+
729
+ var ret = r.headers.get('editable')
730
+ a.abort()
731
+ return '' + ret
732
+ },
733
+ 'true'
734
+ )
735
+
736
+ runTest(
737
+ "test that we can override editable on the server.",
738
+ async () => {
739
+ var r1 = await braid_fetch(`/eval`, {
740
+ method: 'POST',
741
+ subscribe: true,
742
+ body: `void (async () => {
743
+ req.method = 'GET'
744
+ res.setHeader('editable', 'false')
745
+ braid_blob.serve(req, res, {key: ':test'})
746
+ })()`
747
+ })
748
+ return r1.headers.get('editable')
749
+
750
+ },
751
+ 'false'
752
+ )
753
+
754
+ </script>