braid-blob 0.0.17 → 0.0.19
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/.claude/settings.local.json +3 -1
- package/index.js +222 -61
- package/package.json +2 -2
- package/server-demo.js +3 -1
- package/test/tests.js +454 -0
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var {http_server: braidify,
|
|
1
|
+
var {http_server: braidify, fetch: braid_fetch} = require('braid-http'),
|
|
2
2
|
{url_file_db} = require('url-file-db'),
|
|
3
3
|
fs = require('fs'),
|
|
4
4
|
path = require('path')
|
|
@@ -10,7 +10,8 @@ function create_braid_blob() {
|
|
|
10
10
|
cache: {},
|
|
11
11
|
key_to_subs: {},
|
|
12
12
|
peer: null, // we'll try to load this from a file, if not set by the user
|
|
13
|
-
db: null // url-file-db instance
|
|
13
|
+
db: null, // url-file-db instance for blob storage
|
|
14
|
+
meta_db: null // url-file-db instance for meta storage
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
braid_blob.init = async () => {
|
|
@@ -27,10 +28,11 @@ function create_braid_blob() {
|
|
|
27
28
|
await braid_blob.put(key, body, { skip_write: true })
|
|
28
29
|
})
|
|
29
30
|
|
|
30
|
-
// Create meta
|
|
31
|
-
|
|
31
|
+
// Create url-file-db instance for meta storage (in a subfolder)
|
|
32
|
+
// This will create both meta_folder and the db subfolder with recursive: true
|
|
33
|
+
braid_blob.meta_db = await url_file_db.create(`${braid_blob.meta_folder}/db`)
|
|
32
34
|
|
|
33
|
-
// establish a peer id
|
|
35
|
+
// establish a peer id (stored at root of meta_folder, sibling to db subfolder)
|
|
34
36
|
if (!braid_blob.peer)
|
|
35
37
|
try {
|
|
36
38
|
braid_blob.peer = await fs.promises.readFile(`${braid_blob.meta_folder}/peer.txt`, 'utf8')
|
|
@@ -42,14 +44,36 @@ function create_braid_blob() {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
braid_blob.put = async (key, body, options = {}) => {
|
|
47
|
+
// Handle URL case - make a remote PUT request
|
|
48
|
+
if (key instanceof URL) {
|
|
49
|
+
options.my_abort = new AbortController()
|
|
50
|
+
if (options.signal) {
|
|
51
|
+
options.signal.addEventListener('abort', () =>
|
|
52
|
+
options.my_abort.abort())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var params = {
|
|
56
|
+
method: 'PUT',
|
|
57
|
+
signal: options.my_abort.signal,
|
|
58
|
+
retry: () => true,
|
|
59
|
+
body: body
|
|
60
|
+
}
|
|
61
|
+
for (var x of ['headers', 'version', 'peer'])
|
|
62
|
+
if (options[x] != null) params[x] = options[x]
|
|
63
|
+
if (options.content_type) {
|
|
64
|
+
params.headers = { ...params.headers, 'Content-Type': options.content_type }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return await braid_fetch(key.href, params)
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
await braid_blob.init()
|
|
46
71
|
|
|
47
|
-
// Read the meta
|
|
48
|
-
const metaname = `${braid_blob.meta_folder}/${encode_filename(key)}`
|
|
72
|
+
// Read the meta data from meta_db
|
|
49
73
|
var meta = {}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
var meta_content = await braid_blob.meta_db.read(key)
|
|
75
|
+
if (meta_content)
|
|
76
|
+
meta = JSON.parse(meta_content.toString('utf8'))
|
|
53
77
|
|
|
54
78
|
var their_e =
|
|
55
79
|
!options.version ?
|
|
@@ -69,11 +93,11 @@ function create_braid_blob() {
|
|
|
69
93
|
if (!options.skip_write)
|
|
70
94
|
await braid_blob.db.write(key, body)
|
|
71
95
|
|
|
72
|
-
// Write the meta
|
|
96
|
+
// Write the meta data
|
|
73
97
|
if (options.content_type)
|
|
74
98
|
meta.content_type = options.content_type
|
|
75
99
|
|
|
76
|
-
await
|
|
100
|
+
await braid_blob.meta_db.write(key, JSON.stringify(meta))
|
|
77
101
|
|
|
78
102
|
// Notify all subscriptions of the update
|
|
79
103
|
// (except the peer which made the PUT request itself)
|
|
@@ -91,14 +115,49 @@ function create_braid_blob() {
|
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
braid_blob.get = async (key, options = {}) => {
|
|
118
|
+
// Handle URL case - make a remote GET request
|
|
119
|
+
if (key instanceof URL) {
|
|
120
|
+
options.my_abort = new AbortController()
|
|
121
|
+
|
|
122
|
+
var params = {
|
|
123
|
+
signal: options.my_abort.signal,
|
|
124
|
+
subscribe: !!options.subscribe,
|
|
125
|
+
heartbeats: 120,
|
|
126
|
+
}
|
|
127
|
+
if (!options.dont_retry) {
|
|
128
|
+
params.retry = () => true
|
|
129
|
+
}
|
|
130
|
+
for (var x of ['headers', 'parents', 'version', 'peer'])
|
|
131
|
+
if (options[x] != null) params[x] = options[x]
|
|
132
|
+
|
|
133
|
+
var res = await braid_fetch(key.href, params)
|
|
134
|
+
|
|
135
|
+
if (options.subscribe) {
|
|
136
|
+
if (options.dont_retry) {
|
|
137
|
+
var error_happened
|
|
138
|
+
var error_promise = new Promise((_, fail) => error_happened = fail)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
res.subscribe(async update => {
|
|
142
|
+
await options.subscribe(update)
|
|
143
|
+
}, e => options.dont_retry && error_happened(e))
|
|
144
|
+
|
|
145
|
+
if (options.dont_retry) {
|
|
146
|
+
return await error_promise
|
|
147
|
+
}
|
|
148
|
+
return res
|
|
149
|
+
} else {
|
|
150
|
+
return await res.arrayBuffer()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
94
154
|
await braid_blob.init()
|
|
95
155
|
|
|
96
|
-
// Read the meta
|
|
97
|
-
const metaname = `${braid_blob.meta_folder}/${encode_filename(key)}`
|
|
156
|
+
// Read the meta data from meta_db
|
|
98
157
|
var meta = {}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
158
|
+
var meta_content = await braid_blob.meta_db.read(key)
|
|
159
|
+
if (meta_content)
|
|
160
|
+
meta = JSON.parse(meta_content.toString('utf8'))
|
|
102
161
|
if (meta.event == null) return null
|
|
103
162
|
|
|
104
163
|
var result = {
|
|
@@ -106,6 +165,11 @@ function create_braid_blob() {
|
|
|
106
165
|
content_type: meta.content_type
|
|
107
166
|
}
|
|
108
167
|
if (options.header_cb) await options.header_cb(result)
|
|
168
|
+
// Check if requested version/parents is newer than what we have - if so, we don't have it
|
|
169
|
+
if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
|
|
170
|
+
throw new Error('unkown version: ' + options.version)
|
|
171
|
+
if (options.parents && options.parents.length && compare_events(options.parents[0], meta.event) > 0)
|
|
172
|
+
throw new Error('unkown version: ' + options.parents)
|
|
109
173
|
if (options.head) return
|
|
110
174
|
|
|
111
175
|
if (options.subscribe) {
|
|
@@ -171,40 +235,48 @@ function create_braid_blob() {
|
|
|
171
235
|
var body = req.method === 'PUT' && await slurp(req)
|
|
172
236
|
|
|
173
237
|
await within_fiber(options.key, async () => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Read the meta file
|
|
238
|
+
// Read the meta data from meta_db
|
|
177
239
|
var meta = {}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
240
|
+
var meta_content = await braid_blob.meta_db.read(options.key)
|
|
241
|
+
if (meta_content)
|
|
242
|
+
meta = JSON.parse(meta_content.toString('utf8'))
|
|
181
243
|
|
|
182
|
-
if (req.method === 'GET') {
|
|
244
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
183
245
|
if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
|
|
184
246
|
if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
|
|
185
247
|
res.setHeader("Merge-Type", "lww")
|
|
186
248
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
249
|
+
try {
|
|
250
|
+
var result = await braid_blob.get(options.key, {
|
|
251
|
+
peer: req.peer,
|
|
252
|
+
head: req.method == "HEAD",
|
|
253
|
+
version: req.version || null,
|
|
254
|
+
parents: req.parents || null,
|
|
255
|
+
header_cb: (result) => {
|
|
256
|
+
res.setHeader((req.subscribe ? "Current-" : "") +
|
|
257
|
+
"Version", ascii_ify(result.version.map((x) =>
|
|
258
|
+
JSON.stringify(x)).join(", ")))
|
|
259
|
+
if (result.content_type)
|
|
260
|
+
res.setHeader('Content-Type', result.content_type)
|
|
261
|
+
},
|
|
262
|
+
before_send_cb: (result) =>
|
|
263
|
+
res.startSubscription({ onClose: result.unsubscribe }),
|
|
264
|
+
subscribe: req.subscribe ? (update) => {
|
|
265
|
+
res.sendUpdate({
|
|
266
|
+
version: update.version,
|
|
267
|
+
'Merge-Type': 'lww',
|
|
268
|
+
body: update.body
|
|
269
|
+
})
|
|
270
|
+
} : null
|
|
271
|
+
})
|
|
272
|
+
} catch (e) {
|
|
273
|
+
if (e.message && e.message.startsWith('unkown version')) {
|
|
274
|
+
// Server doesn't have this version
|
|
275
|
+
res.statusCode = 309
|
|
276
|
+
res.statusMessage = 'Version Unknown Here'
|
|
277
|
+
return res.end('')
|
|
278
|
+
} else throw e
|
|
279
|
+
}
|
|
208
280
|
|
|
209
281
|
if (!result) {
|
|
210
282
|
res.statusCode = 404
|
|
@@ -234,18 +306,117 @@ function create_braid_blob() {
|
|
|
234
306
|
res.setHeader("Version", version_to_header(meta.event != null ? [meta.event] : []))
|
|
235
307
|
res.end('')
|
|
236
308
|
} else if (req.method === 'DELETE') {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
} catch (e) {}
|
|
240
|
-
try {
|
|
241
|
-
await fs.promises.unlink(metaname)
|
|
242
|
-
} catch (e) {}
|
|
309
|
+
await braid_blob.db.delete(options.key)
|
|
310
|
+
await braid_blob.meta_db.delete(options.key)
|
|
243
311
|
res.statusCode = 204 // No Content
|
|
244
312
|
res.end('')
|
|
245
313
|
}
|
|
246
314
|
})
|
|
247
315
|
}
|
|
248
316
|
|
|
317
|
+
braid_blob.sync = async (a, b, options = {}) => {
|
|
318
|
+
var unsync_cbs = []
|
|
319
|
+
options.my_unsync = () => unsync_cbs.forEach(cb => cb())
|
|
320
|
+
|
|
321
|
+
if ((a instanceof URL) === (b instanceof URL)) {
|
|
322
|
+
// Both are URLs or both are local keys
|
|
323
|
+
var a_ops = {
|
|
324
|
+
subscribe: update => braid_blob.put(b, update.body, {
|
|
325
|
+
version: update.version,
|
|
326
|
+
content_type: update.headers?.['content-type']
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
braid_blob.get(a, a_ops)
|
|
330
|
+
|
|
331
|
+
var b_ops = {
|
|
332
|
+
subscribe: update => braid_blob.put(a, update.body, {
|
|
333
|
+
version: update.version,
|
|
334
|
+
content_type: update.headers?.['content-type']
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
braid_blob.get(b, b_ops)
|
|
338
|
+
} else {
|
|
339
|
+
// One is local, one is remote - make a=local and b=remote (swap if not)
|
|
340
|
+
if (a instanceof URL) {
|
|
341
|
+
let swap = a; a = b; b = swap
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
var closed = false
|
|
345
|
+
options.my_unsync = () => { closed = true; disconnect() }
|
|
346
|
+
|
|
347
|
+
var disconnect = () => { }
|
|
348
|
+
async function connect() {
|
|
349
|
+
var ac = new AbortController()
|
|
350
|
+
var disconnect_cbs = [() => ac.abort()]
|
|
351
|
+
disconnect = () => disconnect_cbs.forEach(cb => cb())
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
// Check if remote has our current version (simple fork-point check)
|
|
355
|
+
var local_result = await braid_blob.get(a)
|
|
356
|
+
var local_version = local_result ? local_result.version : null
|
|
357
|
+
var server_has_our_version = false
|
|
358
|
+
|
|
359
|
+
if (local_version) {
|
|
360
|
+
// Check if server has our version
|
|
361
|
+
var r = await braid_fetch(b.href, {
|
|
362
|
+
signal: ac.signal,
|
|
363
|
+
method: "HEAD",
|
|
364
|
+
version: local_version
|
|
365
|
+
})
|
|
366
|
+
server_has_our_version = r.ok
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Local -> remote: subscribe to future local changes
|
|
370
|
+
var a_ops = {
|
|
371
|
+
subscribe: update => {
|
|
372
|
+
update.signal = ac.signal
|
|
373
|
+
braid_blob.put(b, update.body, {
|
|
374
|
+
version: update.version,
|
|
375
|
+
content_type: update.content_type
|
|
376
|
+
}).catch(e => {
|
|
377
|
+
if (e.name === 'AbortError') {
|
|
378
|
+
// ignore
|
|
379
|
+
} else throw e
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Only set parents if server already has our version
|
|
384
|
+
// If server doesn't have it, omit parents so subscription sends everything
|
|
385
|
+
if (server_has_our_version) {
|
|
386
|
+
a_ops.parents = local_version
|
|
387
|
+
}
|
|
388
|
+
braid_blob.get(a, a_ops)
|
|
389
|
+
|
|
390
|
+
// Remote -> local: subscribe to remote updates
|
|
391
|
+
var b_ops = {
|
|
392
|
+
dont_retry: true,
|
|
393
|
+
subscribe: async update => {
|
|
394
|
+
await braid_blob.put(a, update.body, {
|
|
395
|
+
version: update.version,
|
|
396
|
+
content_type: update.headers?.['content-type']
|
|
397
|
+
})
|
|
398
|
+
},
|
|
399
|
+
}
|
|
400
|
+
// Use fork-point (parents) to avoid receiving data we already have
|
|
401
|
+
if (local_version) {
|
|
402
|
+
b_ops.parents = local_version
|
|
403
|
+
}
|
|
404
|
+
// NOTE: this should not return, but it might throw
|
|
405
|
+
await braid_blob.get(b, b_ops)
|
|
406
|
+
} catch (e) {
|
|
407
|
+
if (closed) {
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
disconnect()
|
|
412
|
+
console.log(`disconnected, retrying in 1 second`)
|
|
413
|
+
setTimeout(connect, 1000)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
connect()
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
249
420
|
function compare_events(a, b) {
|
|
250
421
|
var a_num = get_event_seq(a)
|
|
251
422
|
var b_num = get_event_seq(b)
|
|
@@ -275,16 +446,6 @@ function create_braid_blob() {
|
|
|
275
446
|
return ascii_ify(version.map(v => JSON.stringify(v)).join(', '))
|
|
276
447
|
}
|
|
277
448
|
|
|
278
|
-
function encode_filename(filename) {
|
|
279
|
-
// Swap all "!" and "/" characters
|
|
280
|
-
let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
|
|
281
|
-
|
|
282
|
-
// Encode the filename using encodeURIComponent()
|
|
283
|
-
let encoded = encodeURIComponent(swapped)
|
|
284
|
-
|
|
285
|
-
return encoded
|
|
286
|
-
}
|
|
287
|
-
|
|
288
449
|
function within_fiber(id, func) {
|
|
289
450
|
if (!within_fiber.chains) within_fiber.chains = {}
|
|
290
451
|
var prev = within_fiber.chains[id] || Promise.resolve()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-blob",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Library for collaborative blobs over http using braid.",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braid-blob",
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"braid-http": "~1.3.82",
|
|
14
|
-
"url-file-db": "~0.0.
|
|
14
|
+
"url-file-db": "~0.0.8"
|
|
15
15
|
}
|
|
16
16
|
}
|
package/server-demo.js
CHANGED
|
@@ -8,6 +8,8 @@ var braid_blob = require(`${__dirname}/index.js`)
|
|
|
8
8
|
// braid_blob.db_folder = './custom_files_folder'
|
|
9
9
|
// braid_blob.meta_folder = './custom_meta_folder'
|
|
10
10
|
|
|
11
|
+
braid_blob.init()
|
|
12
|
+
|
|
11
13
|
var server = require("http").createServer(async (req, res) => {
|
|
12
14
|
console.log(`${req.method} ${req.url}`)
|
|
13
15
|
|
|
@@ -19,5 +21,5 @@ server.listen(port, () => {
|
|
|
19
21
|
console.log(`files stored in: ${braid_blob.db_folder}`)
|
|
20
22
|
})
|
|
21
23
|
|
|
22
|
-
// curl -X PUT --data-binary @blob.png http://localhost:8888/blob.png
|
|
24
|
+
// curl -X PUT -H "Content-Type: image/png" --data-binary @blob.png http://localhost:8888/blob.png
|
|
23
25
|
// curl http://localhost:8888/blob.png --output new-blob.png
|
package/test/tests.js
CHANGED
|
@@ -688,6 +688,460 @@ runTest(
|
|
|
688
688
|
'false'
|
|
689
689
|
)
|
|
690
690
|
|
|
691
|
+
runTest(
|
|
692
|
+
"test that meta filenames distinguish between 'a' and 'A' on case-insensitive filesystems",
|
|
693
|
+
async () => {
|
|
694
|
+
var suffix = Math.random().toString(36).slice(2)
|
|
695
|
+
var key1 = 'test-' + suffix + '-a'
|
|
696
|
+
var key2 = 'test-' + suffix + '-A'
|
|
697
|
+
|
|
698
|
+
// PUT to lowercase key with version 100
|
|
699
|
+
var r = await braid_fetch(`/${key1}`, {
|
|
700
|
+
method: 'PUT',
|
|
701
|
+
version: ['100'],
|
|
702
|
+
body: 'lowercase content'
|
|
703
|
+
})
|
|
704
|
+
if (!r.ok) throw 'PUT to lowercase key failed: ' + r.status
|
|
705
|
+
|
|
706
|
+
// PUT to uppercase key with version 200
|
|
707
|
+
var r = await braid_fetch(`/${key2}`, {
|
|
708
|
+
method: 'PUT',
|
|
709
|
+
version: ['200'],
|
|
710
|
+
body: 'uppercase content'
|
|
711
|
+
})
|
|
712
|
+
if (!r.ok) throw 'PUT to uppercase key failed: ' + r.status
|
|
713
|
+
|
|
714
|
+
// GET both and verify they have different versions (stored in meta files)
|
|
715
|
+
var r1 = await braid_fetch(`/${key1}`)
|
|
716
|
+
if (!r1.ok) throw 'GET lowercase key failed: ' + r1.status
|
|
717
|
+
var version1 = r1.headers.get('version')
|
|
718
|
+
|
|
719
|
+
var r2 = await braid_fetch(`/${key2}`)
|
|
720
|
+
if (!r2.ok) throw 'GET uppercase key failed: ' + r2.status
|
|
721
|
+
var version2 = r2.headers.get('version')
|
|
722
|
+
|
|
723
|
+
return version1 + '|' + version2
|
|
724
|
+
},
|
|
725
|
+
'"100"|"200"'
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
runTest(
|
|
729
|
+
"test put with URL (no content_type)",
|
|
730
|
+
async () => {
|
|
731
|
+
var key = 'test-url-put-' + Math.random().toString(36).slice(2)
|
|
732
|
+
|
|
733
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
734
|
+
method: 'POST',
|
|
735
|
+
body: `void (async () => {
|
|
736
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
737
|
+
var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
|
|
738
|
+
await braid_blob.put(url, Buffer.from('url put test'), { version: ['100'] })
|
|
739
|
+
res.end('done')
|
|
740
|
+
})()`
|
|
741
|
+
})
|
|
742
|
+
await r1.text()
|
|
743
|
+
|
|
744
|
+
var r = await braid_fetch(`/${key}`)
|
|
745
|
+
return await r.text()
|
|
746
|
+
},
|
|
747
|
+
'url put test'
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
runTest(
|
|
751
|
+
"test put with URL (with content_type)",
|
|
752
|
+
async () => {
|
|
753
|
+
var key = 'test-url-put-ct-' + Math.random().toString(36).slice(2)
|
|
754
|
+
|
|
755
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
756
|
+
method: 'POST',
|
|
757
|
+
body: `void (async () => {
|
|
758
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
759
|
+
var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
|
|
760
|
+
await braid_blob.put(url, Buffer.from('url put with ct'), {
|
|
761
|
+
version: ['200'],
|
|
762
|
+
content_type: 'text/plain'
|
|
763
|
+
})
|
|
764
|
+
res.end('done')
|
|
765
|
+
})()`
|
|
766
|
+
})
|
|
767
|
+
await r1.text()
|
|
768
|
+
|
|
769
|
+
var r = await braid_fetch(`/${key}`)
|
|
770
|
+
return r.headers.get('content-type') + '|' + await r.text()
|
|
771
|
+
},
|
|
772
|
+
'text/plain|url put with ct'
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
runTest(
|
|
776
|
+
"test get with URL (no subscribe)",
|
|
777
|
+
async () => {
|
|
778
|
+
var key = 'test-url-get-' + Math.random().toString(36).slice(2)
|
|
779
|
+
|
|
780
|
+
await braid_fetch(`/${key}`, {
|
|
781
|
+
method: 'PUT',
|
|
782
|
+
version: ['300'],
|
|
783
|
+
body: 'url get test'
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
787
|
+
method: 'POST',
|
|
788
|
+
body: `void (async () => {
|
|
789
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
790
|
+
var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
|
|
791
|
+
var result = await braid_blob.get(url)
|
|
792
|
+
res.end(Buffer.from(result).toString('utf8'))
|
|
793
|
+
})()`
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
return await r1.text()
|
|
797
|
+
},
|
|
798
|
+
'url get test'
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
runTest(
|
|
802
|
+
"test get with URL (with subscribe)",
|
|
803
|
+
async () => {
|
|
804
|
+
var key = 'test-url-get-sub-' + Math.random().toString(36).slice(2)
|
|
805
|
+
|
|
806
|
+
await braid_fetch(`/${key}`, {
|
|
807
|
+
method: 'PUT',
|
|
808
|
+
version: ['400'],
|
|
809
|
+
body: 'initial'
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
// Use a promise to wait for the eval to complete
|
|
813
|
+
var evalPromise = braid_fetch(`/eval`, {
|
|
814
|
+
method: 'POST',
|
|
815
|
+
body: `void (async () => {
|
|
816
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
817
|
+
var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
|
|
818
|
+
|
|
819
|
+
var updates = []
|
|
820
|
+
var a = new AbortController()
|
|
821
|
+
|
|
822
|
+
// Don't await - braid_blob.get returns immediately when subscribe is used
|
|
823
|
+
braid_blob.get(url, {
|
|
824
|
+
subscribe: update => {
|
|
825
|
+
updates.push(Buffer.from(update.body).toString('utf8'))
|
|
826
|
+
if (updates.length === 2) {
|
|
827
|
+
a.abort()
|
|
828
|
+
res.end(updates.join('|'))
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
signal: a.signal
|
|
832
|
+
})
|
|
833
|
+
})()`
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
// Wait a bit for subscription to be established
|
|
837
|
+
await new Promise(done => setTimeout(done, 100))
|
|
838
|
+
|
|
839
|
+
// Send update
|
|
840
|
+
await braid_fetch(`/${key}`, {
|
|
841
|
+
method: 'PUT',
|
|
842
|
+
version: ['500'],
|
|
843
|
+
body: 'updated'
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
// Wait for the eval to complete
|
|
847
|
+
var r1 = await evalPromise
|
|
848
|
+
return await r1.text()
|
|
849
|
+
},
|
|
850
|
+
'initial|updated'
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
runTest(
|
|
854
|
+
"test sync local to remote",
|
|
855
|
+
async () => {
|
|
856
|
+
var local_key = 'test-sync-local-' + Math.random().toString(36).slice(2)
|
|
857
|
+
var remote_key = 'test-sync-remote-' + Math.random().toString(36).slice(2)
|
|
858
|
+
|
|
859
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
860
|
+
method: 'POST',
|
|
861
|
+
body: `void (async () => {
|
|
862
|
+
try {
|
|
863
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
864
|
+
|
|
865
|
+
// Put something locally first
|
|
866
|
+
await braid_blob.put('${local_key}', Buffer.from('local content'), { version: ['600'] })
|
|
867
|
+
|
|
868
|
+
var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
|
|
869
|
+
|
|
870
|
+
// Start sync
|
|
871
|
+
braid_blob.sync('${local_key}', remote_url)
|
|
872
|
+
|
|
873
|
+
res.end('syncing')
|
|
874
|
+
} catch (e) {
|
|
875
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
876
|
+
}
|
|
877
|
+
})()`
|
|
878
|
+
})
|
|
879
|
+
var result = await r1.text()
|
|
880
|
+
if (result.startsWith('error:')) return result
|
|
881
|
+
|
|
882
|
+
// Wait a bit for sync to happen
|
|
883
|
+
await new Promise(done => setTimeout(done, 100))
|
|
884
|
+
|
|
885
|
+
// Check remote has the content
|
|
886
|
+
var r = await braid_fetch(`/${remote_key}`)
|
|
887
|
+
return await r.text()
|
|
888
|
+
},
|
|
889
|
+
'local content'
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
runTest(
|
|
893
|
+
"test sync two local keys",
|
|
894
|
+
async () => {
|
|
895
|
+
var key1 = 'test-sync-local1-' + Math.random().toString(36).slice(2)
|
|
896
|
+
var key2 = 'test-sync-local2-' + Math.random().toString(36).slice(2)
|
|
897
|
+
|
|
898
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
899
|
+
method: 'POST',
|
|
900
|
+
body: `void (async () => {
|
|
901
|
+
try {
|
|
902
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
903
|
+
|
|
904
|
+
// Put something to first key
|
|
905
|
+
await braid_blob.put('${key1}', Buffer.from('sync local content'), { version: ['700'] })
|
|
906
|
+
|
|
907
|
+
// Start sync between two local keys
|
|
908
|
+
braid_blob.sync('${key1}', '${key2}')
|
|
909
|
+
|
|
910
|
+
res.end('syncing')
|
|
911
|
+
} catch (e) {
|
|
912
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
913
|
+
}
|
|
914
|
+
})()`
|
|
915
|
+
})
|
|
916
|
+
var result = await r1.text()
|
|
917
|
+
if (result.startsWith('error:')) return result
|
|
918
|
+
|
|
919
|
+
// Wait a bit for sync to happen
|
|
920
|
+
await new Promise(done => setTimeout(done, 100))
|
|
921
|
+
|
|
922
|
+
// Check second key has the content
|
|
923
|
+
var r = await braid_fetch(`/${key2}`)
|
|
924
|
+
return await r.text()
|
|
925
|
+
},
|
|
926
|
+
'sync local content'
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
runTest(
|
|
930
|
+
"test sync remote to local (swap)",
|
|
931
|
+
async () => {
|
|
932
|
+
var local_key = 'test-sync-swap-local-' + Math.random().toString(36).slice(2)
|
|
933
|
+
var remote_key = 'test-sync-swap-remote-' + Math.random().toString(36).slice(2)
|
|
934
|
+
|
|
935
|
+
// Put something on the server first
|
|
936
|
+
await braid_fetch(`/${remote_key}`, {
|
|
937
|
+
method: 'PUT',
|
|
938
|
+
version: ['800'],
|
|
939
|
+
body: 'remote content'
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
943
|
+
method: 'POST',
|
|
944
|
+
body: `void (async () => {
|
|
945
|
+
try {
|
|
946
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
947
|
+
var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
|
|
948
|
+
|
|
949
|
+
// Start sync with URL as first argument (should swap internally)
|
|
950
|
+
braid_blob.sync(remote_url, '${local_key}')
|
|
951
|
+
|
|
952
|
+
res.end('syncing')
|
|
953
|
+
} catch (e) {
|
|
954
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
955
|
+
}
|
|
956
|
+
})()`
|
|
957
|
+
})
|
|
958
|
+
var result = await r1.text()
|
|
959
|
+
if (result.startsWith('error:')) return result
|
|
960
|
+
|
|
961
|
+
// Wait a bit for sync to happen
|
|
962
|
+
await new Promise(done => setTimeout(done, 100))
|
|
963
|
+
|
|
964
|
+
// Check local key has the remote content
|
|
965
|
+
var r = await braid_fetch(`/${local_key}`)
|
|
966
|
+
return await r.text()
|
|
967
|
+
},
|
|
968
|
+
'remote content'
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
runTest(
|
|
972
|
+
"test sync when server already has our version",
|
|
973
|
+
async () => {
|
|
974
|
+
var local_key = 'test-sync-has-version-local-' + Math.random().toString(36).slice(2)
|
|
975
|
+
var remote_key = 'test-sync-has-version-remote-' + Math.random().toString(36).slice(2)
|
|
976
|
+
|
|
977
|
+
// Put the same content on both local and remote with the same version
|
|
978
|
+
var version = ['900']
|
|
979
|
+
var content = 'shared content'
|
|
980
|
+
|
|
981
|
+
// Put on remote first
|
|
982
|
+
await braid_fetch(`/${remote_key}`, {
|
|
983
|
+
method: 'PUT',
|
|
984
|
+
version: version,
|
|
985
|
+
body: content
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
989
|
+
method: 'POST',
|
|
990
|
+
body: `void (async () => {
|
|
991
|
+
try {
|
|
992
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
993
|
+
|
|
994
|
+
// Put the same content locally with the same version
|
|
995
|
+
await braid_blob.put('${local_key}', Buffer.from('${content}'), { version: ${JSON.stringify(version)} })
|
|
996
|
+
|
|
997
|
+
var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
|
|
998
|
+
|
|
999
|
+
// Start sync - this should trigger the "server already has our version" path
|
|
1000
|
+
braid_blob.sync('${local_key}', remote_url)
|
|
1001
|
+
|
|
1002
|
+
res.end('syncing')
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1005
|
+
}
|
|
1006
|
+
})()`
|
|
1007
|
+
})
|
|
1008
|
+
var result = await r1.text()
|
|
1009
|
+
if (result.startsWith('error:')) return result
|
|
1010
|
+
|
|
1011
|
+
// Wait a bit for sync to initialize (the console.log should happen quickly)
|
|
1012
|
+
await new Promise(done => setTimeout(done, 100))
|
|
1013
|
+
|
|
1014
|
+
// Verify that both still have the same content
|
|
1015
|
+
var r = await braid_fetch(`/${remote_key}`)
|
|
1016
|
+
return await r.text()
|
|
1017
|
+
},
|
|
1018
|
+
'shared content'
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
runTest(
|
|
1022
|
+
"test sync closed during error",
|
|
1023
|
+
async () => {
|
|
1024
|
+
var local_key = 'test-sync-closed-local-' + Math.random().toString(36).slice(2)
|
|
1025
|
+
var remote_key = 'test-sync-closed-remote-' + Math.random().toString(36).slice(2)
|
|
1026
|
+
|
|
1027
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1028
|
+
method: 'POST',
|
|
1029
|
+
body: `void (async () => {
|
|
1030
|
+
try {
|
|
1031
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
1032
|
+
|
|
1033
|
+
// Use an invalid/unreachable URL to trigger an error
|
|
1034
|
+
var remote_url = new URL('http://localhost:9999/${remote_key}')
|
|
1035
|
+
|
|
1036
|
+
// Start sync
|
|
1037
|
+
var sync_options = {}
|
|
1038
|
+
braid_blob.sync('${local_key}', remote_url, sync_options)
|
|
1039
|
+
|
|
1040
|
+
// Close the sync immediately to trigger the closed path when error occurs
|
|
1041
|
+
sync_options.my_unsync()
|
|
1042
|
+
|
|
1043
|
+
res.end('sync started and closed')
|
|
1044
|
+
} catch (e) {
|
|
1045
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1046
|
+
}
|
|
1047
|
+
})()`
|
|
1048
|
+
})
|
|
1049
|
+
var result = await r1.text()
|
|
1050
|
+
if (result.startsWith('error:')) return result
|
|
1051
|
+
|
|
1052
|
+
// Wait for the connection error and closed message
|
|
1053
|
+
await new Promise(done => setTimeout(done, 200))
|
|
1054
|
+
|
|
1055
|
+
return result
|
|
1056
|
+
},
|
|
1057
|
+
'sync started and closed'
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
runTest(
|
|
1061
|
+
"test sync error with retry",
|
|
1062
|
+
async () => {
|
|
1063
|
+
var local_key = 'test-sync-retry-local-' + Math.random().toString(36).slice(2)
|
|
1064
|
+
var remote_key = 'test-sync-retry-remote-' + Math.random().toString(36).slice(2)
|
|
1065
|
+
|
|
1066
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1067
|
+
method: 'POST',
|
|
1068
|
+
body: `void (async () => {
|
|
1069
|
+
try {
|
|
1070
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
1071
|
+
|
|
1072
|
+
// Use an invalid/unreachable URL to trigger an error
|
|
1073
|
+
var remote_url = new URL('http://localhost:9999/${remote_key}')
|
|
1074
|
+
|
|
1075
|
+
// Start sync without closing it - should trigger retry
|
|
1076
|
+
var sync_options = {}
|
|
1077
|
+
braid_blob.sync('${local_key}', remote_url, sync_options)
|
|
1078
|
+
|
|
1079
|
+
// Wait a bit for the error to occur and retry message to print
|
|
1080
|
+
await new Promise(done => setTimeout(done, 200))
|
|
1081
|
+
|
|
1082
|
+
// Now close it to stop retrying
|
|
1083
|
+
sync_options.my_unsync()
|
|
1084
|
+
|
|
1085
|
+
res.end('sync error occurred')
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1088
|
+
}
|
|
1089
|
+
})()`
|
|
1090
|
+
})
|
|
1091
|
+
var result = await r1.text()
|
|
1092
|
+
|
|
1093
|
+
return result
|
|
1094
|
+
},
|
|
1095
|
+
'sync error occurred'
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
runTest(
|
|
1099
|
+
"test requesting with version/parents server doesn't have",
|
|
1100
|
+
async () => {
|
|
1101
|
+
var key = 'test-parents-unknown-' + Math.random().toString(36).slice(2)
|
|
1102
|
+
|
|
1103
|
+
// Put with version 100
|
|
1104
|
+
await braid_fetch(`/${key}`, {
|
|
1105
|
+
method: 'PUT',
|
|
1106
|
+
version: ['100'],
|
|
1107
|
+
body: 'content v100'
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
// Try to subscribe with parents 200 (newer than what server has)
|
|
1111
|
+
// This triggers the "unkown version" error which gets caught and returns 309
|
|
1112
|
+
var r = await braid_fetch(`/${key}`, {
|
|
1113
|
+
subscribe: true,
|
|
1114
|
+
parents: ['200']
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
return r.status
|
|
1118
|
+
},
|
|
1119
|
+
'309'
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
runTest(
|
|
1123
|
+
"test requesting specific version server doesn't have",
|
|
1124
|
+
async () => {
|
|
1125
|
+
var key = 'test-version-unknown-' + Math.random().toString(36).slice(2)
|
|
1126
|
+
|
|
1127
|
+
// Put with version 100
|
|
1128
|
+
await braid_fetch(`/${key}`, {
|
|
1129
|
+
method: 'PUT',
|
|
1130
|
+
version: ['100'],
|
|
1131
|
+
body: 'content v100'
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
// Try to GET with version 200 (newer than what server has)
|
|
1135
|
+
// This should trigger line 269 when req.version is checked
|
|
1136
|
+
var r = await braid_fetch(`/${key}`, {
|
|
1137
|
+
version: ['200']
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
return r.status
|
|
1141
|
+
},
|
|
1142
|
+
'309'
|
|
1143
|
+
)
|
|
1144
|
+
|
|
691
1145
|
}
|
|
692
1146
|
|
|
693
1147
|
// Export for Node.js (CommonJS)
|