braid-text 0.2.102 → 0.2.104
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 +351 -311
- package/package.json +2 -3
- package/test/tests.js +19 -26
package/index.js
CHANGED
|
@@ -19,361 +19,336 @@ 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
|
-
|
|
96
|
+
var closed
|
|
97
|
+
var disconnect = () => {}
|
|
98
|
+
options.signal?.addEventListener('abort', () => {
|
|
123
99
|
|
|
124
|
-
|
|
125
|
-
|
|
100
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
101
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync abort')
|
|
126
102
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
103
|
+
closed = true
|
|
104
|
+
disconnect()
|
|
105
|
+
})
|
|
130
106
|
|
|
131
|
-
|
|
132
|
-
|
|
107
|
+
var local_first_put
|
|
108
|
+
var local_first_put_promise = new Promise(done => local_first_put = done)
|
|
133
109
|
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
var waitTime = 1
|
|
111
|
+
function handle_error(_e) {
|
|
136
112
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
}
|
|
113
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
114
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text handle_error: ' + _e)
|
|
147
115
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
}
|
|
152
123
|
|
|
153
|
-
|
|
124
|
+
connect()
|
|
125
|
+
async function connect() {
|
|
126
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
127
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before on_pre_connect')
|
|
154
128
|
|
|
155
|
-
|
|
129
|
+
if (options.on_pre_connect) await options.on_pre_connect()
|
|
156
130
|
|
|
157
|
-
|
|
158
|
-
disconnect = () => ac.abort()
|
|
131
|
+
if (closed) return
|
|
159
132
|
|
|
160
|
-
|
|
161
|
-
|
|
133
|
+
var ac = new AbortController()
|
|
134
|
+
disconnect = () => ac.abort()
|
|
162
135
|
|
|
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
|
-
}
|
|
136
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
137
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before fork-point stuff')
|
|
176
138
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
139
|
+
try {
|
|
140
|
+
// fork-point
|
|
141
|
+
async function check_version(version) {
|
|
142
|
+
var r = await braid_fetch(b.href, {
|
|
143
|
+
signal: ac.signal,
|
|
144
|
+
method: "HEAD",
|
|
145
|
+
version,
|
|
146
|
+
headers: get_headers
|
|
147
|
+
})
|
|
148
|
+
if (!r.ok && r.status !== 309 && r.status !== 500)
|
|
149
|
+
throw new Error(`unexpected HEAD status: ${r.status}`)
|
|
150
|
+
return r.ok
|
|
151
|
+
}
|
|
183
152
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
153
|
+
function extend_fork_point(update) {
|
|
154
|
+
resource.meta.fork_point =
|
|
155
|
+
extend_frontier(resource.meta.fork_point,
|
|
156
|
+
update.version, update.parents)
|
|
157
|
+
resource.change_meta()
|
|
158
|
+
}
|
|
190
159
|
|
|
191
|
-
|
|
192
|
-
|
|
160
|
+
// see if remote has the fork point
|
|
161
|
+
if (resource.meta.fork_point &&
|
|
162
|
+
!(await check_version(resource.meta.fork_point))) {
|
|
163
|
+
resource.meta.fork_point = null
|
|
164
|
+
resource.change_meta()
|
|
165
|
+
}
|
|
193
166
|
|
|
194
|
-
|
|
195
|
-
|
|
167
|
+
// otherwise let's binary search for new fork point..
|
|
168
|
+
if (!resource.meta.fork_point) {
|
|
196
169
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
170
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
171
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text fork-point binary search')
|
|
172
|
+
|
|
173
|
+
var bytes = resource.doc.toBytes()
|
|
174
|
+
var [_, events, __] = braid_text.dt_parse([...bytes])
|
|
175
|
+
events = events.map(x => x.join('-'))
|
|
176
|
+
|
|
177
|
+
var min = -1
|
|
178
|
+
var max = events.length
|
|
179
|
+
while (min + 1 < max) {
|
|
180
|
+
var i = Math.floor((min + max)/2)
|
|
181
|
+
var version = [events[i]]
|
|
182
|
+
if (await check_version(version)) {
|
|
183
|
+
min = i
|
|
184
|
+
resource.meta.fork_point = version
|
|
185
|
+
} else max = i
|
|
211
186
|
}
|
|
187
|
+
}
|
|
212
188
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
189
|
+
// local -> remote (with in_flight queue for concurrency control)
|
|
190
|
+
var q = []
|
|
191
|
+
var in_flight = new Map()
|
|
192
|
+
var max_in_flight = 10
|
|
193
|
+
var send_pump_lock = 0
|
|
194
|
+
var temp_acs = new Set()
|
|
195
|
+
ac.signal.addEventListener('abort', () => {
|
|
196
|
+
for (var t of temp_acs) t.abort()
|
|
197
|
+
})
|
|
222
198
|
|
|
223
|
-
|
|
199
|
+
async function send_out(update) {
|
|
224
200
|
|
|
225
|
-
|
|
226
|
-
|
|
201
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
202
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out')
|
|
227
203
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
204
|
+
update.signal = ac.signal
|
|
205
|
+
update.dont_retry = true
|
|
206
|
+
if (options.peer) update.peer = options.peer
|
|
207
|
+
update.headers = put_headers
|
|
208
|
+
var x = await braid_text.put(b, update)
|
|
233
209
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
210
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
211
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out result: ' + x.ok)
|
|
212
|
+
|
|
213
|
+
if (x.ok) {
|
|
214
|
+
local_first_put()
|
|
215
|
+
extend_fork_point(update)
|
|
216
|
+
} else if (x.status === 401 || x.status === 403) {
|
|
217
|
+
await options.on_unauthorized?.()
|
|
218
|
+
} else throw new Error('failed to PUT: ' + x.status)
|
|
219
|
+
}
|
|
244
220
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
})()
|
|
221
|
+
async function send_pump() {
|
|
222
|
+
send_pump_lock++
|
|
223
|
+
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)
|
|
287
241
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
})()
|
|
292
263
|
}
|
|
264
|
+
} finally {
|
|
265
|
+
var retry = send_pump_lock > 1
|
|
266
|
+
send_pump_lock = 0
|
|
267
|
+
if (retry) setTimeout(send_pump, 0)
|
|
293
268
|
}
|
|
269
|
+
}
|
|
294
270
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
271
|
+
var a_ops = {
|
|
272
|
+
signal: ac.signal,
|
|
273
|
+
merge_type: 'dt',
|
|
274
|
+
peer: options.peer,
|
|
275
|
+
subscribe: update => {
|
|
276
|
+
if (closed) return
|
|
277
|
+
if (update.version?.length) {
|
|
278
|
+
q.push(update)
|
|
279
|
+
send_pump()
|
|
305
280
|
}
|
|
306
281
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
282
|
+
}
|
|
283
|
+
if (resource.meta.fork_point)
|
|
284
|
+
a_ops.parents = resource.meta.fork_point
|
|
285
|
+
braid_text.get(a, a_ops)
|
|
310
286
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
287
|
+
// remote -> local
|
|
288
|
+
var remote_res_done
|
|
289
|
+
var remote_res_promise = new Promise(done => remote_res_done = done)
|
|
290
|
+
var remote_res = null
|
|
291
|
+
|
|
292
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
293
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text before GET/sub')
|
|
294
|
+
|
|
295
|
+
var b_ops = {
|
|
296
|
+
signal: ac.signal,
|
|
297
|
+
dont_retry: true,
|
|
298
|
+
headers: { ...get_headers, 'Merge-Type': 'dt', 'accept-encoding': 'updates(dt)' },
|
|
299
|
+
parents: resource.meta.fork_point,
|
|
300
|
+
peer: options.peer,
|
|
301
|
+
heartbeats: 120,
|
|
315
302
|
|
|
316
303
|
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
317
|
-
|
|
304
|
+
heartbeat_cb: () => {
|
|
305
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'got heartbeat')
|
|
306
|
+
},
|
|
318
307
|
|
|
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,
|
|
308
|
+
subscribe: async update => {
|
|
326
309
|
|
|
327
310
|
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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)
|
|
311
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text got update')
|
|
312
|
+
|
|
313
|
+
// Wait for remote_res to be available
|
|
314
|
+
await remote_res_promise
|
|
315
|
+
|
|
316
|
+
// Check if this is a dt-encoded update
|
|
317
|
+
if (update.extra_headers?.encoding === 'dt') {
|
|
318
|
+
var cv = remote_res.headers.get('current-version')
|
|
319
|
+
await braid_text.put(a, {
|
|
320
|
+
body: update.body,
|
|
321
|
+
transfer_encoding: 'dt'
|
|
322
|
+
})
|
|
323
|
+
if (cv) extend_fork_point({ version: JSON.parse(`[${cv}]`), parents: resource.meta.fork_point || [] })
|
|
324
|
+
} else {
|
|
325
|
+
await braid_text.put(a, update)
|
|
326
|
+
if (update.version) extend_fork_point(update)
|
|
356
327
|
}
|
|
328
|
+
},
|
|
329
|
+
on_error: e => {
|
|
330
|
+
options.on_disconnect?.()
|
|
331
|
+
handle_error(e)
|
|
357
332
|
}
|
|
358
|
-
|
|
359
|
-
|
|
333
|
+
}
|
|
334
|
+
// Handle case where remote doesn't exist yet - wait for local to create it
|
|
335
|
+
remote_res = await braid_text.get(b, b_ops)
|
|
360
336
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
options.on_res?.(remote_res)
|
|
373
|
-
// on_error will call handle_error when connection drops
|
|
374
|
-
} catch (e) {
|
|
375
|
-
handle_error(e)
|
|
337
|
+
// DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
|
|
338
|
+
options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text after GET/sub: ' + remote_res?.status)
|
|
339
|
+
|
|
340
|
+
remote_res_done()
|
|
341
|
+
if (remote_res === null) {
|
|
342
|
+
// Remote doesn't exist yet, wait for local to put something
|
|
343
|
+
await local_first_put_promise
|
|
344
|
+
disconnect()
|
|
345
|
+
connect()
|
|
346
|
+
return
|
|
376
347
|
}
|
|
348
|
+
options.on_res?.(remote_res)
|
|
349
|
+
// on_error will call handle_error when connection drops
|
|
350
|
+
} catch (e) {
|
|
351
|
+
handle_error(e)
|
|
377
352
|
}
|
|
378
353
|
}
|
|
379
354
|
}
|
|
@@ -2473,10 +2448,6 @@ function create_braid_text() {
|
|
|
2473
2448
|
return i
|
|
2474
2449
|
}
|
|
2475
2450
|
|
|
2476
|
-
var {
|
|
2477
|
-
encode_file_path_component, encode_to_avoid_icase_collision
|
|
2478
|
-
} = require('url-file-db/canonical_path')
|
|
2479
|
-
|
|
2480
2451
|
// Mapping between keys and their encoded filenames
|
|
2481
2452
|
// Populated at init time, used to avoid re-encoding and handle case collisions
|
|
2482
2453
|
var key_to_filename = new Map()
|
|
@@ -2735,6 +2706,75 @@ function create_braid_text() {
|
|
|
2735
2706
|
}
|
|
2736
2707
|
}
|
|
2737
2708
|
|
|
2709
|
+
// -----------------------------------------------------------------------------
|
|
2710
|
+
// File Path Encoding Utilities (from url-file-db/canonical_path)
|
|
2711
|
+
// -----------------------------------------------------------------------------
|
|
2712
|
+
|
|
2713
|
+
function encode_file_path_component(component) {
|
|
2714
|
+
// Encode characters that are unsafe on various filesystems:
|
|
2715
|
+
// < > : " / \ | ? * - Windows restrictions
|
|
2716
|
+
// % - Reserved for encoding
|
|
2717
|
+
// \x00-\x1f, \x7f - Control characters
|
|
2718
|
+
var encoded = component.replace(/[<>:"|\\?*%\x00-\x1f\x7f/]/g, encode_char)
|
|
2719
|
+
|
|
2720
|
+
// Encode Windows reserved filenames (con, prn, aux, nul, com1-9, lpt1-9)
|
|
2721
|
+
var windows_reserved = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i
|
|
2722
|
+
var match = component.match(windows_reserved)
|
|
2723
|
+
if (match) {
|
|
2724
|
+
var reserved_word = match[1]
|
|
2725
|
+
var last_char = reserved_word[reserved_word.length - 1]
|
|
2726
|
+
var encoded_reserved = reserved_word.slice(0, -1) + encode_char(last_char)
|
|
2727
|
+
var encoded_extension = encoded.slice(reserved_word.length)
|
|
2728
|
+
encoded = encoded_reserved + encoded_extension
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Encode trailing dots and spaces (stripped by Windows)
|
|
2732
|
+
if (encoded.endsWith('.') || encoded.endsWith(' ')) {
|
|
2733
|
+
var last_char = encoded[encoded.length - 1]
|
|
2734
|
+
encoded = encoded.slice(0, -1) + encode_char(last_char)
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
return encoded
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
function encode_to_avoid_icase_collision(component, existing_icomponents) {
|
|
2741
|
+
var icomponent = component.toLowerCase()
|
|
2742
|
+
|
|
2743
|
+
while (existing_icomponents.has(icomponent)) {
|
|
2744
|
+
var found_letter = false
|
|
2745
|
+
|
|
2746
|
+
// Find the last letter (a-zA-Z) that isn't part of a %XX encoding
|
|
2747
|
+
for (var i = component.length - 1; i >= 0; i--) {
|
|
2748
|
+
if (i >= 2 && component[i - 2] === '%') {
|
|
2749
|
+
i -= 2
|
|
2750
|
+
continue
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
var char = component[i]
|
|
2754
|
+
|
|
2755
|
+
// Only encode letters - encoding non-letters doesn't help resolve case collisions
|
|
2756
|
+
if (!/[a-zA-Z]/.test(char)) {
|
|
2757
|
+
continue
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
component = component.slice(0, i) + encode_char(char) + component.slice(i + 1)
|
|
2761
|
+
icomponent = component.toLowerCase()
|
|
2762
|
+
found_letter = true
|
|
2763
|
+
break
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
if (!found_letter) {
|
|
2767
|
+
throw new Error('Should never happen - safety check')
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
return component
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
function encode_char(char) {
|
|
2775
|
+
return '%' + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2738
2778
|
function ascii_ify(s) {
|
|
2739
2779
|
return s.replace(/[^\x20-\x7E]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
|
|
2740
2780
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "braid-text",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.104",
|
|
4
4
|
"description": "Library for collaborative text over http using braid.",
|
|
5
5
|
"author": "Braid Working Group",
|
|
6
6
|
"repository": "braid-org/braid-text",
|
|
7
7
|
"homepage": "https://braid.org",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@braid.org/diamond-types-node": "^2.0.0",
|
|
10
|
-
"braid-http": "~1.3.86"
|
|
11
|
-
"url-file-db": "^0.0.25"
|
|
10
|
+
"braid-http": "~1.3.86"
|
|
12
11
|
}
|
|
13
12
|
}
|
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(
|