braid-blob 0.0.25 → 0.0.26
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 +37 -27
- package/package.json +2 -2
- package/test/tests.js +147 -0
package/index.js
CHANGED
|
@@ -123,18 +123,9 @@ function create_braid_blob() {
|
|
|
123
123
|
if (res.status === 404) return null
|
|
124
124
|
|
|
125
125
|
if (options.subscribe) {
|
|
126
|
-
if (options.dont_retry) {
|
|
127
|
-
var error_happened
|
|
128
|
-
var error_promise = new Promise((_, fail) => error_happened = fail)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
126
|
res.subscribe(async update => {
|
|
132
127
|
await options.subscribe(update)
|
|
133
|
-
}, e => options.
|
|
134
|
-
|
|
135
|
-
if (options.dont_retry) {
|
|
136
|
-
return await error_promise
|
|
137
|
-
}
|
|
128
|
+
}, e => options.on_error?.(e))
|
|
138
129
|
return res
|
|
139
130
|
} else {
|
|
140
131
|
return await res.arrayBuffer()
|
|
@@ -374,6 +365,13 @@ function create_braid_blob() {
|
|
|
374
365
|
var local_first_put_promise = new Promise(done => local_first_put = done)
|
|
375
366
|
var remote_first_put_promise = new Promise(done => remote_first_put = done)
|
|
376
367
|
|
|
368
|
+
function handle_error(e) {
|
|
369
|
+
if (closed) return
|
|
370
|
+
disconnect()
|
|
371
|
+
console.log(`disconnected, retrying in 1 second`)
|
|
372
|
+
setTimeout(connect, 1000)
|
|
373
|
+
}
|
|
374
|
+
|
|
377
375
|
async function connect() {
|
|
378
376
|
var ac = new AbortController()
|
|
379
377
|
disconnect = () => ac.abort()
|
|
@@ -418,16 +416,28 @@ function create_braid_blob() {
|
|
|
418
416
|
}
|
|
419
417
|
|
|
420
418
|
// Remote -> local: subscribe to remote updates
|
|
419
|
+
// We need both: remote_res (for Editable header) and local file exists
|
|
420
|
+
var got_remote_res, got_local_file
|
|
421
|
+
var remote_res_promise = new Promise(done => got_remote_res = done)
|
|
422
|
+
var local_file_promise = new Promise(done => got_local_file = done)
|
|
423
|
+
|
|
424
|
+
// Apply read-only once we have both remote response and local file
|
|
425
|
+
Promise.all([remote_res_promise, local_file_promise]).then(async () => {
|
|
426
|
+
var read_only = remote_res.headers?.get('editable') === 'false'
|
|
427
|
+
await braid_blob.db.set_read_only(a, read_only)
|
|
428
|
+
})
|
|
429
|
+
|
|
421
430
|
var b_ops = {
|
|
422
431
|
signal: ac.signal,
|
|
423
|
-
dont_retry: true,
|
|
424
432
|
subscribe: async update => {
|
|
425
433
|
await braid_blob.put(a, update.body, {
|
|
426
434
|
version: update.version,
|
|
427
435
|
content_type: update.headers?.['content-type']
|
|
428
436
|
})
|
|
437
|
+
got_local_file()
|
|
429
438
|
remote_first_put()
|
|
430
439
|
},
|
|
440
|
+
on_error: handle_error
|
|
431
441
|
}
|
|
432
442
|
// Use fork-point (parents) to avoid receiving data we already have
|
|
433
443
|
if (local_version) {
|
|
@@ -435,25 +445,25 @@ function create_braid_blob() {
|
|
|
435
445
|
}
|
|
436
446
|
|
|
437
447
|
// Set up both subscriptions, handling cases where one doesn't exist yet
|
|
438
|
-
braid_blob.get(a, a_ops).then(x =>
|
|
439
|
-
x
|
|
440
|
-
|
|
448
|
+
braid_blob.get(a, a_ops).then(x => {
|
|
449
|
+
if (x) got_local_file()
|
|
450
|
+
else remote_first_put_promise.then(() =>
|
|
451
|
+
braid_blob.get(a, a_ops))
|
|
452
|
+
})
|
|
441
453
|
|
|
442
|
-
//
|
|
443
|
-
await braid_blob.get(b, b_ops)
|
|
454
|
+
// Get the response to check Editable header
|
|
455
|
+
var remote_res = await braid_blob.get(b, b_ops)
|
|
456
|
+
if (remote_res) got_remote_res()
|
|
444
457
|
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (closed) {
|
|
451
|
-
return
|
|
458
|
+
// If remote doesn't exist yet, wait for it to be created then reconnect
|
|
459
|
+
if (!remote_res) {
|
|
460
|
+
await local_first_put_promise
|
|
461
|
+
disconnect()
|
|
462
|
+
connect()
|
|
452
463
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
setTimeout(connect, 1000)
|
|
464
|
+
// Otherwise, on_error will call handle_error when connection drops
|
|
465
|
+
} catch (e) {
|
|
466
|
+
handle_error(e)
|
|
457
467
|
}
|
|
458
468
|
}
|
|
459
469
|
connect()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-blob",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
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.22"
|
|
15
15
|
}
|
|
16
16
|
}
|
package/test/tests.js
CHANGED
|
@@ -1147,6 +1147,153 @@ runTest(
|
|
|
1147
1147
|
'shared content'
|
|
1148
1148
|
)
|
|
1149
1149
|
|
|
1150
|
+
runTest(
|
|
1151
|
+
"test sync readonly remote to nonexistent local",
|
|
1152
|
+
async () => {
|
|
1153
|
+
var local_key = 'test-sync-readonly-local-' + Math.random().toString(36).slice(2)
|
|
1154
|
+
var remote_key = 'test-sync-readonly-remote-' + Math.random().toString(36).slice(2)
|
|
1155
|
+
|
|
1156
|
+
// Put something on the server first with Editable: false
|
|
1157
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1158
|
+
method: 'POST',
|
|
1159
|
+
body: `void (async () => {
|
|
1160
|
+
try {
|
|
1161
|
+
// Create a custom handler that sets Editable: false
|
|
1162
|
+
req.method = 'PUT'
|
|
1163
|
+
req.version = ['100']
|
|
1164
|
+
req.body = Buffer.from('readonly content')
|
|
1165
|
+
await braid_blob.put('${remote_key}', req.body, { version: req.version })
|
|
1166
|
+
res.end('created')
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1169
|
+
}
|
|
1170
|
+
})()`
|
|
1171
|
+
})
|
|
1172
|
+
var result = await r1.text()
|
|
1173
|
+
if (result.startsWith('error:')) return result
|
|
1174
|
+
|
|
1175
|
+
// Now sync to a local key that doesn't exist, with a readonly remote
|
|
1176
|
+
var r2 = await braid_fetch(`/eval`, {
|
|
1177
|
+
method: 'POST',
|
|
1178
|
+
body: `void (async () => {
|
|
1179
|
+
try {
|
|
1180
|
+
var fs = require('fs').promises
|
|
1181
|
+
var test_id = 'test-sync-readonly-' + Math.random().toString(36).slice(2)
|
|
1182
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1183
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1184
|
+
|
|
1185
|
+
var bb = braid_blob.create_braid_blob()
|
|
1186
|
+
bb.db_folder = db_folder
|
|
1187
|
+
bb.meta_folder = meta_folder
|
|
1188
|
+
|
|
1189
|
+
// Create a mock server URL that returns Editable: false
|
|
1190
|
+
var http = require('http')
|
|
1191
|
+
var {http_server: braidify} = require('braid-http')
|
|
1192
|
+
|
|
1193
|
+
var mock_server = http.createServer((req2, res2) => {
|
|
1194
|
+
braidify(req2, res2)
|
|
1195
|
+
res2.setHeader('Editable', 'false')
|
|
1196
|
+
braid_blob.serve(req2, res2, { key: '${remote_key}' })
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
await new Promise(resolve => mock_server.listen(0, resolve))
|
|
1200
|
+
var port = mock_server.address().port
|
|
1201
|
+
|
|
1202
|
+
var remote_url = new URL('http://localhost:' + port + '/${remote_key}')
|
|
1203
|
+
var ac = new AbortController()
|
|
1204
|
+
|
|
1205
|
+
// Start sync - local key doesn't exist yet
|
|
1206
|
+
bb.sync('${local_key}', remote_url, { signal: ac.signal })
|
|
1207
|
+
|
|
1208
|
+
// Wait for sync to happen
|
|
1209
|
+
await new Promise(done => setTimeout(done, 500))
|
|
1210
|
+
|
|
1211
|
+
// Stop sync
|
|
1212
|
+
ac.abort()
|
|
1213
|
+
mock_server.close()
|
|
1214
|
+
|
|
1215
|
+
// Check if local file exists and has the content
|
|
1216
|
+
var result = await bb.get('${local_key}')
|
|
1217
|
+
|
|
1218
|
+
var response
|
|
1219
|
+
if (!result) {
|
|
1220
|
+
response = 'file not created'
|
|
1221
|
+
} else if (result.body.toString() !== 'readonly content') {
|
|
1222
|
+
response = 'wrong content: ' + result.body.toString()
|
|
1223
|
+
} else {
|
|
1224
|
+
// Check if the file is actually read-only
|
|
1225
|
+
await bb.init()
|
|
1226
|
+
var is_readonly = await bb.db.is_read_only('${local_key}')
|
|
1227
|
+
response = is_readonly ? 'synced and readonly' : 'synced but NOT readonly'
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Clean up
|
|
1231
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1232
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1233
|
+
|
|
1234
|
+
res.end(response)
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1237
|
+
}
|
|
1238
|
+
})()`
|
|
1239
|
+
})
|
|
1240
|
+
return await r2.text()
|
|
1241
|
+
},
|
|
1242
|
+
'synced and readonly'
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
runTest(
|
|
1246
|
+
"test sync does not disconnect unnecessarily",
|
|
1247
|
+
async () => {
|
|
1248
|
+
var local_key = 'test-sync-no-disconnect-local-' + Math.random().toString(36).slice(2)
|
|
1249
|
+
var remote_key = 'test-sync-no-disconnect-remote-' + Math.random().toString(36).slice(2)
|
|
1250
|
+
|
|
1251
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1252
|
+
method: 'POST',
|
|
1253
|
+
body: `void (async () => {
|
|
1254
|
+
try {
|
|
1255
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
1256
|
+
|
|
1257
|
+
// Put something locally first
|
|
1258
|
+
await braid_blob.put('${local_key}', Buffer.from('local content'), { version: ['600'] })
|
|
1259
|
+
|
|
1260
|
+
var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
|
|
1261
|
+
|
|
1262
|
+
// Capture console.log to count disconnects
|
|
1263
|
+
var disconnect_count = 0
|
|
1264
|
+
var original_log = console.log
|
|
1265
|
+
console.log = function(...args) {
|
|
1266
|
+
if (args[0]?.includes?.('disconnected')) disconnect_count++
|
|
1267
|
+
original_log.apply(console, args)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Create an AbortController to stop the sync
|
|
1271
|
+
var ac = new AbortController()
|
|
1272
|
+
|
|
1273
|
+
// Start sync
|
|
1274
|
+
braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
|
|
1275
|
+
|
|
1276
|
+
// Wait for sync to establish and stabilize
|
|
1277
|
+
await new Promise(done => setTimeout(done, 500))
|
|
1278
|
+
|
|
1279
|
+
// Stop sync
|
|
1280
|
+
ac.abort()
|
|
1281
|
+
|
|
1282
|
+
// Restore console.log
|
|
1283
|
+
console.log = original_log
|
|
1284
|
+
|
|
1285
|
+
// Should have zero disconnects during normal operation
|
|
1286
|
+
res.end(disconnect_count === 0 ? 'no disconnects' : 'disconnects: ' + disconnect_count)
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1289
|
+
}
|
|
1290
|
+
})()`
|
|
1291
|
+
})
|
|
1292
|
+
return await r1.text()
|
|
1293
|
+
},
|
|
1294
|
+
'no disconnects'
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1150
1297
|
runTest(
|
|
1151
1298
|
"test sync closed during error",
|
|
1152
1299
|
async () => {
|