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.
Files changed (3) hide show
  1. package/README.md +13 -5
  2. package/index.js +88 -34
  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
@@ -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) 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) { }
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, merge_type: 'dt', peer: file_peer }))
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(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
+ )
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(require('fs').promises.readFile(fullpath, { encoding: 'utf8' }))
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 { 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) { }
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(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
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
- merge_type: 'dt',
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(`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
+ )
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.150",
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.110",
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",