braid-text 0.2.100 → 0.2.101

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 +38 -8
  2. package/package.json +1 -1
  3. package/test/tests.js +191 -0
package/index.js CHANGED
@@ -1293,23 +1293,43 @@ function create_braid_text() {
1293
1293
 
1294
1294
  async function db_folder_init() {
1295
1295
  if (braid_text.verbose) console.log('__!')
1296
- if (!db_folder_init.p) db_folder_init.p = new Promise(async done => {
1296
+ if (!db_folder_init.p) db_folder_init.p = (async () => {
1297
1297
  await fs.promises.mkdir(braid_text.db_folder, { recursive: true });
1298
1298
  await fs.promises.mkdir(`${braid_text.db_folder}/.meta`, { recursive: true })
1299
1299
  await fs.promises.mkdir(`${braid_text.db_folder}/.temp`, { recursive: true })
1300
+ await fs.promises.mkdir(`${braid_text.db_folder}/.wal-intent`, { recursive: true })
1300
1301
 
1301
1302
  // Clean out .temp directory on startup
1302
1303
  var temp_files = await fs.promises.readdir(`${braid_text.db_folder}/.temp`)
1303
1304
  for (var f of temp_files)
1304
1305
  await fs.promises.unlink(`${braid_text.db_folder}/.temp/${f}`)
1305
1306
 
1307
+ // Replay any pending .wal-intent files
1308
+ var intent_files = await fs.promises.readdir(`${braid_text.db_folder}/.wal-intent`)
1309
+ for (var intent_name of intent_files) {
1310
+ var intent_path = `${braid_text.db_folder}/.wal-intent/${intent_name}`
1311
+ var target_path = `${braid_text.db_folder}/${intent_name}`
1312
+
1313
+ var intent_data = await fs.promises.readFile(intent_path)
1314
+ var expected_size = Number(intent_data.readBigUInt64LE(0))
1315
+ var append_data = intent_data.subarray(8)
1316
+
1317
+ var stat = await fs.promises.stat(target_path)
1318
+ if (stat.size < expected_size || stat.size > expected_size + append_data.length)
1319
+ throw new Error(`wal-intent replay failed: ${target_path} size ${stat.size}, expected ${expected_size} to ${expected_size + append_data.length}`)
1320
+
1321
+ // Append whatever portion hasn't been written yet
1322
+ var already_written = stat.size - expected_size
1323
+ if (already_written < append_data.length)
1324
+ await fs.promises.appendFile(target_path, append_data.subarray(already_written))
1325
+ await fs.promises.unlink(intent_path)
1326
+ }
1327
+
1306
1328
  // Populate key_to_filename mapping from existing files
1307
1329
  var files = (await fs.promises.readdir(braid_text.db_folder))
1308
1330
  .filter(x => /\.\d+$/.test(x))
1309
1331
  init_filename_mapping(files)
1310
-
1311
- done()
1312
- })
1332
+ })()
1313
1333
  await db_folder_init.p
1314
1334
  }
1315
1335
 
@@ -1391,10 +1411,20 @@ function create_braid_text() {
1391
1411
  if (currentSize < threshold) {
1392
1412
  if (braid_text.verbose) console.log(`appending to db..`)
1393
1413
 
1394
- let buffer = Buffer.allocUnsafe(4)
1395
- buffer.writeUInt32LE(bytes.length, 0)
1396
- await fs.promises.appendFile(filename, buffer)
1397
- await fs.promises.appendFile(filename, bytes)
1414
+ let len_buf = Buffer.allocUnsafe(4)
1415
+ len_buf.writeUInt32LE(bytes.length, 0)
1416
+ let append_data = Buffer.concat([len_buf, bytes])
1417
+
1418
+ let basename = require('path').basename(filename)
1419
+ let intent_path = `${braid_text.db_folder}/.wal-intent/${basename}`
1420
+ let stat = await fs.promises.stat(filename)
1421
+ let size_buf = Buffer.allocUnsafe(8)
1422
+ size_buf.writeBigUInt64LE(BigInt(stat.size), 0)
1423
+
1424
+ await atomic_write(intent_path, Buffer.concat([size_buf, append_data]),
1425
+ `${braid_text.db_folder}/.temp`)
1426
+ await fs.promises.appendFile(filename, append_data)
1427
+ await fs.promises.unlink(intent_path)
1398
1428
 
1399
1429
  if (braid_text.verbose) console.log("wrote to : " + filename)
1400
1430
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.2.100",
3
+ "version": "0.2.101",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
package/test/tests.js CHANGED
@@ -2665,6 +2665,197 @@ runTest(
2665
2665
  'filename conflict detected'
2666
2666
  )
2667
2667
 
2668
+ runTest(
2669
+ "test wal-intent recovery after simulated crash during append",
2670
+ async () => {
2671
+ var r = await braid_fetch(`/eval`, {
2672
+ method: 'PUT',
2673
+ body: `void (async () => {
2674
+ var fs = require('fs')
2675
+ var test_db = __dirname + '/test_wal_recovery_' + Math.random().toString(36).slice(2)
2676
+ try {
2677
+ // Create a fresh braid_text instance with its own db_folder
2678
+ var bt = braid_text.create_braid_text()
2679
+ bt.db_folder = test_db
2680
+
2681
+ // Do initial PUT (5 chars = 'hello', version ends at 4)
2682
+ await bt.put('/test', {
2683
+ version: ['a-4'],
2684
+ parents: [],
2685
+ body: 'hello'
2686
+ })
2687
+
2688
+ // Do second PUT to trigger an append (6 more chars = ' world', version ends at 10)
2689
+ await bt.put('/test', {
2690
+ version: ['a-10'],
2691
+ parents: ['a-4'],
2692
+ body: 'hello world'
2693
+ })
2694
+
2695
+ var encoded = bt.encode_filename('/test')
2696
+ var db_file = test_db + '/' + encoded + '.1'
2697
+ var intent_file = test_db + '/.wal-intent/' + encoded + '.1'
2698
+
2699
+ // Read the db file
2700
+ var data = await fs.promises.readFile(db_file)
2701
+
2702
+ // Parse chunks to find the last one
2703
+ var cursor = 0
2704
+ var chunks = []
2705
+ while (cursor < data.length) {
2706
+ var chunk_start = cursor
2707
+ var chunk_size = data.readUInt32LE(cursor)
2708
+ cursor += 4 + chunk_size
2709
+ chunks.push({ start: chunk_start, size: chunk_size, end: cursor })
2710
+ }
2711
+
2712
+ if (chunks.length < 2) {
2713
+ res.end('expected at least 2 chunks, got ' + chunks.length)
2714
+ return
2715
+ }
2716
+
2717
+ var last_chunk = chunks[chunks.length - 1]
2718
+ var prev_end = chunks[chunks.length - 2].end
2719
+
2720
+ // Create the wal-intent file: 8-byte size + the last chunk data
2721
+ var size_buf = Buffer.allocUnsafe(8)
2722
+ size_buf.writeBigUInt64LE(BigInt(prev_end), 0)
2723
+ var last_chunk_data = data.subarray(last_chunk.start, last_chunk.end)
2724
+ var intent_data = Buffer.concat([size_buf, last_chunk_data])
2725
+ await fs.promises.writeFile(intent_file, intent_data)
2726
+
2727
+ // Truncate the db file partway through the last chunk (keep ~half)
2728
+ var truncate_point = last_chunk.start + Math.floor((last_chunk.end - last_chunk.start) / 2)
2729
+ await fs.promises.truncate(db_file, truncate_point)
2730
+
2731
+ // Create a new braid_text instance to simulate restart
2732
+ var bt2 = braid_text.create_braid_text()
2733
+ bt2.db_folder = test_db
2734
+
2735
+ // Get the resource - this should trigger wal-intent replay
2736
+ var resource = await bt2.get_resource('/test')
2737
+ var text = resource.doc.get()
2738
+
2739
+ // Verify intent file was cleaned up
2740
+ var intent_exists = true
2741
+ try {
2742
+ await fs.promises.access(intent_file)
2743
+ } catch (e) {
2744
+ intent_exists = false
2745
+ }
2746
+
2747
+ await fs.promises.rm(test_db, { recursive: true, force: true })
2748
+
2749
+ if (intent_exists) {
2750
+ res.end('intent file still exists')
2751
+ return
2752
+ }
2753
+
2754
+ res.end(text)
2755
+ } catch (e) {
2756
+ await fs.promises.rm(test_db, { recursive: true, force: true }).catch(() => {})
2757
+ res.end('error: ' + e.message + ' ' + e.stack)
2758
+ }
2759
+ })()`
2760
+ })
2761
+
2762
+ return await r.text()
2763
+ },
2764
+ 'hello world'
2765
+ )
2766
+
2767
+ runTest(
2768
+ "test wal-intent throws error when db file is too large",
2769
+ async () => {
2770
+ var r = await braid_fetch(`/eval`, {
2771
+ method: 'PUT',
2772
+ body: `void (async () => {
2773
+ var fs = require('fs')
2774
+ var test_db = __dirname + '/test_wal_error_' + Math.random().toString(36).slice(2)
2775
+ try {
2776
+ // Create a fresh braid_text instance with its own db_folder
2777
+ var bt = braid_text.create_braid_text()
2778
+ bt.db_folder = test_db
2779
+
2780
+ // Do initial PUT (5 chars = 'hello', version ends at 4)
2781
+ await bt.put('/test', {
2782
+ version: ['a-4'],
2783
+ parents: [],
2784
+ body: 'hello'
2785
+ })
2786
+
2787
+ // Do second PUT to trigger an append (6 more chars = ' world', version ends at 10)
2788
+ await bt.put('/test', {
2789
+ version: ['a-10'],
2790
+ parents: ['a-4'],
2791
+ body: 'hello world'
2792
+ })
2793
+
2794
+ var encoded = bt.encode_filename('/test')
2795
+ var db_file = test_db + '/' + encoded + '.1'
2796
+ var intent_file = test_db + '/.wal-intent/' + encoded + '.1'
2797
+
2798
+ // Read the db file
2799
+ var data = await fs.promises.readFile(db_file)
2800
+
2801
+ // Parse chunks to find the last one
2802
+ var cursor = 0
2803
+ var chunks = []
2804
+ while (cursor < data.length) {
2805
+ var chunk_start = cursor
2806
+ var chunk_size = data.readUInt32LE(cursor)
2807
+ cursor += 4 + chunk_size
2808
+ chunks.push({ start: chunk_start, size: chunk_size, end: cursor })
2809
+ }
2810
+
2811
+ if (chunks.length < 2) {
2812
+ res.end('expected at least 2 chunks, got ' + chunks.length)
2813
+ return
2814
+ }
2815
+
2816
+ var last_chunk = chunks[chunks.length - 1]
2817
+ var prev_end = chunks[chunks.length - 2].end
2818
+
2819
+ // Create the wal-intent file: 8-byte size + the last chunk data
2820
+ var size_buf = Buffer.allocUnsafe(8)
2821
+ size_buf.writeBigUInt64LE(BigInt(prev_end), 0)
2822
+ var last_chunk_data = data.subarray(last_chunk.start, last_chunk.end)
2823
+ var intent_data = Buffer.concat([size_buf, last_chunk_data])
2824
+ await fs.promises.writeFile(intent_file, intent_data)
2825
+
2826
+ // Append extra garbage to the db file (making it too large)
2827
+ await fs.promises.appendFile(db_file, Buffer.from('extra garbage data'))
2828
+
2829
+ // Create a new braid_text instance to simulate restart
2830
+ var bt2 = braid_text.create_braid_text()
2831
+ bt2.db_folder = test_db
2832
+
2833
+ // Now try to init - this should throw an error
2834
+ var result
2835
+ try {
2836
+ await bt2.db_folder_init()
2837
+ result = 'should have thrown an error'
2838
+ } catch (e) {
2839
+ if (e.message.includes('wal-intent replay failed')) {
2840
+ result = 'correctly threw error'
2841
+ } else {
2842
+ result = 'wrong error: ' + e.message
2843
+ }
2844
+ }
2845
+ await fs.promises.rm(test_db, { recursive: true, force: true })
2846
+ res.end(result)
2847
+ } catch (e) {
2848
+ await fs.promises.rm(test_db, { recursive: true, force: true }).catch(() => {})
2849
+ res.end('error: ' + e.message)
2850
+ }
2851
+ })()`
2852
+ })
2853
+
2854
+ return await r.text()
2855
+ },
2856
+ 'correctly threw error'
2857
+ )
2858
+
2668
2859
  }
2669
2860
 
2670
2861
  // Export for both Node.js and browser environments