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 +25 -0
- package/index.js +91 -0
- package/package.json +1 -1
- package/server-demo.js +6 -0
- package/test.html +118 -0
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
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>
|