braid-blob 0.0.25 → 0.0.27

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.
Files changed (3) hide show
  1. package/index.js +37 -26
  2. package/package.json +2 -2
  3. 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.dont_retry && error_happened(e))
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,6 +416,17 @@ 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
432
  dont_retry: true,
@@ -426,8 +435,10 @@ function create_braid_blob() {
426
435
  version: update.version,
427
436
  content_type: update.headers?.['content-type']
428
437
  })
438
+ got_local_file()
429
439
  remote_first_put()
430
440
  },
441
+ on_error: handle_error
431
442
  }
432
443
  // Use fork-point (parents) to avoid receiving data we already have
433
444
  if (local_version) {
@@ -435,25 +446,25 @@ function create_braid_blob() {
435
446
  }
436
447
 
437
448
  // Set up both subscriptions, handling cases where one doesn't exist yet
438
- braid_blob.get(a, a_ops).then(x =>
439
- x || remote_first_put_promise.then(() =>
440
- braid_blob.get(a, a_ops)))
449
+ braid_blob.get(a, a_ops).then(x => {
450
+ if (x) got_local_file()
451
+ else remote_first_put_promise.then(() =>
452
+ braid_blob.get(a, a_ops))
453
+ })
441
454
 
442
- // NOTE: this should not return, but it might throw
443
- await braid_blob.get(b, b_ops)
455
+ // Get the response to check Editable header
456
+ var remote_res = await braid_blob.get(b, b_ops)
457
+ if (remote_res) got_remote_res()
444
458
 
445
- // this will only return if it couldn't find the key
446
- await local_first_put_promise
447
- disconnect()
448
- connect()
449
- } catch (e) {
450
- if (closed) {
451
- return
459
+ // If remote doesn't exist yet, wait for it to be created then reconnect
460
+ if (!remote_res) {
461
+ await local_first_put_promise
462
+ disconnect()
463
+ connect()
452
464
  }
453
-
454
- disconnect()
455
- console.log(`disconnected, retrying in 1 second`)
456
- setTimeout(connect, 1000)
465
+ // Otherwise, on_error will call handle_error when connection drops
466
+ } catch (e) {
467
+ handle_error(e)
457
468
  }
458
469
  }
459
470
  connect()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
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.21"
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 () => {