braid-text 0.1.3 → 0.2.0

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/README.md CHANGED
@@ -176,3 +176,28 @@ simpleton = simpleton_client(url, options)
176
176
  - `content_type`: <small style="color:lightgrey">[optional]</small> If set, this value will be sent in the `Accept` and `Content-Type` headers to the server.
177
177
 
178
178
  - `simpleton.changed()`: Call this function to report local updates whenever they occur, e.g., in the `oninput` event handler of a textarea being synchronized.
179
+
180
+ ## Testing
181
+
182
+ ### to run unit tests:
183
+ first run the demo server as usual:
184
+
185
+ npm install
186
+ node server-demo.js
187
+
188
+ then open http://localhost:8888/test.html, and the boxes should turn green as the tests pass.
189
+
190
+ ### to run fuzz tests:
191
+
192
+ npm install
193
+ node test.js
194
+
195
+ if the last output line looks like this, good:
196
+
197
+ t = 9999, seed = 1397019, best_n = Infinity @ NaN
198
+
199
+ but it's bad if it looks like this:
200
+
201
+ t = 9999, seed = 1397019, best_n = 5 @ 1396791
202
+
203
+ the number at the end is the random seed that generated the simplest error example
package/editor.html CHANGED
@@ -5,7 +5,7 @@
5
5
  ></textarea>
6
6
  </body>
7
7
  <script src="https://braid.org/code/myers-diff1.js"></script>
8
- <script src="https://unpkg.com/braid-http@~1.0/braid-http-client.js"></script>
8
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
9
9
  <script src="/simpleton-client.js"></script>
10
10
  <script>
11
11
  let simpleton = simpleton_client(location.pathname, {
package/index.js CHANGED
@@ -148,6 +148,7 @@ braid_text.serve = async (req, res, options = {}) => {
148
148
  let ee = null
149
149
  try {
150
150
  patches = await req.patches()
151
+ for (let p of patches) p.content = p.content_text
151
152
  } catch (e) { ee = e }
152
153
  await my_prev_put_p
153
154
 
@@ -304,6 +305,19 @@ braid_text.forget = async (key, options) => {
304
305
  braid_text.put = async (key, options) => {
305
306
  let { version, patches, body, peer } = options
306
307
 
308
+ // support for json patch puts..
309
+ if (patches?.length && patches.every(x => x.unit === 'json')) {
310
+ let resource = (typeof key == 'string') ? await get_resource(key) : key
311
+
312
+ let x = JSON.parse(resource.doc.get())
313
+ for (let p of patches)
314
+ apply_patch(x, p.range, JSON.parse(p.content))
315
+
316
+ return await braid_text.put(key, {
317
+ body: JSON.stringify(x, null, 4)
318
+ })
319
+ }
320
+
307
321
  if (version) validate_version_array(version)
308
322
 
309
323
  // translate a single parent of "root" to the empty array (same meaning)
@@ -1618,6 +1632,84 @@ function createSimpleCache(size) {
1618
1632
  }
1619
1633
  }
1620
1634
 
1635
+ function apply_patch(obj, range, content) {
1636
+
1637
+ // Descend down a bunch of objects until we get to the final object
1638
+ // The final object can be a slice
1639
+ // Set the value in the final object
1640
+
1641
+ var path = range,
1642
+ new_stuff = content
1643
+
1644
+ var path_segment = /^(\.?([^\.\[]+))|(\[((-?\d+):)?(-?\d+)\])|\[("(\\"|[^"])*")\]/
1645
+ var curr_obj = obj,
1646
+ last_obj = null
1647
+
1648
+ // Handle negative indices, like "[-9]" or "[-0]"
1649
+ function de_neg (x) {
1650
+ return x[0] === '-'
1651
+ ? curr_obj.length - parseInt(x.substr(1), 10)
1652
+ : parseInt(x, 10)
1653
+ }
1654
+
1655
+ // Now iterate through each segment of the range e.g. [3].a.b[3][9]
1656
+ while (true) {
1657
+ var match = path_segment.exec(path),
1658
+ subpath = match ? match[0] : '',
1659
+ field = match && match[2],
1660
+ slice_start = match && match[5],
1661
+ slice_end = match && match[6],
1662
+ quoted_field = match && match[7]
1663
+
1664
+ // The field could be expressed as ["nnn"] instead of .nnn
1665
+ if (quoted_field) field = JSON.parse(quoted_field)
1666
+
1667
+ slice_start = slice_start && de_neg(slice_start)
1668
+ slice_end = slice_end && de_neg(slice_end)
1669
+
1670
+ // console.log('Descending', {curr_obj, path, subpath, field, slice_start, slice_end, last_obj})
1671
+
1672
+ // If it's the final item, set it
1673
+ if (path.length === subpath.length) {
1674
+ if (!subpath) return new_stuff
1675
+ else if (field) { // Object
1676
+ if (new_stuff === undefined)
1677
+ delete curr_obj[field] // - Delete a field in object
1678
+ else
1679
+ curr_obj[field] = new_stuff // - Set a field in object
1680
+ } else if (typeof curr_obj === 'string') { // String
1681
+ console.assert(typeof new_stuff === 'string')
1682
+ if (!slice_start) {slice_start = slice_end; slice_end = slice_end+1}
1683
+ if (last_obj) {
1684
+ var s = last_obj[last_field]
1685
+ last_obj[last_field] = (s.slice(0, slice_start)
1686
+ + new_stuff
1687
+ + s.slice(slice_end))
1688
+ } else
1689
+ return obj.slice(0, slice_start) + new_stuff + obj.slice(slice_end)
1690
+ } else // Array
1691
+ if (slice_start) // - Array splice
1692
+ [].splice.apply(curr_obj, [slice_start, slice_end-slice_start]
1693
+ .concat(new_stuff))
1694
+ else { // - Array set
1695
+ console.assert(slice_end >= 0, 'Index '+subpath+' is too small')
1696
+ console.assert(slice_end <= curr_obj.length - 1,
1697
+ 'Index '+subpath+' is too big')
1698
+ curr_obj[slice_end] = new_stuff
1699
+ }
1700
+
1701
+ return obj
1702
+ }
1703
+
1704
+ // Otherwise, descend down the path
1705
+ console.assert(!slice_start, 'No splices allowed in middle of path')
1706
+ last_obj = curr_obj
1707
+ last_field = field || slice_end
1708
+ curr_obj = curr_obj[last_field]
1709
+ path = path.substr(subpath.length)
1710
+ }
1711
+ }
1712
+
1621
1713
  braid_text.encode_filename = encode_filename
1622
1714
  braid_text.decode_filename = decode_filename
1623
1715
 
@@ -12,7 +12,7 @@ dom.BODY = -> DIV(WIKI())
12
12
  window.statebus_fetch = window.fetch
13
13
  window.fetch = window.og_fetch
14
14
  </script>
15
- <script src="https://unpkg.com/braid-http@~1.0/braid-http-client.js"></script>
15
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
16
16
  <script>
17
17
  window.fetch = window.statebus_fetch
18
18
  </script>
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braidjs",
7
7
  "homepage": "https://braid.org",
8
8
  "dependencies": {
9
9
  "diamond-types-node": "^1.0.2",
10
- "braid-http": "^1.0.7"
10
+ "braid-http": "^1.3.4"
11
11
  }
12
12
  }
package/server-demo.js CHANGED
@@ -34,6 +34,12 @@ var server = require("http").createServer(async (req, res) => {
34
34
  return
35
35
  }
36
36
 
37
+ if (req.url == '/test.html') {
38
+ res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-cache" })
39
+ require("fs").createReadStream("./test.html").pipe(res)
40
+ return
41
+ }
42
+
37
43
  // TODO: uncomment out the code below to add /pages endpoint,
38
44
  // which displays all the currently used keys
39
45
  //
@@ -1,4 +1,4 @@
1
- // requires braid-http@~1.0/braid-http-client.js
1
+ // requires braid-http@~1.3/braid-http-client.js
2
2
  //
3
3
  // url: resource endpoint
4
4
  //
@@ -41,10 +41,13 @@ function simpleton_client(url, { apply_remote_update, generate_local_diff_update
41
41
  if (current_version.length === update.parents.length
42
42
  && current_version.every((v, i) => v === update.parents[i])) {
43
43
  current_version = update.version.sort()
44
- update.state = update.body
44
+ update.state = update.body_text
45
45
 
46
46
  if (update.patches) {
47
- for (let p of update.patches) p.range = p.range.match(/\d+/g).map((x) => 1 * x)
47
+ for (let p of update.patches) {
48
+ p.range = p.range.match(/\d+/g).map((x) => 1 * x)
49
+ p.content = p.content_text
50
+ }
48
51
  update.patches.sort((a, b) => a.range[0] - b.range[0])
49
52
 
50
53
  // convert from code-points to js-indicies
package/test.html ADDED
@@ -0,0 +1,139 @@
1
+ <style>
2
+ body {
3
+ font-family: Arial, sans-serif;
4
+ max-width: 800px;
5
+ margin: 0 auto;
6
+ padding: 10px;
7
+ }
8
+ .test {
9
+ margin-bottom: 3px;
10
+ padding: 3px;
11
+ }
12
+ .running {
13
+ background-color: #fffde7;
14
+ }
15
+ .passed {
16
+ background-color: #e8f5e9;
17
+ }
18
+ .failed {
19
+ background-color: #ffebee;
20
+ }
21
+ </style>
22
+ <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
23
+ <div id="testContainer"></div>
24
+ <script type=module>
25
+
26
+ let delay = 0
27
+
28
+ function createTestDiv(testName) {
29
+ const div = document.createElement("div")
30
+ div.className = "test running"
31
+ div.innerHTML = `<span style="font-weight:bold">${testName}: </span><span class="result">Running...</span>`
32
+ testContainer.appendChild(div)
33
+ return div
34
+ }
35
+
36
+ function updateTestResult(div, passed, message, got, expected) {
37
+ div.className = `test ${passed ? "passed" : "failed"}`
38
+
39
+ if (passed) {
40
+ div.querySelector(".result").textContent = message
41
+ div.querySelector(".result").style.fontSize = message.length > 400 ? 'xx-small' : message.length > 100 ? 'small' : ''
42
+ } else {
43
+ div.querySelector(".result").innerHTML = `${message}<br><strong>Got:</strong> ${got}<br><strong>Expected:</strong> ${expected}`
44
+ }
45
+ }
46
+
47
+ async function runTest(testName, testFunction, expectedResult) {
48
+ delay += 70
49
+
50
+ await new Promise(done => setTimeout(done, delay))
51
+ const div = createTestDiv(testName)
52
+ try {
53
+ let x = await testFunction()
54
+ if (x == expectedResult) {
55
+ updateTestResult(div, true, x)
56
+ } else {
57
+ updateTestResult(div, false, "Mismatch:", x, expectedResult)
58
+ }
59
+ } catch (error) {
60
+ updateTestResult(div, false, "Error:", error.message || error, expectedResult)
61
+ }
62
+ }
63
+
64
+ runTest(
65
+ "test getting a binary update from a subscription",
66
+ async () => {
67
+ return await new Promise(async (done, fail) => {
68
+ let key = 'test-' + Math.random().toString(36).slice(2)
69
+
70
+ await fetch(`/${key}`, {
71
+ method: 'PUT',
72
+ body: JSON.stringify({a: 5, b: 6}, null, 4)
73
+ })
74
+
75
+ let r = await braid_fetch(`/${key}`, {
76
+ subscribe: true
77
+ })
78
+
79
+ r.subscribe(update => done(update.body_text), fail)
80
+ })
81
+ },
82
+ JSON.stringify({a: 5, b: 6}, null, 4)
83
+ )
84
+
85
+ runTest(
86
+ "test sending a json patch to some json-text",
87
+ async () => {
88
+ let key = 'test-' + Math.random().toString(36).slice(2)
89
+
90
+ await fetch(`/${key}`, {
91
+ method: 'PUT',
92
+ body: JSON.stringify({a: 5, b: 6})
93
+ })
94
+
95
+ await fetch(`/${key}`, {
96
+ method: 'PUT',
97
+ headers: { 'Content-Range': 'json a' },
98
+ body: '67'
99
+ })
100
+
101
+ let r = await fetch(`/${key}`)
102
+
103
+ return await r.text()
104
+ },
105
+ JSON.stringify({a: 67, b: 6}, null, 4)
106
+ )
107
+
108
+ runTest(
109
+ "test sending multiple json patches to some json-text",
110
+ async () => {
111
+ let key = 'test-' + Math.random().toString(36).slice(2)
112
+
113
+ await fetch(`/${key}`, {
114
+ method: 'PUT',
115
+ body: JSON.stringify({a: 5, b: 6, c: 7})
116
+ })
117
+
118
+ await braid_fetch(`/${key}`, {
119
+ method: 'PUT',
120
+ headers: { 'Content-Range': 'json a' },
121
+ patches: [{
122
+ unit: 'json',
123
+ range: 'a',
124
+ content: '55',
125
+ }, {
126
+ unit: 'json',
127
+ range: 'b',
128
+ content: '66',
129
+ }]
130
+ })
131
+
132
+ let r = await fetch(`/${key}`)
133
+
134
+ return await r.text()
135
+ },
136
+ JSON.stringify({a: 55, b: 66, c: 7}, null, 4)
137
+ )
138
+
139
+ </script>
package/test.js CHANGED
@@ -27,7 +27,7 @@ async function main() {
27
27
  og_log(`t = ${t}, seed = ${seed}, best_n = ${best_n} @ ${best_seed}`)
28
28
  Math.randomSeed(seed)
29
29
 
30
- let n = Math.floor(Math.random() * 15) + 5
30
+ let n = Math.floor(Math.random() * 15)
31
31
  console.log(`n = ${n}`)
32
32
 
33
33
  try {