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.
Files changed (3) hide show
  1. package/index.js +351 -311
  2. package/package.json +2 -3
  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
- 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', () => {
96
+ var closed
97
+ var disconnect = () => {}
98
+ options.signal?.addEventListener('abort', () => {
123
99
 
124
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
125
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync abort')
100
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
101
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text sync abort')
126
102
 
127
- closed = true
128
- disconnect()
129
- })
103
+ closed = true
104
+ disconnect()
105
+ })
130
106
 
131
- var local_first_put
132
- var local_first_put_promise = new Promise(done => local_first_put = done)
107
+ var local_first_put
108
+ var local_first_put_promise = new Promise(done => local_first_put = done)
133
109
 
134
- var waitTime = 1
135
- function handle_error(_e) {
110
+ var waitTime = 1
111
+ function handle_error(_e) {
136
112
 
137
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
138
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text handle_error: ' + _e)
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
- 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')
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
- if (options.on_pre_connect) await options.on_pre_connect()
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
- if (closed) return
129
+ if (options.on_pre_connect) await options.on_pre_connect()
156
130
 
157
- var ac = new AbortController()
158
- disconnect = () => ac.abort()
131
+ if (closed) return
159
132
 
160
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
161
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text connect before fork-point stuff')
133
+ var ac = new AbortController()
134
+ disconnect = () => ac.abort()
162
135
 
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
- }
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
- 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
- }
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
- // 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()
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
- // otherwise let's binary search for new fork point..
192
- if (!resource.meta.fork_point) {
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
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
195
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text fork-point binary search')
167
+ // otherwise let's binary search for new fork point..
168
+ if (!resource.meta.fork_point) {
196
169
 
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
- }
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
- // 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
- })
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
- async function send_out(update) {
199
+ async function send_out(update) {
224
200
 
225
- // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
226
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out')
201
+ // DEBUGGING HACK ID: L04LPFHQ1M -- INVESTIGATING DISCONNECTS
202
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text send_out')
227
203
 
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)
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
- // 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)
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
- 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
- })()
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
- } finally {
289
- var retry = send_pump_lock > 1
290
- send_pump_lock = 0
291
- if (retry) setTimeout(send_pump, 0)
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
- 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
- }
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
- if (resource.meta.fork_point)
308
- a_ops.parents = resource.meta.fork_point
309
- braid_text.get(a, a_ops)
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
- // remote -> local
312
- var remote_res_done
313
- var remote_res_promise = new Promise(done => remote_res_done = done)
314
- var remote_res = null
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
- options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'braid-text before GET/sub')
304
+ heartbeat_cb: () => {
305
+ options.do_investigating_disconnects_log_L04LPFHQ1M?.(a, 'got heartbeat')
306
+ },
318
307
 
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,
308
+ subscribe: async update => {
326
309
 
327
310
  // 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)
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
- // Handle case where remote doesn't exist yet - wait for local to create it
359
- remote_res = await braid_text.get(b, b_ops)
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
- // 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)
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.102",
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
- 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(