braid-text 0.5.8 → 0.5.9

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/server-demo.js CHANGED
@@ -20,9 +20,17 @@ var server = require("http").createServer(async (req, res) => {
20
20
  return
21
21
  }
22
22
 
23
- if (req.url === '/simpleton-sync.js' || req.url === '/web-utils.js'
24
- || req.url === '/textarea-highlights.js' || req.url === '/cursor-sync.js'
25
- || req.url === '/yjs-sync.js') {
23
+ var libs = new Set([
24
+ 'simpleton.js',
25
+ 'text-client.js',
26
+ 'yjs-sync.js',
27
+ 'cursor-sync.js',
28
+ 'textarea-highlights.js',
29
+ 'myers-diff.js',
30
+ 'syncarea.js'
31
+ ])
32
+
33
+ if (libs.has(req.url.substr(1))) {
26
34
  res.writeHead(200, { "Content-Type": "text/javascript", "Cache-Control": "no-cache" })
27
35
  require("fs").createReadStream("./client" + req.url).pipe(res)
28
36
  return
@@ -1,434 +0,0 @@
1
- // Simpleton Javascript Client
2
- //
3
- // requires braid-http@~1.3/braid-http-client.js
4
-
5
- // --- API ---
6
- //
7
- // on_patches?: (patches) => void
8
- // processes incoming patches by applying them to the UI/textarea.
9
- // Patches are guaranteed to be in-order and non-overlapping.
10
- //
11
- // IMPORTANT: Patches have ABSOLUTE positions — each patch's range
12
- // refers to positions in the original state (before any patches in
13
- // this update). When applying multiple patches sequentially, you MUST
14
- // track a cumulative offset to adjust positions:
15
- //
16
- // var offset = 0
17
- // for (var p of patches) {
18
- // apply_at(p.range[0] + offset, p.range[1] + offset, p.content)
19
- // offset += p.content.length - (p.range[1] - p.range[0])
20
- // }
21
- //
22
- // Without offset tracking, multi-patch updates will corrupt the state.
23
- //
24
- // Optional. If not provided, simpleton applies patches internally
25
- // to its own copy of the state, and you can get the new state via
26
- // on_state (see "Local Edit Absorption" below).
27
- //
28
- // When provided, simpleton calls this to apply patches externally,
29
- // then reads back the state via get_state(). This means any un-flushed
30
- // local edits in the UI are absorbed into client_state after each server
31
- // update (see "Local Edit Absorption" below).
32
- //
33
- // on_state?: (state) => void
34
- // called after each server update with the new state
35
- //
36
- // get_patches?: (client_state) => patches
37
- // returns patches representing diff between client_state and current state,
38
- // which are guaranteed to be different if this method is being called.
39
- // (the default does this in a fast/simple way, finding a common prefix
40
- // and suffix, but you can supply something better, or possibly keep
41
- // track of patches as they come from your editor)
42
- //
43
- // get_state: () => current_state
44
- // returns the current state (e.g., textarea.value)
45
- //
46
- // content_type: used for Accept and Content-Type headers
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
- //
60
- // returns { changed, abort }
61
- // call changed() whenever there is a local change,
62
- // and the system will call a combination of get_state and
63
- // get_patches when it needs to. (get_state is required;
64
- // get_patches is optional.)
65
- // call abort() to abort the subscription.
66
- //
67
- //
68
- // --- Local Edit Absorption ---
69
- //
70
- // When on_patches is provided, after applying server patches via
71
- // on_patches(), client_state is set to get_state(). If the UI has
72
- // un-flushed local edits (typed but changed() not yet called), those
73
- // edits are silently absorbed into client_state and will never be sent
74
- // as a diff. In practice, the JS avoids this because changed() is
75
- // called on every keystroke, and additionally, each time a PUT
76
- // completes, the code re-diffs and sends any edits that accumulated
77
- // while the PUT was in flight — so local edits are never stuck
78
- // waiting; they flush as soon as a PUT slot opens up.
79
- //
80
- // When on_patches is NOT provided, client_state is updated by applying
81
- // patches to the old client_state only. In this case, you should
82
- // provide on_state to receive the updated state after each server
83
- // update; otherwise your UI will not reflect remote changes.
84
- //
85
- function simpleton_client(url, {
86
- get_patches,
87
- get_state,
88
-
89
- on_patches,
90
- on_state,
91
- on_error,
92
- on_online,
93
- on_ack,
94
-
95
- headers, // The user can pass in custom headers
96
- // that are forwarded into fetches
97
- content_type,
98
- send_digests
99
- }) {
100
- var peer = Math.random().toString(36).slice(2)
101
- var client_version = [] // sorted version strings
102
- var client_state = "" // text as of client_version
103
- var char_counter = -1 // char-delta for version IDs
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
- }
114
-
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 => {
122
- // ── Parent check ────────────────────────────────────────
123
- // Core simpleton invariant: only accept updates whose
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.
127
- update.parents.sort()
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
- })
147
-
148
- async function apply_update(update) {
149
- // ── Parse and convert patches ───────────────────────────────
150
- // braid_fetch provides body and patch content as bytes;
151
- // body_text and content_text are dynamic properties that
152
- // decode bytes to a string via a UTF-8 decoder.
153
- var patches
154
- if (update.patches) {
155
- for (let patch of update.patches) {
156
- patch.range = patch.range.match(/\d+/g).map((x) => 1 * x)
157
- patch.content = patch.content_text
158
- }
159
- patches = update.patches.sort((a, b) => a.range[0] - b.range[0])
160
-
161
- // ── JS-SPECIFIC: Convert code-point ranges to UTF-16 indices ──
162
- // The wire protocol uses Unicode code-point offsets.
163
- // JS strings are UTF-16, so we must convert.
164
- //
165
- // OTHER LANGUAGES: Skip this conversion if your strings
166
- // are natively indexed by code points (e.g., Emacs Lisp,
167
- // Python, Rust's char iterator).
168
- convert_ranges_codepoints_to_utf16(patches, client_state)
169
- } else
170
- // Initial snapshot: convert body to a patch replacing
171
- // [0,0] so it follows the same code path as incremental
172
- // patches.
173
- patches = [{range: [0, 0], content: update.body_text}]
174
-
175
-
176
- // ── Apply the update ────────────────────────────────────────
177
- if (on_patches) {
178
- // Apply patches to the UI, then read back the
179
- // full state. Warning: if changed() hasn't been
180
- // called for recent local edits, get_state() will
181
- // absorb them into client_state silently — call
182
- // changed() after every local edit to avoid this.
183
- on_patches(patches)
184
- client_state = get_state()
185
- } else
186
- // Apply patches to our internal state; the
187
- // result is delivered via on_state below.
188
- client_state = apply_patches(client_state, patches)
189
-
190
-
191
- // ── Advance version ─────────────────────────────────────────
192
- // IMPORTANT: This must happen synchronously (before any await)
193
- // to prevent the changed() accumulation loop from interleaving
194
- // and capturing a stale client_version during a yield point.
195
- client_version = update.version
196
-
197
- // ── Notify listener ─────────────────────────────────────────
198
- // IMPORTANT: No changed() / flush is called here.
199
- // The JS does NOT send edits after receiving a server
200
- // update. The PUT response handler's async accumulation
201
- // loop handles flushing accumulated edits.
202
- if (on_state) on_state(client_state)
203
-
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)
207
- }
208
-
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()
220
- return
221
- }
222
-
223
- var patches = get_patches ? get_patches(client_state) :
224
- [simple_diff(client_state, new_state)]
225
-
226
- // Save JS-index patches before code-point conversion mutates them
227
- var js_patches = patches.map(p => ({range: [...p.range], content: p.content}))
228
-
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 })
264
-
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
281
- }
282
- }
283
-
284
- function versions_eq(a, b) {
285
- return a.length === b.length && a.every((v, i) => v === b[i])
286
- }
287
-
288
- // ── JS-SPECIFIC: UTF-16 helpers ─────────────────────────────────────
289
- // These handle surrogate pairs in UTF-16 JS strings. Characters
290
- // outside the Basic Multilingual Plane (BMP) are encoded as two
291
- // 16-bit code units (a surrogate pair: high 0xD800-0xDBFF, low
292
- // 0xDC00-0xDFFF). Such a pair represents one Unicode code point.
293
- //
294
- // OTHER LANGUAGES: You don't need these if your string type is
295
- // natively indexed by code points.
296
-
297
- function get_char_size(str, utf16_index) {
298
- const char_code = str.charCodeAt(utf16_index)
299
- return (char_code >= 0xd800 && char_code <= 0xdbff) ? 2 : 1
300
- }
301
-
302
- function count_code_points(str) {
303
- let code_points = 0
304
- for (let i = 0; i < str.length; i++) {
305
- if (str.charCodeAt(i) >= 0xd800 && str.charCodeAt(i) <= 0xdbff) i++
306
- code_points++
307
- }
308
- return code_points
309
- }
310
-
311
- // Converts patch ranges from code-point offsets to UTF-16 indices.
312
- // Patches must be sorted by range[0].
313
- function convert_ranges_codepoints_to_utf16(patches, str) {
314
- let codepoint_index = 0
315
- let utf16_index = 0
316
- for (let patch of patches) {
317
- while (codepoint_index < patch.range[0]) {
318
- utf16_index += get_char_size(str, utf16_index)
319
- codepoint_index++
320
- }
321
- patch.range[0] = utf16_index
322
-
323
- while (codepoint_index < patch.range[1]) {
324
- utf16_index += get_char_size(str, utf16_index)
325
- codepoint_index++
326
- }
327
- patch.range[1] = utf16_index
328
- }
329
- }
330
-
331
- // Converts patch ranges from UTF-16 indices to code-point offsets.
332
- // Patches must be sorted by range[0].
333
- function convert_ranges_utf16_to_codepoints(patches, str) {
334
- let codepoint_index = 0
335
- let utf16_index = 0
336
- for (let patch of patches) {
337
- while (utf16_index < patch.range[0]) {
338
- utf16_index += get_char_size(str, utf16_index)
339
- codepoint_index++
340
- }
341
- patch.range[0] = codepoint_index
342
-
343
- while (utf16_index < patch.range[1]) {
344
- utf16_index += get_char_size(str, utf16_index)
345
- codepoint_index++
346
- }
347
- patch.range[1] = codepoint_index
348
- }
349
- }
350
-
351
- // ── simple_diff ─────────────────────────────────────────────────────
352
- // Finds the longest common prefix and suffix between two strings,
353
- // returning the minimal edit that transforms `old_str` into `new_str`.
354
- //
355
- // Returns: { range: [prefix_len, old_str.length - suffix_len],
356
- // content: new_str.slice(prefix_len, new_str.length - suffix_len) }
357
- //
358
- // This produces a single contiguous edit. For multi-cursor or
359
- // multi-region edits, supply a custom get_patches function instead.
360
- function simple_diff(old_str, new_str) {
361
- // Find common prefix length
362
- var prefix_len = 0
363
- var min_len = Math.min(old_str.length, new_str.length)
364
- while (prefix_len < min_len && old_str[prefix_len] === new_str[prefix_len]) prefix_len++
365
-
366
- // Don't split a surrogate pair: if prefix ends on a low surrogate,
367
- // the preceding high surrogate only matched by coincidence (same
368
- // Unicode block), so back up to include the whole pair in the diff.
369
- if (prefix_len > 0 && is_low_surrogate(old_str, prefix_len))
370
- prefix_len--
371
-
372
- // Find common suffix length (from what remains after prefix)
373
- var suffix_len = 0
374
- min_len -= prefix_len
375
- while (suffix_len < min_len && old_str[old_str.length - suffix_len - 1] === new_str[new_str.length - suffix_len - 1]) suffix_len++
376
-
377
- // Same guard for suffixes: if the range end (old_str.length - suffix_len)
378
- // lands on a low surrogate, the suffix consumed it without its high
379
- // surrogate, so back up.
380
- if (suffix_len > 0 && is_low_surrogate(old_str, old_str.length - suffix_len))
381
- suffix_len--
382
-
383
- return {range: [prefix_len, old_str.length - suffix_len], content: new_str.slice(prefix_len, new_str.length - suffix_len)}
384
- }
385
-
386
- function is_low_surrogate(str, i) {
387
- var c = str.charCodeAt(i)
388
- return c >= 0xdc00 && c <= 0xdfff
389
- }
390
-
391
- function is_high_surrogate(str, i) {
392
- var c = str.charCodeAt(i)
393
- return c >= 0xd800 && c <= 0xdbff
394
- }
395
-
396
- // ── apply_patches ───────────────────────────────────────────────────
397
- // Applies patches to a string, tracking cumulative offset.
398
- // Used when on_patches is not provided, to update
399
- // client_state directly.
400
- //
401
- // Patches must have absolute coordinates (relative to the original
402
- // string, not to the string after previous patches). The offset
403
- // variable tracks the cumulative shift from previous patches.
404
- function apply_patches(state, patches) {
405
- var offset = 0
406
- for (var patch of patches) {
407
- state = state.substring(0, patch.range[0] + offset) + patch.content +
408
- state.substring(patch.range[1] + offset)
409
- offset += patch.content.length - (patch.range[1] - patch.range[0])
410
- }
411
- return state
412
- }
413
-
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>:
417
- async function get_digest(str) {
418
- var bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
419
- return `sha-256=:${btoa(String.fromCharCode(...new Uint8Array(bytes)))}:`
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
- }
434
- }
@@ -1,76 +0,0 @@
1
-
2
- function set_acked_state(textarea, on = true) {
3
- if (on)
4
- textarea.style.caretColor = textarea.old_caretColor
5
- else {
6
- textarea.old_caretColor = textarea.style.caretColor
7
- textarea.style.caretColor = 'red'
8
- }
9
- }
10
-
11
- function set_error_state(textarea, on = true) {
12
- if (on) {
13
- textarea.old_disabled = textarea.disabled
14
- textarea.old_background = textarea.style.background
15
- textarea.old_border = textarea.style.border
16
-
17
- textarea.disabled = true
18
- textarea.style.background = '#fee'
19
- textarea.style.border = '4px solid red'
20
- } else {
21
- textarea.disabled = textarea.old_disabled
22
- textarea.style.background = textarea.old_background
23
- textarea.style.border = textarea.old_border
24
- }
25
- }
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.
29
- function diff(before, after) {
30
- let diff = diff_main(before, after)
31
- let patches = []
32
- let offset = 0
33
- for (let d of diff) {
34
- let p = null
35
- if (d[0] === 1) p = { range: [offset, offset], content: d[1] }
36
- else if (d[0] === -1) {
37
- p = { range: [offset, offset + d[1].length], content: "" }
38
- offset += d[1].length
39
- } else offset += d[1].length
40
- if (p) {
41
- p.unit = "text"
42
- patches.push(p)
43
- }
44
- }
45
- return patches
46
- }
47
-
48
- function apply_patches_and_update_selection(textarea, patches) {
49
- let offset = 0
50
- for (let p of patches) {
51
- p.range[0] += offset
52
- p.range[1] += offset
53
- offset -= p.range[1] - p.range[0]
54
- offset += p.content.length
55
- }
56
-
57
- let original = textarea.value
58
- let sel = [textarea.selectionStart, textarea.selectionEnd]
59
-
60
- for (var p of patches) {
61
- let range = p.range
62
-
63
- for (let i = 0; i < sel.length; i++)
64
- if (sel[i] > range[0])
65
- if (sel[i] > range[1]) sel[i] -= range[1] - range[0]
66
- else sel[i] = range[0]
67
-
68
- for (let i = 0; i < sel.length; i++) if (sel[i] > range[0]) sel[i] += p.content.length
69
-
70
- original = original.substring(0, range[0]) + p.content + original.substring(range[1])
71
- }
72
-
73
- textarea.value = original
74
- textarea.selectionStart = sel[0]
75
- textarea.selectionEnd = sel[1]
76
- }