braid-text 0.3.7 → 0.3.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 +280 -98
- package/package.json +1 -1
- package/server.js +2 -1
package/client/simpleton-sync.js
CHANGED
|
@@ -1,40 +1,106 @@
|
|
|
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
|
+
//
|
|
1
9
|
// requires braid-http@~1.3/braid-http-client.js
|
|
2
|
-
//
|
|
10
|
+
//
|
|
11
|
+
// --- API ---
|
|
12
|
+
//
|
|
3
13
|
// url: resource endpoint
|
|
4
14
|
//
|
|
5
15
|
// on_patches?: (patches) => void
|
|
6
|
-
// processes incoming patches
|
|
16
|
+
// processes incoming patches by applying them to the UI/textarea.
|
|
17
|
+
// IMPORTANT: Patches have ABSOLUTE positions — each patch's range
|
|
18
|
+
// refers to positions in the original state (before any patches in
|
|
19
|
+
// this update). When applying multiple patches sequentially, you MUST
|
|
20
|
+
// track a cumulative offset to adjust positions:
|
|
21
|
+
//
|
|
22
|
+
// var offset = 0
|
|
23
|
+
// for (var p of patches) {
|
|
24
|
+
// apply_at(p.range[0] + offset, p.range[1] + offset, p.content)
|
|
25
|
+
// offset += p.content.length - (p.range[1] - p.range[0])
|
|
26
|
+
// }
|
|
27
|
+
//
|
|
28
|
+
// Without offset tracking, multi-patch updates will corrupt the state.
|
|
29
|
+
//
|
|
30
|
+
// When provided, simpleton calls this to apply patches externally,
|
|
31
|
+
// then reads back the state via get_state(). This means any un-flushed
|
|
32
|
+
// local edits in the UI are absorbed into client_state after each server
|
|
33
|
+
// update (see "Local Edit Absorption" below).
|
|
7
34
|
//
|
|
8
35
|
// on_state?: (state) => void
|
|
9
|
-
//
|
|
36
|
+
// called after each server update with the new state
|
|
10
37
|
//
|
|
11
|
-
// get_patches?: (
|
|
12
|
-
// returns patches representing diff
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// finding a common prefix and suffix,
|
|
18
|
-
// but you can supply something better,
|
|
19
|
-
// or possibly keep track of patches as they come from your editor)
|
|
38
|
+
// get_patches?: (client_state) => patches
|
|
39
|
+
// returns patches representing diff between client_state and current state,
|
|
40
|
+
// which are guaranteed to be different if this method is being called.
|
|
41
|
+
// (the default does this in a fast/simple way, finding a common prefix
|
|
42
|
+
// and suffix, but you can supply something better, or possibly keep
|
|
43
|
+
// track of patches as they come from your editor)
|
|
20
44
|
//
|
|
21
45
|
// get_state: () => current_state
|
|
22
|
-
// returns the current state
|
|
46
|
+
// returns the current state (e.g., textarea.value)
|
|
23
47
|
//
|
|
24
48
|
// [DEPRECATED] apply_remote_update: ({patches, state}) => {...}
|
|
25
49
|
// this is for incoming changes;
|
|
26
50
|
// one of these will be non-null,
|
|
27
51
|
// and can be applied to the current state.
|
|
28
52
|
//
|
|
29
|
-
// [DEPRECATED] generate_local_diff_update: (
|
|
53
|
+
// [DEPRECATED] generate_local_diff_update: (client_state) => {...}
|
|
30
54
|
// this is to generate outgoing changes,
|
|
31
55
|
// and if there are changes, returns { patches, new_state }
|
|
32
56
|
//
|
|
33
57
|
// content_type: used for Accept and Content-Type headers
|
|
34
58
|
//
|
|
35
|
-
// returns { changed }
|
|
36
|
-
// call changed whenever there is a local change,
|
|
59
|
+
// returns { changed, abort }
|
|
60
|
+
// call changed() whenever there is a local change,
|
|
37
61
|
// and the system will call get_patches when it needs to.
|
|
62
|
+
// call abort() to abort the subscription.
|
|
63
|
+
//
|
|
64
|
+
// --- Retry and Reconnection Behavior ---
|
|
65
|
+
//
|
|
66
|
+
// Simpleton relies on braid_fetch for retry/reconnection:
|
|
67
|
+
//
|
|
68
|
+
// Subscription (GET):
|
|
69
|
+
// retry: () => true — always reconnect on any error (network failure,
|
|
70
|
+
// HTTP error, etc.). Reconnection uses exponential backoff:
|
|
71
|
+
// delay = Math.min(retry_count + 1, 3) * 1000 ms
|
|
72
|
+
// i.e., 1s, 2s, 3s, 3s, 3s, ...
|
|
73
|
+
// On reconnect, sends Parents via the parents callback to resume
|
|
74
|
+
// from where the client left off.
|
|
75
|
+
//
|
|
76
|
+
// PUT requests:
|
|
77
|
+
// retry: (res) => res.status !== 550 — retry all errors EXCEPT
|
|
78
|
+
// HTTP 550 (permanent rejection by the server). This means:
|
|
79
|
+
// - Connection failure: retried with backoff
|
|
80
|
+
// - HTTP 408, 429, 500, 502, 503, 504, etc.: retried
|
|
81
|
+
// - HTTP 550: give up, throw error, outstanding_changes stays
|
|
82
|
+
// incremented (potential throttle leak — see note below)
|
|
83
|
+
// - HTTP 401, 403: retried by braid_fetch, but the !r.ok check
|
|
84
|
+
// throws, which calls on_error and exits the async loop
|
|
85
|
+
//
|
|
86
|
+
// NOTE: When a PUT permanently fails (550 or !r.ok), outstanding_changes
|
|
87
|
+
// is incremented but never decremented. If this happens repeatedly, the
|
|
88
|
+
// client will eventually hit max_outstanding_changes and stop sending.
|
|
89
|
+
// This is arguably a bug in the JS implementation too.
|
|
90
|
+
//
|
|
91
|
+
// --- Local Edit Absorption ---
|
|
92
|
+
//
|
|
93
|
+
// When on_patches is provided, after applying server patches via
|
|
94
|
+
// on_patches(), client_state is set to get_state(). If the UI has
|
|
95
|
+
// un-flushed local edits (typed but changed() not yet called), those
|
|
96
|
+
// edits are silently absorbed into client_state and will never be sent
|
|
97
|
+
// as a diff. In practice, the JS avoids this because changed() is
|
|
98
|
+
// called on every keystroke and the async accumulation loop clears
|
|
99
|
+
// the backlog before a server update arrives.
|
|
100
|
+
//
|
|
101
|
+
// When on_patches is NOT provided (internal mode), client_state is
|
|
102
|
+
// updated by applying patches to the old client_state only — local
|
|
103
|
+
// edits stay in the UI and will be captured by the next changed() diff.
|
|
38
104
|
//
|
|
39
105
|
function simpleton_client(url, {
|
|
40
106
|
on_patches,
|
|
@@ -52,11 +118,11 @@ function simpleton_client(url, {
|
|
|
52
118
|
send_digests
|
|
53
119
|
}) {
|
|
54
120
|
var peer = Math.random().toString(36).substr(2)
|
|
55
|
-
var
|
|
56
|
-
var
|
|
57
|
-
var char_counter = -1
|
|
58
|
-
var outstanding_changes = 0
|
|
59
|
-
var max_outstanding_changes = 10
|
|
121
|
+
var client_version = [] // sorted list of version strings; the version we think is current
|
|
122
|
+
var client_state = "" // text content as of client_version (our "client_state")
|
|
123
|
+
var char_counter = -1 // cumulative char-delta for generating version IDs
|
|
124
|
+
var outstanding_changes = 0 // PUTs sent but not yet ACKed
|
|
125
|
+
var max_outstanding_changes = 10 // throttle limit
|
|
60
126
|
var throttled = false
|
|
61
127
|
var throttled_update = null
|
|
62
128
|
var ac = new AbortController()
|
|
@@ -65,21 +131,36 @@ function simpleton_client(url, {
|
|
|
65
131
|
// and our old code wants to send digests..
|
|
66
132
|
if (apply_remote_update) send_digests = true
|
|
67
133
|
|
|
134
|
+
// ── Subscription (GET) ──────────────────────────────────────────────
|
|
135
|
+
//
|
|
136
|
+
// Opens a long-lived GET subscription with retry: () => true, meaning
|
|
137
|
+
// any disconnection (network error, HTTP error) triggers automatic
|
|
138
|
+
// reconnection with exponential backoff.
|
|
139
|
+
//
|
|
140
|
+
// The parents callback sends client_version on reconnect, so the
|
|
141
|
+
// server knows where we left off and can send patches from there.
|
|
142
|
+
//
|
|
143
|
+
// IMPORTANT: No changed() / flush is called on reconnect. The
|
|
144
|
+
// subscription simply resumes. Any queued PUTs are retried by
|
|
145
|
+
// braid_fetch independently.
|
|
68
146
|
braid_fetch(url, {
|
|
69
147
|
headers: { "Merge-Type": "simpleton",
|
|
70
148
|
...(content_type ? {Accept: content_type} : {}) },
|
|
71
149
|
subscribe: true,
|
|
72
150
|
retry: () => true,
|
|
73
151
|
onSubscriptionStatus: (status) => { if (on_online) on_online(status) },
|
|
74
|
-
parents: () =>
|
|
152
|
+
parents: () => client_version.length ? client_version : null,
|
|
75
153
|
peer,
|
|
76
154
|
signal: ac.signal
|
|
77
155
|
}).then(res => {
|
|
78
156
|
if (on_res) on_res(res)
|
|
79
157
|
res.subscribe(async update => {
|
|
80
|
-
//
|
|
158
|
+
// ── Parent check ────────────────────────────────────────
|
|
159
|
+
// Core simpleton invariant: only accept updates whose
|
|
160
|
+
// parents match our client_version exactly. This ensures
|
|
161
|
+
// we stay on a single line of time.
|
|
81
162
|
update.parents.sort()
|
|
82
|
-
if (v_eq(
|
|
163
|
+
if (v_eq(client_version, update.parents)) {
|
|
83
164
|
if (throttled) throttled_update = update
|
|
84
165
|
else await apply_update(update)
|
|
85
166
|
}
|
|
@@ -87,77 +168,126 @@ function simpleton_client(url, {
|
|
|
87
168
|
}).catch(on_error)
|
|
88
169
|
|
|
89
170
|
async function apply_update(update) {
|
|
90
|
-
|
|
171
|
+
// ── Advance version BEFORE applying patches ─────────
|
|
172
|
+
// (Single-threaded; no concurrent code runs between
|
|
173
|
+
// these steps, so this is safe. Other implementations
|
|
174
|
+
// may advance after applying — both are equivalent.)
|
|
175
|
+
client_version = update.version
|
|
91
176
|
update.state = update.body_text
|
|
92
177
|
|
|
178
|
+
// ── Parse and convert patches ───────────────────────
|
|
93
179
|
if (update.patches) {
|
|
94
|
-
for (let
|
|
95
|
-
|
|
96
|
-
|
|
180
|
+
for (let patch of update.patches) {
|
|
181
|
+
patch.range = patch.range.match(/\d+/g).map((x) => 1 * x)
|
|
182
|
+
patch.content = patch.content_text
|
|
97
183
|
}
|
|
98
184
|
update.patches.sort((a, b) => a.range[0] - b.range[0])
|
|
99
185
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
186
|
+
// ── JS-SPECIFIC: Convert code-points to UTF-16 indices ──
|
|
187
|
+
// The wire protocol uses Unicode code-point offsets.
|
|
188
|
+
// JS strings are UTF-16, so we must convert. Characters
|
|
189
|
+
// outside the BMP (emoji, CJK extensions, etc.) take 2
|
|
190
|
+
// UTF-16 code units (a surrogate pair) but count as 1
|
|
191
|
+
// code point.
|
|
192
|
+
//
|
|
193
|
+
// OTHER LANGUAGES: Skip this conversion if your strings
|
|
194
|
+
// are natively indexed by code points (e.g., Emacs Lisp,
|
|
195
|
+
// Python, Rust's char iterator).
|
|
196
|
+
let codepoint_index = 0
|
|
197
|
+
let utf16_index = 0
|
|
198
|
+
for (let patch of update.patches) {
|
|
199
|
+
while (codepoint_index < patch.range[0]) {
|
|
200
|
+
utf16_index += get_char_size(client_state, utf16_index)
|
|
201
|
+
codepoint_index++
|
|
107
202
|
}
|
|
108
|
-
|
|
203
|
+
patch.range[0] = utf16_index
|
|
109
204
|
|
|
110
|
-
while (
|
|
111
|
-
|
|
112
|
-
|
|
205
|
+
while (codepoint_index < patch.range[1]) {
|
|
206
|
+
utf16_index += get_char_size(client_state, utf16_index)
|
|
207
|
+
codepoint_index++
|
|
113
208
|
}
|
|
114
|
-
|
|
209
|
+
patch.range[1] = utf16_index
|
|
115
210
|
}
|
|
116
211
|
}
|
|
117
212
|
|
|
213
|
+
// ── Apply the update ────────────────────────────────
|
|
118
214
|
if (apply_remote_update) {
|
|
119
|
-
// DEPRECATED
|
|
120
|
-
|
|
215
|
+
// DEPRECATED path
|
|
216
|
+
client_state = apply_remote_update(update)
|
|
121
217
|
} else {
|
|
218
|
+
// Convert initial snapshot body to a patch replacing
|
|
219
|
+
// [0,0] — so initial load follows the same code path
|
|
220
|
+
// as incremental patches.
|
|
122
221
|
var patches = update.patches ||
|
|
123
222
|
[{range: [0, 0], content: update.state}]
|
|
124
223
|
if (on_patches) {
|
|
224
|
+
// EXTERNAL MODE: Apply patches to the UI, then
|
|
225
|
+
// read back the full state. Note: this absorbs
|
|
226
|
+
// any un-flushed local edits into client_state.
|
|
125
227
|
on_patches(patches)
|
|
126
|
-
|
|
127
|
-
} else
|
|
228
|
+
client_state = get_state()
|
|
229
|
+
} else {
|
|
230
|
+
// INTERNAL MODE: Apply patches to our internal
|
|
231
|
+
// state only. Local edits in the UI are NOT
|
|
232
|
+
// absorbed — they will be captured by the next
|
|
233
|
+
// changed() diff.
|
|
234
|
+
client_state = apply_patches(client_state, patches)
|
|
235
|
+
}
|
|
128
236
|
}
|
|
129
237
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
238
|
+
// ── Digest verification ─────────────────────────────
|
|
239
|
+
// If the server sent a repr-digest, verify our state
|
|
240
|
+
// matches. On mismatch, THROW — this halts the
|
|
241
|
+
// subscription handler. The document is corrupted and
|
|
242
|
+
// continuing would compound the problem.
|
|
132
243
|
if (update.extra_headers &&
|
|
133
244
|
update.extra_headers["repr-digest"] &&
|
|
134
245
|
update.extra_headers["repr-digest"].startsWith('sha-256=') &&
|
|
135
|
-
update.extra_headers["repr-digest"] !== await get_digest(
|
|
246
|
+
update.extra_headers["repr-digest"] !== await get_digest(client_state)) {
|
|
136
247
|
console.log('repr-digest mismatch!')
|
|
137
248
|
console.log('repr-digest: ' + update.extra_headers["repr-digest"])
|
|
138
|
-
console.log('state: ' +
|
|
249
|
+
console.log('state: ' + client_state)
|
|
139
250
|
throw new Error('repr-digest mismatch')
|
|
140
251
|
}
|
|
141
252
|
|
|
142
|
-
|
|
253
|
+
// ── Notify listener ─────────────────────────────────
|
|
254
|
+
// IMPORTANT: No changed() / flush is called here.
|
|
255
|
+
// The JS does NOT send edits after receiving a server
|
|
256
|
+
// update. The PUT response handler's async accumulation
|
|
257
|
+
// loop handles flushing accumulated edits.
|
|
258
|
+
if (on_state) on_state(client_state)
|
|
143
259
|
}
|
|
144
260
|
|
|
261
|
+
// ── Public interface ────────────────────────────────────────────────
|
|
145
262
|
return {
|
|
146
|
-
|
|
263
|
+
abort: async () => {
|
|
147
264
|
ac.abort()
|
|
148
265
|
},
|
|
266
|
+
|
|
267
|
+
// ── changed() — call when local edits occur ───────────────────
|
|
268
|
+
// This is the entry point for sending local edits. It:
|
|
269
|
+
// 1. Diffs client_state vs current state
|
|
270
|
+
// 2. Checks the throttle (outstanding_changes >= max)
|
|
271
|
+
// 3. Sends a PUT with the diff
|
|
272
|
+
// 4. After the PUT completes, loops to check for MORE accumulated
|
|
273
|
+
// edits (the async accumulation loop), sending them too.
|
|
274
|
+
//
|
|
275
|
+
// The async accumulation loop (while(true) {...}) is equivalent
|
|
276
|
+
// to a callback-driven flush: after each PUT ACK, re-diff and
|
|
277
|
+
// send again if changed. This ensures edits that accumulate
|
|
278
|
+
// during a PUT round-trip are eventually sent.
|
|
149
279
|
changed: () => {
|
|
150
280
|
function get_change() {
|
|
151
281
|
if (generate_local_diff_update) {
|
|
152
282
|
// DEPRECATED
|
|
153
|
-
var update = generate_local_diff_update(
|
|
283
|
+
var update = generate_local_diff_update(client_state)
|
|
154
284
|
if (!update) return null
|
|
155
285
|
return update
|
|
156
286
|
} else {
|
|
157
287
|
var new_state = get_state()
|
|
158
|
-
if (new_state ===
|
|
159
|
-
var patches = get_patches ? get_patches(
|
|
160
|
-
[simple_diff(
|
|
288
|
+
if (new_state === client_state) return null
|
|
289
|
+
var patches = get_patches ? get_patches(client_state) :
|
|
290
|
+
[simple_diff(client_state, new_state)]
|
|
161
291
|
return {patches, new_state}
|
|
162
292
|
}
|
|
163
293
|
}
|
|
@@ -167,7 +297,7 @@ function simpleton_client(url, {
|
|
|
167
297
|
if (throttled) {
|
|
168
298
|
throttled = false
|
|
169
299
|
if (throttled_update &&
|
|
170
|
-
v_eq(
|
|
300
|
+
v_eq(client_version, throttled_update.parents))
|
|
171
301
|
apply_update(throttled_update).catch(on_error)
|
|
172
302
|
throttled_update = null
|
|
173
303
|
}
|
|
@@ -186,41 +316,56 @@ function simpleton_client(url, {
|
|
|
186
316
|
|
|
187
317
|
;(async () => {
|
|
188
318
|
while (true) {
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
319
|
+
// ── JS-SPECIFIC: Convert JS UTF-16 indices to code-points ──
|
|
320
|
+
// The wire protocol uses code-point offsets. See the
|
|
321
|
+
// inverse conversion in the receive path above.
|
|
322
|
+
//
|
|
323
|
+
// OTHER LANGUAGES: Skip this if your strings are
|
|
324
|
+
// natively code-point indexed.
|
|
325
|
+
let codepoint_index = 0
|
|
326
|
+
let utf16_index = 0
|
|
327
|
+
for (let patch of patches) {
|
|
328
|
+
while (utf16_index < patch.range[0]) {
|
|
329
|
+
utf16_index += get_char_size(client_state, utf16_index)
|
|
330
|
+
codepoint_index++
|
|
196
331
|
}
|
|
197
|
-
|
|
332
|
+
patch.range[0] = codepoint_index
|
|
198
333
|
|
|
199
|
-
while (
|
|
200
|
-
|
|
201
|
-
|
|
334
|
+
while (utf16_index < patch.range[1]) {
|
|
335
|
+
utf16_index += get_char_size(client_state, utf16_index)
|
|
336
|
+
codepoint_index++
|
|
202
337
|
}
|
|
203
|
-
|
|
338
|
+
patch.range[1] = codepoint_index
|
|
204
339
|
|
|
205
|
-
|
|
206
|
-
|
|
340
|
+
// ── Update char_counter ──────────────────────
|
|
341
|
+
// Increment by deleted chars + inserted chars
|
|
342
|
+
char_counter += patch.range[1] - patch.range[0]
|
|
343
|
+
char_counter += count_code_points(patch.content)
|
|
207
344
|
|
|
208
|
-
|
|
209
|
-
|
|
345
|
+
patch.unit = "text"
|
|
346
|
+
patch.range = `[${patch.range[0]}:${patch.range[1]}]`
|
|
210
347
|
}
|
|
211
348
|
|
|
349
|
+
// ── Compute version and advance optimistically ──
|
|
212
350
|
var version = [peer + "-" + char_counter]
|
|
213
351
|
|
|
214
|
-
var parents =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
352
|
+
var parents = client_version
|
|
353
|
+
client_version = version // optimistic advance
|
|
354
|
+
client_state = new_state // update client_state
|
|
355
|
+
|
|
356
|
+
// ── Send PUT ────────────────────────────────────
|
|
357
|
+
// Uses braid_fetch with retry: (res) => res.status !== 550
|
|
358
|
+
// This means:
|
|
359
|
+
// - Network failures: retried with backoff
|
|
360
|
+
// - HTTP 408, 429, 500, 502, 503, 504: retried
|
|
361
|
+
// - HTTP 550 (permanent rejection): give up, throw
|
|
362
|
+
// - Other non-ok: throws via !r.ok check below
|
|
218
363
|
outstanding_changes++
|
|
219
364
|
try {
|
|
220
365
|
var r = await braid_fetch(url, {
|
|
221
366
|
headers: {
|
|
222
367
|
"Merge-Type": "simpleton",
|
|
223
|
-
...send_digests && {"Repr-Digest": await get_digest(
|
|
368
|
+
...send_digests && {"Repr-Digest": await get_digest(client_state)},
|
|
224
369
|
...content_type && {"Content-Type": content_type}
|
|
225
370
|
},
|
|
226
371
|
method: "PUT",
|
|
@@ -230,6 +375,11 @@ function simpleton_client(url, {
|
|
|
230
375
|
})
|
|
231
376
|
if (!r.ok) throw new Error(`bad http status: ${r.status}${(r.status === 401 || r.status === 403) ? ` (access denied)` : ''}`)
|
|
232
377
|
} catch (e) {
|
|
378
|
+
// On error, notify and exit the loop.
|
|
379
|
+
// NOTE: outstanding_changes is NOT decremented here.
|
|
380
|
+
// This is arguably a bug — repeated failures will
|
|
381
|
+
// eventually hit max_outstanding_changes and stop
|
|
382
|
+
// the client from sending any more edits.
|
|
233
383
|
on_error(e)
|
|
234
384
|
throw e
|
|
235
385
|
}
|
|
@@ -238,7 +388,9 @@ function simpleton_client(url, {
|
|
|
238
388
|
|
|
239
389
|
throttled = false
|
|
240
390
|
|
|
241
|
-
// Check for
|
|
391
|
+
// ── Check for accumulated edits ─────────────────
|
|
392
|
+
// While the PUT was in flight, more local edits may
|
|
393
|
+
// have occurred. Diff again and loop if changed.
|
|
242
394
|
var more = get_change()
|
|
243
395
|
if (!more) return
|
|
244
396
|
;({patches, new_state} = more)
|
|
@@ -253,9 +405,18 @@ function simpleton_client(url, {
|
|
|
253
405
|
return a.length === b.length && a.every((v, i) => v === b[i])
|
|
254
406
|
}
|
|
255
407
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
408
|
+
// ── JS-SPECIFIC: UTF-16 helpers ─────────────────────────────────────
|
|
409
|
+
// These handle surrogate pairs in UTF-16 JS strings. Characters
|
|
410
|
+
// outside the Basic Multilingual Plane (BMP) are encoded as two
|
|
411
|
+
// 16-bit code units (a surrogate pair: high 0xD800-0xDBFF, low
|
|
412
|
+
// 0xDC00-0xDFFF). Such a pair represents one Unicode code point.
|
|
413
|
+
//
|
|
414
|
+
// OTHER LANGUAGES: You don't need these if your string type is
|
|
415
|
+
// natively indexed by code points.
|
|
416
|
+
|
|
417
|
+
function get_char_size(str, utf16_index) {
|
|
418
|
+
const char_code = str.charCodeAt(utf16_index)
|
|
419
|
+
return (char_code >= 0xd800 && char_code <= 0xdbff) ? 2 : 1
|
|
259
420
|
}
|
|
260
421
|
|
|
261
422
|
function count_code_points(str) {
|
|
@@ -267,32 +428,53 @@ function simpleton_client(url, {
|
|
|
267
428
|
return code_points
|
|
268
429
|
}
|
|
269
430
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
431
|
+
// ── simple_diff ─────────────────────────────────────────────────────
|
|
432
|
+
// Finds the longest common prefix and suffix between two strings,
|
|
433
|
+
// returning the minimal edit that transforms `old_str` into `new_str`.
|
|
434
|
+
//
|
|
435
|
+
// Returns: { range: [prefix_len, old_str.length - suffix_len],
|
|
436
|
+
// content: new_str.slice(prefix_len, new_str.length - suffix_len) }
|
|
437
|
+
//
|
|
438
|
+
// This produces a single contiguous edit. For multi-cursor or
|
|
439
|
+
// multi-region edits, supply a custom get_patches function instead.
|
|
440
|
+
function simple_diff(old_str, new_str) {
|
|
441
|
+
// Find common prefix length
|
|
442
|
+
var prefix_len = 0
|
|
443
|
+
var min_len = Math.min(old_str.length, new_str.length)
|
|
444
|
+
while (prefix_len < min_len && old_str[prefix_len] === new_str[prefix_len]) prefix_len++
|
|
445
|
+
|
|
446
|
+
// Find common suffix length (from what remains after prefix)
|
|
447
|
+
var suffix_len = 0
|
|
448
|
+
min_len -= prefix_len
|
|
449
|
+
while (suffix_len < min_len && old_str[old_str.length - suffix_len - 1] === new_str[new_str.length - suffix_len - 1]) suffix_len++
|
|
450
|
+
|
|
451
|
+
return {range: [prefix_len, old_str.length - suffix_len], content: new_str.slice(prefix_len, new_str.length - suffix_len)}
|
|
282
452
|
}
|
|
283
453
|
|
|
454
|
+
// ── apply_patches ───────────────────────────────────────────────────
|
|
455
|
+
// Applies patches to a string, tracking cumulative offset.
|
|
456
|
+
// Used in INTERNAL MODE (no on_patches callback) to update
|
|
457
|
+
// client_state without touching the UI.
|
|
458
|
+
//
|
|
459
|
+
// Patches must have absolute coordinates (relative to the original
|
|
460
|
+
// string, not to the string after previous patches). The offset
|
|
461
|
+
// variable tracks the cumulative shift from previous patches.
|
|
284
462
|
function apply_patches(state, patches) {
|
|
285
463
|
var offset = 0
|
|
286
|
-
for (var
|
|
287
|
-
state = state.substring(0,
|
|
288
|
-
state.substring(
|
|
289
|
-
offset +=
|
|
464
|
+
for (var patch of patches) {
|
|
465
|
+
state = state.substring(0, patch.range[0] + offset) + patch.content +
|
|
466
|
+
state.substring(patch.range[1] + offset)
|
|
467
|
+
offset += patch.content.length - (patch.range[1] - patch.range[0])
|
|
290
468
|
}
|
|
291
469
|
return state
|
|
292
470
|
}
|
|
293
471
|
|
|
294
|
-
|
|
295
|
-
|
|
472
|
+
// ── get_digest ──────────────────────────────────────────────────────
|
|
473
|
+
// Computes SHA-256 of the UTF-8 encoding of the state string,
|
|
474
|
+
// formatted as the Repr-Digest header value:
|
|
475
|
+
// sha-256=:<base64-encoded-hash>:
|
|
476
|
+
async function get_digest(str) {
|
|
477
|
+
var bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
|
|
296
478
|
return `sha-256=:${btoa(String.fromCharCode(...new Uint8Array(bytes)))}:`
|
|
297
479
|
}
|
|
298
480
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -2160,7 +2160,8 @@ function create_braid_text() {
|
|
|
2160
2160
|
}
|
|
2161
2161
|
)
|
|
2162
2162
|
}
|
|
2163
|
-
|
|
2163
|
+
var result = relative_to_absolute_patches(patches)
|
|
2164
|
+
return result
|
|
2164
2165
|
}
|
|
2165
2166
|
|
|
2166
2167
|
function relative_to_absolute_patches(patches) {
|