braid-text 0.0.14 → 0.0.15
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/index.js +193 -92
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ let braid_text = {
|
|
|
10
10
|
let waiting_puts = 0
|
|
11
11
|
let prev_put_p = null
|
|
12
12
|
|
|
13
|
-
let max_encoded_key_size =
|
|
13
|
+
let max_encoded_key_size = 240
|
|
14
14
|
|
|
15
15
|
braid_text.serve = async (req, res, options = {}) => {
|
|
16
16
|
options = {
|
|
@@ -19,12 +19,32 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
19
19
|
...options // Override with all options passed in
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// free CORS
|
|
23
|
+
res.setHeader("Access-Control-Allow-Origin", "*")
|
|
24
|
+
res.setHeader("Access-Control-Allow-Methods", "*")
|
|
25
|
+
res.setHeader("Access-Control-Allow-Headers", "*")
|
|
26
|
+
res.setHeader("Access-Control-Expose-Headers", "*")
|
|
27
|
+
|
|
28
|
+
function my_end(statusCode, x) {
|
|
29
|
+
res.statusCode = statusCode
|
|
30
|
+
res.end(x ?? '')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let resource = null
|
|
34
|
+
try {
|
|
35
|
+
resource = await get_resource(options.key)
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
braidify(req, res)
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return my_end(400, "The server failed to process this request. The error generated was: " + e)
|
|
40
|
+
}
|
|
25
41
|
|
|
26
42
|
let peer = req.headers["peer"]
|
|
27
43
|
|
|
44
|
+
let merge_type = req.headers["merge-type"]
|
|
45
|
+
if (!merge_type) merge_type = 'simpleton'
|
|
46
|
+
if (merge_type !== 'simpleton' && merge_type !== 'dt') return my_end(400, `Unknown merge type: ${merge_type}`)
|
|
47
|
+
|
|
28
48
|
// set default content type of text/plain
|
|
29
49
|
if (!res.getHeader('content-type')) res.setHeader('Content-Type', 'text/plain')
|
|
30
50
|
|
|
@@ -43,17 +63,6 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
43
63
|
res.setHeader('Content-Type', updatedContentType);
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
// free CORS
|
|
47
|
-
res.setHeader("Access-Control-Allow-Origin", "*")
|
|
48
|
-
res.setHeader("Access-Control-Allow-Methods", "*")
|
|
49
|
-
res.setHeader("Access-Control-Allow-Headers", "*")
|
|
50
|
-
res.setHeader("Access-Control-Expose-Headers", "*")
|
|
51
|
-
|
|
52
|
-
function my_end(statusCode, x) {
|
|
53
|
-
res.statusCode = statusCode
|
|
54
|
-
res.end(x ?? '')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
66
|
if (req.method == "OPTIONS") return my_end(200)
|
|
58
67
|
|
|
59
68
|
if (req.method == "DELETE") {
|
|
@@ -65,7 +74,12 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
65
74
|
if (!req.subscribe) {
|
|
66
75
|
res.setHeader("Accept-Subscribe", "true")
|
|
67
76
|
|
|
68
|
-
let x =
|
|
77
|
+
let x = null
|
|
78
|
+
try {
|
|
79
|
+
x = await braid_text.get(resource, { version: req.version, parents: req.parents })
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return my_end(400, "The server failed to get something. The error generated was: " + e)
|
|
82
|
+
}
|
|
69
83
|
|
|
70
84
|
res.setHeader("Version", x.version.map((x) => JSON.stringify(x)).join(", "))
|
|
71
85
|
|
|
@@ -77,26 +91,30 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
77
91
|
return my_end(200, buffer)
|
|
78
92
|
} else {
|
|
79
93
|
res.setHeader("Editable", "true")
|
|
80
|
-
res.setHeader("Merge-Type",
|
|
94
|
+
res.setHeader("Merge-Type", merge_type)
|
|
81
95
|
if (req.method == "HEAD") return my_end(200)
|
|
82
96
|
|
|
83
97
|
let options = {
|
|
84
98
|
peer,
|
|
85
99
|
version: req.version,
|
|
86
100
|
parents: req.parents,
|
|
87
|
-
merge_type
|
|
101
|
+
merge_type,
|
|
88
102
|
subscribe: x => res.sendVersion(x),
|
|
89
103
|
write: (x) => res.write(x)
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
res.startSubscription({
|
|
93
107
|
onClose: () => {
|
|
94
|
-
if (
|
|
108
|
+
if (merge_type === "dt") resource.clients.delete(options)
|
|
95
109
|
else resource.simpleton_clients.delete(options)
|
|
96
110
|
}
|
|
97
111
|
})
|
|
98
112
|
|
|
99
|
-
|
|
113
|
+
try {
|
|
114
|
+
return await braid_text.get(resource, options)
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return my_end(400, "The server failed to get something. The error generated was: " + e)
|
|
117
|
+
}
|
|
100
118
|
}
|
|
101
119
|
}
|
|
102
120
|
|
|
@@ -136,7 +154,7 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
136
154
|
patches = null
|
|
137
155
|
}
|
|
138
156
|
|
|
139
|
-
await braid_text.put(resource, { peer, version: req.version, parents: req.parents, patches, body, merge_type
|
|
157
|
+
await braid_text.put(resource, { peer, version: req.version, parents: req.parents, patches, body, merge_type })
|
|
140
158
|
|
|
141
159
|
options.put_cb(options.key, resource.doc.get())
|
|
142
160
|
} catch (e) {
|
|
@@ -165,7 +183,7 @@ braid_text.serve = async (req, res, options = {}) => {
|
|
|
165
183
|
// - 428 Precondition Required
|
|
166
184
|
// - pros: the name sounds right
|
|
167
185
|
// - cons: typically implies that the request was missing an http conditional field like If-Match. that is to say, it implies that the request is missing a precondition, not that the server is missing a precondition
|
|
168
|
-
return done_my_turn(425, "The server failed to apply this version.")
|
|
186
|
+
return done_my_turn(425, "The server failed to apply this version. The error generated was: " + e)
|
|
169
187
|
}
|
|
170
188
|
|
|
171
189
|
return done_my_turn(200)
|
|
@@ -183,6 +201,9 @@ braid_text.get = async (key, options) => {
|
|
|
183
201
|
return (await get_resource(key)).doc.get()
|
|
184
202
|
}
|
|
185
203
|
|
|
204
|
+
if (options.version) validate_version_array(options.version)
|
|
205
|
+
if (options.parents) validate_version_array(options.parents)
|
|
206
|
+
|
|
186
207
|
let resource = (typeof key == 'string') ? await get_resource(key) : key
|
|
187
208
|
|
|
188
209
|
if (!options.subscribe) {
|
|
@@ -249,12 +270,12 @@ braid_text.get = async (key, options) => {
|
|
|
249
270
|
} else doc = resource.doc
|
|
250
271
|
|
|
251
272
|
return {
|
|
252
|
-
version: doc.getRemoteVersion().map((x) =>
|
|
273
|
+
version: doc.getRemoteVersion().map((x) => x.join("-")),
|
|
253
274
|
body: doc.get()
|
|
254
275
|
}
|
|
255
276
|
} else {
|
|
256
277
|
if (options.merge_type != "dt") {
|
|
257
|
-
let version = resource.doc.getRemoteVersion().map((x) =>
|
|
278
|
+
let version = resource.doc.getRemoteVersion().map((x) => x.join("-"))
|
|
258
279
|
let x = { version }
|
|
259
280
|
|
|
260
281
|
if (!options.parents && !options.version) {
|
|
@@ -286,7 +307,7 @@ braid_text.get = async (key, options) => {
|
|
|
286
307
|
|
|
287
308
|
if (!options.parents && !options.version) {
|
|
288
309
|
options.subscribe({
|
|
289
|
-
version: [
|
|
310
|
+
version: [],
|
|
290
311
|
parents: [],
|
|
291
312
|
body: "",
|
|
292
313
|
})
|
|
@@ -330,7 +351,13 @@ braid_text.get = async (key, options) => {
|
|
|
330
351
|
}
|
|
331
352
|
|
|
332
353
|
braid_text.put = async (key, options) => {
|
|
333
|
-
let { version, patches, body } = options
|
|
354
|
+
let { version, patches, body, peer } = options
|
|
355
|
+
|
|
356
|
+
if (version) validate_version_array(version)
|
|
357
|
+
if (options.parents) validate_version_array(options.parents)
|
|
358
|
+
if (body != null && patches) throw new Error(`cannot have a body and patches`)
|
|
359
|
+
if (body != null && (typeof body !== 'string')) throw new Error(`body must be a string`)
|
|
360
|
+
if (patches) validate_patches(patches)
|
|
334
361
|
|
|
335
362
|
let resource = (typeof key == 'string') ? await get_resource(key) : key
|
|
336
363
|
|
|
@@ -346,21 +373,34 @@ braid_text.put = async (key, options) => {
|
|
|
346
373
|
patches = patches.map((p) => ({
|
|
347
374
|
...p,
|
|
348
375
|
range: p.range.match(/\d+/g).map((x) => parseInt(x)),
|
|
349
|
-
|
|
350
|
-
}))
|
|
376
|
+
content: [...p.content],
|
|
377
|
+
})).sort((a, b) => a.range[0] - b.range[0])
|
|
378
|
+
|
|
379
|
+
// validate patch positions
|
|
380
|
+
let max_pos = resource.doc.get().length
|
|
381
|
+
let must_be_at_least = 0
|
|
382
|
+
for (let p of patches) {
|
|
383
|
+
if (p.range[0] < must_be_at_least || p.range[0] > max_pos) throw new Error(`invalid patch range position: ${p.range[0]}`)
|
|
384
|
+
if (p.range[1] < p.range[0] || p.range[1] > max_pos) throw new Error(`invalid patch range position: ${p.range[1]}`)
|
|
385
|
+
must_be_at_least = p.range[1]
|
|
386
|
+
}
|
|
351
387
|
|
|
352
388
|
let change_count = patches.reduce((a, b) => a + b.content.length + (b.range[1] - b.range[0]), 0)
|
|
353
389
|
|
|
354
|
-
let og_v = version?.[0] || `${Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
|
|
390
|
+
let og_v = version?.[0] || `${(is_valid_actor(peer) && peer) || Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
|
|
355
391
|
|
|
356
392
|
// reduce the version sequence by the number of char-edits
|
|
357
393
|
let v = decode_version(og_v)
|
|
358
|
-
v = encode_version(v[0], v[1] + 1 - change_count)
|
|
359
394
|
|
|
360
|
-
|
|
395
|
+
// validate version: make sure we haven't seen it already
|
|
396
|
+
if (v[1] <= (resource.actor_seqs[v[0]] ?? -1)) throw new Error(`invalid version: already processed`)
|
|
397
|
+
resource.actor_seqs[v[0]] = v[1]
|
|
398
|
+
|
|
399
|
+
v = `${v[0]}-${v[1] + 1 - change_count}`
|
|
400
|
+
|
|
401
|
+
let parents = resource.doc.getRemoteVersion().map((x) => x.join("-"))
|
|
361
402
|
let og_parents = options.parents || parents
|
|
362
403
|
let ps = og_parents
|
|
363
|
-
if (!ps.length) ps = ["root"]
|
|
364
404
|
|
|
365
405
|
let v_before = resource.doc.getLocalVersion()
|
|
366
406
|
|
|
@@ -374,7 +414,7 @@ braid_text.put = async (key, options) => {
|
|
|
374
414
|
offset--
|
|
375
415
|
ps = [v]
|
|
376
416
|
v = decode_version(v)
|
|
377
|
-
v =
|
|
417
|
+
v = `${v[0]}-${v[1] + 1}`
|
|
378
418
|
}
|
|
379
419
|
// insert
|
|
380
420
|
for (let i = 0; i < p.content?.length ?? 0; i++) {
|
|
@@ -383,7 +423,7 @@ braid_text.put = async (key, options) => {
|
|
|
383
423
|
offset++
|
|
384
424
|
ps = [v]
|
|
385
425
|
v = decode_version(v)
|
|
386
|
-
v =
|
|
426
|
+
v = `${v[0]}-${v[1] + 1}`
|
|
387
427
|
}
|
|
388
428
|
}
|
|
389
429
|
|
|
@@ -401,17 +441,17 @@ braid_text.put = async (key, options) => {
|
|
|
401
441
|
patches = get_xf_patches(resource.doc, v_before)
|
|
402
442
|
console.log(JSON.stringify({ patches }))
|
|
403
443
|
|
|
404
|
-
let version = resource.doc.getRemoteVersion().map((x) =>
|
|
444
|
+
let version = resource.doc.getRemoteVersion().map((x) => x.join("-"))
|
|
405
445
|
|
|
406
446
|
for (let client of resource.simpleton_clients) {
|
|
407
|
-
if (client.peer ==
|
|
447
|
+
if (client.peer == peer) {
|
|
408
448
|
client.my_last_seen_version = [og_v]
|
|
409
449
|
}
|
|
410
450
|
|
|
411
451
|
function set_timeout(time_override) {
|
|
412
452
|
if (client.my_timeout) clearTimeout(client.my_timeout)
|
|
413
453
|
client.my_timeout = setTimeout(() => {
|
|
414
|
-
let version = resource.doc.getRemoteVersion().map((x) =>
|
|
454
|
+
let version = resource.doc.getRemoteVersion().map((x) => x.join("-"))
|
|
415
455
|
let x = { version }
|
|
416
456
|
x.parents = client.my_last_seen_version
|
|
417
457
|
|
|
@@ -428,7 +468,7 @@ braid_text.put = async (key, options) => {
|
|
|
428
468
|
}
|
|
429
469
|
|
|
430
470
|
if (client.my_timeout) {
|
|
431
|
-
if (client.peer ==
|
|
471
|
+
if (client.peer == peer) {
|
|
432
472
|
if (!v_eq(client.my_last_sent_version, og_parents)) {
|
|
433
473
|
// note: we don't add to client.my_unused_version_count,
|
|
434
474
|
// because we're already in a timeout;
|
|
@@ -444,7 +484,7 @@ braid_text.put = async (key, options) => {
|
|
|
444
484
|
}
|
|
445
485
|
|
|
446
486
|
let x = { version }
|
|
447
|
-
if (client.peer ==
|
|
487
|
+
if (client.peer == peer) {
|
|
448
488
|
if (!v_eq(client.my_last_sent_version, og_parents)) {
|
|
449
489
|
client.my_unused_version_count = (client.my_unused_version_count ?? 0) + 1
|
|
450
490
|
set_timeout()
|
|
@@ -491,7 +531,7 @@ braid_text.put = async (key, options) => {
|
|
|
491
531
|
patches: og_patches,
|
|
492
532
|
}
|
|
493
533
|
for (let client of resource.clients) {
|
|
494
|
-
if (client.peer !=
|
|
534
|
+
if (client.peer != peer) client.subscribe(x)
|
|
495
535
|
}
|
|
496
536
|
|
|
497
537
|
await resource.db_delta(resource.doc.getPatchSince(v_before))
|
|
@@ -499,13 +539,12 @@ braid_text.put = async (key, options) => {
|
|
|
499
539
|
|
|
500
540
|
braid_text.list = async () => {
|
|
501
541
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
return [...pages.keys()]
|
|
542
|
+
if (braid_text.db_folder) {
|
|
543
|
+
await db_folder_init()
|
|
544
|
+
var pages = new Set()
|
|
545
|
+
for (let x of await require('fs').promises.readdir(braid_text.db_folder)) pages.add(decode_filename(x.replace(/\.\w+$/, '')))
|
|
546
|
+
return [...pages.keys()]
|
|
547
|
+
} else return Object.keys(get_resource.cache)
|
|
509
548
|
} catch (e) { return [] }
|
|
510
549
|
}
|
|
511
550
|
|
|
@@ -530,6 +569,13 @@ async function get_resource(key) {
|
|
|
530
569
|
resource.doc = defrag_dt(resource.doc)
|
|
531
570
|
resource.need_defrag = false
|
|
532
571
|
|
|
572
|
+
resource.actor_seqs = {}
|
|
573
|
+
let max_version = resource.doc.getLocalVersion()[0] ?? -1
|
|
574
|
+
for (let i = 0; i <= max_version; i++) {
|
|
575
|
+
let v = resource.doc.localToRemoteVersion([i])[0]
|
|
576
|
+
resource.actor_seqs[v[0]] = Math.max(v[1], resource.actor_seqs[v[0]] ?? -1)
|
|
577
|
+
}
|
|
578
|
+
|
|
533
579
|
resource.delete_me = () => {
|
|
534
580
|
delete_me()
|
|
535
581
|
delete cache[key]
|
|
@@ -538,7 +584,54 @@ async function get_resource(key) {
|
|
|
538
584
|
return (cache[key] = resource)
|
|
539
585
|
}
|
|
540
586
|
|
|
587
|
+
async function db_folder_init() {
|
|
588
|
+
console.log('__!')
|
|
589
|
+
if (!db_folder_init.p) db_folder_init.p = new Promise(async done => {
|
|
590
|
+
await fs.promises.mkdir(braid_text.db_folder, { recursive: true });
|
|
591
|
+
|
|
592
|
+
// 0.0.13 -> 0.0.14
|
|
593
|
+
// look for files with key-encodings over max_encoded_key_size,
|
|
594
|
+
// and convert them using the new method
|
|
595
|
+
// for (let x of await fs.promises.readdir(braid_text.db_folder)) {
|
|
596
|
+
// let k = x.replace(/(_[0-9a-f]{64})?\.\w+$/, '')
|
|
597
|
+
// if (k.length > max_encoded_key_size) {
|
|
598
|
+
// k = decode_filename(k)
|
|
599
|
+
|
|
600
|
+
// await fs.promises.rename(`${braid_text.db_folder}/${x}`, `${braid_text.db_folder}/${encode_filename(k)}${x.match(/\.\w+$/)[0]}`)
|
|
601
|
+
// await fs.promises.writeFile(`${braid_text.db_folder}/${encode_filename(k)}.name`, k)
|
|
602
|
+
// }
|
|
603
|
+
// }
|
|
604
|
+
|
|
605
|
+
// 0.0.14 -> 0.0.15
|
|
606
|
+
// basically convert the 0.0.14 files back
|
|
607
|
+
let convert_us = {}
|
|
608
|
+
for (let x of await fs.promises.readdir(braid_text.db_folder)) {
|
|
609
|
+
if (x.endsWith('.name')) {
|
|
610
|
+
let encoded = convert_us[x.slice(0, -'.name'.length)] = encode_filename(await fs.promises.readFile(`${braid_text.db_folder}/${x}`, { encoding: 'utf8' }))
|
|
611
|
+
if (encoded.length > max_encoded_key_size) {
|
|
612
|
+
console.log(`trying to convert file to new format, but the key is too big: ${braid_text.db_folder}/${x}`)
|
|
613
|
+
process.exit()
|
|
614
|
+
}
|
|
615
|
+
console.log(`deleting: ${braid_text.db_folder}/${x}`)
|
|
616
|
+
await fs.promises.unlink(`${braid_text.db_folder}/${x}`)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (Object.keys(convert_us).length) {
|
|
620
|
+
for (let x of await fs.promises.readdir(braid_text.db_folder)) {
|
|
621
|
+
let [_, k, num] = x.match(/^(.*)\.(\d+)$/s)
|
|
622
|
+
if (!convert_us[k]) continue
|
|
623
|
+
console.log(`renaming: ${braid_text.db_folder}/${x} -> ${braid_text.db_folder}/${convert_us[k]}.${num}`)
|
|
624
|
+
if (convert_us[k]) await fs.promises.rename(`${braid_text.db_folder}/${x}`, `${braid_text.db_folder}/${convert_us[k]}.${num}`)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
done()
|
|
629
|
+
})
|
|
630
|
+
await db_folder_init.p
|
|
631
|
+
}
|
|
632
|
+
|
|
541
633
|
async function get_files_for_key(key) {
|
|
634
|
+
await db_folder_init()
|
|
542
635
|
try {
|
|
543
636
|
let re = new RegExp("^" + encode_filename(key).replace(/[^a-zA-Z0-9]/g, "\\$&") + "\\.\\w+$")
|
|
544
637
|
return (await fs.promises.readdir(braid_text.db_folder))
|
|
@@ -548,31 +641,14 @@ async function get_files_for_key(key) {
|
|
|
548
641
|
}
|
|
549
642
|
|
|
550
643
|
async function file_sync(key, process_delta, get_init) {
|
|
644
|
+
let encoded = encode_filename(key)
|
|
645
|
+
|
|
646
|
+
if (encoded.length > max_encoded_key_size) throw new Error(`invalid key: too long (max ${max_encoded_key_size})`)
|
|
647
|
+
|
|
551
648
|
let currentNumber = 0
|
|
552
649
|
let currentSize = 0
|
|
553
650
|
let threshold = 0
|
|
554
651
|
|
|
555
|
-
// Ensure the existence of db_folder
|
|
556
|
-
if (!file_sync.init_p) file_sync.init_p = new Promise(async done => {
|
|
557
|
-
await fs.promises.mkdir(braid_text.db_folder, { recursive: true });
|
|
558
|
-
|
|
559
|
-
// 0.0.13 -> 0.0.14
|
|
560
|
-
// look for files with key-encodings over max_encoded_key_size,
|
|
561
|
-
// and convert them using the new method
|
|
562
|
-
for (let x of await fs.promises.readdir(braid_text.db_folder)) {
|
|
563
|
-
let k = x.replace(/(_[0-9a-f]{64})?\.\w+$/, '')
|
|
564
|
-
if (k.length > max_encoded_key_size) {
|
|
565
|
-
k = decode_filename(k)
|
|
566
|
-
|
|
567
|
-
await fs.promises.rename(`${braid_text.db_folder}/${x}`, `${braid_text.db_folder}/${encode_filename(k)}${x.match(/\.\w+$/)[0]}`)
|
|
568
|
-
await fs.promises.writeFile(`${braid_text.db_folder}/${encode_filename(k)}.name`, k)
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
done()
|
|
573
|
-
})
|
|
574
|
-
await file_sync.init_p
|
|
575
|
-
|
|
576
652
|
// Read existing files and sort by numbers.
|
|
577
653
|
const files = (await get_files_for_key(key))
|
|
578
654
|
.filter(x => x.match(/\.\d+$/))
|
|
@@ -617,7 +693,7 @@ async function file_sync(key, process_delta, get_init) {
|
|
|
617
693
|
return {
|
|
618
694
|
change: async (bytes) => {
|
|
619
695
|
currentSize += bytes.length + 4 // we account for the extra 4 bytes for uint32
|
|
620
|
-
const filename = `${braid_text.db_folder}/${
|
|
696
|
+
const filename = `${braid_text.db_folder}/${encoded}.${currentNumber}`
|
|
621
697
|
if (currentSize < threshold) {
|
|
622
698
|
console.log(`appending to db..`)
|
|
623
699
|
|
|
@@ -631,9 +707,6 @@ async function file_sync(key, process_delta, get_init) {
|
|
|
631
707
|
try {
|
|
632
708
|
console.log(`starting new db..`)
|
|
633
709
|
|
|
634
|
-
let encoded = encode_filename(key)
|
|
635
|
-
if (encoded.length > max_encoded_key_size) await fs.promises.writeFile(`${braid_text.db_folder}/${encoded}.name`, key)
|
|
636
|
-
|
|
637
710
|
currentNumber++
|
|
638
711
|
const init = get_init()
|
|
639
712
|
const buffer = Buffer.allocUnsafe(4)
|
|
@@ -792,7 +865,7 @@ function parseDT(byte_array) {
|
|
|
792
865
|
let num = x >> 2
|
|
793
866
|
|
|
794
867
|
if (x == 1) {
|
|
795
|
-
parents.
|
|
868
|
+
// no parents (e.g. parent is "root")
|
|
796
869
|
} else if (!is_foreign) {
|
|
797
870
|
parents.push(versions[count - num])
|
|
798
871
|
} else {
|
|
@@ -861,7 +934,7 @@ function OpLog_create_bytes(version, parents, pos, ins) {
|
|
|
861
934
|
|
|
862
935
|
let agents = new Set()
|
|
863
936
|
agents.add(version[0])
|
|
864
|
-
for (let p of parents)
|
|
937
|
+
for (let p of parents) agents.add(p[0])
|
|
865
938
|
agents = [...agents]
|
|
866
939
|
|
|
867
940
|
// console.log(JSON.stringify({ agents, parents }, null, 4));
|
|
@@ -882,7 +955,7 @@ function OpLog_create_bytes(version, parents, pos, ins) {
|
|
|
882
955
|
|
|
883
956
|
let branch = []
|
|
884
957
|
|
|
885
|
-
if (parents
|
|
958
|
+
if (parents.length) {
|
|
886
959
|
let frontier = []
|
|
887
960
|
|
|
888
961
|
for (let [i, [agent, seq]] of parents.entries()) {
|
|
@@ -957,7 +1030,7 @@ function OpLog_create_bytes(version, parents, pos, ins) {
|
|
|
957
1030
|
|
|
958
1031
|
write_varint(parents_bytes, 1)
|
|
959
1032
|
|
|
960
|
-
if (parents
|
|
1033
|
+
if (parents.length) {
|
|
961
1034
|
for (let [i, [agent, seq]] of parents.entries()) {
|
|
962
1035
|
let has_more = i < parents.length - 1
|
|
963
1036
|
let agent_i = agent_to_i[agent]
|
|
@@ -983,8 +1056,9 @@ function OpLog_remote_to_local(doc, frontier) {
|
|
|
983
1056
|
let map = Object.fromEntries(frontier.map((x) => [x, true]))
|
|
984
1057
|
|
|
985
1058
|
let local_version = []
|
|
986
|
-
|
|
987
|
-
|
|
1059
|
+
|
|
1060
|
+
let max_version = doc.getLocalVersion()[0] ?? -1
|
|
1061
|
+
for (let i = 0; i <= max_version; i++) {
|
|
988
1062
|
if (map[doc.localToRemoteVersion([i])[0].join("-")]) {
|
|
989
1063
|
local_version.push(i)
|
|
990
1064
|
}
|
|
@@ -993,16 +1067,6 @@ function OpLog_remote_to_local(doc, frontier) {
|
|
|
993
1067
|
return frontier.length == local_version.length && new Uint32Array(local_version)
|
|
994
1068
|
}
|
|
995
1069
|
|
|
996
|
-
function encode_version(agent, seq) {
|
|
997
|
-
return agent + "-" + seq
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
function decode_version(v) {
|
|
1001
|
-
let a = v.split("-")
|
|
1002
|
-
if (a.length > 1) a[1] = parseInt(a[1])
|
|
1003
|
-
return a
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
1070
|
function v_eq(v1, v2) {
|
|
1007
1071
|
return v1.length == v2.length && v1.every((x, i) => x == v2[i])
|
|
1008
1072
|
}
|
|
@@ -1306,11 +1370,6 @@ function encode_filename(filename) {
|
|
|
1306
1370
|
// Encode the filename using encodeURIComponent()
|
|
1307
1371
|
let encoded = encodeURIComponent(swapped)
|
|
1308
1372
|
|
|
1309
|
-
// Do something special with a hash if the encoding is too long
|
|
1310
|
-
if (encoded.length > max_encoded_key_size) {
|
|
1311
|
-
encoded = `${swapped.slice(0, max_encoded_key_size)}_${require('crypto').createHash('sha256').update(filename).digest('hex')}`
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
1373
|
return encoded
|
|
1315
1374
|
}
|
|
1316
1375
|
|
|
@@ -1324,4 +1383,46 @@ function decode_filename(encodedFilename) {
|
|
|
1324
1383
|
return decoded
|
|
1325
1384
|
}
|
|
1326
1385
|
|
|
1386
|
+
function validate_version_array(x) {
|
|
1387
|
+
if (!Array.isArray(x)) throw new Error(`invalid version array: not an array`)
|
|
1388
|
+
for (xx of x) validate_actor_seq(xx)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function validate_actor_seq(x) {
|
|
1392
|
+
if (typeof x !== 'string') throw new Error(`invalid actor-seq: not a string`)
|
|
1393
|
+
let [actor, seq] = decode_version(x)
|
|
1394
|
+
validate_actor(actor)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function validate_actor(x) {
|
|
1398
|
+
if (typeof x !== 'string') throw new Error(`invalid actor: not a string`)
|
|
1399
|
+
if (Buffer.byteLength(x, 'utf8') >= 50) throw new Error(`actor value too long (max 49): ${x}`) // restriction coming from dt
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function is_valid_actor(x) {
|
|
1403
|
+
try {
|
|
1404
|
+
validate_actor(x)
|
|
1405
|
+
return true
|
|
1406
|
+
} catch (e) {}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function decode_version(v) {
|
|
1410
|
+
let m = v.match(/^(.*)-(\d+)$/s)
|
|
1411
|
+
if (!m) throw new Error(`invalid actor-seq version: ${v}`)
|
|
1412
|
+
return [m[1], parseInt(m[2])]
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function validate_patches(patches) {
|
|
1416
|
+
if (!Array.isArray(patches)) throw new Error(`invalid patches: not an array`)
|
|
1417
|
+
for (let p of patches) validate_patch(p)
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function validate_patch(x) {
|
|
1421
|
+
if (typeof x != 'object') throw new Error(`invalid patch: not an object`)
|
|
1422
|
+
if (x.unit && x.unit !== 'text') throw new Error(`invalid patch unit '${x.unit}': only 'text' supported`)
|
|
1423
|
+
if (typeof x.range !== 'string') throw new Error(`invalid patch range: must be a string`)
|
|
1424
|
+
if (!x.range.match(/^\s*\[\s*\d+\s*:\s*\d+\s*\]\s*$/)) throw new Error(`invalid patch range: ${x.range}`)
|
|
1425
|
+
if (typeof x.content !== 'string') throw new Error(`invalid patch content: must be a string`)
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1327
1428
|
module.exports = braid_text
|