braid-blob 0.0.36 → 0.0.38

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/AI-README.md CHANGED
@@ -188,7 +188,7 @@ SUBSCRIPTION_BEHAVIOR:
188
188
  - Subscription callback receives: {body: Buffer, version: [string], content_type: string}
189
189
 
190
190
  ERROR_HANDLING:
191
- - Throws "unkown version: {version}" if requested version > local version
191
+ - Throws "unknown version: {version}" if requested version > local version
192
192
  - Returns 309 status code via serve() when version unknown
193
193
  ```
194
194
 
@@ -426,7 +426,7 @@ url-file-db (^0.0.15):
426
426
  ## ERROR_CONDITIONS
427
427
 
428
428
  ```
429
- "unkown version: {version}"
429
+ "unknown version: {version}"
430
430
  - GET with version/parents newer than local version
431
431
  - Results in 309 status via serve()
432
432
 
package/README.md CHANGED
@@ -88,6 +88,49 @@ Handles HTTP requests for blob storage and synchronization.
88
88
  - `PUT` - Store/update a blob
89
89
  - `DELETE` - Remove a blob
90
90
 
91
+ ### `braid_blob.get(key, options)`
92
+
93
+ Retrieves a blob from local storage or a remote URL.
94
+
95
+ **Parameters:**
96
+ - `key` - Local storage key (string) or remote URL (URL object)
97
+ - `options` - Optional configuration object
98
+ - `version` - Request a specific version
99
+ - `parents` - Version parents for subscription fork-point
100
+ - `subscribe` - Callback function for real-time updates
101
+ - `head` - If true, returns only metadata (version, content_type) without body
102
+ - `content_type` / `accept` - Content type for the request
103
+ - `signal` - AbortSignal for cancellation
104
+
105
+ **Returns:** `{version, body, content_type}` object, or `null` if not found.
106
+
107
+ ### `braid_blob.put(key, body, options)`
108
+
109
+ Stores a blob to local storage or a remote URL.
110
+
111
+ **Parameters:**
112
+ - `key` - Local storage key (string) or remote URL (URL object)
113
+ - `body` - Buffer or data to store
114
+ - `options` - Optional configuration object
115
+ - `version` - Version identifier
116
+ - `content_type` / `accept` - Content type of the blob
117
+ - `signal` - AbortSignal for cancellation
118
+
119
+ ### `braid_blob.sync(a, b, options)`
120
+
121
+ Bidirectionally synchronizes blobs between two endpoints (local keys or URLs).
122
+
123
+ **Parameters:**
124
+ - `a` - First endpoint (local key or URL)
125
+ - `b` - Second endpoint (local key or URL)
126
+ - `options` - Optional configuration object
127
+ - `signal` - AbortSignal for cancellation (use to stop sync)
128
+ - `content_type` / `accept` - Content type for requests
129
+ - `on_pre_connect` - Async callback before connection attempt
130
+ - `on_disconnect` - Callback when connection drops
131
+ - `on_unauthorized` - Callback on 401/403 responses
132
+ - `on_res` - Callback receiving the response object
133
+
91
134
  ## Testing
92
135
 
93
136
  ### to run unit tests:
package/index.js CHANGED
@@ -92,20 +92,23 @@ function create_braid_blob() {
92
92
  }
93
93
 
94
94
  braid_blob.put = async (key, body, options = {}) => {
95
+ options = normalize_options(options)
96
+
95
97
  // Handle URL case - make a remote PUT request
96
98
  if (key instanceof URL) {
97
99
 
98
100
  var params = {
99
101
  method: 'PUT',
100
102
  signal: options.signal,
101
- retry: () => true,
102
103
  body: body
103
104
  }
105
+ if (!options.dont_retry)
106
+ params.retry = () => true
104
107
  for (var x of ['headers', 'version', 'peer'])
105
108
  if (options[x] != null) params[x] = options[x]
106
- if (options.content_type) {
107
- params.headers = { ...params.headers, 'Content-Type': options.content_type }
108
- }
109
+ if (options.content_type)
110
+ params.headers = { ...params.headers,
111
+ 'Content-Type': options.content_type }
109
112
 
110
113
  return await braid_fetch(key.href, params)
111
114
  }
@@ -146,7 +149,7 @@ function create_braid_blob() {
146
149
  // (except the peer which made the PUT request itself)
147
150
  if (braid_blob.key_to_subs[key])
148
151
  for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
149
- if (peer !== options.peer)
152
+ if (!options.peer || options.peer !== peer)
150
153
  sub.sendUpdate({
151
154
  version: [meta.event],
152
155
  'Merge-Type': 'aww',
@@ -158,6 +161,8 @@ function create_braid_blob() {
158
161
  }
159
162
 
160
163
  braid_blob.get = async (key, options = {}) => {
164
+ options = normalize_options(options)
165
+
161
166
  // Handle URL case - make a remote GET request
162
167
  if (key instanceof URL) {
163
168
  var params = {
@@ -166,14 +171,24 @@ function create_braid_blob() {
166
171
  heartbeats: 120,
167
172
  }
168
173
  if (!options.dont_retry) {
169
- params.retry = (res) => res.status !== 404
174
+ params.retry = (res) => res.status !== 309 &&
175
+ res.status !== 404 && res.status !== 406
170
176
  }
177
+ if (options.head) params.method = 'HEAD'
171
178
  for (var x of ['headers', 'parents', 'version', 'peer'])
172
179
  if (options[x] != null) params[x] = options[x]
180
+ if (options.content_type)
181
+ params.headers = { ...params.headers,
182
+ 'Accept': options.content_type }
173
183
 
174
184
  var res = await braid_fetch(key.href, params)
175
185
 
176
- if (res.status === 404) return null
186
+ if (!res.ok) return null
187
+
188
+ var result = {}
189
+ if (res.version) result.version = res.version
190
+
191
+ if (options.head) return result
177
192
 
178
193
  if (options.subscribe) {
179
194
  res.subscribe(async update => {
@@ -181,7 +196,8 @@ function create_braid_blob() {
181
196
  }, e => options.on_error?.(e))
182
197
  return res
183
198
  } else {
184
- return await res.arrayBuffer()
199
+ result.body = await res.arrayBuffer()
200
+ return result
185
201
  }
186
202
  }
187
203
 
@@ -192,15 +208,15 @@ function create_braid_blob() {
192
208
 
193
209
  var result = {
194
210
  version: [meta.event],
195
- content_type: meta.content_type
211
+ content_type: meta.content_type || options.content_type
196
212
  }
197
213
  if (options.header_cb) await options.header_cb(result)
198
214
  if (options.signal?.aborted) return
199
215
  // Check if requested version/parents is newer than what we have - if so, we don't have it
200
216
  if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
201
- throw new Error('unkown version: ' + options.version)
217
+ throw new Error('unknown version: ' + options.version)
202
218
  if (options.parents && options.parents.length && compare_events(options.parents[0], meta.event) > 0)
203
- throw new Error('unkown version: ' + options.parents)
219
+ throw new Error('unknown version: ' + options.parents)
204
220
  if (options.head) return result
205
221
 
206
222
  if (options.subscribe) {
@@ -219,7 +235,7 @@ function create_braid_blob() {
219
235
  options.my_subscribe({
220
236
  body: update.body,
221
237
  version: update.version,
222
- content_type: meta.content_type
238
+ content_type: meta.content_type || options.content_type
223
239
  })
224
240
  }
225
241
  })
@@ -253,6 +269,8 @@ function create_braid_blob() {
253
269
  }
254
270
 
255
271
  braid_blob.delete = async (key, options = {}) => {
272
+ options = normalize_options(options)
273
+
256
274
  // Handle URL case - make a remote DELETE request
257
275
  if (key instanceof URL) {
258
276
 
@@ -326,7 +344,7 @@ function create_braid_blob() {
326
344
  } : null
327
345
  })
328
346
  } catch (e) {
329
- if (e.message && e.message.startsWith('unkown version')) {
347
+ if (e.message && e.message.startsWith('unknown version')) {
330
348
  // Server doesn't have this version
331
349
  res.statusCode = 309
332
350
  res.statusMessage = 'Version Unknown Here'
@@ -370,6 +388,9 @@ function create_braid_blob() {
370
388
  }
371
389
 
372
390
  braid_blob.sync = (a, b, options = {}) => {
391
+ options = normalize_options(options)
392
+ if (!options.peer) options.peer = Math.random().toString(36).slice(2)
393
+
373
394
  if ((a instanceof URL) === (b instanceof URL)) {
374
395
  // Both are URLs or both are local keys
375
396
  var a_first_put, b_first_put
@@ -378,11 +399,16 @@ function create_braid_blob() {
378
399
 
379
400
  var a_ops = {
380
401
  signal: options.signal,
402
+ headers: options.headers,
403
+ content_type: options.content_type,
404
+ peer: options.peer,
381
405
  subscribe: update => {
382
406
  braid_blob.put(b, update.body, {
383
407
  signal: options.signal,
384
408
  version: update.version,
385
- content_type: update.headers?.['content-type']
409
+ headers: options.headers,
410
+ content_type: update.content_type,
411
+ peer: options.peer,
386
412
  }).then(a_first_put)
387
413
  }
388
414
  }
@@ -392,11 +418,16 @@ function create_braid_blob() {
392
418
 
393
419
  var b_ops = {
394
420
  signal: options.signal,
421
+ headers: options.headers,
422
+ content_type: options.content_type,
423
+ peer: options.peer,
395
424
  subscribe: update => {
396
425
  braid_blob.put(a, update.body, {
397
426
  signal: options.signal,
398
427
  version: update.version,
399
- content_type: update.headers?.['content-type']
428
+ headers: options.headers,
429
+ content_type: update.content_type,
430
+ peer: options.peer,
400
431
  }).then(b_first_put)
401
432
  }
402
433
  }
@@ -426,6 +457,8 @@ function create_braid_blob() {
426
457
  }
427
458
 
428
459
  async function connect() {
460
+ if (options.on_pre_connect) await options.on_pre_connect()
461
+
429
462
  var ac = new AbortController()
430
463
  disconnect = () => ac.abort()
431
464
 
@@ -433,34 +466,50 @@ function create_braid_blob() {
433
466
  // Check if remote has our current version (simple fork-point check)
434
467
  var local_result = await braid_blob.get(a, {
435
468
  signal: ac.signal,
436
- head: true
469
+ head: true,
470
+ headers: options.headers,
471
+ content_type: options.content_type,
472
+ peer: options.peer,
437
473
  })
438
474
  var local_version = local_result ? local_result.version : null
439
475
  var server_has_our_version = false
440
476
 
441
477
  if (local_version) {
442
- // Check if server has our version
443
- var r = await braid_fetch(b.href, {
478
+ var r = await braid_blob.get(b, {
444
479
  signal: ac.signal,
445
- method: "HEAD",
446
- version: local_version
480
+ head: true,
481
+ dont_retry: true,
482
+ version: local_version,
483
+ headers: options.headers,
484
+ content_type: options.content_type,
485
+ peer: options.peer,
447
486
  })
448
- server_has_our_version = r.ok
487
+ server_has_our_version = !!r
449
488
  }
450
489
 
451
490
  // Local -> remote: subscribe to future local changes
452
491
  var a_ops = {
453
492
  signal: ac.signal,
454
- subscribe: update => {
455
- braid_blob.put(b, update.body, {
456
- signal: ac.signal,
457
- version: update.version,
458
- content_type: update.content_type
459
- }).then(local_first_put).catch(e => {
460
- if (e.name === 'AbortError') {
461
- // ignore
462
- } else throw e
463
- })
493
+ headers: options.headers,
494
+ content_type: options.content_type,
495
+ peer: options.peer,
496
+ subscribe: async update => {
497
+ try {
498
+ var x = await braid_blob.put(b, update.body, {
499
+ signal: ac.signal,
500
+ dont_retry: true,
501
+ version: update.version,
502
+ headers: options.headers,
503
+ content_type: update.content_type,
504
+ peer: options.peer,
505
+ })
506
+ if (x.ok) local_first_put()
507
+ else if (x.status === 401 || x.status === 403) {
508
+ await options.on_unauthorized?.()
509
+ } else throw new Error('failed to PUT: ' + x.status)
510
+ } catch (e) {
511
+ if (e.name !== 'AbortError') throw e
512
+ }
464
513
  }
465
514
  }
466
515
  // Only set parents if server already has our version
@@ -473,14 +522,22 @@ function create_braid_blob() {
473
522
  var b_ops = {
474
523
  signal: ac.signal,
475
524
  dont_retry: true,
525
+ headers: options.headers,
526
+ content_type: options.content_type,
527
+ peer: options.peer,
476
528
  subscribe: async update => {
477
529
  await braid_blob.put(a, update.body, {
478
530
  version: update.version,
479
- content_type: update.headers?.['content-type']
531
+ headers: options.headers,
532
+ content_type: update.content_type,
533
+ peer: options.peer,
480
534
  })
481
535
  remote_first_put()
482
536
  },
483
- on_error: handle_error
537
+ on_error: e => {
538
+ options.on_disconnect?.()
539
+ handle_error(e)
540
+ }
484
541
  }
485
542
  // Use fork-point (parents) to avoid receiving data we already have
486
543
  if (local_version) {
@@ -500,6 +557,9 @@ function create_braid_blob() {
500
557
  disconnect()
501
558
  connect()
502
559
  }
560
+
561
+ options.on_res?.(remote_res)
562
+
503
563
  // Otherwise, on_error will call handle_error when connection drops
504
564
  } catch (e) {
505
565
  handle_error(e)
@@ -617,6 +677,59 @@ function create_braid_blob() {
617
677
  }
618
678
  }
619
679
 
680
+ function get_header(headers, key) {
681
+ if (!headers) return
682
+
683
+ // optimization..
684
+ if (headers.hasOwnProperty(key))
685
+ return headers[key]
686
+
687
+ var lowerKey = key.toLowerCase()
688
+ for (var headerKey of Object.keys(headers))
689
+ if (headerKey.toLowerCase() === lowerKey)
690
+ return headers[headerKey]
691
+ }
692
+
693
+ function normalize_options(options = {}) {
694
+ if (!normalize_options.special) {
695
+ normalize_options.special = {
696
+ version: 'version',
697
+ parents: 'parents',
698
+ 'content-type': 'content_type',
699
+ accept: 'content_type',
700
+ peer: 'peer',
701
+ }
702
+ }
703
+
704
+ var normalized = {}
705
+ Object.assign(normalized, options)
706
+
707
+ // Normalize top-level accept to content_type
708
+ if (options.accept) {
709
+ normalized.content_type = options.accept
710
+ delete normalized.accept
711
+ }
712
+
713
+ if (options.headers) {
714
+ normalized.headers = {}
715
+ for (var [k, v] of (options.headers instanceof Headers ?
716
+ options.headers.entries() :
717
+ Object.entries(options.headers))) {
718
+ var s = normalize_options.special[k]
719
+ if (s) normalized[s] = v
720
+ else normalized.headers[k] = v
721
+ }
722
+ }
723
+
724
+ // Normalize version/parents strings to arrays
725
+ if (typeof normalized.version === 'string')
726
+ normalized.version = JSON.parse('[' + normalized.version + ']')
727
+ if (typeof normalized.parents === 'string')
728
+ normalized.parents = JSON.parse('[' + normalized.parents + ']')
729
+
730
+ return normalized
731
+ }
732
+
620
733
  braid_blob.create_braid_blob = create_braid_blob
621
734
 
622
735
  return braid_blob
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
@@ -10,6 +10,6 @@
10
10
  "test:browser": "node test/test.js --browser"
11
11
  },
12
12
  "dependencies": {
13
- "braid-http": "~1.3.83"
13
+ "braid-http": "~1.3.84"
14
14
  }
15
15
  }
package/test/tests.js CHANGED
@@ -918,7 +918,7 @@ runTest(
918
918
  var braid_blob = require(\`\${__dirname}/../index.js\`)
919
919
  var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
920
920
  var result = await braid_blob.get(url)
921
- res.end(Buffer.from(result).toString('utf8'))
921
+ res.end(Buffer.from(result.body).toString('utf8'))
922
922
  })()`
923
923
  })
924
924
 
@@ -1147,58 +1147,6 @@ runTest(
1147
1147
  'shared content'
1148
1148
  )
1149
1149
 
1150
- runTest(
1151
- "test sync does not disconnect unnecessarily",
1152
- async () => {
1153
- var local_key = 'test-sync-no-disconnect-local-' + Math.random().toString(36).slice(2)
1154
- var remote_key = 'test-sync-no-disconnect-remote-' + Math.random().toString(36).slice(2)
1155
-
1156
- var r1 = await braid_fetch(`/eval`, {
1157
- method: 'POST',
1158
- body: `void (async () => {
1159
- try {
1160
- var braid_blob = require(\`\${__dirname}/../index.js\`)
1161
-
1162
- // Put something locally first
1163
- await braid_blob.put('${local_key}', Buffer.from('local content'), { version: ['600'] })
1164
-
1165
- var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
1166
-
1167
- // Capture console.log to count disconnects
1168
- var disconnect_count = 0
1169
- var original_log = console.log
1170
- console.log = function(...args) {
1171
- if (args[0]?.includes?.('disconnected')) disconnect_count++
1172
- original_log.apply(console, args)
1173
- }
1174
-
1175
- // Create an AbortController to stop the sync
1176
- var ac = new AbortController()
1177
-
1178
- // Start sync
1179
- braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
1180
-
1181
- // Wait for sync to establish and stabilize
1182
- await new Promise(done => setTimeout(done, 500))
1183
-
1184
- // Stop sync
1185
- ac.abort()
1186
-
1187
- // Restore console.log
1188
- console.log = original_log
1189
-
1190
- // Should have zero disconnects during normal operation
1191
- res.end(disconnect_count === 0 ? 'no disconnects' : 'disconnects: ' + disconnect_count)
1192
- } catch (e) {
1193
- res.end('error: ' + e.message + ' ' + e.stack)
1194
- }
1195
- })()`
1196
- })
1197
- return await r1.text()
1198
- },
1199
- 'no disconnects'
1200
- )
1201
-
1202
1150
  runTest(
1203
1151
  "test sync connect does not read file body for version check",
1204
1152
  async () => {
@@ -1355,7 +1303,7 @@ runTest(
1355
1303
  })
1356
1304
 
1357
1305
  // Try to subscribe with parents 200 (newer than what server has)
1358
- // This triggers the "unkown version" error which gets caught and returns 309
1306
+ // This triggers the "unknown version" error which gets caught and returns 309
1359
1307
  var r = await braid_fetch(`/${key}`, {
1360
1308
  subscribe: true,
1361
1309
  parents: ['200']