braidfs 0.0.150 → 0.0.152
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/README.md +13 -5
- package/index.js +88 -34
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ diamond-types CRDT over the Braid HTTP protocol, guaranteeing conflict-free
|
|
|
80
80
|
editing with multiple editors, whether online or offline.
|
|
81
81
|
|
|
82
82
|
A novel trick using [Time Machines](https://braid.org/time-machines) lets us
|
|
83
|
-
|
|
83
|
+
make regular text editors conflict-free, as well, without speaking CRDT!
|
|
84
84
|
This means that you can edit a file in Emacs, even while other people edit the
|
|
85
85
|
same file, without write conflicts, and without adding CRDT code to Emacs.
|
|
86
86
|
(Still under development.)
|
|
@@ -171,12 +171,20 @@ applied to the running braidfs daemon. The only exception is the `port`
|
|
|
171
171
|
setting, which requires restarting the daemon after a change.
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
## Testing
|
|
175
|
+
|
|
176
|
+
Run the test suite with:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
npm test
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This starts a local braid-text and braid-blob server, spawns the braidfs daemon,
|
|
183
|
+
and runs through a series of sync, edit, readonly, and restart scenarios to
|
|
184
|
+
verify everything works end-to-end.
|
|
185
|
+
|
|
174
186
|
## Limitations & Future Work
|
|
175
187
|
|
|
176
|
-
- Doesn't sync binary yet. Just text text mime-types:
|
|
177
|
-
`text/*`, `application/html`, and `application/json`
|
|
178
|
-
- Binary blob support would be pretty easy and nice to add.
|
|
179
|
-
- Contact us if you'd like to add it!
|
|
180
188
|
- Doesn't update your editor's text with remote updates until you save
|
|
181
189
|
- It's not hard to make it live-update, though, so that you can see your edits integrated with others' before you save.
|
|
182
190
|
- Contact us if you'd like to help! It would be a fun project!
|
package/index.js
CHANGED
|
@@ -7,7 +7,13 @@ var { diff_main } = require(`${__dirname}/diff.js`),
|
|
|
7
7
|
|
|
8
8
|
// Helper function to check if a file is binary based on its extension
|
|
9
9
|
function is_binary(filename) {
|
|
10
|
-
const binaryExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3',
|
|
10
|
+
const binaryExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3',
|
|
11
|
+
'.zip', '.tar', '.rar', '.pdf', '.doc', '.docx',
|
|
12
|
+
'.xls', '.xlsx', '.ppt', '.pptx', '.exe', '.dll',
|
|
13
|
+
'.so', '.dylib', '.bin', '.iso', '.img', '.bmp',
|
|
14
|
+
'.tiff', '.svg', '.webp', '.avi', '.mov', '.wmv',
|
|
15
|
+
'.flv', '.mkv', '.wav', '.flac', '.aac', '.ogg',
|
|
16
|
+
'.wma', '.7z', '.gz', '.bz2', '.xz'];
|
|
11
17
|
return binaryExtensions.includes(require('path').extname(filename).toLowerCase());
|
|
12
18
|
}
|
|
13
19
|
|
|
@@ -102,7 +108,8 @@ console.log(`braidfs version: ${require(`${__dirname}/package.json`).version}`)
|
|
|
102
108
|
// process command line args (argv was already processed above for --sync-base)
|
|
103
109
|
if (argv.length === 1 && argv[0].match(/^(run|serve)$/)) {
|
|
104
110
|
return main()
|
|
105
|
-
} else if (argv.length && argv.length % 2 == 0
|
|
111
|
+
} else if (argv.length && argv.length % 2 == 0
|
|
112
|
+
&& argv.every((x, i) => i % 2 != 0 || x.match(/^(sync|unsync)$/))) {
|
|
106
113
|
return (async () => {
|
|
107
114
|
for (let i = 0; i < argv.length; i += 2) {
|
|
108
115
|
var sync = argv[i] === 'sync',
|
|
@@ -124,7 +131,8 @@ if (argv.length === 1 && argv[0].match(/^(run|serve)$/)) {
|
|
|
124
131
|
}]
|
|
125
132
|
})
|
|
126
133
|
if (res.ok) {
|
|
127
|
-
console.log(`Now ${sync ? '' : 'un'}subscribed ${sync ? 'to' : 'from'}
|
|
134
|
+
console.log(`Now ${sync ? '' : 'un'}subscribed ${sync ? 'to' : 'from'}`
|
|
135
|
+
+ ` ${url} in ~/http/.braidfs/config`)
|
|
128
136
|
} else {
|
|
129
137
|
console.log(`failed to ${operation} ${url}`)
|
|
130
138
|
console.log(`server responded with ${res.status}: ${await res.text()}`)
|
|
@@ -183,7 +191,8 @@ async function main() {
|
|
|
183
191
|
var sync = await sync_url.cache[normalize_url(path)]
|
|
184
192
|
|
|
185
193
|
var parents = JSON.parse(decodeURIComponent(m[2]))
|
|
186
|
-
var parent_text = sync?.version_to_text_cache.get(JSON.stringify(parents))
|
|
194
|
+
var parent_text = sync?.version_to_text_cache.get(JSON.stringify(parents))
|
|
195
|
+
?? (await braid_text.get(sync.url, { version: parents, full_response: true })).body
|
|
187
196
|
|
|
188
197
|
var text = await new Promise(done => {
|
|
189
198
|
const chunks = []
|
|
@@ -201,9 +210,14 @@ async function main() {
|
|
|
201
210
|
await braid_text.put(sync.url, { version, parents, patches, merge_type: 'dt' })
|
|
202
211
|
|
|
203
212
|
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
204
|
-
require('fs').appendFileSync(investigating_disconnects_log,
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
require('fs').appendFileSync(investigating_disconnects_log,
|
|
214
|
+
`${Date.now()}:${sync.url} -- plugin edited `
|
|
215
|
+
+ `(${sync.investigating_disconnects_thinks_connected})\n`)
|
|
216
|
+
|
|
217
|
+
// may be able to do this more efficiently.. we want to
|
|
218
|
+
// make sure we're capturing a file write that is after
|
|
219
|
+
// our version was written.. there may be a way we can
|
|
220
|
+
// avoid calling file_needs_writing here
|
|
207
221
|
await new Promise(done => {
|
|
208
222
|
sync.file_written_cbs.push(done)
|
|
209
223
|
sync.signal_file_needs_writing()
|
|
@@ -214,7 +228,9 @@ async function main() {
|
|
|
214
228
|
|
|
215
229
|
if (url !== '.braidfs/config' && url !== '.braidfs/errors') {
|
|
216
230
|
res.writeHead(404, { 'Content-Type': 'text/html' })
|
|
217
|
-
return res.end('Nothing to see here. You can go to
|
|
231
|
+
return res.end('Nothing to see here. You can go to '
|
|
232
|
+
+ '<a href=".braidfs/config">.braidfs/config</a>'
|
|
233
|
+
+ 'or <a href=".braidfs/errors">.braidfs/errors</a>')
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
braid_text.serve(req, res, { key: normalize_url(url) })
|
|
@@ -232,18 +248,25 @@ async function main() {
|
|
|
232
248
|
subscribe: async update => {
|
|
233
249
|
let prev = config
|
|
234
250
|
|
|
235
|
-
let
|
|
251
|
+
let config_text = await braid_text.get('.braidfs/config')
|
|
236
252
|
try {
|
|
237
|
-
config = JSON.parse(
|
|
253
|
+
config = JSON.parse(config_text)
|
|
238
254
|
|
|
239
255
|
// did anything get deleted?
|
|
240
|
-
var
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
var old_urls = Object.entries(prev.sync)
|
|
257
|
+
.filter(sync_entry => sync_entry[1])
|
|
258
|
+
.map(sync_entry => normalize_url(sync_entry[0])
|
|
259
|
+
.replace(/^https?:\/\//, ''))
|
|
260
|
+
var new_urls = new Set(Object.entries(config.sync)
|
|
261
|
+
.filter(sync_entry => sync_entry[1])
|
|
262
|
+
.map(sync_entry => normalize_url(sync_entry[0])
|
|
263
|
+
.replace(/^https?:\/\//, '')))
|
|
264
|
+
for (let url of old_urls.filter(url => !new_urls.has(url)))
|
|
243
265
|
unsync_url(url)
|
|
244
266
|
|
|
245
267
|
// sync all the new stuff
|
|
246
|
-
for (let
|
|
268
|
+
for (let sync_entry of Object.entries(config.sync))
|
|
269
|
+
if (sync_entry[1]) sync_url(sync_entry[0])
|
|
247
270
|
|
|
248
271
|
// if any auth stuff has changed,
|
|
249
272
|
// have the appropriate connections reconnect
|
|
@@ -257,11 +280,11 @@ async function main() {
|
|
|
257
280
|
|| JSON.stringify(prev.cookies[domain]) !== JSON.stringify(v))
|
|
258
281
|
changed.add(domain)
|
|
259
282
|
// ok, have every domain which has changed reconnect
|
|
260
|
-
for (let [path,
|
|
283
|
+
for (let [path, sync] of Object.entries(sync_url.cache))
|
|
261
284
|
if (changed.has(path.split(/\//)[0].split(/:/)[0]))
|
|
262
|
-
(await
|
|
285
|
+
(await sync).reconnect?.()
|
|
263
286
|
} catch (e) {
|
|
264
|
-
if (
|
|
287
|
+
if (config !== '') console.log(`warning: config file is currently invalid.`)
|
|
265
288
|
return
|
|
266
289
|
}
|
|
267
290
|
}
|
|
@@ -269,7 +292,9 @@ async function main() {
|
|
|
269
292
|
sync_url('.braidfs/errors')
|
|
270
293
|
|
|
271
294
|
// console.log({ sync: config.sync })
|
|
272
|
-
for (let
|
|
295
|
+
for (let sync_entry of Object.entries(config.sync))
|
|
296
|
+
if (sync_entry[1])
|
|
297
|
+
sync_url(sync_entry[0])
|
|
273
298
|
|
|
274
299
|
watch_files()
|
|
275
300
|
setTimeout(scan_files, 1200)
|
|
@@ -524,6 +549,7 @@ function sync_url(url) {
|
|
|
524
549
|
headers: {
|
|
525
550
|
// in case it supports dt, so it doesn't give us "simpleton"
|
|
526
551
|
'Merge-Type': 'dt',
|
|
552
|
+
Cookie: config.cookies?.[new URL(url).hostname] || undefined
|
|
527
553
|
}
|
|
528
554
|
})
|
|
529
555
|
if (self.ac.signal.aborted) return
|
|
@@ -634,7 +660,8 @@ function sync_url(url) {
|
|
|
634
660
|
await save_meta()
|
|
635
661
|
if (self.ac.signal.aborted) return
|
|
636
662
|
|
|
637
|
-
if (self.file_read_only !== null
|
|
663
|
+
if (self.file_read_only !== null
|
|
664
|
+
&& await is_read_only(fullpath) !== self.file_read_only) {
|
|
638
665
|
if (self.ac.signal.aborted) return
|
|
639
666
|
await set_read_only(fullpath, self.file_read_only)
|
|
640
667
|
}
|
|
@@ -825,7 +852,7 @@ function sync_url(url) {
|
|
|
825
852
|
if (!self.local_edit_counter) self.local_edit_counter = 0
|
|
826
853
|
|
|
827
854
|
try {
|
|
828
|
-
self.file_last_text = (await wait_on(braid_text.get(url, { version: file_last_version }))).body
|
|
855
|
+
self.file_last_text = (await wait_on(braid_text.get(url, { version: file_last_version, full_response: true }))).body
|
|
829
856
|
} catch (e) {
|
|
830
857
|
if (self.ac.signal.aborted) return
|
|
831
858
|
// the version from the meta file doesn't exist..
|
|
@@ -834,8 +861,9 @@ function sync_url(url) {
|
|
|
834
861
|
// we want to load the current file contents,
|
|
835
862
|
// which we can acheive by setting file_last_version
|
|
836
863
|
// to the latest
|
|
837
|
-
console.log(`WARNING: there was an issue with the config file,
|
|
838
|
-
|
|
864
|
+
console.log(`WARNING: there was an issue with the config file, `
|
|
865
|
+
+ `and it is reverting to the contents at: ${braidfs_config_file}`)
|
|
866
|
+
var x = await wait_on(braid_text.get(url, {full_response: true}))
|
|
839
867
|
if (self.ac.signal.aborted) return
|
|
840
868
|
file_last_version = x.version
|
|
841
869
|
self.file_last_text = x.body
|
|
@@ -844,11 +872,15 @@ function sync_url(url) {
|
|
|
844
872
|
}
|
|
845
873
|
if (self.ac.signal.aborted) return
|
|
846
874
|
|
|
847
|
-
file_needs_writing = !v_eq(
|
|
875
|
+
file_needs_writing = !v_eq(
|
|
876
|
+
file_last_version,
|
|
877
|
+
(await wait_on(braid_text.get(url, {full_response: true}))).version
|
|
878
|
+
)
|
|
848
879
|
if (self.ac.signal.aborted) return
|
|
849
880
|
|
|
850
881
|
// sanity check
|
|
851
|
-
if (file_last_digest && sha256(self.file_last_text) != file_last_digest)
|
|
882
|
+
if (file_last_digest && sha256(self.file_last_text) != file_last_digest)
|
|
883
|
+
throw new Error('file_last_text does not match file_last_digest')
|
|
852
884
|
} else if (await wait_on(require('fs').promises.access(fullpath).then(() => 1, () => 0))) {
|
|
853
885
|
if (self.ac.signal.aborted) return
|
|
854
886
|
// file exists, but not meta file
|
|
@@ -904,7 +936,9 @@ function sync_url(url) {
|
|
|
904
936
|
}
|
|
905
937
|
if (self.ac.signal.aborted) return
|
|
906
938
|
|
|
907
|
-
if (self.file_read_only === null)
|
|
939
|
+
if (self.file_read_only === null)
|
|
940
|
+
try { self.file_read_only = await wait_on(is_read_only(fullpath)) }
|
|
941
|
+
catch (e) { }
|
|
908
942
|
if (self.ac.signal.aborted) return
|
|
909
943
|
|
|
910
944
|
let text = await wait_on(require('fs').promises.readFile(
|
|
@@ -929,11 +963,16 @@ function sync_url(url) {
|
|
|
929
963
|
|
|
930
964
|
add_to_version_cache(text, version)
|
|
931
965
|
|
|
932
|
-
await wait_on(braid_text.put(url, { version, parents, patches,
|
|
966
|
+
await wait_on(braid_text.put(url, { version, parents, patches,
|
|
967
|
+
merge_type: 'dt', peer: file_peer }))
|
|
933
968
|
if (self.ac.signal.aborted) return
|
|
934
969
|
|
|
935
970
|
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
936
|
-
require('fs').appendFileSync(
|
|
971
|
+
require('fs').appendFileSync(
|
|
972
|
+
investigating_disconnects_log,
|
|
973
|
+
`${Date.now()}:${url} -- file edited `
|
|
974
|
+
+ `(${self.investigating_disconnects_thinks_connected})\n`
|
|
975
|
+
)
|
|
937
976
|
|
|
938
977
|
await write_meta_file()
|
|
939
978
|
if (self.ac.signal.aborted) return
|
|
@@ -956,12 +995,14 @@ function sync_url(url) {
|
|
|
956
995
|
if (self.ac.signal.aborted) return
|
|
957
996
|
} else if (file_needs_writing) {
|
|
958
997
|
file_needs_writing = false
|
|
959
|
-
let { version, body } = await wait_on(braid_text.get(url, {}))
|
|
998
|
+
let { version, body } = await wait_on(braid_text.get(url, {full_response: true}))
|
|
960
999
|
if (self.ac.signal.aborted) return
|
|
961
1000
|
if (!v_eq(version, file_last_version)) {
|
|
962
1001
|
// let's do a final check to see if anything has changed
|
|
963
1002
|
// before writing out a new version of the file
|
|
964
|
-
let text = await wait_on(
|
|
1003
|
+
let text = await wait_on(
|
|
1004
|
+
require('fs').promises.readFile(fullpath, { encoding: 'utf8' })
|
|
1005
|
+
)
|
|
965
1006
|
if (self.ac.signal.aborted) return
|
|
966
1007
|
if (self.file_last_text != text) {
|
|
967
1008
|
// if the text is different, let's read it first..
|
|
@@ -974,7 +1015,10 @@ function sync_url(url) {
|
|
|
974
1015
|
|
|
975
1016
|
add_to_version_cache(body, version)
|
|
976
1017
|
|
|
977
|
-
try {
|
|
1018
|
+
try {
|
|
1019
|
+
if (await wait_on(is_read_only(fullpath)))
|
|
1020
|
+
await wait_on(set_read_only(fullpath, false))
|
|
1021
|
+
} catch (e) { }
|
|
978
1022
|
if (self.ac.signal.aborted) return
|
|
979
1023
|
|
|
980
1024
|
file_last_version = version
|
|
@@ -994,7 +1038,9 @@ function sync_url(url) {
|
|
|
994
1038
|
}
|
|
995
1039
|
if (self.ac.signal.aborted) return
|
|
996
1040
|
|
|
997
|
-
self.file_mtimeNs_str = '' + (await wait_on(
|
|
1041
|
+
self.file_mtimeNs_str = '' + (await wait_on(
|
|
1042
|
+
require('fs').promises.stat(fullpath, { bigint: true })
|
|
1043
|
+
)).mtimeNs
|
|
998
1044
|
if (self.ac.signal.aborted) return
|
|
999
1045
|
|
|
1000
1046
|
for (var cb of self.file_written_cbs) cb()
|
|
@@ -1028,11 +1074,16 @@ function sync_url(url) {
|
|
|
1028
1074
|
aborts.clear()
|
|
1029
1075
|
}
|
|
1030
1076
|
|
|
1031
|
-
// Subscribe to local changes to trigger file writes
|
|
1077
|
+
// Subscribe to local changes to trigger file writes.
|
|
1078
|
+
// head: true = header-only updates: we only need to know *that*
|
|
1079
|
+
// something changed. (Without it, braid-text materializes the
|
|
1080
|
+
// doc's entire edit history as patch objects just for this
|
|
1081
|
+
// subscription, which caused huge transient memory at startup.)
|
|
1082
|
+
// The single initial head update triggers the initial write.
|
|
1032
1083
|
braid_text.get(url, {
|
|
1033
1084
|
signal: ac.signal,
|
|
1034
1085
|
peer: file_peer,
|
|
1035
|
-
|
|
1086
|
+
head: true,
|
|
1036
1087
|
subscribe: () => {
|
|
1037
1088
|
if (self.ac.signal.aborted) return
|
|
1038
1089
|
self.signal_file_needs_writing()
|
|
@@ -1341,7 +1392,10 @@ async function set_read_only(fullpath, read_only) {
|
|
|
1341
1392
|
|
|
1342
1393
|
if (require('os').platform() === "win32") {
|
|
1343
1394
|
await new Promise((resolve, reject) => {
|
|
1344
|
-
require("child_process").exec(
|
|
1395
|
+
require("child_process").exec(
|
|
1396
|
+
`fsutil file setattr readonly "${fullpath}" ${!!read_only}`
|
|
1397
|
+
, (error) => error ? reject(error) : resolve()
|
|
1398
|
+
)
|
|
1345
1399
|
})
|
|
1346
1400
|
} else {
|
|
1347
1401
|
let mode = (await require('fs').promises.stat(fullpath)).mode
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braidfs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.152",
|
|
4
4
|
"description": "braid technology synchronizing files and webpages",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braidfs",
|
|
7
7
|
"homepage": "https://braid.org",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"braid-http": "~1.3.89",
|
|
10
|
-
"braid-text": "~0.
|
|
10
|
+
"braid-text": "~0.5.16",
|
|
11
11
|
"braid-blob": "~0.0.72",
|
|
12
12
|
"chokidar": "^4.0.3",
|
|
13
13
|
"undici": "^7.18.2"
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"braidfs": "./index.sh"
|
|
17
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test/test.js"
|
|
20
|
+
},
|
|
18
21
|
"files": [
|
|
19
22
|
"index.sh",
|
|
20
23
|
"index.js",
|