braid-text 0.2.78 ā 0.2.80
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 +12 -13
- package/package.json +1 -1
- package/test/test.js +306 -0
- package/test/tests.js +44 -10
- package/.claude/settings.local.json +0 -14
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,10 @@ function create_braid_text() {
|
|
|
189
197
|
disconnect()
|
|
190
198
|
connect()
|
|
191
199
|
}
|
|
192
|
-
|
|
200
|
+
options.on_res?.(remote_result)
|
|
201
|
+
// on_error will call handle_error when connection drops
|
|
193
202
|
} catch (e) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
disconnect()
|
|
197
|
-
console.log(`disconnected, retrying in 1 second`)
|
|
198
|
-
setTimeout(connect, 1000)
|
|
203
|
+
handle_error(e)
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
}
|
|
@@ -533,19 +538,13 @@ function create_braid_text() {
|
|
|
533
538
|
if (res.status === 404) return null
|
|
534
539
|
|
|
535
540
|
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
541
|
res.subscribe(async update => {
|
|
542
542
|
update.body = update.body_text
|
|
543
543
|
if (update.patches)
|
|
544
544
|
for (var p of update.patches) p.content = p.content_text
|
|
545
545
|
await options.subscribe(update)
|
|
546
|
-
}, e => options.
|
|
546
|
+
}, e => options.on_error?.(e))
|
|
547
547
|
|
|
548
|
-
if (options.dont_retry) return await error_promise
|
|
549
548
|
return res
|
|
550
549
|
} else return await res.text()
|
|
551
550
|
}
|
package/package.json
CHANGED
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
|
|
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 === '
|
|
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
|
-
'
|
|
34
|
+
'hi'
|
|
42
35
|
)
|
|
43
36
|
|
|
44
37
|
runTest(
|
|
@@ -1988,6 +1981,47 @@ runTest(
|
|
|
1988
1981
|
'created locally'
|
|
1989
1982
|
)
|
|
1990
1983
|
|
|
1984
|
+
runTest(
|
|
1985
|
+
"test braid_text.sync on_res callback",
|
|
1986
|
+
async () => {
|
|
1987
|
+
var local_key = 'test-local-' + Math.random().toString(36).slice(2)
|
|
1988
|
+
var remote_key = 'test-remote-' + Math.random().toString(36).slice(2)
|
|
1989
|
+
|
|
1990
|
+
// Create the remote resource first
|
|
1991
|
+
var r = await braid_fetch(`/${remote_key}`, {
|
|
1992
|
+
method: 'PUT',
|
|
1993
|
+
body: 'remote content'
|
|
1994
|
+
})
|
|
1995
|
+
if (!r.ok) return 'put failed: ' + r.status
|
|
1996
|
+
|
|
1997
|
+
// Start sync with on_res callback and verify it gets called
|
|
1998
|
+
var r = await braid_fetch(`/eval`, {
|
|
1999
|
+
method: 'PUT',
|
|
2000
|
+
body: `void (async () => {
|
|
2001
|
+
var ac = new AbortController()
|
|
2002
|
+
var got_res = false
|
|
2003
|
+
|
|
2004
|
+
braid_text.sync('/${local_key}', new URL('http://localhost:8889/${remote_key}'), {
|
|
2005
|
+
signal: ac.signal,
|
|
2006
|
+
on_res: (response) => {
|
|
2007
|
+
got_res = response && typeof response.headers !== 'undefined'
|
|
2008
|
+
}
|
|
2009
|
+
})
|
|
2010
|
+
|
|
2011
|
+
// Wait for sync to establish and on_res to be called
|
|
2012
|
+
await new Promise(done => setTimeout(done, 200))
|
|
2013
|
+
|
|
2014
|
+
ac.abort()
|
|
2015
|
+
res.end(got_res ? 'on_res called' : 'on_res not called')
|
|
2016
|
+
})()`
|
|
2017
|
+
})
|
|
2018
|
+
if (!r.ok) return 'eval failed: ' + r.status
|
|
2019
|
+
|
|
2020
|
+
return await r.text()
|
|
2021
|
+
},
|
|
2022
|
+
'on_res called'
|
|
2023
|
+
)
|
|
2024
|
+
|
|
1991
2025
|
runTest(
|
|
1992
2026
|
"test getting a binary update from a subscription",
|
|
1993
2027
|
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
|
-
}
|