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 +25 -0
- package/editor.html +1 -1
- package/index.js +92 -0
- package/markdown-editor.html +1 -1
- package/package.json +2 -2
- package/server-demo.js +6 -0
- package/simpleton-client.js +6 -3
- package/test.html +139 -0
- package/test.js +1 -1
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.
|
|
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
|
|
package/markdown-editor.html
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
//
|
package/simpleton-client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// requires braid-http@~1.
|
|
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.
|
|
44
|
+
update.state = update.body_text
|
|
45
45
|
|
|
46
46
|
if (update.patches) {
|
|
47
|
-
for (let p of update.patches)
|
|
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