braid-blob 0.0.41 → 0.0.43
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 +28 -8
- package/package.json +1 -1
- package/test/tests.js +250 -0
package/index.js
CHANGED
|
@@ -11,6 +11,8 @@ function create_braid_blob() {
|
|
|
11
11
|
db: null // object with read/write/delete methods
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
var temp_folder = null // will be set in init
|
|
15
|
+
|
|
14
16
|
braid_blob.init = async () => {
|
|
15
17
|
// We only want to initialize once
|
|
16
18
|
var init_p = real_init()
|
|
@@ -21,6 +23,15 @@ function create_braid_blob() {
|
|
|
21
23
|
// Ensure our meta folder exists
|
|
22
24
|
await require('fs').promises.mkdir(braid_blob.meta_folder, { recursive: true })
|
|
23
25
|
|
|
26
|
+
// Create a temp folder inside the meta folder for writing temp files,
|
|
27
|
+
// for atomic writing.
|
|
28
|
+
// The temp folder is called "temp",
|
|
29
|
+
// And this is guaranteed not to conflict with any other files,
|
|
30
|
+
// because other files are the result of encode_filename,
|
|
31
|
+
// which always ends with a ".XX" (for handling insensitive filesystems)
|
|
32
|
+
temp_folder = `${braid_blob.meta_folder}/temp`
|
|
33
|
+
await require('fs').promises.mkdir(temp_folder, { recursive: true })
|
|
34
|
+
|
|
24
35
|
// Set up db - either use provided object or create file-based storage
|
|
25
36
|
if (typeof braid_blob.db_folder === 'string') {
|
|
26
37
|
await require('fs').promises.mkdir(braid_blob.db_folder, { recursive: true })
|
|
@@ -36,7 +47,7 @@ function create_braid_blob() {
|
|
|
36
47
|
},
|
|
37
48
|
write: async (key, data) => {
|
|
38
49
|
var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
|
|
39
|
-
await
|
|
50
|
+
await atomic_write(file_path, data, temp_folder)
|
|
40
51
|
},
|
|
41
52
|
delete: async (key) => {
|
|
42
53
|
var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
|
|
@@ -74,9 +85,8 @@ function create_braid_blob() {
|
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
async function save_meta(key) {
|
|
77
|
-
await
|
|
78
|
-
|
|
79
|
-
JSON.stringify(braid_blob.meta_cache[key]))
|
|
88
|
+
await atomic_write(`${braid_blob.meta_folder}/${encode_filename(key)}`,
|
|
89
|
+
JSON.stringify(braid_blob.meta_cache[key]), temp_folder)
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
async function delete_meta(key) {
|
|
@@ -442,19 +452,23 @@ function create_braid_blob() {
|
|
|
442
452
|
let swap = a; a = b; b = swap
|
|
443
453
|
}
|
|
444
454
|
|
|
445
|
-
var ac =
|
|
446
|
-
options.signal?.addEventListener('abort', () => ac
|
|
455
|
+
var ac = null
|
|
456
|
+
options.signal?.addEventListener('abort', () => ac?.abort())
|
|
447
457
|
|
|
448
458
|
function handle_error(e) {
|
|
449
|
-
if (
|
|
459
|
+
if (options.signal?.aborted) return
|
|
450
460
|
console.log(`disconnected, retrying in 1 second`)
|
|
451
461
|
setTimeout(connect, 1000)
|
|
452
462
|
}
|
|
453
463
|
|
|
454
464
|
async function connect() {
|
|
455
|
-
if (
|
|
465
|
+
if (options.signal?.aborted) return
|
|
456
466
|
if (options.on_pre_connect) await options.on_pre_connect()
|
|
457
467
|
|
|
468
|
+
// Abort stuff in the previous connect
|
|
469
|
+
ac?.abort()
|
|
470
|
+
ac = new AbortController()
|
|
471
|
+
|
|
458
472
|
try {
|
|
459
473
|
// Check if remote has our current version (simple fork-point check)
|
|
460
474
|
var server_has_our_version = false
|
|
@@ -733,6 +747,12 @@ function create_braid_blob() {
|
|
|
733
747
|
return normalized
|
|
734
748
|
}
|
|
735
749
|
|
|
750
|
+
async function atomic_write(final_destination, data, temp_folder) {
|
|
751
|
+
var temp = `${temp_folder}/${Math.random().toString(36).slice(2)}`
|
|
752
|
+
await require('fs').promises.writeFile(temp, data)
|
|
753
|
+
await require('fs').promises.rename(temp, final_destination)
|
|
754
|
+
}
|
|
755
|
+
|
|
736
756
|
braid_blob.create_braid_blob = create_braid_blob
|
|
737
757
|
|
|
738
758
|
return braid_blob
|
package/package.json
CHANGED
package/test/tests.js
CHANGED
|
@@ -1799,6 +1799,256 @@ runTest(
|
|
|
1799
1799
|
'true'
|
|
1800
1800
|
)
|
|
1801
1801
|
|
|
1802
|
+
runTest(
|
|
1803
|
+
"test atomic write creates temp folder on init",
|
|
1804
|
+
async () => {
|
|
1805
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1806
|
+
method: 'POST',
|
|
1807
|
+
body: `void (async () => {
|
|
1808
|
+
var fs = require('fs').promises
|
|
1809
|
+
var test_id = 'test-atomic-init-' + Math.random().toString(36).slice(2)
|
|
1810
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1811
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1812
|
+
|
|
1813
|
+
try {
|
|
1814
|
+
var bb = braid_blob.create_braid_blob()
|
|
1815
|
+
bb.db_folder = db_folder
|
|
1816
|
+
bb.meta_folder = meta_folder
|
|
1817
|
+
|
|
1818
|
+
// Initialize
|
|
1819
|
+
await bb.init()
|
|
1820
|
+
|
|
1821
|
+
// Check that temp folder exists inside meta folder
|
|
1822
|
+
var temp_folder = meta_folder + '/temp'
|
|
1823
|
+
var stat = await fs.stat(temp_folder)
|
|
1824
|
+
var is_dir = stat.isDirectory()
|
|
1825
|
+
|
|
1826
|
+
res.end(is_dir ? 'true' : 'not a directory')
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
res.end('error: ' + e.message)
|
|
1829
|
+
} finally {
|
|
1830
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1831
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1832
|
+
}
|
|
1833
|
+
})()`
|
|
1834
|
+
})
|
|
1835
|
+
return await r1.text()
|
|
1836
|
+
},
|
|
1837
|
+
'true'
|
|
1838
|
+
)
|
|
1839
|
+
|
|
1840
|
+
runTest(
|
|
1841
|
+
"test atomic write leaves no temp files after successful write",
|
|
1842
|
+
async () => {
|
|
1843
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1844
|
+
method: 'POST',
|
|
1845
|
+
body: `void (async () => {
|
|
1846
|
+
var fs = require('fs').promises
|
|
1847
|
+
var test_id = 'test-atomic-cleanup-' + Math.random().toString(36).slice(2)
|
|
1848
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1849
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1850
|
+
|
|
1851
|
+
try {
|
|
1852
|
+
var bb = braid_blob.create_braid_blob()
|
|
1853
|
+
bb.db_folder = db_folder
|
|
1854
|
+
bb.meta_folder = meta_folder
|
|
1855
|
+
|
|
1856
|
+
// Do a write
|
|
1857
|
+
await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
|
|
1858
|
+
|
|
1859
|
+
// Check that temp folder is empty (no leftover temp files)
|
|
1860
|
+
var temp_folder = meta_folder + '/temp'
|
|
1861
|
+
var files = await fs.readdir(temp_folder)
|
|
1862
|
+
|
|
1863
|
+
res.end(files.length === 0 ? 'true' : 'leftover files: ' + files.join(', '))
|
|
1864
|
+
} catch (e) {
|
|
1865
|
+
res.end('error: ' + e.message)
|
|
1866
|
+
} finally {
|
|
1867
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1868
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1869
|
+
}
|
|
1870
|
+
})()`
|
|
1871
|
+
})
|
|
1872
|
+
return await r1.text()
|
|
1873
|
+
},
|
|
1874
|
+
'true'
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
runTest(
|
|
1878
|
+
"test atomic write data file integrity",
|
|
1879
|
+
async () => {
|
|
1880
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1881
|
+
method: 'POST',
|
|
1882
|
+
body: `void (async () => {
|
|
1883
|
+
var fs = require('fs').promises
|
|
1884
|
+
var test_id = 'test-atomic-integrity-' + Math.random().toString(36).slice(2)
|
|
1885
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1886
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1887
|
+
|
|
1888
|
+
try {
|
|
1889
|
+
var bb = braid_blob.create_braid_blob()
|
|
1890
|
+
bb.db_folder = db_folder
|
|
1891
|
+
bb.meta_folder = meta_folder
|
|
1892
|
+
|
|
1893
|
+
// Write initial content
|
|
1894
|
+
await bb.put('/test-file', Buffer.from('initial content'), { version: ['1'] })
|
|
1895
|
+
|
|
1896
|
+
// Verify we can read it back correctly
|
|
1897
|
+
var result = await bb.get('/test-file')
|
|
1898
|
+
var content = result.body.toString()
|
|
1899
|
+
|
|
1900
|
+
res.end(content === 'initial content' ? 'true' : 'wrong content: ' + content)
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
res.end('error: ' + e.message)
|
|
1903
|
+
} finally {
|
|
1904
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1905
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1906
|
+
}
|
|
1907
|
+
})()`
|
|
1908
|
+
})
|
|
1909
|
+
return await r1.text()
|
|
1910
|
+
},
|
|
1911
|
+
'true'
|
|
1912
|
+
)
|
|
1913
|
+
|
|
1914
|
+
runTest(
|
|
1915
|
+
"test atomic write - multiple rapid writes preserve last value",
|
|
1916
|
+
async () => {
|
|
1917
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1918
|
+
method: 'POST',
|
|
1919
|
+
body: `void (async () => {
|
|
1920
|
+
var fs = require('fs').promises
|
|
1921
|
+
var test_id = 'test-atomic-rapid-' + Math.random().toString(36).slice(2)
|
|
1922
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1923
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1924
|
+
|
|
1925
|
+
try {
|
|
1926
|
+
var bb = braid_blob.create_braid_blob()
|
|
1927
|
+
bb.db_folder = db_folder
|
|
1928
|
+
bb.meta_folder = meta_folder
|
|
1929
|
+
|
|
1930
|
+
// Do multiple rapid writes
|
|
1931
|
+
await bb.put('/test-file', Buffer.from('write1'), { version: ['1'] })
|
|
1932
|
+
await bb.put('/test-file', Buffer.from('write2'), { version: ['2'] })
|
|
1933
|
+
await bb.put('/test-file', Buffer.from('write3'), { version: ['3'] })
|
|
1934
|
+
|
|
1935
|
+
// Verify last write won
|
|
1936
|
+
var result = await bb.get('/test-file')
|
|
1937
|
+
var content = result.body.toString()
|
|
1938
|
+
var version = result.version[0]
|
|
1939
|
+
|
|
1940
|
+
// Also verify temp folder is clean
|
|
1941
|
+
var temp_folder = meta_folder + '/temp'
|
|
1942
|
+
var files = await fs.readdir(temp_folder)
|
|
1943
|
+
|
|
1944
|
+
res.end(content === 'write3' && version === '3' && files.length === 0 ? 'true' :
|
|
1945
|
+
'content=' + content + ', version=' + version + ', temp_files=' + files.length)
|
|
1946
|
+
} catch (e) {
|
|
1947
|
+
res.end('error: ' + e.message)
|
|
1948
|
+
} finally {
|
|
1949
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1950
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1951
|
+
}
|
|
1952
|
+
})()`
|
|
1953
|
+
})
|
|
1954
|
+
return await r1.text()
|
|
1955
|
+
},
|
|
1956
|
+
'true'
|
|
1957
|
+
)
|
|
1958
|
+
|
|
1959
|
+
runTest(
|
|
1960
|
+
"test atomic write - meta file is also written atomically",
|
|
1961
|
+
async () => {
|
|
1962
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1963
|
+
method: 'POST',
|
|
1964
|
+
body: `void (async () => {
|
|
1965
|
+
var fs = require('fs').promises
|
|
1966
|
+
var test_id = 'test-atomic-meta-' + Math.random().toString(36).slice(2)
|
|
1967
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1968
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1969
|
+
|
|
1970
|
+
try {
|
|
1971
|
+
var bb = braid_blob.create_braid_blob()
|
|
1972
|
+
bb.db_folder = db_folder
|
|
1973
|
+
bb.meta_folder = meta_folder
|
|
1974
|
+
|
|
1975
|
+
// Write with content_type to test meta file
|
|
1976
|
+
await bb.put('/test-file', Buffer.from('content'), {
|
|
1977
|
+
version: ['test-version'],
|
|
1978
|
+
content_type: 'text/plain'
|
|
1979
|
+
})
|
|
1980
|
+
|
|
1981
|
+
// Create new instance to read from disk (not cache)
|
|
1982
|
+
var bb2 = braid_blob.create_braid_blob()
|
|
1983
|
+
bb2.db_folder = db_folder
|
|
1984
|
+
bb2.meta_folder = meta_folder
|
|
1985
|
+
|
|
1986
|
+
var result = await bb2.get('/test-file')
|
|
1987
|
+
|
|
1988
|
+
// Verify both version and content_type are correctly persisted
|
|
1989
|
+
var version_ok = result.version[0] === 'test-version'
|
|
1990
|
+
var ct_ok = result.content_type === 'text/plain'
|
|
1991
|
+
|
|
1992
|
+
res.end(version_ok && ct_ok ? 'true' :
|
|
1993
|
+
'version_ok=' + version_ok + ', ct_ok=' + ct_ok +
|
|
1994
|
+
', version=' + result.version[0] + ', ct=' + result.content_type)
|
|
1995
|
+
} catch (e) {
|
|
1996
|
+
res.end('error: ' + e.message)
|
|
1997
|
+
} finally {
|
|
1998
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1999
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
2000
|
+
}
|
|
2001
|
+
})()`
|
|
2002
|
+
})
|
|
2003
|
+
return await r1.text()
|
|
2004
|
+
},
|
|
2005
|
+
'true'
|
|
2006
|
+
)
|
|
2007
|
+
|
|
2008
|
+
runTest(
|
|
2009
|
+
"test sync abort stops retry after error",
|
|
2010
|
+
async () => {
|
|
2011
|
+
var local_key = 'test-sync-abort-retry-' + Math.random().toString(36).slice(2)
|
|
2012
|
+
|
|
2013
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
2014
|
+
method: 'POST',
|
|
2015
|
+
body: `void (async () => {
|
|
2016
|
+
try {
|
|
2017
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
2018
|
+
|
|
2019
|
+
// Use unreachable URL to trigger errors (RFC 5737 TEST-NET-1, guaranteed not routable)
|
|
2020
|
+
var remote_url = new URL('http://192.0.2.1:12345/unreachable')
|
|
2021
|
+
|
|
2022
|
+
var connect_count = 0
|
|
2023
|
+
var ac = new AbortController()
|
|
2024
|
+
|
|
2025
|
+
// Start sync - will fail and try to reconnect
|
|
2026
|
+
braid_blob.sync('${local_key}', remote_url, {
|
|
2027
|
+
signal: ac.signal,
|
|
2028
|
+
on_pre_connect: () => {
|
|
2029
|
+
connect_count++
|
|
2030
|
+
// Abort after first connect attempt
|
|
2031
|
+
if (connect_count === 1) {
|
|
2032
|
+
setTimeout(() => ac.abort(), 50)
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
})
|
|
2036
|
+
|
|
2037
|
+
// Wait long enough for potential retries (retry is 1 second)
|
|
2038
|
+
await new Promise(done => setTimeout(done, 1500))
|
|
2039
|
+
|
|
2040
|
+
// Should only have 1 connect attempt since we aborted
|
|
2041
|
+
res.end(connect_count === 1 ? 'true' : 'connect_count=' + connect_count)
|
|
2042
|
+
} catch (e) {
|
|
2043
|
+
res.end('error: ' + e.message)
|
|
2044
|
+
}
|
|
2045
|
+
})()`
|
|
2046
|
+
})
|
|
2047
|
+
return await r1.text()
|
|
2048
|
+
},
|
|
2049
|
+
'true'
|
|
2050
|
+
)
|
|
2051
|
+
|
|
1802
2052
|
}
|
|
1803
2053
|
|
|
1804
2054
|
// Export for Node.js (CommonJS)
|