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.
Files changed (3) hide show
  1. package/README.md +13 -5
  2. package/index.js +92 -35
  3. 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
- making regular text editors conflict-free, as well, without speaking CRDT!
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', '.zip', '.tar', '.rar', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.exe', '.dll', '.so', '.dylib', '.bin', '.iso', '.img', '.bmp', '.tiff', '.svg', '.webp', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.wav', '.flac', '.aac', '.ogg', '.wma', '.7z', '.gz', '.bz2', '.xz'];
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 && argv.every((x, i) => i % 2 != 0 || x.match(/^(sync|unsync)$/))) {
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'} ${url} in ~/http/.braidfs/config`)
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)) ?? (await braid_text.get(sync.url, { parents })).body
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, `${Date.now()}:${sync.url} -- plugin edited (${sync.investigating_disconnects_thinks_connected})\n`)
205
-
206
- // may be able to do this more efficiently.. we want to make sure we're capturing a file write that is after our version was written.. there may be a way we can avoid calling file_needs_writing here
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 <a href=".braidfs/config">.braidfs/config</a> or <a href=".braidfs/errors">.braidfs/errors</a>')
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 x = await braid_text.get('.braidfs/config')
251
+ let config_text = await braid_text.get('.braidfs/config')
236
252
  try {
237
- config = JSON.parse(x)
253
+ config = JSON.parse(config_text)
238
254
 
239
255
  // did anything get deleted?
240
- var old_syncs = Object.entries(prev.sync).filter(x => x[1]).map(x => normalize_url(x[0]).replace(/^https?:\/\//, ''))
241
- var new_syncs = new Set(Object.entries(config.sync).filter(x => x[1]).map(x => normalize_url(x[0]).replace(/^https?:\/\//, '')))
242
- for (let url of old_syncs.filter(x => !new_syncs.has(x)))
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 x of Object.entries(config.sync)) if (x[1]) sync_url(x[0])
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, x] of Object.entries(sync_url.cache))
283
+ for (let [path, sync] of Object.entries(sync_url.cache))
261
284
  if (changed.has(path.split(/\//)[0].split(/:/)[0]))
262
- (await x).reconnect?.()
285
+ (await sync).reconnect?.()
263
286
  } catch (e) {
264
- if (x !== '') console.log(`warning: config file is currently invalid.`)
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 x of Object.entries(config.sync)) if (x[1]) sync_url(x[0])
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 && await is_read_only(fullpath) !== self.file_read_only) {
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, and it is reverting to the contents at: ${braidfs_config_file}`)
838
- var x = await wait_on(braid_text.get(url, {}))
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(file_last_version, (await wait_on(braid_text.get(url, {}))).version)
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) throw new Error('file_last_text does not match 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) try { self.file_read_only = await wait_on(is_read_only(fullpath)) } catch (e) { }
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, merge_type: 'dt' }))
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(investigating_disconnects_log, `${Date.now()}:${url} -- file edited (${self.investigating_disconnects_thinks_connected})\n`)
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(require('fs').promises.readFile(fullpath, { encoding: 'utf8' }))
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 { if (await wait_on(is_read_only(fullpath))) await wait_on(set_read_only(fullpath, false)) } catch (e) { }
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(require('fs').promises.stat(fullpath, { bigint: true }))).mtimeNs
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: self.peer,
1033
- merge_type: 'dt',
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(`fsutil file setattr readonly "${fullpath}" ${!!read_only}`, (error) => error ? reject(error) : resolve())
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.149",
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.2.108",
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",