braid-blob 0.0.24 → 0.0.26
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/index.js +100 -62
- package/package.json +2 -2
- package/test/tests.js +366 -8
package/index.js
CHANGED
|
@@ -40,15 +40,10 @@ function create_braid_blob() {
|
|
|
40
40
|
braid_blob.put = async (key, body, options = {}) => {
|
|
41
41
|
// Handle URL case - make a remote PUT request
|
|
42
42
|
if (key instanceof URL) {
|
|
43
|
-
options.my_abort = new AbortController()
|
|
44
|
-
if (options.signal) {
|
|
45
|
-
options.signal.addEventListener('abort', () =>
|
|
46
|
-
options.my_abort.abort())
|
|
47
|
-
}
|
|
48
43
|
|
|
49
44
|
var params = {
|
|
50
45
|
method: 'PUT',
|
|
51
|
-
signal: options.
|
|
46
|
+
signal: options.signal,
|
|
52
47
|
retry: () => true,
|
|
53
48
|
body: body
|
|
54
49
|
}
|
|
@@ -62,6 +57,7 @@ function create_braid_blob() {
|
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
await braid_blob.init()
|
|
60
|
+
if (options.signal?.aborted) return
|
|
65
61
|
|
|
66
62
|
// Read the meta data using new meta API
|
|
67
63
|
var meta = braid_blob.db.get_meta(key) || {}
|
|
@@ -83,6 +79,7 @@ function create_braid_blob() {
|
|
|
83
79
|
// Write the file using url-file-db (unless skip_write is set)
|
|
84
80
|
if (!options.skip_write)
|
|
85
81
|
await braid_blob.db.write(key, body)
|
|
82
|
+
if (options.signal?.aborted) return
|
|
86
83
|
|
|
87
84
|
// Update only the fields we want to change in metadata
|
|
88
85
|
var meta_updates = { event: their_e }
|
|
@@ -90,6 +87,7 @@ function create_braid_blob() {
|
|
|
90
87
|
meta_updates.content_type = options.content_type
|
|
91
88
|
|
|
92
89
|
await braid_blob.db.update_meta(key, meta_updates)
|
|
90
|
+
if (options.signal?.aborted) return
|
|
93
91
|
|
|
94
92
|
// Notify all subscriptions of the update
|
|
95
93
|
// (except the peer which made the PUT request itself)
|
|
@@ -109,34 +107,25 @@ function create_braid_blob() {
|
|
|
109
107
|
braid_blob.get = async (key, options = {}) => {
|
|
110
108
|
// Handle URL case - make a remote GET request
|
|
111
109
|
if (key instanceof URL) {
|
|
112
|
-
options.my_abort = new AbortController()
|
|
113
|
-
|
|
114
110
|
var params = {
|
|
115
|
-
signal: options.
|
|
111
|
+
signal: options.signal,
|
|
116
112
|
subscribe: !!options.subscribe,
|
|
117
113
|
heartbeats: 120,
|
|
118
114
|
}
|
|
119
115
|
if (!options.dont_retry) {
|
|
120
|
-
params.retry = () =>
|
|
116
|
+
params.retry = (res) => res.status !== 404
|
|
121
117
|
}
|
|
122
118
|
for (var x of ['headers', 'parents', 'version', 'peer'])
|
|
123
119
|
if (options[x] != null) params[x] = options[x]
|
|
124
120
|
|
|
125
121
|
var res = await braid_fetch(key.href, params)
|
|
126
122
|
|
|
127
|
-
if (
|
|
128
|
-
if (options.dont_retry) {
|
|
129
|
-
var error_happened
|
|
130
|
-
var error_promise = new Promise((_, fail) => error_happened = fail)
|
|
131
|
-
}
|
|
123
|
+
if (res.status === 404) return null
|
|
132
124
|
|
|
125
|
+
if (options.subscribe) {
|
|
133
126
|
res.subscribe(async update => {
|
|
134
127
|
await options.subscribe(update)
|
|
135
|
-
}, e => options.
|
|
136
|
-
|
|
137
|
-
if (options.dont_retry) {
|
|
138
|
-
return await error_promise
|
|
139
|
-
}
|
|
128
|
+
}, e => options.on_error?.(e))
|
|
140
129
|
return res
|
|
141
130
|
} else {
|
|
142
131
|
return await res.arrayBuffer()
|
|
@@ -154,6 +143,7 @@ function create_braid_blob() {
|
|
|
154
143
|
content_type: meta.content_type
|
|
155
144
|
}
|
|
156
145
|
if (options.header_cb) await options.header_cb(result)
|
|
146
|
+
if (options.signal?.aborted) return
|
|
157
147
|
// Check if requested version/parents is newer than what we have - if so, we don't have it
|
|
158
148
|
if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
|
|
159
149
|
throw new Error('unkown version: ' + options.version)
|
|
@@ -164,7 +154,8 @@ function create_braid_blob() {
|
|
|
164
154
|
if (options.subscribe) {
|
|
165
155
|
var subscribe_chain = Promise.resolve()
|
|
166
156
|
options.my_subscribe = (x) => subscribe_chain =
|
|
167
|
-
subscribe_chain.then(() =>
|
|
157
|
+
subscribe_chain.then(() =>
|
|
158
|
+
!options.signal?.aborted && options.subscribe(x))
|
|
168
159
|
|
|
169
160
|
// Start a subscription for future updates
|
|
170
161
|
if (!braid_blob.key_to_subs[key])
|
|
@@ -181,14 +172,14 @@ function create_braid_blob() {
|
|
|
181
172
|
}
|
|
182
173
|
})
|
|
183
174
|
|
|
184
|
-
|
|
185
|
-
result.unsubscribe = () => {
|
|
175
|
+
options.signal?.addEventListener('abort', () => {
|
|
186
176
|
braid_blob.key_to_subs[key].delete(peer)
|
|
187
177
|
if (!braid_blob.key_to_subs[key].size)
|
|
188
178
|
delete braid_blob.key_to_subs[key]
|
|
189
|
-
}
|
|
179
|
+
})
|
|
190
180
|
|
|
191
181
|
if (options.before_send_cb) await options.before_send_cb(result)
|
|
182
|
+
if (options.signal?.aborted) return
|
|
192
183
|
|
|
193
184
|
// Send an immediate update if needed
|
|
194
185
|
if (!options.parents ||
|
|
@@ -212,15 +203,10 @@ function create_braid_blob() {
|
|
|
212
203
|
braid_blob.delete = async (key, options = {}) => {
|
|
213
204
|
// Handle URL case - make a remote DELETE request
|
|
214
205
|
if (key instanceof URL) {
|
|
215
|
-
options.my_abort = new AbortController()
|
|
216
|
-
if (options.signal) {
|
|
217
|
-
options.signal.addEventListener('abort', () =>
|
|
218
|
-
options.my_abort.abort())
|
|
219
|
-
}
|
|
220
206
|
|
|
221
207
|
var params = {
|
|
222
208
|
method: 'DELETE',
|
|
223
|
-
signal: options.
|
|
209
|
+
signal: options.signal
|
|
224
210
|
}
|
|
225
211
|
for (var x of ['headers', 'peer'])
|
|
226
212
|
if (options[x] != null) params[x] = options[x]
|
|
@@ -229,6 +215,7 @@ function create_braid_blob() {
|
|
|
229
215
|
}
|
|
230
216
|
|
|
231
217
|
await braid_blob.init()
|
|
218
|
+
if (options.signal?.aborted) return
|
|
232
219
|
|
|
233
220
|
// Delete the file from the database
|
|
234
221
|
await braid_blob.db.delete(key)
|
|
@@ -329,27 +316,40 @@ function create_braid_blob() {
|
|
|
329
316
|
})
|
|
330
317
|
}
|
|
331
318
|
|
|
332
|
-
braid_blob.sync =
|
|
333
|
-
var unsync_cbs = []
|
|
334
|
-
options.my_unsync = () => unsync_cbs.forEach(cb => cb())
|
|
335
|
-
|
|
319
|
+
braid_blob.sync = (a, b, options = {}) => {
|
|
336
320
|
if ((a instanceof URL) === (b instanceof URL)) {
|
|
337
321
|
// Both are URLs or both are local keys
|
|
322
|
+
var a_first_put, b_first_put
|
|
323
|
+
var a_first_put_promise = new Promise(done => a_first_put = done)
|
|
324
|
+
var b_first_put_promise = new Promise(done => b_first_put = done)
|
|
325
|
+
|
|
338
326
|
var a_ops = {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
327
|
+
signal: options.signal,
|
|
328
|
+
subscribe: update => {
|
|
329
|
+
braid_blob.put(b, update.body, {
|
|
330
|
+
signal: options.signal,
|
|
331
|
+
version: update.version,
|
|
332
|
+
content_type: update.headers?.['content-type']
|
|
333
|
+
}).then(a_first_put)
|
|
334
|
+
}
|
|
343
335
|
}
|
|
344
|
-
braid_blob.get(a, a_ops)
|
|
336
|
+
braid_blob.get(a, a_ops).then(x =>
|
|
337
|
+
x || b_first_put_promise.then(() =>
|
|
338
|
+
braid_blob.get(a, a_ops)))
|
|
345
339
|
|
|
346
340
|
var b_ops = {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
341
|
+
signal: options.signal,
|
|
342
|
+
subscribe: update => {
|
|
343
|
+
braid_blob.put(a, update.body, {
|
|
344
|
+
signal: options.signal,
|
|
345
|
+
version: update.version,
|
|
346
|
+
content_type: update.headers?.['content-type']
|
|
347
|
+
}).then(b_first_put)
|
|
348
|
+
}
|
|
351
349
|
}
|
|
352
|
-
braid_blob.get(b, b_ops)
|
|
350
|
+
braid_blob.get(b, b_ops).then(x =>
|
|
351
|
+
x || a_first_put_promise.then(() =>
|
|
352
|
+
braid_blob.get(b, b_ops)))
|
|
353
353
|
} else {
|
|
354
354
|
// One is local, one is remote - make a=local and b=remote (swap if not)
|
|
355
355
|
if (a instanceof URL) {
|
|
@@ -357,17 +357,30 @@ function create_braid_blob() {
|
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
var closed = false
|
|
360
|
-
options.my_unsync = () => { closed = true; disconnect() }
|
|
361
|
-
|
|
362
360
|
var disconnect = () => { }
|
|
361
|
+
options.signal?.addEventListener('abort', () =>
|
|
362
|
+
{ closed = true; disconnect() })
|
|
363
|
+
|
|
364
|
+
var local_first_put, remote_first_put
|
|
365
|
+
var local_first_put_promise = new Promise(done => local_first_put = done)
|
|
366
|
+
var remote_first_put_promise = new Promise(done => remote_first_put = done)
|
|
367
|
+
|
|
368
|
+
function handle_error(e) {
|
|
369
|
+
if (closed) return
|
|
370
|
+
disconnect()
|
|
371
|
+
console.log(`disconnected, retrying in 1 second`)
|
|
372
|
+
setTimeout(connect, 1000)
|
|
373
|
+
}
|
|
374
|
+
|
|
363
375
|
async function connect() {
|
|
364
376
|
var ac = new AbortController()
|
|
365
|
-
|
|
366
|
-
disconnect = () => disconnect_cbs.forEach(cb => cb())
|
|
377
|
+
disconnect = () => ac.abort()
|
|
367
378
|
|
|
368
379
|
try {
|
|
369
380
|
// Check if remote has our current version (simple fork-point check)
|
|
370
|
-
var local_result = await braid_blob.get(a
|
|
381
|
+
var local_result = await braid_blob.get(a, {
|
|
382
|
+
signal: ac.signal
|
|
383
|
+
})
|
|
371
384
|
var local_version = local_result ? local_result.version : null
|
|
372
385
|
var server_has_our_version = false
|
|
373
386
|
|
|
@@ -383,12 +396,13 @@ function create_braid_blob() {
|
|
|
383
396
|
|
|
384
397
|
// Local -> remote: subscribe to future local changes
|
|
385
398
|
var a_ops = {
|
|
399
|
+
signal: ac.signal,
|
|
386
400
|
subscribe: update => {
|
|
387
|
-
update.signal = ac.signal
|
|
388
401
|
braid_blob.put(b, update.body, {
|
|
402
|
+
signal: ac.signal,
|
|
389
403
|
version: update.version,
|
|
390
404
|
content_type: update.content_type
|
|
391
|
-
}).catch(e => {
|
|
405
|
+
}).then(local_first_put).catch(e => {
|
|
392
406
|
if (e.name === 'AbortError') {
|
|
393
407
|
// ignore
|
|
394
408
|
} else throw e
|
|
@@ -400,32 +414,56 @@ function create_braid_blob() {
|
|
|
400
414
|
if (server_has_our_version) {
|
|
401
415
|
a_ops.parents = local_version
|
|
402
416
|
}
|
|
403
|
-
braid_blob.get(a, a_ops)
|
|
404
417
|
|
|
405
418
|
// Remote -> local: subscribe to remote updates
|
|
419
|
+
// We need both: remote_res (for Editable header) and local file exists
|
|
420
|
+
var got_remote_res, got_local_file
|
|
421
|
+
var remote_res_promise = new Promise(done => got_remote_res = done)
|
|
422
|
+
var local_file_promise = new Promise(done => got_local_file = done)
|
|
423
|
+
|
|
424
|
+
// Apply read-only once we have both remote response and local file
|
|
425
|
+
Promise.all([remote_res_promise, local_file_promise]).then(async () => {
|
|
426
|
+
var read_only = remote_res.headers?.get('editable') === 'false'
|
|
427
|
+
await braid_blob.db.set_read_only(a, read_only)
|
|
428
|
+
})
|
|
429
|
+
|
|
406
430
|
var b_ops = {
|
|
407
|
-
|
|
431
|
+
signal: ac.signal,
|
|
408
432
|
subscribe: async update => {
|
|
409
433
|
await braid_blob.put(a, update.body, {
|
|
410
434
|
version: update.version,
|
|
411
435
|
content_type: update.headers?.['content-type']
|
|
412
436
|
})
|
|
437
|
+
got_local_file()
|
|
438
|
+
remote_first_put()
|
|
413
439
|
},
|
|
440
|
+
on_error: handle_error
|
|
414
441
|
}
|
|
415
442
|
// Use fork-point (parents) to avoid receiving data we already have
|
|
416
443
|
if (local_version) {
|
|
417
444
|
b_ops.parents = local_version
|
|
418
445
|
}
|
|
419
|
-
// NOTE: this should not return, but it might throw
|
|
420
|
-
await braid_blob.get(b, b_ops)
|
|
421
|
-
} catch (e) {
|
|
422
|
-
if (closed) {
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
446
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
447
|
+
// Set up both subscriptions, handling cases where one doesn't exist yet
|
|
448
|
+
braid_blob.get(a, a_ops).then(x => {
|
|
449
|
+
if (x) got_local_file()
|
|
450
|
+
else remote_first_put_promise.then(() =>
|
|
451
|
+
braid_blob.get(a, a_ops))
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
// Get the response to check Editable header
|
|
455
|
+
var remote_res = await braid_blob.get(b, b_ops)
|
|
456
|
+
if (remote_res) got_remote_res()
|
|
457
|
+
|
|
458
|
+
// If remote doesn't exist yet, wait for it to be created then reconnect
|
|
459
|
+
if (!remote_res) {
|
|
460
|
+
await local_first_put_promise
|
|
461
|
+
disconnect()
|
|
462
|
+
connect()
|
|
463
|
+
}
|
|
464
|
+
// Otherwise, on_error will call handle_error when connection drops
|
|
465
|
+
} catch (e) {
|
|
466
|
+
handle_error(e)
|
|
429
467
|
}
|
|
430
468
|
}
|
|
431
469
|
connect()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-blob",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
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.
|
|
14
|
+
"url-file-db": "^0.0.22"
|
|
15
15
|
}
|
|
16
16
|
}
|
package/test/tests.js
CHANGED
|
@@ -1147,6 +1147,153 @@ runTest(
|
|
|
1147
1147
|
'shared content'
|
|
1148
1148
|
)
|
|
1149
1149
|
|
|
1150
|
+
runTest(
|
|
1151
|
+
"test sync readonly remote to nonexistent local",
|
|
1152
|
+
async () => {
|
|
1153
|
+
var local_key = 'test-sync-readonly-local-' + Math.random().toString(36).slice(2)
|
|
1154
|
+
var remote_key = 'test-sync-readonly-remote-' + Math.random().toString(36).slice(2)
|
|
1155
|
+
|
|
1156
|
+
// Put something on the server first with Editable: false
|
|
1157
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1158
|
+
method: 'POST',
|
|
1159
|
+
body: `void (async () => {
|
|
1160
|
+
try {
|
|
1161
|
+
// Create a custom handler that sets Editable: false
|
|
1162
|
+
req.method = 'PUT'
|
|
1163
|
+
req.version = ['100']
|
|
1164
|
+
req.body = Buffer.from('readonly content')
|
|
1165
|
+
await braid_blob.put('${remote_key}', req.body, { version: req.version })
|
|
1166
|
+
res.end('created')
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1169
|
+
}
|
|
1170
|
+
})()`
|
|
1171
|
+
})
|
|
1172
|
+
var result = await r1.text()
|
|
1173
|
+
if (result.startsWith('error:')) return result
|
|
1174
|
+
|
|
1175
|
+
// Now sync to a local key that doesn't exist, with a readonly remote
|
|
1176
|
+
var r2 = await braid_fetch(`/eval`, {
|
|
1177
|
+
method: 'POST',
|
|
1178
|
+
body: `void (async () => {
|
|
1179
|
+
try {
|
|
1180
|
+
var fs = require('fs').promises
|
|
1181
|
+
var test_id = 'test-sync-readonly-' + Math.random().toString(36).slice(2)
|
|
1182
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1183
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1184
|
+
|
|
1185
|
+
var bb = braid_blob.create_braid_blob()
|
|
1186
|
+
bb.db_folder = db_folder
|
|
1187
|
+
bb.meta_folder = meta_folder
|
|
1188
|
+
|
|
1189
|
+
// Create a mock server URL that returns Editable: false
|
|
1190
|
+
var http = require('http')
|
|
1191
|
+
var {http_server: braidify} = require('braid-http')
|
|
1192
|
+
|
|
1193
|
+
var mock_server = http.createServer((req2, res2) => {
|
|
1194
|
+
braidify(req2, res2)
|
|
1195
|
+
res2.setHeader('Editable', 'false')
|
|
1196
|
+
braid_blob.serve(req2, res2, { key: '${remote_key}' })
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
await new Promise(resolve => mock_server.listen(0, resolve))
|
|
1200
|
+
var port = mock_server.address().port
|
|
1201
|
+
|
|
1202
|
+
var remote_url = new URL('http://localhost:' + port + '/${remote_key}')
|
|
1203
|
+
var ac = new AbortController()
|
|
1204
|
+
|
|
1205
|
+
// Start sync - local key doesn't exist yet
|
|
1206
|
+
bb.sync('${local_key}', remote_url, { signal: ac.signal })
|
|
1207
|
+
|
|
1208
|
+
// Wait for sync to happen
|
|
1209
|
+
await new Promise(done => setTimeout(done, 500))
|
|
1210
|
+
|
|
1211
|
+
// Stop sync
|
|
1212
|
+
ac.abort()
|
|
1213
|
+
mock_server.close()
|
|
1214
|
+
|
|
1215
|
+
// Check if local file exists and has the content
|
|
1216
|
+
var result = await bb.get('${local_key}')
|
|
1217
|
+
|
|
1218
|
+
var response
|
|
1219
|
+
if (!result) {
|
|
1220
|
+
response = 'file not created'
|
|
1221
|
+
} else if (result.body.toString() !== 'readonly content') {
|
|
1222
|
+
response = 'wrong content: ' + result.body.toString()
|
|
1223
|
+
} else {
|
|
1224
|
+
// Check if the file is actually read-only
|
|
1225
|
+
await bb.init()
|
|
1226
|
+
var is_readonly = await bb.db.is_read_only('${local_key}')
|
|
1227
|
+
response = is_readonly ? 'synced and readonly' : 'synced but NOT readonly'
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Clean up
|
|
1231
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1232
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1233
|
+
|
|
1234
|
+
res.end(response)
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1237
|
+
}
|
|
1238
|
+
})()`
|
|
1239
|
+
})
|
|
1240
|
+
return await r2.text()
|
|
1241
|
+
},
|
|
1242
|
+
'synced and readonly'
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
runTest(
|
|
1246
|
+
"test sync does not disconnect unnecessarily",
|
|
1247
|
+
async () => {
|
|
1248
|
+
var local_key = 'test-sync-no-disconnect-local-' + Math.random().toString(36).slice(2)
|
|
1249
|
+
var remote_key = 'test-sync-no-disconnect-remote-' + Math.random().toString(36).slice(2)
|
|
1250
|
+
|
|
1251
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1252
|
+
method: 'POST',
|
|
1253
|
+
body: `void (async () => {
|
|
1254
|
+
try {
|
|
1255
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
1256
|
+
|
|
1257
|
+
// Put something locally first
|
|
1258
|
+
await braid_blob.put('${local_key}', Buffer.from('local content'), { version: ['600'] })
|
|
1259
|
+
|
|
1260
|
+
var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
|
|
1261
|
+
|
|
1262
|
+
// Capture console.log to count disconnects
|
|
1263
|
+
var disconnect_count = 0
|
|
1264
|
+
var original_log = console.log
|
|
1265
|
+
console.log = function(...args) {
|
|
1266
|
+
if (args[0]?.includes?.('disconnected')) disconnect_count++
|
|
1267
|
+
original_log.apply(console, args)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Create an AbortController to stop the sync
|
|
1271
|
+
var ac = new AbortController()
|
|
1272
|
+
|
|
1273
|
+
// Start sync
|
|
1274
|
+
braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
|
|
1275
|
+
|
|
1276
|
+
// Wait for sync to establish and stabilize
|
|
1277
|
+
await new Promise(done => setTimeout(done, 500))
|
|
1278
|
+
|
|
1279
|
+
// Stop sync
|
|
1280
|
+
ac.abort()
|
|
1281
|
+
|
|
1282
|
+
// Restore console.log
|
|
1283
|
+
console.log = original_log
|
|
1284
|
+
|
|
1285
|
+
// Should have zero disconnects during normal operation
|
|
1286
|
+
res.end(disconnect_count === 0 ? 'no disconnects' : 'disconnects: ' + disconnect_count)
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
res.end('error: ' + e.message + ' ' + e.stack)
|
|
1289
|
+
}
|
|
1290
|
+
})()`
|
|
1291
|
+
})
|
|
1292
|
+
return await r1.text()
|
|
1293
|
+
},
|
|
1294
|
+
'no disconnects'
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1150
1297
|
runTest(
|
|
1151
1298
|
"test sync closed during error",
|
|
1152
1299
|
async () => {
|
|
@@ -1162,12 +1309,14 @@ runTest(
|
|
|
1162
1309
|
// Use an invalid/unreachable URL to trigger an error
|
|
1163
1310
|
var remote_url = new URL('http://localhost:9999/${remote_key}')
|
|
1164
1311
|
|
|
1165
|
-
//
|
|
1166
|
-
var
|
|
1167
|
-
|
|
1312
|
+
// Create an AbortController to stop the sync
|
|
1313
|
+
var ac = new AbortController()
|
|
1314
|
+
|
|
1315
|
+
// Start sync with signal
|
|
1316
|
+
braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
|
|
1168
1317
|
|
|
1169
1318
|
// Close the sync immediately to trigger the closed path when error occurs
|
|
1170
|
-
|
|
1319
|
+
ac.abort()
|
|
1171
1320
|
|
|
1172
1321
|
res.end('sync started and closed')
|
|
1173
1322
|
} catch (e) {
|
|
@@ -1201,15 +1350,17 @@ runTest(
|
|
|
1201
1350
|
// Use an invalid/unreachable URL to trigger an error
|
|
1202
1351
|
var remote_url = new URL('http://localhost:9999/${remote_key}')
|
|
1203
1352
|
|
|
1204
|
-
//
|
|
1205
|
-
var
|
|
1206
|
-
|
|
1353
|
+
// Create an AbortController to stop the sync
|
|
1354
|
+
var ac = new AbortController()
|
|
1355
|
+
|
|
1356
|
+
// Start sync with signal - should trigger retry on error
|
|
1357
|
+
braid_blob.sync('${local_key}', remote_url, { signal: ac.signal })
|
|
1207
1358
|
|
|
1208
1359
|
// Wait a bit for the error to occur and retry message to print
|
|
1209
1360
|
await new Promise(done => setTimeout(done, 200))
|
|
1210
1361
|
|
|
1211
1362
|
// Now close it to stop retrying
|
|
1212
|
-
|
|
1363
|
+
ac.abort()
|
|
1213
1364
|
|
|
1214
1365
|
res.end('sync error occurred')
|
|
1215
1366
|
} catch (e) {
|
|
@@ -1494,6 +1645,213 @@ runTest(
|
|
|
1494
1645
|
'true'
|
|
1495
1646
|
)
|
|
1496
1647
|
|
|
1648
|
+
runTest(
|
|
1649
|
+
"test get with URL returns null on 404",
|
|
1650
|
+
async () => {
|
|
1651
|
+
var key = 'test-url-get-404-' + Math.random().toString(36).slice(2)
|
|
1652
|
+
|
|
1653
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1654
|
+
method: 'POST',
|
|
1655
|
+
body: `void (async () => {
|
|
1656
|
+
var braid_blob = require(\`\${__dirname}/../index.js\`)
|
|
1657
|
+
var url = new URL('http://localhost:' + req.socket.localPort + '/${key}')
|
|
1658
|
+
var result = await braid_blob.get(url)
|
|
1659
|
+
res.end(result === null ? 'null' : 'not null: ' + JSON.stringify(result))
|
|
1660
|
+
})()`
|
|
1661
|
+
})
|
|
1662
|
+
|
|
1663
|
+
return await r1.text()
|
|
1664
|
+
},
|
|
1665
|
+
'null'
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
runTest(
|
|
1669
|
+
"test signal abort stops local put operation",
|
|
1670
|
+
async () => {
|
|
1671
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1672
|
+
method: 'POST',
|
|
1673
|
+
body: `void (async () => {
|
|
1674
|
+
var fs = require('fs').promises
|
|
1675
|
+
var test_id = 'test-abort-put-' + Math.random().toString(36).slice(2)
|
|
1676
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1677
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1678
|
+
|
|
1679
|
+
try {
|
|
1680
|
+
var bb = braid_blob.create_braid_blob()
|
|
1681
|
+
bb.db_folder = db_folder
|
|
1682
|
+
bb.meta_folder = meta_folder
|
|
1683
|
+
|
|
1684
|
+
// Create an already-aborted signal
|
|
1685
|
+
var ac = new AbortController()
|
|
1686
|
+
ac.abort()
|
|
1687
|
+
|
|
1688
|
+
// Try to put with aborted signal
|
|
1689
|
+
var result = await bb.put('/test-file', Buffer.from('hello'), {
|
|
1690
|
+
signal: ac.signal
|
|
1691
|
+
})
|
|
1692
|
+
|
|
1693
|
+
// Result should be undefined since operation was aborted
|
|
1694
|
+
res.end(result === undefined ? 'aborted' : 'not aborted: ' + result)
|
|
1695
|
+
} catch (e) {
|
|
1696
|
+
res.end('error: ' + e.message)
|
|
1697
|
+
} finally {
|
|
1698
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1699
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1700
|
+
}
|
|
1701
|
+
})()`
|
|
1702
|
+
})
|
|
1703
|
+
return await r1.text()
|
|
1704
|
+
},
|
|
1705
|
+
'aborted'
|
|
1706
|
+
)
|
|
1707
|
+
|
|
1708
|
+
runTest(
|
|
1709
|
+
"test signal abort stops local get operation",
|
|
1710
|
+
async () => {
|
|
1711
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1712
|
+
method: 'POST',
|
|
1713
|
+
body: `void (async () => {
|
|
1714
|
+
var fs = require('fs').promises
|
|
1715
|
+
var test_id = 'test-abort-get-' + Math.random().toString(36).slice(2)
|
|
1716
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1717
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1718
|
+
|
|
1719
|
+
try {
|
|
1720
|
+
var bb = braid_blob.create_braid_blob()
|
|
1721
|
+
bb.db_folder = db_folder
|
|
1722
|
+
bb.meta_folder = meta_folder
|
|
1723
|
+
|
|
1724
|
+
// Put a file first
|
|
1725
|
+
await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
|
|
1726
|
+
|
|
1727
|
+
// Create an already-aborted signal
|
|
1728
|
+
var ac = new AbortController()
|
|
1729
|
+
ac.abort()
|
|
1730
|
+
|
|
1731
|
+
// Try to get with aborted signal (after header_cb)
|
|
1732
|
+
var header_called = false
|
|
1733
|
+
var result = await bb.get('/test-file', {
|
|
1734
|
+
signal: ac.signal,
|
|
1735
|
+
header_cb: () => { header_called = true }
|
|
1736
|
+
})
|
|
1737
|
+
|
|
1738
|
+
// Result should be undefined since operation was aborted after header_cb
|
|
1739
|
+
res.end(header_called && result === undefined ? 'aborted' : 'not aborted: header=' + header_called + ' result=' + JSON.stringify(result))
|
|
1740
|
+
} catch (e) {
|
|
1741
|
+
res.end('error: ' + e.message)
|
|
1742
|
+
} finally {
|
|
1743
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1744
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1745
|
+
}
|
|
1746
|
+
})()`
|
|
1747
|
+
})
|
|
1748
|
+
return await r1.text()
|
|
1749
|
+
},
|
|
1750
|
+
'aborted'
|
|
1751
|
+
)
|
|
1752
|
+
|
|
1753
|
+
runTest(
|
|
1754
|
+
"test signal abort stops local delete operation",
|
|
1755
|
+
async () => {
|
|
1756
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1757
|
+
method: 'POST',
|
|
1758
|
+
body: `void (async () => {
|
|
1759
|
+
var fs = require('fs').promises
|
|
1760
|
+
var test_id = 'test-abort-delete-' + Math.random().toString(36).slice(2)
|
|
1761
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1762
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1763
|
+
|
|
1764
|
+
try {
|
|
1765
|
+
var bb = braid_blob.create_braid_blob()
|
|
1766
|
+
bb.db_folder = db_folder
|
|
1767
|
+
bb.meta_folder = meta_folder
|
|
1768
|
+
|
|
1769
|
+
// Put a file first
|
|
1770
|
+
await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
|
|
1771
|
+
|
|
1772
|
+
// Create an already-aborted signal
|
|
1773
|
+
var ac = new AbortController()
|
|
1774
|
+
ac.abort()
|
|
1775
|
+
|
|
1776
|
+
// Try to delete with aborted signal
|
|
1777
|
+
await bb.delete('/test-file', { signal: ac.signal })
|
|
1778
|
+
|
|
1779
|
+
// File should still exist since delete was aborted
|
|
1780
|
+
var result = await bb.get('/test-file')
|
|
1781
|
+
res.end(result && result.body ? 'still exists' : 'deleted')
|
|
1782
|
+
} catch (e) {
|
|
1783
|
+
res.end('error: ' + e.message)
|
|
1784
|
+
} finally {
|
|
1785
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1786
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1787
|
+
}
|
|
1788
|
+
})()`
|
|
1789
|
+
})
|
|
1790
|
+
return await r1.text()
|
|
1791
|
+
},
|
|
1792
|
+
'still exists'
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
runTest(
|
|
1796
|
+
"test signal abort stops subscription updates",
|
|
1797
|
+
async () => {
|
|
1798
|
+
var r1 = await braid_fetch(`/eval`, {
|
|
1799
|
+
method: 'POST',
|
|
1800
|
+
body: `void (async () => {
|
|
1801
|
+
var fs = require('fs').promises
|
|
1802
|
+
var test_id = 'test-abort-sub-' + Math.random().toString(36).slice(2)
|
|
1803
|
+
var db_folder = __dirname + '/' + test_id + '-db'
|
|
1804
|
+
var meta_folder = __dirname + '/' + test_id + '-meta'
|
|
1805
|
+
|
|
1806
|
+
try {
|
|
1807
|
+
var bb = braid_blob.create_braid_blob()
|
|
1808
|
+
bb.db_folder = db_folder
|
|
1809
|
+
bb.meta_folder = meta_folder
|
|
1810
|
+
|
|
1811
|
+
// Put a file first
|
|
1812
|
+
await bb.put('/test-file', Buffer.from('v1'), { version: ['1'] })
|
|
1813
|
+
|
|
1814
|
+
// Subscribe with an AbortController
|
|
1815
|
+
var ac = new AbortController()
|
|
1816
|
+
var updates = []
|
|
1817
|
+
|
|
1818
|
+
await bb.get('/test-file', {
|
|
1819
|
+
signal: ac.signal,
|
|
1820
|
+
subscribe: (update) => {
|
|
1821
|
+
updates.push(update.body.toString())
|
|
1822
|
+
}
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
// Should have received initial update
|
|
1826
|
+
if (updates.length !== 1 || updates[0] !== 'v1') {
|
|
1827
|
+
res.end('initial update wrong: ' + JSON.stringify(updates))
|
|
1828
|
+
return
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Abort the subscription
|
|
1832
|
+
ac.abort()
|
|
1833
|
+
|
|
1834
|
+
// Put another update
|
|
1835
|
+
await bb.put('/test-file', Buffer.from('v2'), { version: ['2'] })
|
|
1836
|
+
|
|
1837
|
+
// Wait a bit for any updates to propagate
|
|
1838
|
+
await new Promise(done => setTimeout(done, 50))
|
|
1839
|
+
|
|
1840
|
+
// Should still only have the initial update
|
|
1841
|
+
res.end(updates.length === 1 ? 'stopped' : 'got extra: ' + JSON.stringify(updates))
|
|
1842
|
+
} catch (e) {
|
|
1843
|
+
res.end('error: ' + e.message)
|
|
1844
|
+
} finally {
|
|
1845
|
+
await fs.rm(db_folder, { recursive: true, force: true })
|
|
1846
|
+
await fs.rm(meta_folder, { recursive: true, force: true })
|
|
1847
|
+
}
|
|
1848
|
+
})()`
|
|
1849
|
+
})
|
|
1850
|
+
return await r1.text()
|
|
1851
|
+
},
|
|
1852
|
+
'stopped'
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1497
1855
|
}
|
|
1498
1856
|
|
|
1499
1857
|
// Export for Node.js (CommonJS)
|