braid-blob 0.0.40 → 0.0.42
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/AI-README.md +13 -32
- package/index.js +24 -8
- package/package.json +1 -1
- package/test/tests.js +370 -0
package/AI-README.md
CHANGED
|
@@ -54,11 +54,6 @@ DEPENDENCY_UPDATES:
|
|
|
54
54
|
- Format: "updates to {package}@{version}"
|
|
55
55
|
- Example: "0.0.18 - ... updates to url-file-db 0.0.8"
|
|
56
56
|
|
|
57
|
-
RESOLVED_ISSUES:
|
|
58
|
-
- url-file-db < 0.0.13: Bug where reading non-existent files returned "index" file contents
|
|
59
|
-
- Fixed in url-file-db 0.0.13+ (properly returns null for non-existent files)
|
|
60
|
-
- Caused "test sync local to remote" to fail with unexpected "shared content"
|
|
61
|
-
- url-file-db 0.0.15+ also relaxed path requirements (no longer requires leading "/")
|
|
62
57
|
```
|
|
63
58
|
|
|
64
59
|
## MODULE_STRUCTURE
|
|
@@ -67,7 +62,7 @@ RESOLVED_ISSUES:
|
|
|
67
62
|
EXPORT: create_braid_blob() -> braid_blob_instance
|
|
68
63
|
MODULE_TYPE: CommonJS
|
|
69
64
|
MAIN_ENTRY: index.js
|
|
70
|
-
DEPENDENCIES: [braid-http,
|
|
65
|
+
DEPENDENCIES: [braid-http, fs]
|
|
71
66
|
```
|
|
72
67
|
|
|
73
68
|
## DATA_MODEL
|
|
@@ -81,9 +76,9 @@ braid_blob_instance = {
|
|
|
81
76
|
|
|
82
77
|
// Runtime state
|
|
83
78
|
cache: object // internal cache
|
|
79
|
+
meta_cache: object // metadata cache
|
|
84
80
|
key_to_subs: Map<key, Map<peer, subscription>> // subscription tracking
|
|
85
|
-
db:
|
|
86
|
-
meta_db: url_file_db_instance // metadata storage backend
|
|
81
|
+
db: {read, write, delete} // blob storage backend (auto-created or custom)
|
|
87
82
|
|
|
88
83
|
// Methods
|
|
89
84
|
init: async () -> void
|
|
@@ -130,6 +125,7 @@ INPUT:
|
|
|
130
125
|
content_type?: string // MIME type
|
|
131
126
|
peer?: string // peer identifier
|
|
132
127
|
skip_write?: boolean // skip disk write (for external changes)
|
|
128
|
+
db?: {read, write, delete} // custom db backend (overrides braid_blob.db)
|
|
133
129
|
signal?: AbortSignal // for URL mode
|
|
134
130
|
headers?: object // for URL mode
|
|
135
131
|
}
|
|
@@ -137,8 +133,8 @@ INPUT:
|
|
|
137
133
|
OUTPUT: version_string
|
|
138
134
|
|
|
139
135
|
SIDE_EFFECTS:
|
|
140
|
-
- Writes blob to
|
|
141
|
-
- Writes metadata to meta_folder
|
|
136
|
+
- Writes blob to db (options.db or braid_blob.db)
|
|
137
|
+
- Writes metadata to meta_folder
|
|
142
138
|
- Notifies active subscriptions (except originating peer)
|
|
143
139
|
- If key instanceof URL: makes remote HTTP PUT via braid_fetch
|
|
144
140
|
|
|
@@ -163,6 +159,7 @@ INPUT:
|
|
|
163
159
|
parents?: [version] // fork-point for subscriptions
|
|
164
160
|
version?: [version] // request specific version
|
|
165
161
|
peer?: string // peer identifier
|
|
162
|
+
db?: {read, write, delete} // custom db backend (overrides braid_blob.db)
|
|
166
163
|
signal?: AbortSignal // for URL mode
|
|
167
164
|
dont_retry?: boolean // for URL mode subscriptions
|
|
168
165
|
}
|
|
@@ -294,14 +291,11 @@ isAcceptable(contentType, acceptHeader) -> boolean
|
|
|
294
291
|
|
|
295
292
|
```
|
|
296
293
|
db_folder/
|
|
297
|
-
{
|
|
298
|
-
-
|
|
299
|
-
- Key mapping: URL-safe encoding of keys
|
|
294
|
+
{encoded_key} # Blob data files
|
|
295
|
+
- Key encoding: encode_filename() escapes special chars
|
|
300
296
|
|
|
301
297
|
meta_folder/
|
|
302
|
-
|
|
303
|
-
db/ # url-file-db for metadata
|
|
304
|
-
{encoded_key}.txt # JSON: {event: version, content_type: mime}
|
|
298
|
+
{encoded_key} # JSON metadata files: {event: version, content_type: mime}
|
|
305
299
|
```
|
|
306
300
|
|
|
307
301
|
## PROTOCOL_DETAILS
|
|
@@ -344,8 +338,8 @@ BRAID_UPDATE_FORMAT:
|
|
|
344
338
|
INITIALIZATION:
|
|
345
339
|
- init() called lazily by put/get/serve
|
|
346
340
|
- init() runs once (subsequent calls return same promise)
|
|
347
|
-
- Creates db
|
|
348
|
-
-
|
|
341
|
+
- Creates db object with read/write/delete methods (or uses provided db_folder object)
|
|
342
|
+
- Generates peer ID if not set
|
|
349
343
|
|
|
350
344
|
SUBSCRIPTION_MANAGEMENT:
|
|
351
345
|
- key_to_subs: Map<string, Map<string, {sendUpdate}>>
|
|
@@ -354,11 +348,6 @@ SUBSCRIPTION_MANAGEMENT:
|
|
|
354
348
|
- Prevents echo: put() doesn't notify originating peer
|
|
355
349
|
- Serialized updates: subscribe_chain ensures sequential callback execution
|
|
356
350
|
|
|
357
|
-
FILE_WATCHING:
|
|
358
|
-
- url-file-db monitors db_folder for external changes
|
|
359
|
-
- External changes trigger put() with skip_write: true
|
|
360
|
-
- Subscriptions notified of external changes
|
|
361
|
-
|
|
362
351
|
CONCURRENCY_CONTROL:
|
|
363
352
|
- within_fiber(key, fn) serializes operations per key
|
|
364
353
|
- Uses promise chain stored in within_fiber.chains[key]
|
|
@@ -413,14 +402,6 @@ braid-http:
|
|
|
413
402
|
- http_server (braidify): Adds Braid protocol support to Node.js HTTP
|
|
414
403
|
- fetch (braid_fetch): Braid-aware fetch implementation
|
|
415
404
|
- Handles: Subscribe headers, Version headers, streaming updates
|
|
416
|
-
|
|
417
|
-
url-file-db (^0.0.15):
|
|
418
|
-
- Bidirectional URL ↔ filesystem mapping
|
|
419
|
-
- Collision-resistant encoding (case-insensitive filesystem safe)
|
|
420
|
-
- File watching for external changes
|
|
421
|
-
- Separate instances for blobs (db) and metadata (meta_db)
|
|
422
|
-
- API change in 0.0.15: use get_canonical_path() instead of url_path_to_canonical_path()
|
|
423
|
-
- Fixed in 0.0.13+: properly returns null for non-existent files (not "index" content)
|
|
424
405
|
```
|
|
425
406
|
|
|
426
407
|
## ERROR_CONDITIONS
|
|
@@ -455,7 +436,7 @@ TEST_RUNNER: test/test.js
|
|
|
455
436
|
- Browser mode: Opens puppeteer, loads test.html
|
|
456
437
|
|
|
457
438
|
TEST_SUITE: test/tests.js
|
|
458
|
-
-
|
|
439
|
+
- 50+ test cases covering:
|
|
459
440
|
- Basic put/get operations
|
|
460
441
|
- Subscriptions and updates
|
|
461
442
|
- Version conflict resolution
|
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) {
|
|
@@ -126,7 +136,7 @@ function create_braid_blob() {
|
|
|
126
136
|
meta.event = their_e
|
|
127
137
|
|
|
128
138
|
if (!options.skip_write)
|
|
129
|
-
await braid_blob.db.write(key, body)
|
|
139
|
+
await (options.db || braid_blob.db).write(key, body)
|
|
130
140
|
if (options.signal?.aborted) return
|
|
131
141
|
|
|
132
142
|
if (options.content_type)
|
|
@@ -254,12 +264,12 @@ function create_braid_blob() {
|
|
|
254
264
|
// Send an immediate update if needed
|
|
255
265
|
if (compare_events(result.version?.[0], options.parents?.[0]) > 0) {
|
|
256
266
|
result.sent = true
|
|
257
|
-
result.body = await braid_blob.db.read(key)
|
|
267
|
+
result.body = await (options.db || braid_blob.db).read(key)
|
|
258
268
|
options.my_subscribe(result)
|
|
259
269
|
}
|
|
260
270
|
} else {
|
|
261
271
|
// If not subscribe, send the body now
|
|
262
|
-
result.body = await braid_blob.db.read(key)
|
|
272
|
+
result.body = await (options.db || braid_blob.db).read(key)
|
|
263
273
|
}
|
|
264
274
|
|
|
265
275
|
return result
|
|
@@ -294,7 +304,7 @@ function create_braid_blob() {
|
|
|
294
304
|
var meta = await get_meta(key)
|
|
295
305
|
if (options.signal?.aborted) return
|
|
296
306
|
|
|
297
|
-
await braid_blob.db.delete(key)
|
|
307
|
+
await (options.db || braid_blob.db).delete(key)
|
|
298
308
|
await delete_meta(key)
|
|
299
309
|
|
|
300
310
|
// Notify all subscriptions of the delete
|
|
@@ -733,6 +743,12 @@ function create_braid_blob() {
|
|
|
733
743
|
return normalized
|
|
734
744
|
}
|
|
735
745
|
|
|
746
|
+
async function atomic_write(final_destination, data, temp_folder) {
|
|
747
|
+
var temp = `${temp_folder}/${Math.random().toString(36).slice(2)}`
|
|
748
|
+
await require('fs').promises.writeFile(temp, data)
|
|
749
|
+
await require('fs').promises.rename(temp, final_destination)
|
|
750
|
+
}
|
|
751
|
+
|
|
736
752
|
braid_blob.create_braid_blob = create_braid_blob
|
|
737
753
|
|
|
738
754
|
return braid_blob
|
package/package.json
CHANGED
package/test/tests.js
CHANGED
|
@@ -1635,6 +1635,376 @@ runTest(
|
|
|
1635
1635
|
'stopped'
|
|
1636
1636
|
)
|
|
1637
1637
|
|
|
1638
|
+
runTest(
|
|
1639
|
+
"test options.db in put writes to custom db",
|
|
1640
|
+
async () => {
|
|
1641
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1642
|
+
method: 'POST',
|
|
1643
|
+
body: `void (async () => {
|
|
1644
|
+
var test_key = '/test-custom-db-put-' + Math.random().toString(36).slice(2)
|
|
1645
|
+
|
|
1646
|
+
// Create a simple in-memory db
|
|
1647
|
+
var custom_storage = {}
|
|
1648
|
+
var custom_db = {
|
|
1649
|
+
read: async (key) => custom_storage[key] || null,
|
|
1650
|
+
write: async (key, data) => { custom_storage[key] = data },
|
|
1651
|
+
delete: async (key) => { delete custom_storage[key] }
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Put using the custom db
|
|
1655
|
+
await braid_blob.put(test_key, Buffer.from('custom db content'), {
|
|
1656
|
+
version: ['100'],
|
|
1657
|
+
db: custom_db
|
|
1658
|
+
})
|
|
1659
|
+
|
|
1660
|
+
// Verify content is in custom db
|
|
1661
|
+
var custom_content = await custom_db.read(test_key)
|
|
1662
|
+
var custom_ok = custom_content && custom_content.toString() === 'custom db content'
|
|
1663
|
+
|
|
1664
|
+
// Verify content is NOT in the default db
|
|
1665
|
+
var default_content = await braid_blob.db.read(test_key)
|
|
1666
|
+
var default_empty = default_content === null
|
|
1667
|
+
|
|
1668
|
+
res.end(custom_ok && default_empty ? 'true' :
|
|
1669
|
+
'custom_ok=' + custom_ok + ', default_empty=' + default_empty)
|
|
1670
|
+
})()`
|
|
1671
|
+
})
|
|
1672
|
+
return await r1.text()
|
|
1673
|
+
},
|
|
1674
|
+
'true'
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
runTest(
|
|
1678
|
+
"test options.db in get reads from custom db",
|
|
1679
|
+
async () => {
|
|
1680
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1681
|
+
method: 'POST',
|
|
1682
|
+
body: `void (async () => {
|
|
1683
|
+
var test_key = '/test-custom-db-get-' + Math.random().toString(36).slice(2)
|
|
1684
|
+
|
|
1685
|
+
// Create a simple in-memory db with some content
|
|
1686
|
+
var custom_storage = {}
|
|
1687
|
+
custom_storage[test_key] = Buffer.from('from custom db')
|
|
1688
|
+
var custom_db = {
|
|
1689
|
+
read: async (key) => custom_storage[key] || null,
|
|
1690
|
+
write: async (key, data) => { custom_storage[key] = data },
|
|
1691
|
+
delete: async (key) => { delete custom_storage[key] }
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Put with skip_write to just create meta
|
|
1695
|
+
await braid_blob.put(test_key, Buffer.from('ignored'), {
|
|
1696
|
+
version: ['200'],
|
|
1697
|
+
skip_write: true
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
// Get using the custom db - should read from custom db
|
|
1701
|
+
var result = await braid_blob.get(test_key, { db: custom_db })
|
|
1702
|
+
|
|
1703
|
+
res.end(result && result.body.toString() === 'from custom db' ? 'true' :
|
|
1704
|
+
'got: ' + (result ? result.body.toString() : 'null'))
|
|
1705
|
+
})()`
|
|
1706
|
+
})
|
|
1707
|
+
return await r1.text()
|
|
1708
|
+
},
|
|
1709
|
+
'true'
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
runTest(
|
|
1713
|
+
"test options.db in delete deletes from custom db",
|
|
1714
|
+
async () => {
|
|
1715
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1716
|
+
method: 'POST',
|
|
1717
|
+
body: `void (async () => {
|
|
1718
|
+
var test_key = '/test-custom-db-delete-' + Math.random().toString(36).slice(2)
|
|
1719
|
+
|
|
1720
|
+
// Create a simple in-memory db
|
|
1721
|
+
var custom_storage = {}
|
|
1722
|
+
custom_storage[test_key] = Buffer.from('custom content')
|
|
1723
|
+
var custom_db = {
|
|
1724
|
+
read: async (key) => custom_storage[key] || null,
|
|
1725
|
+
write: async (key, data) => { custom_storage[key] = data },
|
|
1726
|
+
delete: async (key) => { delete custom_storage[key] }
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Also put to default db
|
|
1730
|
+
await braid_blob.put(test_key, Buffer.from('default content'), {
|
|
1731
|
+
version: ['300']
|
|
1732
|
+
})
|
|
1733
|
+
|
|
1734
|
+
// Delete using custom db - should only delete from custom db
|
|
1735
|
+
await braid_blob.delete(test_key, { db: custom_db })
|
|
1736
|
+
|
|
1737
|
+
// Verify custom db content is gone
|
|
1738
|
+
var custom_content = await custom_db.read(test_key)
|
|
1739
|
+
var custom_deleted = custom_content === null
|
|
1740
|
+
|
|
1741
|
+
// Verify default db content still exists
|
|
1742
|
+
var default_content = await braid_blob.db.read(test_key)
|
|
1743
|
+
var default_exists = default_content && default_content.toString() === 'default content'
|
|
1744
|
+
|
|
1745
|
+
res.end(custom_deleted && default_exists ? 'true' :
|
|
1746
|
+
'custom_deleted=' + custom_deleted + ', default_exists=' + default_exists)
|
|
1747
|
+
})()`
|
|
1748
|
+
})
|
|
1749
|
+
return await r1.text()
|
|
1750
|
+
},
|
|
1751
|
+
'true'
|
|
1752
|
+
)
|
|
1753
|
+
|
|
1754
|
+
runTest(
|
|
1755
|
+
"test options.db in get subscribe uses custom db for initial update",
|
|
1756
|
+
async () => {
|
|
1757
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1758
|
+
method: 'POST',
|
|
1759
|
+
body: `void (async () => {
|
|
1760
|
+
var test_key = '/test-custom-db-sub-' + Math.random().toString(36).slice(2)
|
|
1761
|
+
|
|
1762
|
+
// Create a simple in-memory db with content
|
|
1763
|
+
var custom_storage = {}
|
|
1764
|
+
custom_storage[test_key] = Buffer.from('subscribe custom content')
|
|
1765
|
+
var custom_db = {
|
|
1766
|
+
read: async (key) => custom_storage[key] || null,
|
|
1767
|
+
write: async (key, data) => { custom_storage[key] = data },
|
|
1768
|
+
delete: async (key) => { delete custom_storage[key] }
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Create meta with version using skip_write
|
|
1772
|
+
await braid_blob.put(test_key, Buffer.from('ignored'), {
|
|
1773
|
+
version: ['400'],
|
|
1774
|
+
skip_write: true
|
|
1775
|
+
})
|
|
1776
|
+
|
|
1777
|
+
// Subscribe using custom db - initial update should come from custom db
|
|
1778
|
+
var ac = new AbortController()
|
|
1779
|
+
var received_content = null
|
|
1780
|
+
|
|
1781
|
+
await braid_blob.get(test_key, {
|
|
1782
|
+
db: custom_db,
|
|
1783
|
+
signal: ac.signal,
|
|
1784
|
+
subscribe: (update) => {
|
|
1785
|
+
received_content = update.body.toString()
|
|
1786
|
+
}
|
|
1787
|
+
})
|
|
1788
|
+
|
|
1789
|
+
// Wait for update
|
|
1790
|
+
await new Promise(done => setTimeout(done, 50))
|
|
1791
|
+
ac.abort()
|
|
1792
|
+
|
|
1793
|
+
res.end(received_content === 'subscribe custom content' ? 'true' :
|
|
1794
|
+
'got: ' + received_content)
|
|
1795
|
+
})()`
|
|
1796
|
+
})
|
|
1797
|
+
return await r1.text()
|
|
1798
|
+
},
|
|
1799
|
+
'true'
|
|
1800
|
+
)
|
|
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
|
+
|
|
1638
2008
|
}
|
|
1639
2009
|
|
|
1640
2010
|
// Export for Node.js (CommonJS)
|