braid-blob 0.0.23 → 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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm test:*)"
5
+ ],
6
+ "deny": [],
7
+ "ask": []
8
+ }
9
+ }
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 ||
@@ -209,6 +209,32 @@ function create_braid_blob() {
209
209
  return result
210
210
  }
211
211
 
212
+ braid_blob.delete = async (key, options = {}) => {
213
+ // Handle URL case - make a remote DELETE request
214
+ if (key instanceof URL) {
215
+
216
+ var params = {
217
+ method: 'DELETE',
218
+ signal: options.signal
219
+ }
220
+ for (var x of ['headers', 'peer'])
221
+ if (options[x] != null) params[x] = options[x]
222
+
223
+ return await braid_fetch(key.href, params)
224
+ }
225
+
226
+ await braid_blob.init()
227
+ if (options.signal?.aborted) return
228
+
229
+ // Delete the file from the database
230
+ await braid_blob.db.delete(key)
231
+
232
+ // TODO: notify subscribers of deletion once we have a protocol for that
233
+ // For now, just clean up the subscriptions
234
+ if (braid_blob.key_to_subs[key])
235
+ delete braid_blob.key_to_subs[key]
236
+ }
237
+
212
238
  braid_blob.serve = async (req, res, options = {}) => {
213
239
  await braid_blob.init()
214
240
 
@@ -292,34 +318,47 @@ function create_braid_blob() {
292
318
  res.setHeader("Version", version_to_header(event != null ? [event] : []))
293
319
  res.end('')
294
320
  } else if (req.method === 'DELETE') {
295
- await braid_blob.db.delete(options.key)
321
+ await braid_blob.delete(options.key)
296
322
  res.statusCode = 204 // No Content
297
323
  res.end('')
298
324
  }
299
325
  })
300
326
  }
301
327
 
302
- braid_blob.sync = async (a, b, options = {}) => {
303
- var unsync_cbs = []
304
- options.my_unsync = () => unsync_cbs.forEach(cb => cb())
305
-
328
+ braid_blob.sync = (a, b, options = {}) => {
306
329
  if ((a instanceof URL) === (b instanceof URL)) {
307
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
+
308
335
  var a_ops = {
309
- subscribe: update => braid_blob.put(b, update.body, {
310
- version: update.version,
311
- content_type: update.headers?.['content-type']
312
- })
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
+ }
313
344
  }
314
- 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)))
315
348
 
316
349
  var b_ops = {
317
- subscribe: update => braid_blob.put(a, update.body, {
318
- version: update.version,
319
- content_type: update.headers?.['content-type']
320
- })
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
+ }
321
358
  }
322
- 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)))
323
362
  } else {
324
363
  // One is local, one is remote - make a=local and b=remote (swap if not)
325
364
  if (a instanceof URL) {
@@ -327,17 +366,23 @@ function create_braid_blob() {
327
366
  }
328
367
 
329
368
  var closed = false
330
- options.my_unsync = () => { closed = true; disconnect() }
331
-
332
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
+
333
377
  async function connect() {
334
378
  var ac = new AbortController()
335
- var disconnect_cbs = [() => ac.abort()]
336
- disconnect = () => disconnect_cbs.forEach(cb => cb())
379
+ disconnect = () => ac.abort()
337
380
 
338
381
  try {
339
382
  // Check if remote has our current version (simple fork-point check)
340
- var local_result = await braid_blob.get(a)
383
+ var local_result = await braid_blob.get(a, {
384
+ signal: ac.signal
385
+ })
341
386
  var local_version = local_result ? local_result.version : null
342
387
  var server_has_our_version = false
343
388
 
@@ -353,12 +398,13 @@ function create_braid_blob() {
353
398
 
354
399
  // Local -> remote: subscribe to future local changes
355
400
  var a_ops = {
401
+ signal: ac.signal,
356
402
  subscribe: update => {
357
- update.signal = ac.signal
358
403
  braid_blob.put(b, update.body, {
404
+ signal: ac.signal,
359
405
  version: update.version,
360
406
  content_type: update.content_type
361
- }).catch(e => {
407
+ }).then(local_first_put).catch(e => {
362
408
  if (e.name === 'AbortError') {
363
409
  // ignore
364
410
  } else throw e
@@ -370,24 +416,36 @@ function create_braid_blob() {
370
416
  if (server_has_our_version) {
371
417
  a_ops.parents = local_version
372
418
  }
373
- braid_blob.get(a, a_ops)
374
419
 
375
420
  // Remote -> local: subscribe to remote updates
376
421
  var b_ops = {
422
+ signal: ac.signal,
377
423
  dont_retry: true,
378
424
  subscribe: async update => {
379
425
  await braid_blob.put(a, update.body, {
380
426
  version: update.version,
381
427
  content_type: update.headers?.['content-type']
382
428
  })
429
+ remote_first_put()
383
430
  },
384
431
  }
385
432
  // Use fork-point (parents) to avoid receiving data we already have
386
433
  if (local_version) {
387
434
  b_ops.parents = local_version
388
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
+
389
442
  // NOTE: this should not return, but it might throw
390
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()
391
449
  } catch (e) {
392
450
  if (closed) {
393
451
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.23",
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
@@ -381,6 +381,102 @@ runTest(
381
381
  '204'
382
382
  )
383
383
 
384
+ runTest(
385
+ "test braid_blob.delete() directly",
386
+ async () => {
387
+ var r1 = await braid_fetch(`/eval`, {
388
+ method: 'POST',
389
+ body: `void (async () => {
390
+ var test_id = 'test-db-' + Math.random().toString(36).slice(2)
391
+ var db_folder = __dirname + '/' + test_id + '-db'
392
+ var meta_folder = __dirname + '/' + test_id + '-meta'
393
+
394
+ var bb = braid_blob.create_braid_blob()
395
+ bb.db_folder = db_folder
396
+ bb.meta_folder = meta_folder
397
+
398
+ try {
399
+ // Put a file
400
+ await bb.put('/test-file', Buffer.from('hello'))
401
+
402
+ // Verify it exists
403
+ var result = await bb.get('/test-file')
404
+ if (!result || !result.body) {
405
+ res.end('error: file not found after put')
406
+ return
407
+ }
408
+
409
+ // Delete it
410
+ await bb.delete('/test-file')
411
+
412
+ // Verify it's gone
413
+ var result2 = await bb.get('/test-file')
414
+ if (result2) {
415
+ res.end('error: file still exists after delete')
416
+ return
417
+ }
418
+
419
+ res.end('true')
420
+ } catch (e) {
421
+ res.end('error: ' + e.message)
422
+ } finally {
423
+ await require('fs').promises.rm(db_folder, { recursive: true, force: true })
424
+ await require('fs').promises.rm(meta_folder, { recursive: true, force: true })
425
+ }
426
+ })()`
427
+ })
428
+ return await r1.text()
429
+ },
430
+ 'true'
431
+ )
432
+
433
+ runTest(
434
+ "test braid_blob.delete() cleans up subscriptions",
435
+ async () => {
436
+ var r1 = await braid_fetch(`/eval`, {
437
+ method: 'POST',
438
+ body: `void (async () => {
439
+ var test_id = 'test-db-' + Math.random().toString(36).slice(2)
440
+ var db_folder = __dirname + '/' + test_id + '-db'
441
+ var meta_folder = __dirname + '/' + test_id + '-meta'
442
+
443
+ var bb = braid_blob.create_braid_blob()
444
+ bb.db_folder = db_folder
445
+ bb.meta_folder = meta_folder
446
+
447
+ try {
448
+ // Put a file
449
+ await bb.put('/test-file', Buffer.from('hello'))
450
+
451
+ // Subscribe to it
452
+ var got_update = false
453
+ await bb.get('/test-file', {
454
+ subscribe: (update) => { got_update = true }
455
+ })
456
+
457
+ // Verify subscription exists
458
+ var has_sub_before = !!bb.key_to_subs['/test-file']
459
+
460
+ // Delete it
461
+ await bb.delete('/test-file')
462
+
463
+ // Verify subscription is cleaned up
464
+ var has_sub_after = !!bb.key_to_subs['/test-file']
465
+
466
+ res.end('' + (has_sub_before && !has_sub_after))
467
+ } catch (e) {
468
+ res.end('error: ' + e.message)
469
+ } finally {
470
+ await require('fs').promises.rm(db_folder, { recursive: true, force: true })
471
+ await require('fs').promises.rm(meta_folder, { recursive: true, force: true })
472
+ }
473
+ })()`
474
+ })
475
+ return await r1.text()
476
+ },
477
+ 'true'
478
+ )
479
+
384
480
  runTest(
385
481
  "test that subscribe returns current-version header",
386
482
  async () => {
@@ -1066,12 +1162,14 @@ runTest(
1066
1162
  // Use an invalid/unreachable URL to trigger an error
1067
1163
  var remote_url = new URL('http://localhost:9999/${remote_key}')
1068
1164
 
1069
- // Start sync
1070
- var sync_options = {}
1071
- 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 })
1072
1170
 
1073
1171
  // Close the sync immediately to trigger the closed path when error occurs
1074
- sync_options.my_unsync()
1172
+ ac.abort()
1075
1173
 
1076
1174
  res.end('sync started and closed')
1077
1175
  } catch (e) {
@@ -1105,15 +1203,17 @@ runTest(
1105
1203
  // Use an invalid/unreachable URL to trigger an error
1106
1204
  var remote_url = new URL('http://localhost:9999/${remote_key}')
1107
1205
 
1108
- // Start sync without closing it - should trigger retry
1109
- var sync_options = {}
1110
- 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 })
1111
1211
 
1112
1212
  // Wait a bit for the error to occur and retry message to print
1113
1213
  await new Promise(done => setTimeout(done, 200))
1114
1214
 
1115
1215
  // Now close it to stop retrying
1116
- sync_options.my_unsync()
1216
+ ac.abort()
1117
1217
 
1118
1218
  res.end('sync error occurred')
1119
1219
  } catch (e) {
@@ -1398,6 +1498,213 @@ runTest(
1398
1498
  'true'
1399
1499
  )
1400
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
+
1401
1708
  }
1402
1709
 
1403
1710
  // Export for Node.js (CommonJS)