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.
@@ -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
- content_type,
109
- headers: custom_headers, // The user can pass in custom headers
110
- // that are forwared into the fetch
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 outstanding_changes = 0 // PUTs sent, not yet ACKed
122
- var max_outstanding_changes = 10 // throttle limit
123
- var throttled = false
124
- var throttled_updates = []
125
- var ac = new AbortController()
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
- // ── Subscription (GET) ──────────────────────────────────────────────
128
- //
129
- // Opens a long-lived GET subscription with retry: () => true, meaning
130
- // any disconnection (network error, HTTP error) triggers automatic
131
- // reconnection with backoff.
132
- //
133
- // The parents callback sends client_version on reconnect, so the
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 client_version exactly. This ensures
156
- // we stay on a single line of time.
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
- var last_queued = throttled_updates.length
159
- ? throttled_updates[throttled_updates.length - 1].version
160
- : client_version
161
- if (versions_eq(last_queued, update.parents))
162
- if (throttled) throttled_updates.push(update)
163
- else await apply_update(update)
164
- }, on_error)
165
- }).catch(on_error)
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
- // ── Digest verification ─────────────────────────────────────
224
- // If the server sent a repr-digest, verify our state
225
- // matches. On mismatch, THROW — this halts the
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
- // ── Public interface ────────────────────────────────────────────────
247
- return {
248
- // ── abort() cancel the subscription ─────────────────────────
249
- abort: () => ac.abort(),
250
-
251
- // ── changed() — call when local edits occur ───────────────────
252
- // This is the entry point for sending local edits. It:
253
- // 1. Diffs client_state vs current state
254
- // 2. Checks the throttle (outstanding_changes >= max)
255
- // 3. Sends a PUT with the diff
256
- // 4. After the PUT completes, loops to check for MORE accumulated
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
- if (outstanding_changes >= max_outstanding_changes) {
285
- throttled = true
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
- ;(async () => {
295
- while (true) {
296
- // ── JS-SPECIFIC: Convert JS UTF-16 ranges to code-points ──
297
- // The wire protocol uses code-point offsets. See the
298
- // inverse conversion in the receive path above.
299
- //
300
- // OTHER LANGUAGES: Skip this if your strings are
301
- // natively code-point indexed.
302
- convert_ranges_utf16_to_codepoints(patches, client_state)
303
-
304
- for (let patch of patches) {
305
- // ── Update char_counter ─────────────────────────
306
- // Increment by deleted chars + inserted chars
307
- char_counter += patch.range[1] - patch.range[0]
308
- char_counter += count_code_points(patch.content)
309
-
310
- patch.unit = "text"
311
- patch.range = `[${patch.range[0]}:${patch.range[1]}]`
312
- }
313
-
314
- // ── Compute version and advance optimistically ──────
315
- var version = [peer + "-" + char_counter]
316
-
317
- var parents = client_version
318
- client_version = version // optimistic advance
319
- client_state = new_state // update client_state
320
-
321
- // ── Send PUT ────────────────────────────────────────
322
- // Uses braid_fetch with retry: (res) => res.status !== 550
323
- // This means:
324
- // - Network failures: retried with backoff
325
- // - HTTP 401, 403, 408, 429, 500, 502, 503, 504: retried
326
- // - HTTP 550 (Repr-Digest mismatch / out of sync):
327
- // give up, throw — client must be re-created
328
- outstanding_changes++
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
- // ── get_digest ──────────────────────────────────────────────────────
499
- // Computes SHA-256 of the UTF-8 encoding of the state string,
500
- // formatted as the Repr-Digest header value:
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
  }
@@ -1,16 +1,15 @@
1
1
 
2
- function set_acked_state(textarea, binary = true) {
3
- if (!binary) {
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, binary = true) {
13
- if (binary) {
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.6",
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.106"
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' || q === 'demo') {
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
- // Given a version frontier, incorporate a new update (version + parents)
97
- // to compute the new frontier. Walks the DT version DAG if needed.
98
- function extend_frontier(frontier, version, parents) {
99
- var frontier_set = new Set(frontier)
100
- // Fast path: if the frontier contains all the update's parents,
101
- // just swap them out for the new version
102
- if (parents.length &&
103
- parents.every(p => frontier_set.has(p))) {
104
- parents.forEach(p => frontier_set.delete(p))
105
- for (var event of version) frontier_set.add(event)
106
- frontier = [...frontier_set.values()]
107
- } else {
108
- // Slow path: walk the full DT history to compute the frontier
109
- var looking_for = frontier_set
110
- for (var event of version) looking_for.add(event)
111
-
112
- frontier = []
113
- var shadow = new Set()
114
-
115
- var bytes = resource.dt.doc.toBytes()
116
- var [_, events, parentss] = braid_text.dt_parse([...bytes])
117
- for (var i = events.length - 1; i >= 0 && looking_for.size; i--) {
118
- var e = events[i].join('-')
119
- if (looking_for.has(e)) {
120
- looking_for.delete(e)
121
- if (!shadow.has(e)) frontier.push(e)
122
- shadow.add(e)
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
- function extend_fork_point(update) {
132
- resource.meta.fork_point =
133
- extend_frontier(resource.meta.fork_point,
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
- .map(p => p.toLowerCase().startsWith('charset=') ? 'charset=utf-8' : p)
374
- .join('; '))
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
- : has_parents ? 'since-parents'
411
- : (req.subscribe || req.parents || req.headers['accept-transfer-encoding']) ? 'up-to-version'
412
- : false,
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
- : has_parents ? 'since-parents'
692
- : (options.subscribe || options.parents || options.transfer_encoding) ? 'up-to-version'
693
- : false,
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
- getting.history === 'since-parents' ? options.parents : undefined)
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
- getting.history === 'since-parents' ? options.parents : undefined)
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]) resource.dt.known_versions[syn_actor] = new RangeSet()
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, parents: version_before_yjs_sync, body: resource.dt.doc.getPatchSince(yjs_v_before), encoding: 'dt' }
981
- : { version: resource.version, parents: version_before_yjs_sync, patches: xf }
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)) if (/\.(dt|yjs)\.\d+$/.test(x)) pages.add(decode_filename(x.replace(/\.(dt|yjs)\.\d+$/, '')))
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 [] }