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