braid-text 0.2.78 → 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
@@ -99,6 +99,13 @@ function create_braid_text() {
99
99
  var local_first_put
100
100
  var local_first_put_promise = new Promise(done => local_first_put = done)
101
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
+
102
109
  connect()
103
110
  async function connect() {
104
111
  if (options.on_connect) options.on_connect()
@@ -180,6 +187,7 @@ function create_braid_text() {
180
187
  await braid_text.put(a, update)
181
188
  extend_fork_point(update)
182
189
  },
190
+ on_error: handle_error
183
191
  }
184
192
  // Handle case where remote doesn't exist yet - wait for local to create it
185
193
  var remote_result = await braid_text.get(b, b_ops)
@@ -189,13 +197,9 @@ function create_braid_text() {
189
197
  disconnect()
190
198
  connect()
191
199
  }
192
- // NOTE: if remote exists, this should not return, but it might throw
200
+ // on_error will call handle_error when connection drops
193
201
  } catch (e) {
194
- if (closed) return
195
-
196
- disconnect()
197
- console.log(`disconnected, retrying in 1 second`)
198
- setTimeout(connect, 1000)
202
+ handle_error(e)
199
203
  }
200
204
  }
201
205
  }
@@ -533,19 +537,13 @@ function create_braid_text() {
533
537
  if (res.status === 404) return null
534
538
 
535
539
  if (options.subscribe) {
536
- if (options.dont_retry) {
537
- var error_happened
538
- var error_promise = new Promise((_, fail) => error_happened = fail)
539
- }
540
-
541
540
  res.subscribe(async update => {
542
541
  update.body = update.body_text
543
542
  if (update.patches)
544
543
  for (var p of update.patches) p.content = p.content_text
545
544
  await options.subscribe(update)
546
- }, e => options.dont_retry && error_happened(e))
545
+ }, e => options.on_error?.(e))
547
546
 
548
- if (options.dont_retry) return await error_promise
549
547
  return res
550
548
  } else return await res.text()
551
549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.2.78",
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/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(
@@ -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
- }