braid-text 0.0.1

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 ADDED
@@ -0,0 +1,1263 @@
1
+
2
+ let { Doc } = require("diamond-types-node")
3
+ let braidify = require("braid-http").http_server
4
+ let fs = require("fs")
5
+
6
+ let braid_text = {
7
+ db_folder: './braid-text-db'
8
+ }
9
+
10
+ let waiting_puts = 0
11
+ let prev_put_p = null
12
+
13
+ braid_text.serve = async (req, res, options = {}) => {
14
+ options = {
15
+ key: req.url.split('?')[0], // Default key
16
+ put_cb: (key, val) => { }, // Default callback when a PUT changes a key
17
+ ...options // Override with all options passed in
18
+ }
19
+
20
+ let resource = await get_resource(options.key)
21
+
22
+ braidify(req, res)
23
+
24
+ let peer = req.headers["peer"]
25
+
26
+ // set default content type of text/plain
27
+ if (!res.getHeader('content-type')) res.setHeader('Content-Type', 'text/plain')
28
+
29
+ // no matter what the content type is,
30
+ // we want to set the charset to utf-8
31
+ const contentType = res.getHeader('Content-Type')
32
+ const parsedContentType = contentType.split(';').map(part => part.trim())
33
+ const charsetParam = parsedContentType.find(part => part.toLowerCase().startsWith('charset='))
34
+ if (!charsetParam)
35
+ res.setHeader('Content-Type', `${contentType}; charset=utf-8`)
36
+ else if (charsetParam.toLowerCase() !== 'charset=utf-8') {
37
+ // Replace the existing charset with utf-8
38
+ const updatedContentType = parsedContentType
39
+ .map(part => (part.toLowerCase().startsWith('charset=') ? 'charset=utf-8' : part))
40
+ .join('; ');
41
+ res.setHeader('Content-Type', updatedContentType);
42
+ }
43
+
44
+ // free CORS
45
+ res.setHeader("Access-Control-Allow-Origin", "*")
46
+ res.setHeader("Access-Control-Allow-Methods", "*")
47
+ res.setHeader("Access-Control-Allow-Headers", "*")
48
+ res.setHeader("Access-Control-Expose-Headers", "*")
49
+
50
+ function my_end(statusCode, x) {
51
+ res.statusCode = statusCode
52
+ res.end(x ?? '')
53
+ }
54
+
55
+ if (req.method == "OPTIONS") return my_end(200)
56
+
57
+ if (req.method == "DELETE") {
58
+ await resource.delete_me()
59
+ return my_end(200)
60
+ }
61
+
62
+ if (req.method == "GET" || req.method == "HEAD") {
63
+ if (!req.subscribe) {
64
+ res.setHeader("Accept-Subscribe", "true")
65
+
66
+ let x = await braid_text.get(resource, { version: req.version, parents: req.parents })
67
+
68
+ res.setHeader("Version", x.version.map((x) => JSON.stringify(x)).join(", "))
69
+
70
+ const buffer = Buffer.from(x.body, "utf8")
71
+ res.setHeader("Content-Length", buffer.length)
72
+
73
+ if (req.method === "HEAD") return my_end(200)
74
+
75
+ return my_end(200, buffer)
76
+ } else {
77
+ res.setHeader("Editable", "true")
78
+ res.setHeader("Merge-Type", req.headers["merge-type"] === "dt" ? "dt" : "simpleton")
79
+ if (req.method == "HEAD") return my_end(200)
80
+
81
+ let options = {
82
+ peer,
83
+ version: req.version,
84
+ parents: req.parents,
85
+ merge_type: req.headers["merge-type"],
86
+ subscribe: x => res.sendVersion(x),
87
+ write: (x) => res.write(x)
88
+ }
89
+
90
+ res.startSubscription({
91
+ onClose: () => {
92
+ if (req.headers["merge-type"] === "dt") resource.clients.delete(options)
93
+ else resource.simpleton_clients.delete(options)
94
+ }
95
+ })
96
+
97
+ return braid_text.get(resource, options)
98
+ }
99
+ }
100
+
101
+ if (req.method == "PUT" || req.method == "POST" || req.method == "PATCH") {
102
+ if (waiting_puts >= 100) {
103
+ console.log(`The server is busy.`)
104
+ return my_end(503, "The server is busy.")
105
+ }
106
+
107
+ waiting_puts++
108
+ console.log(`waiting_puts(after++) = ${waiting_puts}`)
109
+
110
+ let my_prev_put_p = prev_put_p
111
+ let done_my_turn = null
112
+ prev_put_p = new Promise(
113
+ (done) =>
114
+ (done_my_turn = (statusCode, x) => {
115
+ waiting_puts--
116
+ console.log(`waiting_puts(after--) = ${waiting_puts}`)
117
+ my_end(statusCode, x)
118
+ done()
119
+ })
120
+ )
121
+ let patches = await req.patches()
122
+ await my_prev_put_p
123
+
124
+ let body = null
125
+ if (patches[0]?.unit === 'everything') {
126
+ body = patches[0].content
127
+ patches = null
128
+ }
129
+
130
+ try {
131
+ await braid_text.put(resource, { peer, version: req.version, parents: req.parents, patches, body, merge_type: req.headers["merge-type"] })
132
+ } catch (e) {
133
+ console.log(`EEE= ${e}:${e.stack}`)
134
+ // we couldn't apply the version, presumably because we're missing its parents.
135
+ // we want to send a 4XX error, so the client will resend this request later,
136
+ // hopefully after we've received the necessary parents.
137
+
138
+ // here are some 4XX error code options..
139
+ //
140
+ // - 425 Too Early
141
+ // - pros: our message is too early
142
+ // - cons: associated with some "Early-Data" http thing, which we're not using
143
+ // - 400 Bad Request
144
+ // - pros: pretty generic
145
+ // - cons: implies client shouldn't resend as-is
146
+ // - 409 Conflict
147
+ // - pros: doesn't imply modifications needed
148
+ // - cons: the message is not conflicting with anything
149
+ // - 412 Precondition Failed
150
+ // - pros: kindof true.. the precondition of having another version has failed..
151
+ // - cons: not strictly true, as this code is associated with http's If-Unmodified-Since stuff
152
+ // - 422 Unprocessable Content
153
+ // - pros: it's true
154
+ // - cons: implies client shouldn't resend as-is (at least, it says that here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422)
155
+ // - 428 Precondition Required
156
+ // - pros: the name sounds right
157
+ // - 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
158
+ return done_my_turn(425, "The server is missing the parents of this version.")
159
+ }
160
+
161
+ options.put_cb(options.key, resource.doc.get())
162
+
163
+ return done_my_turn(200)
164
+ }
165
+
166
+ throw new Error("unknown")
167
+ }
168
+
169
+ braid_text.get = async (key, options) => {
170
+ if (!options) return get_resource.cache?.[key]?.doc.get()
171
+
172
+ let resource = (typeof key == 'string') ? await get_resource(key) : key
173
+
174
+ if (!options.subscribe) {
175
+ let doc = null
176
+ if (options.version || options.parents) {
177
+ let frontier = {}
178
+ options.version?.forEach((x) => (frontier[x] = true))
179
+ options.parents?.forEach((x) => (frontier[x] = true))
180
+
181
+ let local_version = []
182
+ let [agents, versions, parentss] = parseDT([...resource.doc.toBytes()])
183
+ for (let i = 0; i < versions.length; i++) {
184
+ if (frontier[versions[i].join("-")]) {
185
+ local_version.push(i)
186
+ }
187
+ }
188
+ local_version = new Uint32Array(local_version)
189
+
190
+ let after_versions = {}
191
+ let [_, after_versions_array, __] = parseDT([...resource.doc.getPatchSince(local_version)])
192
+ for (let v of after_versions_array) after_versions[v.join("-")] = true
193
+
194
+ let new_doc = new Doc()
195
+ let op_runs = resource.doc.getOpsSince([])
196
+ let i = 0
197
+ op_runs.forEach((op_run) => {
198
+ let parents = parentss[i].map((x) => x.join("-"))
199
+ let start = op_run.start
200
+ let end = start + 1
201
+ let content = op_run.content?.[0]
202
+
203
+ let len = op_run.end - op_run.start
204
+ let base_i = i
205
+ for (let j = 1; j <= len; j++) {
206
+ let I = base_i + j
207
+ if (
208
+ j == len ||
209
+ parentss[I].length != 1 ||
210
+ parentss[I][0][0] != versions[I - 1][0] ||
211
+ parentss[I][0][1] != versions[I - 1][1] ||
212
+ versions[I][0] != versions[I - 1][0] ||
213
+ versions[I][1] != versions[I - 1][1] + 1
214
+ ) {
215
+ for (; i < I; i++) {
216
+ let version = versions[i].join("-")
217
+ if (!after_versions[version]) {
218
+ new_doc.mergeBytes(
219
+ OpLog_create_bytes(
220
+ version,
221
+ parentss[i].map((x) => x.join("-")),
222
+ content ? start + (i - base_i) : start,
223
+ content?.[0]
224
+ )
225
+ )
226
+ }
227
+ if (op_run.content) content = content.slice(1)
228
+ }
229
+ content = ""
230
+ }
231
+ if (op_run.content) content += op_run.content[j]
232
+ }
233
+ })
234
+ doc = new_doc
235
+ } else doc = resource.doc
236
+
237
+ return {
238
+ version: doc.getRemoteVersion().map((x) => encode_version(...x)),
239
+ body: doc.get()
240
+ }
241
+ } else {
242
+ if (options.merge_type != "dt") {
243
+ let version = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
244
+ let x = { version }
245
+
246
+ if (!options.parents && !options.version) {
247
+ x.parents = []
248
+ x.body = resource.doc.get()
249
+ options.subscribe(x)
250
+ } else {
251
+ x.parents = options.version ? options.version : options.parents
252
+ options.my_last_seen_version = x.parents
253
+
254
+ // only send them a version from these parents if we have these parents (otherwise we'll assume these parents are more recent, probably versions they created but haven't sent us yet, and we'll send them appropriate rebased updates when they send us these versions)
255
+ let local_version = OpLog_remote_to_local(resource.doc, x.parents)
256
+ if (local_version) {
257
+ x.patches = get_xf_patches(resource.doc, local_version)
258
+ options.subscribe(x)
259
+ }
260
+ }
261
+
262
+ options.my_last_sent_version = x.version
263
+ resource.simpleton_clients.add(options)
264
+ } else {
265
+ let updates = null
266
+
267
+ if (resource.need_defrag) {
268
+ console.log(`doing defrag..`)
269
+ resource.need_defrag = false
270
+ resource.doc = defrag_dt(resource.doc)
271
+ }
272
+
273
+ if (!options.parents && !options.version) {
274
+ options.subscribe({
275
+ version: ["root"],
276
+ parents: [],
277
+ body: "",
278
+ })
279
+
280
+ updates = OpLog_get_patches(resource.doc.toBytes(), resource.doc.getOpsSince([]))
281
+ } else {
282
+ // Then start the subscription from the parents in options
283
+ let parents = Object.fromEntries((options.parents ? options.parents : options.version).map((x) => [x, true]))
284
+
285
+ let local_version = []
286
+ let [agents, versions, parentss] = parseDT([...resource.doc.toBytes()])
287
+ for (let i = 0; i < versions.length; i++) {
288
+ if (parents[versions[i].join("-")]) local_version.push(i)
289
+ }
290
+ local_version = new Uint32Array(local_version)
291
+
292
+ updates = OpLog_get_patches(resource.doc.getPatchSince(local_version), resource.doc.getOpsSince(local_version))
293
+ }
294
+
295
+ for (let u of updates) {
296
+ u.version = decode_version(u.version)
297
+ u.version[1] += u.end - u.start - 1
298
+ u.version = u.version.join("-")
299
+
300
+ options.subscribe({
301
+ version: [u.version],
302
+ parents: u.parents,
303
+ patches: [{ unit: u.unit, range: u.range, content: u.content }],
304
+ })
305
+ }
306
+
307
+ // Output at least *some* data, or else chrome gets confused and
308
+ // thinks the connection failed. This isn't strictly necessary,
309
+ // but it makes fewer scary errors get printed out in the JS
310
+ // console.
311
+ if (updates.length === 0) options.write?.("\r\n")
312
+
313
+ resource.clients.add(options)
314
+ }
315
+ }
316
+ }
317
+
318
+ braid_text.put = async (key, options) => {
319
+ let { version, patches, body } = options
320
+
321
+ let resource = (typeof key == 'string') ? await get_resource(key) : key
322
+
323
+ if (body != null) {
324
+ patches = [{
325
+ unit: 'text',
326
+ range: `[0:${count_code_points(resource.doc.get())}]`,
327
+ content: body
328
+ }]
329
+ }
330
+
331
+ let og_patches = patches
332
+ patches = patches.map((p) => ({
333
+ ...p,
334
+ range: p.range.match(/\d+/g).map((x) => parseInt(x)),
335
+ ...(p.content ? { content: [...p.content] } : {}),
336
+ }))
337
+
338
+ let change_count = patches.reduce((a, b) => a + b.content.length + (b.range[1] - b.range[0]), 0)
339
+
340
+ let og_v = version?.[0] || `${Math.random().toString(36).slice(2, 7)}-${change_count - 1}`
341
+
342
+ // reduce the version sequence by the number of char-edits
343
+ let v = decode_version(og_v)
344
+ v = encode_version(v[0], v[1] + 1 - change_count)
345
+
346
+ let parents = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
347
+ let og_parents = options.parents || parents
348
+ let ps = og_parents
349
+ if (!ps.length) ps = ["root"]
350
+
351
+ let v_before = resource.doc.getLocalVersion()
352
+
353
+ let bytes = []
354
+
355
+ let offset = 0
356
+ for (let p of patches) {
357
+ // delete
358
+ for (let i = p.range[0]; i < p.range[1]; i++) {
359
+ bytes.push(OpLog_create_bytes(v, ps, p.range[1] - 1 + offset, null))
360
+ offset--
361
+ ps = [v]
362
+ v = decode_version(v)
363
+ v = encode_version(v[0], v[1] + 1)
364
+ }
365
+ // insert
366
+ for (let i = 0; i < p.content?.length ?? 0; i++) {
367
+ let c = p.content[i]
368
+ bytes.push(OpLog_create_bytes(v, ps, p.range[1] + offset, c))
369
+ offset++
370
+ ps = [v]
371
+ v = decode_version(v)
372
+ v = encode_version(v[0], v[1] + 1)
373
+ }
374
+ }
375
+
376
+ for (let b of bytes) resource.doc.mergeBytes(b)
377
+
378
+ resource.need_defrag = true
379
+
380
+ let v_after = resource.doc.getLocalVersion()
381
+ if (JSON.stringify(v_before) === JSON.stringify(v_after)) {
382
+ console.log(`we got a version we already had: ${v_before}`)
383
+ return
384
+ }
385
+
386
+ if (options.merge_type != "dt") {
387
+ patches = get_xf_patches(resource.doc, v_before)
388
+ console.log(JSON.stringify({ patches }))
389
+
390
+ let version = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
391
+
392
+ for (let client of resource.simpleton_clients) {
393
+ if (client.peer == options.peer) {
394
+ client.my_last_seen_version = [og_v]
395
+ }
396
+
397
+ function set_timeout(time_override) {
398
+ if (client.my_timeout) clearTimeout(client.my_timeout)
399
+ client.my_timeout = setTimeout(() => {
400
+ let version = resource.doc.getRemoteVersion().map((x) => encode_version(...x))
401
+ let x = { version }
402
+ x.parents = client.my_last_seen_version
403
+
404
+ console.log("rebasing after timeout.. ")
405
+ console.log(" client.my_unused_version_count = " + client.my_unused_version_count)
406
+ x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, client.my_last_seen_version))
407
+
408
+ console.log(`sending from rebase: ${JSON.stringify(x)}`)
409
+ client.subscribe(x)
410
+ client.my_last_sent_version = x.version
411
+
412
+ delete client.my_timeout
413
+ }, time_override ?? Math.min(3000, 23 * Math.pow(1.5, client.my_unused_version_count - 1)))
414
+ }
415
+
416
+ if (client.my_timeout) {
417
+ if (client.peer == options.peer) {
418
+ if (!v_eq(client.my_last_sent_version, og_parents)) {
419
+ // note: we don't add to client.my_unused_version_count,
420
+ // because we're already in a timeout;
421
+ // we'll just extend it here..
422
+ set_timeout()
423
+ } else {
424
+ // hm.. it appears we got a correctly parented version,
425
+ // which suggests that maybe we can stop the timeout early
426
+ set_timeout(0)
427
+ }
428
+ }
429
+ continue
430
+ }
431
+
432
+ let x = { version }
433
+ if (client.peer == options.peer) {
434
+ if (!v_eq(client.my_last_sent_version, og_parents)) {
435
+ client.my_unused_version_count = (client.my_unused_version_count ?? 0) + 1
436
+ set_timeout()
437
+ continue
438
+ } else {
439
+ delete client.my_unused_version_count
440
+ }
441
+
442
+ x.parents = options.version
443
+ if (!v_eq(version, options.version)) {
444
+ console.log("rebasing..")
445
+ x.patches = get_xf_patches(resource.doc, OpLog_remote_to_local(resource.doc, [og_v]))
446
+ } else {
447
+ // this client already has this version,
448
+ // so let's pretend to send it back, but not
449
+ console.log(`not reflecting back to simpleton`)
450
+ client.my_last_sent_version = x.version
451
+ continue
452
+ }
453
+ } else {
454
+ x.parents = parents
455
+ x.patches = patches
456
+ }
457
+ console.log(`sending: ${JSON.stringify(x)}`)
458
+ client.subscribe(x)
459
+ client.my_last_sent_version = x.version
460
+ }
461
+ } else {
462
+ if (resource.simpleton_clients.size) {
463
+ patches = get_xf_patches(resource.doc, v_before)
464
+ let x = { version: [og_v], parents, patches }
465
+ console.log(`sending: ${JSON.stringify(x)}`)
466
+ for (let client of resource.simpleton_clients) {
467
+ if (client.my_timeout) continue
468
+ client.subscribe(x)
469
+ client.my_last_sent_version = x.version
470
+ }
471
+ }
472
+ }
473
+
474
+ let x = {
475
+ version: [og_v],
476
+ parents: og_parents,
477
+ patches: og_patches,
478
+ }
479
+ for (let client of resource.clients) {
480
+ if (client.peer != options.peer) client.subscribe(x)
481
+ }
482
+
483
+ await resource.db_delta(resource.doc.getPatchSince(v_before))
484
+ }
485
+
486
+ async function get_resource(key) {
487
+ let cache = get_resource.cache || (get_resource.cache = {})
488
+ if (cache[key]) return cache[key]
489
+
490
+ let resource = {}
491
+ resource.clients = new Set()
492
+ resource.simpleton_clients = new Set()
493
+
494
+ resource.doc = new Doc("server")
495
+
496
+ let { change, delete_me } = braid_text.db_folder
497
+ ? await file_sync(
498
+ braid_text.db_folder,
499
+ encodeURIComponent(key),
500
+ (bytes) => resource.doc.mergeBytes(bytes),
501
+ () => resource.doc.toBytes()
502
+ )
503
+ : { change: () => { }, delete_me: () => { } }
504
+
505
+ resource.db_delta = change
506
+
507
+ resource.doc = defrag_dt(resource.doc)
508
+ resource.need_defrag = false
509
+
510
+ resource.delete_me = () => {
511
+ delete_me()
512
+ delete cache[key]
513
+ }
514
+
515
+ return (cache[key] = resource)
516
+ }
517
+
518
+ async function file_sync(db_folder, filename_base, process_delta, get_init) {
519
+ let currentNumber = 0
520
+ let currentSize = 0
521
+ let threshold = 0
522
+
523
+ // Ensure the existence of db_folder
524
+ try {
525
+ await fs.promises.access(db_folder);
526
+ } catch (err) {
527
+ if (err.code === 'ENOENT') {
528
+ await fs.promises.mkdir(db_folder, { recursive: true });
529
+ } else {
530
+ throw err;
531
+ }
532
+ }
533
+
534
+ // Read existing files and sort by numbers.
535
+ async function get_sorted_files() {
536
+ let re = new RegExp("^" + filename_base.replace(/[^a-zA-Z0-9]/g, "\\$&") + "\\.\\d+$")
537
+ return (await fs.promises.readdir(db_folder))
538
+ .filter((a) => re.test(a))
539
+ .sort((a, b) => parseInt(a.match(/\d+$/)[0]) - parseInt(b.match(/\d+$/)[0]))
540
+ .map((a) => `${db_folder}/${a}`)
541
+ }
542
+
543
+ const files = await get_sorted_files()
544
+
545
+ // Try to process files starting from the highest number.
546
+ let done = false
547
+ for (let i = files.length - 1; i >= 0; i--) {
548
+ if (done) {
549
+ await fs.promises.unlink(files[i])
550
+ continue
551
+ }
552
+ try {
553
+ const filename = files[i]
554
+ console.log(`trying to process file: ${filename}`)
555
+ const data = await fs.promises.readFile(filename)
556
+
557
+ let cursor = 0
558
+ let isFirstChunk = true
559
+ while (cursor < data.length) {
560
+ const chunkSize = data.readUInt32LE(cursor)
561
+ cursor += 4
562
+ const chunk = data.slice(cursor, cursor + chunkSize)
563
+ cursor += chunkSize
564
+
565
+ if (isFirstChunk) {
566
+ isFirstChunk = false
567
+ threshold = chunkSize * 10
568
+ }
569
+ process_delta(chunk)
570
+ }
571
+
572
+ currentSize = data.length
573
+ currentNumber = parseInt(filename.match(/\d+$/)[0])
574
+ done = true
575
+ } catch (error) {
576
+ console.error(`Error processing file: ${files[i]}`)
577
+ await fs.promises.unlink(files[i])
578
+ }
579
+ }
580
+
581
+ return {
582
+ change: async (bytes) => {
583
+ currentSize += bytes.length + 4 // we account for the extra 4 bytes for uint32
584
+ const filename = `${db_folder}/${filename_base}.${currentNumber}`
585
+ if (currentSize < threshold) {
586
+ console.log(`appending to db..`)
587
+
588
+ let buffer = Buffer.allocUnsafe(4)
589
+ buffer.writeUInt32LE(bytes.length, 0)
590
+ await fs.promises.appendFile(filename, buffer)
591
+ await fs.promises.appendFile(filename, bytes)
592
+
593
+ console.log("wrote to : " + filename)
594
+ } else {
595
+ try {
596
+ console.log(`starting new db..`)
597
+
598
+ currentNumber++
599
+ const init = get_init()
600
+ const buffer = Buffer.allocUnsafe(4)
601
+ buffer.writeUInt32LE(init.length, 0)
602
+
603
+ const newFilename = `${db_folder}/${filename_base}.${currentNumber}`
604
+ await fs.promises.writeFile(newFilename, buffer)
605
+ await fs.promises.appendFile(newFilename, init)
606
+
607
+ console.log("wrote to : " + newFilename)
608
+
609
+ currentSize = 4 + init.length
610
+ threshold = currentSize * 10
611
+ try {
612
+ await fs.promises.unlink(filename)
613
+ } catch (e) { }
614
+ } catch (e) {
615
+ console.log(`e = ${e.stack}`)
616
+ }
617
+ }
618
+ },
619
+ delete_me: async () => {
620
+ await Promise.all(
621
+ (
622
+ await get_sorted_files()
623
+ ).map((file) => {
624
+ return new Promise((resolve, reject) => {
625
+ fs.unlink(file, (err) => {
626
+ if (err) {
627
+ console.error(`Error deleting file: ${file}`)
628
+ reject(err)
629
+ } else {
630
+ console.log(`Deleted file: ${file}`)
631
+ resolve()
632
+ }
633
+ })
634
+ })
635
+ })
636
+ )
637
+ },
638
+ }
639
+ }
640
+
641
+ //////////////////////////////////////////////////////////////////
642
+ //////////////////////////////////////////////////////////////////
643
+ //////////////////////////////////////////////////////////////////
644
+
645
+ function defrag_dt(doc) {
646
+ let fresh_doc = new Doc("server")
647
+ fresh_doc.mergeBytes(doc.toBytes())
648
+ return fresh_doc
649
+ }
650
+
651
+ function OpLog_get_patches(bytes, op_runs) {
652
+ // console.log(`op_runs = `, op_runs);
653
+
654
+ let [agents, versions, parentss] = parseDT([...bytes])
655
+
656
+ // console.log(JSON.stringify({agents, versions, parentss}, null, 4))
657
+
658
+ let i = 0
659
+ let patches = []
660
+ op_runs.forEach((op_run) => {
661
+ let version = versions[i].join("-")
662
+ let parents = parentss[i].map((x) => x.join("-"))
663
+ let start = op_run.start
664
+ let end = start + 1
665
+ if (op_run.content) op_run.content = [...op_run.content]
666
+ let content = op_run.content?.[0]
667
+ let len = op_run.end - op_run.start
668
+ for (let j = 1; j <= len; j++) {
669
+ let I = i + j
670
+ if (
671
+ j == len ||
672
+ parentss[I].length != 1 ||
673
+ parentss[I][0][0] != versions[I - 1][0] ||
674
+ parentss[I][0][1] != versions[I - 1][1] ||
675
+ versions[I][0] != versions[I - 1][0] ||
676
+ versions[I][1] != versions[I - 1][1] + 1
677
+ ) {
678
+ patches.push({
679
+ version,
680
+ parents,
681
+ unit: "text",
682
+ range: content ? `[${start}:${start}]` : `[${start}:${end}]`,
683
+ content: content ?? "",
684
+ start,
685
+ end,
686
+ })
687
+ if (j == len) break
688
+ version = versions[I].join("-")
689
+ parents = parentss[I].map((x) => x.join("-"))
690
+ start = op_run.start + j
691
+ content = ""
692
+ }
693
+ end++
694
+ if (op_run.content) content += op_run.content[j]
695
+ }
696
+ i += len
697
+ })
698
+ return patches
699
+ }
700
+
701
+ function parseDT(byte_array) {
702
+ if (new TextDecoder().decode(new Uint8Array(byte_array.splice(0, 8))) !== "DMNDTYPS") throw new Error("dt parse error, expected DMNDTYPS")
703
+
704
+ if (byte_array.shift() != 0) throw new Error("dt parse error, expected version 0")
705
+
706
+ let agents = []
707
+ let versions = []
708
+ let parentss = []
709
+
710
+ while (byte_array.length) {
711
+ let id = byte_array.shift()
712
+ let len = read_varint(byte_array)
713
+ if (id == 1) {
714
+ } else if (id == 3) {
715
+ let goal = byte_array.length - len
716
+ while (byte_array.length > goal) {
717
+ agents.push(read_string(byte_array))
718
+ }
719
+ } else if (id == 20) {
720
+ } else if (id == 21) {
721
+ let seqs = {}
722
+ let goal = byte_array.length - len
723
+ while (byte_array.length > goal) {
724
+ let part0 = read_varint(byte_array)
725
+ let has_jump = part0 & 1
726
+ let agent_i = (part0 >> 1) - 1
727
+ let run_length = read_varint(byte_array)
728
+ let jump = 0
729
+ if (has_jump) {
730
+ let part2 = read_varint(byte_array)
731
+ jump = part2 >> 1
732
+ if (part2 & 1) jump *= -1
733
+ }
734
+ let base = (seqs[agent_i] || 0) + jump
735
+
736
+ for (let i = 0; i < run_length; i++) {
737
+ versions.push([agents[agent_i], base + i])
738
+ }
739
+ seqs[agent_i] = base + run_length
740
+ }
741
+ } else if (id == 23) {
742
+ let count = 0
743
+ let goal = byte_array.length - len
744
+ while (byte_array.length > goal) {
745
+ let run_len = read_varint(byte_array)
746
+
747
+ let parents = []
748
+ let has_more = 1
749
+ while (has_more) {
750
+ let x = read_varint(byte_array)
751
+ let is_foreign = 0x1 & x
752
+ has_more = 0x2 & x
753
+ let num = x >> 2
754
+
755
+ if (x == 1) {
756
+ parents.push(["root"])
757
+ } else if (!is_foreign) {
758
+ parents.push(versions[count - num])
759
+ } else {
760
+ parents.push([agents[num - 1], read_varint(byte_array)])
761
+ }
762
+ }
763
+ parentss.push(parents)
764
+ count++
765
+
766
+ for (let i = 0; i < run_len - 1; i++) {
767
+ parentss.push([versions[count - 1]])
768
+ count++
769
+ }
770
+ }
771
+ } else {
772
+ byte_array.splice(0, len)
773
+ }
774
+ }
775
+
776
+ function read_string(byte_array) {
777
+ return new TextDecoder().decode(new Uint8Array(byte_array.splice(0, read_varint(byte_array))))
778
+ }
779
+
780
+ function read_varint(byte_array) {
781
+ let result = 0
782
+ let shift = 0
783
+ while (true) {
784
+ if (byte_array.length === 0) throw new Error("byte array does not contain varint")
785
+
786
+ let byte_val = byte_array.shift()
787
+ result |= (byte_val & 0x7f) << shift
788
+ if ((byte_val & 0x80) == 0) return result
789
+ shift += 7
790
+ }
791
+ }
792
+
793
+ return [agents, versions, parentss]
794
+ }
795
+
796
+ function OpLog_create_bytes(version, parents, pos, ins) {
797
+ // console.log(`args = ${JSON.stringify({ version, parents, pos, ins }, null, 4)}`)
798
+
799
+ function write_varint(bytes, value) {
800
+ while (value >= 0x80) {
801
+ bytes.push((value & 0x7f) | 0x80)
802
+ value >>= 7
803
+ }
804
+ bytes.push(value)
805
+ }
806
+
807
+ function write_string(byte_array, str) {
808
+ let str_bytes = new TextEncoder().encode(str)
809
+ write_varint(byte_array, str_bytes.length)
810
+ byte_array.push(...str_bytes)
811
+ }
812
+
813
+ version = decode_version(version)
814
+ parents = parents.map(decode_version)
815
+
816
+ let bytes = []
817
+ bytes = bytes.concat(Array.from(new TextEncoder().encode("DMNDTYPS")))
818
+ bytes.push(0)
819
+
820
+ let file_info = []
821
+ let agent_names = []
822
+
823
+ let agents = new Set()
824
+ agents.add(version[0])
825
+ for (let p of parents) if (p.length > 1) agents.add(p[0])
826
+ agents = [...agents]
827
+
828
+ // console.log(JSON.stringify({ agents, parents }, null, 4));
829
+
830
+ let agent_to_i = {}
831
+ for (let [i, agent] of agents.entries()) {
832
+ agent_to_i[agent] = i
833
+ write_string(agent_names, agent)
834
+ }
835
+
836
+ file_info.push(3)
837
+ write_varint(file_info, agent_names.length)
838
+ file_info.push(...agent_names)
839
+
840
+ bytes.push(1)
841
+ write_varint(bytes, file_info.length)
842
+ bytes.push(...file_info)
843
+
844
+ let branch = []
845
+
846
+ if (parents[0].length > 1) {
847
+ let frontier = []
848
+
849
+ for (let [i, [agent, seq]] of parents.entries()) {
850
+ let has_more = i < parents.length - 1
851
+ let mapped = agent_to_i[agent]
852
+ let n = ((mapped + 1) << 1) | (has_more ? 1 : 0)
853
+ write_varint(frontier, n)
854
+ write_varint(frontier, seq)
855
+ }
856
+
857
+ branch.push(12)
858
+ write_varint(branch, frontier.length)
859
+ branch.push(...frontier)
860
+ }
861
+
862
+ bytes.push(10)
863
+ write_varint(bytes, branch.length)
864
+ bytes.push(...branch)
865
+
866
+ let patches = []
867
+
868
+ if (ins) {
869
+ let inserted_content_bytes = []
870
+
871
+ inserted_content_bytes.push(0) // ins (not del, which is 1)
872
+
873
+ inserted_content_bytes.push(13) // "content" enum (rather than compressed)
874
+
875
+ let encoder = new TextEncoder()
876
+ let utf8Bytes = encoder.encode(ins)
877
+
878
+ inserted_content_bytes.push(1 + utf8Bytes.length) // length of content chunk
879
+ inserted_content_bytes.push(4) // "plain text" enum
880
+
881
+ for (let b of utf8Bytes) inserted_content_bytes.push(b) // actual text
882
+
883
+ inserted_content_bytes.push(25) // "known" enum
884
+ inserted_content_bytes.push(1) // length of "known" chunk
885
+ inserted_content_bytes.push(3) // content of length 1, and we "know" it
886
+
887
+ patches.push(24)
888
+ write_varint(patches, inserted_content_bytes.length)
889
+ patches.push(...inserted_content_bytes)
890
+ }
891
+
892
+ // write in the version
893
+ let version_bytes = []
894
+
895
+ let [agent, seq] = version
896
+ let agent_i = agent_to_i[agent]
897
+ let jump = seq
898
+
899
+ write_varint(version_bytes, ((agent_i + 1) << 1) | (jump != 0 ? 1 : 0))
900
+ write_varint(version_bytes, 1)
901
+ if (jump) write_varint(version_bytes, jump << 1)
902
+
903
+ patches.push(21)
904
+ write_varint(patches, version_bytes.length)
905
+ patches.push(...version_bytes)
906
+
907
+ // write in "op" bytes (some encoding of position)
908
+ let op_bytes = []
909
+
910
+ write_varint(op_bytes, (pos << 4) | (pos ? 2 : 0) | (ins ? 0 : 4))
911
+
912
+ patches.push(22)
913
+ write_varint(patches, op_bytes.length)
914
+ patches.push(...op_bytes)
915
+
916
+ // write in parents
917
+ let parents_bytes = []
918
+
919
+ write_varint(parents_bytes, 1)
920
+
921
+ if (parents[0].length > 1) {
922
+ for (let [i, [agent, seq]] of parents.entries()) {
923
+ let has_more = i < parents.length - 1
924
+ let agent_i = agent_to_i[agent]
925
+ write_varint(parents_bytes, ((agent_i + 1) << 2) | (has_more ? 2 : 0) | 1)
926
+ write_varint(parents_bytes, seq)
927
+ }
928
+ } else write_varint(parents_bytes, 1)
929
+
930
+ patches.push(23)
931
+ write_varint(patches, parents_bytes.length)
932
+ patches.push(...parents_bytes)
933
+
934
+ // write in patches
935
+ bytes.push(20)
936
+ write_varint(bytes, patches.length)
937
+ bytes.push(...patches)
938
+
939
+ // console.log(bytes);
940
+ return bytes
941
+ }
942
+
943
+ function OpLog_remote_to_local(doc, frontier) {
944
+ let map = Object.fromEntries(frontier.map((x) => [x, true]))
945
+
946
+ let local_version = []
947
+ let [agents, versions, parentss] = parseDT([...doc.toBytes()])
948
+ for (let i = 0; i < versions.length; i++) {
949
+ if (map[doc.localToRemoteVersion([i])[0].join("-")]) {
950
+ local_version.push(i)
951
+ }
952
+ }
953
+
954
+ return frontier.length == local_version.length && new Uint32Array(local_version)
955
+ }
956
+
957
+ function encode_version(agent, seq) {
958
+ return agent + "-" + seq
959
+ }
960
+
961
+ function decode_version(v) {
962
+ let a = v.split("-")
963
+ if (a.length > 1) a[1] = parseInt(a[1])
964
+ return a
965
+ }
966
+
967
+ function v_eq(v1, v2) {
968
+ return v1.length == v2.length && v1.every((x, i) => x == v2[i])
969
+ }
970
+
971
+ function get_xf_patches(doc, v) {
972
+ let patches = []
973
+ for (let xf of doc.xfSince(v)) {
974
+ patches.push(
975
+ xf.kind == "Ins"
976
+ ? {
977
+ unit: "text",
978
+ range: `[${xf.start}:${xf.start}]`,
979
+ content: xf.content,
980
+ }
981
+ : {
982
+ unit: "text",
983
+ range: `[${xf.start}:${xf.end}]`,
984
+ content: "",
985
+ }
986
+ )
987
+ }
988
+ return relative_to_absolute_patches(patches)
989
+ }
990
+
991
+ function relative_to_absolute_patches(patches) {
992
+ let avl = create_avl_tree((node) => {
993
+ let parent = node.parent
994
+ if (parent.left == node) {
995
+ parent.left_size -= node.left_size + node.size
996
+ } else {
997
+ node.left_size += parent.left_size + parent.size
998
+ }
999
+ })
1000
+ avl.root.size = Infinity
1001
+ avl.root.left_size = 0
1002
+
1003
+ function resize(node, new_size) {
1004
+ if (node.size == new_size) return
1005
+ let delta = new_size - node.size
1006
+ node.size = new_size
1007
+ while (node.parent) {
1008
+ if (node.parent.left == node) node.parent.left_size += delta
1009
+ node = node.parent
1010
+ }
1011
+ }
1012
+
1013
+ for (let p of patches) {
1014
+ let [start, end] = p.range.match(/\d+/g).map((x) => 1 * x)
1015
+ let del = end - start
1016
+
1017
+ let node = avl.root
1018
+ while (true) {
1019
+ if (start < node.left_size || (node.left && node.content == null && start == node.left_size)) {
1020
+ node = node.left
1021
+ } else if (start > node.left_size + node.size || (node.content == null && start == node.left_size + node.size)) {
1022
+ start -= node.left_size + node.size
1023
+ node = node.right
1024
+ } else {
1025
+ start -= node.left_size
1026
+ break
1027
+ }
1028
+ }
1029
+
1030
+ let remaining = start + del - node.size
1031
+ if (remaining < 0) {
1032
+ if (node.content == null) {
1033
+ if (start > 0) {
1034
+ let x = { size: 0, left_size: 0 }
1035
+ avl.add(node, "left", x)
1036
+ resize(x, start)
1037
+ }
1038
+ let x = { size: 0, left_size: 0, content: p.content, del }
1039
+ avl.add(node, "left", x)
1040
+ resize(x, count_code_points(x.content))
1041
+ resize(node, node.size - (start + del))
1042
+ } else {
1043
+ node.content = node.content.slice(0, codePoints_to_index(node.content, start)) + p.content + node.content.slice(codePoints_to_index(node.content, start + del))
1044
+ resize(node, count_code_points(node.content))
1045
+ }
1046
+ } else {
1047
+ let next
1048
+ let middle_del = 0
1049
+ while (remaining >= (next = avl.next(node)).size) {
1050
+ remaining -= next.size
1051
+ middle_del += next.del ?? next.size
1052
+ resize(next, 0)
1053
+ avl.del(next)
1054
+ }
1055
+
1056
+ if (node.content == null) {
1057
+ if (next.content == null) {
1058
+ if (start == 0) {
1059
+ node.content = p.content
1060
+ node.del = node.size + middle_del + remaining
1061
+ resize(node, count_code_points(node.content))
1062
+ } else {
1063
+ let x = {
1064
+ size: 0,
1065
+ left_size: 0,
1066
+ content: p.content,
1067
+ del: node.size - start + middle_del + remaining,
1068
+ }
1069
+ resize(node, start)
1070
+ avl.add(node, "right", x)
1071
+ resize(x, count_code_points(x.content))
1072
+ }
1073
+ resize(next, next.size - remaining)
1074
+ } else {
1075
+ next.del += node.size - start + middle_del
1076
+ next.content = p.content + next.content.slice(codePoints_to_index(next.content, remaining))
1077
+ resize(node, start)
1078
+ if (node.size == 0) avl.del(node)
1079
+ resize(next, count_code_points(next.content))
1080
+ }
1081
+ } else {
1082
+ if (next.content == null) {
1083
+ node.del += middle_del + remaining
1084
+ node.content = node.content.slice(0, codePoints_to_index(node.content, start)) + p.content
1085
+ resize(node, count_code_points(node.content))
1086
+ resize(next, next.size - remaining)
1087
+ } else {
1088
+ node.del += middle_del + next.del
1089
+ node.content = node.content.slice(0, codePoints_to_index(node.content, start)) + p.content + next.content.slice(codePoints_to_index(next.content, remaining))
1090
+ resize(node, count_code_points(node.content))
1091
+ resize(next, 0)
1092
+ avl.del(next)
1093
+ }
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ let new_patches = []
1099
+ let offset = 0
1100
+ let node = avl.root
1101
+ while (node.left) node = node.left
1102
+ while (node) {
1103
+ if (node.content == null) {
1104
+ offset += node.size
1105
+ } else {
1106
+ new_patches.push({
1107
+ unit: patches[0].unit,
1108
+ range: `[${offset}:${offset + node.del}]`,
1109
+ content: node.content,
1110
+ })
1111
+ offset += node.del
1112
+ }
1113
+
1114
+ node = avl.next(node)
1115
+ }
1116
+ return new_patches
1117
+ }
1118
+
1119
+ function create_avl_tree(on_rotate) {
1120
+ let self = { root: { height: 1 } }
1121
+
1122
+ self.calc_height = (node) => {
1123
+ node.height = 1 + Math.max(node.left?.height ?? 0, node.right?.height ?? 0)
1124
+ }
1125
+
1126
+ self.rechild = (child, new_child) => {
1127
+ if (child.parent) {
1128
+ if (child.parent.left == child) {
1129
+ child.parent.left = new_child
1130
+ } else {
1131
+ child.parent.right = new_child
1132
+ }
1133
+ } else {
1134
+ self.root = new_child
1135
+ }
1136
+ if (new_child) new_child.parent = child.parent
1137
+ }
1138
+
1139
+ self.rotate = (node) => {
1140
+ on_rotate(node)
1141
+
1142
+ let parent = node.parent
1143
+ let left = parent.right == node ? "left" : "right"
1144
+ let right = parent.right == node ? "right" : "left"
1145
+
1146
+ parent[right] = node[left]
1147
+ if (parent[right]) parent[right].parent = parent
1148
+ self.calc_height(parent)
1149
+
1150
+ self.rechild(parent, node)
1151
+ parent.parent = node
1152
+
1153
+ node[left] = parent
1154
+ }
1155
+
1156
+ self.fix_avl = (node) => {
1157
+ self.calc_height(node)
1158
+ let diff = (node.right?.height ?? 0) - (node.left?.height ?? 0)
1159
+ if (Math.abs(diff) >= 2) {
1160
+ if (diff > 0) {
1161
+ if ((node.right.left?.height ?? 0) > (node.right.right?.height ?? 0)) self.rotate(node.right.left)
1162
+ self.rotate((node = node.right))
1163
+ } else {
1164
+ if ((node.left.right?.height ?? 0) > (node.left.left?.height ?? 0)) self.rotate(node.left.right)
1165
+ self.rotate((node = node.left))
1166
+ }
1167
+ self.fix_avl(node)
1168
+ } else if (node.parent) self.fix_avl(node.parent)
1169
+ }
1170
+
1171
+ self.add = (node, side, add_me) => {
1172
+ let other_side = side == "left" ? "right" : "left"
1173
+ add_me.height = 1
1174
+
1175
+ if (node[side]) {
1176
+ node = node[side]
1177
+ while (node[other_side]) node = node[other_side]
1178
+ node[other_side] = add_me
1179
+ } else {
1180
+ node[side] = add_me
1181
+ }
1182
+ add_me.parent = node
1183
+ self.fix_avl(node)
1184
+ }
1185
+
1186
+ self.del = (node) => {
1187
+ if (node.left && node.right) {
1188
+ let cursor = node.right
1189
+ while (cursor.left) cursor = cursor.left
1190
+ cursor.left = node.left
1191
+
1192
+ // breaks abstraction
1193
+ cursor.left_size = node.left_size
1194
+ let y = cursor
1195
+ while (y.parent != node) {
1196
+ y = y.parent
1197
+ y.left_size -= cursor.size
1198
+ }
1199
+
1200
+ node.left.parent = cursor
1201
+ if (cursor == node.right) {
1202
+ self.rechild(node, cursor)
1203
+ self.fix_avl(cursor)
1204
+ } else {
1205
+ let x = cursor.parent
1206
+ self.rechild(cursor, cursor.right)
1207
+ cursor.right = node.right
1208
+ node.right.parent = cursor
1209
+ self.rechild(node, cursor)
1210
+ self.fix_avl(x)
1211
+ }
1212
+ } else {
1213
+ self.rechild(node, node.left || node.right || null)
1214
+ if (node.parent) self.fix_avl(node.parent)
1215
+ }
1216
+ }
1217
+
1218
+ self.next = (node) => {
1219
+ if (node.right) {
1220
+ node = node.right
1221
+ while (node.left) node = node.left
1222
+ return node
1223
+ } else {
1224
+ while (node.parent && node.parent.right == node) node = node.parent
1225
+ return node.parent
1226
+ }
1227
+ }
1228
+
1229
+ return self
1230
+ }
1231
+
1232
+ function count_code_points(str) {
1233
+ let code_points = 0;
1234
+ for (let i = 0; i < str.length; i++) {
1235
+ if (str.charCodeAt(i) >= 0xD800 && str.charCodeAt(i) <= 0xDBFF) i++;
1236
+ code_points++;
1237
+ }
1238
+ return code_points;
1239
+ }
1240
+
1241
+ function index_to_codePoints(str, index) {
1242
+ let i = 0
1243
+ let c = 0
1244
+ while (i < index && i < str.length) {
1245
+ const charCode = str.charCodeAt(i)
1246
+ i += (charCode >= 0xd800 && charCode <= 0xdbff) ? 2 : 1
1247
+ c++
1248
+ }
1249
+ return c
1250
+ }
1251
+
1252
+ function codePoints_to_index(str, codePoints) {
1253
+ let i = 0
1254
+ let c = 0
1255
+ while (c < codePoints && i < str.length) {
1256
+ const charCode = str.charCodeAt(i)
1257
+ i += (charCode >= 0xd800 && charCode <= 0xdbff) ? 2 : 1
1258
+ c++
1259
+ }
1260
+ return i
1261
+ }
1262
+
1263
+ module.exports = braid_text