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.
Files changed (3) hide show
  1. package/index.js +28 -8
  2. package/package.json +1 -1
  3. 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 require('fs').promises.writeFile(file_path, data)
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 require('fs').promises.writeFile(
78
- `${braid_blob.meta_folder}/${encode_filename(key)}`,
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 = new AbortController()
446
- options.signal?.addEventListener('abort', () => ac.abort())
455
+ var ac = null
456
+ options.signal?.addEventListener('abort', () => ac?.abort())
447
457
 
448
458
  function handle_error(e) {
449
- if (ac.signal.aborted) return
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 (ac.signal.aborted) return
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
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
@@ -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)