braid-text 0.2.104 → 0.2.105

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.
Files changed (3) hide show
  1. package/index.js +93 -81
  2. package/package.json +1 -1
  3. package/test/tests.js +42 -0
package/index.js CHANGED
@@ -93,45 +93,27 @@ function create_braid_text() {
93
93
  return frontier.sort()
94
94
  }
95
95
 
96
- var closed
97
- var disconnect = () => {}
98
96
  options.signal?.addEventListener('abort', () => {
99
-
100
97
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
101
98
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync abort')
102
-
103
- closed = true
104
- disconnect()
105
99
  })
106
100
 
107
101
  var local_first_put
108
102
  var local_first_put_promise = new Promise(done => local_first_put = done)
109
103
 
110
- var waitTime = 1
111
- function handle_error(_e) {
112
-
104
+ reconnector(options.signal, (_e, count) => {
113
105
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
114
106
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text handle_error: ' + _e)
115
107
 
116
- if (closed) return
117
- disconnect()
118
- var delay = waitTime * 1000
119
- console.log(`disconnected from ${b}, retrying in ${waitTime} second${waitTime > 1 ? 's' : ''}`)
120
- setTimeout(connect, delay)
121
- waitTime = Math.min(waitTime + 1, 3)
122
- }
123
-
124
- connect()
125
- async function connect() {
108
+ var delay = Math.min(count, 3) * 1000
109
+ console.log(`disconnected from ${b}, retrying in ${delay}ms`)
110
+ return delay
111
+ }, async (signal, handle_error) => {
126
112
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
127
113
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before on_pre_connect')
128
114
 
129
115
  if (options.on_pre_connect) await options.on_pre_connect()
130
-
131
- if (closed) return
132
-
133
- var ac = new AbortController()
134
- disconnect = () => ac.abort()
116
+ if (signal.aborted) return
135
117
 
136
118
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
137
119
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before fork-point stuff')
@@ -140,11 +122,13 @@ function create_braid_text() {
140
122
  // fork-point
141
123
  async function check_version(version) {
142
124
  var r = await braid_fetch(b.href, {
143
- signal: ac.signal,
125
+ signal,
144
126
  method: "HEAD",
145
127
  version,
146
128
  headers: get_headers
147
129
  })
130
+ if (signal.aborted) return
131
+
148
132
  if (!r.ok && r.status !== 309 && r.status !== 500)
149
133
  throw new Error(`unexpected HEAD status: ${r.status}`)
150
134
  return r.ok
@@ -160,9 +144,12 @@ function create_braid_text() {
160
144
  // see if remote has the fork point
161
145
  if (resource.meta.fork_point &&
162
146
  !(await check_version(resource.meta.fork_point))) {
147
+ if (signal.aborted) return
148
+
163
149
  resource.meta.fork_point = null
164
150
  resource.change_meta()
165
151
  }
152
+ if (signal.aborted) return
166
153
 
167
154
  // otherwise let's binary search for new fork point..
168
155
  if (!resource.meta.fork_point) {
@@ -180,6 +167,8 @@ function create_braid_text() {
180
167
  var i = Math.floor((min + max)/2)
181
168
  var version = [events[i]]
182
169
  if (await check_version(version)) {
170
+ if (signal.aborted) return
171
+
183
172
  min = i
184
173
  resource.meta.fork_point = version
185
174
  } else max = i
@@ -192,7 +181,7 @@ function create_braid_text() {
192
181
  var max_in_flight = 10
193
182
  var send_pump_lock = 0
194
183
  var temp_acs = new Set()
195
- ac.signal.addEventListener('abort', () => {
184
+ signal.addEventListener('abort', () => {
196
185
  for (var t of temp_acs) t.abort()
197
186
  })
198
187
 
@@ -201,11 +190,12 @@ function create_braid_text() {
201
190
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
202
191
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out')
203
192
 
204
- update.signal = ac.signal
193
+ update.signal = signal
205
194
  update.dont_retry = true
206
195
  if (options.peer) update.peer = options.peer
207
196
  update.headers = put_headers
208
197
  var x = await braid_text.put(b, update)
198
+ if (signal.aborted) return
209
199
 
210
200
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
211
201
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out result: ' + x.ok)
@@ -219,61 +209,52 @@ function create_braid_text() {
219
209
  }
220
210
 
221
211
  async function send_pump() {
212
+ if (signal.aborted) return
222
213
  send_pump_lock++
223
214
  if (send_pump_lock > 1) return
224
- try {
225
- if (closed) return
226
- if (in_flight.size >= max_in_flight) return
227
- if (!q.length) {
228
- // Extend frontier based on in-flight updates
229
- var frontier = resource.meta.fork_point || []
230
- for (var u of in_flight.values())
231
- frontier = extend_frontier(frontier, u.version, u.parents)
232
-
233
- var temp_ac = new AbortController()
234
- temp_acs.add(temp_ac)
235
- var temp_ops = {
236
- signal: temp_ac.signal,
237
- parents: frontier,
238
- merge_type: 'dt',
239
- peer: options.peer,
240
- subscribe: u => u.version?.length && q.push(u)
241
- }
242
- await braid_text.get(a, temp_ops)
243
- temp_ac.abort()
244
- temp_acs.delete(temp_ac)
245
- }
246
- while (q.length && in_flight.size < max_in_flight) {
247
- let u = q.shift()
248
- if (!u.version?.length) continue
249
- in_flight.set(u.version[0], u);
250
- (async () => {
251
- try {
252
- if (closed) return
253
- await send_out(u)
254
- if (closed) return
255
- in_flight.delete(u.version[0])
256
- setTimeout(send_pump, 0)
257
- } catch (e) {
258
- if (e.name === 'AbortError') {
259
- // ignore
260
- } else handle_error(e)
261
- }
262
- })()
263
- }
264
- } finally {
265
- var retry = send_pump_lock > 1
266
- send_pump_lock = 0
267
- if (retry) setTimeout(send_pump, 0)
215
+ if (in_flight.size >= max_in_flight) return
216
+ if (!q.length) {
217
+ // Extend frontier based on in-flight updates
218
+ var frontier = resource.meta.fork_point || []
219
+ for (var u of in_flight.values())
220
+ frontier = extend_frontier(frontier, u.version, u.parents)
221
+
222
+ var temp_ac = new AbortController()
223
+ temp_acs.add(temp_ac)
224
+ await braid_text.get(a, {
225
+ signal: temp_ac.signal,
226
+ parents: frontier,
227
+ merge_type: 'dt',
228
+ peer: options.peer,
229
+ subscribe: u => u.version?.length && q.push(u)
230
+ })
231
+ if (signal.aborted) return
232
+ temp_ac.abort()
233
+ temp_acs.delete(temp_ac)
268
234
  }
235
+ while (q.length && in_flight.size < max_in_flight) {
236
+ let u = q.shift()
237
+ if (!u.version?.length) continue
238
+ in_flight.set(u.version[0], u)
239
+ void (async () => {
240
+ try {
241
+ await send_out(u)
242
+ if (signal.aborted) return
243
+ in_flight.delete(u.version[0])
244
+ setTimeout(send_pump, 0)
245
+ } catch (e) { handle_error(e) }
246
+ })()
247
+ }
248
+ if (send_pump_lock > 1) setTimeout(send_pump, 0)
249
+ send_pump_lock = 0
269
250
  }
270
251
 
271
252
  var a_ops = {
272
- signal: ac.signal,
253
+ signal,
273
254
  merge_type: 'dt',
274
255
  peer: options.peer,
275
256
  subscribe: update => {
276
- if (closed) return
257
+ if (signal.aborted) return
277
258
  if (update.version?.length) {
278
259
  q.push(update)
279
260
  send_pump()
@@ -293,7 +274,7 @@ function create_braid_text() {
293
274
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text before GET/sub')
294
275
 
295
276
  var b_ops = {
296
- signal: ac.signal,
277
+ signal,
297
278
  dont_retry: true,
298
279
  headers: { ...get_headers, 'Merge-Type': 'dt', 'accept-encoding': 'updates(dt)' },
299
280
  parents: resource.meta.fork_point,
@@ -312,6 +293,7 @@ function create_braid_text() {
312
293
 
313
294
  // Wait for remote_res to be available
314
295
  await remote_res_promise
296
+ if (signal.aborted) return
315
297
 
316
298
  // Check if this is a dt-encoded update
317
299
  if (update.extra_headers?.encoding === 'dt') {
@@ -320,9 +302,11 @@ function create_braid_text() {
320
302
  body: update.body,
321
303
  transfer_encoding: 'dt'
322
304
  })
305
+ if (signal.aborted) return
323
306
  if (cv) extend_fork_point({ version: JSON.parse(`[${cv}]`), parents: resource.meta.fork_point || [] })
324
307
  } else {
325
308
  await braid_text.put(a, update)
309
+ if (signal.aborted) return
326
310
  if (update.version) extend_fork_point(update)
327
311
  }
328
312
  },
@@ -333,6 +317,7 @@ function create_braid_text() {
333
317
  }
334
318
  // Handle case where remote doesn't exist yet - wait for local to create it
335
319
  remote_res = await braid_text.get(b, b_ops)
320
+ if (signal.aborted) return
336
321
 
337
322
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
338
323
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text after GET/sub: ' + remote_res?.status)
@@ -341,16 +326,12 @@ function create_braid_text() {
341
326
  if (remote_res === null) {
342
327
  // Remote doesn't exist yet, wait for local to put something
343
328
  await local_first_put_promise
344
- disconnect()
345
- connect()
346
- return
329
+ return handle_error(new Error('try again'))
347
330
  }
348
331
  options.on_res?.(remote_res)
349
332
  // on_error will call handle_error when connection drops
350
- } catch (e) {
351
- handle_error(e)
352
- }
353
- }
333
+ } catch (e) { handle_error(e) }
334
+ })
354
335
  }
355
336
 
356
337
  braid_text.serve = async (req, res, options = {}) => {
@@ -2823,6 +2804,37 @@ function create_braid_text() {
2823
2804
  return within_fiber.chains[id] = curr
2824
2805
  }
2825
2806
 
2807
+ // Calls func(inner_signal, reconnect) immediately and handles reconnection.
2808
+ // - inner_signal: AbortSignal that aborts when reconnect() is called or outter_signal aborts
2809
+ // - reconnect(error): call this to trigger a reconnection after get_delay(error, count) ms
2810
+ // - Multiple/rapid reconnect() calls are safe - only one reconnection will be scheduled
2811
+ // - If outter_signal aborts, no further calls to func will occur
2812
+ function reconnector(outter_signal, get_delay, func) {
2813
+ if (outter_signal?.aborted) return
2814
+
2815
+ var current_inner_ac = null
2816
+ outter_signal?.addEventListener('abort', () =>
2817
+ current_inner_ac?.abort())
2818
+
2819
+ var reconnect_count = 0
2820
+ connect()
2821
+ function connect() {
2822
+ if (outter_signal?.aborted) return
2823
+
2824
+ var ac = current_inner_ac = new AbortController()
2825
+ var inner_signal = ac.signal
2826
+
2827
+ func(inner_signal, (e) => {
2828
+ if (outter_signal?.aborted ||
2829
+ inner_signal.aborted) return
2830
+
2831
+ ac.abort()
2832
+ var delay = get_delay(e, ++reconnect_count)
2833
+ setTimeout(connect, delay)
2834
+ })
2835
+ }
2836
+ }
2837
+
2826
2838
  braid_text.get_resource = get_resource
2827
2839
 
2828
2840
  braid_text.db_folder_init = db_folder_init
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.2.104",
3
+ "version": "0.2.105",
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/test/tests.js CHANGED
@@ -2849,6 +2849,48 @@ runTest(
2849
2849
  'correctly threw error'
2850
2850
  )
2851
2851
 
2852
+ // Tests for reconnector/sync edge cases
2853
+
2854
+ runTest(
2855
+ "test braid_text.sync remote null triggers local_first_put_promise path",
2856
+ async () => {
2857
+ var local_key = 'test-local-' + Math.random().toString(36).slice(2)
2858
+
2859
+ // Use the /404 endpoint which always returns 404 (null from braid_text.get)
2860
+ var r = await braid_fetch(`/eval`, {
2861
+ method: 'PUT',
2862
+ body: `void (async () => {
2863
+ var ac = new AbortController()
2864
+ var reconnect_count = 0
2865
+
2866
+ braid_text.sync('/${local_key}', new URL('http://localhost:8889/404'), {
2867
+ signal: ac.signal,
2868
+ on_pre_connect: () => {
2869
+ reconnect_count++
2870
+ if (reconnect_count >= 2) {
2871
+ ac.abort()
2872
+ res.end('reconnected after local put')
2873
+ }
2874
+ }
2875
+ })
2876
+
2877
+ // Wait a bit then put something locally - this should trigger
2878
+ // the local_first_put_promise to resolve and cause reconnect
2879
+ await new Promise(done => setTimeout(done, 100))
2880
+ await braid_text.put('/${local_key}', { body: 'local data' })
2881
+
2882
+ // Wait for reconnect
2883
+ await new Promise(done => setTimeout(done, 2000))
2884
+ res.end('did not reconnect')
2885
+ })()`
2886
+ })
2887
+ if (!r.ok) return 'eval failed: ' + r.status
2888
+
2889
+ return await r.text()
2890
+ },
2891
+ 'reconnected after local put'
2892
+ )
2893
+
2852
2894
  }
2853
2895
 
2854
2896
  // Export for both Node.js and browser environments