braid-text 0.2.114 → 0.2.115

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 (2) hide show
  1. package/index.js +254 -113
  2. package/package.json +4 -1
package/index.js CHANGED
@@ -850,8 +850,9 @@ function create_braid_text() {
850
850
  return await braid_fetch(key.href, params)
851
851
  }
852
852
 
853
- return await within_fiber('put:' + key, async () => {
854
- let resource = (typeof key == 'string') ? await get_resource(key) : key
853
+ let resource = (typeof key == 'string') ? await get_resource(key) : key
854
+
855
+ return await within_fiber('put:' + resource.key, async () => {
855
856
 
856
857
  // support for json patch puts..
857
858
  if (options.patches && options.patches.length &&
@@ -862,7 +863,7 @@ function create_braid_text() {
862
863
  options = { body: JSON.stringify(x, null, 4) }
863
864
  }
864
865
 
865
- let { version, patches, body, peer } = options
866
+ let { version, parents, patches, body, peer } = options
866
867
 
867
868
  if (options.transfer_encoding === 'dt') {
868
869
  var start_i = 1 + resource.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
@@ -896,30 +897,24 @@ function create_braid_text() {
896
897
  if (version && version.length > 1)
897
898
  throw new Error(`cannot put a version with multiple ids`)
898
899
 
899
- // translate a single parent of "root" to the empty array (same meaning)
900
- let options_parents = options.parents
901
- if (options_parents?.length === 1 && options_parents[0] === 'root')
902
- options_parents = []
903
-
904
900
  if (body != null && patches) throw new Error(`cannot have a body and patches`)
905
901
  if (body != null && (typeof body !== 'string')) throw new Error(`body must be a string`)
906
902
  if (patches) validate_patches(patches)
907
903
 
908
- if (options_parents) {
904
+ if (parents) {
909
905
  // make sure we have all these parents
910
- for (let p of options_parents) {
906
+ for (let p of parents) {
911
907
  let P = decode_version(p)
912
908
  if (!resource.actor_seqs[P[0]]?.has(P[1]))
913
909
  throw new Error(`missing parent version: ${p}`)
914
910
  }
915
911
  }
916
912
 
917
- let parents = resource.version
918
- let og_parents = options_parents || parents
913
+ if (!parents) parents = resource.version
914
+
915
+ let max_pos = resource.length_cache.get('' + parents) ??
916
+ (v_eq(resource.version, parents) ? resource.doc.len() : dt_len(resource.doc, parents))
919
917
 
920
- let max_pos = resource.length_cache.get('' + og_parents) ??
921
- (v_eq(parents, og_parents) ? resource.doc.len() : dt_len(resource.doc, og_parents))
922
-
923
918
  if (body != null) {
924
919
  patches = [{
925
920
  unit: 'text',
@@ -928,7 +923,6 @@ function create_braid_text() {
928
923
  }]
929
924
  }
930
925
 
931
- let og_patches = patches
932
926
  patches = patches.map((p) => ({
933
927
  ...p,
934
928
  range: p.range.match(/-?\d+/g).map((x) => {
@@ -940,94 +934,66 @@ function create_braid_text() {
940
934
  content_codepoints: [...p.content],
941
935
  })).sort((a, b) => a.range[0] - b.range[0])
942
936
 
943
- // validate patch positions
944
- let must_be_at_least = 0
945
- for (let p of patches) {
946
- if (p.range[0] < must_be_at_least || p.range[0] > max_pos) throw new Error(`invalid patch range position: ${p.range[0]}`)
947
- if (p.range[1] < p.range[0] || p.range[1] > max_pos) throw new Error(`invalid patch range position: ${p.range[1]}`)
948
- must_be_at_least = p.range[1]
949
- }
950
-
951
937
  let change_count = patches.reduce((a, b) => a + b.content_codepoints.length + (b.range[1] - b.range[0]), 0)
952
938
 
953
- let og_v = version?.[0] || `${(is_valid_actor(peer) && peer) || Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
954
-
955
- let v = decode_version(og_v)
939
+ version = version?.[0] || `${(is_valid_actor(peer) && peer) || Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
956
940
 
957
- resource.length_cache.put(`${v[0]}-${v[1]}`, patches.reduce((a, b) =>
958
- a + (b.content_codepoints?.length ?? 0) - (b.range[1] - b.range[0]),
959
- max_pos))
941
+ let v = decode_version(version)
942
+ var low_seq = v[1] + 1 - change_count
960
943
 
961
- // validate version: make sure we haven't seen it already
962
- if (resource.actor_seqs[v[0]]?.has(v[1])) {
944
+ // make sure we haven't seen this already
945
+ var intersects_range = resource.actor_seqs[v[0]]?.has(low_seq, v[1])
946
+ if (intersects_range) {
947
+ // if low_seq is below the range min,
948
+ // then the intersection has gaps,
949
+ // which is bad, meaning the prior versions must be different,
950
+ // because what we're inserting is contiguous
951
+ if (low_seq < intersects_range[0])
952
+ throw new Error('invalid update: different from previous update with same version')
963
953
 
964
- if (!options.validate_already_seen_versions) return { change_count }
965
-
966
- // if we have seen it already, make sure it's the same as before
967
- let updates = dt_get_patches(resource.doc, og_parents)
968
-
969
- let seen = {}
970
- for (let u of updates) {
971
- u.version = decode_version(u.version)
972
-
973
- if (!u.content) {
974
- // delete
975
- let v = u.version
976
- for (let i = 0; i < u.end - u.start; i++) {
977
- let ps = (i < u.end - u.start - 1) ? [`${v[0]}-${v[1] - i - 1}`] : u.parents
978
- seen[JSON.stringify([v[0], v[1] - i, ps, u.start + i])] = true
979
- }
980
- } else {
981
- // insert
982
- let v = u.version
983
- let content = [...u.content]
984
- for (let i = 0; i < content.length; i++) {
985
- let ps = (i > 0) ? [`${v[0]}-${v[1] - content.length + i}`] : u.parents
986
- seen[JSON.stringify([v[0], v[1] + 1 - content.length + i, ps, u.start + i, content[i]])] = true
987
- }
988
- }
954
+ // see if we only have *some* of the versions
955
+ var new_count = v[1] - intersects_range[1]
956
+ if (new_count > 0) {
957
+ // divide the patches between old and new..
958
+ var new_patches = split_patches(patches, change_count - new_count)
989
959
  }
990
960
 
991
- v = `${v[0]}-${v[1] + 1 - change_count}`
992
- let ps = og_parents
993
- let offset = 0
994
- for (let p of patches) {
995
- // delete
996
- for (let i = p.range[0]; i < p.range[1]; i++) {
997
- let vv = decode_version(v)
961
+ if (options.validate_already_seen_versions)
962
+ validate_old_patches(resource, `${v[0]}-${low_seq}`, parents, patches)
998
963
 
999
- if (!seen[JSON.stringify([vv[0], vv[1], ps, p.range[1] - 1 + offset])]) throw new Error('invalid update: different from previous update with same version')
964
+ if (new_count <= 0) return { change_count }
1000
965
 
1001
- offset--
1002
- ps = [v]
1003
- v = vv
1004
- v = `${v[0]}-${v[1] + 1}`
1005
- }
1006
- // insert
1007
- for (let i = 0; i < p.content_codepoints?.length ?? 0; i++) {
1008
- let vv = decode_version(v)
1009
- let c = p.content_codepoints[i]
966
+ change_count = new_count
967
+ low_seq = v[1] + 1 - change_count
968
+ parents = [`${v[0]}-${low_seq - 1}`]
969
+ max_pos = resource.length_cache.get('' + parents) ??
970
+ (v_eq(resource.version, parents) ? resource.doc.len() : dt_len(resource.doc, parents))
971
+ patches = new_patches
972
+ }
1010
973
 
1011
- if (!seen[JSON.stringify([vv[0], vv[1], ps, p.range[1] + offset, c])]) throw new Error('invalid update: different from previous update with same version')
974
+ // validate patch positions
975
+ let must_be_at_least = 0
976
+ for (let p of patches) {
977
+ if (p.range[0] < must_be_at_least || p.range[0] > max_pos)
978
+ throw new Error(`invalid patch range position: ${p.range[0]}`)
979
+ if (p.range[1] < p.range[0] || p.range[1] > max_pos)
980
+ throw new Error(`invalid patch range position: ${p.range[1]}`)
981
+ must_be_at_least = p.range[1]
982
+ }
1012
983
 
1013
- offset++
1014
- ps = [v]
1015
- v = vv
1016
- v = `${v[0]}-${v[1] + 1}`
1017
- }
1018
- }
984
+ resource.length_cache.put(`${v[0]}-${v[1]}`, patches.reduce((a, b) =>
985
+ a + (b.content_codepoints?.length ?? 0) - (b.range[1] - b.range[0]),
986
+ max_pos))
1019
987
 
1020
- // we already have this version, so nothing left to do
1021
- return { change_count: change_count }
1022
- }
1023
988
  if (!resource.actor_seqs[v[0]]) resource.actor_seqs[v[0]] = new RangeSet()
1024
- resource.actor_seqs[v[0]].add_range(v[1] + 1 - change_count, v[1])
989
+ resource.actor_seqs[v[0]].add_range(low_seq, v[1])
1025
990
 
1026
- // reduce the version sequence by the number of char-edits
1027
- v = `${v[0]}-${v[1] + 1 - change_count}`
991
+ // get the version of the first character-wise edit
992
+ v = `${v[0]}-${low_seq}`
1028
993
 
1029
- let ps = og_parents
994
+ let ps = parents
1030
995
 
996
+ let version_before = resource.version
1031
997
  let v_before = resource.doc.getLocalVersion()
1032
998
 
1033
999
  let bytes = []
@@ -1060,14 +1026,12 @@ function create_braid_text() {
1060
1026
  var post_commit_updates = []
1061
1027
 
1062
1028
  if (options.merge_type != "dt") {
1063
- patches = get_xf_patches(resource.doc, v_before)
1029
+ let patches = get_xf_patches(resource.doc, v_before)
1064
1030
  if (braid_text.verbose) console.log(JSON.stringify({ patches }))
1065
1031
 
1066
- let version = resource.version
1067
-
1068
1032
  for (let client of resource.simpleton_clients) {
1069
1033
  if (peer && client.peer === peer) {
1070
- client.my_last_seen_version = [og_v]
1034
+ client.my_last_seen_version = [version]
1071
1035
  }
1072
1036
 
1073
1037
  function set_timeout(time_override) {
@@ -1076,10 +1040,10 @@ function create_braid_text() {
1076
1040
  // if the doc has been freed, exit early
1077
1041
  if (resource.doc.__wbg_ptr === 0) return
1078
1042
 
1079
- let version = resource.version
1080
- let x = { version }
1081
- x.parents = client.my_last_seen_version
1082
-
1043
+ let x = {
1044
+ version: resource.version,
1045
+ parents: client.my_last_seen_version
1046
+ }
1083
1047
  if (braid_text.verbose) console.log("rebasing after timeout.. ")
1084
1048
  if (braid_text.verbose) console.log(" client.my_unused_version_count = " + client.my_unused_version_count)
1085
1049
  x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, client.my_last_seen_version))
@@ -1094,7 +1058,7 @@ function create_braid_text() {
1094
1058
 
1095
1059
  if (client.my_timeout) {
1096
1060
  if (peer && client.peer === peer) {
1097
- if (!v_eq(client.my_last_sent_version, og_parents)) {
1061
+ if (!v_eq(client.my_last_sent_version, parents)) {
1098
1062
  // note: we don't add to client.my_unused_version_count,
1099
1063
  // because we're already in a timeout;
1100
1064
  // we'll just extend it here..
@@ -1108,9 +1072,9 @@ function create_braid_text() {
1108
1072
  continue
1109
1073
  }
1110
1074
 
1111
- let x = { version }
1075
+ let x = { version: resource.version }
1112
1076
  if (peer && client.peer === peer) {
1113
- if (!v_eq(client.my_last_sent_version, og_parents)) {
1077
+ if (!v_eq(client.my_last_sent_version, parents)) {
1114
1078
  client.my_unused_version_count = (client.my_unused_version_count ?? 0) + 1
1115
1079
  set_timeout()
1116
1080
  continue
@@ -1118,10 +1082,10 @@ function create_braid_text() {
1118
1082
  delete client.my_unused_version_count
1119
1083
  }
1120
1084
 
1121
- x.parents = options.version
1122
- if (!v_eq(version, options.version)) {
1085
+ x.parents = [version]
1086
+ if (!v_eq(x.version, x.parents)) {
1123
1087
  if (braid_text.verbose) console.log("rebasing..")
1124
- x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, [og_v]))
1088
+ x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, x.parents))
1125
1089
  } else {
1126
1090
  // this client already has this version,
1127
1091
  // so let's pretend to send it back, but not
@@ -1130,7 +1094,7 @@ function create_braid_text() {
1130
1094
  continue
1131
1095
  }
1132
1096
  } else {
1133
- x.parents = parents
1097
+ x.parents = version_before
1134
1098
  x.patches = patches
1135
1099
  }
1136
1100
  if (braid_text.verbose) console.log(`sending: ${JSON.stringify(x)}`)
@@ -1139,9 +1103,11 @@ function create_braid_text() {
1139
1103
  }
1140
1104
  } else {
1141
1105
  if (resource.simpleton_clients.size) {
1142
- let version = resource.version
1143
- patches = get_xf_patches(resource.doc, v_before)
1144
- let x = { version, parents, patches }
1106
+ let x = {
1107
+ version: resource.version,
1108
+ parents: version_before,
1109
+ patches: get_xf_patches(resource.doc, v_before)
1110
+ }
1145
1111
  if (braid_text.verbose) console.log(`sending: ${JSON.stringify(x)}`)
1146
1112
  for (let client of resource.simpleton_clients) {
1147
1113
  if (client.my_timeout) continue
@@ -1152,9 +1118,13 @@ function create_braid_text() {
1152
1118
  }
1153
1119
 
1154
1120
  var x = {
1155
- version: [og_v],
1156
- parents: og_parents,
1157
- patches: og_patches,
1121
+ version: [version],
1122
+ parents,
1123
+ patches: patches.map(p => ({
1124
+ unit: p.unit,
1125
+ range: `[${p.range.join(':')}]`,
1126
+ content: p.content
1127
+ })),
1158
1128
  }
1159
1129
  for (let client of resource.clients) {
1160
1130
  if (!peer || client.peer !== peer)
@@ -1540,6 +1510,62 @@ function create_braid_text() {
1540
1510
  if (!seqs.length) delete ns.actor_seqs[actor]
1541
1511
  }
1542
1512
 
1513
+ function validate_old_patches(resource, base_v, parents, patches) {
1514
+ // if we have seen it already, make sure it's the same as before
1515
+ let updates = dt_get_patches(resource.doc, parents)
1516
+
1517
+ let seen = {}
1518
+ for (let u of updates) {
1519
+ u.version = decode_version(u.version)
1520
+
1521
+ if (!u.content) {
1522
+ // delete
1523
+ let v = u.version
1524
+ for (let i = 0; i < u.end - u.start; i++) {
1525
+ let ps = (i < u.end - u.start - 1) ? [`${v[0]}-${v[1] - i - 1}`] : u.parents
1526
+ seen[JSON.stringify([v[0], v[1] - i, ps, u.start + i])] = true
1527
+ }
1528
+ } else {
1529
+ // insert
1530
+ let v = u.version
1531
+ let content = [...u.content]
1532
+ for (let i = 0; i < content.length; i++) {
1533
+ let ps = (i > 0) ? [`${v[0]}-${v[1] - content.length + i}`] : u.parents
1534
+ seen[JSON.stringify([v[0], v[1] + 1 - content.length + i, ps, u.start + i, content[i]])] = true
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ let v = base_v
1540
+ let ps = parents
1541
+ let offset = 0
1542
+ for (let p of patches) {
1543
+ // delete
1544
+ for (let i = p.range[0]; i < p.range[1]; i++) {
1545
+ let vv = decode_version(v)
1546
+
1547
+ if (!seen[JSON.stringify([vv[0], vv[1], ps, p.range[1] - 1 + offset])]) throw new Error('invalid update: different from previous update with same version')
1548
+
1549
+ offset--
1550
+ ps = [v]
1551
+ v = vv
1552
+ v = `${v[0]}-${v[1] + 1}`
1553
+ }
1554
+ // insert
1555
+ for (let i = 0; i < p.content_codepoints?.length ?? 0; i++) {
1556
+ let vv = decode_version(v)
1557
+ let c = p.content_codepoints[i]
1558
+
1559
+ if (!seen[JSON.stringify([vv[0], vv[1], ps, p.range[1] + offset, c])]) throw new Error('invalid update: different from previous update with same version')
1560
+
1561
+ offset++
1562
+ ps = [v]
1563
+ v = vv
1564
+ v = `${v[0]}-${v[1] + 1}`
1565
+ }
1566
+ }
1567
+ }
1568
+
1543
1569
  //////////////////////////////////////////////////////////////////
1544
1570
  //////////////////////////////////////////////////////////////////
1545
1571
  //////////////////////////////////////////////////////////////////
@@ -2543,6 +2569,120 @@ function create_braid_text() {
2543
2569
  if (typeof x.content !== 'string') throw new Error(`invalid patch content: must be a string`)
2544
2570
  }
2545
2571
 
2572
+ // Splits an array of patches at a given character position within the
2573
+ // combined delete+insert sequence.
2574
+ //
2575
+ // Patches are objects with:
2576
+ // - unit: string (e.g., 'text')
2577
+ // - range: [start, end] - character positions for deletion
2578
+ // - content: string - the content to insert
2579
+ // - content_codepoints: array of single characters
2580
+ //
2581
+ // Each patch represents a "replace" operation: delete then insert.
2582
+ // The combined sequence for patches is:
2583
+ // del(patch1), ins(patch1), del(patch2), ins(patch2), ...
2584
+ //
2585
+ // The split_point is an index into this combined sequence.
2586
+ //
2587
+ // Example: patches with del(3),ins(4),del(2),ins(5)
2588
+ // - split_point 1 falls in first del(3)
2589
+ // - split_point 5 falls in first ins(4) (positions 3-6)
2590
+ // - split_point 7 falls in second del(2) (positions 7-8)
2591
+ //
2592
+ // First patches: operations up to split_point
2593
+ // Second patches: operations from split_point onward (ranges adjusted)
2594
+ function split_patches(patches, split_point) {
2595
+ let second_patches = []
2596
+
2597
+ let position = 0 // current position in the combined sequence
2598
+ let adjustment = 0 // how much to adjust second patches' ranges
2599
+ let first_len = 0 // how many patches stay in first (modified in place)
2600
+
2601
+ for (let i = 0; i < patches.length; i++) {
2602
+ let p = patches[i]
2603
+ let delete_length = p.range[1] - p.range[0]
2604
+ let insert_length = p.content_codepoints.length
2605
+
2606
+ let del_start = position
2607
+ let del_end = position + delete_length
2608
+ let ins_start = del_end
2609
+ let ins_end = ins_start + insert_length
2610
+
2611
+ if (split_point >= ins_end) {
2612
+ // Entire patch is before split point - stays in first (unchanged)
2613
+ first_len++
2614
+ // Adjustment: this patch removes delete_length and adds insert_length
2615
+ adjustment += insert_length - delete_length
2616
+ } else if (split_point <= del_start) {
2617
+ // Entire patch is after split point - goes to second (adjusted)
2618
+ second_patches.push({
2619
+ unit: p.unit,
2620
+ range: [p.range[0] + adjustment, p.range[1] + adjustment],
2621
+ content: p.content,
2622
+ content_codepoints: p.content_codepoints
2623
+ })
2624
+ } else if (split_point <= del_end) {
2625
+ // Split point is within the delete portion
2626
+ let del_chars_before = split_point - del_start
2627
+
2628
+ // Save original values before modifying
2629
+ let original_range_end = p.range[1]
2630
+ let original_content = p.content
2631
+ let original_content_codepoints = p.content_codepoints
2632
+
2633
+ // First patches: partial delete, no insert (modify in place)
2634
+ p.range[1] = p.range[0] + del_chars_before
2635
+ p.content = ''
2636
+ p.content_codepoints = []
2637
+ first_len++
2638
+
2639
+ // Adjustment from partial delete
2640
+ adjustment -= del_chars_before
2641
+
2642
+ // Second patches: remaining delete + full insert (adjusted)
2643
+ second_patches.push({
2644
+ unit: p.unit,
2645
+ range: [p.range[1] + adjustment, original_range_end + adjustment],
2646
+ content: original_content,
2647
+ content_codepoints: original_content_codepoints
2648
+ })
2649
+ } else {
2650
+ // Split point is within the insert portion (split_point > del_end && split_point < ins_end)
2651
+ let ins_chars_before = split_point - ins_start
2652
+ let original_content_codepoints = p.content_codepoints
2653
+
2654
+ // First patches: full delete + partial insert (modify in place)
2655
+ p.content_codepoints = p.content_codepoints.slice(0, ins_chars_before)
2656
+ p.content = p.content_codepoints.join('')
2657
+ first_len++
2658
+
2659
+ // After first patches applied, the position for remaining insert is:
2660
+ // p.range[0] (original position)
2661
+ // + adjustment (net change from all prior first_patches)
2662
+ // + ins_chars_before (what this patch's first part inserted)
2663
+ let adjusted_pos = p.range[0] + adjustment + ins_chars_before
2664
+
2665
+ let content_codepoints = original_content_codepoints.slice(ins_chars_before)
2666
+ second_patches.push({
2667
+ unit: p.unit,
2668
+ range: [adjusted_pos, adjusted_pos],
2669
+ content: content_codepoints.join(''),
2670
+ content_codepoints
2671
+ })
2672
+
2673
+ // Update adjustment: full delete removed, partial insert added
2674
+ adjustment += ins_chars_before - delete_length
2675
+ }
2676
+
2677
+ position = ins_end
2678
+ }
2679
+
2680
+ // Truncate patches array to only contain first_patches
2681
+ patches.length = first_len
2682
+
2683
+ return second_patches
2684
+ }
2685
+
2546
2686
  function createSimpleCache(size) {
2547
2687
  const maxSize = size
2548
2688
  const cache = new Map()
@@ -2661,7 +2801,7 @@ function create_braid_text() {
2661
2801
  }
2662
2802
 
2663
2803
  add_range(low_inclusive, high_inclusive) {
2664
- if (low_inclusive > high_inclusive) return
2804
+ if (low_inclusive > high_inclusive) throw new Error('invalid range')
2665
2805
 
2666
2806
  const startIndex = this._bs(mid => this.ranges[mid][1] >= low_inclusive - 1, this.ranges.length, true)
2667
2807
  const endIndex = this._bs(mid => this.ranges[mid][0] <= high_inclusive + 1, -1, false)
@@ -2676,9 +2816,10 @@ function create_braid_text() {
2676
2816
  }
2677
2817
  }
2678
2818
 
2679
- has(x) {
2680
- var index = this._bs(mid => this.ranges[mid][0] <= x, -1, false)
2681
- return index !== -1 && x <= this.ranges[index][1]
2819
+ has(x, high) {
2820
+ if (high === undefined) high = x
2821
+ var index = this._bs(mid => this.ranges[mid][0] <= high, -1, false)
2822
+ return index !== -1 && x <= this.ranges[index][1] && this.ranges[index]
2682
2823
  }
2683
2824
 
2684
2825
  _bs(condition, defaultR, moveLeft) {
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.2.114",
3
+ "version": "0.2.115",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
7
7
  "homepage": "https://braid.org",
8
+ "scripts": {
9
+ "test": "node test/test.js"
10
+ },
8
11
  "files": [
9
12
  "index.js",
10
13
  "simpleton-client.js",