braid-text 0.1.2 → 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 +178 -41
- package/package.json +1 -1
- package/server-demo.js +6 -0
- package/test.html +118 -0
- package/test.js +88 -122
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
|
@@ -220,10 +220,14 @@ braid_text.get = async (key, options) => {
|
|
|
220
220
|
let doc = resource.doc
|
|
221
221
|
if (options.version || options.parents) doc = dt_get(doc, options.version || options.parents)
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
let ret = {
|
|
224
224
|
version: doc.getRemoteVersion().map((x) => x.join("-")).sort(),
|
|
225
225
|
body: doc.get()
|
|
226
226
|
}
|
|
227
|
+
|
|
228
|
+
if (options.version || options.parents) doc.free()
|
|
229
|
+
|
|
230
|
+
return ret
|
|
227
231
|
} else {
|
|
228
232
|
if (options.merge_type != "dt") {
|
|
229
233
|
let version = resource.doc.getRemoteVersion().map((x) => x.join("-")).sort()
|
|
@@ -269,17 +273,12 @@ braid_text.get = async (key, options) => {
|
|
|
269
273
|
updates = dt_get_patches(resource.doc, options.parents || options.version)
|
|
270
274
|
}
|
|
271
275
|
|
|
272
|
-
for (let u of updates)
|
|
273
|
-
u.version = decode_version(u.version)
|
|
274
|
-
u.version[1] += u.end - u.start - 1
|
|
275
|
-
u.version = u.version.join("-")
|
|
276
|
-
|
|
276
|
+
for (let u of updates)
|
|
277
277
|
options.subscribe({
|
|
278
278
|
version: [u.version],
|
|
279
279
|
parents: u.parents,
|
|
280
280
|
patches: [{ unit: u.unit, range: u.range, content: u.content }],
|
|
281
281
|
})
|
|
282
|
-
}
|
|
283
282
|
|
|
284
283
|
// Output at least *some* data, or else chrome gets confused and
|
|
285
284
|
// thinks the connection failed. This isn't strictly necessary,
|
|
@@ -292,9 +291,32 @@ braid_text.get = async (key, options) => {
|
|
|
292
291
|
}
|
|
293
292
|
}
|
|
294
293
|
|
|
294
|
+
braid_text.forget = async (key, options) => {
|
|
295
|
+
if (!options) throw new Error('options is required')
|
|
296
|
+
|
|
297
|
+
let resource = (typeof key == 'string') ? await get_resource(key) : key
|
|
298
|
+
|
|
299
|
+
if (options.merge_type != "dt")
|
|
300
|
+
resource.simpleton_clients.delete(options)
|
|
301
|
+
else resource.clients.delete(options)
|
|
302
|
+
}
|
|
303
|
+
|
|
295
304
|
braid_text.put = async (key, options) => {
|
|
296
305
|
let { version, patches, body, peer } = options
|
|
297
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
|
+
|
|
298
320
|
if (version) validate_version_array(version)
|
|
299
321
|
|
|
300
322
|
// translate a single parent of "root" to the empty array (same meaning)
|
|
@@ -320,9 +342,15 @@ braid_text.put = async (key, options) => {
|
|
|
320
342
|
let parents = resource.doc.getRemoteVersion().map((x) => x.join("-")).sort()
|
|
321
343
|
let og_parents = options_parents || parents
|
|
322
344
|
|
|
345
|
+
function get_len() {
|
|
346
|
+
let d = dt_get(resource.doc, og_parents)
|
|
347
|
+
let len = d.len()
|
|
348
|
+
d.free()
|
|
349
|
+
return len
|
|
350
|
+
}
|
|
351
|
+
|
|
323
352
|
let max_pos = resource.length_cache.get('' + og_parents) ??
|
|
324
|
-
(v_eq(parents, og_parents) ? resource.doc.len() :
|
|
325
|
-
dt_get(resource.doc, og_parents).len())
|
|
353
|
+
(v_eq(parents, og_parents) ? resource.doc.len() : get_len())
|
|
326
354
|
|
|
327
355
|
if (body != null) {
|
|
328
356
|
patches = [{
|
|
@@ -336,7 +364,7 @@ braid_text.put = async (key, options) => {
|
|
|
336
364
|
patches = patches.map((p) => ({
|
|
337
365
|
...p,
|
|
338
366
|
range: p.range.match(/\d+/g).map((x) => parseInt(x)),
|
|
339
|
-
|
|
367
|
+
content_codepoints: [...p.content],
|
|
340
368
|
})).sort((a, b) => a.range[0] - b.range[0])
|
|
341
369
|
|
|
342
370
|
// validate patch positions
|
|
@@ -347,7 +375,7 @@ braid_text.put = async (key, options) => {
|
|
|
347
375
|
must_be_at_least = p.range[1]
|
|
348
376
|
}
|
|
349
377
|
|
|
350
|
-
let change_count = patches.reduce((a, b) => a + b.
|
|
378
|
+
let change_count = patches.reduce((a, b) => a + b.content_codepoints.length + (b.range[1] - b.range[0]), 0)
|
|
351
379
|
|
|
352
380
|
let og_v = version?.[0] || `${(is_valid_actor(peer) && peer) || Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
|
|
353
381
|
|
|
@@ -365,7 +393,6 @@ braid_text.put = async (key, options) => {
|
|
|
365
393
|
let seen = {}
|
|
366
394
|
for (let u of updates) {
|
|
367
395
|
u.version = decode_version(u.version)
|
|
368
|
-
u.version[1] += u.end - u.start - 1
|
|
369
396
|
|
|
370
397
|
if (!u.content) {
|
|
371
398
|
// delete
|
|
@@ -401,9 +428,9 @@ braid_text.put = async (key, options) => {
|
|
|
401
428
|
v = `${v[0]}-${v[1] + 1}`
|
|
402
429
|
}
|
|
403
430
|
// insert
|
|
404
|
-
for (let i = 0; i < p.
|
|
431
|
+
for (let i = 0; i < p.content_codepoints?.length ?? 0; i++) {
|
|
405
432
|
let vv = decode_version(v)
|
|
406
|
-
let c = p.
|
|
433
|
+
let c = p.content_codepoints[i]
|
|
407
434
|
|
|
408
435
|
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')
|
|
409
436
|
|
|
@@ -420,7 +447,7 @@ braid_text.put = async (key, options) => {
|
|
|
420
447
|
resource.actor_seqs[v[0]] = v[1]
|
|
421
448
|
|
|
422
449
|
resource.length_cache.put(`${v[0]}-${v[1]}`, patches.reduce((a, b) =>
|
|
423
|
-
a + (b.
|
|
450
|
+
a + (b.content_codepoints.length ? b.content_codepoints.length : -(b.range[1] - b.range[0])),
|
|
424
451
|
max_pos))
|
|
425
452
|
|
|
426
453
|
v = `${v[0]}-${v[1] + 1 - change_count}`
|
|
@@ -434,21 +461,21 @@ braid_text.put = async (key, options) => {
|
|
|
434
461
|
let offset = 0
|
|
435
462
|
for (let p of patches) {
|
|
436
463
|
// delete
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
offset
|
|
440
|
-
|
|
464
|
+
let del = p.range[1] - p.range[0]
|
|
465
|
+
if (del) {
|
|
466
|
+
bytes.push(dt_create_bytes(v, ps, p.range[0] + offset, del, null))
|
|
467
|
+
offset -= del
|
|
441
468
|
v = decode_version(v)
|
|
442
|
-
|
|
469
|
+
ps = [`${v[0]}-${v[1] + (del - 1)}`]
|
|
470
|
+
v = `${v[0]}-${v[1] + del}`
|
|
443
471
|
}
|
|
444
472
|
// insert
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
offset++
|
|
449
|
-
ps = [v]
|
|
473
|
+
if (p.content?.length) {
|
|
474
|
+
bytes.push(dt_create_bytes(v, ps, p.range[1] + offset, 0, p.content))
|
|
475
|
+
offset += p.content_codepoints.length
|
|
450
476
|
v = decode_version(v)
|
|
451
|
-
|
|
477
|
+
ps = [`${v[0]}-${v[1] + (p.content_codepoints.length - 1)}`]
|
|
478
|
+
v = `${v[0]}-${v[1] + p.content_codepoints.length}`
|
|
452
479
|
}
|
|
453
480
|
}
|
|
454
481
|
|
|
@@ -787,6 +814,8 @@ async function file_sync(key, process_delta, get_init) {
|
|
|
787
814
|
//////////////////////////////////////////////////////////////////
|
|
788
815
|
|
|
789
816
|
function dt_get(doc, version, agent = null) {
|
|
817
|
+
if (dt_get.last_doc) dt_get.last_doc.free()
|
|
818
|
+
|
|
790
819
|
let bytes = doc.toBytes()
|
|
791
820
|
dt_get.last_doc = doc = Doc.fromBytes(bytes, agent)
|
|
792
821
|
|
|
@@ -834,6 +863,7 @@ function dt_get(doc, version, agent = null) {
|
|
|
834
863
|
op_run.start + (i - base_i) :
|
|
835
864
|
op_run.start) :
|
|
836
865
|
op_run.end - 1 - (i - base_i),
|
|
866
|
+
op_run.content?.[i - base_i] != null ? 0 : 1,
|
|
837
867
|
op_run.content?.[i - base_i]
|
|
838
868
|
)
|
|
839
869
|
)
|
|
@@ -866,12 +896,16 @@ function dt_get_patches(doc, version = null) {
|
|
|
866
896
|
|
|
867
897
|
before_doc.mergeBytes(after_bytes)
|
|
868
898
|
op_runs = before_doc.getOpsSince(before_doc_frontier)
|
|
899
|
+
|
|
900
|
+
before_doc.free()
|
|
869
901
|
} else op_runs = doc.getOpsSince([])
|
|
870
902
|
|
|
903
|
+
doc.free()
|
|
904
|
+
|
|
871
905
|
let i = 0
|
|
872
906
|
let patches = []
|
|
873
907
|
op_runs.forEach((op_run) => {
|
|
874
|
-
let version = versions[i]
|
|
908
|
+
let version = versions[i]
|
|
875
909
|
let parents = parentss[i].map((x) => x.join("-")).sort()
|
|
876
910
|
let start = op_run.start
|
|
877
911
|
let end = start + 1
|
|
@@ -899,7 +933,7 @@ function dt_get_patches(doc, version = null) {
|
|
|
899
933
|
op_run.start + (end - start)) :
|
|
900
934
|
(op_run.end - (start - op_run.start))
|
|
901
935
|
patches.push({
|
|
902
|
-
version
|
|
936
|
+
version: `${version[0]}-${version[1] + e - s - 1}`,
|
|
903
937
|
parents,
|
|
904
938
|
unit: "text",
|
|
905
939
|
range: op_run.content ? `[${s}:${s}]` : `[${s}:${e}]`,
|
|
@@ -908,7 +942,7 @@ function dt_get_patches(doc, version = null) {
|
|
|
908
942
|
end: e,
|
|
909
943
|
})
|
|
910
944
|
if (j == len) break
|
|
911
|
-
version = versions[I]
|
|
945
|
+
version = versions[I]
|
|
912
946
|
parents = parentss[I].map((x) => x.join("-")).sort()
|
|
913
947
|
start = op_run.start + j
|
|
914
948
|
}
|
|
@@ -1014,7 +1048,8 @@ function dt_parse(byte_array) {
|
|
|
1014
1048
|
return [agents, versions, parentss]
|
|
1015
1049
|
}
|
|
1016
1050
|
|
|
1017
|
-
function dt_create_bytes(version, parents, pos, ins) {
|
|
1051
|
+
function dt_create_bytes(version, parents, pos, del, ins) {
|
|
1052
|
+
if (del) pos += del - 1
|
|
1018
1053
|
|
|
1019
1054
|
function write_varint(bytes, value) {
|
|
1020
1055
|
while (value >= 0x80) {
|
|
@@ -1085,6 +1120,8 @@ function dt_create_bytes(version, parents, pos, ins) {
|
|
|
1085
1120
|
|
|
1086
1121
|
let patches = []
|
|
1087
1122
|
|
|
1123
|
+
let unicode_chars = ins ? [...ins] : []
|
|
1124
|
+
|
|
1088
1125
|
if (ins) {
|
|
1089
1126
|
let inserted_content_bytes = []
|
|
1090
1127
|
|
|
@@ -1095,18 +1132,21 @@ function dt_create_bytes(version, parents, pos, ins) {
|
|
|
1095
1132
|
let encoder = new TextEncoder()
|
|
1096
1133
|
let utf8Bytes = encoder.encode(ins)
|
|
1097
1134
|
|
|
1098
|
-
inserted_content_bytes
|
|
1135
|
+
write_varint(inserted_content_bytes, 1 + utf8Bytes.length)
|
|
1136
|
+
// inserted_content_bytes.push(1 + utf8Bytes.length) // length of content chunk
|
|
1099
1137
|
inserted_content_bytes.push(4) // "plain text" enum
|
|
1100
1138
|
|
|
1101
1139
|
for (let b of utf8Bytes) inserted_content_bytes.push(b) // actual text
|
|
1102
1140
|
|
|
1103
1141
|
inserted_content_bytes.push(25) // "known" enum
|
|
1104
|
-
|
|
1105
|
-
|
|
1142
|
+
let known_chunk = []
|
|
1143
|
+
write_varint(known_chunk, unicode_chars.length * 2 + 1)
|
|
1144
|
+
write_varint(inserted_content_bytes, known_chunk.length)
|
|
1145
|
+
inserted_content_bytes.push(...known_chunk)
|
|
1106
1146
|
|
|
1107
1147
|
patches.push(24)
|
|
1108
1148
|
write_varint(patches, inserted_content_bytes.length)
|
|
1109
|
-
patches.push(
|
|
1149
|
+
for (let b of inserted_content_bytes) patches.push(b)
|
|
1110
1150
|
}
|
|
1111
1151
|
|
|
1112
1152
|
// write in the version
|
|
@@ -1117,26 +1157,43 @@ function dt_create_bytes(version, parents, pos, ins) {
|
|
|
1117
1157
|
let jump = seq
|
|
1118
1158
|
|
|
1119
1159
|
write_varint(version_bytes, ((agent_i + 1) << 1) | (jump != 0 ? 1 : 0))
|
|
1120
|
-
write_varint(version_bytes,
|
|
1160
|
+
write_varint(version_bytes, ins ? unicode_chars.length : del)
|
|
1121
1161
|
if (jump) write_varint(version_bytes, jump << 1)
|
|
1122
1162
|
|
|
1123
1163
|
patches.push(21)
|
|
1124
1164
|
write_varint(patches, version_bytes.length)
|
|
1125
|
-
patches.push(
|
|
1165
|
+
for (let b of version_bytes) patches.push(b)
|
|
1126
1166
|
|
|
1127
1167
|
// write in "op" bytes (some encoding of position)
|
|
1128
1168
|
let op_bytes = []
|
|
1129
1169
|
|
|
1130
|
-
|
|
1170
|
+
if (del) {
|
|
1171
|
+
if (pos == 0) {
|
|
1172
|
+
write_varint(op_bytes, 4)
|
|
1173
|
+
} else if (del == 1) {
|
|
1174
|
+
write_varint(op_bytes, pos * 16 + 6)
|
|
1175
|
+
} else {
|
|
1176
|
+
write_varint(op_bytes, del * 16 + 7)
|
|
1177
|
+
write_varint(op_bytes, pos * 2 + 2)
|
|
1178
|
+
}
|
|
1179
|
+
} else if (unicode_chars.length == 1) {
|
|
1180
|
+
if (pos == 0) write_varint(op_bytes, 0)
|
|
1181
|
+
else write_varint(op_bytes, pos * 16 + 2)
|
|
1182
|
+
} else if (pos == 0) {
|
|
1183
|
+
write_varint(op_bytes, unicode_chars.length * 8 + 1)
|
|
1184
|
+
} else {
|
|
1185
|
+
write_varint(op_bytes, unicode_chars.length * 8 + 3)
|
|
1186
|
+
write_varint(op_bytes, pos * 2)
|
|
1187
|
+
}
|
|
1131
1188
|
|
|
1132
1189
|
patches.push(22)
|
|
1133
1190
|
write_varint(patches, op_bytes.length)
|
|
1134
|
-
patches.push(
|
|
1191
|
+
for (let b of op_bytes) patches.push(b)
|
|
1135
1192
|
|
|
1136
1193
|
// write in parents
|
|
1137
1194
|
let parents_bytes = []
|
|
1138
1195
|
|
|
1139
|
-
write_varint(parents_bytes,
|
|
1196
|
+
write_varint(parents_bytes, ins ? unicode_chars.length : del)
|
|
1140
1197
|
|
|
1141
1198
|
if (parents.length) {
|
|
1142
1199
|
for (let [i, [agent, seq]] of parents.entries()) {
|
|
@@ -1154,14 +1211,16 @@ function dt_create_bytes(version, parents, pos, ins) {
|
|
|
1154
1211
|
// write in patches
|
|
1155
1212
|
bytes.push(20)
|
|
1156
1213
|
write_varint(bytes, patches.length)
|
|
1157
|
-
bytes.push(
|
|
1214
|
+
for (let b of patches) bytes.push(b)
|
|
1158
1215
|
|
|
1159
1216
|
// console.log(bytes);
|
|
1160
1217
|
return bytes
|
|
1161
1218
|
}
|
|
1162
1219
|
|
|
1163
1220
|
function defrag_dt(doc) {
|
|
1164
|
-
|
|
1221
|
+
let bytes = doc.toBytes()
|
|
1222
|
+
doc.free()
|
|
1223
|
+
return Doc.fromBytes(bytes, 'server')
|
|
1165
1224
|
}
|
|
1166
1225
|
|
|
1167
1226
|
function OpLog_remote_to_local(doc, frontier) {
|
|
@@ -1572,6 +1631,84 @@ function createSimpleCache(size) {
|
|
|
1572
1631
|
}
|
|
1573
1632
|
}
|
|
1574
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
|
+
|
|
1575
1712
|
braid_text.encode_filename = encode_filename
|
|
1576
1713
|
braid_text.decode_filename = decode_filename
|
|
1577
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>
|
package/test.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
|
|
2
2
|
let { Doc } = require("diamond-types-node")
|
|
3
|
-
let
|
|
3
|
+
let braid_text = require('./index.js')
|
|
4
|
+
let {dt_get, dt_get_patches, dt_parse, dt_create_bytes} = braid_text
|
|
5
|
+
|
|
6
|
+
process.on("unhandledRejection", (x) =>
|
|
7
|
+
console.log(`unhandledRejection: ${x.stack}`)
|
|
8
|
+
)
|
|
9
|
+
process.on("uncaughtException", (x) =>
|
|
10
|
+
console.log(`uncaughtException: ${x.stack}`)
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
braid_text.db_folder = null
|
|
4
14
|
|
|
5
15
|
async function main() {
|
|
6
|
-
let best_seed =
|
|
16
|
+
let best_seed = NaN
|
|
7
17
|
let best_n = Infinity
|
|
8
18
|
let base = Math.floor(Math.random() * 10000000)
|
|
9
19
|
|
|
@@ -11,89 +21,89 @@ async function main() {
|
|
|
11
21
|
console.log = () => {}
|
|
12
22
|
for (let t = 0; t < 10000; t++) {
|
|
13
23
|
let seed = base + t
|
|
14
|
-
// for (let t = 0; t <
|
|
15
|
-
// let seed =
|
|
24
|
+
// for (let t = 0; t < 1; t++) {
|
|
25
|
+
// let seed = 7572861
|
|
16
26
|
|
|
17
27
|
og_log(`t = ${t}, seed = ${seed}, best_n = ${best_n} @ ${best_seed}`)
|
|
18
28
|
Math.randomSeed(seed)
|
|
19
29
|
|
|
20
|
-
let n = Math.floor(Math.random() * 15)
|
|
30
|
+
let n = Math.floor(Math.random() * 15) + 5
|
|
21
31
|
console.log(`n = ${n}`)
|
|
22
32
|
|
|
23
33
|
try {
|
|
24
|
-
//
|
|
34
|
+
// create a bunch of edits called doc,
|
|
35
|
+
// and remember a point along the way of adding all these edits,
|
|
36
|
+
// called middle_doc
|
|
25
37
|
let doc = new Doc('server')
|
|
26
|
-
|
|
27
38
|
let middle_doc = null
|
|
28
39
|
|
|
29
|
-
if (!middle_doc && (Math.random() < 1/n || n == 0))
|
|
40
|
+
if (!middle_doc && (Math.random() < 1/n || n == 0))
|
|
30
41
|
middle_doc = Doc.fromBytes(doc.toBytes())
|
|
31
|
-
}
|
|
32
42
|
for (let i = 0; i < n; i++) {
|
|
33
|
-
|
|
43
|
+
console.log(`edit ${i}`)
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
make_random_edit(doc)
|
|
46
|
+
if (!middle_doc && (Math.random() < 1/n || i == n - 1))
|
|
36
47
|
middle_doc = Doc.fromBytes(doc.toBytes())
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (!middle_doc) throw 'bad'
|
|
40
|
-
|
|
41
|
-
// 2. let x = the resulting string
|
|
42
|
-
let x = doc.get()
|
|
43
|
-
console.log('x = ' + x)
|
|
44
|
-
|
|
45
|
-
// // 3. use the code for sending these edits over the wire to create a new dt
|
|
46
|
-
let updates = dt_get_patches(doc)
|
|
47
|
-
console.log(updates)
|
|
48
|
-
|
|
49
|
-
let new_doc = new Doc('server')
|
|
50
|
-
apply_updates(new_doc, updates)
|
|
51
|
-
let y = new_doc.get()
|
|
52
|
-
console.log('y = ' + y)
|
|
53
|
-
|
|
54
|
-
// 4. is the resulting string == x?
|
|
55
|
-
console.log(x == y)
|
|
56
|
-
if (x != y && n < best_n) {
|
|
57
|
-
best_n = n
|
|
58
|
-
best_seed = seed
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 5. test dt_get
|
|
62
|
-
let middle_v = middle_doc.getRemoteVersion().map(x => x.join('-'))
|
|
63
|
-
let new_middle_doc = dt_get(doc, middle_v)
|
|
64
|
-
console.log('new_middle_doc = ' + new_middle_doc.get())
|
|
65
|
-
if (middle_doc.get() != new_middle_doc.get() && n < best_n) {
|
|
66
|
-
best_n = n
|
|
67
|
-
best_seed = seed
|
|
68
48
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
49
|
+
if (!middle_doc) throw new Error('bad')
|
|
50
|
+
|
|
51
|
+
// put them into braid-text
|
|
52
|
+
let dt_to_braid = async (doc, key) => {
|
|
53
|
+
await braid_text.get(key, {})
|
|
54
|
+
for (let x of dt_get_patches(doc)) {
|
|
55
|
+
console.log(`x = `, x)
|
|
56
|
+
let y = {
|
|
57
|
+
merge_type: 'dt',
|
|
58
|
+
version: [x.version],
|
|
59
|
+
parents: x.parents,
|
|
60
|
+
patches: [{
|
|
61
|
+
unit: x.unit,
|
|
62
|
+
range: x.range,
|
|
63
|
+
content: x.content
|
|
64
|
+
}]
|
|
65
|
+
}
|
|
66
|
+
await braid_text.put(key, y)
|
|
67
|
+
y.validate_already_seen_versions = true
|
|
68
|
+
await braid_text.put(key, y)
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
71
|
+
await dt_to_braid(doc, 'doc')
|
|
72
|
+
await dt_to_braid(middle_doc, 'middle_doc')
|
|
73
|
+
console.log(`doc dt = ${doc.get()}`)
|
|
74
|
+
console.log(`middle_doc dt = ${middle_doc.get()}`)
|
|
75
|
+
console.log(`doc = ${await braid_text.get('doc')}`)
|
|
76
|
+
console.log(`middle_doc = ${await braid_text.get('middle_doc')}`)
|
|
77
|
+
|
|
78
|
+
// ensure they look right
|
|
79
|
+
if (doc.get() != await braid_text.get('doc')) throw new Error('bad')
|
|
80
|
+
if (middle_doc.get() != await braid_text.get('middle_doc')) throw new Error('bad')
|
|
81
|
+
|
|
82
|
+
// test getting old version
|
|
83
|
+
let middle_v = middle_doc.getRemoteVersion().map(x => x.join('-'))
|
|
84
|
+
console.log(`middle_doc = ${await braid_text.get('middle_doc')}`)
|
|
85
|
+
console.log(`middle_v = `, middle_v)
|
|
86
|
+
|
|
87
|
+
let doc_v = doc.getRemoteVersion().map(x => x.join('-'))
|
|
88
|
+
console.log(`doc_v = `, doc_v)
|
|
89
|
+
|
|
90
|
+
console.log(`doc = `, await braid_text.get('doc', {version: middle_v}))
|
|
91
|
+
if (await braid_text.get('middle_doc') != (await braid_text.get('doc', {version: middle_v})).body) throw new Error('bad')
|
|
92
|
+
|
|
93
|
+
// try getting updates from middle_doc to doc
|
|
94
|
+
let o = {merge_type: 'dt', parents: middle_v, subscribe: update => {
|
|
95
|
+
braid_text.put('middle_doc', update)
|
|
96
|
+
}}
|
|
97
|
+
await braid_text.get('doc', o)
|
|
98
|
+
await braid_text.forget('doc', o)
|
|
99
|
+
|
|
100
|
+
if (await braid_text.get('middle_doc') != await braid_text.get('doc')) throw new Error('bad')
|
|
101
|
+
|
|
102
|
+
doc.free()
|
|
103
|
+
middle_doc.free()
|
|
104
|
+
for (let p of Object.values(braid_text.cache))
|
|
105
|
+
(await p).doc.free()
|
|
106
|
+
braid_text.cache = {}
|
|
97
107
|
} catch (e) {
|
|
98
108
|
if (console.log == og_log) throw e
|
|
99
109
|
if (n < best_n) {
|
|
@@ -132,74 +142,30 @@ function make_random_edit(doc) {
|
|
|
132
142
|
let len = parent_doc.len()
|
|
133
143
|
console.log(`len = ${len}`)
|
|
134
144
|
|
|
145
|
+
parent_doc.free()
|
|
146
|
+
|
|
135
147
|
if (len && Math.random() > 0.5) {
|
|
136
148
|
// delete
|
|
137
149
|
let start = Math.floor(Math.random() * len)
|
|
138
150
|
let del_len = Math.floor(Math.random() * (len - start - 1)) + 1
|
|
139
151
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
console.log(args)
|
|
144
|
-
doc.mergeBytes(dt_create_bytes(...args))
|
|
145
|
-
parents = [v]
|
|
146
|
-
}
|
|
152
|
+
let args = [`${agent}-${base_seq}`, parents, start, del_len, null]
|
|
153
|
+
console.log(args)
|
|
154
|
+
doc.mergeBytes(dt_create_bytes(...args))
|
|
147
155
|
} else {
|
|
148
156
|
// insert
|
|
149
157
|
let start = Math.floor(Math.random() * (len + 1))
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.log(args)
|
|
156
|
-
doc.mergeBytes(dt_create_bytes(...args))
|
|
157
|
-
parents = [v]
|
|
158
|
-
}
|
|
158
|
+
let ins = Array(Math.floor(Math.random() * 10) + 1).fill(0).map(() => getRandomCharacter()).join('')
|
|
159
|
+
|
|
160
|
+
let args = [`${agent}-${base_seq}`, parents, start, 0, ins]
|
|
161
|
+
console.log(args)
|
|
162
|
+
doc.mergeBytes(dt_create_bytes(...args))
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
// work here
|
|
162
166
|
console.log(`doc => ${doc.get()}`)
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
function apply_updates(doc, updates) {
|
|
166
|
-
for (let u of updates) {
|
|
167
|
-
u.range = u.range.match(/\d+/g).map((x) => parseInt(x))
|
|
168
|
-
u.content = [...u.content]
|
|
169
|
-
|
|
170
|
-
let v = u.version
|
|
171
|
-
let ps = u.parents
|
|
172
|
-
|
|
173
|
-
console.log('UPDATE:', u)
|
|
174
|
-
|
|
175
|
-
// delete
|
|
176
|
-
for (let i = u.range[1] - 1; i >= u.range[0]; i--) {
|
|
177
|
-
|
|
178
|
-
// work here
|
|
179
|
-
let args = [v, ps, i, null]
|
|
180
|
-
console.log(`args`, args)
|
|
181
|
-
|
|
182
|
-
doc.mergeBytes(dt_create_bytes(...args))
|
|
183
|
-
ps = [v]
|
|
184
|
-
v = decode_version(v)
|
|
185
|
-
v = `${v[0]}-${v[1] + 1}`
|
|
186
|
-
}
|
|
187
|
-
// insert
|
|
188
|
-
for (let i = 0; i < u.content?.length ?? 0; i++) {
|
|
189
|
-
let c = u.content[i]
|
|
190
|
-
|
|
191
|
-
// work here
|
|
192
|
-
let args = [v, ps, u.range[0] + i, c]
|
|
193
|
-
console.log(`args`, args)
|
|
194
|
-
|
|
195
|
-
doc.mergeBytes(dt_create_bytes(...args))
|
|
196
|
-
ps = [v]
|
|
197
|
-
v = decode_version(v)
|
|
198
|
-
v = `${v[0]}-${v[1] + 1}`
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
169
|
//////////////////////////////////////////////////////////////////
|
|
204
170
|
//////////////////////////////////////////////////////////////////
|
|
205
171
|
//////////////////////////////////////////////////////////////////
|