braid-text 0.2.77 → 0.2.79

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 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,22 @@ 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
+
102
+ function handle_error(_e) {
103
+ if (closed) return
104
+ disconnect()
105
+ console.log(`disconnected, retrying in 1 second`)
106
+ setTimeout(connect, 1000)
107
+ }
108
+
87
109
  connect()
88
110
  async function connect() {
89
111
  if (options.on_connect) options.on_connect()
@@ -91,9 +113,7 @@ function create_braid_text() {
91
113
  if (closed) return
92
114
 
93
115
  var ac = new AbortController()
94
- var disconnect_cbs = [() => ac.abort()]
95
-
96
- disconnect = () => disconnect_cbs.forEach(cb => cb())
116
+ disconnect = () => ac.abort()
97
117
 
98
118
  try {
99
119
  // fork-point
@@ -142,9 +162,11 @@ function create_braid_text() {
142
162
 
143
163
  // local -> remote
144
164
  var a_ops = {
165
+ signal: ac.signal,
145
166
  subscribe: update => {
146
167
  update.signal = ac.signal
147
168
  braid_text.put(b, update).then((x) => {
169
+ local_first_put()
148
170
  extend_fork_point(update)
149
171
  }).catch(e => {
150
172
  if (e.name === 'AbortError') {
@@ -155,28 +177,31 @@ function create_braid_text() {
155
177
  }
156
178
  if (resource.meta.fork_point)
157
179
  a_ops.parents = resource.meta.fork_point
158
- disconnect_cbs.push(() => braid_text.forget(a, a_ops))
159
180
  braid_text.get(a, a_ops)
160
181
 
161
182
  // remote -> local
162
183
  var b_ops = {
184
+ signal: ac.signal,
163
185
  dont_retry: true,
164
186
  subscribe: async update => {
165
187
  await braid_text.put(a, update)
166
188
  extend_fork_point(update)
167
189
  },
190
+ on_error: handle_error
191
+ }
192
+ // Handle case where remote doesn't exist yet - wait for local to create it
193
+ var remote_result = await braid_text.get(b, b_ops)
194
+ if (remote_result === null) {
195
+ // Remote doesn't exist yet, wait for local to put something
196
+ await local_first_put_promise
197
+ disconnect()
198
+ connect()
168
199
  }
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)
200
+ // on_error will call handle_error when connection drops
172
201
  } catch (e) {
173
- if (closed) return
174
-
175
- disconnect()
176
- console.log(`disconnected, retrying in 1 second`)
177
- setTimeout(connect, 1000)
202
+ handle_error(e)
178
203
  }
179
- }
204
+ }
180
205
  }
181
206
  }
182
207
 
@@ -468,15 +493,9 @@ function create_braid_text() {
468
493
 
469
494
  // Handle URL - make a DELETE request
470
495
  if (key instanceof URL) {
471
- options.my_abort = new AbortController()
472
- if (options.signal)
473
- options.signal.addEventListener('abort', () =>
474
- options.my_abort.abort())
475
-
476
496
  var params = {
477
497
  method: 'DELETE',
478
- signal: options.my_abort.signal,
479
- retry: () => true,
498
+ signal: options.signal,
480
499
  }
481
500
  for (var x of ['headers', 'peer'])
482
501
  if (options[x] != null) params[x] = options[x]
@@ -502,33 +521,29 @@ function create_braid_text() {
502
521
  if (key instanceof URL) {
503
522
  if (!options) options = {}
504
523
 
505
- options.my_abort = new AbortController()
506
-
507
524
  var params = {
508
- signal: options.my_abort.signal,
525
+ signal: options.signal,
509
526
  subscribe: !!options.subscribe,
510
527
  heartbeats: 120,
511
528
  }
512
- if (!options.dont_retry) params.retry = () => true
529
+ if (!options.dont_retry) {
530
+ params.retry = (res) => res.status !== 404
531
+ }
513
532
  for (var x of ['headers', 'parents', 'version', 'peer'])
514
533
  if (options[x] != null) params[x] = options[x]
515
534
 
516
535
  var res = await braid_fetch(key.href, params)
517
536
 
518
- if (options.subscribe) {
519
- if (options.dont_retry) {
520
- var error_happened
521
- var error_promise = new Promise((_, fail) => error_happened = fail)
522
- }
537
+ if (res.status === 404) return null
523
538
 
539
+ if (options.subscribe) {
524
540
  res.subscribe(async update => {
525
541
  update.body = update.body_text
526
542
  if (update.patches)
527
543
  for (var p of update.patches) p.content = p.content_text
528
544
  await options.subscribe(update)
529
- }, e => options.dont_retry && error_happened(e))
545
+ }, e => options.on_error?.(e))
530
546
 
531
- if (options.dont_retry) return await error_promise
532
547
  return res
533
548
  } else return await res.text()
534
549
  }
@@ -603,6 +618,8 @@ function create_braid_text() {
603
618
 
604
619
  options.my_last_sent_version = x.version
605
620
  resource.simpleton_clients.add(options)
621
+ options.signal?.addEventListener('abort', () =>
622
+ resource.simpleton_clients.delete(options))
606
623
  } else {
607
624
 
608
625
  if (options.accept_encoding?.match(/updates\s*\((.*)\)/)?.[1].split(',').map(x=>x.trim()).includes('dt')) {
@@ -650,22 +667,12 @@ function create_braid_text() {
650
667
  }
651
668
 
652
669
  resource.clients.add(options)
670
+ options.signal?.addEventListener('abort', () =>
671
+ resource.clients.delete(options))
653
672
  }
654
673
  }
655
674
  }
656
675
 
657
- braid_text.forget = async (key, options) => {
658
- if (!options) throw new Error('options is required')
659
-
660
- if (key instanceof URL) return options.my_abort.abort()
661
-
662
- let resource = (typeof key == 'string') ? await get_resource(key) : key
663
-
664
- if (options.merge_type != "dt")
665
- resource.simpleton_clients.delete(options)
666
- else resource.clients.delete(options)
667
- }
668
-
669
676
  braid_text.put = async (key, options) => {
670
677
  if (options.version) {
671
678
  validate_version_array(options.version)
@@ -677,14 +684,9 @@ function create_braid_text() {
677
684
  }
678
685
 
679
686
  if (key instanceof URL) {
680
- options.my_abort = new AbortController()
681
- if (options.signal)
682
- options.signal.addEventListener('abort', () =>
683
- options.my_abort.abort())
684
-
685
687
  var params = {
686
688
  method: 'PUT',
687
- signal: options.my_abort.signal,
689
+ signal: options.signal,
688
690
  retry: () => true,
689
691
  }
690
692
  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.77",
3
+ "version": "0.2.79",
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/test.js ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Unified test runner - can run in console mode (Node.js) or browser mode (server)
4
+ const http = require('http')
5
+ const {fetch: braid_fetch} = require('braid-http')
6
+ const defineTests = require('./tests.js')
7
+
8
+ // Parse command line arguments
9
+ const args = process.argv.slice(2)
10
+ const mode = args.includes('--browser') || args.includes('-b') ? 'browser' : 'console'
11
+ const portArg = args.find(arg => arg.startsWith('--port='))?.split('=')[1]
12
+ || args.find(arg => !arg.startsWith('-') && !isNaN(arg))
13
+ const port = portArg || 8889
14
+ const filterArg = args.find(arg => arg.startsWith('--filter='))?.split('=')[1]
15
+ || args.find(arg => arg.startsWith('--grep='))?.split('=')[1]
16
+
17
+ // Show help if requested
18
+ if (args.includes('--help') || args.includes('-h')) {
19
+ console.log(`
20
+ Usage: node test.js [options]
21
+
22
+ Options:
23
+ --browser, -b Start server for browser testing (default: console mode)
24
+ --port=PORT Specify port number (default: 8889)
25
+ PORT Port number as positional argument
26
+ --filter=PATTERN Only run tests matching pattern (case-insensitive)
27
+ --grep=PATTERN Alias for --filter
28
+ --help, -h Show this help message
29
+
30
+ Examples:
31
+ node test.js # Run all tests in console
32
+ node test.js --filter="sync" # Run only tests with "sync" in name
33
+ node test.js --grep="digest" # Run only tests with "digest" in name
34
+ node test.js --browser # Start browser test server
35
+ node test.js --browser --port=9000
36
+ node test.js -b 9000 # Short form with port
37
+ `)
38
+ process.exit(0)
39
+ }
40
+
41
+ // ============================================================================
42
+ // Shared Server Code
43
+ // ============================================================================
44
+
45
+ function createTestServer(options = {}) {
46
+ const {
47
+ port = 8889,
48
+ runTests = false,
49
+ logRequests = false
50
+ } = options
51
+
52
+ const braid_text = require(`${__dirname}/../index.js`)
53
+ braid_text.db_folder = `${__dirname}/test_db_folder`
54
+
55
+ const braid_text2 = braid_text.create_braid_text()
56
+ braid_text2.db_folder = null
57
+
58
+ const server = http.createServer(async (req, res) => {
59
+ if (logRequests) {
60
+ console.log(`${req.method} ${req.url}`)
61
+ }
62
+
63
+ // Free the CORS
64
+ braid_text.free_cors(res)
65
+ if (req.method === 'OPTIONS') return
66
+
67
+ if (req.url.startsWith('/have_error')) {
68
+ res.statusCode = 569
69
+ return res.end('error')
70
+ }
71
+
72
+ if (req.url.startsWith('/404')) {
73
+ res.statusCode = 404
74
+ return res.end('Not Found')
75
+ }
76
+
77
+ if (req.url.startsWith('/eval')) {
78
+ var body = await new Promise(done => {
79
+ var chunks = []
80
+ req.on('data', chunk => chunks.push(chunk))
81
+ req.on('end', () => done(Buffer.concat(chunks)))
82
+ })
83
+ try {
84
+ eval('' + body)
85
+ } catch (error) {
86
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
87
+ res.end(`Error: ${error.message}`)
88
+ }
89
+ return
90
+ }
91
+
92
+ if (req.url.startsWith('/test.html')) {
93
+ let parts = req.url.split(/[\?&=]/g)
94
+
95
+ if (parts[1] === 'check') {
96
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" })
97
+ return res.end(JSON.stringify({
98
+ checking: parts[2],
99
+ result: (await braid_text.get(parts[2])) != null
100
+ }))
101
+ } else if (parts[1] === 'dt_create_bytes_big_name') {
102
+ try {
103
+ braid_text.dt_create_bytes('x'.repeat(1000000) + '-0', [], 0, 0, 'hi')
104
+ return res.end(JSON.stringify({ ok: true }))
105
+ } catch (e) {
106
+ return res.end(JSON.stringify({ ok: false, error: '' + e }))
107
+ }
108
+ } else if (parts[1] === 'dt_create_bytes_many_names') {
109
+ try {
110
+ braid_text.dt_create_bytes('hi-0', new Array(1000000).fill(0).map((x, i) => `x${i}-0`), 0, 0, 'hi')
111
+ return res.end(JSON.stringify({ ok: true }))
112
+ } catch (e) {
113
+ return res.end(JSON.stringify({ ok: false, error: '' + e }))
114
+ }
115
+ }
116
+
117
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" })
118
+ require("fs").createReadStream(`${__dirname}/test.html`).pipe(res)
119
+ return
120
+ }
121
+
122
+ // Serve tests.js file for browser
123
+ if (req.url.startsWith('/tests.js')) {
124
+ res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" })
125
+ require("fs").createReadStream(`${__dirname}/tests.js`).pipe(res)
126
+ return
127
+ }
128
+
129
+ // Now serve the collaborative text!
130
+ braid_text.serve(req, res)
131
+ })
132
+
133
+ return {
134
+ server,
135
+ start: () => new Promise((resolve) => {
136
+ server.listen(port, 'localhost', () => {
137
+ if (runTests) {
138
+ console.log(`Test server running on http://localhost:${port}`)
139
+ } else {
140
+ console.log(`serving: http://localhost:${port}/test.html`)
141
+ }
142
+ resolve()
143
+ })
144
+ }),
145
+ port
146
+ }
147
+ }
148
+
149
+ // ============================================================================
150
+ // Console Test Mode (Node.js)
151
+ // ============================================================================
152
+
153
+ async function runConsoleTests() {
154
+ // Test tracking
155
+ let totalTests = 0
156
+ let passedTests = 0
157
+ let failedTests = 0
158
+
159
+ // Handle unhandled rejections during tests (some tests intentionally cause errors)
160
+ const unhandledRejections = []
161
+ process.on('unhandledRejection', (reason, promise) => {
162
+ // Collect but don't crash - some tests intentionally trigger errors
163
+ unhandledRejections.push({ reason, promise })
164
+ })
165
+
166
+ // Node.js test runner implementation
167
+ // Store tests to run sequentially instead of in parallel
168
+ const testsToRun = []
169
+
170
+ function runTest(testName, testFunction, expectedResult) {
171
+ // Apply filter if specified
172
+ if (filterArg && !testName.toLowerCase().includes(filterArg.toLowerCase())) {
173
+ return // Skip this test
174
+ }
175
+
176
+ totalTests++
177
+ testsToRun.push({ testName, testFunction, expectedResult })
178
+ }
179
+
180
+ // Create a braid_fetch wrapper that points to localhost
181
+ function createBraidFetch(baseUrl) {
182
+ return async (url, options = {}) => {
183
+ const fullUrl = url.startsWith('http') ? url : `${baseUrl}${url}`
184
+ return braid_fetch(fullUrl, options)
185
+ }
186
+ }
187
+
188
+ console.log('Starting braid-text tests...\n')
189
+
190
+ // Create and start the test server
191
+ const testServer = createTestServer({
192
+ port,
193
+ runTests: true,
194
+ logRequests: false
195
+ })
196
+
197
+ await testServer.start()
198
+
199
+ // Create braid_fetch bound to test server
200
+ const testBraidFetch = createBraidFetch(`http://localhost:${port}`)
201
+
202
+ // Load the real diamond-types module for Node.js
203
+ const { Doc, OpLog } = require('@braid.org/diamond-types-node')
204
+
205
+ // Define globals needed for some tests
206
+ global.Doc = Doc
207
+ global.OpLog = OpLog
208
+ global.dt_p = Promise.resolve() // No initialization needed for Node.js version
209
+ global.fetch = testBraidFetch
210
+ global.AbortController = AbortController
211
+ global.crypto = require('crypto').webcrypto
212
+
213
+ // Run all tests
214
+ defineTests(runTest, testBraidFetch)
215
+
216
+ // Run tests sequentially (not in parallel) to avoid conflicts
217
+ for (const { testName, testFunction, expectedResult } of testsToRun) {
218
+ try {
219
+ const result = await testFunction()
220
+ if (result == expectedResult) {
221
+ passedTests++
222
+ console.log(`āœ“ ${testName}`)
223
+ } else {
224
+ failedTests++
225
+ console.log(`āœ— ${testName}`)
226
+ console.log(` Expected: ${expectedResult}`)
227
+ console.log(` Got: ${result}`)
228
+ }
229
+ } catch (error) {
230
+ failedTests++
231
+ console.log(`āœ— ${testName}`)
232
+ console.log(` Error: ${error.message || error}`)
233
+ }
234
+ }
235
+
236
+ // Print summary
237
+ console.log('\n' + '='.repeat(50))
238
+ console.log(`Total: ${totalTests} | Passed: ${passedTests} | Failed: ${failedTests}`)
239
+ console.log('='.repeat(50))
240
+
241
+ // Clean up test database folder
242
+ console.log('Cleaning up test database folder...')
243
+ const fs = require('fs')
244
+ const path = require('path')
245
+ const testDbPath = path.join(__dirname, 'test_db_folder')
246
+ try {
247
+ if (fs.existsSync(testDbPath)) {
248
+ fs.rmSync(testDbPath, { recursive: true, force: true })
249
+ console.log('Test database folder removed')
250
+ }
251
+ } catch (err) {
252
+ console.log(`Warning: Could not remove test database folder: ${err.message}`)
253
+ }
254
+
255
+ // Force close the server and all connections
256
+ console.log('Closing server...')
257
+ testServer.server.close(() => {
258
+ console.log('Server closed callback - calling process.exit()')
259
+ process.exit(failedTests > 0 ? 1 : 0)
260
+ })
261
+
262
+ // Also close all active connections if the method exists (Node 18.2+)
263
+ if (typeof testServer.server.closeAllConnections === 'function') {
264
+ console.log('Closing all connections...')
265
+ testServer.server.closeAllConnections()
266
+ }
267
+
268
+ // Fallback: force exit after a short delay even if server hasn't fully closed
269
+ console.log('Setting 200ms timeout fallback...')
270
+ setTimeout(() => {
271
+ console.log('Timeout reached - calling process.exit()')
272
+ process.exit(failedTests > 0 ? 1 : 0)
273
+ }, 200)
274
+ }
275
+
276
+ // ============================================================================
277
+ // Browser Test Mode (Server)
278
+ // ============================================================================
279
+
280
+ async function runBrowserMode() {
281
+ const testServer = createTestServer({
282
+ port,
283
+ runTests: false,
284
+ logRequests: true
285
+ })
286
+
287
+ await testServer.start()
288
+ }
289
+
290
+ // ============================================================================
291
+ // Main Entry Point
292
+ // ============================================================================
293
+
294
+ async function main() {
295
+ if (mode === 'browser') {
296
+ await runBrowserMode()
297
+ } else {
298
+ await runConsoleTests()
299
+ }
300
+ }
301
+
302
+ // Run the appropriate mode
303
+ main().catch(err => {
304
+ console.error('Fatal error:', err)
305
+ process.exit(1)
306
+ })
package/test/tests.js CHANGED
@@ -14,31 +14,24 @@ runTest(
14
14
  })
15
15
  if (!r.ok) return 'got: ' + r.status
16
16
 
17
- var r1p = braid_fetch(`/eval`, {
17
+ var r1 = await braid_fetch(`/eval`, {
18
18
  method: 'PUT',
19
19
  body: `void (async () => {
20
20
  var x = await new Promise(done => {
21
21
  braid_text.get(new URL('http://localhost:8889/${key}'), {
22
22
  subscribe: update => {
23
- if (update.body_text === 'yo') done(update.body_text)
23
+ if (update.body_text === 'hi') done(update.body_text)
24
24
  }
25
25
  })
26
26
  })
27
27
  res.end(x)
28
28
  })()`
29
29
  })
30
-
31
- var r2 = await braid_fetch(`/${key}`, {
32
- method: 'PUT',
33
- body: 'yo'
34
- })
35
-
36
- var r1 = await r1p
37
30
  if (!r1.ok) return 'got: ' + r.status
38
31
 
39
32
  return await r1.text()
40
33
  },
41
- 'yo'
34
+ 'hi'
42
35
  )
43
36
 
44
37
  runTest(
@@ -50,13 +43,14 @@ runTest(
50
43
  method: 'PUT',
51
44
  body: `void (async () => {
52
45
  var count = 0
53
- var ops
54
- braid_text.sync('/${key}', new URL('http://localhost:8889/have_error'), ops = {
46
+ var ac = new AbortController()
47
+ braid_text.sync('/${key}', new URL('http://localhost:8889/have_error'), {
48
+ signal: ac.signal,
55
49
  on_connect: () => {
56
50
  count++
57
51
  if (count === 2) {
58
52
  res.end('it reconnected!')
59
- ops.my_unsync()
53
+ ac.abort()
60
54
  }
61
55
  }
62
56
  })
@@ -201,13 +195,14 @@ runTest(
201
195
  method: 'PUT',
202
196
  body: `void (async () => {
203
197
  var count = 0
204
- var ops
205
- braid_text.sync('/${key_a}', new URL('http://localhost:8889/have_error'), ops = {
198
+ var ac = new AbortController()
199
+ braid_text.sync('/${key_a}', new URL('http://localhost:8889/have_error'), {
200
+ signal: ac.signal,
206
201
  on_connect: () => {
207
202
  count++
208
203
  if (count === 2) {
209
204
  res.end('it reconnected!')
210
- ops.my_unsync()
205
+ ac.abort()
211
206
  }
212
207
  }
213
208
  })
@@ -265,10 +260,10 @@ runTest(
265
260
  var r = await braid_fetch(`/eval`, {
266
261
  method: 'PUT',
267
262
  body: `void (async () => {
268
- var ops = {}
269
- braid_text.sync('/${key_a}', '/${key_b}', ops)
263
+ var ac = new AbortController()
264
+ braid_text.sync('/${key_a}', '/${key_b}', {signal: ac.signal})
270
265
  await new Promise(done => setTimeout(done, 100))
271
- ops.my_unsync()
266
+ ac.abort()
272
267
  res.end('')
273
268
  })()`
274
269
  })
@@ -283,10 +278,10 @@ runTest(
283
278
  var r = await braid_fetch(`/eval`, {
284
279
  method: 'PUT',
285
280
  body: `void (async () => {
286
- var ops = {}
287
- braid_text.sync('/${key_a}', new URL('http://localhost:8889/${key_b}'), ops)
281
+ var ac = new AbortController()
282
+ braid_text.sync('/${key_a}', new URL('http://localhost:8889/${key_b}'), {signal: ac.signal})
288
283
  await new Promise(done => setTimeout(done, 100))
289
- ops.my_unsync()
284
+ ac.abort()
290
285
  res.end('')
291
286
  })()`
292
287
  })
@@ -456,14 +451,15 @@ runTest(
456
451
  method: 'PUT',
457
452
  body: `void (async () => {
458
453
  var url = new URL('http://localhost:8889/${key}')
454
+ var ac = new AbortController()
459
455
  var update = await new Promise(done => {
460
- var o = {
456
+ braid_text.get(url, {
457
+ signal: ac.signal,
461
458
  subscribe: update => {
462
- braid_text.forget(url, o)
459
+ ac.abort()
463
460
  done(update)
464
461
  }
465
- }
466
- braid_text.get(url, o)
462
+ })
467
463
  })
468
464
  res.end(update.body)
469
465
  })()`
@@ -1929,6 +1925,62 @@ runTest(
1929
1925
  'got: '
1930
1926
  )
1931
1927
 
1928
+ runTest(
1929
+ "test braid_text.get(url) returns null for 404",
1930
+ async () => {
1931
+ // Use the /404 endpoint that always returns 404
1932
+ var r = await braid_fetch(`/eval`, {
1933
+ method: 'PUT',
1934
+ body: `void (async () => {
1935
+ var result = await braid_text.get(new URL('http://localhost:8889/404'))
1936
+ res.end(result === null ? 'null' : 'not null: ' + result)
1937
+ })()`
1938
+ })
1939
+ return await r.text()
1940
+ },
1941
+ 'null'
1942
+ )
1943
+
1944
+ runTest(
1945
+ "test braid_text.sync handles remote not existing yet",
1946
+ async () => {
1947
+ var local_key = 'test-local-' + Math.random().toString(36).slice(2)
1948
+ var remote_key = 'test-remote-' + Math.random().toString(36).slice(2)
1949
+
1950
+ // Start sync between a local key and a remote URL that doesn't exist yet
1951
+ // The sync should wait for local to create something, then push to remote
1952
+ var r = await braid_fetch(`/eval`, {
1953
+ method: 'PUT',
1954
+ body: `void (async () => {
1955
+ var ac = new AbortController()
1956
+
1957
+ // Start sync - remote doesn't exist yet
1958
+ braid_text.sync('/${local_key}', new URL('http://localhost:8889/${remote_key}'), {
1959
+ signal: ac.signal
1960
+ })
1961
+
1962
+ // Wait a bit then put something locally
1963
+ await new Promise(done => setTimeout(done, 100))
1964
+ await braid_text.put('/${local_key}', { body: 'created locally' })
1965
+
1966
+ // Wait for sync to propagate
1967
+ await new Promise(done => setTimeout(done, 200))
1968
+
1969
+ // Stop sync
1970
+ ac.abort()
1971
+
1972
+ res.end('done')
1973
+ })()`
1974
+ })
1975
+ if (!r.ok) return 'eval failed: ' + r.status
1976
+
1977
+ // Check that remote now has the content
1978
+ var r2 = await braid_fetch(`/${remote_key}`)
1979
+ return await r2.text()
1980
+ },
1981
+ 'created locally'
1982
+ )
1983
+
1932
1984
  runTest(
1933
1985
  "test getting a binary update from a subscription",
1934
1986
  async () => {
@@ -1,14 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(node test/test.js:*)",
5
- "Bash(node:*)",
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)\")",
8
- "Bash(git push)",
9
- "Bash(npm publish:*)"
10
- ],
11
- "deny": [],
12
- "ask": []
13
- }
14
- }