braid-blob 0.0.38 → 0.0.40

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.
Files changed (3) hide show
  1. package/index.js +325 -322
  2. package/package.json +1 -1
  3. package/test/tests.js +35 -78
package/index.js CHANGED
@@ -1,6 +1,4 @@
1
- var {http_server: braidify, fetch: braid_fetch} = require('braid-http'),
2
- fs = require('fs'),
3
- path = require('path')
1
+ var {http_server: braidify, fetch: braid_fetch} = require('braid-http')
4
2
 
5
3
  function create_braid_blob() {
6
4
  var braid_blob = {
@@ -21,29 +19,29 @@ function create_braid_blob() {
21
19
 
22
20
  async function real_init() {
23
21
  // Ensure our meta folder exists
24
- await fs.promises.mkdir(braid_blob.meta_folder, { recursive: true })
22
+ await require('fs').promises.mkdir(braid_blob.meta_folder, { recursive: true })
25
23
 
26
24
  // Set up db - either use provided object or create file-based storage
27
25
  if (typeof braid_blob.db_folder === 'string') {
28
- await fs.promises.mkdir(braid_blob.db_folder, { recursive: true })
26
+ await require('fs').promises.mkdir(braid_blob.db_folder, { recursive: true })
29
27
  braid_blob.db = {
30
28
  read: async (key) => {
31
- var file_path = path.join(braid_blob.db_folder, encode_filename(key))
29
+ var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
32
30
  try {
33
- return await fs.promises.readFile(file_path)
31
+ return await require('fs').promises.readFile(file_path)
34
32
  } catch (e) {
35
33
  if (e.code === 'ENOENT') return null
36
34
  throw e
37
35
  }
38
36
  },
39
37
  write: async (key, data) => {
40
- var file_path = path.join(braid_blob.db_folder, encode_filename(key))
41
- await fs.promises.writeFile(file_path, data)
38
+ var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
39
+ await require('fs').promises.writeFile(file_path, data)
42
40
  },
43
41
  delete: async (key) => {
44
- var file_path = path.join(braid_blob.db_folder, encode_filename(key))
42
+ var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
45
43
  try {
46
- await fs.promises.unlink(file_path)
44
+ await require('fs').promises.unlink(file_path)
47
45
  } catch (e) {
48
46
  if (e.code !== 'ENOENT') throw e
49
47
  }
@@ -60,32 +58,32 @@ function create_braid_blob() {
60
58
  }
61
59
  }
62
60
 
63
- function get_meta(key) {
64
- if (braid_blob.meta_cache[key]) return braid_blob.meta_cache[key]
65
- var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
66
- try {
67
- var data = fs.readFileSync(meta_path, 'utf8')
68
- braid_blob.meta_cache[key] = JSON.parse(data)
69
- return braid_blob.meta_cache[key]
70
- } catch (e) {
71
- if (e.code === 'ENOENT') return null
72
- throw e
61
+ async function get_meta(key) {
62
+ if (!braid_blob.meta_cache[key]) {
63
+ try {
64
+ braid_blob.meta_cache[key] = JSON.parse(
65
+ await require('fs').promises.readFile(
66
+ `${braid_blob.meta_folder}/${encode_filename(key)}`, 'utf8'))
67
+ } catch (e) {
68
+ if (e.code === 'ENOENT')
69
+ braid_blob.meta_cache[key] = {}
70
+ else throw e
71
+ }
73
72
  }
73
+ return braid_blob.meta_cache[key]
74
74
  }
75
75
 
76
- async function update_meta(key, updates) {
77
- var meta = get_meta(key) || {}
78
- Object.assign(meta, updates)
79
- braid_blob.meta_cache[key] = meta
80
- var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
81
- await fs.promises.writeFile(meta_path, JSON.stringify(meta))
76
+ async function save_meta(key) {
77
+ await require('fs').promises.writeFile(
78
+ `${braid_blob.meta_folder}/${encode_filename(key)}`,
79
+ JSON.stringify(braid_blob.meta_cache[key]))
82
80
  }
83
81
 
84
82
  async function delete_meta(key) {
85
83
  delete braid_blob.meta_cache[key]
86
- var meta_path = path.join(braid_blob.meta_folder, encode_filename(key))
87
84
  try {
88
- await fs.promises.unlink(meta_path)
85
+ await require('fs').promises.unlink(
86
+ `${braid_blob.meta_folder}/${encode_filename(key)}`)
89
87
  } catch (e) {
90
88
  if (e.code !== 'ENOENT') throw e
91
89
  }
@@ -96,11 +94,10 @@ function create_braid_blob() {
96
94
 
97
95
  // Handle URL case - make a remote PUT request
98
96
  if (key instanceof URL) {
99
-
100
97
  var params = {
101
98
  method: 'PUT',
102
99
  signal: options.signal,
103
- body: body
100
+ body
104
101
  }
105
102
  if (!options.dont_retry)
106
103
  params.retry = () => true
@@ -116,48 +113,43 @@ function create_braid_blob() {
116
113
  await braid_blob.init()
117
114
  if (options.signal?.aborted) return
118
115
 
119
- var meta = get_meta(key) || {}
116
+ return await within_fiber(key, async () => {
117
+ var meta = await get_meta(key)
118
+ if (options.signal?.aborted) return
120
119
 
121
- var their_e =
122
- !options.version ?
120
+ var their_e = options.version ? options.version[0] :
123
121
  // we'll give them a event id in this case
124
- `${braid_blob.peer}-${Math.max(Date.now(),
125
- meta.event ? 1*get_event_seq(meta.event) + 1 : -Infinity)}` :
126
- !options.version.length ?
127
- null :
128
- options.version[0]
129
-
130
- if (their_e != null &&
131
- (meta.event == null ||
132
- compare_events(their_e, meta.event) > 0)) {
133
- meta.event = their_e
134
-
135
- // Write the file using url-file-db (unless skip_write is set)
136
- if (!options.skip_write)
137
- await braid_blob.db.write(key, body)
138
- if (options.signal?.aborted) return
122
+ `${braid_blob.peer}-${max_seq('' + Date.now(),
123
+ meta.event ? increment_seq(get_event_seq(meta.event)) : '')}`
139
124
 
140
- // Update only the fields we want to change in metadata
141
- var meta_updates = { event: their_e }
142
- if (options.content_type)
143
- meta_updates.content_type = options.content_type
125
+ if (compare_events(their_e, meta.event) > 0) {
126
+ meta.event = their_e
144
127
 
145
- await update_meta(key, meta_updates)
146
- if (options.signal?.aborted) return
128
+ if (!options.skip_write)
129
+ await braid_blob.db.write(key, body)
130
+ if (options.signal?.aborted) return
147
131
 
148
- // Notify all subscriptions of the update
149
- // (except the peer which made the PUT request itself)
150
- if (braid_blob.key_to_subs[key])
151
- for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
152
- if (!options.peer || options.peer !== peer)
153
- sub.sendUpdate({
154
- version: [meta.event],
155
- 'Merge-Type': 'aww',
156
- body
157
- })
158
- }
132
+ if (options.content_type)
133
+ meta.content_type = options.content_type
134
+
135
+ await save_meta(key)
136
+ if (options.signal?.aborted) return
159
137
 
160
- return meta.event
138
+ // Notify all subscriptions of the update
139
+ // (except the peer which made the PUT request itself)
140
+ var update = {
141
+ version: [meta.event],
142
+ content_type: meta.content_type,
143
+ body
144
+ }
145
+ if (braid_blob.key_to_subs[key])
146
+ for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
147
+ if (!options.peer || options.peer !== peer)
148
+ await sub.sendUpdate(update)
149
+ }
150
+
151
+ return meta.event
152
+ })
161
153
  }
162
154
 
163
155
  braid_blob.get = async (key, options = {}) => {
@@ -167,7 +159,7 @@ function create_braid_blob() {
167
159
  if (key instanceof URL) {
168
160
  var params = {
169
161
  signal: options.signal,
170
- subscribe: !!options.subscribe,
162
+ subscribe: options.subscribe,
171
163
  heartbeats: 120,
172
164
  }
173
165
  if (!options.dont_retry) {
@@ -183,7 +175,9 @@ function create_braid_blob() {
183
175
 
184
176
  var res = await braid_fetch(key.href, params)
185
177
 
186
- if (!res.ok) return null
178
+ if (!res.ok)
179
+ if (options.subscribe) throw new Error('failed to subscribe')
180
+ else return null
187
181
 
188
182
  var result = {}
189
183
  if (res.version) result.version = res.version
@@ -192,6 +186,8 @@ function create_braid_blob() {
192
186
 
193
187
  if (options.subscribe) {
194
188
  res.subscribe(async update => {
189
+ if (update.status === 404) update.delete = true
190
+ update.content_type = update.extra_headers['content-type']
195
191
  await options.subscribe(update)
196
192
  }, e => options.on_error?.(e))
197
193
  return res
@@ -202,70 +198,72 @@ function create_braid_blob() {
202
198
  }
203
199
 
204
200
  await braid_blob.init()
201
+ if (options.signal?.aborted) return
205
202
 
206
- var meta = get_meta(key) || {}
207
- if (meta.event == null) return null
203
+ return await within_fiber(key, async () => {
204
+ var meta = await get_meta(key)
205
+ if (options.signal?.aborted) return
208
206
 
209
- var result = {
210
- version: [meta.event],
211
- content_type: meta.content_type || options.content_type
212
- }
213
- if (options.header_cb) await options.header_cb(result)
214
- if (options.signal?.aborted) return
215
- // Check if requested version/parents is newer than what we have - if so, we don't have it
216
- if (options.version && options.version.length && compare_events(options.version[0], meta.event) > 0)
217
- throw new Error('unknown version: ' + options.version)
218
- if (options.parents && options.parents.length && compare_events(options.parents[0], meta.event) > 0)
219
- throw new Error('unknown version: ' + options.parents)
220
- if (options.head) return result
221
-
222
- if (options.subscribe) {
223
- var subscribe_chain = Promise.resolve()
224
- options.my_subscribe = (x) => subscribe_chain =
225
- subscribe_chain.then(() =>
226
- !options.signal?.aborted && options.subscribe(x))
227
-
228
- // Start a subscription for future updates
229
- if (!braid_blob.key_to_subs[key])
230
- braid_blob.key_to_subs[key] = new Map()
231
-
232
- var peer = options.peer || Math.random().toString(36).slice(2)
233
- braid_blob.key_to_subs[key].set(peer, {
234
- sendUpdate: (update) => {
235
- options.my_subscribe({
236
- body: update.body,
237
- version: update.version,
238
- content_type: meta.content_type || options.content_type
239
- })
240
- }
241
- })
207
+ if (!meta.event && !options.subscribe) return null
242
208
 
243
- options.signal?.addEventListener('abort', () => {
244
- braid_blob.key_to_subs[key].delete(peer)
245
- if (!braid_blob.key_to_subs[key].size)
246
- delete braid_blob.key_to_subs[key]
247
- })
209
+ var result = {
210
+ version: meta.event ? [meta.event] : [],
211
+ content_type: meta.content_type
212
+ }
248
213
 
249
- if (options.before_send_cb) await options.before_send_cb(result)
214
+ if (options.header_cb) await options.header_cb(result)
250
215
  if (options.signal?.aborted) return
251
216
 
252
- // Send an immediate update if needed
253
- if (!options.parents ||
254
- !options.parents.length ||
255
- compare_events(result.version[0], options.parents[0]) > 0) {
256
- result.sent = true
257
- options.my_subscribe({
258
- body: await braid_blob.db.read(key),
259
- version: result.version,
260
- content_type: result.content_type
217
+ // Check if requested version/parents is newer than what we have - if so, we don't have it
218
+ if (!options.subscribe) {
219
+ if (compare_events(options.version?.[0], meta.event) > 0)
220
+ throw new Error('unknown version: ' + options.version)
221
+ if (compare_events(options.parents?.[0], meta.event) > 0)
222
+ throw new Error('unknown version: ' + options.parents)
223
+ }
224
+ if (options.head) return result
225
+
226
+ if (options.subscribe) {
227
+ var subscribe_chain = Promise.resolve()
228
+ options.my_subscribe = (x) => subscribe_chain =
229
+ subscribe_chain.then(() =>
230
+ !options.signal?.aborted && options.subscribe(x))
231
+
232
+ // Start a subscription for future updates
233
+ if (!braid_blob.key_to_subs[key])
234
+ braid_blob.key_to_subs[key] = new Map()
235
+
236
+ var peer = options.peer || Math.random().toString(36).slice(2)
237
+ braid_blob.key_to_subs[key].set(peer, {
238
+ sendUpdate: (update) => {
239
+ if (update.delete) options.my_subscribe(update)
240
+ else if (compare_events(update.version[0], options.parents?.[0]) > 0)
241
+ options.my_subscribe(update)
242
+ }
243
+ })
244
+
245
+ options.signal?.addEventListener('abort', () => {
246
+ braid_blob.key_to_subs[key].delete(peer)
247
+ if (!braid_blob.key_to_subs[key].size)
248
+ delete braid_blob.key_to_subs[key]
261
249
  })
250
+
251
+ if (options.before_send_cb) await options.before_send_cb()
252
+ if (options.signal?.aborted) return
253
+
254
+ // Send an immediate update if needed
255
+ if (compare_events(result.version?.[0], options.parents?.[0]) > 0) {
256
+ result.sent = true
257
+ result.body = await braid_blob.db.read(key)
258
+ options.my_subscribe(result)
259
+ }
260
+ } else {
261
+ // If not subscribe, send the body now
262
+ result.body = await braid_blob.db.read(key)
262
263
  }
263
- } else {
264
- // If not subscribe, send the body now
265
- result.body = await braid_blob.db.read(key)
266
- }
267
264
 
268
- return result
265
+ return result
266
+ })
269
267
  }
270
268
 
271
269
  braid_blob.delete = async (key, options = {}) => {
@@ -273,13 +271,18 @@ function create_braid_blob() {
273
271
 
274
272
  // Handle URL case - make a remote DELETE request
275
273
  if (key instanceof URL) {
276
-
277
274
  var params = {
278
275
  method: 'DELETE',
279
276
  signal: options.signal
280
277
  }
278
+ if (!options.dont_retry)
279
+ params.retry = (res) => res.status !== 309 &&
280
+ res.status !== 404 && res.status !== 406
281
281
  for (var x of ['headers', 'peer'])
282
282
  if (options[x] != null) params[x] = options[x]
283
+ if (options.content_type)
284
+ params.headers = { ...params.headers,
285
+ 'Accept': options.content_type }
283
286
 
284
287
  return await braid_fetch(key.href, params)
285
288
  }
@@ -287,14 +290,24 @@ function create_braid_blob() {
287
290
  await braid_blob.init()
288
291
  if (options.signal?.aborted) return
289
292
 
290
- // Delete the file and its metadata
291
- await braid_blob.db.delete(key)
292
- await delete_meta(key)
293
+ return await within_fiber(key, async () => {
294
+ var meta = await get_meta(key)
295
+ if (options.signal?.aborted) return
296
+
297
+ await braid_blob.db.delete(key)
298
+ await delete_meta(key)
293
299
 
294
- // TODO: notify subscribers of deletion once we have a protocol for that
295
- // For now, just clean up the subscriptions
296
- if (braid_blob.key_to_subs[key])
297
- delete braid_blob.key_to_subs[key]
300
+ // Notify all subscriptions of the delete
301
+ // (except the peer which made the DELETE request itself)
302
+ var update = {
303
+ delete: true,
304
+ content_type: meta.content_type
305
+ }
306
+ if (braid_blob.key_to_subs[key])
307
+ for (var [peer, sub] of braid_blob.key_to_subs[key].entries())
308
+ if (!options.peer || options.peer !== peer)
309
+ sub.sendUpdate(update)
310
+ })
298
311
  }
299
312
 
300
313
  braid_blob.serve = async (req, res, options = {}) => {
@@ -309,82 +322,85 @@ function create_braid_blob() {
309
322
  if (res.is_multiplexer) return
310
323
 
311
324
  // Handle OPTIONS request
312
- if (req.method === 'OPTIONS') return res.end();
325
+ if (req.method === 'OPTIONS') return res.end()
313
326
 
314
327
  // consume PUT body
315
328
  var body = req.method === 'PUT' && await slurp(req)
316
329
 
317
- await within_fiber(options.key, async () => {
318
- if (req.method === 'GET' || req.method === 'HEAD') {
319
- if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
320
- if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
321
- res.setHeader("Merge-Type", "aww")
330
+ if (req.method === 'GET' || req.method === 'HEAD') {
331
+ if (!res.hasHeader("editable")) res.setHeader("Editable", "true")
332
+ if (!req.subscribe) res.setHeader("Accept-Subscribe", "true")
333
+ res.setHeader("Merge-Type", "aww")
322
334
 
323
- try {
324
- var result = await braid_blob.get(options.key, {
325
- peer: req.peer,
326
- head: req.method == "HEAD",
327
- version: req.version || null,
328
- parents: req.parents || null,
329
- header_cb: (result) => {
330
- res.setHeader((req.subscribe ? "Current-" : "") +
331
- "Version", ascii_ify(result.version.map((x) =>
332
- JSON.stringify(x)).join(", ")))
333
- if (result.content_type)
334
- res.setHeader('Content-Type', result.content_type)
335
- },
336
- before_send_cb: (result) =>
337
- res.startSubscription({ onClose: result.unsubscribe }),
338
- subscribe: req.subscribe ? (update) => {
339
- res.sendUpdate({
340
- version: update.version,
341
- 'Merge-Type': 'aww',
342
- body: update.body
343
- })
344
- } : null
345
- })
346
- } catch (e) {
347
- if (e.message && e.message.startsWith('unknown version')) {
348
- // Server doesn't have this version
349
- res.statusCode = 309
350
- res.statusMessage = 'Version Unknown Here'
351
- return res.end('')
352
- } else throw e
353
- }
335
+ try {
336
+ var result = await braid_blob.get(options.key, {
337
+ peer: req.peer,
338
+ head: req.method === "HEAD",
339
+ version: req.version,
340
+ parents: req.parents,
341
+ header_cb: (result) => {
342
+ res.setHeader((req.subscribe ? "Current-" : "") +
343
+ "Version", version_to_header(result.version))
344
+ if (result.content_type)
345
+ res.setHeader('Content-Type', result.content_type)
346
+ },
347
+ before_send_cb: () => res.startSubscription(),
348
+ subscribe: req.subscribe ? (update) => {
349
+ if (update.delete) {
350
+ update.status = 404
351
+ delete update.delete
352
+ }
353
+ if (update.content_type) {
354
+ update['Content-Type'] = update.content_type
355
+ delete update.content_type
356
+ }
357
+ update['Merge-Type'] = 'aww'
358
+ res.sendUpdate(update)
359
+ } : null
360
+ })
361
+ } catch (e) {
362
+ if (e.message && e.message.startsWith('unknown version')) {
363
+ // Server doesn't have this version
364
+ res.statusCode = 309
365
+ res.statusMessage = 'Version Unknown Here'
366
+ return res.end('')
367
+ } else throw e
368
+ }
354
369
 
355
- if (!result) {
356
- res.statusCode = 404
357
- return res.end('File Not Found')
358
- }
370
+ if (!result) {
371
+ res.statusCode = 404
372
+ return res.end('File Not Found')
373
+ }
359
374
 
360
- if (result.content_type && req.headers.accept &&
361
- !isAcceptable(result.content_type, req.headers.accept)) {
362
- res.statusCode = 406
363
- return res.end(`Content-Type of ${result.content_type} not in Accept: ${req.headers.accept}`)
364
- }
375
+ if (result.content_type && req.headers.accept &&
376
+ !isAcceptable(result.content_type, req.headers.accept)) {
377
+ res.statusCode = 406
378
+ return res.end(`Content-Type of ${result.content_type} not in Accept: ${req.headers.accept}`)
379
+ }
365
380
 
366
- if (req.method == "HEAD") return res.end('')
367
- else if (!req.subscribe) return res.end(result.body)
368
- else {
369
- // If no immediate update was sent,
370
- // get the node http code to send headers
371
- if (!result.sent) res.write('\n\n')
372
- }
373
- } else if (req.method === 'PUT') {
374
- // Handle PUT request to update binary files
375
- var event = await braid_blob.put(options.key, body, {
376
- version: req.version,
377
- content_type: req.headers['content-type'],
378
- peer: req.peer
379
- })
380
- res.setHeader("Version", version_to_header(event != null ? [event] : []))
381
- res.end('')
382
- } else if (req.method === 'DELETE') {
383
- await braid_blob.delete(options.key)
384
- res.statusCode = 204 // No Content
385
- res.end('')
381
+ if (req.method == "HEAD") return res.end('')
382
+ else if (!req.subscribe) return res.end(result.body)
383
+ else {
384
+ // If no immediate update was sent,
385
+ // get the node http code to send headers
386
+ if (!result.sent) res.write('\n\n')
386
387
  }
387
- })
388
+ } else if (req.method === 'PUT') {
389
+ // Handle PUT request to update binary files
390
+ var event = await braid_blob.put(options.key, body, {
391
+ version: req.version,
392
+ content_type: req.headers['content-type'],
393
+ peer: req.peer
394
+ })
395
+ res.setHeader("Version", version_to_header(event != null ? [event] : []))
396
+ res.end('')
397
+ } else if (req.method === 'DELETE') {
398
+ await braid_blob.delete(options.key, {
399
+ content_type: req.headers['content-type'],
400
+ peer: req.peer
401
+ })
402
+ res.end('')
403
+ }
388
404
  }
389
405
 
390
406
  braid_blob.sync = (a, b, options = {}) => {
@@ -392,175 +408,131 @@ function create_braid_blob() {
392
408
  if (!options.peer) options.peer = Math.random().toString(36).slice(2)
393
409
 
394
410
  if ((a instanceof URL) === (b instanceof URL)) {
395
- // Both are URLs or both are local keys
396
- var a_first_put, b_first_put
397
- var a_first_put_promise = new Promise(done => a_first_put = done)
398
- var b_first_put_promise = new Promise(done => b_first_put = done)
399
-
400
- var a_ops = {
401
- signal: options.signal,
402
- headers: options.headers,
403
- content_type: options.content_type,
404
- peer: options.peer,
405
- subscribe: update => {
406
- braid_blob.put(b, update.body, {
407
- signal: options.signal,
411
+ braid_blob.get(a, {
412
+ ...options,
413
+ subscribe: async update => {
414
+ if (update.delete) await braid_blob.delete(b, {
415
+ ...options,
416
+ content_type: update.content_type,
417
+ })
418
+ else await braid_blob.put(b, update.body, {
419
+ ...options,
408
420
  version: update.version,
409
- headers: options.headers,
410
421
  content_type: update.content_type,
411
- peer: options.peer,
412
- }).then(a_first_put)
422
+ })
413
423
  }
414
- }
415
- braid_blob.get(a, a_ops).then(x =>
416
- x || b_first_put_promise.then(() =>
417
- braid_blob.get(a, a_ops)))
418
-
419
- var b_ops = {
420
- signal: options.signal,
421
- headers: options.headers,
422
- content_type: options.content_type,
423
- peer: options.peer,
424
- subscribe: update => {
425
- braid_blob.put(a, update.body, {
426
- signal: options.signal,
424
+ })
425
+ braid_blob.get(b, {
426
+ ...options,
427
+ subscribe: async update => {
428
+ if (update.delete) await braid_blob.delete(a, {
429
+ ...options,
430
+ content_type: update.content_type,
431
+ })
432
+ else await braid_blob.put(a, update.body, {
433
+ ...options,
427
434
  version: update.version,
428
- headers: options.headers,
429
435
  content_type: update.content_type,
430
- peer: options.peer,
431
- }).then(b_first_put)
436
+ })
432
437
  }
433
- }
434
- braid_blob.get(b, b_ops).then(x =>
435
- x || a_first_put_promise.then(() =>
436
- braid_blob.get(b, b_ops)))
438
+ })
437
439
  } else {
438
440
  // One is local, one is remote - make a=local and b=remote (swap if not)
439
441
  if (a instanceof URL) {
440
442
  let swap = a; a = b; b = swap
441
443
  }
442
444
 
443
- var closed = false
444
- var disconnect = () => { }
445
- options.signal?.addEventListener('abort', () =>
446
- { closed = true; disconnect() })
447
-
448
- var local_first_put, remote_first_put
449
- var local_first_put_promise = new Promise(done => local_first_put = done)
450
- var remote_first_put_promise = new Promise(done => remote_first_put = done)
445
+ var ac = new AbortController()
446
+ options.signal?.addEventListener('abort', () => ac.abort())
451
447
 
452
448
  function handle_error(e) {
453
- if (closed) return
454
- disconnect()
449
+ if (ac.signal.aborted) return
455
450
  console.log(`disconnected, retrying in 1 second`)
456
451
  setTimeout(connect, 1000)
457
452
  }
458
453
 
459
454
  async function connect() {
455
+ if (ac.signal.aborted) return
460
456
  if (options.on_pre_connect) await options.on_pre_connect()
461
457
 
462
- var ac = new AbortController()
463
- disconnect = () => ac.abort()
464
-
465
458
  try {
466
459
  // Check if remote has our current version (simple fork-point check)
467
- var local_result = await braid_blob.get(a, {
468
- signal: ac.signal,
469
- head: true,
470
- headers: options.headers,
471
- content_type: options.content_type,
472
- peer: options.peer,
473
- })
474
- var local_version = local_result ? local_result.version : null
475
460
  var server_has_our_version = false
476
-
461
+ var local_version = (await braid_blob.get(a, {
462
+ ...options,
463
+ signal: ac.signal,
464
+ head: true
465
+ }))?.version
477
466
  if (local_version) {
478
467
  var r = await braid_blob.get(b, {
468
+ ...options,
479
469
  signal: ac.signal,
480
470
  head: true,
481
471
  dont_retry: true,
482
472
  version: local_version,
483
- headers: options.headers,
484
- content_type: options.content_type,
485
- peer: options.peer,
486
473
  })
487
474
  server_has_our_version = !!r
488
475
  }
489
476
 
490
- // Local -> remote: subscribe to future local changes
491
- var a_ops = {
477
+ // Local -> remote
478
+ await braid_blob.get(a, {
479
+ ...options,
492
480
  signal: ac.signal,
493
- headers: options.headers,
494
- content_type: options.content_type,
495
- peer: options.peer,
481
+ parents: server_has_our_version ? local_version : null,
496
482
  subscribe: async update => {
497
483
  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)
484
+ if (update.delete) {
485
+ var x = await braid_blob.delete(b, {
486
+ ...options,
487
+ signal: ac.signal,
488
+ dont_retry: true,
489
+ content_type: update.content_type,
490
+ })
491
+ if (!x.ok) handle_error(new Error('failed to delete'))
492
+ } else {
493
+ var x = await braid_blob.put(b, update.body, {
494
+ ...options,
495
+ signal: ac.signal,
496
+ dont_retry: true,
497
+ version: update.version,
498
+ content_type: update.content_type,
499
+ })
500
+ if ((x.status === 401 || x.status === 403) && options.on_unauthorized) {
501
+ await options.on_unauthorized?.()
502
+ } else if (!x.ok) handle_error(new Error('failed to PUT: ' + x.status))
503
+ }
510
504
  } catch (e) {
511
- if (e.name !== 'AbortError') throw e
505
+ if (e.name !== 'AbortError')
506
+ handle_error(e)
512
507
  }
513
508
  }
514
- }
515
- // Only set parents if server already has our version
516
- // If server doesn't have it, omit parents so subscription sends everything
517
- if (server_has_our_version) {
518
- a_ops.parents = local_version
519
- }
509
+ })
520
510
 
521
- // Remote -> local: subscribe to remote updates
522
- var b_ops = {
511
+ // Remote -> local
512
+ var remote_res = await braid_blob.get(b, {
513
+ ...options,
523
514
  signal: ac.signal,
524
515
  dont_retry: true,
525
- headers: options.headers,
526
- content_type: options.content_type,
527
- peer: options.peer,
516
+ parents: local_version,
528
517
  subscribe: async update => {
529
- await braid_blob.put(a, update.body, {
518
+ if (update.delete) await braid_blob.delete(a, {
519
+ ...options,
520
+ signal: ac.signal,
521
+ content_type: update.content_type,
522
+ })
523
+ else await braid_blob.put(a, update.body, {
524
+ ...options,
525
+ signal: ac.signal,
530
526
  version: update.version,
531
- headers: options.headers,
532
527
  content_type: update.content_type,
533
- peer: options.peer,
534
528
  })
535
- remote_first_put()
536
529
  },
537
530
  on_error: e => {
538
531
  options.on_disconnect?.()
539
532
  handle_error(e)
540
533
  }
541
- }
542
- // Use fork-point (parents) to avoid receiving data we already have
543
- if (local_version) {
544
- b_ops.parents = local_version
545
- }
546
-
547
- // Set up both subscriptions, handling cases where one doesn't exist yet
548
- braid_blob.get(a, a_ops).then(x =>
549
- x || remote_first_put_promise.then(() =>
550
- braid_blob.get(a, a_ops)))
551
-
552
- var remote_res = await braid_blob.get(b, b_ops)
553
-
554
- // If remote doesn't exist yet, wait for it to be created then reconnect
555
- if (!remote_res) {
556
- await local_first_put_promise
557
- disconnect()
558
- connect()
559
- }
560
-
534
+ })
561
535
  options.on_res?.(remote_res)
562
-
563
- // Otherwise, on_error will call handle_error when connection drops
564
536
  } catch (e) {
565
537
  handle_error(e)
566
538
  }
@@ -570,24 +542,55 @@ function create_braid_blob() {
570
542
  }
571
543
 
572
544
  function compare_events(a, b) {
573
- var a_num = get_event_seq(a)
574
- var b_num = get_event_seq(b)
575
-
576
- var c = a_num.length - b_num.length
577
- if (c) return c
545
+ if (!a) a = ''
546
+ if (!b) b = ''
578
547
 
579
- var c = a_num.localeCompare(b_num)
548
+ var c = compare_seqs(get_event_seq(a), get_event_seq(b))
580
549
  if (c) return c
581
550
 
582
- return a.localeCompare(b)
551
+ if (a < b) return -1
552
+ if (a > b) return 1
553
+ return 0
583
554
  }
584
555
 
585
556
  function get_event_seq(e) {
557
+ if (!e) return ''
558
+
586
559
  for (let i = e.length - 1; i >= 0; i--)
587
560
  if (e[i] === '-') return e.slice(i + 1)
588
561
  return e
589
562
  }
590
563
 
564
+ function increment_seq(s) {
565
+ if (!s) return '1'
566
+
567
+ let last = s[s.length - 1]
568
+ let rest = s.slice(0, -1)
569
+
570
+ if (last >= '0' && last <= '8')
571
+ return rest + String.fromCharCode(last.charCodeAt(0) + 1)
572
+ else
573
+ return increment_seq(rest) + '0'
574
+ }
575
+
576
+ function max_seq(a, b) {
577
+ if (!a) a = ''
578
+ if (!b) b = ''
579
+
580
+ if (compare_seqs(a, b) > 0) return a
581
+ return b
582
+ }
583
+
584
+ function compare_seqs(a, b) {
585
+ if (!a) a = ''
586
+ if (!b) b = ''
587
+
588
+ if (a.length !== b.length) return a.length - b.length
589
+ if (a < b) return -1
590
+ if (a > b) return 1
591
+ return 0
592
+ }
593
+
591
594
  function ascii_ify(s) {
592
595
  return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
593
596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
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
@@ -378,7 +378,7 @@ runTest(
378
378
 
379
379
  return r.status
380
380
  },
381
- '204'
381
+ '200'
382
382
  )
383
383
 
384
384
  runTest(
@@ -431,45 +431,35 @@ runTest(
431
431
  )
432
432
 
433
433
  runTest(
434
- "test braid_blob.delete() cleans up subscriptions",
434
+ "test that aborting cleans up subscription",
435
435
  async () => {
436
436
  var r1 = await braid_fetch(`/eval`, {
437
437
  method: 'POST',
438
438
  body: `void (async () => {
439
- var test_id = 'test-db-' + Math.random().toString(36).slice(2)
440
- var db_folder = __dirname + '/' + test_id + '-db'
441
- var meta_folder = __dirname + '/' + test_id + '-meta'
439
+ var test_id = '/test-' + Math.random().toString(36).slice(2)
442
440
 
443
- var bb = braid_blob.create_braid_blob()
444
- bb.db_folder = db_folder
445
- bb.meta_folder = meta_folder
441
+ // Put a file
442
+ await braid_blob.put(test_id, 'hello')
446
443
 
447
- try {
448
- // Put a file
449
- await bb.put('/test-file', Buffer.from('hello'))
450
-
451
- // Subscribe to it
452
- var got_update = false
453
- await bb.get('/test-file', {
454
- subscribe: (update) => { got_update = true }
455
- })
444
+ // Subscribe to it
445
+ var got_update = false
446
+ var ac = new AbortController()
447
+ await braid_blob.get(test_id, {
448
+ signal: ac.signal,
449
+ subscribe: (update) => { got_update = true }
450
+ })
456
451
 
457
- // Verify subscription exists
458
- var has_sub_before = !!bb.key_to_subs['/test-file']
452
+ // Verify subscription exists
453
+ var has_sub_before = !!braid_blob.key_to_subs[test_id]
459
454
 
460
- // Delete it
461
- await bb.delete('/test-file')
455
+ await new Promise(done => setTimeout(done, 30))
456
+ ac.abort()
457
+ await new Promise(done => setTimeout(done, 30))
462
458
 
463
- // Verify subscription is cleaned up
464
- var has_sub_after = !!bb.key_to_subs['/test-file']
459
+ // Verify subscription is cleaned up
460
+ var has_sub_after = !!braid_blob.key_to_subs[test_id]
465
461
 
466
- res.end('' + (has_sub_before && !has_sub_after))
467
- } catch (e) {
468
- res.end('error: ' + e.message)
469
- } finally {
470
- await require('fs').promises.rm(db_folder, { recursive: true, force: true })
471
- await require('fs').promises.rm(meta_folder, { recursive: true, force: true })
472
- }
462
+ res.end('' + (has_sub_before && !has_sub_after))
473
463
  })()`
474
464
  })
475
465
  return await r1.text()
@@ -640,19 +630,6 @@ runTest(
640
630
  'false'
641
631
  )
642
632
 
643
- runTest(
644
- "test that subscribe sends 404 if there is no file.",
645
- async () => {
646
- var key = 'test-' + Math.random().toString(36).slice(2)
647
-
648
- var r = await braid_fetch(`/${key}`, {
649
- subscribe: true,
650
- })
651
- return r.status
652
- },
653
- '404'
654
- )
655
-
656
633
  runTest(
657
634
  "test that we get 404 when file doesn't exist, on GET without subscribe.",
658
635
  async () => {
@@ -1072,8 +1049,7 @@ runTest(
1072
1049
  method: 'POST',
1073
1050
  body: `void (async () => {
1074
1051
  try {
1075
- var braid_blob = require(\`\${__dirname}/../index.js\`)
1076
- var remote_url = new URL('http://localhost:' + req.socket.localPort + '${remote_key}')
1052
+ var remote_url = new URL('http://localhost:' + port + '${remote_key}')
1077
1053
 
1078
1054
  // Start sync with URL as first argument (should swap internally)
1079
1055
  braid_blob.sync(remote_url, '${local_key}')
@@ -1165,10 +1141,8 @@ runTest(
1165
1141
  method: 'POST',
1166
1142
  body: `void (async () => {
1167
1143
  try {
1168
- var braid_blob = require(\`\${__dirname}/../index.js\`)
1169
-
1170
1144
  // 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'] })
1145
+ await braid_blob.put('${local_key}', 'same content', { version: ['same-version-123'] })
1172
1146
 
1173
1147
  // Wrap db.read to count calls for our specific key
1174
1148
  var read_count = 0
@@ -1178,7 +1152,7 @@ runTest(
1178
1152
  return original_read.call(this, key)
1179
1153
  }
1180
1154
 
1181
- var remote_url = new URL('http://localhost:' + req.socket.localPort + '/${remote_key}')
1155
+ var remote_url = new URL('http://localhost:' + port + '/${remote_key}')
1182
1156
 
1183
1157
  // Create an AbortController to stop the sync
1184
1158
  var ac = new AbortController()
@@ -1305,7 +1279,6 @@ runTest(
1305
1279
  // Try to subscribe with parents 200 (newer than what server has)
1306
1280
  // This triggers the "unknown version" error which gets caught and returns 309
1307
1281
  var r = await braid_fetch(`/${key}`, {
1308
- subscribe: true,
1309
1282
  parents: ['200']
1310
1283
  })
1311
1284
 
@@ -1537,38 +1510,22 @@ runTest(
1537
1510
  var r1 = await braid_fetch(`/eval`, {
1538
1511
  method: 'POST',
1539
1512
  body: `void (async () => {
1540
- var fs = require('fs').promises
1541
- var test_id = 'test-abort-get-' + Math.random().toString(36).slice(2)
1542
- var db_folder = __dirname + '/' + test_id + '-db'
1543
- var meta_folder = __dirname + '/' + test_id + '-meta'
1544
-
1545
- try {
1546
- var bb = braid_blob.create_braid_blob()
1547
- bb.db_folder = db_folder
1548
- bb.meta_folder = meta_folder
1513
+ var test_id = '/test-abort-get-' + Math.random().toString(36).slice(2)
1549
1514
 
1550
- // Put a file first
1551
- await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
1515
+ // Put a file first
1516
+ await braid_blob.put(test_id, 'hello', { version: ['1'] })
1552
1517
 
1553
- // Create an already-aborted signal
1554
- var ac = new AbortController()
1555
- ac.abort()
1518
+ // Create an already-aborted signal
1519
+ var ac = new AbortController()
1520
+ ac.abort()
1556
1521
 
1557
- // Try to get with aborted signal (after header_cb)
1558
- var header_called = false
1559
- var result = await bb.get('/test-file', {
1560
- signal: ac.signal,
1561
- header_cb: () => { header_called = true }
1562
- })
1522
+ // Try to get with aborted signal (after header_cb)
1523
+ var result = await braid_blob.get(test_id, {
1524
+ signal: ac.signal,
1525
+ })
1563
1526
 
1564
- // Result should be undefined since operation was aborted after header_cb
1565
- res.end(header_called && result === undefined ? 'aborted' : 'not aborted: header=' + header_called + ' result=' + JSON.stringify(result))
1566
- } catch (e) {
1567
- res.end('error: ' + e.message)
1568
- } finally {
1569
- await fs.rm(db_folder, { recursive: true, force: true })
1570
- await fs.rm(meta_folder, { recursive: true, force: true })
1571
- }
1527
+ // Result should be undefined since operation was aborted already
1528
+ res.end(result === undefined ? 'aborted' : 'not aborted')
1572
1529
  })()`
1573
1530
  })
1574
1531
  return await r1.text()