braid-text 0.2.104 → 0.2.106

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 +94 -80
  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,54 @@ 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
- })()
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
+ var get_options = {
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)
263
230
  }
264
- } finally {
265
- var retry = send_pump_lock > 1
266
- send_pump_lock = 0
267
- if (retry) setTimeout(send_pump, 0)
231
+ await braid_text.get(a, get_options)
232
+ await get_options.my_subscribe_chain
233
+ if (signal.aborted) return
234
+ temp_ac.abort()
235
+ temp_acs.delete(temp_ac)
236
+ }
237
+ while (q.length && in_flight.size < max_in_flight) {
238
+ let u = q.shift()
239
+ if (!u.version?.length) continue
240
+ in_flight.set(u.version[0], u)
241
+ void (async () => {
242
+ try {
243
+ await send_out(u)
244
+ if (signal.aborted) return
245
+ in_flight.delete(u.version[0])
246
+ setTimeout(send_pump, 0)
247
+ } catch (e) { handle_error(e) }
248
+ })()
268
249
  }
250
+ if (send_pump_lock > 1) setTimeout(send_pump, 0)
251
+ send_pump_lock = 0
269
252
  }
270
253
 
271
254
  var a_ops = {
272
- signal: ac.signal,
255
+ signal,
273
256
  merge_type: 'dt',
274
257
  peer: options.peer,
275
258
  subscribe: update => {
276
- if (closed) return
259
+ if (signal.aborted) return
277
260
  if (update.version?.length) {
278
261
  q.push(update)
279
262
  send_pump()
@@ -293,7 +276,7 @@ function create_braid_text() {
293
276
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text before GET/sub')
294
277
 
295
278
  var b_ops = {
296
- signal: ac.signal,
279
+ signal,
297
280
  dont_retry: true,
298
281
  headers: { ...get_headers, 'Merge-Type': 'dt', 'accept-encoding': 'updates(dt)' },
299
282
  parents: resource.meta.fork_point,
@@ -312,6 +295,7 @@ function create_braid_text() {
312
295
 
313
296
  // Wait for remote_res to be available
314
297
  await remote_res_promise
298
+ if (signal.aborted) return
315
299
 
316
300
  // Check if this is a dt-encoded update
317
301
  if (update.extra_headers?.encoding === 'dt') {
@@ -320,9 +304,11 @@ function create_braid_text() {
320
304
  body: update.body,
321
305
  transfer_encoding: 'dt'
322
306
  })
307
+ if (signal.aborted) return
323
308
  if (cv) extend_fork_point({ version: JSON.parse(`[${cv}]`), parents: resource.meta.fork_point || [] })
324
309
  } else {
325
310
  await braid_text.put(a, update)
311
+ if (signal.aborted) return
326
312
  if (update.version) extend_fork_point(update)
327
313
  }
328
314
  },
@@ -333,6 +319,7 @@ function create_braid_text() {
333
319
  }
334
320
  // Handle case where remote doesn't exist yet - wait for local to create it
335
321
  remote_res = await braid_text.get(b, b_ops)
322
+ if (signal.aborted) return
336
323
 
337
324
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
338
325
  options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text after GET/sub: ' + remote_res?.status)
@@ -341,16 +328,12 @@ function create_braid_text() {
341
328
  if (remote_res === null) {
342
329
  // Remote doesn't exist yet, wait for local to put something
343
330
  await local_first_put_promise
344
- disconnect()
345
- connect()
346
- return
331
+ return handle_error(new Error('try again'))
347
332
  }
348
333
  options.on_res?.(remote_res)
349
334
  // on_error will call handle_error when connection drops
350
- } catch (e) {
351
- handle_error(e)
352
- }
353
- }
335
+ } catch (e) { handle_error(e) }
336
+ })
354
337
  }
355
338
 
356
339
  braid_text.serve = async (req, res, options = {}) => {
@@ -2823,6 +2806,37 @@ function create_braid_text() {
2823
2806
  return within_fiber.chains[id] = curr
2824
2807
  }
2825
2808
 
2809
+ // Calls func(inner_signal, reconnect) immediately and handles reconnection.
2810
+ // - inner_signal: AbortSignal that aborts when reconnect() is called or outter_signal aborts
2811
+ // - reconnect(error): call this to trigger a reconnection after get_delay(error, count) ms
2812
+ // - Multiple/rapid reconnect() calls are safe - only one reconnection will be scheduled
2813
+ // - If outter_signal aborts, no further calls to func will occur
2814
+ function reconnector(outter_signal, get_delay, func) {
2815
+ if (outter_signal?.aborted) return
2816
+
2817
+ var current_inner_ac = null
2818
+ outter_signal?.addEventListener('abort', () =>
2819
+ current_inner_ac?.abort())
2820
+
2821
+ var reconnect_count = 0
2822
+ connect()
2823
+ function connect() {
2824
+ if (outter_signal?.aborted) return
2825
+
2826
+ var ac = current_inner_ac = new AbortController()
2827
+ var inner_signal = ac.signal
2828
+
2829
+ func(inner_signal, (e) => {
2830
+ if (outter_signal?.aborted ||
2831
+ inner_signal.aborted) return
2832
+
2833
+ ac.abort()
2834
+ var delay = get_delay(e, ++reconnect_count)
2835
+ setTimeout(connect, delay)
2836
+ })
2837
+ }
2838
+ }
2839
+
2826
2840
  braid_text.get_resource = get_resource
2827
2841
 
2828
2842
  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.106",
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