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.
@@ -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
- // processes incoming state
36
+ // called after each server update with the new state
10
37
  //
11
- // get_patches?: (prev_state) => patches
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)
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: (prev_state) => {...}
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 current_version = []
56
- var prev_state = ""
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: () => current_version.length ? current_version : null,
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
- // Only accept the update if its parents == our current version
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(current_version, update.parents)) {
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
- current_version = update.version
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 p of update.patches) {
95
- p.range = p.range.match(/\d+/g).map((x) => 1 * x)
96
- p.content = p.content_text
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
- // convert from code-points to js-indicies
101
- let c = 0
102
- let i = 0
103
- for (let p of update.patches) {
104
- while (c < p.range[0]) {
105
- i += get_char_size(prev_state, i)
106
- c++
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
- p.range[0] = i
203
+ patch.range[0] = utf16_index
109
204
 
110
- while (c < p.range[1]) {
111
- i += get_char_size(prev_state, i)
112
- c++
205
+ while (codepoint_index < patch.range[1]) {
206
+ utf16_index += get_char_size(client_state, utf16_index)
207
+ codepoint_index++
113
208
  }
114
- p.range[1] = i
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
- prev_state = apply_remote_update(update)
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
- prev_state = get_state()
127
- } else prev_state = apply_patches(prev_state, patches)
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
- // if the server gave us a digest,
131
- // go ahead and check it against our new state..
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(prev_state)) {
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: ' + prev_state)
249
+ console.log('state: ' + client_state)
139
250
  throw new Error('repr-digest mismatch')
140
251
  }
141
252
 
142
- if (on_state) on_state(prev_state)
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
- stop: async () => {
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(prev_state)
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 === prev_state) return null
159
- var patches = get_patches ? get_patches(prev_state) :
160
- [simple_diff(prev_state, new_state)]
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(current_version, throttled_update.parents))
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
- // convert from js-indicies to code-points
190
- let c = 0
191
- let i = 0
192
- for (let p of patches) {
193
- while (i < p.range[0]) {
194
- i += get_char_size(prev_state, i)
195
- c++
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
- p.range[0] = c
332
+ patch.range[0] = codepoint_index
198
333
 
199
- while (i < p.range[1]) {
200
- i += get_char_size(prev_state, i)
201
- c++
334
+ while (utf16_index < patch.range[1]) {
335
+ utf16_index += get_char_size(client_state, utf16_index)
336
+ codepoint_index++
202
337
  }
203
- p.range[1] = c
338
+ patch.range[1] = codepoint_index
204
339
 
205
- char_counter += p.range[1] - p.range[0]
206
- char_counter += count_code_points(p.content)
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
- p.unit = "text"
209
- p.range = `[${p.range[0]}:${p.range[1]}]`
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 = current_version
215
- current_version = version
216
- prev_state = new_state
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(prev_state)},
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 more changes that accumulated while we were sending
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
- function get_char_size(s, i) {
257
- const charCode = s.charCodeAt(i)
258
- return (charCode >= 0xd800 && charCode <= 0xdbff) ? 2 : 1
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
- function simple_diff(a, b) {
271
- // Find common prefix
272
- var p = 0
273
- var len = Math.min(a.length, b.length)
274
- while (p < len && a[p] === b[p]) p++
275
-
276
- // Find common suffix (from what remains after prefix)
277
- var s = 0
278
- len -= p
279
- while (s < len && a[a.length - s - 1] === b[b.length - s - 1]) s++
280
-
281
- return {range: [p, a.length - s], content: b.slice(p, b.length - s)}
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 p of patches) {
287
- state = state.substring(0, p.range[0] + offset) + p.content +
288
- state.substring(p.range[1] + offset)
289
- offset += p.content.length - (p.range[1] - p.range[0])
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
- async function get_digest(s) {
295
- var bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.3.7",
3
+ "version": "0.3.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",
package/server.js CHANGED
@@ -2160,7 +2160,8 @@ function create_braid_text() {
2160
2160
  }
2161
2161
  )
2162
2162
  }
2163
- return relative_to_absolute_patches(patches)
2163
+ var result = relative_to_absolute_patches(patches)
2164
+ return result
2164
2165
  }
2165
2166
 
2166
2167
  function relative_to_absolute_patches(patches) {