braid-text 0.1.3 → 0.1.4

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/index.js CHANGED
@@ -304,6 +304,19 @@ braid_text.forget = async (key, options) => {
304
304
  braid_text.put = async (key, options) => {
305
305
  let { version, patches, body, peer } = options
306
306
 
307
+ // support for json patch puts..
308
+ if (patches?.length && patches.every(x => x.unit === 'json')) {
309
+ let resource = (typeof key == 'string') ? await get_resource(key) : key
310
+
311
+ let x = JSON.parse(resource.doc.get())
312
+ for (let p of patches)
313
+ apply_patch(x, p.range, JSON.parse(p.content))
314
+
315
+ return await braid_text.put(key, {
316
+ body: JSON.stringify(x, null, 4)
317
+ })
318
+ }
319
+
307
320
  if (version) validate_version_array(version)
308
321
 
309
322
  // translate a single parent of "root" to the empty array (same meaning)
@@ -1618,6 +1631,84 @@ function createSimpleCache(size) {
1618
1631
  }
1619
1632
  }
1620
1633
 
1634
+ function apply_patch(obj, range, content) {
1635
+
1636
+ // Descend down a bunch of objects until we get to the final object
1637
+ // The final object can be a slice
1638
+ // Set the value in the final object
1639
+
1640
+ var path = range,
1641
+ new_stuff = content
1642
+
1643
+ var path_segment = /^(\.?([^\.\[]+))|(\[((-?\d+):)?(-?\d+)\])|\[("(\\"|[^"])*")\]/
1644
+ var curr_obj = obj,
1645
+ last_obj = null
1646
+
1647
+ // Handle negative indices, like "[-9]" or "[-0]"
1648
+ function de_neg (x) {
1649
+ return x[0] === '-'
1650
+ ? curr_obj.length - parseInt(x.substr(1), 10)
1651
+ : parseInt(x, 10)
1652
+ }
1653
+
1654
+ // Now iterate through each segment of the range e.g. [3].a.b[3][9]
1655
+ while (true) {
1656
+ var match = path_segment.exec(path),
1657
+ subpath = match ? match[0] : '',
1658
+ field = match && match[2],
1659
+ slice_start = match && match[5],
1660
+ slice_end = match && match[6],
1661
+ quoted_field = match && match[7]
1662
+
1663
+ // The field could be expressed as ["nnn"] instead of .nnn
1664
+ if (quoted_field) field = JSON.parse(quoted_field)
1665
+
1666
+ slice_start = slice_start && de_neg(slice_start)
1667
+ slice_end = slice_end && de_neg(slice_end)
1668
+
1669
+ // console.log('Descending', {curr_obj, path, subpath, field, slice_start, slice_end, last_obj})
1670
+
1671
+ // If it's the final item, set it
1672
+ if (path.length === subpath.length) {
1673
+ if (!subpath) return new_stuff
1674
+ else if (field) { // Object
1675
+ if (new_stuff === undefined)
1676
+ delete curr_obj[field] // - Delete a field in object
1677
+ else
1678
+ curr_obj[field] = new_stuff // - Set a field in object
1679
+ } else if (typeof curr_obj === 'string') { // String
1680
+ console.assert(typeof new_stuff === 'string')
1681
+ if (!slice_start) {slice_start = slice_end; slice_end = slice_end+1}
1682
+ if (last_obj) {
1683
+ var s = last_obj[last_field]
1684
+ last_obj[last_field] = (s.slice(0, slice_start)
1685
+ + new_stuff
1686
+ + s.slice(slice_end))
1687
+ } else
1688
+ return obj.slice(0, slice_start) + new_stuff + obj.slice(slice_end)
1689
+ } else // Array
1690
+ if (slice_start) // - Array splice
1691
+ [].splice.apply(curr_obj, [slice_start, slice_end-slice_start]
1692
+ .concat(new_stuff))
1693
+ else { // - Array set
1694
+ console.assert(slice_end >= 0, 'Index '+subpath+' is too small')
1695
+ console.assert(slice_end <= curr_obj.length - 1,
1696
+ 'Index '+subpath+' is too big')
1697
+ curr_obj[slice_end] = new_stuff
1698
+ }
1699
+
1700
+ return obj
1701
+ }
1702
+
1703
+ // Otherwise, descend down the path
1704
+ console.assert(!slice_start, 'No splices allowed in middle of path')
1705
+ last_obj = curr_obj
1706
+ last_field = field || slice_end
1707
+ curr_obj = curr_obj[last_field]
1708
+ path = path.substr(subpath.length)
1709
+ }
1710
+ }
1711
+
1621
1712
  braid_text.encode_filename = encode_filename
1622
1713
  braid_text.decode_filename = decode_filename
1623
1714
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braidjs",
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
  //
package/test.html ADDED
@@ -0,0 +1,118 @@
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.1/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 sending a json patch to some json-text",
66
+ async () => {
67
+ let key = 'test-' + Math.random().toString(36).slice(2)
68
+
69
+ await fetch(`/${key}`, {
70
+ method: 'PUT',
71
+ body: JSON.stringify({a: 5, b: 6})
72
+ })
73
+
74
+ await fetch(`/${key}`, {
75
+ method: 'PUT',
76
+ headers: { 'Content-Range': 'json a' },
77
+ body: '67'
78
+ })
79
+
80
+ let r = await fetch(`/${key}`)
81
+
82
+ return await r.text()
83
+ },
84
+ JSON.stringify({a: 67, b: 6}, null, 4)
85
+ )
86
+
87
+ runTest(
88
+ "test sending multiple json patches to some json-text",
89
+ async () => {
90
+ let key = 'test-' + Math.random().toString(36).slice(2)
91
+
92
+ await fetch(`/${key}`, {
93
+ method: 'PUT',
94
+ body: JSON.stringify({a: 5, b: 6, c: 7})
95
+ })
96
+
97
+ await braid_fetch(`/${key}`, {
98
+ method: 'PUT',
99
+ headers: { 'Content-Range': 'json a' },
100
+ patches: [{
101
+ unit: 'json',
102
+ range: 'a',
103
+ content: '55',
104
+ }, {
105
+ unit: 'json',
106
+ range: 'b',
107
+ content: '66',
108
+ }]
109
+ })
110
+
111
+ let r = await fetch(`/${key}`)
112
+
113
+ return await r.text()
114
+ },
115
+ JSON.stringify({a: 55, b: 66, c: 7}, null, 4)
116
+ )
117
+
118
+ </script>