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.
- package/.claude/settings.local.json +3 -1
- package/index.js +66 -42
- package/package.json +1 -1
- package/test/fuzz-test.js +12 -8
- package/test/tests.js +108 -16
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
521
|
+
signal: options.signal,
|
|
489
522
|
subscribe: !!options.subscribe,
|
|
490
523
|
heartbeats: 120,
|
|
491
524
|
}
|
|
492
|
-
if (!options.dont_retry)
|
|
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.
|
|
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
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
|
|
273
|
+
var ac = new AbortController()
|
|
274
274
|
await new Promise(async done => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
braid_text.sync('/${key}', new URL('http://localhost:8889/have_error'),
|
|
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
|
-
|
|
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
|
|
205
|
-
braid_text.sync('/${key_a}', new URL('http://localhost:8889/have_error'),
|
|
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
|
-
|
|
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
|
|
269
|
-
braid_text.sync('/${key_a}', '/${key_b}',
|
|
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
|
-
|
|
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
|
|
287
|
-
braid_text.sync('/${key_a}', new URL('http://localhost:8889/${key_b}'),
|
|
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
|
-
|
|
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
|
-
|
|
463
|
+
braid_text.get(url, {
|
|
464
|
+
signal: ac.signal,
|
|
461
465
|
subscribe: update => {
|
|
462
|
-
|
|
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 () => {
|