braidfs 0.0.149 → 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 +92 -35
- 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
|
|
@@ -861,6 +893,8 @@ function sync_url(url) {
|
|
|
861
893
|
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
862
894
|
do_investigating_disconnects_log(url, 'after within_fiber')
|
|
863
895
|
|
|
896
|
+
var file_peer = Math.random().toString(36).slice(2)
|
|
897
|
+
|
|
864
898
|
await file_loop_pump()
|
|
865
899
|
async function file_loop_pump() {
|
|
866
900
|
if (self.ac.signal.aborted) return
|
|
@@ -902,7 +936,9 @@ function sync_url(url) {
|
|
|
902
936
|
}
|
|
903
937
|
if (self.ac.signal.aborted) return
|
|
904
938
|
|
|
905
|
-
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) { }
|
|
906
942
|
if (self.ac.signal.aborted) return
|
|
907
943
|
|
|
908
944
|
let text = await wait_on(require('fs').promises.readFile(
|
|
@@ -927,11 +963,16 @@ function sync_url(url) {
|
|
|
927
963
|
|
|
928
964
|
add_to_version_cache(text, version)
|
|
929
965
|
|
|
930
|
-
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 }))
|
|
931
968
|
if (self.ac.signal.aborted) return
|
|
932
969
|
|
|
933
970
|
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
934
|
-
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
|
+
)
|
|
935
976
|
|
|
936
977
|
await write_meta_file()
|
|
937
978
|
if (self.ac.signal.aborted) return
|
|
@@ -954,12 +995,14 @@ function sync_url(url) {
|
|
|
954
995
|
if (self.ac.signal.aborted) return
|
|
955
996
|
} else if (file_needs_writing) {
|
|
956
997
|
file_needs_writing = false
|
|
957
|
-
let { version, body } = await wait_on(braid_text.get(url, {}))
|
|
998
|
+
let { version, body } = await wait_on(braid_text.get(url, {full_response: true}))
|
|
958
999
|
if (self.ac.signal.aborted) return
|
|
959
1000
|
if (!v_eq(version, file_last_version)) {
|
|
960
1001
|
// let's do a final check to see if anything has changed
|
|
961
1002
|
// before writing out a new version of the file
|
|
962
|
-
let text = await wait_on(
|
|
1003
|
+
let text = await wait_on(
|
|
1004
|
+
require('fs').promises.readFile(fullpath, { encoding: 'utf8' })
|
|
1005
|
+
)
|
|
963
1006
|
if (self.ac.signal.aborted) return
|
|
964
1007
|
if (self.file_last_text != text) {
|
|
965
1008
|
// if the text is different, let's read it first..
|
|
@@ -972,7 +1015,10 @@ function sync_url(url) {
|
|
|
972
1015
|
|
|
973
1016
|
add_to_version_cache(body, version)
|
|
974
1017
|
|
|
975
|
-
try {
|
|
1018
|
+
try {
|
|
1019
|
+
if (await wait_on(is_read_only(fullpath)))
|
|
1020
|
+
await wait_on(set_read_only(fullpath, false))
|
|
1021
|
+
} catch (e) { }
|
|
976
1022
|
if (self.ac.signal.aborted) return
|
|
977
1023
|
|
|
978
1024
|
file_last_version = version
|
|
@@ -992,7 +1038,9 @@ function sync_url(url) {
|
|
|
992
1038
|
}
|
|
993
1039
|
if (self.ac.signal.aborted) return
|
|
994
1040
|
|
|
995
|
-
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
|
|
996
1044
|
if (self.ac.signal.aborted) return
|
|
997
1045
|
|
|
998
1046
|
for (var cb of self.file_written_cbs) cb()
|
|
@@ -1026,11 +1074,16 @@ function sync_url(url) {
|
|
|
1026
1074
|
aborts.clear()
|
|
1027
1075
|
}
|
|
1028
1076
|
|
|
1029
|
-
// 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.
|
|
1030
1083
|
braid_text.get(url, {
|
|
1031
1084
|
signal: ac.signal,
|
|
1032
|
-
peer:
|
|
1033
|
-
|
|
1085
|
+
peer: file_peer,
|
|
1086
|
+
head: true,
|
|
1034
1087
|
subscribe: () => {
|
|
1035
1088
|
if (self.ac.signal.aborted) return
|
|
1036
1089
|
self.signal_file_needs_writing()
|
|
@@ -1045,6 +1098,7 @@ function sync_url(url) {
|
|
|
1045
1098
|
|
|
1046
1099
|
fork_point_hint: old_meta_fork_point,
|
|
1047
1100
|
signal: ac.signal,
|
|
1101
|
+
peer: self.peer,
|
|
1048
1102
|
headers: {
|
|
1049
1103
|
'Content-Type': 'text/plain',
|
|
1050
1104
|
...(x => x && { Cookie: x })(config.cookies?.[new URL(url).hostname])
|
|
@@ -1338,7 +1392,10 @@ async function set_read_only(fullpath, read_only) {
|
|
|
1338
1392
|
|
|
1339
1393
|
if (require('os').platform() === "win32") {
|
|
1340
1394
|
await new Promise((resolve, reject) => {
|
|
1341
|
-
require("child_process").exec(
|
|
1395
|
+
require("child_process").exec(
|
|
1396
|
+
`fsutil file setattr readonly "${fullpath}" ${!!read_only}`
|
|
1397
|
+
, (error) => error ? reject(error) : resolve()
|
|
1398
|
+
)
|
|
1342
1399
|
})
|
|
1343
1400
|
} else {
|
|
1344
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",
|