braid-blob 0.0.18 → 0.0.20

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} = 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')
@@ -44,6 +44,29 @@ function create_braid_blob() {
44
44
  }
45
45
 
46
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
+
47
70
  await braid_blob.init()
48
71
 
49
72
  // Read the meta data from meta_db
@@ -92,6 +115,42 @@ function create_braid_blob() {
92
115
  }
93
116
 
94
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
+
95
154
  await braid_blob.init()
96
155
 
97
156
  // Read the meta data from meta_db
@@ -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) {
@@ -177,32 +241,42 @@ function create_braid_blob() {
177
241
  if (meta_content)
178
242
  meta = JSON.parse(meta_content.toString('utf8'))
179
243
 
180
- if (req.method === 'GET') {
244
+ if (req.method === 'GET' || req.method === 'HEAD') {
181
245
  if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
182
246
  if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
183
247
  res.setHeader("Merge-Type", "lww")
184
248
 
185
- var result = await braid_blob.get(options.key, {
186
- peer: req.peer,
187
- head: req.method == "HEAD",
188
- parents: req.parents || null,
189
- header_cb: (result) => {
190
- res.setHeader((req.subscribe ? "Current-" : "") +
191
- "Version", ascii_ify(result.version.map((x) =>
192
- JSON.stringify(x)).join(", ")))
193
- if (result.content_type)
194
- res.setHeader('Content-Type', result.content_type)
195
- },
196
- before_send_cb: (result) =>
197
- res.startSubscription({ onClose: result.unsubscribe }),
198
- subscribe: req.subscribe ? (update) => {
199
- res.sendUpdate({
200
- version: update.version,
201
- 'Merge-Type': 'lww',
202
- body: update.body
203
- })
204
- } : null
205
- })
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
+ }
206
280
 
207
281
  if (!result) {
208
282
  res.statusCode = 404
@@ -240,6 +314,109 @@ function create_braid_blob() {
240
314
  })
241
315
  }
242
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
+
243
420
  function compare_events(a, b) {
244
421
  var a_num = get_event_seq(a)
245
422
  var b_num = get_event_seq(b)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
package/test/tests.js CHANGED
@@ -725,6 +725,423 @@ runTest(
725
725
  '"100"|"200"'
726
726
  )
727
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
+
728
1145
  }
729
1146
 
730
1147
  // Export for Node.js (CommonJS)