braid-text 0.2.103 → 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 +300 -313
  2. package/package.json +1 -1
  3. package/test/tests.js +61 -26
package/index.js CHANGED
@@ -19,363 +19,319 @@ function create_braid_text() {
19
19
  braid_text.sync = async (a, b, options = {}) => {
20
20
  if (!options.merge_type) options.merge_type = 'dt'
21
21
 
22
- if ((a instanceof URL) === (b instanceof URL)) {
23
- // Both are URLs or both are local keys
24
- var a_first_put, b_first_put
25
- var a_first_put_promise = new Promise(done => a_first_put = done)
26
- var b_first_put_promise = new Promise(done => b_first_put = done)
27
-
28
- var a_ops = {
29
- signal: options.signal,
30
- subscribe: update => {
31
- update.signal = options.signal
32
- braid_text.put(b, update).then(a_first_put)
33
- },
34
- merge_type: options.merge_type,
35
- }
36
- braid_text.get(a, a_ops).then(x =>
37
- x || b_first_put_promise.then(() =>
38
- braid_text.get(a, a_ops)))
39
-
40
- var b_ops = {
41
- signal: options.signal,
42
- subscribe: update => {
43
- update.signal = options.signal
44
- braid_text.put(a, update).then(b_first_put)
45
- },
46
- merge_type: options.merge_type,
47
- }
48
- braid_text.get(b, b_ops).then(x =>
49
- x || a_first_put_promise.then(() =>
50
- braid_text.get(b, b_ops)))
51
- } else {
52
- // make a=local and b=remote (swap if not)
53
- if (a instanceof URL) { let swap = a; a = b; b = swap }
54
-
55
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
56
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync start')
57
-
58
- // Extract content type for proper Accept (GET) vs Content-Type (PUT) usage
59
- var content_type
60
- var get_headers = {}
61
- var put_headers = {}
62
- if (options.headers) {
63
- for (var [k, v] of Object.entries(options.headers)) {
64
- var lk = k.toLowerCase()
65
- if (lk === 'accept' || lk === 'content-type') {
66
- content_type = v
67
- } else {
68
- get_headers[k] = v
69
- put_headers[k] = v
70
- }
22
+ // Support for same-type params removed for now,
23
+ // since it is unused, unoptimized,
24
+ // and not as well battle tested
25
+ if ((a instanceof URL) === (b instanceof URL))
26
+ throw new Error(`one parameter should be local string key, and the other a remote URL object`)
27
+
28
+ // make a=local and b=remote (swap if not)
29
+ if (a instanceof URL) { let swap = a; a = b; b = swap }
30
+
31
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
32
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync start')
33
+
34
+ // Extract content type for proper Accept (GET) vs Content-Type (PUT) usage
35
+ var content_type
36
+ var get_headers = {}
37
+ var put_headers = {}
38
+ if (options.headers) {
39
+ for (var [k, v] of Object.entries(options.headers)) {
40
+ var lk = k.toLowerCase()
41
+ if (lk === 'accept' || lk === 'content-type') {
42
+ content_type = v
43
+ } else {
44
+ get_headers[k] = v
45
+ put_headers[k] = v
71
46
  }
72
47
  }
73
- if (content_type) {
74
- get_headers['Accept'] = content_type
75
- put_headers['Content-Type'] = content_type
76
- }
77
-
78
- var resource = (typeof a == 'string') ? await get_resource(a) : a
79
-
80
- if (!resource.meta.fork_point && options.fork_point_hint) {
81
- resource.meta.fork_point = options.fork_point_hint
82
- resource.change_meta()
83
- }
48
+ }
49
+ if (content_type) {
50
+ get_headers['Accept'] = content_type
51
+ put_headers['Content-Type'] = content_type
52
+ }
84
53
 
85
- function extend_frontier(frontier, version, parents) {
86
- // special case:
87
- // if current frontier has all parents,
88
- // then we can just remove those
89
- // and add version
90
- var frontier_set = new Set(frontier)
91
- if (parents.length &&
92
- parents.every(p => frontier_set.has(p))) {
93
- parents.forEach(p => frontier_set.delete(p))
94
- for (var event of version) frontier_set.add(event)
95
- frontier = [...frontier_set.values()]
96
- } else {
97
- // full-proof approach..
98
- var looking_for = frontier_set
99
- for (var event of version) looking_for.add(event)
54
+ var resource = (typeof a == 'string') ? await get_resource(a) : a
100
55
 
101
- frontier = []
102
- var shadow = new Set()
56
+ if (!resource.meta.fork_point && options.fork_point_hint) {
57
+ resource.meta.fork_point = options.fork_point_hint
58
+ resource.change_meta()
59
+ }
103
60
 
104
- var bytes = resource.doc.toBytes()
105
- var [_, events, parentss] = braid_text.dt_parse([...bytes])
106
- for (var i = events.length - 1; i >= 0 && looking_for.size; i--) {
107
- var e = events[i].join('-')
108
- if (looking_for.has(e)) {
109
- looking_for.delete(e)
110
- if (!shadow.has(e)) frontier.push(e)
111
- shadow.add(e)
112
- }
113
- if (shadow.has(e))
114
- parentss[i].forEach(p => shadow.add(p.join('-')))
61
+ function extend_frontier(frontier, version, parents) {
62
+ // special case:
63
+ // if current frontier has all parents,
64
+ // then we can just remove those
65
+ // and add version
66
+ var frontier_set = new Set(frontier)
67
+ if (parents.length &&
68
+ parents.every(p => frontier_set.has(p))) {
69
+ parents.forEach(p => frontier_set.delete(p))
70
+ for (var event of version) frontier_set.add(event)
71
+ frontier = [...frontier_set.values()]
72
+ } else {
73
+ // full-proof approach..
74
+ var looking_for = frontier_set
75
+ for (var event of version) looking_for.add(event)
76
+
77
+ frontier = []
78
+ var shadow = new Set()
79
+
80
+ var bytes = resource.doc.toBytes()
81
+ var [_, events, parentss] = braid_text.dt_parse([...bytes])
82
+ for (var i = events.length - 1; i >= 0 && looking_for.size; i--) {
83
+ var e = events[i].join('-')
84
+ if (looking_for.has(e)) {
85
+ looking_for.delete(e)
86
+ if (!shadow.has(e)) frontier.push(e)
87
+ shadow.add(e)
115
88
  }
89
+ if (shadow.has(e))
90
+ parentss[i].forEach(p => shadow.add(p.join('-')))
116
91
  }
117
- return frontier.sort()
118
92
  }
93
+ return frontier.sort()
94
+ }
119
95
 
120
- var closed
121
- var disconnect = () => {}
122
- options.signal?.addEventListener('abort', () => {
123
-
124
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
125
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync abort')
96
+ options.signal?.addEventListener('abort', () => {
97
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
98
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync abort')
99
+ })
126
100
 
127
- closed = true
128
- disconnect()
129
- })
101
+ var local_first_put
102
+ var local_first_put_promise = new Promise(done => local_first_put = done)
130
103
 
131
- var local_first_put
132
- var local_first_put_promise = new Promise(done => local_first_put = done)
104
+ reconnector(options.signal, (_e, count) => {
105
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
106
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text handle_error: ' + _e)
133
107
 
134
- var waitTime = 1
135
- function handle_error(_e) {
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) => {
112
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
113
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before on_pre_connect')
136
114
 
137
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
138
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text handle_error: ' + _e)
115
+ if (options.on_pre_connect) await options.on_pre_connect()
116
+ if (signal.aborted) return
139
117
 
140
- if (closed) return
141
- disconnect()
142
- var delay = waitTime * 1000
143
- console.log(`disconnected from ${b}, retrying in ${waitTime} second${waitTime > 1 ? 's' : ''}`)
144
- setTimeout(connect, delay)
145
- waitTime = Math.min(waitTime + 1, 3)
146
- }
118
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
119
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before fork-point stuff')
147
120
 
148
- connect()
149
- async function connect() {
150
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
151
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before on_pre_connect')
121
+ try {
122
+ // fork-point
123
+ async function check_version(version) {
124
+ var r = await braid_fetch(b.href, {
125
+ signal,
126
+ method: "HEAD",
127
+ version,
128
+ headers: get_headers
129
+ })
130
+ if (signal.aborted) return
152
131
 
153
- if (options.on_pre_connect) await options.on_pre_connect()
132
+ if (!r.ok && r.status !== 309 && r.status !== 500)
133
+ throw new Error(`unexpected HEAD status: ${r.status}`)
134
+ return r.ok
135
+ }
154
136
 
155
- if (closed) return
137
+ function extend_fork_point(update) {
138
+ resource.meta.fork_point =
139
+ extend_frontier(resource.meta.fork_point,
140
+ update.version, update.parents)
141
+ resource.change_meta()
142
+ }
156
143
 
157
- var ac = new AbortController()
158
- disconnect = () => ac.abort()
144
+ // see if remote has the fork point
145
+ if (resource.meta.fork_point &&
146
+ !(await check_version(resource.meta.fork_point))) {
147
+ if (signal.aborted) return
159
148
 
160
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
161
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before fork-point stuff')
149
+ resource.meta.fork_point = null
150
+ resource.change_meta()
151
+ }
152
+ if (signal.aborted) return
162
153
 
163
- try {
164
- // fork-point
165
- async function check_version(version) {
166
- var r = await braid_fetch(b.href, {
167
- signal: ac.signal,
168
- method: "HEAD",
169
- version,
170
- headers: get_headers
171
- })
172
- if (!r.ok && r.status !== 309 && r.status !== 500)
173
- throw new Error(`unexpected HEAD status: ${r.status}`)
174
- return r.ok
175
- }
154
+ // otherwise let's binary search for new fork point..
155
+ if (!resource.meta.fork_point) {
176
156
 
177
- function extend_fork_point(update) {
178
- resource.meta.fork_point =
179
- extend_frontier(resource.meta.fork_point,
180
- update.version, update.parents)
181
- resource.change_meta()
182
- }
157
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
158
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text fork-point binary search')
183
159
 
184
- // see if remote has the fork point
185
- if (resource.meta.fork_point &&
186
- !(await check_version(resource.meta.fork_point))) {
187
- resource.meta.fork_point = null
188
- resource.change_meta()
160
+ var bytes = resource.doc.toBytes()
161
+ var [_, events, __] = braid_text.dt_parse([...bytes])
162
+ events = events.map(x => x.join('-'))
163
+
164
+ var min = -1
165
+ var max = events.length
166
+ while (min + 1 < max) {
167
+ var i = Math.floor((min + max)/2)
168
+ var version = [events[i]]
169
+ if (await check_version(version)) {
170
+ if (signal.aborted) return
171
+
172
+ min = i
173
+ resource.meta.fork_point = version
174
+ } else max = i
189
175
  }
176
+ }
190
177
 
191
- // otherwise let's binary search for new fork point..
192
- if (!resource.meta.fork_point) {
193
-
194
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
195
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text fork-point binary search')
178
+ // local -> remote (with in_flight queue for concurrency control)
179
+ var q = []
180
+ var in_flight = new Map()
181
+ var max_in_flight = 10
182
+ var send_pump_lock = 0
183
+ var temp_acs = new Set()
184
+ signal.addEventListener('abort', () => {
185
+ for (var t of temp_acs) t.abort()
186
+ })
196
187
 
197
- var bytes = resource.doc.toBytes()
198
- var [_, events, __] = braid_text.dt_parse([...bytes])
199
- events = events.map(x => x.join('-'))
200
-
201
- var min = -1
202
- var max = events.length
203
- while (min + 1 < max) {
204
- var i = Math.floor((min + max)/2)
205
- var version = [events[i]]
206
- if (await check_version(version)) {
207
- min = i
208
- resource.meta.fork_point = version
209
- } else max = i
210
- }
211
- }
188
+ async function send_out(update) {
212
189
 
213
- // local -> remote (with in_flight queue for concurrency control)
214
- var q = []
215
- var in_flight = new Map()
216
- var max_in_flight = 10
217
- var send_pump_lock = 0
218
- var temp_acs = new Set()
219
- ac.signal.addEventListener('abort', () => {
220
- for (var t of temp_acs) t.abort()
221
- })
222
-
223
- async function send_out(update) {
190
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
191
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out')
224
192
 
225
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
226
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out')
193
+ update.signal = signal
194
+ update.dont_retry = true
195
+ if (options.peer) update.peer = options.peer
196
+ update.headers = put_headers
197
+ var x = await braid_text.put(b, update)
198
+ if (signal.aborted) return
227
199
 
228
- update.signal = ac.signal
229
- update.dont_retry = true
230
- if (options.peer) update.peer = options.peer
231
- update.headers = put_headers
232
- var x = await braid_text.put(b, update)
200
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
201
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out result: ' + x.ok)
202
+
203
+ if (x.ok) {
204
+ local_first_put()
205
+ extend_fork_point(update)
206
+ } else if (x.status === 401 || x.status === 403) {
207
+ await options.on_unauthorized?.()
208
+ } else throw new Error('failed to PUT: ' + x.status)
209
+ }
233
210
 
234
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
235
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out result: ' + x.ok)
236
-
237
- if (x.ok) {
238
- local_first_put()
239
- extend_fork_point(update)
240
- } else if (x.status === 401 || x.status === 403) {
241
- await options.on_unauthorized?.()
242
- } else throw new Error('failed to PUT: ' + x.status)
211
+ async function send_pump() {
212
+ if (signal.aborted) return
213
+ send_pump_lock++
214
+ if (send_pump_lock > 1) return
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)
243
234
  }
244
-
245
- async function send_pump() {
246
- send_pump_lock++
247
- if (send_pump_lock > 1) return
248
- try {
249
- if (closed) return
250
- if (in_flight.size >= max_in_flight) return
251
- if (!q.length) {
252
- // Extend frontier based on in-flight updates
253
- var frontier = resource.meta.fork_point || []
254
- for (var u of in_flight.values())
255
- frontier = extend_frontier(frontier, u.version, u.parents)
256
-
257
- var temp_ac = new AbortController()
258
- temp_acs.add(temp_ac)
259
- var temp_ops = {
260
- signal: temp_ac.signal,
261
- parents: frontier,
262
- merge_type: 'dt',
263
- peer: options.peer,
264
- subscribe: u => u.version?.length && q.push(u)
265
- }
266
- await braid_text.get(a, temp_ops)
267
- temp_ac.abort()
268
- temp_acs.delete(temp_ac)
269
- }
270
- while (q.length && in_flight.size < max_in_flight) {
271
- let u = q.shift()
272
- if (!u.version?.length) continue
273
- in_flight.set(u.version[0], u);
274
- (async () => {
275
- try {
276
- if (closed) return
277
- await send_out(u)
278
- if (closed) return
279
- in_flight.delete(u.version[0])
280
- setTimeout(send_pump, 0)
281
- } catch (e) {
282
- if (e.name === 'AbortError') {
283
- // ignore
284
- } else handle_error(e)
285
- }
286
- })()
287
- }
288
- } finally {
289
- var retry = send_pump_lock > 1
290
- send_pump_lock = 0
291
- if (retry) setTimeout(send_pump, 0)
292
- }
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
+ })()
293
247
  }
248
+ if (send_pump_lock > 1) setTimeout(send_pump, 0)
249
+ send_pump_lock = 0
250
+ }
294
251
 
295
- var a_ops = {
296
- signal: ac.signal,
297
- merge_type: 'dt',
298
- peer: options.peer,
299
- subscribe: update => {
300
- if (closed) return
301
- if (update.version?.length) {
302
- q.push(update)
303
- send_pump()
304
- }
252
+ var a_ops = {
253
+ signal,
254
+ merge_type: 'dt',
255
+ peer: options.peer,
256
+ subscribe: update => {
257
+ if (signal.aborted) return
258
+ if (update.version?.length) {
259
+ q.push(update)
260
+ send_pump()
305
261
  }
306
262
  }
307
- if (resource.meta.fork_point)
308
- a_ops.parents = resource.meta.fork_point
309
- braid_text.get(a, a_ops)
263
+ }
264
+ if (resource.meta.fork_point)
265
+ a_ops.parents = resource.meta.fork_point
266
+ braid_text.get(a, a_ops)
267
+
268
+ // remote -> local
269
+ var remote_res_done
270
+ var remote_res_promise = new Promise(done => remote_res_done = done)
271
+ var remote_res = null
272
+
273
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
274
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text before GET/sub')
310
275
 
311
- // remote -> local
312
- var remote_res_done
313
- var remote_res_promise = new Promise(done => remote_res_done = done)
314
- var remote_res = null
276
+ var b_ops = {
277
+ signal,
278
+ dont_retry: true,
279
+ headers: { ...get_headers, 'Merge-Type': 'dt', 'accept-encoding': 'updates(dt)' },
280
+ parents: resource.meta.fork_point,
281
+ peer: options.peer,
282
+ heartbeats: 120,
315
283
 
316
284
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
317
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text before GET/sub')
285
+ heartbeat_cb: () => {
286
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'got heartbeat')
287
+ },
318
288
 
319
- var b_ops = {
320
- signal: ac.signal,
321
- dont_retry: true,
322
- headers: { ...get_headers, 'Merge-Type': 'dt', 'accept-encoding': 'updates(dt)' },
323
- parents: resource.meta.fork_point,
324
- peer: options.peer,
325
- heartbeats: 120,
289
+ subscribe: async update => {
326
290
 
327
291
  // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
328
- heartbeat_cb: () => {
329
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'got heartbeat')
330
- },
331
-
332
- subscribe: async update => {
333
-
334
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
335
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text got update')
336
-
337
- // Wait for remote_res to be available
338
- await remote_res_promise
339
-
340
- // Check if this is a dt-encoded update
341
- if (update.extra_headers?.encoding === 'dt') {
342
- var cv = remote_res.headers.get('current-version')
343
- await braid_text.put(a, {
344
- body: update.body,
345
- transfer_encoding: 'dt'
346
- })
347
- if (cv) extend_fork_point({ version: JSON.parse(`[${cv}]`), parents: resource.meta.fork_point || [] })
348
- } else {
349
- await braid_text.put(a, update)
350
- if (update.version) extend_fork_point(update)
351
- }
352
- },
353
- on_error: e => {
354
- options.on_disconnect?.()
355
- handle_error(e)
292
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text got update')
293
+
294
+ // Wait for remote_res to be available
295
+ await remote_res_promise
296
+ if (signal.aborted) return
297
+
298
+ // Check if this is a dt-encoded update
299
+ if (update.extra_headers?.encoding === 'dt') {
300
+ var cv = remote_res.headers.get('current-version')
301
+ await braid_text.put(a, {
302
+ body: update.body,
303
+ transfer_encoding: 'dt'
304
+ })
305
+ if (signal.aborted) return
306
+ if (cv) extend_fork_point({ version: JSON.parse(`[${cv}]`), parents: resource.meta.fork_point || [] })
307
+ } else {
308
+ await braid_text.put(a, update)
309
+ if (signal.aborted) return
310
+ if (update.version) extend_fork_point(update)
356
311
  }
312
+ },
313
+ on_error: e => {
314
+ options.on_disconnect?.()
315
+ handle_error(e)
357
316
  }
358
- // Handle case where remote doesn't exist yet - wait for local to create it
359
- remote_res = await braid_text.get(b, b_ops)
317
+ }
318
+ // Handle case where remote doesn't exist yet - wait for local to create it
319
+ remote_res = await braid_text.get(b, b_ops)
320
+ if (signal.aborted) return
360
321
 
361
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
362
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text after GET/sub: ' + remote_res?.status)
363
-
364
- remote_res_done()
365
- if (remote_res === null) {
366
- // Remote doesn't exist yet, wait for local to put something
367
- await local_first_put_promise
368
- disconnect()
369
- connect()
370
- return
371
- }
372
- options.on_res?.(remote_res)
373
- // on_error will call handle_error when connection drops
374
- } catch (e) {
375
- handle_error(e)
322
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
323
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text after GET/sub: ' + remote_res?.status)
324
+
325
+ remote_res_done()
326
+ if (remote_res === null) {
327
+ // Remote doesn't exist yet, wait for local to put something
328
+ await local_first_put_promise
329
+ return handle_error(new Error('try again'))
376
330
  }
377
- }
378
- }
331
+ options.on_res?.(remote_res)
332
+ // on_error will call handle_error when connection drops
333
+ } catch (e) { handle_error(e) }
334
+ })
379
335
  }
380
336
 
381
337
  braid_text.serve = async (req, res, options = {}) => {
@@ -2848,6 +2804,37 @@ function create_braid_text() {
2848
2804
  return within_fiber.chains[id] = curr
2849
2805
  }
2850
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
+
2851
2838
  braid_text.get_resource = get_resource
2852
2839
 
2853
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.103",
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
@@ -127,28 +127,23 @@ runTest(
127
127
  var key_a = 'test-a-' + Math.random().toString(36).slice(2)
128
128
  var key_b = 'test-b-' + Math.random().toString(36).slice(2)
129
129
 
130
- var r = await braid_fetch(`/${key_a}`, {
131
- method: 'PUT',
132
- body: 'hi'
133
- })
134
- if (!r.ok) return 'got: ' + r.status
135
-
136
130
  var r = await braid_fetch(`/eval`, {
137
131
  method: 'PUT',
138
132
  body: `void (async () => {
139
- braid_text.sync(new URL('http://localhost:8889/${key_a}'),
140
- new URL('http://localhost:8889/${key_b}'))
141
- res.end('')
133
+ try {
134
+ await braid_text.sync(new URL('http://localhost:8889/${key_a}'),
135
+ new URL('http://localhost:8889/${key_b}'))
136
+ res.end('no error')
137
+ } catch (e) {
138
+ res.end('' + e)
139
+ }
142
140
  })()`
143
141
  })
144
142
  if (!r.ok) return 'got: ' + r.status
145
143
 
146
- await new Promise(done => setTimeout(done, 100))
147
-
148
- var r = await braid_fetch(`/${key_b}`)
149
144
  return 'got: ' + (await r.text())
150
145
  },
151
- 'got: hi'
146
+ 'got: Error: one parameter should be local string key, and the other a remote URL object'
152
147
  )
153
148
 
154
149
  runTest(
@@ -261,7 +256,10 @@ runTest(
261
256
  method: 'PUT',
262
257
  body: `void (async () => {
263
258
  var ac = new AbortController()
264
- braid_text.sync('/${key_a}', '/${key_b}', {signal: ac.signal})
259
+ braid_text.get('/${key_a}', {
260
+ signal: ac.signal,
261
+ subscribe: update => braid_text.put('/${key_b}', update)
262
+ })
265
263
  await new Promise(done => setTimeout(done, 100))
266
264
  ac.abort()
267
265
  res.end('')
@@ -298,27 +296,22 @@ runTest(
298
296
  var key_a = 'test-a-' + Math.random().toString(36).slice(2)
299
297
  var key_b = 'test-b-' + Math.random().toString(36).slice(2)
300
298
 
301
- var r = await braid_fetch(`/${key_a}`, {
302
- method: 'PUT',
303
- body: 'hi'
304
- })
305
- if (!r.ok) return 'got: ' + r.status
306
-
307
299
  var r = await braid_fetch(`/eval`, {
308
300
  method: 'PUT',
309
301
  body: `void (async () => {
310
- braid_text.sync('/${key_a}', '/${key_b}')
311
- res.end('')
302
+ try {
303
+ await braid_text.sync('/${key_a}', '/${key_b}')
304
+ res.end('no error')
305
+ } catch (e) {
306
+ res.end('' + e)
307
+ }
312
308
  })()`
313
309
  })
314
310
  if (!r.ok) return 'got: ' + r.status
315
311
 
316
- await new Promise(done => setTimeout(done, 100))
317
-
318
- var r = await braid_fetch(`/${key_b}`)
319
312
  return 'got: ' + (await r.text())
320
313
  },
321
- 'got: hi'
314
+ 'got: Error: one parameter should be local string key, and the other a remote URL object'
322
315
  )
323
316
 
324
317
  runTest(
@@ -2856,6 +2849,48 @@ runTest(
2856
2849
  'correctly threw error'
2857
2850
  )
2858
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
+
2859
2894
  }
2860
2895
 
2861
2896
  // Export for both Node.js and browser environments