braid-blob 0.0.17 → 0.0.19

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.
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(node test/test.js:*)"
4
+ "Bash(node test/test.js:*)",
5
+ "Bash(lsof:*)",
6
+ "Bash(xargs kill -9)"
5
7
  ],
6
8
  "deny": [],
7
9
  "ask": []
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- var {http_server: braidify, free_cors} = require('braid-http'),
1
+ var {http_server: braidify, fetch: braid_fetch} = require('braid-http'),
2
2
  {url_file_db} = require('url-file-db'),
3
3
  fs = require('fs'),
4
4
  path = require('path')
@@ -10,7 +10,8 @@ function create_braid_blob() {
10
10
  cache: {},
11
11
  key_to_subs: {},
12
12
  peer: null, // we'll try to load this from a file, if not set by the user
13
- db: null // url-file-db instance
13
+ db: null, // url-file-db instance for blob storage
14
+ meta_db: null // url-file-db instance for meta storage
14
15
  }
15
16
 
16
17
  braid_blob.init = async () => {
@@ -27,10 +28,11 @@ function create_braid_blob() {
27
28
  await braid_blob.put(key, body, { skip_write: true })
28
29
  })
29
30
 
30
- // Create meta folder
31
- await fs.promises.mkdir(braid_blob.meta_folder, { recursive: true })
31
+ // Create url-file-db instance for meta storage (in a subfolder)
32
+ // This will create both meta_folder and the db subfolder with recursive: true
33
+ braid_blob.meta_db = await url_file_db.create(`${braid_blob.meta_folder}/db`)
32
34
 
33
- // establish a peer id
35
+ // establish a peer id (stored at root of meta_folder, sibling to db subfolder)
34
36
  if (!braid_blob.peer)
35
37
  try {
36
38
  braid_blob.peer = await fs.promises.readFile(`${braid_blob.meta_folder}/peer.txt`, 'utf8')
@@ -42,14 +44,36 @@ function create_braid_blob() {
42
44
  }
43
45
 
44
46
  braid_blob.put = async (key, body, options = {}) => {
47
+ // Handle URL case - make a remote PUT request
48
+ if (key instanceof URL) {
49
+ options.my_abort = new AbortController()
50
+ if (options.signal) {
51
+ options.signal.addEventListener('abort', () =>
52
+ options.my_abort.abort())
53
+ }
54
+
55
+ var params = {
56
+ method: 'PUT',
57
+ signal: options.my_abort.signal,
58
+ retry: () => true,
59
+ body: body
60
+ }
61
+ for (var x of ['headers', 'version', 'peer'])
62
+ if (options[x] != null) params[x] = options[x]
63
+ if (options.content_type) {
64
+ params.headers = { ...params.headers, 'Content-Type': options.content_type }
65
+ }
66
+
67
+ return await braid_fetch(key.href, params)
68
+ }
69
+
45
70
  await braid_blob.init()
46
71
 
47
- // Read the meta file
48
- const metaname = `${braid_blob.meta_folder}/${encode_filename(key)}`
72
+ // Read the meta data from meta_db
49
73
  var meta = {}
50
- try {
51
- meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
52
- } catch (e) {}
74
+ var meta_content = await braid_blob.meta_db.read(key)
75
+ if (meta_content)
76
+ meta = JSON.parse(meta_content.toString('utf8'))
53
77
 
54
78
  var their_e =
55
79
  !options.version ?
@@ -69,11 +93,11 @@ function create_braid_blob() {
69
93
  if (!options.skip_write)
70
94
  await braid_blob.db.write(key, body)
71
95
 
72
- // Write the meta file
96
+ // Write the meta data
73
97
  if (options.content_type)
74
98
  meta.content_type = options.content_type
75
99
 
76
- await fs.promises.writeFile(metaname, JSON.stringify(meta))
100
+ await braid_blob.meta_db.write(key, JSON.stringify(meta))
77
101
 
78
102
  // Notify all subscriptions of the update
79
103
  // (except the peer which made the PUT request itself)
@@ -91,14 +115,49 @@ function create_braid_blob() {
91
115
  }
92
116
 
93
117
  braid_blob.get = async (key, options = {}) => {
118
+ // Handle URL case - make a remote GET request
119
+ if (key instanceof URL) {
120
+ options.my_abort = new AbortController()
121
+
122
+ var params = {
123
+ signal: options.my_abort.signal,
124
+ subscribe: !!options.subscribe,
125
+ heartbeats: 120,
126
+ }
127
+ if (!options.dont_retry) {
128
+ params.retry = () => true
129
+ }
130
+ for (var x of ['headers', 'parents', 'version', 'peer'])
131
+ if (options[x] != null) params[x] = options[x]
132
+
133
+ var res = await braid_fetch(key.href, params)
134
+
135
+ if (options.subscribe) {
136
+ if (options.dont_retry) {
137
+ var error_happened
138
+ var error_promise = new Promise((_, fail) => error_happened = fail)
139
+ }
140
+
141
+ res.subscribe(async update => {
142
+ await options.subscribe(update)
143
+ }, e => options.dont_retry && error_happened(e))
144
+
145
+ if (options.dont_retry) {
146
+ return await error_promise
147
+ }
148
+ return res
149
+ } else {
150
+ return await res.arrayBuffer()
151
+ }
152
+ }
153
+
94
154
  await braid_blob.init()
95
155
 
96
- // Read the meta file
97
- const metaname = `${braid_blob.meta_folder}/${encode_filename(key)}`
156
+ // Read the meta data from meta_db
98
157
  var meta = {}
99
- try {
100
- meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
101
- } catch (e) {}
158
+ var meta_content = await braid_blob.meta_db.read(key)
159
+ if (meta_content)
160
+ meta = JSON.parse(meta_content.toString('utf8'))
102
161
  if (meta.event == null) return null
103
162
 
104
163
  var result = {
@@ -106,6 +165,11 @@ function create_braid_blob() {
106
165
  content_type: meta.content_type
107
166
  }
108
167
  if (options.header_cb) await options.header_cb(result)
168
+ // Check if requested version/parents is newer than what we have - if so, we don't have it
169
+ if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
170
+ throw new Error('unkown version: ' + options.version)
171
+ if (options.parents && options.parents.length && compare_events(options.parents[0], meta.event) > 0)
172
+ throw new Error('unkown version: ' + options.parents)
109
173
  if (options.head) return
110
174
 
111
175
  if (options.subscribe) {
@@ -171,40 +235,48 @@ function create_braid_blob() {
171
235
  var body = req.method === 'PUT' && await slurp(req)
172
236
 
173
237
  await within_fiber(options.key, async () => {
174
- const metaname = `${braid_blob.meta_folder}/${encode_filename(options.key)}`
175
-
176
- // Read the meta file
238
+ // Read the meta data from meta_db
177
239
  var meta = {}
178
- try {
179
- meta = JSON.parse(await fs.promises.readFile(metaname, 'utf8'))
180
- } catch (e) {}
240
+ var meta_content = await braid_blob.meta_db.read(options.key)
241
+ if (meta_content)
242
+ meta = JSON.parse(meta_content.toString('utf8'))
181
243
 
182
- if (req.method === 'GET') {
244
+ if (req.method === 'GET' || req.method === 'HEAD') {
183
245
  if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
184
246
  if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
185
247
  res.setHeader("Merge-Type", "lww")
186
248
 
187
- var result = await braid_blob.get(options.key, {
188
- peer: req.peer,
189
- head: req.method == "HEAD",
190
- parents: req.parents || null,
191
- header_cb: (result) => {
192
- res.setHeader((req.subscribe ? "Current-" : "") +
193
- "Version", ascii_ify(result.version.map((x) =>
194
- JSON.stringify(x)).join(", ")))
195
- if (result.content_type)
196
- res.setHeader('Content-Type', result.content_type)
197
- },
198
- before_send_cb: (result) =>
199
- res.startSubscription({ onClose: result.unsubscribe }),
200
- subscribe: req.subscribe ? (update) => {
201
- res.sendUpdate({
202
- version: update.version,
203
- 'Merge-Type': 'lww',
204
- body: update.body
205
- })
206
- } : null
207
- })
249
+ try {
250
+ var result = await braid_blob.get(options.key, {
251
+ peer: req.peer,
252
+ head: req.method == "HEAD",
253
+ version: req.version || null,
254
+ parents: req.parents || null,
255
+ header_cb: (result) => {
256
+ res.setHeader((req.subscribe ? "Current-" : "") +
257
+ "Version", ascii_ify(result.version.map((x) =>
258
+ JSON.stringify(x)).join(", ")))
259
+ if (result.content_type)
260
+ res.setHeader('Content-Type', result.content_type)
261
+ },
262
+ before_send_cb: (result) =>
263
+ res.startSubscription({ onClose: result.unsubscribe }),
264
+ subscribe: req.subscribe ? (update) => {
265
+ res.sendUpdate({
266
+ version: update.version,
267
+ 'Merge-Type': 'lww',
268
+ body: update.body
269
+ })
270
+ } : null
271
+ })
272
+ } catch (e) {
273
+ if (e.message && e.message.startsWith('unkown version')) {
274
+ // Server doesn't have this version
275
+ res.statusCode = 309
276
+ res.statusMessage = 'Version Unknown Here'
277
+ return res.end('')
278
+ } else throw e
279
+ }
208
280
 
209
281
  if (!result) {
210
282
  res.statusCode = 404
@@ -234,18 +306,117 @@ function create_braid_blob() {
234
306
  res.setHeader("Version", version_to_header(meta.event != null ? [meta.event] : []))
235
307
  res.end('')
236
308
  } else if (req.method === 'DELETE') {
237
- try {
238
- await braid_blob.db.delete(options.key)
239
- } catch (e) {}
240
- try {
241
- await fs.promises.unlink(metaname)
242
- } catch (e) {}
309
+ await braid_blob.db.delete(options.key)
310
+ await braid_blob.meta_db.delete(options.key)
243
311
  res.statusCode = 204 // No Content
244
312
  res.end('')
245
313
  }
246
314
  })
247
315
  }
248
316
 
317
+ braid_blob.sync = async (a, b, options = {}) => {
318
+ var unsync_cbs = []
319
+ options.my_unsync = () => unsync_cbs.forEach(cb => cb())
320
+
321
+ if ((a instanceof URL) === (b instanceof URL)) {
322
+ // Both are URLs or both are local keys
323
+ var a_ops = {
324
+ subscribe: update => braid_blob.put(b, update.body, {
325
+ version: update.version,
326
+ content_type: update.headers?.['content-type']
327
+ })
328
+ }
329
+ braid_blob.get(a, a_ops)
330
+
331
+ var b_ops = {
332
+ subscribe: update => braid_blob.put(a, update.body, {
333
+ version: update.version,
334
+ content_type: update.headers?.['content-type']
335
+ })
336
+ }
337
+ braid_blob.get(b, b_ops)
338
+ } else {
339
+ // One is local, one is remote - make a=local and b=remote (swap if not)
340
+ if (a instanceof URL) {
341
+ let swap = a; a = b; b = swap
342
+ }
343
+
344
+ var closed = false
345
+ options.my_unsync = () => { closed = true; disconnect() }
346
+
347
+ var disconnect = () => { }
348
+ async function connect() {
349
+ var ac = new AbortController()
350
+ var disconnect_cbs = [() => ac.abort()]
351
+ disconnect = () => disconnect_cbs.forEach(cb => cb())
352
+
353
+ try {
354
+ // Check if remote has our current version (simple fork-point check)
355
+ var local_result = await braid_blob.get(a)
356
+ var local_version = local_result ? local_result.version : null
357
+ var server_has_our_version = false
358
+
359
+ if (local_version) {
360
+ // Check if server has our version
361
+ var r = await braid_fetch(b.href, {
362
+ signal: ac.signal,
363
+ method: "HEAD",
364
+ version: local_version
365
+ })
366
+ server_has_our_version = r.ok
367
+ }
368
+
369
+ // Local -> remote: subscribe to future local changes
370
+ var a_ops = {
371
+ subscribe: update => {
372
+ update.signal = ac.signal
373
+ braid_blob.put(b, update.body, {
374
+ version: update.version,
375
+ content_type: update.content_type
376
+ }).catch(e => {
377
+ if (e.name === 'AbortError') {
378
+ // ignore
379
+ } else throw e
380
+ })
381
+ }
382
+ }
383
+ // Only set parents if server already has our version
384
+ // If server doesn't have it, omit parents so subscription sends everything
385
+ if (server_has_our_version) {
386
+ a_ops.parents = local_version
387
+ }
388
+ braid_blob.get(a, a_ops)
389
+
390
+ // Remote -> local: subscribe to remote updates
391
+ var b_ops = {
392
+ dont_retry: true,
393
+ subscribe: async update => {
394
+ await braid_blob.put(a, update.body, {
395
+ version: update.version,
396
+ content_type: update.headers?.['content-type']
397
+ })
398
+ },
399
+ }
400
+ // Use fork-point (parents) to avoid receiving data we already have
401
+ if (local_version) {
402
+ b_ops.parents = local_version
403
+ }
404
+ // NOTE: this should not return, but it might throw
405
+ await braid_blob.get(b, b_ops)
406
+ } catch (e) {
407
+ if (closed) {
408
+ return
409
+ }
410
+
411
+ disconnect()
412
+ console.log(`disconnected, retrying in 1 second`)
413
+ setTimeout(connect, 1000)
414
+ }
415
+ }
416
+ connect()
417
+ }
418
+ }
419
+
249
420
  function compare_events(a, b) {
250
421
  var a_num = get_event_seq(a)
251
422
  var b_num = get_event_seq(b)
@@ -275,16 +446,6 @@ function create_braid_blob() {
275
446
  return ascii_ify(version.map(v => JSON.stringify(v)).join(', '))
276
447
  }
277
448
 
278
- function encode_filename(filename) {
279
- // Swap all "!" and "/" characters
280
- let swapped = filename.replace(/[!/]/g, (match) => (match === "!" ? "/" : "!"))
281
-
282
- // Encode the filename using encodeURIComponent()
283
- let encoded = encodeURIComponent(swapped)
284
-
285
- return encoded
286
- }
287
-
288
449
  function within_fiber(id, func) {
289
450
  if (!within_fiber.chains) within_fiber.chains = {}
290
451
  var prev = within_fiber.chains[id] || Promise.resolve()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
@@ -11,6 +11,6 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "braid-http": "~1.3.82",
14
- "url-file-db": "~0.0.7"
14
+ "url-file-db": "~0.0.8"
15
15
  }
16
16
  }
package/server-demo.js CHANGED
@@ -8,6 +8,8 @@ var braid_blob = require(`${__dirname}/index.js`)
8
8
  // braid_blob.db_folder = './custom_files_folder'
9
9
  // braid_blob.meta_folder = './custom_meta_folder'
10
10
 
11
+ braid_blob.init()
12
+
11
13
  var server = require("http").createServer(async (req, res) => {
12
14
  console.log(`${req.method} ${req.url}`)
13
15
 
@@ -19,5 +21,5 @@ server.listen(port, () => {
19
21
  console.log(`files stored in: ${braid_blob.db_folder}`)
20
22
  })
21
23
 
22
- // curl -X PUT --data-binary @blob.png http://localhost:8888/blob.png
24
+ // curl -X PUT -H "Content-Type: image/png" --data-binary @blob.png http://localhost:8888/blob.png
23
25
  // curl http://localhost:8888/blob.png --output new-blob.png
package/test/tests.js CHANGED
@@ -688,6 +688,460 @@ runTest(
688
688
  'false'
689
689
  )
690
690
 
691
+ runTest(
692
+ "test that meta filenames distinguish between 'a' and 'A' on case-insensitive filesystems",
693
+ async () => {
694
+ var suffix = Math.random().toString(36).slice(2)
695
+ var key1 = 'test-' + suffix + '-a'
696
+ var key2 = 'test-' + suffix + '-A'
697
+
698
+ // PUT to lowercase key with version 100
699
+ var r = await braid_fetch(`/${key1}`, {
700
+ method: 'PUT',
701
+ version: ['100'],
702
+ body: 'lowercase content'
703
+ })
704
+ if (!r.ok) throw 'PUT to lowercase key failed: ' + r.status
705
+
706
+ // PUT to uppercase key with version 200
707
+ var r = await braid_fetch(`/${key2}`, {
708
+ method: 'PUT',
709
+ version: ['200'],
710
+ body: 'uppercase content'
711
+ })
712
+ if (!r.ok) throw 'PUT to uppercase key failed: ' + r.status
713
+
714
+ // GET both and verify they have different versions (stored in meta files)
715
+ var r1 = await braid_fetch(`/${key1}`)
716
+ if (!r1.ok) throw 'GET lowercase key failed: ' + r1.status
717
+ var version1 = r1.headers.get('version')
718
+
719
+ var r2 = await braid_fetch(`/${key2}`)
720
+ if (!r2.ok) throw 'GET uppercase key failed: ' + r2.status
721
+ var version2 = r2.headers.get('version')
722
+
723
+ return version1 + '|' + version2
724
+ },
725
+ '"100"|"200"'
726
+ )
727
+
728
+ runTest(
729
+ "test put with URL (no content_type)",
730
+ async () => {
731
+ var key = 'test-url-put-' + Math.random().toString(36).slice(2)
732
+
733
+ var r1 = await braid_fetch(`/eval`, {
734
+ method: 'POST',
735
+ body: `void (async () => {
736
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
737
+ var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
738
+ await braid_blob.put(url, Buffer.from('url put test'), { version: ['100'] })
739
+ res.end('done')
740
+ })()`
741
+ })
742
+ await r1.text()
743
+
744
+ var r = await braid_fetch(`/${key}`)
745
+ return await r.text()
746
+ },
747
+ 'url put test'
748
+ )
749
+
750
+ runTest(
751
+ "test put with URL (with content_type)",
752
+ async () => {
753
+ var key = 'test-url-put-ct-' + Math.random().toString(36).slice(2)
754
+
755
+ var r1 = await braid_fetch(`/eval`, {
756
+ method: 'POST',
757
+ body: `void (async () => {
758
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
759
+ var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
760
+ await braid_blob.put(url, Buffer.from('url put with ct'), {
761
+ version: ['200'],
762
+ content_type: 'text/plain'
763
+ })
764
+ res.end('done')
765
+ })()`
766
+ })
767
+ await r1.text()
768
+
769
+ var r = await braid_fetch(`/${key}`)
770
+ return r.headers.get('content-type') + '|' + await r.text()
771
+ },
772
+ 'text/plain|url put with ct'
773
+ )
774
+
775
+ runTest(
776
+ "test get with URL (no subscribe)",
777
+ async () => {
778
+ var key = 'test-url-get-' + Math.random().toString(36).slice(2)
779
+
780
+ await braid_fetch(`/${key}`, {
781
+ method: 'PUT',
782
+ version: ['300'],
783
+ body: 'url get test'
784
+ })
785
+
786
+ var r1 = await braid_fetch(`/eval`, {
787
+ method: 'POST',
788
+ body: `void (async () => {
789
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
790
+ var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
791
+ var result = await braid_blob.get(url)
792
+ res.end(Buffer.from(result).toString('utf8'))
793
+ })()`
794
+ })
795
+
796
+ return await r1.text()
797
+ },
798
+ 'url get test'
799
+ )
800
+
801
+ runTest(
802
+ "test get with URL (with subscribe)",
803
+ async () => {
804
+ var key = 'test-url-get-sub-' + Math.random().toString(36).slice(2)
805
+
806
+ await braid_fetch(`/${key}`, {
807
+ method: 'PUT',
808
+ version: ['400'],
809
+ body: 'initial'
810
+ })
811
+
812
+ // Use a promise to wait for the eval to complete
813
+ var evalPromise = braid_fetch(`/eval`, {
814
+ method: 'POST',
815
+ body: `void (async () => {
816
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
817
+ var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
818
+
819
+ var updates = []
820
+ var a = new AbortController()
821
+
822
+ // Don't await - braid_blob.get returns immediately when subscribe is used
823
+ braid_blob.get(url, {
824
+ subscribe: update => {
825
+ updates.push(Buffer.from(update.body).toString('utf8'))
826
+ if (updates.length === 2) {
827
+ a.abort()
828
+ res.end(updates.join('|'))
829
+ }
830
+ },
831
+ signal: a.signal
832
+ })
833
+ })()`
834
+ })
835
+
836
+ // Wait a bit for subscription to be established
837
+ await new Promise(done => setTimeout(done, 100))
838
+
839
+ // Send update
840
+ await braid_fetch(`/${key}`, {
841
+ method: 'PUT',
842
+ version: ['500'],
843
+ body: 'updated'
844
+ })
845
+
846
+ // Wait for the eval to complete
847
+ var r1 = await evalPromise
848
+ return await r1.text()
849
+ },
850
+ 'initial|updated'
851
+ )
852
+
853
+ runTest(
854
+ "test sync local to remote",
855
+ async () => {
856
+ var local_key = 'test-sync-local-' + Math.random().toString(36).slice(2)
857
+ var remote_key = 'test-sync-remote-' + Math.random().toString(36).slice(2)
858
+
859
+ var r1 = await braid_fetch(`/eval`, {
860
+ method: 'POST',
861
+ body: `void (async () => {
862
+ try {
863
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
864
+
865
+ // Put something locally first
866
+ await braid_blob.put('${local_key}', Buffer.from('local content'), { version: ['600'] })
867
+
868
+ var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
869
+
870
+ // Start sync
871
+ braid_blob.sync('${local_key}', remote_url)
872
+
873
+ res.end('syncing')
874
+ } catch (e) {
875
+ res.end('error: ' + e.message + ' ' + e.stack)
876
+ }
877
+ })()`
878
+ })
879
+ var result = await r1.text()
880
+ if (result.startsWith('error:')) return result
881
+
882
+ // Wait a bit for sync to happen
883
+ await new Promise(done => setTimeout(done, 100))
884
+
885
+ // Check remote has the content
886
+ var r = await braid_fetch(`/${remote_key}`)
887
+ return await r.text()
888
+ },
889
+ 'local content'
890
+ )
891
+
892
+ runTest(
893
+ "test sync two local keys",
894
+ async () => {
895
+ var key1 = 'test-sync-local1-' + Math.random().toString(36).slice(2)
896
+ var key2 = 'test-sync-local2-' + Math.random().toString(36).slice(2)
897
+
898
+ var r1 = await braid_fetch(`/eval`, {
899
+ method: 'POST',
900
+ body: `void (async () => {
901
+ try {
902
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
903
+
904
+ // Put something to first key
905
+ await braid_blob.put('${key1}', Buffer.from('sync local content'), { version: ['700'] })
906
+
907
+ // Start sync between two local keys
908
+ braid_blob.sync('${key1}', '${key2}')
909
+
910
+ res.end('syncing')
911
+ } catch (e) {
912
+ res.end('error: ' + e.message + ' ' + e.stack)
913
+ }
914
+ })()`
915
+ })
916
+ var result = await r1.text()
917
+ if (result.startsWith('error:')) return result
918
+
919
+ // Wait a bit for sync to happen
920
+ await new Promise(done => setTimeout(done, 100))
921
+
922
+ // Check second key has the content
923
+ var r = await braid_fetch(`/${key2}`)
924
+ return await r.text()
925
+ },
926
+ 'sync local content'
927
+ )
928
+
929
+ runTest(
930
+ "test sync remote to local (swap)",
931
+ async () => {
932
+ var local_key = 'test-sync-swap-local-' + Math.random().toString(36).slice(2)
933
+ var remote_key = 'test-sync-swap-remote-' + Math.random().toString(36).slice(2)
934
+
935
+ // Put something on the server first
936
+ await braid_fetch(`/${remote_key}`, {
937
+ method: 'PUT',
938
+ version: ['800'],
939
+ body: 'remote content'
940
+ })
941
+
942
+ var r1 = await braid_fetch(`/eval`, {
943
+ method: 'POST',
944
+ body: `void (async () => {
945
+ try {
946
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
947
+ var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
948
+
949
+ // Start sync with URL as first argument (should swap internally)
950
+ braid_blob.sync(remote_url, '${local_key}')
951
+
952
+ res.end('syncing')
953
+ } catch (e) {
954
+ res.end('error: ' + e.message + ' ' + e.stack)
955
+ }
956
+ })()`
957
+ })
958
+ var result = await r1.text()
959
+ if (result.startsWith('error:')) return result
960
+
961
+ // Wait a bit for sync to happen
962
+ await new Promise(done => setTimeout(done, 100))
963
+
964
+ // Check local key has the remote content
965
+ var r = await braid_fetch(`/${local_key}`)
966
+ return await r.text()
967
+ },
968
+ 'remote content'
969
+ )
970
+
971
+ runTest(
972
+ "test sync when server already has our version",
973
+ async () => {
974
+ var local_key = 'test-sync-has-version-local-' + Math.random().toString(36).slice(2)
975
+ var remote_key = 'test-sync-has-version-remote-' + Math.random().toString(36).slice(2)
976
+
977
+ // Put the same content on both local and remote with the same version
978
+ var version = ['900']
979
+ var content = 'shared content'
980
+
981
+ // Put on remote first
982
+ await braid_fetch(`/${remote_key}`, {
983
+ method: 'PUT',
984
+ version: version,
985
+ body: content
986
+ })
987
+
988
+ var r1 = await braid_fetch(`/eval`, {
989
+ method: 'POST',
990
+ body: `void (async () => {
991
+ try {
992
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
993
+
994
+ // Put the same content locally with the same version
995
+ await braid_blob.put('${local_key}', Buffer.from('${content}'), { version: ${JSON.stringify(version)} })
996
+
997
+ var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
998
+
999
+ // Start sync - this should trigger the "server already has our version" path
1000
+ braid_blob.sync('${local_key}', remote_url)
1001
+
1002
+ res.end('syncing')
1003
+ } catch (e) {
1004
+ res.end('error: ' + e.message + ' ' + e.stack)
1005
+ }
1006
+ })()`
1007
+ })
1008
+ var result = await r1.text()
1009
+ if (result.startsWith('error:')) return result
1010
+
1011
+ // Wait a bit for sync to initialize (the console.log should happen quickly)
1012
+ await new Promise(done => setTimeout(done, 100))
1013
+
1014
+ // Verify that both still have the same content
1015
+ var r = await braid_fetch(`/${remote_key}`)
1016
+ return await r.text()
1017
+ },
1018
+ 'shared content'
1019
+ )
1020
+
1021
+ runTest(
1022
+ "test sync closed during error",
1023
+ async () => {
1024
+ var local_key = 'test-sync-closed-local-' + Math.random().toString(36).slice(2)
1025
+ var remote_key = 'test-sync-closed-remote-' + Math.random().toString(36).slice(2)
1026
+
1027
+ var r1 = await braid_fetch(`/eval`, {
1028
+ method: 'POST',
1029
+ body: `void (async () => {
1030
+ try {
1031
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
1032
+
1033
+ // Use an invalid/unreachable URL to trigger an error
1034
+ var remote_url = new URL('http://localhost:9999/${remote_key}')
1035
+
1036
+ // Start sync
1037
+ var sync_options = {}
1038
+ braid_blob.sync('${local_key}', remote_url, sync_options)
1039
+
1040
+ // Close the sync immediately to trigger the closed path when error occurs
1041
+ sync_options.my_unsync()
1042
+
1043
+ res.end('sync started and closed')
1044
+ } catch (e) {
1045
+ res.end('error: ' + e.message + ' ' + e.stack)
1046
+ }
1047
+ })()`
1048
+ })
1049
+ var result = await r1.text()
1050
+ if (result.startsWith('error:')) return result
1051
+
1052
+ // Wait for the connection error and closed message
1053
+ await new Promise(done => setTimeout(done, 200))
1054
+
1055
+ return result
1056
+ },
1057
+ 'sync started and closed'
1058
+ )
1059
+
1060
+ runTest(
1061
+ "test sync error with retry",
1062
+ async () => {
1063
+ var local_key = 'test-sync-retry-local-' + Math.random().toString(36).slice(2)
1064
+ var remote_key = 'test-sync-retry-remote-' + Math.random().toString(36).slice(2)
1065
+
1066
+ var r1 = await braid_fetch(`/eval`, {
1067
+ method: 'POST',
1068
+ body: `void (async () => {
1069
+ try {
1070
+ var braid_blob = require(\`\${__dirname}/../index.js\`)
1071
+
1072
+ // Use an invalid/unreachable URL to trigger an error
1073
+ var remote_url = new URL('http://localhost:9999/${remote_key}')
1074
+
1075
+ // Start sync without closing it - should trigger retry
1076
+ var sync_options = {}
1077
+ braid_blob.sync('${local_key}', remote_url, sync_options)
1078
+
1079
+ // Wait a bit for the error to occur and retry message to print
1080
+ await new Promise(done => setTimeout(done, 200))
1081
+
1082
+ // Now close it to stop retrying
1083
+ sync_options.my_unsync()
1084
+
1085
+ res.end('sync error occurred')
1086
+ } catch (e) {
1087
+ res.end('error: ' + e.message + ' ' + e.stack)
1088
+ }
1089
+ })()`
1090
+ })
1091
+ var result = await r1.text()
1092
+
1093
+ return result
1094
+ },
1095
+ 'sync error occurred'
1096
+ )
1097
+
1098
+ runTest(
1099
+ "test requesting with version/parents server doesn't have",
1100
+ async () => {
1101
+ var key = 'test-parents-unknown-' + Math.random().toString(36).slice(2)
1102
+
1103
+ // Put with version 100
1104
+ await braid_fetch(`/${key}`, {
1105
+ method: 'PUT',
1106
+ version: ['100'],
1107
+ body: 'content v100'
1108
+ })
1109
+
1110
+ // Try to subscribe with parents 200 (newer than what server has)
1111
+ // This triggers the "unkown version" error which gets caught and returns 309
1112
+ var r = await braid_fetch(`/${key}`, {
1113
+ subscribe: true,
1114
+ parents: ['200']
1115
+ })
1116
+
1117
+ return r.status
1118
+ },
1119
+ '309'
1120
+ )
1121
+
1122
+ runTest(
1123
+ "test requesting specific version server doesn't have",
1124
+ async () => {
1125
+ var key = 'test-version-unknown-' + Math.random().toString(36).slice(2)
1126
+
1127
+ // Put with version 100
1128
+ await braid_fetch(`/${key}`, {
1129
+ method: 'PUT',
1130
+ version: ['100'],
1131
+ body: 'content v100'
1132
+ })
1133
+
1134
+ // Try to GET with version 200 (newer than what server has)
1135
+ // This should trigger line 269 when req.version is checked
1136
+ var r = await braid_fetch(`/${key}`, {
1137
+ version: ['200']
1138
+ })
1139
+
1140
+ return r.status
1141
+ },
1142
+ '309'
1143
+ )
1144
+
691
1145
  }
692
1146
 
693
1147
  // Export for Node.js (CommonJS)