braid-blob 0.0.35 → 0.0.37

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,24 @@ function create_braid_blob() {
92
92
  }
93
93
 
94
94
  braid_blob.put = async (key, body, options = {}) => {
95
+ // What's the content type?
96
+ var content_type = options.content_type || options.accept || get_header(options.headers, 'content-type') || get_header(options.headers, 'accept')
97
+
95
98
  // Handle URL case - make a remote PUT request
96
99
  if (key instanceof URL) {
97
100
 
98
101
  var params = {
99
102
  method: 'PUT',
100
103
  signal: options.signal,
101
- retry: () => true,
102
104
  body: body
103
105
  }
106
+ if (!options.dont_retry)
107
+ params.retry = () => true
104
108
  for (var x of ['headers', 'version', 'peer'])
105
109
  if (options[x] != null) params[x] = options[x]
106
- if (options.content_type) {
107
- params.headers = { ...params.headers, 'Content-Type': options.content_type }
108
- }
110
+ if (content_type)
111
+ params.headers = { ...params.headers,
112
+ 'Content-Type': content_type }
109
113
 
110
114
  return await braid_fetch(key.href, params)
111
115
  }
@@ -136,8 +140,8 @@ function create_braid_blob() {
136
140
 
137
141
  // Update only the fields we want to change in metadata
138
142
  var meta_updates = { event: their_e }
139
- if (options.content_type)
140
- meta_updates.content_type = options.content_type
143
+ if (content_type)
144
+ meta_updates.content_type = content_type
141
145
 
142
146
  await update_meta(key, meta_updates)
143
147
  if (options.signal?.aborted) return
@@ -158,6 +162,9 @@ function create_braid_blob() {
158
162
  }
159
163
 
160
164
  braid_blob.get = async (key, options = {}) => {
165
+ // What's the content type?
166
+ var content_type = options.content_type || options.accept || get_header(options.headers, 'content-type') || get_header(options.headers, 'accept')
167
+
161
168
  // Handle URL case - make a remote GET request
162
169
  if (key instanceof URL) {
163
170
  var params = {
@@ -166,14 +173,24 @@ function create_braid_blob() {
166
173
  heartbeats: 120,
167
174
  }
168
175
  if (!options.dont_retry) {
169
- params.retry = (res) => res.status !== 404
176
+ params.retry = (res) => res.status !== 309 &&
177
+ res.status !== 404 && res.status !== 406
170
178
  }
179
+ if (options.head) params.method = 'HEAD'
171
180
  for (var x of ['headers', 'parents', 'version', 'peer'])
172
181
  if (options[x] != null) params[x] = options[x]
182
+ if (content_type)
183
+ params.headers = { ...params.headers,
184
+ 'Accept': content_type }
173
185
 
174
186
  var res = await braid_fetch(key.href, params)
175
187
 
176
- if (res.status === 404) return null
188
+ if (!res.ok) return null
189
+
190
+ var result = {}
191
+ if (res.version) result.version = res.version
192
+
193
+ if (options.head) return result
177
194
 
178
195
  if (options.subscribe) {
179
196
  res.subscribe(async update => {
@@ -181,7 +198,8 @@ function create_braid_blob() {
181
198
  }, e => options.on_error?.(e))
182
199
  return res
183
200
  } else {
184
- return await res.arrayBuffer()
201
+ result.body = await res.arrayBuffer()
202
+ return result
185
203
  }
186
204
  }
187
205
 
@@ -192,16 +210,16 @@ function create_braid_blob() {
192
210
 
193
211
  var result = {
194
212
  version: [meta.event],
195
- content_type: meta.content_type
213
+ content_type: meta.content_type || content_type
196
214
  }
197
215
  if (options.header_cb) await options.header_cb(result)
198
216
  if (options.signal?.aborted) return
199
217
  // Check if requested version/parents is newer than what we have - if so, we don't have it
200
218
  if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
201
- throw new Error('unkown version: ' + options.version)
219
+ throw new Error('unknown version: ' + options.version)
202
220
  if (options.parents && options.parents.length && compare_events(options.parents[0], meta.event) > 0)
203
- throw new Error('unkown version: ' + options.parents)
204
- if (options.head) return
221
+ throw new Error('unknown version: ' + options.parents)
222
+ if (options.head) return result
205
223
 
206
224
  if (options.subscribe) {
207
225
  var subscribe_chain = Promise.resolve()
@@ -219,7 +237,7 @@ function create_braid_blob() {
219
237
  options.my_subscribe({
220
238
  body: update.body,
221
239
  version: update.version,
222
- content_type: meta.content_type
240
+ content_type: meta.content_type || content_type
223
241
  })
224
242
  }
225
243
  })
@@ -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
+ // What's the content type?
392
+ var content_type = options.content_type || options.accept || get_header(options.headers, 'content-type') || get_header(options.headers, 'accept')
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,14 @@ function create_braid_blob() {
378
399
 
379
400
  var a_ops = {
380
401
  signal: options.signal,
402
+ headers: options.headers,
403
+ content_type,
381
404
  subscribe: update => {
382
405
  braid_blob.put(b, update.body, {
383
406
  signal: options.signal,
384
407
  version: update.version,
385
- content_type: update.headers?.['content-type']
408
+ headers: options.headers,
409
+ content_type: update.content_type
386
410
  }).then(a_first_put)
387
411
  }
388
412
  }
@@ -392,11 +416,14 @@ function create_braid_blob() {
392
416
 
393
417
  var b_ops = {
394
418
  signal: options.signal,
419
+ headers: options.headers,
420
+ content_type,
395
421
  subscribe: update => {
396
422
  braid_blob.put(a, update.body, {
397
423
  signal: options.signal,
398
424
  version: update.version,
399
- content_type: update.headers?.['content-type']
425
+ headers: options.headers,
426
+ content_type: update.content_type
400
427
  }).then(b_first_put)
401
428
  }
402
429
  }
@@ -426,40 +453,55 @@ function create_braid_blob() {
426
453
  }
427
454
 
428
455
  async function connect() {
456
+ if (options.on_pre_connect) await options.on_pre_connect()
457
+
429
458
  var ac = new AbortController()
430
459
  disconnect = () => ac.abort()
431
460
 
432
461
  try {
433
462
  // Check if remote has our current version (simple fork-point check)
434
463
  var local_result = await braid_blob.get(a, {
435
- signal: ac.signal
464
+ signal: ac.signal,
465
+ head: true,
466
+ headers: options.headers,
467
+ content_type
436
468
  })
437
469
  var local_version = local_result ? local_result.version : null
438
470
  var server_has_our_version = false
439
471
 
440
472
  if (local_version) {
441
- // Check if server has our version
442
- var r = await braid_fetch(b.href, {
473
+ var r = await braid_blob.get(b, {
443
474
  signal: ac.signal,
444
- method: "HEAD",
445
- version: local_version
475
+ head: true,
476
+ dont_retry: true,
477
+ version: local_version,
478
+ headers: options.headers,
479
+ content_type
446
480
  })
447
- server_has_our_version = r.ok
481
+ server_has_our_version = !!r
448
482
  }
449
483
 
450
484
  // Local -> remote: subscribe to future local changes
451
485
  var a_ops = {
452
486
  signal: ac.signal,
453
- subscribe: update => {
454
- braid_blob.put(b, update.body, {
455
- signal: ac.signal,
456
- version: update.version,
457
- content_type: update.content_type
458
- }).then(local_first_put).catch(e => {
459
- if (e.name === 'AbortError') {
460
- // ignore
461
- } else throw e
462
- })
487
+ headers: options.headers,
488
+ content_type,
489
+ subscribe: async update => {
490
+ try {
491
+ var x = await braid_blob.put(b, update.body, {
492
+ signal: ac.signal,
493
+ dont_retry: true,
494
+ version: update.version,
495
+ headers: options.headers,
496
+ content_type: update.content_type
497
+ })
498
+ if (x.ok) local_first_put()
499
+ else if (x.status === 401 || x.status === 403) {
500
+ await options.on_unauthorized?.()
501
+ } else throw new Error('failed to PUT: ' + x.status)
502
+ } catch (e) {
503
+ if (e.name !== 'AbortError') throw e
504
+ }
463
505
  }
464
506
  }
465
507
  // Only set parents if server already has our version
@@ -472,14 +514,20 @@ function create_braid_blob() {
472
514
  var b_ops = {
473
515
  signal: ac.signal,
474
516
  dont_retry: true,
517
+ headers: options.headers,
518
+ content_type,
475
519
  subscribe: async update => {
476
520
  await braid_blob.put(a, update.body, {
477
521
  version: update.version,
478
- content_type: update.headers?.['content-type']
522
+ headers: options.headers,
523
+ content_type: update.content_type
479
524
  })
480
525
  remote_first_put()
481
526
  },
482
- on_error: handle_error
527
+ on_error: e => {
528
+ options.on_disconnect?.()
529
+ handle_error(e)
530
+ }
483
531
  }
484
532
  // Use fork-point (parents) to avoid receiving data we already have
485
533
  if (local_version) {
@@ -499,6 +547,9 @@ function create_braid_blob() {
499
547
  disconnect()
500
548
  connect()
501
549
  }
550
+
551
+ options.on_res?.(remote_res)
552
+
502
553
  // Otherwise, on_error will call handle_error when connection drops
503
554
  } catch (e) {
504
555
  handle_error(e)
@@ -616,6 +667,19 @@ function create_braid_blob() {
616
667
  }
617
668
  }
618
669
 
670
+ function get_header(headers, key) {
671
+ if (!headers) return
672
+
673
+ // optimization..
674
+ if (headers.hasOwnProperty(key))
675
+ return headers[key]
676
+
677
+ var lowerKey = key.toLowerCase()
678
+ for (var headerKey of Object.keys(headers))
679
+ if (headerKey.toLowerCase() === lowerKey)
680
+ return headers[headerKey]
681
+ }
682
+
619
683
  braid_blob.create_braid_blob = create_braid_blob
620
684
 
621
685
  return braid_blob
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
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
 
@@ -1148,10 +1148,18 @@ runTest(
1148
1148
  )
1149
1149
 
1150
1150
  runTest(
1151
- "test sync does not disconnect unnecessarily",
1151
+ "test sync connect does not read file body for version check",
1152
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)
1153
+ var local_key = '/test-sync-no-read-' + Math.random().toString(36).slice(2)
1154
+ var remote_key = 'test-sync-no-read-remote-' + Math.random().toString(36).slice(2)
1155
+
1156
+ // Put something on remote with SAME version as local, so no data needs to flow
1157
+ var put_result = await braid_fetch(`/${remote_key}`, {
1158
+ method: 'PUT',
1159
+ version: ['same-version-123'],
1160
+ body: 'same content'
1161
+ })
1162
+ if (!put_result.ok) return 'PUT status: ' + put_result.status
1155
1163
 
1156
1164
  var r1 = await braid_fetch(`/eval`, {
1157
1165
  method: 'POST',
@@ -1159,36 +1167,38 @@ runTest(
1159
1167
  try {
1160
1168
  var braid_blob = require(\`\${__dirname}/../index.js\`)
1161
1169
 
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}')
1170
+ // Put locally with SAME version - so when sync connects, no updates need to flow
1171
+ await braid_blob.put('${local_key}', Buffer.from('same content'), { version: ['same-version-123'] })
1166
1172
 
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
+ // Wrap db.read to count calls for our specific key
1174
+ var read_count = 0
1175
+ var original_read = braid_blob.db.read
1176
+ braid_blob.db.read = async function(key) {
1177
+ if (key === '${local_key}') read_count++
1178
+ return original_read.call(this, key)
1173
1179
  }
1174
1180
 
1181
+ var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
1182
+
1175
1183
  // Create an AbortController to stop the sync
1176
1184
  var ac = new AbortController()
1177
1185
 
1178
- // Start sync
1186
+ // Start sync - since both have same version, no updates should flow
1179
1187
  braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
1180
1188
 
1181
- // Wait for sync to establish and stabilize
1182
- await new Promise(done => setTimeout(done, 500))
1189
+ // Wait for sync to establish connection
1190
+ await new Promise(done => setTimeout(done, 300))
1183
1191
 
1184
1192
  // Stop sync
1185
1193
  ac.abort()
1186
1194
 
1187
- // Restore console.log
1188
- console.log = original_log
1195
+ // Restore original read
1196
+ braid_blob.db.read = original_read
1189
1197
 
1190
- // Should have zero disconnects during normal operation
1191
- res.end(disconnect_count === 0 ? 'no disconnects' : 'disconnects: ' + disconnect_count)
1198
+ // db.read should not have been called since:
1199
+ // 1. Initial version check uses head:true (no body read)
1200
+ // 2. Both have same version so no updates flow
1201
+ res.end(read_count === 0 ? 'no reads' : 'reads: ' + read_count)
1192
1202
  } catch (e) {
1193
1203
  res.end('error: ' + e.message + ' ' + e.stack)
1194
1204
  }
@@ -1196,7 +1206,7 @@ runTest(
1196
1206
  })
1197
1207
  return await r1.text()
1198
1208
  },
1199
- 'no disconnects'
1209
+ 'no reads'
1200
1210
  )
1201
1211
 
1202
1212
  runTest(
@@ -1293,7 +1303,7 @@ runTest(
1293
1303
  })
1294
1304
 
1295
1305
  // Try to subscribe with parents 200 (newer than what server has)
1296
- // 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
1297
1307
  var r = await braid_fetch(`/${key}`, {
1298
1308
  subscribe: true,
1299
1309
  parents: ['200']