braid-text 0.2.100 → 0.2.102
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.
- package/index.js +38 -8
- package/package.json +2 -2
- 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 =
|
|
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
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-text",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.102",
|
|
4
4
|
"description": "Library for collaborative text over http using braid.",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braid-text",
|
|
7
7
|
"homepage": "https://braid.org",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@braid.org/diamond-types-node": "^2.0.0",
|
|
10
|
-
"braid-http": "~1.3.
|
|
10
|
+
"braid-http": "~1.3.86",
|
|
11
11
|
"url-file-db": "^0.0.25"
|
|
12
12
|
}
|
|
13
13
|
}
|
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
|