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 +2 -2
- package/README.md +43 -0
- package/index.js +99 -35
- package/package.json +2 -2
- package/test/tests.js +33 -23
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 "
|
|
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
|
-
"
|
|
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 (
|
|
107
|
-
params.headers = { ...params.headers,
|
|
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 (
|
|
140
|
-
meta_updates.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 !==
|
|
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.
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
var r = await braid_fetch(b.href, {
|
|
473
|
+
var r = await braid_blob.get(b, {
|
|
443
474
|
signal: ac.signal,
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
522
|
+
headers: options.headers,
|
|
523
|
+
content_type: update.content_type
|
|
479
524
|
})
|
|
480
525
|
remote_first_put()
|
|
481
526
|
},
|
|
482
|
-
on_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.
|
|
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.
|
|
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
|
|
1151
|
+
"test sync connect does not read file body for version check",
|
|
1152
1152
|
async () => {
|
|
1153
|
-
var local_key = 'test-sync-no-
|
|
1154
|
-
var remote_key = 'test-sync-no-
|
|
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
|
|
1163
|
-
await braid_blob.put('${local_key}', Buffer.from('
|
|
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
|
-
//
|
|
1168
|
-
var
|
|
1169
|
-
var
|
|
1170
|
-
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
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
|
|
1182
|
-
await new Promise(done => setTimeout(done,
|
|
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
|
|
1188
|
-
|
|
1195
|
+
// Restore original read
|
|
1196
|
+
braid_blob.db.read = original_read
|
|
1189
1197
|
|
|
1190
|
-
//
|
|
1191
|
-
|
|
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
|
|
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 "
|
|
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']
|