braid-blob 0.0.39 → 0.0.41

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 (4) hide show
  1. package/AI-README.md +13 -32
  2. package/index.js +325 -320
  3. package/package.json +1 -1
  4. package/test/tests.js +199 -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 (options.db || 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
137
+
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
+ }
159
150
 
160
- return meta.event
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 (options.db || 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 (options.db || 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 (options.db || 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,173 +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, { head: true })
468
- var local_version = local_result ? local_result.version : null
469
460
  var server_has_our_version = false
470
-
461
+ var local_version = (await braid_blob.get(a, {
462
+ ...options,
463
+ signal: ac.signal,
464
+ head: true
465
+ }))?.version
471
466
  if (local_version) {
472
467
  var r = await braid_blob.get(b, {
468
+ ...options,
473
469
  signal: ac.signal,
474
470
  head: true,
475
471
  dont_retry: true,
476
472
  version: local_version,
477
- headers: options.headers,
478
- content_type: options.content_type,
479
- peer: options.peer,
480
473
  })
481
474
  server_has_our_version = !!r
482
475
  }
483
476
 
484
- // Local -> remote: subscribe to future local changes
485
- var a_ops = {
477
+ // Local -> remote
478
+ await braid_blob.get(a, {
479
+ ...options,
486
480
  signal: ac.signal,
487
- headers: options.headers,
488
- content_type: options.content_type,
489
- peer: options.peer,
481
+ parents: server_has_our_version ? local_version : null,
490
482
  subscribe: async update => {
491
483
  try {
492
- var x = await braid_blob.put(b, update.body, {
493
- signal: ac.signal,
494
- dont_retry: true,
495
- version: update.version,
496
- headers: options.headers,
497
- content_type: update.content_type,
498
- peer: options.peer,
499
- })
500
- if (x.ok) local_first_put()
501
- else if (x.status === 401 || x.status === 403) {
502
- await options.on_unauthorized?.()
503
- } 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
+ }
504
504
  } catch (e) {
505
- if (e.name !== 'AbortError') throw e
505
+ if (e.name !== 'AbortError')
506
+ handle_error(e)
506
507
  }
507
508
  }
508
- }
509
- // Only set parents if server already has our version
510
- // If server doesn't have it, omit parents so subscription sends everything
511
- if (server_has_our_version) {
512
- a_ops.parents = local_version
513
- }
509
+ })
514
510
 
515
- // Remote -> local: subscribe to remote updates
516
- var b_ops = {
511
+ // Remote -> local
512
+ var remote_res = await braid_blob.get(b, {
513
+ ...options,
517
514
  signal: ac.signal,
518
515
  dont_retry: true,
519
- headers: options.headers,
520
- content_type: options.content_type,
521
- peer: options.peer,
516
+ parents: local_version,
522
517
  subscribe: async update => {
523
- 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,
524
526
  version: update.version,
525
- headers: options.headers,
526
527
  content_type: update.content_type,
527
- peer: options.peer,
528
528
  })
529
- remote_first_put()
530
529
  },
531
530
  on_error: e => {
532
531
  options.on_disconnect?.()
533
532
  handle_error(e)
534
533
  }
535
- }
536
- // Use fork-point (parents) to avoid receiving data we already have
537
- if (local_version) {
538
- b_ops.parents = local_version
539
- }
540
-
541
- // Set up both subscriptions, handling cases where one doesn't exist yet
542
- braid_blob.get(a, a_ops).then(x =>
543
- x || remote_first_put_promise.then(async () => {
544
- // update parents, since we know remote has the version we just got from them..
545
- var local_result = await braid_blob.get(a, { head: true })
546
- a_ops.parents = local_result.version
547
- braid_blob.get(a, a_ops)
548
- }))
549
-
550
- var remote_res = await braid_blob.get(b, b_ops)
551
-
552
- // If remote doesn't exist yet, wait for it to be created then reconnect
553
- if (!remote_res) {
554
- await local_first_put_promise
555
- disconnect()
556
- connect()
557
- }
558
-
534
+ })
559
535
  options.on_res?.(remote_res)
560
-
561
- // Otherwise, on_error will call handle_error when connection drops
562
536
  } catch (e) {
563
537
  handle_error(e)
564
538
  }
@@ -568,24 +542,55 @@ function create_braid_blob() {
568
542
  }
569
543
 
570
544
  function compare_events(a, b) {
571
- var a_num = get_event_seq(a)
572
- var b_num = get_event_seq(b)
573
-
574
- var c = a_num.length - b_num.length
575
- if (c) return c
545
+ if (!a) a = ''
546
+ if (!b) b = ''
576
547
 
577
- var c = a_num.localeCompare(b_num)
548
+ var c = compare_seqs(get_event_seq(a), get_event_seq(b))
578
549
  if (c) return c
579
550
 
580
- return a.localeCompare(b)
551
+ if (a < b) return -1
552
+ if (a > b) return 1
553
+ return 0
581
554
  }
582
555
 
583
556
  function get_event_seq(e) {
557
+ if (!e) return ''
558
+
584
559
  for (let i = e.length - 1; i >= 0; i--)
585
560
  if (e[i] === '-') return e.slice(i + 1)
586
561
  return e
587
562
  }
588
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
+
589
594
  function ascii_ify(s) {
590
595
  return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
591
596
  }