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.
- package/index.js +300 -313
- package/package.json +1 -1
- 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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
options.
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
})
|
|
101
|
+
var local_first_put
|
|
102
|
+
var local_first_put_promise = new Promise(done => local_first_put = done)
|
|
130
103
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
115
|
+
if (options.on_pre_connect) await options.on_pre_connect()
|
|
116
|
+
if (signal.aborted) return
|
|
139
117
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
149
|
+
resource.meta.fork_point = null
|
|
150
|
+
resource.change_meta()
|
|
151
|
+
}
|
|
152
|
+
if (signal.aborted) return
|
|
162
153
|
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
214
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
285
|
+
heartbeat_cb: () => {
|
|
286
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'got heartbeat')
|
|
287
|
+
},
|
|
318
288
|
|
|
319
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
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
|
-
|
|
140
|
-
new URL('http://localhost:8889/${
|
|
141
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
311
|
-
|
|
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:
|
|
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
|