braid-text 0.5.6 → 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 +147 -219
- package/client/web-utils.js +8 -7
- package/package.json +2 -2
- package/server-demo.js +1 -1
- package/server.js +71 -51
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.
|
|
@@ -55,6 +45,18 @@
|
|
|
55
45
|
//
|
|
56
46
|
// content_type: used for Accept and Content-Type headers
|
|
57
47
|
//
|
|
48
|
+
// on_error?: (error) => void
|
|
49
|
+
// called when an error occurs (e.g., network failure, digest mismatch)
|
|
50
|
+
//
|
|
51
|
+
// on_online?: (is_online) => void
|
|
52
|
+
// called when the connection status changes
|
|
53
|
+
//
|
|
54
|
+
// on_ack?: () => void
|
|
55
|
+
// called when all outstanding PUTs have been acknowledged
|
|
56
|
+
//
|
|
57
|
+
// send_digests?: boolean
|
|
58
|
+
// if truthy, includes a Repr-Digest header with each PUT
|
|
59
|
+
//
|
|
58
60
|
// returns { changed, abort }
|
|
59
61
|
// call changed() whenever there is a local change,
|
|
60
62
|
// and the system will call a combination of get_state and
|
|
@@ -62,26 +64,6 @@
|
|
|
62
64
|
// get_patches is optional.)
|
|
63
65
|
// call abort() to abort the subscription.
|
|
64
66
|
//
|
|
65
|
-
// --- Retry and Reconnection Behavior ---
|
|
66
|
-
//
|
|
67
|
-
// Simpleton relies on braid_fetch for retry/reconnection:
|
|
68
|
-
//
|
|
69
|
-
// Subscription (GET):
|
|
70
|
-
// retry: () => true — always reconnect on any error (network failure,
|
|
71
|
-
// HTTP error, etc.). Reconnection backs off:
|
|
72
|
-
// delay = Math.min(retry_count + 1, 3) * 1000 ms
|
|
73
|
-
// i.e., 1s, 2s, 3s, 3s, 3s, ...
|
|
74
|
-
// On reconnect, sends Parents via the parents callback to resume
|
|
75
|
-
// from where the client left off.
|
|
76
|
-
//
|
|
77
|
-
// PUT requests:
|
|
78
|
-
// retry: (res) => res.status !== 550 — retry all errors EXCEPT
|
|
79
|
-
// HTTP 550 (Repr-Digest mismatch, meaning client is out of sync).
|
|
80
|
-
// This means:
|
|
81
|
-
// - Connection failure: retried with backoff
|
|
82
|
-
// - HTTP 401, 403, 408, 429, 500, 502, 503, 504, etc.: retried
|
|
83
|
-
// - HTTP 550: out of sync — stop retrying, throw error. The
|
|
84
|
-
// client must be torn down and restarted from scratch.
|
|
85
67
|
//
|
|
86
68
|
// --- Local Edit Absorption ---
|
|
87
69
|
//
|
|
@@ -101,68 +83,67 @@
|
|
|
101
83
|
// update; otherwise your UI will not reflect remote changes.
|
|
102
84
|
//
|
|
103
85
|
function simpleton_client(url, {
|
|
104
|
-
on_patches,
|
|
105
|
-
on_state,
|
|
106
86
|
get_patches,
|
|
107
87
|
get_state,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
88
|
+
|
|
89
|
+
on_patches,
|
|
90
|
+
on_state,
|
|
111
91
|
on_error,
|
|
112
|
-
on_res,
|
|
113
92
|
on_online,
|
|
114
93
|
on_ack,
|
|
94
|
+
|
|
95
|
+
headers, // The user can pass in custom headers
|
|
96
|
+
// that are forwarded into fetches
|
|
97
|
+
content_type,
|
|
115
98
|
send_digests
|
|
116
99
|
}) {
|
|
117
100
|
var peer = Math.random().toString(36).slice(2)
|
|
118
101
|
var client_version = [] // sorted version strings
|
|
119
102
|
var client_state = "" // text as of client_version
|
|
120
103
|
var char_counter = -1 // char-delta for version IDs
|
|
121
|
-
var
|
|
122
|
-
var
|
|
123
|
-
var
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
var dirty = false // true when local edits exist but haven't been sent
|
|
105
|
+
var is_online = false
|
|
106
|
+
var outstanding_puts = 0
|
|
107
|
+
|
|
108
|
+
// extend the headers with merge-type and peer
|
|
109
|
+
headers = {
|
|
110
|
+
...headers,
|
|
111
|
+
"Merge-Type": "simpleton",
|
|
112
|
+
Peer: peer,
|
|
113
|
+
}
|
|
126
114
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// server knows where we left off and can send patches from there.
|
|
135
|
-
//
|
|
136
|
-
// IMPORTANT: No changed() / flush is called on reconnect. The
|
|
137
|
-
// subscription simply resumes. Any queued PUTs are retried by
|
|
138
|
-
// braid_fetch independently.
|
|
139
|
-
braid_fetch(url, {
|
|
140
|
-
peer,
|
|
141
|
-
subscribe: true,
|
|
142
|
-
heartbeats: 20,
|
|
143
|
-
signal: ac.signal,
|
|
144
|
-
retry: () => true,
|
|
145
|
-
parents: () => client_version.length ? client_version : null,
|
|
146
|
-
onSubscriptionStatus: status => on_online && on_online(status.online),
|
|
147
|
-
headers: { ...custom_headers,
|
|
148
|
-
"Merge-Type": "simpleton",
|
|
149
|
-
...content_type && {Accept: content_type} },
|
|
150
|
-
}).then(res => {
|
|
151
|
-
if (on_res) on_res(res)
|
|
152
|
-
res.subscribe(async update => {
|
|
115
|
+
// Manages both the GET subscription and PUT requests through a single
|
|
116
|
+
// channel with automatic reconnection and PUT queuing.
|
|
117
|
+
var channel = reliable_update_channel(url, {
|
|
118
|
+
reconnect_from_parents: () => client_version.length ? client_version : null,
|
|
119
|
+
get_headers: { ...headers, ...content_type && {Accept: content_type} },
|
|
120
|
+
put_headers: { ...headers, ...content_type && {"Content-Type": content_type} },
|
|
121
|
+
on_update: async update => {
|
|
153
122
|
// ── Parent check ────────────────────────────────────────
|
|
154
123
|
// Core simpleton invariant: only accept updates whose
|
|
155
|
-
// parents match our
|
|
156
|
-
//
|
|
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.
|
|
157
127
|
update.parents.sort()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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()
|
|
138
|
+
},
|
|
139
|
+
on_error: err => on_error && on_error(err),
|
|
140
|
+
|
|
141
|
+
// this api is preliminary and undocumented;
|
|
142
|
+
// we use it to tell the reliable_update_channel to die,
|
|
143
|
+
// if there is a digest mismatch on the server,
|
|
144
|
+
// which will result in a 550 status code
|
|
145
|
+
no_retry_status_codes: [550]
|
|
146
|
+
})
|
|
166
147
|
|
|
167
148
|
async function apply_update(update) {
|
|
168
149
|
// ── Parse and convert patches ───────────────────────────────
|
|
@@ -185,12 +166,12 @@ function simpleton_client(url, {
|
|
|
185
166
|
// are natively indexed by code points (e.g., Emacs Lisp,
|
|
186
167
|
// Python, Rust's char iterator).
|
|
187
168
|
convert_ranges_codepoints_to_utf16(patches, client_state)
|
|
188
|
-
} else
|
|
169
|
+
} else
|
|
189
170
|
// Initial snapshot: convert body to a patch replacing
|
|
190
171
|
// [0,0] so it follows the same code path as incremental
|
|
191
172
|
// patches.
|
|
192
173
|
patches = [{range: [0, 0], content: update.body_text}]
|
|
193
|
-
|
|
174
|
+
|
|
194
175
|
|
|
195
176
|
// ── Apply the update ────────────────────────────────────────
|
|
196
177
|
if (on_patches) {
|
|
@@ -201,11 +182,11 @@ function simpleton_client(url, {
|
|
|
201
182
|
// changed() after every local edit to avoid this.
|
|
202
183
|
on_patches(patches)
|
|
203
184
|
client_state = get_state()
|
|
204
|
-
} else
|
|
185
|
+
} else
|
|
205
186
|
// Apply patches to our internal state; the
|
|
206
187
|
// result is delivered via on_state below.
|
|
207
188
|
client_state = apply_patches(client_state, patches)
|
|
208
|
-
|
|
189
|
+
|
|
209
190
|
|
|
210
191
|
// ── Advance version ─────────────────────────────────────────
|
|
211
192
|
// IMPORTANT: This must happen synchronously (before any await)
|
|
@@ -220,148 +201,83 @@ function simpleton_client(url, {
|
|
|
220
201
|
// loop handles flushing accumulated edits.
|
|
221
202
|
if (on_state) on_state(client_state)
|
|
222
203
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
// subscription handler. The document is corrupted and
|
|
227
|
-
// continuing would compound the problem.
|
|
228
|
-
//
|
|
229
|
-
// This is placed after advancing client_version and calling
|
|
230
|
-
// on_state so that the await does not create a yield point
|
|
231
|
-
// between applying patches and advancing client_version.
|
|
232
|
-
// That yield point previously allowed the changed() PUT loop
|
|
233
|
-
// to interleave, capturing a stale client_version and causing
|
|
234
|
-
// edit loss.
|
|
235
|
-
if (update.extra_headers &&
|
|
236
|
-
update.extra_headers["repr-digest"] &&
|
|
237
|
-
update.extra_headers["repr-digest"].startsWith('sha-256=') &&
|
|
238
|
-
update.extra_headers["repr-digest"] !== await get_digest(client_state)) {
|
|
239
|
-
console.log('repr-digest mismatch!')
|
|
240
|
-
console.log('repr-digest: ' + update.extra_headers["repr-digest"])
|
|
241
|
-
console.log('state: ' + client_state)
|
|
242
|
-
throw new Error('repr-digest mismatch')
|
|
243
|
-
}
|
|
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)
|
|
244
207
|
}
|
|
245
208
|
|
|
246
|
-
// ──
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// edits (the async accumulation loop), sending them too.
|
|
258
|
-
//
|
|
259
|
-
// The async accumulation loop (while(true) {...}) is equivalent
|
|
260
|
-
// to a callback-driven flush: after each PUT ACK, re-diff and
|
|
261
|
-
// send again if changed. This ensures edits that accumulate
|
|
262
|
-
// during a PUT round-trip are eventually sent.
|
|
263
|
-
changed: () => {
|
|
264
|
-
function get_change() {
|
|
265
|
-
var new_state = get_state()
|
|
266
|
-
if (new_state === client_state) return null
|
|
267
|
-
var patches = get_patches ? get_patches(client_state) :
|
|
268
|
-
[simple_diff(client_state, new_state)]
|
|
269
|
-
return {patches, new_state}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
var change = get_change()
|
|
273
|
-
if (!change) {
|
|
274
|
-
if (throttled) {
|
|
275
|
-
throttled = false
|
|
276
|
-
for (var update of throttled_updates)
|
|
277
|
-
if (versions_eq(client_version, update.parents))
|
|
278
|
-
apply_update(update).catch(on_error)
|
|
279
|
-
throttled_updates = []
|
|
280
|
-
}
|
|
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()
|
|
281
220
|
return
|
|
282
221
|
}
|
|
283
222
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
var {patches, new_state} = change
|
|
223
|
+
var patches = get_patches ? get_patches(client_state) :
|
|
224
|
+
[simple_diff(client_state, new_state)]
|
|
290
225
|
|
|
291
226
|
// Save JS-index patches before code-point conversion mutates them
|
|
292
227
|
var js_patches = patches.map(p => ({range: [...p.range], content: p.content}))
|
|
293
228
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
var r = await braid_fetch(url, {
|
|
331
|
-
method: "PUT",
|
|
332
|
-
peer, version, parents, patches,
|
|
333
|
-
retry: (res) => res.status !== 550,
|
|
334
|
-
headers: {
|
|
335
|
-
...custom_headers,
|
|
336
|
-
"Merge-Type": "simpleton",
|
|
337
|
-
...send_digests && {
|
|
338
|
-
"Repr-Digest": await get_digest(client_state) },
|
|
339
|
-
...content_type && {
|
|
340
|
-
"Content-Type": content_type }
|
|
341
|
-
}
|
|
342
|
-
})
|
|
343
|
-
if (!r.ok) throw new Error(`bad http status: ${r.status}`)
|
|
344
|
-
} catch (e) {
|
|
345
|
-
// A 550 means Repr-Digest check failed — we're out
|
|
346
|
-
// of sync. The client must be torn down and
|
|
347
|
-
// re-created from scratch.
|
|
348
|
-
on_error(e)
|
|
349
|
-
throw e
|
|
350
|
-
}
|
|
351
|
-
throttled = false
|
|
352
|
-
outstanding_changes--
|
|
353
|
-
if (on_ack && !outstanding_changes) on_ack()
|
|
354
|
-
|
|
355
|
-
// ── Check for accumulated edits ─────────────────────
|
|
356
|
-
// While the PUT was in flight, more local edits may
|
|
357
|
-
// have occurred. Diff again and loop if changed.
|
|
358
|
-
var more = get_change()
|
|
359
|
-
if (!more) return
|
|
360
|
-
;({patches, new_state} = more)
|
|
361
|
-
}
|
|
362
|
-
})()
|
|
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 })
|
|
363
264
|
|
|
364
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
|
|
365
281
|
}
|
|
366
282
|
}
|
|
367
283
|
|
|
@@ -495,12 +411,24 @@ function simpleton_client(url, {
|
|
|
495
411
|
return state
|
|
496
412
|
}
|
|
497
413
|
|
|
498
|
-
//
|
|
499
|
-
// Computes SHA-256 of the UTF-8 encoding of the
|
|
500
|
-
//
|
|
501
|
-
// 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>:
|
|
502
417
|
async function get_digest(str) {
|
|
503
418
|
var bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
|
|
504
419
|
return `sha-256=:${btoa(String.fromCharCode(...new Uint8Array(bytes)))}:`
|
|
505
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
|
+
}
|
|
506
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": "
|
|
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 = {
|
|
@@ -959,7 +969,8 @@ function create_braid_text() {
|
|
|
959
969
|
}
|
|
960
970
|
for (var b of dt_bytes) resource.dt.doc.mergeBytes(b)
|
|
961
971
|
resource.version = resource.dt.doc.getRemoteVersion().map(x => x.join("-")).sort()
|
|
962
|
-
if (!resource.dt.known_versions[syn_actor])
|
|
972
|
+
if (!resource.dt.known_versions[syn_actor])
|
|
973
|
+
resource.dt.known_versions[syn_actor] = new RangeSet()
|
|
963
974
|
resource.dt.known_versions[syn_actor].add_range(0, syn_seq - 1)
|
|
964
975
|
await resource.dt.log.save(resource.dt.doc.getPatchSince(yjs_v_before))
|
|
965
976
|
|
|
@@ -977,8 +988,15 @@ function create_braid_text() {
|
|
|
977
988
|
if (!peer || client.peer !== peer)
|
|
978
989
|
await client.send_update(
|
|
979
990
|
client.accept_encoding_dt
|
|
980
|
-
? { version: resource.version,
|
|
981
|
-
|
|
991
|
+
? { version: resource.version,
|
|
992
|
+
parents: version_before_yjs_sync,
|
|
993
|
+
body: resource.dt.doc.getPatchSince(yjs_v_before),
|
|
994
|
+
encoding: 'dt'
|
|
995
|
+
}
|
|
996
|
+
: { version: resource.version,
|
|
997
|
+
parents: version_before_yjs_sync,
|
|
998
|
+
patches: xf
|
|
999
|
+
}
|
|
982
1000
|
)
|
|
983
1001
|
}
|
|
984
1002
|
}
|
|
@@ -1349,7 +1367,9 @@ function create_braid_text() {
|
|
|
1349
1367
|
if (braid_text.db_folder) {
|
|
1350
1368
|
await db_folder_init()
|
|
1351
1369
|
var pages = new Set()
|
|
1352
|
-
for (let x of await require('fs').promises.readdir(braid_text.db_folder))
|
|
1370
|
+
for (let x of await require('fs').promises.readdir(braid_text.db_folder))
|
|
1371
|
+
if (/\.(dt|yjs)\.\d+$/.test(x))
|
|
1372
|
+
pages.add(decode_filename(x.replace(/\.(dt|yjs)\.\d+$/, '')))
|
|
1353
1373
|
return [...pages.keys()]
|
|
1354
1374
|
} else return Object.keys(braid_text.cache)
|
|
1355
1375
|
} catch (e) { return [] }
|