braid-text 0.0.14 → 0.0.16

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 CHANGED
@@ -10,7 +10,7 @@ let braid_text = {
10
10
  let waiting_puts = 0
11
11
  let prev_put_p = null
12
12
 
13
- let max_encoded_key_size = 170
13
+ let max_encoded_key_size = 240
14
14
 
15
15
  braid_text.serve = async (req, res, options = {}) => {
16
16
  options = {
@@ -19,12 +19,32 @@ braid_text.serve = async (req, res, options = {}) => {
19
19
  ...options // Override with all options passed in
20
20
  }
21
21
 
22
- let resource = await get_resource(options.key)
22
+ // free CORS
23
+ res.setHeader("Access-Control-Allow-Origin", "*")
24
+ res.setHeader("Access-Control-Allow-Methods", "*")
25
+ res.setHeader("Access-Control-Allow-Headers", "*")
26
+ res.setHeader("Access-Control-Expose-Headers", "*")
27
+
28
+ function my_end(statusCode, x) {
29
+ res.statusCode = statusCode
30
+ res.end(x ?? '')
31
+ }
32
+
33
+ let resource = null
34
+ try {
35
+ resource = await get_resource(options.key)
23
36
 
24
- braidify(req, res)
37
+ braidify(req, res)
38
+ } catch (e) {
39
+ return my_end(400, "The server failed to process this request. The error generated was: " + e)
40
+ }
25
41
 
26
42
  let peer = req.headers["peer"]
27
43
 
44
+ let merge_type = req.headers["merge-type"]
45
+ if (!merge_type) merge_type = 'simpleton'
46
+ if (merge_type !== 'simpleton' && merge_type !== 'dt') return my_end(400, `Unknown merge type: ${merge_type}`)
47
+
28
48
  // set default content type of text/plain
29
49
  if (!res.getHeader('content-type')) res.setHeader('Content-Type', 'text/plain')
30
50
 
@@ -43,17 +63,6 @@ braid_text.serve = async (req, res, options = {}) => {
43
63
  res.setHeader('Content-Type', updatedContentType);
44
64
  }
45
65
 
46
- // free CORS
47
- res.setHeader("Access-Control-Allow-Origin", "*")
48
- res.setHeader("Access-Control-Allow-Methods", "*")
49
- res.setHeader("Access-Control-Allow-Headers", "*")
50
- res.setHeader("Access-Control-Expose-Headers", "*")
51
-
52
- function my_end(statusCode, x) {
53
- res.statusCode = statusCode
54
- res.end(x ?? '')
55
- }
56
-
57
66
  if (req.method == "OPTIONS") return my_end(200)
58
67
 
59
68
  if (req.method == "DELETE") {
@@ -65,7 +74,12 @@ braid_text.serve = async (req, res, options = {}) => {
65
74
  if (!req.subscribe) {
66
75
  res.setHeader("Accept-Subscribe", "true")
67
76
 
68
- let x = await braid_text.get(resource, { version: req.version, parents: req.parents })
77
+ let x = null
78
+ try {
79
+ x = await braid_text.get(resource, { version: req.version, parents: req.parents })
80
+ } catch (e) {
81
+ return my_end(400, "The server failed to get something. The error generated was: " + e)
82
+ }
69
83
 
70
84
  res.setHeader("Version", x.version.map((x) => JSON.stringify(x)).join(", "))
71
85
 
@@ -77,26 +91,30 @@ braid_text.serve = async (req, res, options = {}) => {
77
91
  return my_end(200, buffer)
78
92
  } else {
79
93
  res.setHeader("Editable", "true")
80
- res.setHeader("Merge-Type", req.headers["merge-type"] === "dt" ? "dt" : "simpleton")
94
+ res.setHeader("Merge-Type", merge_type)
81
95
  if (req.method == "HEAD") return my_end(200)
82
96
 
83
97
  let options = {
84
98
  peer,
85
99
  version: req.version,
86
100
  parents: req.parents,
87
- merge_type: req.headers["merge-type"],
101
+ merge_type,
88
102
  subscribe: x => res.sendVersion(x),
89
103
  write: (x) => res.write(x)
90
104
  }
91
105
 
92
106
  res.startSubscription({
93
107
  onClose: () => {
94
- if (req.headers["merge-type"] === "dt") resource.clients.delete(options)
108
+ if (merge_type === "dt") resource.clients.delete(options)
95
109
  else resource.simpleton_clients.delete(options)
96
110
  }
97
111
  })
98
112
 
99
- return braid_text.get(resource, options)
113
+ try {
114
+ return await braid_text.get(resource, options)
115
+ } catch (e) {
116
+ return my_end(400, "The server failed to get something. The error generated was: " + e)
117
+ }
100
118
  }
101
119
  }
102
120
 
@@ -136,7 +154,7 @@ braid_text.serve = async (req, res, options = {}) => {
136
154
  patches = null
137
155
  }
138
156
 
139
- await braid_text.put(resource, { peer, version: req.version, parents: req.parents, patches, body, merge_type: req.headers["merge-type"] })
157
+ await braid_text.put(resource, { peer, version: req.version, parents: req.parents, patches, body, merge_type })
140
158
 
141
159
  options.put_cb(options.key, resource.doc.get())
142
160
  } catch (e) {
@@ -165,7 +183,7 @@ braid_text.serve = async (req, res, options = {}) => {
165
183
  // - 428 Precondition Required
166
184
  // - pros: the name sounds right
167
185
  // - cons: typically implies that the request was missing an http conditional field like If-Match. that is to say, it implies that the request is missing a precondition, not that the server is missing a precondition
168
- return done_my_turn(425, "The server failed to apply this version.")
186
+ return done_my_turn(425, "The server failed to apply this version. The error generated was: " + e)
169
187
  }
170
188
 
171
189
  return done_my_turn(200)
@@ -183,6 +201,9 @@ braid_text.get = async (key, options) => {
183
201
  return (await get_resource(key)).doc.get()
184
202
  }
185
203
 
204
+ if (options.version) validate_version_array(options.version)
205
+ if (options.parents) validate_version_array(options.parents)
206
+
186
207
  let resource = (typeof key == 'string') ? await get_resource(key) : key
187
208
 
188
209
  if (!options.subscribe) {
@@ -249,12 +270,12 @@ braid_text.get = async (key, options) => {
249
270
  } else doc = resource.doc
250
271
 
251
272
  return {
252
- version: doc.getRemoteVersion().map((x) => encode_version(...x)),
273
+ version: doc.getRemoteVersion().map((x) => x.join("-")),
253
274
  body: doc.get()
254
275
  }
255
276
  } else {
256
277
  if (options.merge_type != "dt") {
257
- let version = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
278
+ let version = resource.doc.getRemoteVersion().map((x) => x.join("-"))
258
279
  let x = { version }
259
280
 
260
281
  if (!options.parents && !options.version) {
@@ -286,7 +307,7 @@ braid_text.get = async (key, options) => {
286
307
 
287
308
  if (!options.parents && !options.version) {
288
309
  options.subscribe({
289
- version: ["root"],
310
+ version: [],
290
311
  parents: [],
291
312
  body: "",
292
313
  })
@@ -330,7 +351,13 @@ braid_text.get = async (key, options) => {
330
351
  }
331
352
 
332
353
  braid_text.put = async (key, options) => {
333
- let { version, patches, body } = options
354
+ let { version, patches, body, peer } = options
355
+
356
+ if (version) validate_version_array(version)
357
+ if (options.parents) validate_version_array(options.parents)
358
+ if (body != null && patches) throw new Error(`cannot have a body and patches`)
359
+ if (body != null && (typeof body !== 'string')) throw new Error(`body must be a string`)
360
+ if (patches) validate_patches(patches)
334
361
 
335
362
  let resource = (typeof key == 'string') ? await get_resource(key) : key
336
363
 
@@ -346,21 +373,34 @@ braid_text.put = async (key, options) => {
346
373
  patches = patches.map((p) => ({
347
374
  ...p,
348
375
  range: p.range.match(/\d+/g).map((x) => parseInt(x)),
349
- ...(p.content ? { content: [...p.content] } : {}),
350
- }))
376
+ content: [...p.content],
377
+ })).sort((a, b) => a.range[0] - b.range[0])
378
+
379
+ // validate patch positions
380
+ let max_pos = resource.doc.get().length
381
+ let must_be_at_least = 0
382
+ for (let p of patches) {
383
+ if (p.range[0] < must_be_at_least || p.range[0] > max_pos) throw new Error(`invalid patch range position: ${p.range[0]}`)
384
+ if (p.range[1] < p.range[0] || p.range[1] > max_pos) throw new Error(`invalid patch range position: ${p.range[1]}`)
385
+ must_be_at_least = p.range[1]
386
+ }
351
387
 
352
388
  let change_count = patches.reduce((a, b) => a + b.content.length + (b.range[1] - b.range[0]), 0)
353
389
 
354
- let og_v = version?.[0] || `${Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
390
+ let og_v = version?.[0] || `${(is_valid_actor(peer) && peer) || Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
355
391
 
356
392
  // reduce the version sequence by the number of char-edits
357
393
  let v = decode_version(og_v)
358
- v = encode_version(v[0], v[1] + 1 - change_count)
359
394
 
360
- let parents = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
395
+ // validate version: make sure we haven't seen it already
396
+ if (v[1] <= (resource.actor_seqs[v[0]] ?? -1)) throw new Error(`invalid version: already processed`)
397
+ resource.actor_seqs[v[0]] = v[1]
398
+
399
+ v = `${v[0]}-${v[1] + 1 - change_count}`
400
+
401
+ let parents = resource.doc.getRemoteVersion().map((x) => x.join("-"))
361
402
  let og_parents = options.parents || parents
362
403
  let ps = og_parents
363
- if (!ps.length) ps = ["root"]
364
404
 
365
405
  let v_before = resource.doc.getLocalVersion()
366
406
 
@@ -374,7 +414,7 @@ braid_text.put = async (key, options) => {
374
414
  offset--
375
415
  ps = [v]
376
416
  v = decode_version(v)
377
- v = encode_version(v[0], v[1] + 1)
417
+ v = `${v[0]}-${v[1] + 1}`
378
418
  }
379
419
  // insert
380
420
  for (let i = 0; i < p.content?.length ?? 0; i++) {
@@ -383,7 +423,7 @@ braid_text.put = async (key, options) => {
383
423
  offset++
384
424
  ps = [v]
385
425
  v = decode_version(v)
386
- v = encode_version(v[0], v[1] + 1)
426
+ v = `${v[0]}-${v[1] + 1}`
387
427
  }
388
428
  }
389
429
 
@@ -401,17 +441,17 @@ braid_text.put = async (key, options) => {
401
441
  patches = get_xf_patches(resource.doc, v_before)
402
442
  console.log(JSON.stringify({ patches }))
403
443
 
404
- let version = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
444
+ let version = resource.doc.getRemoteVersion().map((x) => x.join("-"))
405
445
 
406
446
  for (let client of resource.simpleton_clients) {
407
- if (client.peer == options.peer) {
447
+ if (client.peer == peer) {
408
448
  client.my_last_seen_version = [og_v]
409
449
  }
410
450
 
411
451
  function set_timeout(time_override) {
412
452
  if (client.my_timeout) clearTimeout(client.my_timeout)
413
453
  client.my_timeout = setTimeout(() => {
414
- let version = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
454
+ let version = resource.doc.getRemoteVersion().map((x) => x.join("-"))
415
455
  let x = { version }
416
456
  x.parents = client.my_last_seen_version
417
457
 
@@ -428,7 +468,7 @@ braid_text.put = async (key, options) => {
428
468
  }
429
469
 
430
470
  if (client.my_timeout) {
431
- if (client.peer == options.peer) {
471
+ if (client.peer == peer) {
432
472
  if (!v_eq(client.my_last_sent_version, og_parents)) {
433
473
  // note: we don't add to client.my_unused_version_count,
434
474
  // because we're already in a timeout;
@@ -444,7 +484,7 @@ braid_text.put = async (key, options) => {
444
484
  }
445
485
 
446
486
  let x = { version }
447
- if (client.peer == options.peer) {
487
+ if (client.peer == peer) {
448
488
  if (!v_eq(client.my_last_sent_version, og_parents)) {
449
489
  client.my_unused_version_count = (client.my_unused_version_count ?? 0) + 1
450
490
  set_timeout()
@@ -491,7 +531,7 @@ braid_text.put = async (key, options) => {
491
531
  patches: og_patches,
492
532
  }
493
533
  for (let client of resource.clients) {
494
- if (client.peer != options.peer) client.subscribe(x)
534
+ if (client.peer != peer) client.subscribe(x)
495
535
  }
496
536
 
497
537
  await resource.db_delta(resource.doc.getPatchSince(v_before))
@@ -499,13 +539,12 @@ braid_text.put = async (key, options) => {
499
539
 
500
540
  braid_text.list = async () => {
501
541
  try {
502
- var pages = new Set()
503
- for (let x of await require('fs').promises.readdir(braid_text.db_folder)) {
504
- let k = x.replace(/\.\w+$/, '')
505
- if (k.length <= max_encoded_key_size) pages.add(decode_filename(k))
506
- else if (x.endsWith('.name')) pages.add(await require('fs').promises.readFile(`${braid_text.db_folder}/${x}`, { encoding: 'utf8' }))
507
- }
508
- return [...pages.keys()]
542
+ if (braid_text.db_folder) {
543
+ await db_folder_init()
544
+ var pages = new Set()
545
+ for (let x of await require('fs').promises.readdir(braid_text.db_folder)) pages.add(decode_filename(x.replace(/\.\w+$/, '')))
546
+ return [...pages.keys()]
547
+ } else return Object.keys(get_resource.cache)
509
548
  } catch (e) { return [] }
510
549
  }
511
550
 
@@ -530,6 +569,13 @@ async function get_resource(key) {
530
569
  resource.doc = defrag_dt(resource.doc)
531
570
  resource.need_defrag = false
532
571
 
572
+ resource.actor_seqs = {}
573
+ let max_version = resource.doc.getLocalVersion()[0] ?? -1
574
+ for (let i = 0; i <= max_version; i++) {
575
+ let v = resource.doc.localToRemoteVersion([i])[0]
576
+ resource.actor_seqs[v[0]] = Math.max(v[1], resource.actor_seqs[v[0]] ?? -1)
577
+ }
578
+
533
579
  resource.delete_me = () => {
534
580
  delete_me()
535
581
  delete cache[key]
@@ -538,7 +584,54 @@ async function get_resource(key) {
538
584
  return (cache[key] = resource)
539
585
  }
540
586
 
587
+ async function db_folder_init() {
588
+ console.log('__!')
589
+ if (!db_folder_init.p) db_folder_init.p = new Promise(async done => {
590
+ await fs.promises.mkdir(braid_text.db_folder, { recursive: true });
591
+
592
+ // 0.0.13 -> 0.0.14
593
+ // look for files with key-encodings over max_encoded_key_size,
594
+ // and convert them using the new method
595
+ // for (let x of await fs.promises.readdir(braid_text.db_folder)) {
596
+ // let k = x.replace(/(_[0-9a-f]{64})?\.\w+$/, '')
597
+ // if (k.length > max_encoded_key_size) {
598
+ // k = decode_filename(k)
599
+
600
+ // await fs.promises.rename(`${braid_text.db_folder}/${x}`, `${braid_text.db_folder}/${encode_filename(k)}${x.match(/\.\w+$/)[0]}`)
601
+ // await fs.promises.writeFile(`${braid_text.db_folder}/${encode_filename(k)}.name`, k)
602
+ // }
603
+ // }
604
+
605
+ // 0.0.14 -> 0.0.15
606
+ // basically convert the 0.0.14 files back
607
+ let convert_us = {}
608
+ for (let x of await fs.promises.readdir(braid_text.db_folder)) {
609
+ if (x.endsWith('.name')) {
610
+ let encoded = convert_us[x.slice(0, -'.name'.length)] = encode_filename(await fs.promises.readFile(`${braid_text.db_folder}/${x}`, { encoding: 'utf8' }))
611
+ if (encoded.length > max_encoded_key_size) {
612
+ console.log(`trying to convert file to new format, but the key is too big: ${braid_text.db_folder}/${x}`)
613
+ process.exit()
614
+ }
615
+ console.log(`deleting: ${braid_text.db_folder}/${x}`)
616
+ await fs.promises.unlink(`${braid_text.db_folder}/${x}`)
617
+ }
618
+ }
619
+ if (Object.keys(convert_us).length) {
620
+ for (let x of await fs.promises.readdir(braid_text.db_folder)) {
621
+ let [_, k, num] = x.match(/^(.*)\.(\d+)$/s)
622
+ if (!convert_us[k]) continue
623
+ console.log(`renaming: ${braid_text.db_folder}/${x} -> ${braid_text.db_folder}/${convert_us[k]}.${num}`)
624
+ if (convert_us[k]) await fs.promises.rename(`${braid_text.db_folder}/${x}`, `${braid_text.db_folder}/${convert_us[k]}.${num}`)
625
+ }
626
+ }
627
+
628
+ done()
629
+ })
630
+ await db_folder_init.p
631
+ }
632
+
541
633
  async function get_files_for_key(key) {
634
+ await db_folder_init()
542
635
  try {
543
636
  let re = new RegExp("^" + encode_filename(key).replace(/[^a-zA-Z0-9]/g, "\\$&") + "\\.\\w+$")
544
637
  return (await fs.promises.readdir(braid_text.db_folder))
@@ -548,31 +641,14 @@ async function get_files_for_key(key) {
548
641
  }
549
642
 
550
643
  async function file_sync(key, process_delta, get_init) {
644
+ let encoded = encode_filename(key)
645
+
646
+ if (encoded.length > max_encoded_key_size) throw new Error(`invalid key: too long (max ${max_encoded_key_size})`)
647
+
551
648
  let currentNumber = 0
552
649
  let currentSize = 0
553
650
  let threshold = 0
554
651
 
555
- // Ensure the existence of db_folder
556
- if (!file_sync.init_p) file_sync.init_p = new Promise(async done => {
557
- await fs.promises.mkdir(braid_text.db_folder, { recursive: true });
558
-
559
- // 0.0.13 -> 0.0.14
560
- // look for files with key-encodings over max_encoded_key_size,
561
- // and convert them using the new method
562
- for (let x of await fs.promises.readdir(braid_text.db_folder)) {
563
- let k = x.replace(/(_[0-9a-f]{64})?\.\w+$/, '')
564
- if (k.length > max_encoded_key_size) {
565
- k = decode_filename(k)
566
-
567
- await fs.promises.rename(`${braid_text.db_folder}/${x}`, `${braid_text.db_folder}/${encode_filename(k)}${x.match(/\.\w+$/)[0]}`)
568
- await fs.promises.writeFile(`${braid_text.db_folder}/${encode_filename(k)}.name`, k)
569
- }
570
- }
571
-
572
- done()
573
- })
574
- await file_sync.init_p
575
-
576
652
  // Read existing files and sort by numbers.
577
653
  const files = (await get_files_for_key(key))
578
654
  .filter(x => x.match(/\.\d+$/))
@@ -617,7 +693,7 @@ async function file_sync(key, process_delta, get_init) {
617
693
  return {
618
694
  change: async (bytes) => {
619
695
  currentSize += bytes.length + 4 // we account for the extra 4 bytes for uint32
620
- const filename = `${braid_text.db_folder}/${encode_filename(key)}.${currentNumber}`
696
+ const filename = `${braid_text.db_folder}/${encoded}.${currentNumber}`
621
697
  if (currentSize < threshold) {
622
698
  console.log(`appending to db..`)
623
699
 
@@ -631,9 +707,6 @@ async function file_sync(key, process_delta, get_init) {
631
707
  try {
632
708
  console.log(`starting new db..`)
633
709
 
634
- let encoded = encode_filename(key)
635
- if (encoded.length > max_encoded_key_size) await fs.promises.writeFile(`${braid_text.db_folder}/${encoded}.name`, key)
636
-
637
710
  currentNumber++
638
711
  const init = get_init()
639
712
  const buffer = Buffer.allocUnsafe(4)
@@ -792,7 +865,7 @@ function parseDT(byte_array) {
792
865
  let num = x >> 2
793
866
 
794
867
  if (x == 1) {
795
- parents.push(["root"])
868
+ // no parents (e.g. parent is "root")
796
869
  } else if (!is_foreign) {
797
870
  parents.push(versions[count - num])
798
871
  } else {
@@ -861,7 +934,7 @@ function OpLog_create_bytes(version, parents, pos, ins) {
861
934
 
862
935
  let agents = new Set()
863
936
  agents.add(version[0])
864
- for (let p of parents) if (p.length > 1) agents.add(p[0])
937
+ for (let p of parents) agents.add(p[0])
865
938
  agents = [...agents]
866
939
 
867
940
  // console.log(JSON.stringify({ agents, parents }, null, 4));
@@ -882,7 +955,7 @@ function OpLog_create_bytes(version, parents, pos, ins) {
882
955
 
883
956
  let branch = []
884
957
 
885
- if (parents[0].length > 1) {
958
+ if (parents.length) {
886
959
  let frontier = []
887
960
 
888
961
  for (let [i, [agent, seq]] of parents.entries()) {
@@ -957,7 +1030,7 @@ function OpLog_create_bytes(version, parents, pos, ins) {
957
1030
 
958
1031
  write_varint(parents_bytes, 1)
959
1032
 
960
- if (parents[0].length > 1) {
1033
+ if (parents.length) {
961
1034
  for (let [i, [agent, seq]] of parents.entries()) {
962
1035
  let has_more = i < parents.length - 1
963
1036
  let agent_i = agent_to_i[agent]
@@ -983,8 +1056,9 @@ function OpLog_remote_to_local(doc, frontier) {
983
1056
  let map = Object.fromEntries(frontier.map((x) => [x, true]))
984
1057
 
985
1058
  let local_version = []
986
- let [agents, versions, parentss] = parseDT([...doc.toBytes()])
987
- for (let i = 0; i < versions.length; i++) {
1059
+
1060
+ let max_version = doc.getLocalVersion()[0] ?? -1
1061
+ for (let i = 0; i <= max_version; i++) {
988
1062
  if (map[doc.localToRemoteVersion([i])[0].join("-")]) {
989
1063
  local_version.push(i)
990
1064
  }
@@ -993,16 +1067,6 @@ function OpLog_remote_to_local(doc, frontier) {
993
1067
  return frontier.length == local_version.length && new Uint32Array(local_version)
994
1068
  }
995
1069
 
996
- function encode_version(agent, seq) {
997
- return agent + "-" + seq
998
- }
999
-
1000
- function decode_version(v) {
1001
- let a = v.split("-")
1002
- if (a.length > 1) a[1] = parseInt(a[1])
1003
- return a
1004
- }
1005
-
1006
1070
  function v_eq(v1, v2) {
1007
1071
  return v1.length == v2.length && v1.every((x, i) => x == v2[i])
1008
1072
  }
@@ -1306,11 +1370,6 @@ function encode_filename(filename) {
1306
1370
  // Encode the filename using encodeURIComponent()
1307
1371
  let encoded = encodeURIComponent(swapped)
1308
1372
 
1309
- // Do something special with a hash if the encoding is too long
1310
- if (encoded.length > max_encoded_key_size) {
1311
- encoded = `${swapped.slice(0, max_encoded_key_size)}_${require('crypto').createHash('sha256').update(filename).digest('hex')}`
1312
- }
1313
-
1314
1373
  return encoded
1315
1374
  }
1316
1375
 
@@ -1324,4 +1383,46 @@ function decode_filename(encodedFilename) {
1324
1383
  return decoded
1325
1384
  }
1326
1385
 
1386
+ function validate_version_array(x) {
1387
+ if (!Array.isArray(x)) throw new Error(`invalid version array: not an array`)
1388
+ for (xx of x) validate_actor_seq(xx)
1389
+ }
1390
+
1391
+ function validate_actor_seq(x) {
1392
+ if (typeof x !== 'string') throw new Error(`invalid actor-seq: not a string`)
1393
+ let [actor, seq] = decode_version(x)
1394
+ validate_actor(actor)
1395
+ }
1396
+
1397
+ function validate_actor(x) {
1398
+ if (typeof x !== 'string') throw new Error(`invalid actor: not a string`)
1399
+ if (Buffer.byteLength(x, 'utf8') >= 50) throw new Error(`actor value too long (max 49): ${x}`) // restriction coming from dt
1400
+ }
1401
+
1402
+ function is_valid_actor(x) {
1403
+ try {
1404
+ validate_actor(x)
1405
+ return true
1406
+ } catch (e) {}
1407
+ }
1408
+
1409
+ function decode_version(v) {
1410
+ let m = v.match(/^(.*)-(\d+)$/s)
1411
+ if (!m) throw new Error(`invalid actor-seq version: ${v}`)
1412
+ return [m[1], parseInt(m[2])]
1413
+ }
1414
+
1415
+ function validate_patches(patches) {
1416
+ if (!Array.isArray(patches)) throw new Error(`invalid patches: not an array`)
1417
+ for (let p of patches) validate_patch(p)
1418
+ }
1419
+
1420
+ function validate_patch(x) {
1421
+ if (typeof x != 'object') throw new Error(`invalid patch: not an object`)
1422
+ if (x.unit && x.unit !== 'text') throw new Error(`invalid patch unit '${x.unit}': only 'text' supported`)
1423
+ if (typeof x.range !== 'string') throw new Error(`invalid patch range: must be a string`)
1424
+ if (!x.range.match(/^\s*\[\s*\d+\s*:\s*\d+\s*\]\s*$/)) throw new Error(`invalid patch range: ${x.range}`)
1425
+ if (typeof x.content !== 'string') throw new Error(`invalid patch content: must be a string`)
1426
+ }
1427
+
1327
1428
  module.exports = braid_text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braidjs",
@@ -24,6 +24,7 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
24
24
  var char_counter = -1
25
25
  var outstanding_changes = 0
26
26
  var max_outstanding_changes = 10
27
+ var ac = new AbortController()
27
28
 
28
29
  braid_fetch_wrapper(url, {
29
30
  headers: { "Merge-Type": "simpleton",
@@ -31,7 +32,8 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
31
32
  subscribe: true,
32
33
  retry: true,
33
34
  parents: () => current_version.length ? current_version : null,
34
- peer
35
+ peer,
36
+ signal: ac.signal
35
37
  }).then(res =>
36
38
  res.subscribe(update => {
37
39
  // Only accept the update if its parents == our current version
@@ -69,6 +71,9 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
69
71
  )
70
72
 
71
73
  return {
74
+ stop: async () => {
75
+ ac.abort()
76
+ },
72
77
  changed: async () => {
73
78
  if (outstanding_changes >= max_outstanding_changes) return
74
79
  while (true) {
@@ -141,6 +146,7 @@ async function braid_fetch_wrapper(url, params) {
141
146
  var subscribe_handler = null
142
147
  connect()
143
148
  async function connect() {
149
+ if (params.signal?.aborted) return
144
150
  try {
145
151
  var c = await braid_fetch(url, { ...params, parents: params.parents?.() })
146
152
  c.subscribe((...args) => subscribe_handler?.(...args), on_error)