braid-blob 0.0.24 → 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.
Files changed (3) hide show
  1. package/index.js +100 -62
  2. package/package.json +2 -2
  3. package/test/tests.js +366 -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,34 +107,25 @@ 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
 
127
- if (options.subscribe) {
128
- if (options.dont_retry) {
129
- var error_happened
130
- var error_promise = new Promise((_, fail) => error_happened = fail)
131
- }
123
+ if (res.status === 404) return null
132
124
 
125
+ if (options.subscribe) {
133
126
  res.subscribe(async update => {
134
127
  await options.subscribe(update)
135
- }, e => options.dont_retry && error_happened(e))
136
-
137
- if (options.dont_retry) {
138
- return await error_promise
139
- }
128
+ }, e => options.on_error?.(e))
140
129
  return res
141
130
  } else {
142
131
  return await res.arrayBuffer()
@@ -154,6 +143,7 @@ function create_braid_blob() {
154
143
  content_type: meta.content_type
155
144
  }
156
145
  if (options.header_cb) await options.header_cb(result)
146
+ if (options.signal?.aborted) return
157
147
  // Check if requested version/parents is newer than what we have - if so, we don't have it
158
148
  if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
159
149
  throw new Error('unkown version: ' + options.version)
@@ -164,7 +154,8 @@ function create_braid_blob() {
164
154
  if (options.subscribe) {
165
155
  var subscribe_chain = Promise.resolve()
166
156
  options.my_subscribe = (x) => subscribe_chain =
167
- subscribe_chain.then(() => options.subscribe(x))
157
+ subscribe_chain.then(() =>
158
+ !options.signal?.aborted && options.subscribe(x))
168
159
 
169
160
  // Start a subscription for future updates
170
161
  if (!braid_blob.key_to_subs[key])
@@ -181,14 +172,14 @@ function create_braid_blob() {
181
172
  }
182
173
  })
183
174
 
184
- // Store unsubscribe function
185
- result.unsubscribe = () => {
175
+ options.signal?.addEventListener('abort', () => {
186
176
  braid_blob.key_to_subs[key].delete(peer)
187
177
  if (!braid_blob.key_to_subs[key].size)
188
178
  delete braid_blob.key_to_subs[key]
189
- }
179
+ })
190
180
 
191
181
  if (options.before_send_cb) await options.before_send_cb(result)
182
+ if (options.signal?.aborted) return
192
183
 
193
184
  // Send an immediate update if needed
194
185
  if (!options.parents ||
@@ -212,15 +203,10 @@ function create_braid_blob() {
212
203
  braid_blob.delete = async (key, options = {}) => {
213
204
  // Handle URL case - make a remote DELETE request
214
205
  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
206
 
221
207
  var params = {
222
208
  method: 'DELETE',
223
- signal: options.my_abort.signal
209
+ signal: options.signal
224
210
  }
225
211
  for (var x of ['headers', 'peer'])
226
212
  if (options[x] != null) params[x] = options[x]
@@ -229,6 +215,7 @@ function create_braid_blob() {
229
215
  }
230
216
 
231
217
  await braid_blob.init()
218
+ if (options.signal?.aborted) return
232
219
 
233
220
  // Delete the file from the database
234
221
  await braid_blob.db.delete(key)
@@ -329,27 +316,40 @@ function create_braid_blob() {
329
316
  })
330
317
  }
331
318
 
332
- braid_blob.sync = async (a, b, options = {}) => {
333
- var unsync_cbs = []
334
- options.my_unsync = () => unsync_cbs.forEach(cb => cb())
335
-
319
+ braid_blob.sync = (a, b, options = {}) => {
336
320
  if ((a instanceof URL) === (b instanceof URL)) {
337
321
  // Both are URLs or both are local keys
322
+ var a_first_put, b_first_put
323
+ var a_first_put_promise = new Promise(done => a_first_put = done)
324
+ var b_first_put_promise = new Promise(done => b_first_put = done)
325
+
338
326
  var a_ops = {
339
- subscribe: update => braid_blob.put(b, update.body, {
340
- version: update.version,
341
- content_type: update.headers?.['content-type']
342
- })
327
+ signal: options.signal,
328
+ subscribe: update => {
329
+ braid_blob.put(b, update.body, {
330
+ signal: options.signal,
331
+ version: update.version,
332
+ content_type: update.headers?.['content-type']
333
+ }).then(a_first_put)
334
+ }
343
335
  }
344
- braid_blob.get(a, a_ops)
336
+ braid_blob.get(a, a_ops).then(x =>
337
+ x || b_first_put_promise.then(() =>
338
+ braid_blob.get(a, a_ops)))
345
339
 
346
340
  var b_ops = {
347
- subscribe: update => braid_blob.put(a, update.body, {
348
- version: update.version,
349
- content_type: update.headers?.['content-type']
350
- })
341
+ signal: options.signal,
342
+ subscribe: update => {
343
+ braid_blob.put(a, update.body, {
344
+ signal: options.signal,
345
+ version: update.version,
346
+ content_type: update.headers?.['content-type']
347
+ }).then(b_first_put)
348
+ }
351
349
  }
352
- braid_blob.get(b, b_ops)
350
+ braid_blob.get(b, b_ops).then(x =>
351
+ x || a_first_put_promise.then(() =>
352
+ braid_blob.get(b, b_ops)))
353
353
  } else {
354
354
  // One is local, one is remote - make a=local and b=remote (swap if not)
355
355
  if (a instanceof URL) {
@@ -357,17 +357,30 @@ function create_braid_blob() {
357
357
  }
358
358
 
359
359
  var closed = false
360
- options.my_unsync = () => { closed = true; disconnect() }
361
-
362
360
  var disconnect = () => { }
361
+ options.signal?.addEventListener('abort', () =>
362
+ { closed = true; disconnect() })
363
+
364
+ var local_first_put, remote_first_put
365
+ var local_first_put_promise = new Promise(done => local_first_put = done)
366
+ var remote_first_put_promise = new Promise(done => remote_first_put = done)
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
+
363
375
  async function connect() {
364
376
  var ac = new AbortController()
365
- var disconnect_cbs = [() => ac.abort()]
366
- disconnect = () => disconnect_cbs.forEach(cb => cb())
377
+ disconnect = () => ac.abort()
367
378
 
368
379
  try {
369
380
  // Check if remote has our current version (simple fork-point check)
370
- var local_result = await braid_blob.get(a)
381
+ var local_result = await braid_blob.get(a, {
382
+ signal: ac.signal
383
+ })
371
384
  var local_version = local_result ? local_result.version : null
372
385
  var server_has_our_version = false
373
386
 
@@ -383,12 +396,13 @@ function create_braid_blob() {
383
396
 
384
397
  // Local -> remote: subscribe to future local changes
385
398
  var a_ops = {
399
+ signal: ac.signal,
386
400
  subscribe: update => {
387
- update.signal = ac.signal
388
401
  braid_blob.put(b, update.body, {
402
+ signal: ac.signal,
389
403
  version: update.version,
390
404
  content_type: update.content_type
391
- }).catch(e => {
405
+ }).then(local_first_put).catch(e => {
392
406
  if (e.name === 'AbortError') {
393
407
  // ignore
394
408
  } else throw e
@@ -400,32 +414,56 @@ function create_braid_blob() {
400
414
  if (server_has_our_version) {
401
415
  a_ops.parents = local_version
402
416
  }
403
- braid_blob.get(a, a_ops)
404
417
 
405
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
+
406
430
  var b_ops = {
407
- dont_retry: true,
431
+ signal: ac.signal,
408
432
  subscribe: async update => {
409
433
  await braid_blob.put(a, update.body, {
410
434
  version: update.version,
411
435
  content_type: update.headers?.['content-type']
412
436
  })
437
+ got_local_file()
438
+ remote_first_put()
413
439
  },
440
+ on_error: handle_error
414
441
  }
415
442
  // Use fork-point (parents) to avoid receiving data we already have
416
443
  if (local_version) {
417
444
  b_ops.parents = local_version
418
445
  }
419
- // NOTE: this should not return, but it might throw
420
- await braid_blob.get(b, b_ops)
421
- } catch (e) {
422
- if (closed) {
423
- return
424
- }
425
446
 
426
- disconnect()
427
- console.log(`disconnected, retrying in 1 second`)
428
- setTimeout(connect, 1000)
447
+ // Set up both subscriptions, handling cases where one doesn't exist yet
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
+ })
453
+
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()
457
+
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()
463
+ }
464
+ // Otherwise, on_error will call handle_error when connection drops
465
+ } catch (e) {
466
+ handle_error(e)
429
467
  }
430
468
  }
431
469
  connect()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.24",
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.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 () => {
@@ -1162,12 +1309,14 @@ runTest(
1162
1309
  // Use an invalid/unreachable URL to trigger an error
1163
1310
  var remote_url = new URL('http://localhost:9999/${remote_key}')
1164
1311
 
1165
- // Start sync
1166
- var sync_options = {}
1167
- braid_blob.sync('${local_key}', remote_url, sync_options)
1312
+ // Create an AbortController to stop the sync
1313
+ var ac = new AbortController()
1314
+
1315
+ // Start sync with signal
1316
+ braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
1168
1317
 
1169
1318
  // Close the sync immediately to trigger the closed path when error occurs
1170
- sync_options.my_unsync()
1319
+ ac.abort()
1171
1320
 
1172
1321
  res.end('sync started and closed')
1173
1322
  } catch (e) {
@@ -1201,15 +1350,17 @@ runTest(
1201
1350
  // Use an invalid/unreachable URL to trigger an error
1202
1351
  var remote_url = new URL('http://localhost:9999/${remote_key}')
1203
1352
 
1204
- // Start sync without closing it - should trigger retry
1205
- var sync_options = {}
1206
- braid_blob.sync('${local_key}', remote_url, sync_options)
1353
+ // Create an AbortController to stop the sync
1354
+ var ac = new AbortController()
1355
+
1356
+ // Start sync with signal - should trigger retry on error
1357
+ braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
1207
1358
 
1208
1359
  // Wait a bit for the error to occur and retry message to print
1209
1360
  await new Promise(done => setTimeout(done, 200))
1210
1361
 
1211
1362
  // Now close it to stop retrying
1212
- sync_options.my_unsync()
1363
+ ac.abort()
1213
1364
 
1214
1365
  res.end('sync error occurred')
1215
1366
  } catch (e) {
@@ -1494,6 +1645,213 @@ runTest(
1494
1645
  'true'
1495
1646
  )
1496
1647
 
1648
+ runTest(
1649
+ "test get with URL returns null on 404",
1650
+ async () => {
1651
+ var key = 'test-url-get-404-' + Math.random().toString(36).slice(2)
1652
+
1653
+ var r1 = await braid_fetch(`/eval`, {
1654
+ method: 'POST',
1655
+ body: `void (async () => {
1656
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
1657
+ var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
1658
+ var result = await braid_blob.get(url)
1659
+ res.end(result === null ? 'null' : 'not null: ' + JSON.stringify(result))
1660
+ })()`
1661
+ })
1662
+
1663
+ return await r1.text()
1664
+ },
1665
+ 'null'
1666
+ )
1667
+
1668
+ runTest(
1669
+ "test signal abort stops local put operation",
1670
+ async () => {
1671
+ var r1 = await braid_fetch(`/eval`, {
1672
+ method: 'POST',
1673
+ body: `void (async () => {
1674
+ var fs = require('fs').promises
1675
+ var test_id = 'test-abort-put-' + Math.random().toString(36).slice(2)
1676
+ var db_folder = __dirname + '/' + test_id + '-db'
1677
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1678
+
1679
+ try {
1680
+ var bb = braid_blob.create_braid_blob()
1681
+ bb.db_folder = db_folder
1682
+ bb.meta_folder = meta_folder
1683
+
1684
+ // Create an already-aborted signal
1685
+ var ac = new AbortController()
1686
+ ac.abort()
1687
+
1688
+ // Try to put with aborted signal
1689
+ var result = await bb.put('/test-file', Buffer.from('hello'), {
1690
+ signal: ac.signal
1691
+ })
1692
+
1693
+ // Result should be undefined since operation was aborted
1694
+ res.end(result === undefined ? 'aborted' : 'not aborted: ' + result)
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
+ 'aborted'
1706
+ )
1707
+
1708
+ runTest(
1709
+ "test signal abort stops local get operation",
1710
+ async () => {
1711
+ var r1 = await braid_fetch(`/eval`, {
1712
+ method: 'POST',
1713
+ body: `void (async () => {
1714
+ var fs = require('fs').promises
1715
+ var test_id = 'test-abort-get-' + Math.random().toString(36).slice(2)
1716
+ var db_folder = __dirname + '/' + test_id + '-db'
1717
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1718
+
1719
+ try {
1720
+ var bb = braid_blob.create_braid_blob()
1721
+ bb.db_folder = db_folder
1722
+ bb.meta_folder = meta_folder
1723
+
1724
+ // Put a file first
1725
+ await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
1726
+
1727
+ // Create an already-aborted signal
1728
+ var ac = new AbortController()
1729
+ ac.abort()
1730
+
1731
+ // Try to get with aborted signal (after header_cb)
1732
+ var header_called = false
1733
+ var result = await bb.get('/test-file', {
1734
+ signal: ac.signal,
1735
+ header_cb: () => { header_called = true }
1736
+ })
1737
+
1738
+ // Result should be undefined since operation was aborted after header_cb
1739
+ res.end(header_called && result === undefined ? 'aborted' : 'not aborted: header=' + header_called + ' result=' + JSON.stringify(result))
1740
+ } catch (e) {
1741
+ res.end('error: ' + e.message)
1742
+ } finally {
1743
+ await fs.rm(db_folder, { recursive: true, force: true })
1744
+ await fs.rm(meta_folder, { recursive: true, force: true })
1745
+ }
1746
+ })()`
1747
+ })
1748
+ return await r1.text()
1749
+ },
1750
+ 'aborted'
1751
+ )
1752
+
1753
+ runTest(
1754
+ "test signal abort stops local delete operation",
1755
+ async () => {
1756
+ var r1 = await braid_fetch(`/eval`, {
1757
+ method: 'POST',
1758
+ body: `void (async () => {
1759
+ var fs = require('fs').promises
1760
+ var test_id = 'test-abort-delete-' + Math.random().toString(36).slice(2)
1761
+ var db_folder = __dirname + '/' + test_id + '-db'
1762
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1763
+
1764
+ try {
1765
+ var bb = braid_blob.create_braid_blob()
1766
+ bb.db_folder = db_folder
1767
+ bb.meta_folder = meta_folder
1768
+
1769
+ // Put a file first
1770
+ await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
1771
+
1772
+ // Create an already-aborted signal
1773
+ var ac = new AbortController()
1774
+ ac.abort()
1775
+
1776
+ // Try to delete with aborted signal
1777
+ await bb.delete('/test-file', { signal: ac.signal })
1778
+
1779
+ // File should still exist since delete was aborted
1780
+ var result = await bb.get('/test-file')
1781
+ res.end(result && result.body ? 'still exists' : 'deleted')
1782
+ } catch (e) {
1783
+ res.end('error: ' + e.message)
1784
+ } finally {
1785
+ await fs.rm(db_folder, { recursive: true, force: true })
1786
+ await fs.rm(meta_folder, { recursive: true, force: true })
1787
+ }
1788
+ })()`
1789
+ })
1790
+ return await r1.text()
1791
+ },
1792
+ 'still exists'
1793
+ )
1794
+
1795
+ runTest(
1796
+ "test signal abort stops subscription updates",
1797
+ async () => {
1798
+ var r1 = await braid_fetch(`/eval`, {
1799
+ method: 'POST',
1800
+ body: `void (async () => {
1801
+ var fs = require('fs').promises
1802
+ var test_id = 'test-abort-sub-' + Math.random().toString(36).slice(2)
1803
+ var db_folder = __dirname + '/' + test_id + '-db'
1804
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1805
+
1806
+ try {
1807
+ var bb = braid_blob.create_braid_blob()
1808
+ bb.db_folder = db_folder
1809
+ bb.meta_folder = meta_folder
1810
+
1811
+ // Put a file first
1812
+ await bb.put('/test-file', Buffer.from('v1'), { version: ['1'] })
1813
+
1814
+ // Subscribe with an AbortController
1815
+ var ac = new AbortController()
1816
+ var updates = []
1817
+
1818
+ await bb.get('/test-file', {
1819
+ signal: ac.signal,
1820
+ subscribe: (update) => {
1821
+ updates.push(update.body.toString())
1822
+ }
1823
+ })
1824
+
1825
+ // Should have received initial update
1826
+ if (updates.length !== 1 || updates[0] !== 'v1') {
1827
+ res.end('initial update wrong: ' + JSON.stringify(updates))
1828
+ return
1829
+ }
1830
+
1831
+ // Abort the subscription
1832
+ ac.abort()
1833
+
1834
+ // Put another update
1835
+ await bb.put('/test-file', Buffer.from('v2'), { version: ['2'] })
1836
+
1837
+ // Wait a bit for any updates to propagate
1838
+ await new Promise(done => setTimeout(done, 50))
1839
+
1840
+ // Should still only have the initial update
1841
+ res.end(updates.length === 1 ? 'stopped' : 'got extra: ' + JSON.stringify(updates))
1842
+ } catch (e) {
1843
+ res.end('error: ' + e.message)
1844
+ } finally {
1845
+ await fs.rm(db_folder, { recursive: true, force: true })
1846
+ await fs.rm(meta_folder, { recursive: true, force: true })
1847
+ }
1848
+ })()`
1849
+ })
1850
+ return await r1.text()
1851
+ },
1852
+ 'stopped'
1853
+ )
1854
+
1497
1855
  }
1498
1856
 
1499
1857
  // Export for Node.js (CommonJS)