braid-blob 0.0.24 → 0.0.25

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 +70 -42
  2. package/package.json +1 -1
  3. package/test/tests.js +219 -8
package/index.js CHANGED
@@ -40,15 +40,10 @@ function create_braid_blob() {
40
40
  braid_blob.put = async (key, body, options = {}) => {
41
41
  // Handle URL case - make a remote PUT request
42
42
  if (key instanceof URL) {
43
- options.my_abort = new AbortController()
44
- if (options.signal) {
45
- options.signal.addEventListener('abort', () =>
46
- options.my_abort.abort())
47
- }
48
43
 
49
44
  var params = {
50
45
  method: 'PUT',
51
- signal: options.my_abort.signal,
46
+ signal: options.signal,
52
47
  retry: () => true,
53
48
  body: body
54
49
  }
@@ -62,6 +57,7 @@ function create_braid_blob() {
62
57
  }
63
58
 
64
59
  await braid_blob.init()
60
+ if (options.signal?.aborted) return
65
61
 
66
62
  // Read the meta data using new meta API
67
63
  var meta = braid_blob.db.get_meta(key) || {}
@@ -83,6 +79,7 @@ function create_braid_blob() {
83
79
  // Write the file using url-file-db (unless skip_write is set)
84
80
  if (!options.skip_write)
85
81
  await braid_blob.db.write(key, body)
82
+ if (options.signal?.aborted) return
86
83
 
87
84
  // Update only the fields we want to change in metadata
88
85
  var meta_updates = { event: their_e }
@@ -90,6 +87,7 @@ function create_braid_blob() {
90
87
  meta_updates.content_type = options.content_type
91
88
 
92
89
  await braid_blob.db.update_meta(key, meta_updates)
90
+ if (options.signal?.aborted) return
93
91
 
94
92
  // Notify all subscriptions of the update
95
93
  // (except the peer which made the PUT request itself)
@@ -109,21 +107,21 @@ function create_braid_blob() {
109
107
  braid_blob.get = async (key, options = {}) => {
110
108
  // Handle URL case - make a remote GET request
111
109
  if (key instanceof URL) {
112
- options.my_abort = new AbortController()
113
-
114
110
  var params = {
115
- signal: options.my_abort.signal,
111
+ signal: options.signal,
116
112
  subscribe: !!options.subscribe,
117
113
  heartbeats: 120,
118
114
  }
119
115
  if (!options.dont_retry) {
120
- params.retry = () => true
116
+ params.retry = (res) => res.status !== 404
121
117
  }
122
118
  for (var x of ['headers', 'parents', 'version', 'peer'])
123
119
  if (options[x] != null) params[x] = options[x]
124
120
 
125
121
  var res = await braid_fetch(key.href, params)
126
122
 
123
+ if (res.status === 404) return null
124
+
127
125
  if (options.subscribe) {
128
126
  if (options.dont_retry) {
129
127
  var error_happened
@@ -154,6 +152,7 @@ function create_braid_blob() {
154
152
  content_type: meta.content_type
155
153
  }
156
154
  if (options.header_cb) await options.header_cb(result)
155
+ if (options.signal?.aborted) return
157
156
  // Check if requested version/parents is newer than what we have - if so, we don't have it
158
157
  if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
159
158
  throw new Error('unkown version: ' + options.version)
@@ -164,7 +163,8 @@ function create_braid_blob() {
164
163
  if (options.subscribe) {
165
164
  var subscribe_chain = Promise.resolve()
166
165
  options.my_subscribe = (x) => subscribe_chain =
167
- subscribe_chain.then(() => options.subscribe(x))
166
+ subscribe_chain.then(() =>
167
+ !options.signal?.aborted && options.subscribe(x))
168
168
 
169
169
  // Start a subscription for future updates
170
170
  if (!braid_blob.key_to_subs[key])
@@ -181,14 +181,14 @@ function create_braid_blob() {
181
181
  }
182
182
  })
183
183
 
184
- // Store unsubscribe function
185
- result.unsubscribe = () => {
184
+ options.signal?.addEventListener('abort', () => {
186
185
  braid_blob.key_to_subs[key].delete(peer)
187
186
  if (!braid_blob.key_to_subs[key].size)
188
187
  delete braid_blob.key_to_subs[key]
189
- }
188
+ })
190
189
 
191
190
  if (options.before_send_cb) await options.before_send_cb(result)
191
+ if (options.signal?.aborted) return
192
192
 
193
193
  // Send an immediate update if needed
194
194
  if (!options.parents ||
@@ -212,15 +212,10 @@ function create_braid_blob() {
212
212
  braid_blob.delete = async (key, options = {}) => {
213
213
  // Handle URL case - make a remote DELETE request
214
214
  if (key instanceof URL) {
215
- options.my_abort = new AbortController()
216
- if (options.signal) {
217
- options.signal.addEventListener('abort', () =>
218
- options.my_abort.abort())
219
- }
220
215
 
221
216
  var params = {
222
217
  method: 'DELETE',
223
- signal: options.my_abort.signal
218
+ signal: options.signal
224
219
  }
225
220
  for (var x of ['headers', 'peer'])
226
221
  if (options[x] != null) params[x] = options[x]
@@ -229,6 +224,7 @@ function create_braid_blob() {
229
224
  }
230
225
 
231
226
  await braid_blob.init()
227
+ if (options.signal?.aborted) return
232
228
 
233
229
  // Delete the file from the database
234
230
  await braid_blob.db.delete(key)
@@ -329,27 +325,40 @@ function create_braid_blob() {
329
325
  })
330
326
  }
331
327
 
332
- braid_blob.sync = async (a, b, options = {}) => {
333
- var unsync_cbs = []
334
- options.my_unsync = () => unsync_cbs.forEach(cb => cb())
335
-
328
+ braid_blob.sync = (a, b, options = {}) => {
336
329
  if ((a instanceof URL) === (b instanceof URL)) {
337
330
  // Both are URLs or both are local keys
331
+ var a_first_put, b_first_put
332
+ var a_first_put_promise = new Promise(done => a_first_put = done)
333
+ var b_first_put_promise = new Promise(done => b_first_put = done)
334
+
338
335
  var a_ops = {
339
- subscribe: update => braid_blob.put(b, update.body, {
340
- version: update.version,
341
- content_type: update.headers?.['content-type']
342
- })
336
+ signal: options.signal,
337
+ subscribe: update => {
338
+ braid_blob.put(b, update.body, {
339
+ signal: options.signal,
340
+ version: update.version,
341
+ content_type: update.headers?.['content-type']
342
+ }).then(a_first_put)
343
+ }
343
344
  }
344
- braid_blob.get(a, a_ops)
345
+ braid_blob.get(a, a_ops).then(x =>
346
+ x || b_first_put_promise.then(() =>
347
+ braid_blob.get(a, a_ops)))
345
348
 
346
349
  var b_ops = {
347
- subscribe: update => braid_blob.put(a, update.body, {
348
- version: update.version,
349
- content_type: update.headers?.['content-type']
350
- })
350
+ signal: options.signal,
351
+ subscribe: update => {
352
+ braid_blob.put(a, update.body, {
353
+ signal: options.signal,
354
+ version: update.version,
355
+ content_type: update.headers?.['content-type']
356
+ }).then(b_first_put)
357
+ }
351
358
  }
352
- braid_blob.get(b, b_ops)
359
+ braid_blob.get(b, b_ops).then(x =>
360
+ x || a_first_put_promise.then(() =>
361
+ braid_blob.get(b, b_ops)))
353
362
  } else {
354
363
  // One is local, one is remote - make a=local and b=remote (swap if not)
355
364
  if (a instanceof URL) {
@@ -357,17 +366,23 @@ function create_braid_blob() {
357
366
  }
358
367
 
359
368
  var closed = false
360
- options.my_unsync = () => { closed = true; disconnect() }
361
-
362
369
  var disconnect = () => { }
370
+ options.signal?.addEventListener('abort', () =>
371
+ { closed = true; disconnect() })
372
+
373
+ var local_first_put, remote_first_put
374
+ var local_first_put_promise = new Promise(done => local_first_put = done)
375
+ var remote_first_put_promise = new Promise(done => remote_first_put = done)
376
+
363
377
  async function connect() {
364
378
  var ac = new AbortController()
365
- var disconnect_cbs = [() => ac.abort()]
366
- disconnect = () => disconnect_cbs.forEach(cb => cb())
379
+ disconnect = () => ac.abort()
367
380
 
368
381
  try {
369
382
  // Check if remote has our current version (simple fork-point check)
370
- var local_result = await braid_blob.get(a)
383
+ var local_result = await braid_blob.get(a, {
384
+ signal: ac.signal
385
+ })
371
386
  var local_version = local_result ? local_result.version : null
372
387
  var server_has_our_version = false
373
388
 
@@ -383,12 +398,13 @@ function create_braid_blob() {
383
398
 
384
399
  // Local -> remote: subscribe to future local changes
385
400
  var a_ops = {
401
+ signal: ac.signal,
386
402
  subscribe: update => {
387
- update.signal = ac.signal
388
403
  braid_blob.put(b, update.body, {
404
+ signal: ac.signal,
389
405
  version: update.version,
390
406
  content_type: update.content_type
391
- }).catch(e => {
407
+ }).then(local_first_put).catch(e => {
392
408
  if (e.name === 'AbortError') {
393
409
  // ignore
394
410
  } else throw e
@@ -400,24 +416,36 @@ function create_braid_blob() {
400
416
  if (server_has_our_version) {
401
417
  a_ops.parents = local_version
402
418
  }
403
- braid_blob.get(a, a_ops)
404
419
 
405
420
  // Remote -> local: subscribe to remote updates
406
421
  var b_ops = {
422
+ signal: ac.signal,
407
423
  dont_retry: true,
408
424
  subscribe: async update => {
409
425
  await braid_blob.put(a, update.body, {
410
426
  version: update.version,
411
427
  content_type: update.headers?.['content-type']
412
428
  })
429
+ remote_first_put()
413
430
  },
414
431
  }
415
432
  // Use fork-point (parents) to avoid receiving data we already have
416
433
  if (local_version) {
417
434
  b_ops.parents = local_version
418
435
  }
436
+
437
+ // 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)))
441
+
419
442
  // NOTE: this should not return, but it might throw
420
443
  await braid_blob.get(b, b_ops)
444
+
445
+ // this will only return if it couldn't find the key
446
+ await local_first_put_promise
447
+ disconnect()
448
+ connect()
421
449
  } catch (e) {
422
450
  if (closed) {
423
451
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
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/tests.js CHANGED
@@ -1162,12 +1162,14 @@ runTest(
1162
1162
  // Use an invalid/unreachable URL to trigger an error
1163
1163
  var remote_url = new URL('http://localhost:9999/${remote_key}')
1164
1164
 
1165
- // Start sync
1166
- var sync_options = {}
1167
- braid_blob.sync('${local_key}', remote_url, sync_options)
1165
+ // Create an AbortController to stop the sync
1166
+ var ac = new AbortController()
1167
+
1168
+ // Start sync with signal
1169
+ braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
1168
1170
 
1169
1171
  // Close the sync immediately to trigger the closed path when error occurs
1170
- sync_options.my_unsync()
1172
+ ac.abort()
1171
1173
 
1172
1174
  res.end('sync started and closed')
1173
1175
  } catch (e) {
@@ -1201,15 +1203,17 @@ runTest(
1201
1203
  // Use an invalid/unreachable URL to trigger an error
1202
1204
  var remote_url = new URL('http://localhost:9999/${remote_key}')
1203
1205
 
1204
- // Start sync without closing it - should trigger retry
1205
- var sync_options = {}
1206
- braid_blob.sync('${local_key}', remote_url, sync_options)
1206
+ // Create an AbortController to stop the sync
1207
+ var ac = new AbortController()
1208
+
1209
+ // Start sync with signal - should trigger retry on error
1210
+ braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
1207
1211
 
1208
1212
  // Wait a bit for the error to occur and retry message to print
1209
1213
  await new Promise(done => setTimeout(done, 200))
1210
1214
 
1211
1215
  // Now close it to stop retrying
1212
- sync_options.my_unsync()
1216
+ ac.abort()
1213
1217
 
1214
1218
  res.end('sync error occurred')
1215
1219
  } catch (e) {
@@ -1494,6 +1498,213 @@ runTest(
1494
1498
  'true'
1495
1499
  )
1496
1500
 
1501
+ runTest(
1502
+ "test get with URL returns null on 404",
1503
+ async () => {
1504
+ var key = 'test-url-get-404-' + Math.random().toString(36).slice(2)
1505
+
1506
+ var r1 = await braid_fetch(`/eval`, {
1507
+ method: 'POST',
1508
+ body: `void (async () => {
1509
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
1510
+ var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
1511
+ var result = await braid_blob.get(url)
1512
+ res.end(result === null ? 'null' : 'not null: ' + JSON.stringify(result))
1513
+ })()`
1514
+ })
1515
+
1516
+ return await r1.text()
1517
+ },
1518
+ 'null'
1519
+ )
1520
+
1521
+ runTest(
1522
+ "test signal abort stops local put operation",
1523
+ async () => {
1524
+ var r1 = await braid_fetch(`/eval`, {
1525
+ method: 'POST',
1526
+ body: `void (async () => {
1527
+ var fs = require('fs').promises
1528
+ var test_id = 'test-abort-put-' + Math.random().toString(36).slice(2)
1529
+ var db_folder = __dirname + '/' + test_id + '-db'
1530
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1531
+
1532
+ try {
1533
+ var bb = braid_blob.create_braid_blob()
1534
+ bb.db_folder = db_folder
1535
+ bb.meta_folder = meta_folder
1536
+
1537
+ // Create an already-aborted signal
1538
+ var ac = new AbortController()
1539
+ ac.abort()
1540
+
1541
+ // Try to put with aborted signal
1542
+ var result = await bb.put('/test-file', Buffer.from('hello'), {
1543
+ signal: ac.signal
1544
+ })
1545
+
1546
+ // Result should be undefined since operation was aborted
1547
+ res.end(result === undefined ? 'aborted' : 'not aborted: ' + result)
1548
+ } catch (e) {
1549
+ res.end('error: ' + e.message)
1550
+ } finally {
1551
+ await fs.rm(db_folder, { recursive: true, force: true })
1552
+ await fs.rm(meta_folder, { recursive: true, force: true })
1553
+ }
1554
+ })()`
1555
+ })
1556
+ return await r1.text()
1557
+ },
1558
+ 'aborted'
1559
+ )
1560
+
1561
+ runTest(
1562
+ "test signal abort stops local get operation",
1563
+ async () => {
1564
+ var r1 = await braid_fetch(`/eval`, {
1565
+ method: 'POST',
1566
+ body: `void (async () => {
1567
+ var fs = require('fs').promises
1568
+ var test_id = 'test-abort-get-' + Math.random().toString(36).slice(2)
1569
+ var db_folder = __dirname + '/' + test_id + '-db'
1570
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1571
+
1572
+ try {
1573
+ var bb = braid_blob.create_braid_blob()
1574
+ bb.db_folder = db_folder
1575
+ bb.meta_folder = meta_folder
1576
+
1577
+ // Put a file first
1578
+ await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
1579
+
1580
+ // Create an already-aborted signal
1581
+ var ac = new AbortController()
1582
+ ac.abort()
1583
+
1584
+ // Try to get with aborted signal (after header_cb)
1585
+ var header_called = false
1586
+ var result = await bb.get('/test-file', {
1587
+ signal: ac.signal,
1588
+ header_cb: () => { header_called = true }
1589
+ })
1590
+
1591
+ // Result should be undefined since operation was aborted after header_cb
1592
+ res.end(header_called && result === undefined ? 'aborted' : 'not aborted: header=' + header_called + ' result=' + JSON.stringify(result))
1593
+ } catch (e) {
1594
+ res.end('error: ' + e.message)
1595
+ } finally {
1596
+ await fs.rm(db_folder, { recursive: true, force: true })
1597
+ await fs.rm(meta_folder, { recursive: true, force: true })
1598
+ }
1599
+ })()`
1600
+ })
1601
+ return await r1.text()
1602
+ },
1603
+ 'aborted'
1604
+ )
1605
+
1606
+ runTest(
1607
+ "test signal abort stops local delete operation",
1608
+ async () => {
1609
+ var r1 = await braid_fetch(`/eval`, {
1610
+ method: 'POST',
1611
+ body: `void (async () => {
1612
+ var fs = require('fs').promises
1613
+ var test_id = 'test-abort-delete-' + Math.random().toString(36).slice(2)
1614
+ var db_folder = __dirname + '/' + test_id + '-db'
1615
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1616
+
1617
+ try {
1618
+ var bb = braid_blob.create_braid_blob()
1619
+ bb.db_folder = db_folder
1620
+ bb.meta_folder = meta_folder
1621
+
1622
+ // Put a file first
1623
+ await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
1624
+
1625
+ // Create an already-aborted signal
1626
+ var ac = new AbortController()
1627
+ ac.abort()
1628
+
1629
+ // Try to delete with aborted signal
1630
+ await bb.delete('/test-file', { signal: ac.signal })
1631
+
1632
+ // File should still exist since delete was aborted
1633
+ var result = await bb.get('/test-file')
1634
+ res.end(result && result.body ? 'still exists' : 'deleted')
1635
+ } catch (e) {
1636
+ res.end('error: ' + e.message)
1637
+ } finally {
1638
+ await fs.rm(db_folder, { recursive: true, force: true })
1639
+ await fs.rm(meta_folder, { recursive: true, force: true })
1640
+ }
1641
+ })()`
1642
+ })
1643
+ return await r1.text()
1644
+ },
1645
+ 'still exists'
1646
+ )
1647
+
1648
+ runTest(
1649
+ "test signal abort stops subscription updates",
1650
+ async () => {
1651
+ var r1 = await braid_fetch(`/eval`, {
1652
+ method: 'POST',
1653
+ body: `void (async () => {
1654
+ var fs = require('fs').promises
1655
+ var test_id = 'test-abort-sub-' + Math.random().toString(36).slice(2)
1656
+ var db_folder = __dirname + '/' + test_id + '-db'
1657
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1658
+
1659
+ try {
1660
+ var bb = braid_blob.create_braid_blob()
1661
+ bb.db_folder = db_folder
1662
+ bb.meta_folder = meta_folder
1663
+
1664
+ // Put a file first
1665
+ await bb.put('/test-file', Buffer.from('v1'), { version: ['1'] })
1666
+
1667
+ // Subscribe with an AbortController
1668
+ var ac = new AbortController()
1669
+ var updates = []
1670
+
1671
+ await bb.get('/test-file', {
1672
+ signal: ac.signal,
1673
+ subscribe: (update) => {
1674
+ updates.push(update.body.toString())
1675
+ }
1676
+ })
1677
+
1678
+ // Should have received initial update
1679
+ if (updates.length !== 1 || updates[0] !== 'v1') {
1680
+ res.end('initial update wrong: ' + JSON.stringify(updates))
1681
+ return
1682
+ }
1683
+
1684
+ // Abort the subscription
1685
+ ac.abort()
1686
+
1687
+ // Put another update
1688
+ await bb.put('/test-file', Buffer.from('v2'), { version: ['2'] })
1689
+
1690
+ // Wait a bit for any updates to propagate
1691
+ await new Promise(done => setTimeout(done, 50))
1692
+
1693
+ // Should still only have the initial update
1694
+ res.end(updates.length === 1 ? 'stopped' : 'got extra: ' + JSON.stringify(updates))
1695
+ } catch (e) {
1696
+ res.end('error: ' + e.message)
1697
+ } finally {
1698
+ await fs.rm(db_folder, { recursive: true, force: true })
1699
+ await fs.rm(meta_folder, { recursive: true, force: true })
1700
+ }
1701
+ })()`
1702
+ })
1703
+ return await r1.text()
1704
+ },
1705
+ 'stopped'
1706
+ )
1707
+
1497
1708
  }
1498
1709
 
1499
1710
  // Export for Node.js (CommonJS)