braid-text 0.2.68 → 0.2.70

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
@@ -16,13 +16,168 @@ function create_braid_text() {
16
16
 
17
17
  let max_encoded_key_size = 240
18
18
 
19
- braid_text.sync = async (a, b, options) => {
20
- braid_text.get(a, {
21
- subscribe: update => braid_text.put(b, update)
22
- })
23
- braid_text.get(b, {
24
- subscribe: update => braid_text.put(a, update)
25
- })
19
+ braid_text.sync = async (a, b, options = {}) => {
20
+ var unsync_cbs = []
21
+ options.my_unsync = () => unsync_cbs.forEach(cb => cb())
22
+
23
+ if (!options.merge_type) options.merge_type = 'dt'
24
+
25
+ if ((a instanceof URL) === (b instanceof URL)) {
26
+ var a_ops = {
27
+ subscribe: update => braid_text.put(b, update),
28
+ merge_type: options.merge_type,
29
+ }
30
+ braid_text.get(a, a_ops)
31
+ unsync_cbs.push(() => braid_text.forget(a, a_ops))
32
+
33
+ var b_ops = {
34
+ subscribe: update => braid_text.put(a, update),
35
+ merge_type: options.merge_type,
36
+ }
37
+ braid_text.get(b, b_ops)
38
+ unsync_cbs.push(() => braid_text.forget(b, b_ops))
39
+ } else {
40
+ // make a=local and b=remote (swap if not)
41
+ if (a instanceof URL) { let swap = a; a = b; b = swap }
42
+
43
+ var resource = (typeof a == 'string') ? await get_resource(a) : a
44
+
45
+ function extend_frontier(frontier, version, parents) {
46
+ // special case:
47
+ // if current frontier has all parents,
48
+ // then we can just remove those
49
+ // and add version
50
+ var frontier_set = new Set(frontier)
51
+ if (parents.length &&
52
+ parents.every(p => frontier_set.has(p))) {
53
+ parents.forEach(p => frontier_set.delete(p))
54
+ for (var event of version) frontier_set.add(event)
55
+ frontier = [...frontier_set.values()]
56
+ } else {
57
+ // full-proof approach..
58
+ var looking_for = frontier_set
59
+ for (var event of version) looking_for.add(event)
60
+
61
+ frontier = []
62
+ var shadow = new Set()
63
+
64
+ var bytes = resource.doc.toBytes()
65
+ var [_, events, parentss] = braid_text.dt_parse([...bytes])
66
+ for (var i = events.length - 1; i >= 0 && looking_for.size; i--) {
67
+ var e = events[i].join('-')
68
+ if (looking_for.has(e)) {
69
+ looking_for.delete(e)
70
+ if (!shadow.has(e)) frontier.push(e)
71
+ shadow.add(e)
72
+ }
73
+ if (shadow.has(e))
74
+ parentss[i].forEach(p => shadow.add(p.join('-')))
75
+ }
76
+ }
77
+ return frontier.sort()
78
+ }
79
+
80
+ var closed
81
+ var disconnect
82
+ unsync_cbs.push(() => {
83
+ closed = true
84
+ disconnect()
85
+ })
86
+
87
+ connect()
88
+ async function connect() {
89
+ if (options.on_connect) options.on_connect()
90
+
91
+ if (closed) return
92
+
93
+ var ac = new AbortController()
94
+ var disconnect_cbs = [() => ac.abort()]
95
+
96
+ disconnect = () => disconnect_cbs.forEach(cb => cb())
97
+
98
+ try {
99
+ // fork-point
100
+ async function check_version(version) {
101
+ var r = await braid_fetch(b.href, {
102
+ signal: ac.signal,
103
+ method: "HEAD",
104
+ version
105
+ })
106
+ if (!r.ok && r.status !== 309 && r.status !== 500)
107
+ throw new Error(`unexpected HEAD status: ${r.status}`)
108
+ return r.ok
109
+ }
110
+
111
+ function extend_fork_point(update) {
112
+ resource.meta.fork_point =
113
+ extend_frontier(resource.meta.fork_point,
114
+ update.version, update.parents)
115
+ resource.change_meta()
116
+ }
117
+
118
+ // see if remote has the fork point
119
+ if (resource.meta.fork_point &&
120
+ !(await check_version(resource.meta.fork_point))) {
121
+ resource.meta.fork_point = null
122
+ resource.change_meta()
123
+ }
124
+
125
+ // otherwise let's binary search for new fork point..
126
+ if (!resource.meta.fork_point) {
127
+ var bytes = resource.doc.toBytes()
128
+ var [_, events, __] = braid_text.dt_parse([...bytes])
129
+ events = events.map(x => x.join('-'))
130
+
131
+ var min = -1
132
+ var max = events.length
133
+ while (min + 1 < max) {
134
+ var i = Math.floor((min + max)/2)
135
+ var version = [events[i]]
136
+ if (await check_version(version)) {
137
+ min = i
138
+ resource.meta.fork_point = version
139
+ } else max = i
140
+ }
141
+ }
142
+
143
+ // local -> remote
144
+ var a_ops = {
145
+ subscribe: update => {
146
+ update.signal = ac.signal
147
+ braid_text.put(b, update).then((x) => {
148
+ extend_fork_point(update)
149
+ }).catch(e => {
150
+ if (e.name === 'AbortError') {
151
+ // ignore
152
+ } else throw e
153
+ })
154
+ }
155
+ }
156
+ if (resource.meta.fork_point)
157
+ a_ops.parents = resource.meta.fork_point
158
+ disconnect_cbs.push(() => braid_text.forget(a, a_ops))
159
+ braid_text.get(a, a_ops)
160
+
161
+ // remote -> local
162
+ var b_ops = {
163
+ dont_retry: true,
164
+ subscribe: async update => {
165
+ await braid_text.put(a, update)
166
+ extend_fork_point(update)
167
+ },
168
+ }
169
+ disconnect_cbs.push(() => braid_text.forget(b, b_ops))
170
+ // NOTE: this should not return, but it might throw
171
+ await braid_text.get(b, b_ops)
172
+ } catch (e) {
173
+ if (closed) return
174
+
175
+ disconnect()
176
+ console.log(`disconnected, retrying in 1 second`)
177
+ setTimeout(connect, 1000)
178
+ }
179
+ }
180
+ }
26
181
  }
27
182
 
28
183
  braid_text.serve = async (req, res, options = {}) => {
@@ -45,6 +200,10 @@ function create_braid_text() {
45
200
 
46
201
  braidify(req, res)
47
202
  if (res.is_multiplexer) return
203
+
204
+ // Sort version arrays from external sources
205
+ if (req.version) req.version.sort()
206
+ if (req.parents) req.parents.sort()
48
207
  } catch (e) {
49
208
  return my_end(500, "The server failed to process this request. The error generated was: " + e)
50
209
  }
@@ -301,6 +460,15 @@ function create_braid_text() {
301
460
  }
302
461
 
303
462
  braid_text.get = async (key, options) => {
463
+ if (options && options.version) {
464
+ validate_version_array(options.version)
465
+ options.version.sort()
466
+ }
467
+ if (options && options.parents) {
468
+ validate_version_array(options.parents)
469
+ options.parents.sort()
470
+ }
471
+
304
472
  if (key instanceof URL) {
305
473
  if (!options) options = {}
306
474
 
@@ -308,22 +476,29 @@ function create_braid_text() {
308
476
 
309
477
  var params = {
310
478
  signal: options.my_abort.signal,
311
- retry: () => true,
312
479
  subscribe: !!options.subscribe,
313
480
  heartbeats: 120,
314
481
  }
482
+ if (!options.dont_retry) params.retry = () => true
315
483
  for (var x of ['headers', 'parents', 'version', 'peer'])
316
484
  if (options[x] != null) params[x] = options[x]
317
485
 
318
486
  var res = await braid_fetch(key.href, params)
319
487
 
320
488
  if (options.subscribe) {
321
- res.subscribe(update => {
489
+ if (options.dont_retry) {
490
+ var error_happened
491
+ var error_promise = new Promise((_, fail) => error_happened = fail)
492
+ }
493
+
494
+ res.subscribe(async update => {
322
495
  update.body = update.body_text
323
496
  if (update.patches)
324
497
  for (var p of update.patches) p.content = p.content_text
325
- options.subscribe(update)
326
- })
498
+ await options.subscribe(update)
499
+ }, e => options.dont_retry && error_happened(e))
500
+
501
+ if (options.dont_retry) return await error_promise
327
502
  return res
328
503
  } else return await res.text()
329
504
  }
@@ -334,9 +509,6 @@ function create_braid_text() {
334
509
  return (await get_resource(key)).val
335
510
  }
336
511
 
337
- if (options.version) validate_version_array(options.version)
338
- if (options.parents) validate_version_array(options.parents)
339
-
340
512
  let resource = (typeof key == 'string') ? await get_resource(key) : key
341
513
  var version = resource.version
342
514
 
@@ -374,13 +546,19 @@ function create_braid_text() {
374
546
  body: resource.doc.get()
375
547
  }
376
548
  } else {
549
+ options.my_subscribe_chain = Promise.resolve()
550
+ options.my_subscribe = (x) =>
551
+ options.my_subscribe_chain =
552
+ options.my_subscribe_chain.then(() =>
553
+ options.subscribe(x))
554
+
377
555
  if (options.merge_type != "dt") {
378
556
  let x = { version }
379
557
 
380
558
  if (!options.parents && !options.version) {
381
559
  x.parents = []
382
560
  x.body = resource.doc.get()
383
- options.subscribe(x)
561
+ options.my_subscribe(x)
384
562
  } else {
385
563
  x.parents = options.version ? options.version : options.parents
386
564
  options.my_last_seen_version = x.parents
@@ -389,7 +567,7 @@ function create_braid_text() {
389
567
  let local_version = OpLog_remote_to_local(resource.doc, x.parents)
390
568
  if (local_version) {
391
569
  x.patches = get_xf_patches(resource.doc, local_version)
392
- options.subscribe(x)
570
+ options.my_subscribe(x)
393
571
  }
394
572
  }
395
573
 
@@ -401,7 +579,7 @@ function create_braid_text() {
401
579
  // optimization: if client wants past current version,
402
580
  // send empty dt
403
581
  if (options.parents && v_eq(options.parents, version)) {
404
- options.subscribe({ encoding: 'dt', body: new Doc().toBytes() })
582
+ options.my_subscribe({ encoding: 'dt', body: new Doc().toBytes() })
405
583
  } else {
406
584
  var bytes = resource.doc.toBytes()
407
585
  if (options.parents) {
@@ -410,12 +588,12 @@ function create_braid_text() {
410
588
  dt_get_local_version(bytes, options.parents))
411
589
  doc.free()
412
590
  }
413
- options.subscribe({ encoding: 'dt', body: bytes })
591
+ options.my_subscribe({ encoding: 'dt', body: bytes })
414
592
  }
415
593
  } else {
416
594
  var updates = null
417
595
  if (!options.parents && !options.version) {
418
- options.subscribe({
596
+ options.my_subscribe({
419
597
  version: [],
420
598
  parents: [],
421
599
  body: "",
@@ -428,7 +606,7 @@ function create_braid_text() {
428
606
  }
429
607
 
430
608
  for (let u of updates)
431
- options.subscribe({
609
+ options.my_subscribe({
432
610
  version: [u.version],
433
611
  parents: u.parents,
434
612
  patches: [{ unit: u.unit, range: u.range, content: u.content }],
@@ -459,8 +637,20 @@ function create_braid_text() {
459
637
  }
460
638
 
461
639
  braid_text.put = async (key, options) => {
640
+ if (options.version) {
641
+ validate_version_array(options.version)
642
+ options.version.sort()
643
+ }
644
+ if (options.parents) {
645
+ validate_version_array(options.parents)
646
+ options.parents.sort()
647
+ }
648
+
462
649
  if (key instanceof URL) {
463
650
  options.my_abort = new AbortController()
651
+ if (options.signal)
652
+ options.signal.addEventListener('abort', () =>
653
+ options.my_abort.abort())
464
654
 
465
655
  var params = {
466
656
  method: 'PUT',
@@ -473,315 +663,311 @@ function create_braid_text() {
473
663
  return await braid_fetch(key.href, params)
474
664
  }
475
665
 
476
- let { version, patches, body, peer } = options
477
-
478
- // support for json patch puts..
479
- if (patches?.length && patches.every(x => x.unit === 'json')) {
666
+ return await within_fiber('put:' + key, async () => {
480
667
  let resource = (typeof key == 'string') ? await get_resource(key) : key
481
-
482
- let x = JSON.parse(resource.doc.get())
483
- for (let p of patches)
484
- apply_patch(x, p.range, p.content === '' ? undefined : JSON.parse(p.content))
485
668
 
486
- return await braid_text.put(key, {
487
- body: JSON.stringify(x, null, 4)
488
- })
489
- }
669
+ // support for json patch puts..
670
+ if (options.patches && options.patches.length &&
671
+ options.patches.every(x => x.unit === 'json')) {
672
+ let x = JSON.parse(resource.doc.get())
673
+ for (let p of options.patches)
674
+ apply_patch(x, p.range, p.content === '' ? undefined : JSON.parse(p.content))
675
+ options = { body: JSON.stringify(x, null, 4) }
676
+ }
490
677
 
491
- let resource = (typeof key == 'string') ? await get_resource(key) : key
678
+ let { version, patches, body, peer } = options
492
679
 
493
- if (options.transfer_encoding === 'dt') {
494
- var start_i = 1 + resource.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
495
-
496
- resource.doc.mergeBytes(body)
680
+ if (options.transfer_encoding === 'dt') {
681
+ var start_i = 1 + resource.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
682
+
683
+ resource.doc.mergeBytes(body)
497
684
 
498
- var end_i = resource.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
499
- for (var i = start_i; i <= end_i; i++) {
500
- let v = resource.doc.localToRemoteVersion([i])[0]
501
- if (!resource.actor_seqs[v[0]]) resource.actor_seqs[v[0]] = new braid_text.RangeSet()
502
- resource.actor_seqs[v[0]].add_range(v[1], v[1])
503
- }
504
- resource.val = resource.doc.get()
505
- resource.version = resource.doc.getRemoteVersion().map(x => x.join("-")).sort()
685
+ var end_i = resource.doc.getLocalVersion().reduce((a, b) => Math.max(a, b), -1)
686
+ for (var i = start_i; i <= end_i; i++) {
687
+ let v = resource.doc.localToRemoteVersion([i])[0]
688
+ if (!resource.actor_seqs[v[0]]) resource.actor_seqs[v[0]] = new braid_text.RangeSet()
689
+ resource.actor_seqs[v[0]].add_range(v[1], v[1])
690
+ }
691
+ resource.val = resource.doc.get()
692
+ resource.version = resource.doc.getRemoteVersion().map(x => x.join("-")).sort()
506
693
 
507
- await resource.db_delta(body)
508
- return { change_count: end_i - start_i + 1 }
509
- }
694
+ await resource.db_delta(body)
695
+ return { change_count: end_i - start_i + 1 }
696
+ }
510
697
 
511
- if (version) validate_version_array(version)
512
- if (version && !version.length) {
513
- console.log(`warning: ignoring put with empty version`)
514
- return { change_count: 0 }
515
- }
516
- if (version && version.length > 1)
517
- throw new Error(`cannot put a version with multiple ids`)
518
-
519
- // translate a single parent of "root" to the empty array (same meaning)
520
- let options_parents = options.parents
521
- if (options_parents?.length === 1 && options_parents[0] === 'root')
522
- options_parents = []
523
-
524
- if (options_parents) validate_version_array(options_parents)
525
- if (body != null && patches) throw new Error(`cannot have a body and patches`)
526
- if (body != null && (typeof body !== 'string')) throw new Error(`body must be a string`)
527
- if (patches) validate_patches(patches)
528
-
529
- if (options_parents) {
530
- // make sure we have all these parents
531
- for (let p of options_parents) {
532
- let P = decode_version(p)
533
- if (!resource.actor_seqs[P[0]]?.has(P[1]))
534
- throw new Error(`missing parent version: ${p}`)
698
+ if (version && !version.length) {
699
+ console.log(`warning: ignoring put with empty version`)
700
+ return { change_count: 0 }
701
+ }
702
+ if (version && version.length > 1)
703
+ throw new Error(`cannot put a version with multiple ids`)
704
+
705
+ // translate a single parent of "root" to the empty array (same meaning)
706
+ let options_parents = options.parents
707
+ if (options_parents?.length === 1 && options_parents[0] === 'root')
708
+ options_parents = []
709
+
710
+ if (body != null && patches) throw new Error(`cannot have a body and patches`)
711
+ if (body != null && (typeof body !== 'string')) throw new Error(`body must be a string`)
712
+ if (patches) validate_patches(patches)
713
+
714
+ if (options_parents) {
715
+ // make sure we have all these parents
716
+ for (let p of options_parents) {
717
+ let P = decode_version(p)
718
+ if (!resource.actor_seqs[P[0]]?.has(P[1]))
719
+ throw new Error(`missing parent version: ${p}`)
720
+ }
535
721
  }
536
- }
537
722
 
538
- let parents = resource.version
539
- let og_parents = options_parents || parents
723
+ let parents = resource.version
724
+ let og_parents = options_parents || parents
540
725
 
541
- let max_pos = resource.length_cache.get('' + og_parents) ??
542
- (v_eq(parents, og_parents) ? resource.doc.len() : dt_len(resource.doc, og_parents))
543
-
544
- if (body != null) {
545
- patches = [{
546
- unit: 'text',
547
- range: `[0:${max_pos}]`,
548
- content: body
549
- }]
550
- }
726
+ let max_pos = resource.length_cache.get('' + og_parents) ??
727
+ (v_eq(parents, og_parents) ? resource.doc.len() : dt_len(resource.doc, og_parents))
728
+
729
+ if (body != null) {
730
+ patches = [{
731
+ unit: 'text',
732
+ range: `[0:${max_pos}]`,
733
+ content: body
734
+ }]
735
+ }
551
736
 
552
- let og_patches = patches
553
- patches = patches.map((p) => ({
554
- ...p,
555
- range: p.range.match(/\d+/g).map((x) => parseInt(x)),
556
- content_codepoints: [...p.content],
557
- })).sort((a, b) => a.range[0] - b.range[0])
737
+ let og_patches = patches
738
+ patches = patches.map((p) => ({
739
+ ...p,
740
+ range: p.range.match(/\d+/g).map((x) => parseInt(x)),
741
+ content_codepoints: [...p.content],
742
+ })).sort((a, b) => a.range[0] - b.range[0])
558
743
 
559
- // validate patch positions
560
- let must_be_at_least = 0
561
- for (let p of patches) {
562
- if (p.range[0] < must_be_at_least || p.range[0] > max_pos) throw new Error(`invalid patch range position: ${p.range[0]}`)
563
- if (p.range[1] < p.range[0] || p.range[1] > max_pos) throw new Error(`invalid patch range position: ${p.range[1]}`)
564
- must_be_at_least = p.range[1]
565
- }
744
+ // validate patch positions
745
+ let must_be_at_least = 0
746
+ for (let p of patches) {
747
+ if (p.range[0] < must_be_at_least || p.range[0] > max_pos) throw new Error(`invalid patch range position: ${p.range[0]}`)
748
+ if (p.range[1] < p.range[0] || p.range[1] > max_pos) throw new Error(`invalid patch range position: ${p.range[1]}`)
749
+ must_be_at_least = p.range[1]
750
+ }
566
751
 
567
- let change_count = patches.reduce((a, b) => a + b.content_codepoints.length + (b.range[1] - b.range[0]), 0)
752
+ let change_count = patches.reduce((a, b) => a + b.content_codepoints.length + (b.range[1] - b.range[0]), 0)
568
753
 
569
- let og_v = version?.[0] || `${(is_valid_actor(peer) && peer) || Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
754
+ let og_v = version?.[0] || `${(is_valid_actor(peer) && peer) || Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
570
755
 
571
- let v = decode_version(og_v)
756
+ let v = decode_version(og_v)
572
757
 
573
- resource.length_cache.put(`${v[0]}-${v[1]}`, patches.reduce((a, b) =>
574
- a + (b.content_codepoints?.length ?? 0) - (b.range[1] - b.range[0]),
575
- max_pos))
758
+ resource.length_cache.put(`${v[0]}-${v[1]}`, patches.reduce((a, b) =>
759
+ a + (b.content_codepoints?.length ?? 0) - (b.range[1] - b.range[0]),
760
+ max_pos))
576
761
 
577
- // validate version: make sure we haven't seen it already
578
- if (resource.actor_seqs[v[0]]?.has(v[1])) {
762
+ // validate version: make sure we haven't seen it already
763
+ if (resource.actor_seqs[v[0]]?.has(v[1])) {
579
764
 
580
- if (!options.validate_already_seen_versions) return { change_count }
765
+ if (!options.validate_already_seen_versions) return { change_count }
581
766
 
582
- // if we have seen it already, make sure it's the same as before
583
- let updates = dt_get_patches(resource.doc, og_parents)
767
+ // if we have seen it already, make sure it's the same as before
768
+ let updates = dt_get_patches(resource.doc, og_parents)
584
769
 
585
- let seen = {}
586
- for (let u of updates) {
587
- u.version = decode_version(u.version)
770
+ let seen = {}
771
+ for (let u of updates) {
772
+ u.version = decode_version(u.version)
773
+
774
+ if (!u.content) {
775
+ // delete
776
+ let v = u.version
777
+ for (let i = 0; i < u.end - u.start; i++) {
778
+ let ps = (i < u.end - u.start - 1) ? [`${v[0]}-${v[1] - i - 1}`] : u.parents
779
+ seen[JSON.stringify([v[0], v[1] - i, ps, u.start + i])] = true
780
+ }
781
+ } else {
782
+ // insert
783
+ let v = u.version
784
+ let content = [...u.content]
785
+ for (let i = 0; i < content.length; i++) {
786
+ let ps = (i > 0) ? [`${v[0]}-${v[1] - content.length + i}`] : u.parents
787
+ seen[JSON.stringify([v[0], v[1] + 1 - content.length + i, ps, u.start + i, content[i]])] = true
788
+ }
789
+ }
790
+ }
588
791
 
589
- if (!u.content) {
792
+ v = `${v[0]}-${v[1] + 1 - change_count}`
793
+ let ps = og_parents
794
+ let offset = 0
795
+ for (let p of patches) {
590
796
  // delete
591
- let v = u.version
592
- for (let i = 0; i < u.end - u.start; i++) {
593
- let ps = (i < u.end - u.start - 1) ? [`${v[0]}-${v[1] - i - 1}`] : u.parents
594
- seen[JSON.stringify([v[0], v[1] - i, ps, u.start + i])] = true
797
+ for (let i = p.range[0]; i < p.range[1]; i++) {
798
+ let vv = decode_version(v)
799
+
800
+ 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')
801
+
802
+ offset--
803
+ ps = [v]
804
+ v = vv
805
+ v = `${v[0]}-${v[1] + 1}`
595
806
  }
596
- } else {
597
807
  // insert
598
- let v = u.version
599
- let content = [...u.content]
600
- for (let i = 0; i < content.length; i++) {
601
- let ps = (i > 0) ? [`${v[0]}-${v[1] - content.length + i}`] : u.parents
602
- seen[JSON.stringify([v[0], v[1] + 1 - content.length + i, ps, u.start + i, content[i]])] = true
808
+ for (let i = 0; i < p.content_codepoints?.length ?? 0; i++) {
809
+ let vv = decode_version(v)
810
+ let c = p.content_codepoints[i]
811
+
812
+ 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')
813
+
814
+ offset++
815
+ ps = [v]
816
+ v = vv
817
+ v = `${v[0]}-${v[1] + 1}`
603
818
  }
604
819
  }
820
+
821
+ // we already have this version, so nothing left to do
822
+ return { change_count: change_count }
605
823
  }
824
+ if (!resource.actor_seqs[v[0]]) resource.actor_seqs[v[0]] = new RangeSet()
825
+ resource.actor_seqs[v[0]].add_range(v[1] + 1 - change_count, v[1])
606
826
 
827
+ // reduce the version sequence by the number of char-edits
607
828
  v = `${v[0]}-${v[1] + 1 - change_count}`
829
+
608
830
  let ps = og_parents
831
+
832
+ let v_before = resource.doc.getLocalVersion()
833
+
834
+ let bytes = []
835
+
609
836
  let offset = 0
610
837
  for (let p of patches) {
611
838
  // delete
612
- for (let i = p.range[0]; i < p.range[1]; i++) {
613
- let vv = decode_version(v)
614
-
615
- 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')
616
-
617
- offset--
618
- ps = [v]
619
- v = vv
620
- v = `${v[0]}-${v[1] + 1}`
839
+ let del = p.range[1] - p.range[0]
840
+ if (del) {
841
+ bytes.push(dt_create_bytes(v, ps, p.range[0] + offset, del, null))
842
+ offset -= del
843
+ v = decode_version(v)
844
+ ps = [`${v[0]}-${v[1] + (del - 1)}`]
845
+ v = `${v[0]}-${v[1] + del}`
621
846
  }
622
847
  // insert
623
- for (let i = 0; i < p.content_codepoints?.length ?? 0; i++) {
624
- let vv = decode_version(v)
625
- let c = p.content_codepoints[i]
626
-
627
- 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')
628
-
629
- offset++
630
- ps = [v]
631
- v = vv
632
- v = `${v[0]}-${v[1] + 1}`
848
+ if (p.content?.length) {
849
+ bytes.push(dt_create_bytes(v, ps, p.range[1] + offset, 0, p.content))
850
+ offset += p.content_codepoints.length
851
+ v = decode_version(v)
852
+ ps = [`${v[0]}-${v[1] + (p.content_codepoints.length - 1)}`]
853
+ v = `${v[0]}-${v[1] + p.content_codepoints.length}`
633
854
  }
634
855
  }
635
856
 
636
- // we already have this version, so nothing left to do
637
- return { change_count: change_count }
638
- }
639
- if (!resource.actor_seqs[v[0]]) resource.actor_seqs[v[0]] = new RangeSet()
640
- resource.actor_seqs[v[0]].add_range(v[1] + 1 - change_count, v[1])
641
-
642
- // reduce the version sequence by the number of char-edits
643
- v = `${v[0]}-${v[1] + 1 - change_count}`
644
-
645
- let ps = og_parents
646
-
647
- let v_before = resource.doc.getLocalVersion()
648
-
649
- let bytes = []
650
-
651
- let offset = 0
652
- for (let p of patches) {
653
- // delete
654
- let del = p.range[1] - p.range[0]
655
- if (del) {
656
- bytes.push(dt_create_bytes(v, ps, p.range[0] + offset, del, null))
657
- offset -= del
658
- v = decode_version(v)
659
- ps = [`${v[0]}-${v[1] + (del - 1)}`]
660
- v = `${v[0]}-${v[1] + del}`
661
- }
662
- // insert
663
- if (p.content?.length) {
664
- bytes.push(dt_create_bytes(v, ps, p.range[1] + offset, 0, p.content))
665
- offset += p.content_codepoints.length
666
- v = decode_version(v)
667
- ps = [`${v[0]}-${v[1] + (p.content_codepoints.length - 1)}`]
668
- v = `${v[0]}-${v[1] + p.content_codepoints.length}`
669
- }
670
- }
857
+ for (let b of bytes) resource.doc.mergeBytes(b)
858
+ resource.val = resource.doc.get()
859
+ resource.version = resource.doc.getRemoteVersion().map(x => x.join("-")).sort()
671
860
 
672
- for (let b of bytes) resource.doc.mergeBytes(b)
673
- resource.val = resource.doc.get()
674
- resource.version = resource.doc.getRemoteVersion().map(x => x.join("-")).sort()
861
+ var post_commit_updates = []
675
862
 
676
- var post_commit_updates = []
863
+ if (options.merge_type != "dt") {
864
+ patches = get_xf_patches(resource.doc, v_before)
865
+ if (braid_text.verbose) console.log(JSON.stringify({ patches }))
677
866
 
678
- if (options.merge_type != "dt") {
679
- patches = get_xf_patches(resource.doc, v_before)
680
- if (braid_text.verbose) console.log(JSON.stringify({ patches }))
867
+ let version = resource.version
681
868
 
682
- let version = resource.version
869
+ for (let client of resource.simpleton_clients) {
870
+ if (peer && client.peer === peer) {
871
+ client.my_last_seen_version = [og_v]
872
+ }
683
873
 
684
- for (let client of resource.simpleton_clients) {
685
- if (peer && client.peer === peer) {
686
- client.my_last_seen_version = [og_v]
687
- }
874
+ function set_timeout(time_override) {
875
+ if (client.my_timeout) clearTimeout(client.my_timeout)
876
+ client.my_timeout = setTimeout(() => {
877
+ // if the doc has been freed, exit early
878
+ if (resource.doc.__wbg_ptr === 0) return
688
879
 
689
- function set_timeout(time_override) {
690
- if (client.my_timeout) clearTimeout(client.my_timeout)
691
- client.my_timeout = setTimeout(() => {
692
- // if the doc has been freed, exit early
693
- if (resource.doc.__wbg_ptr === 0) return
880
+ let version = resource.version
881
+ let x = { version }
882
+ x.parents = client.my_last_seen_version
694
883
 
695
- let version = resource.version
696
- let x = { version }
697
- x.parents = client.my_last_seen_version
884
+ if (braid_text.verbose) console.log("rebasing after timeout.. ")
885
+ if (braid_text.verbose) console.log(" client.my_unused_version_count = " + client.my_unused_version_count)
886
+ x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, client.my_last_seen_version))
698
887
 
699
- if (braid_text.verbose) console.log("rebasing after timeout.. ")
700
- if (braid_text.verbose) console.log(" client.my_unused_version_count = " + client.my_unused_version_count)
701
- x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, client.my_last_seen_version))
888
+ if (braid_text.verbose) console.log(`sending from rebase: ${JSON.stringify(x)}`)
889
+ client.my_subscribe(x)
890
+ client.my_last_sent_version = x.version
702
891
 
703
- if (braid_text.verbose) console.log(`sending from rebase: ${JSON.stringify(x)}`)
704
- client.subscribe(x)
705
- client.my_last_sent_version = x.version
892
+ delete client.my_timeout
893
+ }, time_override ?? Math.min(3000, 23 * Math.pow(1.5, client.my_unused_version_count - 1)))
894
+ }
706
895
 
707
- delete client.my_timeout
708
- }, time_override ?? Math.min(3000, 23 * Math.pow(1.5, client.my_unused_version_count - 1)))
709
- }
896
+ if (client.my_timeout) {
897
+ if (peer && client.peer === peer) {
898
+ if (!v_eq(client.my_last_sent_version, og_parents)) {
899
+ // note: we don't add to client.my_unused_version_count,
900
+ // because we're already in a timeout;
901
+ // we'll just extend it here..
902
+ set_timeout()
903
+ } else {
904
+ // hm.. it appears we got a correctly parented version,
905
+ // which suggests that maybe we can stop the timeout early
906
+ set_timeout(0)
907
+ }
908
+ }
909
+ continue
910
+ }
710
911
 
711
- if (client.my_timeout) {
912
+ let x = { version }
712
913
  if (peer && client.peer === peer) {
713
914
  if (!v_eq(client.my_last_sent_version, og_parents)) {
714
- // note: we don't add to client.my_unused_version_count,
715
- // because we're already in a timeout;
716
- // we'll just extend it here..
915
+ client.my_unused_version_count = (client.my_unused_version_count ?? 0) + 1
717
916
  set_timeout()
917
+ continue
718
918
  } else {
719
- // hm.. it appears we got a correctly parented version,
720
- // which suggests that maybe we can stop the timeout early
721
- set_timeout(0)
919
+ delete client.my_unused_version_count
722
920
  }
723
- }
724
- continue
725
- }
726
921
 
727
- let x = { version }
728
- if (peer && client.peer === peer) {
729
- if (!v_eq(client.my_last_sent_version, og_parents)) {
730
- client.my_unused_version_count = (client.my_unused_version_count ?? 0) + 1
731
- set_timeout()
732
- continue
922
+ x.parents = options.version
923
+ if (!v_eq(version, options.version)) {
924
+ if (braid_text.verbose) console.log("rebasing..")
925
+ x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, [og_v]))
926
+ } else {
927
+ // this client already has this version,
928
+ // so let's pretend to send it back, but not
929
+ if (braid_text.verbose) console.log(`not reflecting back to simpleton`)
930
+ client.my_last_sent_version = x.version
931
+ continue
932
+ }
733
933
  } else {
734
- delete client.my_unused_version_count
934
+ x.parents = parents
935
+ x.patches = patches
735
936
  }
736
-
737
- x.parents = options.version
738
- if (!v_eq(version, options.version)) {
739
- if (braid_text.verbose) console.log("rebasing..")
740
- x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, [og_v]))
741
- } else {
742
- // this client already has this version,
743
- // so let's pretend to send it back, but not
744
- if (braid_text.verbose) console.log(`not reflecting back to simpleton`)
937
+ if (braid_text.verbose) console.log(`sending: ${JSON.stringify(x)}`)
938
+ post_commit_updates.push([client, x])
939
+ client.my_last_sent_version = x.version
940
+ }
941
+ } else {
942
+ if (resource.simpleton_clients.size) {
943
+ let version = resource.version
944
+ patches = get_xf_patches(resource.doc, v_before)
945
+ let x = { version, parents, patches }
946
+ if (braid_text.verbose) console.log(`sending: ${JSON.stringify(x)}`)
947
+ for (let client of resource.simpleton_clients) {
948
+ if (client.my_timeout) continue
949
+ post_commit_updates.push([client, x])
745
950
  client.my_last_sent_version = x.version
746
- continue
747
951
  }
748
- } else {
749
- x.parents = parents
750
- x.patches = patches
751
952
  }
752
- if (braid_text.verbose) console.log(`sending: ${JSON.stringify(x)}`)
753
- post_commit_updates.push([client, x])
754
- client.my_last_sent_version = x.version
755
953
  }
756
- } else {
757
- if (resource.simpleton_clients.size) {
758
- let version = resource.version
759
- patches = get_xf_patches(resource.doc, v_before)
760
- let x = { version, parents, patches }
761
- if (braid_text.verbose) console.log(`sending: ${JSON.stringify(x)}`)
762
- for (let client of resource.simpleton_clients) {
763
- if (client.my_timeout) continue
954
+
955
+ var x = {
956
+ version: [og_v],
957
+ parents: og_parents,
958
+ patches: og_patches,
959
+ }
960
+ for (let client of resource.clients) {
961
+ if (!peer || client.peer !== peer)
764
962
  post_commit_updates.push([client, x])
765
- client.my_last_sent_version = x.version
766
- }
767
963
  }
768
- }
769
-
770
- var x = {
771
- version: [og_v],
772
- parents: og_parents,
773
- patches: og_patches,
774
- }
775
- for (let client of resource.clients) {
776
- if (!peer || client.peer !== peer)
777
- post_commit_updates.push([client, x])
778
- }
779
964
 
780
- await resource.db_delta(resource.doc.getPatchSince(v_before))
965
+ await resource.db_delta(resource.doc.getPatchSince(v_before))
781
966
 
782
- for (var [client, x] of post_commit_updates) client.subscribe(x)
967
+ for (var [client, x] of post_commit_updates) client.my_subscribe(x)
783
968
 
784
- return { change_count }
969
+ return { change_count }
970
+ })
785
971
  }
786
972
 
787
973
  braid_text.list = async () => {
@@ -821,10 +1007,7 @@ function create_braid_text() {
821
1007
  : { change: () => { }, change_meta: () => {} }
822
1008
 
823
1009
  resource.db_delta = change
824
- resource.update_meta = (meta) => {
825
- Object.assign(resource.meta, meta)
826
- change_meta()
827
- }
1010
+ resource.change_meta = change_meta
828
1011
 
829
1012
  resource.actor_seqs = {}
830
1013
 
@@ -959,48 +1142,45 @@ function create_braid_text() {
959
1142
  } catch (e) {}
960
1143
  set_meta(JSON.parse(meta_file_content))
961
1144
 
962
- let chain = Promise.resolve()
963
1145
  return {
964
- change: async (bytes) => {
965
- await (chain = chain.then(async () => {
966
- if (!bytes) currentSize = threshold
967
- else currentSize += bytes.length + 4 // we account for the extra 4 bytes for uint32
968
- const filename = `${braid_text.db_folder}/${encoded}.${currentNumber}`
969
- if (currentSize < threshold) {
970
- if (braid_text.verbose) console.log(`appending to db..`)
971
-
972
- let buffer = Buffer.allocUnsafe(4)
973
- buffer.writeUInt32LE(bytes.length, 0)
974
- await fs.promises.appendFile(filename, buffer)
975
- await fs.promises.appendFile(filename, bytes)
976
-
977
- if (braid_text.verbose) console.log("wrote to : " + filename)
978
- } else {
1146
+ change: (bytes) => within_fiber('file:' + key, async () => {
1147
+ if (!bytes) currentSize = threshold
1148
+ else currentSize += bytes.length + 4 // we account for the extra 4 bytes for uint32
1149
+ const filename = `${braid_text.db_folder}/${encoded}.${currentNumber}`
1150
+ if (currentSize < threshold) {
1151
+ if (braid_text.verbose) console.log(`appending to db..`)
1152
+
1153
+ let buffer = Buffer.allocUnsafe(4)
1154
+ buffer.writeUInt32LE(bytes.length, 0)
1155
+ await fs.promises.appendFile(filename, buffer)
1156
+ await fs.promises.appendFile(filename, bytes)
1157
+
1158
+ if (braid_text.verbose) console.log("wrote to : " + filename)
1159
+ } else {
1160
+ try {
1161
+ if (braid_text.verbose) console.log(`starting new db..`)
1162
+
1163
+ currentNumber++
1164
+ const init = get_init()
1165
+ const buffer = Buffer.allocUnsafe(4)
1166
+ buffer.writeUInt32LE(init.length, 0)
1167
+
1168
+ const newFilename = `${braid_text.db_folder}/${encoded}.${currentNumber}`
1169
+ await fs.promises.writeFile(newFilename, buffer)
1170
+ await fs.promises.appendFile(newFilename, init)
1171
+
1172
+ if (braid_text.verbose) console.log("wrote to : " + newFilename)
1173
+
1174
+ currentSize = 4 + init.length
1175
+ threshold = currentSize * 10
979
1176
  try {
980
- if (braid_text.verbose) console.log(`starting new db..`)
981
-
982
- currentNumber++
983
- const init = get_init()
984
- const buffer = Buffer.allocUnsafe(4)
985
- buffer.writeUInt32LE(init.length, 0)
986
-
987
- const newFilename = `${braid_text.db_folder}/${encoded}.${currentNumber}`
988
- await fs.promises.writeFile(newFilename, buffer)
989
- await fs.promises.appendFile(newFilename, init)
990
-
991
- if (braid_text.verbose) console.log("wrote to : " + newFilename)
992
-
993
- currentSize = 4 + init.length
994
- threshold = currentSize * 10
995
- try {
996
- await fs.promises.unlink(filename)
997
- } catch (e) { }
998
- } catch (e) {
999
- if (braid_text.verbose) console.log(`e = ${e.stack}`)
1000
- }
1177
+ await fs.promises.unlink(filename)
1178
+ } catch (e) { }
1179
+ } catch (e) {
1180
+ if (braid_text.verbose) console.log(`e = ${e.stack}`)
1001
1181
  }
1002
- }))
1003
- },
1182
+ }
1183
+ }),
1004
1184
  change_meta: async () => {
1005
1185
  meta_dirty = true
1006
1186
  if (meta_saving) return
@@ -2271,6 +2451,20 @@ function create_braid_text() {
2271
2451
  return `sha-256=:${require('crypto').createHash('sha256').update(s).digest('base64')}:`
2272
2452
  }
2273
2453
 
2454
+ function within_fiber(id, func) {
2455
+ if (!within_fiber.chains) within_fiber.chains = {}
2456
+ var prev = within_fiber.chains[id] || Promise.resolve()
2457
+ var curr = prev.then(async () => {
2458
+ try {
2459
+ return await func()
2460
+ } finally {
2461
+ if (within_fiber.chains[id] === curr)
2462
+ delete within_fiber.chains[id]
2463
+ }
2464
+ })
2465
+ return within_fiber.chains[id] = curr
2466
+ }
2467
+
2274
2468
  braid_text.get_resource = get_resource
2275
2469
 
2276
2470
  braid_text.encode_filename = encode_filename