braid-text 0.5.7 → 0.5.8
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/client/simpleton-sync.js +114 -188
- package/client/web-utils.js +8 -7
- package/package.json +2 -2
- package/server-demo.js +1 -1
- package/server.js +57 -47
package/client/simpleton-sync.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
// ******* Reference Implementation of Simpleton Client Algorithm *********
|
|
3
|
-
// ************************************************************************
|
|
4
|
-
//
|
|
5
|
-
// This is the canonical JS reference for implementing a simpleton client.
|
|
6
|
-
// Other language implementations should mirror this logic exactly, with
|
|
7
|
-
// adaptations only for language-specific details (e.g., string encoding).
|
|
8
|
-
//
|
|
9
|
-
// requires braid-http@~1.3/braid-http-client.js
|
|
1
|
+
// Simpleton Javascript Client
|
|
10
2
|
//
|
|
3
|
+
// requires braid-http@~1.3/braid-http-client.js
|
|
4
|
+
|
|
11
5
|
// --- API ---
|
|
12
6
|
//
|
|
13
|
-
// url: resource endpoint
|
|
14
|
-
//
|
|
15
|
-
// headers: custom headers that get forwarded through into the fetch
|
|
16
|
-
//
|
|
17
7
|
// on_patches?: (patches) => void
|
|
18
8
|
// processes incoming patches by applying them to the UI/textarea.
|
|
19
9
|
// Patches are guaranteed to be in-order and non-overlapping.
|
|
@@ -74,26 +64,6 @@
|
|
|
74
64
|
// get_patches is optional.)
|
|
75
65
|
// call abort() to abort the subscription.
|
|
76
66
|
//
|
|
77
|
-
// --- Retry and Reconnection Behavior ---
|
|
78
|
-
//
|
|
79
|
-
// Simpleton relies on braid_fetch for retry/reconnection:
|
|
80
|
-
//
|
|
81
|
-
// Subscription (GET):
|
|
82
|
-
// retry: () => true — always reconnect on any error (network failure,
|
|
83
|
-
// HTTP error, etc.). Reconnection backs off:
|
|
84
|
-
// delay = Math.min(retry_count + 1, 3) * 1000 ms
|
|
85
|
-
// i.e., 1s, 2s, 3s, 3s, 3s, ...
|
|
86
|
-
// On reconnect, sends Parents via the parents callback to resume
|
|
87
|
-
// from where the client left off.
|
|
88
|
-
//
|
|
89
|
-
// PUT requests:
|
|
90
|
-
// retry: (res) => res.status !== 550 — retry all errors EXCEPT
|
|
91
|
-
// HTTP 550 (Repr-Digest mismatch, meaning client is out of sync).
|
|
92
|
-
// This means:
|
|
93
|
-
// - Connection failure: retried with backoff
|
|
94
|
-
// - HTTP 401, 403, 408, 429, 500, 502, 503, 504, etc.: retried
|
|
95
|
-
// - HTTP 550: out of sync — stop retrying, throw error. The
|
|
96
|
-
// client must be torn down and restarted from scratch.
|
|
97
67
|
//
|
|
98
68
|
// --- Local Edit Absorption ---
|
|
99
69
|
//
|
|
@@ -113,26 +83,27 @@
|
|
|
113
83
|
// update; otherwise your UI will not reflect remote changes.
|
|
114
84
|
//
|
|
115
85
|
function simpleton_client(url, {
|
|
116
|
-
on_patches,
|
|
117
|
-
on_state,
|
|
118
86
|
get_patches,
|
|
119
87
|
get_state,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
88
|
+
|
|
89
|
+
on_patches,
|
|
90
|
+
on_state,
|
|
123
91
|
on_error,
|
|
124
92
|
on_online,
|
|
125
93
|
on_ack,
|
|
94
|
+
|
|
95
|
+
headers, // The user can pass in custom headers
|
|
96
|
+
// that are forwarded into fetches
|
|
97
|
+
content_type,
|
|
126
98
|
send_digests
|
|
127
99
|
}) {
|
|
128
100
|
var peer = Math.random().toString(36).slice(2)
|
|
129
101
|
var client_version = [] // sorted version strings
|
|
130
102
|
var client_state = "" // text as of client_version
|
|
131
103
|
var char_counter = -1 // char-delta for version IDs
|
|
132
|
-
var
|
|
133
|
-
var
|
|
134
|
-
var
|
|
135
|
-
var throttled_updates = []
|
|
104
|
+
var dirty = false // true when local edits exist but haven't been sent
|
|
105
|
+
var is_online = false
|
|
106
|
+
var outstanding_puts = 0
|
|
136
107
|
|
|
137
108
|
// extend the headers with merge-type and peer
|
|
138
109
|
headers = {
|
|
@@ -145,31 +116,26 @@ function simpleton_client(url, {
|
|
|
145
116
|
// channel with automatic reconnection and PUT queuing.
|
|
146
117
|
var channel = reliable_update_channel(url, {
|
|
147
118
|
reconnect_from_parents: () => client_version.length ? client_version : null,
|
|
148
|
-
get_headers: {
|
|
149
|
-
|
|
150
|
-
...content_type && {Accept: content_type}
|
|
151
|
-
},
|
|
152
|
-
put_headers: {
|
|
153
|
-
...headers,
|
|
154
|
-
...content_type && {"Content-Type": content_type}
|
|
155
|
-
},
|
|
119
|
+
get_headers: { ...headers, ...content_type && {Accept: content_type} },
|
|
120
|
+
put_headers: { ...headers, ...content_type && {"Content-Type": content_type} },
|
|
156
121
|
on_update: async update => {
|
|
157
122
|
// ── Parent check ────────────────────────────────────────
|
|
158
123
|
// Core simpleton invariant: only accept updates whose
|
|
159
|
-
// parents
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// queued but not applied — they'll be applied later when
|
|
163
|
-
// the throttle clears (see changed()).
|
|
124
|
+
// parents match our current version. If we're dirty
|
|
125
|
+
// (have unsent local edits), skip — we'll reconnect
|
|
126
|
+
// once the edits are flushed.
|
|
164
127
|
update.parents.sort()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
128
|
+
if (!dirty && versions_eq(client_version, update.parents))
|
|
129
|
+
await apply_update(update)
|
|
130
|
+
},
|
|
131
|
+
on_status: status => {
|
|
132
|
+
is_online = status.online
|
|
133
|
+
outstanding_puts = status.outstanding_puts
|
|
134
|
+
if (on_online) on_online(status.online)
|
|
135
|
+
if (on_ack && outstanding_puts === 0) on_ack()
|
|
136
|
+
if (dirty && is_online && outstanding_puts < 10)
|
|
137
|
+
try_send()
|
|
171
138
|
},
|
|
172
|
-
on_status: status => on_online && on_online(status.online),
|
|
173
139
|
on_error: err => on_error && on_error(err),
|
|
174
140
|
|
|
175
141
|
// this api is preliminary and undocumented;
|
|
@@ -200,12 +166,12 @@ function simpleton_client(url, {
|
|
|
200
166
|
// are natively indexed by code points (e.g., Emacs Lisp,
|
|
201
167
|
// Python, Rust's char iterator).
|
|
202
168
|
convert_ranges_codepoints_to_utf16(patches, client_state)
|
|
203
|
-
} else
|
|
169
|
+
} else
|
|
204
170
|
// Initial snapshot: convert body to a patch replacing
|
|
205
171
|
// [0,0] so it follows the same code path as incremental
|
|
206
172
|
// patches.
|
|
207
173
|
patches = [{range: [0, 0], content: update.body_text}]
|
|
208
|
-
|
|
174
|
+
|
|
209
175
|
|
|
210
176
|
// ── Apply the update ────────────────────────────────────────
|
|
211
177
|
if (on_patches) {
|
|
@@ -216,11 +182,11 @@ function simpleton_client(url, {
|
|
|
216
182
|
// changed() after every local edit to avoid this.
|
|
217
183
|
on_patches(patches)
|
|
218
184
|
client_state = get_state()
|
|
219
|
-
} else
|
|
185
|
+
} else
|
|
220
186
|
// Apply patches to our internal state; the
|
|
221
187
|
// result is delivered via on_state below.
|
|
222
188
|
client_state = apply_patches(client_state, patches)
|
|
223
|
-
|
|
189
|
+
|
|
224
190
|
|
|
225
191
|
// ── Advance version ─────────────────────────────────────────
|
|
226
192
|
// IMPORTANT: This must happen synchronously (before any await)
|
|
@@ -235,135 +201,83 @@ function simpleton_client(url, {
|
|
|
235
201
|
// loop handles flushing accumulated edits.
|
|
236
202
|
if (on_state) on_state(client_state)
|
|
237
203
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
// subscription handler. The document is corrupted and
|
|
242
|
-
// continuing would compound the problem.
|
|
243
|
-
//
|
|
244
|
-
// This is placed after advancing client_version and calling
|
|
245
|
-
// on_state so that the await does not create a yield point
|
|
246
|
-
// between applying patches and advancing client_version.
|
|
247
|
-
// That yield point previously allowed the changed() PUT loop
|
|
248
|
-
// to interleave, capturing a stale client_version and causing
|
|
249
|
-
// edit loss.
|
|
250
|
-
if (update.extra_headers &&
|
|
251
|
-
update.extra_headers["repr-digest"] &&
|
|
252
|
-
update.extra_headers["repr-digest"].startsWith('sha-256=') &&
|
|
253
|
-
update.extra_headers["repr-digest"] !== await get_digest(client_state)) {
|
|
254
|
-
console.log('repr-digest mismatch!')
|
|
255
|
-
console.log('repr-digest: ' + update.extra_headers["repr-digest"])
|
|
256
|
-
console.log('state: ' + client_state)
|
|
257
|
-
throw new Error('repr-digest mismatch')
|
|
258
|
-
}
|
|
204
|
+
// Now verify that we did this correct, and are in sync. We do this
|
|
205
|
+
// at the end, so the prior updating is atomic.
|
|
206
|
+
await check_digest(update, client_state)
|
|
259
207
|
}
|
|
260
208
|
|
|
261
|
-
// ──
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// edits (the async accumulation loop), sending them too.
|
|
273
|
-
//
|
|
274
|
-
// The async accumulation loop (while(true) {...}) is equivalent
|
|
275
|
-
// to a callback-driven flush: after each PUT ACK, re-diff and
|
|
276
|
-
// send again if changed. This ensures edits that accumulate
|
|
277
|
-
// during a PUT round-trip are eventually sent.
|
|
278
|
-
changed: () => {
|
|
279
|
-
function get_change() {
|
|
280
|
-
var new_state = get_state()
|
|
281
|
-
if (new_state === client_state) return null
|
|
282
|
-
var patches = get_patches ? get_patches(client_state) :
|
|
283
|
-
[simple_diff(client_state, new_state)]
|
|
284
|
-
return {patches, new_state}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
var change = get_change()
|
|
288
|
-
if (!change) {
|
|
289
|
-
if (throttled) {
|
|
290
|
-
throttled = false
|
|
291
|
-
for (var update of throttled_updates)
|
|
292
|
-
if (versions_eq(client_version, update.parents))
|
|
293
|
-
apply_update(update).catch(on_error)
|
|
294
|
-
throttled_updates = []
|
|
295
|
-
}
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (outstanding_changes >= max_outstanding_changes) {
|
|
300
|
-
throttled = true
|
|
209
|
+
// ── try_send — attempt to flush local edits ───────────────────────
|
|
210
|
+
// Called from changed() and on_status. Diffs client_state vs current
|
|
211
|
+
// state and sends a PUT if there's a change. If dirty but no diff,
|
|
212
|
+
// we may have missed updates while dirty, so reconnect to re-sync.
|
|
213
|
+
function try_send() {
|
|
214
|
+
var new_state = get_state()
|
|
215
|
+
if (new_state === client_state) {
|
|
216
|
+
// No local diff — but we were dirty, meaning we may have
|
|
217
|
+
// skipped incoming updates. Reconnect to catch up.
|
|
218
|
+
dirty = false
|
|
219
|
+
channel.reconnect()
|
|
301
220
|
return
|
|
302
221
|
}
|
|
303
222
|
|
|
304
|
-
var
|
|
223
|
+
var patches = get_patches ? get_patches(client_state) :
|
|
224
|
+
[simple_diff(client_state, new_state)]
|
|
305
225
|
|
|
306
226
|
// Save JS-index patches before code-point conversion mutates them
|
|
307
227
|
var js_patches = patches.map(p => ({range: [...p.range], content: p.content}))
|
|
308
228
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
await channel.put({
|
|
346
|
-
version, parents, patches,
|
|
347
|
-
headers: {
|
|
348
|
-
...send_digests && {
|
|
349
|
-
"Repr-Digest": await get_digest(client_state) }
|
|
350
|
-
}
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
throttled = false
|
|
354
|
-
outstanding_changes--
|
|
355
|
-
if (on_ack && !outstanding_changes) on_ack()
|
|
356
|
-
|
|
357
|
-
// ── Check for accumulated edits ─────────────────────
|
|
358
|
-
// While the PUT was in flight, more local edits may
|
|
359
|
-
// have occurred. Diff again and loop if changed.
|
|
360
|
-
var more = get_change()
|
|
361
|
-
if (!more) return
|
|
362
|
-
;({patches, new_state} = more)
|
|
363
|
-
}
|
|
364
|
-
})()
|
|
229
|
+
// ── JS-SPECIFIC: Convert JS UTF-16 ranges to code-points ────
|
|
230
|
+
// The wire protocol uses code-point offsets. See the
|
|
231
|
+
// inverse conversion in the receive path above.
|
|
232
|
+
//
|
|
233
|
+
// OTHER LANGUAGES: Skip this if your strings are
|
|
234
|
+
// natively code-point indexed.
|
|
235
|
+
convert_ranges_utf16_to_codepoints(patches, client_state)
|
|
236
|
+
|
|
237
|
+
for (let patch of patches) {
|
|
238
|
+
// ── Update char_counter ─────────────────────────────────
|
|
239
|
+
// Increment by deleted chars + inserted chars
|
|
240
|
+
char_counter += patch.range[1] - patch.range[0]
|
|
241
|
+
char_counter += count_code_points(patch.content)
|
|
242
|
+
|
|
243
|
+
patch.unit = "text"
|
|
244
|
+
patch.range = `[${patch.range[0]}:${patch.range[1]}]`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Compute version and advance optimistically ──────────────
|
|
248
|
+
var version = [peer + "-" + char_counter]
|
|
249
|
+
|
|
250
|
+
var parents = client_version
|
|
251
|
+
client_version = version // optimistic advance
|
|
252
|
+
client_state = new_state // update client_state
|
|
253
|
+
dirty = false
|
|
254
|
+
|
|
255
|
+
// Send Update — when the PUT completes, on_status fires
|
|
256
|
+
// with updated outstanding_puts, which will call try_send
|
|
257
|
+
// again if dirty.
|
|
258
|
+
if (send_digests)
|
|
259
|
+
get_digest(client_state).then(digest =>
|
|
260
|
+
channel.put({ version, parents, patches,
|
|
261
|
+
headers: { "Repr-Digest": digest } }))
|
|
262
|
+
else
|
|
263
|
+
channel.put({ version, parents, patches })
|
|
365
264
|
|
|
366
265
|
return js_patches
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Public interface ────────────────────────────────────────────────
|
|
269
|
+
return {
|
|
270
|
+
// ── abort() — cancel the subscription ─────────────────────────
|
|
271
|
+
abort: () => channel.close(),
|
|
272
|
+
|
|
273
|
+
// ── changed() — call when local edits occur ───────────────────
|
|
274
|
+
// If online and under the PUT limit, sends immediately.
|
|
275
|
+
// Otherwise marks dirty — on_status will flush later.
|
|
276
|
+
changed: () => {
|
|
277
|
+
if (is_online && outstanding_puts < 10)
|
|
278
|
+
return try_send()
|
|
279
|
+
else
|
|
280
|
+
dirty = true
|
|
367
281
|
}
|
|
368
282
|
}
|
|
369
283
|
|
|
@@ -497,12 +411,24 @@ function simpleton_client(url, {
|
|
|
497
411
|
return state
|
|
498
412
|
}
|
|
499
413
|
|
|
500
|
-
//
|
|
501
|
-
// Computes SHA-256 of the UTF-8 encoding of the
|
|
502
|
-
//
|
|
503
|
-
// sha-256=:<base64-encoded-hash>:
|
|
414
|
+
// get_digest():
|
|
415
|
+
// - Computes SHA-256 of the UTF-8 encoding of the string
|
|
416
|
+
// - Formatted as Repr-Digest: sha-256=:<base64-encoded-hash>:
|
|
504
417
|
async function get_digest(str) {
|
|
505
418
|
var bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
|
|
506
419
|
return `sha-256=:${btoa(String.fromCharCode(...new Uint8Array(bytes)))}:`
|
|
507
420
|
}
|
|
421
|
+
// check_digest():
|
|
422
|
+
// - Makes sure the current state matches the digest in the update
|
|
423
|
+
async function check_digest(update, client_state) {
|
|
424
|
+
// If the server sent a repr-digest, verify our state matches. Throw
|
|
425
|
+
// exception if it fails.
|
|
426
|
+
if (update.extra_headers?.["repr-digest"]?.startsWith('sha-256=')
|
|
427
|
+
&& update.extra_headers["repr-digest"] !== await get_digest(client_state)) {
|
|
428
|
+
console.log('repr-digest mismatch!')
|
|
429
|
+
console.log('repr-digest: ' + update.extra_headers["repr-digest"])
|
|
430
|
+
console.log('state: ' + client_state)
|
|
431
|
+
throw new Error('repr-digest mismatch')
|
|
432
|
+
}
|
|
433
|
+
}
|
|
508
434
|
}
|
package/client/web-utils.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
|
|
2
|
-
function set_acked_state(textarea,
|
|
3
|
-
if (
|
|
2
|
+
function set_acked_state(textarea, on = true) {
|
|
3
|
+
if (on)
|
|
4
|
+
textarea.style.caretColor = textarea.old_caretColor
|
|
5
|
+
else {
|
|
4
6
|
textarea.old_caretColor = textarea.style.caretColor
|
|
5
|
-
|
|
6
7
|
textarea.style.caretColor = 'red'
|
|
7
|
-
} else {
|
|
8
|
-
textarea.style.caretColor = textarea.old_caretColor
|
|
9
8
|
}
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
function set_error_state(textarea,
|
|
13
|
-
if (
|
|
11
|
+
function set_error_state(textarea, on = true) {
|
|
12
|
+
if (on) {
|
|
14
13
|
textarea.old_disabled = textarea.disabled
|
|
15
14
|
textarea.old_background = textarea.style.background
|
|
16
15
|
textarea.old_border = textarea.style.border
|
|
@@ -25,6 +24,8 @@ function set_error_state(textarea, binary = true) {
|
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
// A convenient wrapper around the myers-diff.js library's "diff_main()" function,
|
|
28
|
+
// which is defined in https://braid.org/code/myers-diff1.js.
|
|
28
29
|
function diff(before, after) {
|
|
29
30
|
let diff = diff_main(before, after)
|
|
30
31
|
let patches = []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-text",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "Library for collaborative text over http using braid.",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braid-text",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@braid.org/diamond-types-node": "^2.0.1",
|
|
28
|
-
"braid-http": "~1.3.
|
|
28
|
+
"braid-http": "~1.3.124"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"yjs": "^13.6.0"
|
package/server-demo.js
CHANGED
|
@@ -14,7 +14,7 @@ var server = require("http").createServer(async (req, res) => {
|
|
|
14
14
|
if (req.method === 'OPTIONS') return res.end()
|
|
15
15
|
|
|
16
16
|
var q = req.url.split('?').slice(-1)[0]
|
|
17
|
-
if (q === 'editor' || q === 'markdown-editor' || q === 'yjs-editor'
|
|
17
|
+
if (q === 'editor' || q === 'markdown-editor' || q === 'yjs-editor') {
|
|
18
18
|
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-cache" })
|
|
19
19
|
require("fs").createReadStream(`./client/${q}.html`).pipe(res)
|
|
20
20
|
return
|
package/server.js
CHANGED
|
@@ -93,45 +93,55 @@ function create_braid_text() {
|
|
|
93
93
|
resource.save_meta()
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
96
|
+
// When we get an ackowledgement that a remote server has a version
|
|
97
|
+
// that we have:
|
|
98
|
+
//
|
|
99
|
+
// - In a PUT acknowledgement
|
|
100
|
+
// - Or a GET response
|
|
101
|
+
//
|
|
102
|
+
// ...then we extend our known fork point "frontier" to include that
|
|
103
|
+
// version.
|
|
104
|
+
function extend_fork_point(update) {
|
|
105
|
+
|
|
106
|
+
// Given a version frontier, incorporate a new update (version +
|
|
107
|
+
// parents) to compute the new frontier. Walks the DT version DAG
|
|
108
|
+
// if needed.
|
|
109
|
+
function extend_frontier(frontier, version, parents) {
|
|
110
|
+
var frontier_set = new Set(frontier)
|
|
111
|
+
// Fast path: if the frontier contains all the update's parents,
|
|
112
|
+
// just swap them out for the new version
|
|
113
|
+
if (parents.length &&
|
|
114
|
+
parents.every(p => frontier_set.has(p))) {
|
|
115
|
+
parents.forEach(p => frontier_set.delete(p))
|
|
116
|
+
for (var event of version) frontier_set.add(event)
|
|
117
|
+
frontier = [...frontier_set.values()]
|
|
118
|
+
} else {
|
|
119
|
+
// Slow path: walk the full DT history to compute the frontier
|
|
120
|
+
var looking_for = frontier_set
|
|
121
|
+
for (var event of version) looking_for.add(event)
|
|
122
|
+
|
|
123
|
+
frontier = []
|
|
124
|
+
var shadow = new Set()
|
|
125
|
+
|
|
126
|
+
var bytes = resource.dt.doc.toBytes()
|
|
127
|
+
var [_, events, parentss] = braid_text.dt_parse([...bytes])
|
|
128
|
+
for (var i = events.length - 1; i >= 0 && looking_for.size; i--) {
|
|
129
|
+
var e = events[i].join('-')
|
|
130
|
+
if (looking_for.has(e)) {
|
|
131
|
+
looking_for.delete(e)
|
|
132
|
+
if (!shadow.has(e)) frontier.push(e)
|
|
133
|
+
shadow.add(e)
|
|
134
|
+
}
|
|
135
|
+
if (shadow.has(e))
|
|
136
|
+
parentss[i].forEach(p => shadow.add(p.join('-')))
|
|
123
137
|
}
|
|
124
|
-
if (shadow.has(e))
|
|
125
|
-
parentss[i].forEach(p => shadow.add(p.join('-')))
|
|
126
138
|
}
|
|
139
|
+
return frontier.sort()
|
|
127
140
|
}
|
|
128
|
-
return frontier.sort()
|
|
129
|
-
}
|
|
130
141
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
update.version, update.parents)
|
|
142
|
+
resource.meta.fork_point = extend_frontier(resource.meta.fork_point,
|
|
143
|
+
update.version,
|
|
144
|
+
update.parents)
|
|
135
145
|
resource.save_meta()
|
|
136
146
|
}
|
|
137
147
|
|
|
@@ -370,8 +380,8 @@ function create_braid_text() {
|
|
|
370
380
|
res.setHeader('Content-Type', `${ct}; charset=utf-8`)
|
|
371
381
|
else if (charset.toLowerCase() !== 'charset=utf-8')
|
|
372
382
|
res.setHeader('Content-Type', ct_parts
|
|
373
|
-
|
|
374
|
-
|
|
383
|
+
.map(p => p.toLowerCase().startsWith('charset=') ? 'charset=utf-8' : p)
|
|
384
|
+
.join('; '))
|
|
375
385
|
|
|
376
386
|
// ── Handle simple methods that don't need further processing ──
|
|
377
387
|
|
|
@@ -407,9 +417,9 @@ function create_braid_text() {
|
|
|
407
417
|
var getting = {
|
|
408
418
|
subscribe: !!req.subscribe,
|
|
409
419
|
history: (has_parents && v_eq(req.parents, resource.version)) ? false
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
420
|
+
: has_parents ? 'since-parents'
|
|
421
|
+
: (req.subscribe || req.parents || req.headers['accept-transfer-encoding']) ? 'up-to-version'
|
|
422
|
+
: false,
|
|
413
423
|
transfer_encoding: req.headers['accept-transfer-encoding'],
|
|
414
424
|
}
|
|
415
425
|
getting.single_snapshot = !getting.subscribe && !getting.history
|
|
@@ -493,8 +503,7 @@ function create_braid_text() {
|
|
|
493
503
|
merge_type,
|
|
494
504
|
signal: aborter.signal,
|
|
495
505
|
accept_encoding:
|
|
496
|
-
req.headers['x-accept-encoding'] ??
|
|
497
|
-
req.headers['accept-encoding'],
|
|
506
|
+
req.headers['x-accept-encoding'] ?? req.headers['accept-encoding'],
|
|
498
507
|
subscribe: update => {
|
|
499
508
|
// Add digest for integrity checking on the client
|
|
500
509
|
if (update.version && v_eq(update.version, resource.version))
|
|
@@ -688,9 +697,9 @@ function create_braid_text() {
|
|
|
688
697
|
// 'up-to-version' = bring client up to current (from scratch)
|
|
689
698
|
// false = no history needed
|
|
690
699
|
history: (has_parents && v_eq(options.parents, version)) ? false
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
700
|
+
: has_parents ? 'since-parents'
|
|
701
|
+
: (options.subscribe || options.parents || options.transfer_encoding) ? 'up-to-version'
|
|
702
|
+
: false,
|
|
694
703
|
transfer_encoding: options.transfer_encoding,
|
|
695
704
|
}
|
|
696
705
|
getting.single_snapshot = !getting.subscribe && !getting.history
|
|
@@ -733,6 +742,7 @@ function create_braid_text() {
|
|
|
733
742
|
return { body: bytes }
|
|
734
743
|
}
|
|
735
744
|
|
|
745
|
+
// Each merge-type has a different way of getting history
|
|
736
746
|
switch (merge_type) {
|
|
737
747
|
|
|
738
748
|
case 'yjs':
|
|
@@ -774,7 +784,7 @@ function create_braid_text() {
|
|
|
774
784
|
|
|
775
785
|
if (getting.history && !getting.subscribe)
|
|
776
786
|
return dt_get_patches(resource.dt.doc,
|
|
777
|
-
|
|
787
|
+
getting.history === 'since-parents' ? options.parents : undefined)
|
|
778
788
|
|
|
779
789
|
if (getting.subscribe) {
|
|
780
790
|
var client = {
|
|
@@ -807,7 +817,7 @@ function create_braid_text() {
|
|
|
807
817
|
|
|
808
818
|
if (getting.history && !getting.subscribe)
|
|
809
819
|
return dt_get_patches(resource.dt.doc,
|
|
810
|
-
|
|
820
|
+
getting.history === 'since-parents' ? options.parents : undefined)
|
|
811
821
|
|
|
812
822
|
if (getting.subscribe) {
|
|
813
823
|
var client = {
|