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/README.md +122 -0
- package/client.js +173 -0
- package/editor.html +79 -0
- package/index.js +1263 -0
- package/markdown-editor.html +316 -0
- package/package.json +12 -0
- package/server-demo.js +79 -0
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
|