braid-text 0.2.76 → 0.2.78

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.
@@ -4,7 +4,9 @@
4
4
  "Bash(node test/test.js:*)",
5
5
  "Bash(node:*)",
6
6
  "Bash(git add:*)",
7
- "Bash(git commit -m \"$(cat <<''EOF''\n0.2.74 - updates url-file-db to 0.0.19\n\nšŸ¤– Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")"
7
+ "Bash(git commit -m \"$(cat <<''EOF''\n0.2.74 - updates url-file-db to 0.0.19\n\nšŸ¤– Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
8
+ "Bash(git push)",
9
+ "Bash(npm publish:*)"
8
10
  ],
9
11
  "deny": [],
10
12
  "ask": []
package/index.js CHANGED
@@ -17,25 +17,37 @@ function create_braid_text() {
17
17
  let max_encoded_key_size = 240
18
18
 
19
19
  braid_text.sync = async (a, b, options = {}) => {
20
- var unsync_cbs = []
21
- options.my_unsync = () => unsync_cbs.forEach(cb => cb())
22
-
23
20
  if (!options.merge_type) options.merge_type = 'dt'
24
21
 
25
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
+
26
28
  var a_ops = {
27
- subscribe: update => braid_text.put(b, update),
29
+ signal: options.signal,
30
+ subscribe: update => {
31
+ update.signal = options.signal
32
+ braid_text.put(b, update).then(a_first_put)
33
+ },
28
34
  merge_type: options.merge_type,
29
35
  }
30
- braid_text.get(a, a_ops)
31
- unsync_cbs.push(() => braid_text.forget(a, a_ops))
36
+ braid_text.get(a, a_ops).then(x =>
37
+ x || b_first_put_promise.then(() =>
38
+ braid_text.get(a, a_ops)))
32
39
 
33
40
  var b_ops = {
34
- subscribe: update => braid_text.put(a, update),
41
+ signal: options.signal,
42
+ subscribe: update => {
43
+ update.signal = options.signal
44
+ braid_text.put(a, update).then(b_first_put)
45
+ },
35
46
  merge_type: options.merge_type,
36
47
  }
37
- braid_text.get(b, b_ops)
38
- unsync_cbs.push(() => braid_text.forget(b, b_ops))
48
+ braid_text.get(b, b_ops).then(x =>
49
+ x || a_first_put_promise.then(() =>
50
+ braid_text.get(b, b_ops)))
39
51
  } else {
40
52
  // make a=local and b=remote (swap if not)
41
53
  if (a instanceof URL) { let swap = a; a = b; b = swap }
@@ -78,12 +90,15 @@ function create_braid_text() {
78
90
  }
79
91
 
80
92
  var closed
81
- var disconnect
82
- unsync_cbs.push(() => {
93
+ var disconnect = () => {}
94
+ options.signal?.addEventListener('abort', () => {
83
95
  closed = true
84
96
  disconnect()
85
97
  })
86
98
 
99
+ var local_first_put
100
+ var local_first_put_promise = new Promise(done => local_first_put = done)
101
+
87
102
  connect()
88
103
  async function connect() {
89
104
  if (options.on_connect) options.on_connect()
@@ -91,9 +106,7 @@ function create_braid_text() {
91
106
  if (closed) return
92
107
 
93
108
  var ac = new AbortController()
94
- var disconnect_cbs = [() => ac.abort()]
95
-
96
- disconnect = () => disconnect_cbs.forEach(cb => cb())
109
+ disconnect = () => ac.abort()
97
110
 
98
111
  try {
99
112
  // fork-point
@@ -142,9 +155,11 @@ function create_braid_text() {
142
155
 
143
156
  // local -> remote
144
157
  var a_ops = {
158
+ signal: ac.signal,
145
159
  subscribe: update => {
146
160
  update.signal = ac.signal
147
161
  braid_text.put(b, update).then((x) => {
162
+ local_first_put()
148
163
  extend_fork_point(update)
149
164
  }).catch(e => {
150
165
  if (e.name === 'AbortError') {
@@ -155,20 +170,26 @@ function create_braid_text() {
155
170
  }
156
171
  if (resource.meta.fork_point)
157
172
  a_ops.parents = resource.meta.fork_point
158
- disconnect_cbs.push(() => braid_text.forget(a, a_ops))
159
173
  braid_text.get(a, a_ops)
160
174
 
161
175
  // remote -> local
162
176
  var b_ops = {
177
+ signal: ac.signal,
163
178
  dont_retry: true,
164
179
  subscribe: async update => {
165
180
  await braid_text.put(a, update)
166
181
  extend_fork_point(update)
167
182
  },
168
183
  }
169
- disconnect_cbs.push(() => braid_text.forget(b, b_ops))
170
- // NOTE: this should not return, but it might throw
171
- await braid_text.get(b, b_ops)
184
+ // Handle case where remote doesn't exist yet - wait for local to create it
185
+ var remote_result = await braid_text.get(b, b_ops)
186
+ if (remote_result === null) {
187
+ // Remote doesn't exist yet, wait for local to put something
188
+ await local_first_put_promise
189
+ disconnect()
190
+ connect()
191
+ }
192
+ // NOTE: if remote exists, this should not return, but it might throw
172
193
  } catch (e) {
173
194
  if (closed) return
174
195
 
@@ -176,7 +197,7 @@ function create_braid_text() {
176
197
  console.log(`disconnected, retrying in 1 second`)
177
198
  setTimeout(connect, 1000)
178
199
  }
179
- }
200
+ }
180
201
  }
181
202
  }
182
203
 
@@ -463,7 +484,21 @@ function create_braid_text() {
463
484
  throw new Error("unknown")
464
485
  }
465
486
 
466
- braid_text.delete = async (key) => {
487
+ braid_text.delete = async (key, options) => {
488
+ if (!options) options = {}
489
+
490
+ // Handle URL - make a DELETE request
491
+ if (key instanceof URL) {
492
+ var params = {
493
+ method: 'DELETE',
494
+ signal: options.signal,
495
+ }
496
+ for (var x of ['headers', 'peer'])
497
+ if (options[x] != null) params[x] = options[x]
498
+
499
+ return await braid_fetch(key.href, params)
500
+ }
501
+
467
502
  // Accept either a key string or a resource object
468
503
  let resource = (typeof key == 'string') ? await get_resource(key) : key
469
504
  await resource.delete()
@@ -482,19 +517,21 @@ function create_braid_text() {
482
517
  if (key instanceof URL) {
483
518
  if (!options) options = {}
484
519
 
485
- options.my_abort = new AbortController()
486
-
487
520
  var params = {
488
- signal: options.my_abort.signal,
521
+ signal: options.signal,
489
522
  subscribe: !!options.subscribe,
490
523
  heartbeats: 120,
491
524
  }
492
- if (!options.dont_retry) params.retry = () => true
525
+ if (!options.dont_retry) {
526
+ params.retry = (res) => res.status !== 404
527
+ }
493
528
  for (var x of ['headers', 'parents', 'version', 'peer'])
494
529
  if (options[x] != null) params[x] = options[x]
495
530
 
496
531
  var res = await braid_fetch(key.href, params)
497
532
 
533
+ if (res.status === 404) return null
534
+
498
535
  if (options.subscribe) {
499
536
  if (options.dont_retry) {
500
537
  var error_happened
@@ -583,6 +620,8 @@ function create_braid_text() {
583
620
 
584
621
  options.my_last_sent_version = x.version
585
622
  resource.simpleton_clients.add(options)
623
+ options.signal?.addEventListener('abort', () =>
624
+ resource.simpleton_clients.delete(options))
586
625
  } else {
587
626
 
588
627
  if (options.accept_encoding?.match(/updates\s*\((.*)\)/)?.[1].split(',').map(x=>x.trim()).includes('dt')) {
@@ -630,22 +669,12 @@ function create_braid_text() {
630
669
  }
631
670
 
632
671
  resource.clients.add(options)
672
+ options.signal?.addEventListener('abort', () =>
673
+ resource.clients.delete(options))
633
674
  }
634
675
  }
635
676
  }
636
677
 
637
- braid_text.forget = async (key, options) => {
638
- if (!options) throw new Error('options is required')
639
-
640
- if (key instanceof URL) return options.my_abort.abort()
641
-
642
- let resource = (typeof key == 'string') ? await get_resource(key) : key
643
-
644
- if (options.merge_type != "dt")
645
- resource.simpleton_clients.delete(options)
646
- else resource.clients.delete(options)
647
- }
648
-
649
678
  braid_text.put = async (key, options) => {
650
679
  if (options.version) {
651
680
  validate_version_array(options.version)
@@ -657,14 +686,9 @@ function create_braid_text() {
657
686
  }
658
687
 
659
688
  if (key instanceof URL) {
660
- options.my_abort = new AbortController()
661
- if (options.signal)
662
- options.signal.addEventListener('abort', () =>
663
- options.my_abort.abort())
664
-
665
689
  var params = {
666
690
  method: 'PUT',
667
- signal: options.my_abort.signal,
691
+ signal: options.signal,
668
692
  retry: () => true,
669
693
  }
670
694
  for (var x of ['headers', 'parents', 'version', 'peer', 'body', 'patches'])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.2.76",
3
+ "version": "0.2.78",
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/fuzz-test.js CHANGED
@@ -270,16 +270,20 @@ async function main() {
270
270
 
271
271
  // try getting updates from middle_doc to doc
272
272
  if (!v_eq(middle_v, doc_v)) {
273
- var o
273
+ var ac = new AbortController()
274
274
  await new Promise(async done => {
275
- o = {merge_type: 'dt', parents: middle_v, subscribe: async update => {
276
- await braid_text.put('middle_doc', update)
277
- middle_v = (await braid_text.get_resource('middle_doc')).version
278
- if (v_eq(doc_v, middle_v)) done()
279
- }}
280
- braid_text.get('doc', o)
275
+ braid_text.get('doc', {
276
+ signal: ac.signal,
277
+ merge_type: 'dt',
278
+ parents: middle_v,
279
+ subscribe: async update => {
280
+ await braid_text.put('middle_doc', update)
281
+ middle_v = (await braid_text.get_resource('middle_doc')).version
282
+ if (v_eq(doc_v, middle_v)) done()
283
+ }
284
+ })
281
285
  })
282
- await braid_text.forget('doc', o)
286
+ ac.abort()
283
287
  }
284
288
 
285
289
  if (await braid_text.get('middle_doc') != await braid_text.get('doc')) throw new Error('bad')
package/test/tests.js CHANGED
@@ -50,13 +50,14 @@ runTest(
50
50
  method: 'PUT',
51
51
  body: `void (async () => {
52
52
  var count = 0
53
- var ops
54
- braid_text.sync('/${key}', new URL('http://localhost:8889/have_error'), ops = {
53
+ var ac = new AbortController()
54
+ braid_text.sync('/${key}', new URL('http://localhost:8889/have_error'), {
55
+ signal: ac.signal,
55
56
  on_connect: () => {
56
57
  count++
57
58
  if (count === 2) {
58
59
  res.end('it reconnected!')
59
- ops.my_unsync()
60
+ ac.abort()
60
61
  }
61
62
  }
62
63
  })
@@ -201,13 +202,14 @@ runTest(
201
202
  method: 'PUT',
202
203
  body: `void (async () => {
203
204
  var count = 0
204
- var ops
205
- braid_text.sync('/${key_a}', new URL('http://localhost:8889/have_error'), ops = {
205
+ var ac = new AbortController()
206
+ braid_text.sync('/${key_a}', new URL('http://localhost:8889/have_error'), {
207
+ signal: ac.signal,
206
208
  on_connect: () => {
207
209
  count++
208
210
  if (count === 2) {
209
211
  res.end('it reconnected!')
210
- ops.my_unsync()
212
+ ac.abort()
211
213
  }
212
214
  }
213
215
  })
@@ -265,10 +267,10 @@ runTest(
265
267
  var r = await braid_fetch(`/eval`, {
266
268
  method: 'PUT',
267
269
  body: `void (async () => {
268
- var ops = {}
269
- braid_text.sync('/${key_a}', '/${key_b}', ops)
270
+ var ac = new AbortController()
271
+ braid_text.sync('/${key_a}', '/${key_b}', {signal: ac.signal})
270
272
  await new Promise(done => setTimeout(done, 100))
271
- ops.my_unsync()
273
+ ac.abort()
272
274
  res.end('')
273
275
  })()`
274
276
  })
@@ -283,10 +285,10 @@ runTest(
283
285
  var r = await braid_fetch(`/eval`, {
284
286
  method: 'PUT',
285
287
  body: `void (async () => {
286
- var ops = {}
287
- braid_text.sync('/${key_a}', new URL('http://localhost:8889/${key_b}'), ops)
288
+ var ac = new AbortController()
289
+ braid_text.sync('/${key_a}', new URL('http://localhost:8889/${key_b}'), {signal: ac.signal})
288
290
  await new Promise(done => setTimeout(done, 100))
289
- ops.my_unsync()
291
+ ac.abort()
290
292
  res.end('')
291
293
  })()`
292
294
  })
@@ -456,14 +458,15 @@ runTest(
456
458
  method: 'PUT',
457
459
  body: `void (async () => {
458
460
  var url = new URL('http://localhost:8889/${key}')
461
+ var ac = new AbortController()
459
462
  var update = await new Promise(done => {
460
- var o = {
463
+ braid_text.get(url, {
464
+ signal: ac.signal,
461
465
  subscribe: update => {
462
- braid_text.forget(url, o)
466
+ ac.abort()
463
467
  done(update)
464
468
  }
465
- }
466
- braid_text.get(url, o)
469
+ })
467
470
  })
468
471
  res.end(update.body)
469
472
  })()`
@@ -1896,6 +1899,95 @@ runTest(
1896
1899
  'ok'
1897
1900
  )
1898
1901
 
1902
+ runTest(
1903
+ "test braid_text.delete(url)",
1904
+ async () => {
1905
+ var key = 'test-' + Math.random().toString(36).slice(2)
1906
+
1907
+ // Create a resource first
1908
+ await braid_fetch(`/${key}`, {
1909
+ method: 'PUT',
1910
+ body: 'hello there'
1911
+ })
1912
+
1913
+ // Verify it exists
1914
+ let r1 = await braid_fetch(`/${key}`)
1915
+ if ((await r1.text()) !== 'hello there') return 'Resource not created properly'
1916
+
1917
+ // Delete using braid_text.delete(url)
1918
+ var r = await braid_fetch(`/eval`, {
1919
+ method: 'PUT',
1920
+ body: `void (async () => {
1921
+ await braid_text.delete(new URL('http://localhost:8889/${key}'))
1922
+ res.end('deleted')
1923
+ })()`
1924
+ })
1925
+ if (!r.ok) return 'delete failed: ' + r.status
1926
+ if ((await r.text()) !== 'deleted') return 'delete did not complete'
1927
+
1928
+ // Verify it's deleted (should be empty)
1929
+ let r2 = await braid_fetch(`/${key}`)
1930
+ return 'got: ' + (await r2.text())
1931
+ },
1932
+ 'got: '
1933
+ )
1934
+
1935
+ runTest(
1936
+ "test braid_text.get(url) returns null for 404",
1937
+ async () => {
1938
+ // Use the /404 endpoint that always returns 404
1939
+ var r = await braid_fetch(`/eval`, {
1940
+ method: 'PUT',
1941
+ body: `void (async () => {
1942
+ var result = await braid_text.get(new URL('http://localhost:8889/404'))
1943
+ res.end(result === null ? 'null' : 'not null: ' + result)
1944
+ })()`
1945
+ })
1946
+ return await r.text()
1947
+ },
1948
+ 'null'
1949
+ )
1950
+
1951
+ runTest(
1952
+ "test braid_text.sync handles remote not existing yet",
1953
+ async () => {
1954
+ var local_key = 'test-local-' + Math.random().toString(36).slice(2)
1955
+ var remote_key = 'test-remote-' + Math.random().toString(36).slice(2)
1956
+
1957
+ // Start sync between a local key and a remote URL that doesn't exist yet
1958
+ // The sync should wait for local to create something, then push to remote
1959
+ var r = await braid_fetch(`/eval`, {
1960
+ method: 'PUT',
1961
+ body: `void (async () => {
1962
+ var ac = new AbortController()
1963
+
1964
+ // Start sync - remote doesn't exist yet
1965
+ braid_text.sync('/${local_key}', new URL('http://localhost:8889/${remote_key}'), {
1966
+ signal: ac.signal
1967
+ })
1968
+
1969
+ // Wait a bit then put something locally
1970
+ await new Promise(done => setTimeout(done, 100))
1971
+ await braid_text.put('/${local_key}', { body: 'created locally' })
1972
+
1973
+ // Wait for sync to propagate
1974
+ await new Promise(done => setTimeout(done, 200))
1975
+
1976
+ // Stop sync
1977
+ ac.abort()
1978
+
1979
+ res.end('done')
1980
+ })()`
1981
+ })
1982
+ if (!r.ok) return 'eval failed: ' + r.status
1983
+
1984
+ // Check that remote now has the content
1985
+ var r2 = await braid_fetch(`/${remote_key}`)
1986
+ return await r2.text()
1987
+ },
1988
+ 'created locally'
1989
+ )
1990
+
1899
1991
  runTest(
1900
1992
  "test getting a binary update from a subscription",
1901
1993
  async () => {