Files
dat-thang/tests/interop/cabal/cable_js_c_live.js
T
divorce b735900f2e 0.0.002
ddat interop with dat js
cable interop with cabal js
ddat's frontend = dat-thang
cable fe = cable-thang
dillo support based on cel's dillo-dat
bootstrap node
xplatform support

Signed-off-by: divo <418@cryspace.space>
2026-10-19 00:17:02 -04:00

185 lines
7.2 KiB
JavaScript

'use strict'
// One SDL/C Cable client and one Cabal Core JavaScript client share a local
// Hyperswarm bootstrap node, discover one another, and exchange chat records.
const assert = require('assert')
const { spawn } = require('child_process')
const crypto = require('crypto')
const dgram = require('dgram')
const fs = require('fs')
const os = require('os')
const path = require('path')
const readline = require('readline')
const { startHyperswarm, loadCabalCore } = require('./cable_hyperswarm')
const binary = path.resolve(process.argv[2] || 'build/linux/test_cable_thang')
const { Cabal, requireCabal } = loadCabalCore()
const DHT = requireCabal('hyperdht')
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cable-js-c-live-'))
const roomKey = crypto.randomBytes(32).toString('hex')
const address = 'cabal://' + roomKey
const cHome = path.join(root, 'c-client')
const jsHome = path.join(root, 'js-client')
const cLines = []
const cWaiters = []
let cClient = null
let cabal = null
let bootstrapper = null
let relay = null
function freeUdpPort () {
return new Promise((resolve, reject) => {
const socket = dgram.createSocket('udp4')
socket.once('error', reject)
socket.bind(0, '127.0.0.1', () => {
const port = socket.address().port
socket.close(err => err ? reject(err) : resolve(port))
})
})
}
function ready (instance) {
return new Promise((resolve, reject) => instance.ready(err => err ? reject(err) : resolve()))
}
function waitFor (predicate, description, timeoutMs = 90000) {
const started = Date.now()
return new Promise((resolve, reject) => {
const timer = setInterval(() => {
if (Date.now() - started > timeoutMs) {
clearInterval(timer)
reject(new Error('timeout waiting for ' + description))
return
}
try {
const value = predicate()
if (value) { clearInterval(timer); resolve(value) }
} catch (err) { clearInterval(timer); reject(err) }
}, 50)
})
}
function cLine (line) {
cLines.push(line)
process.stdout.write('C> ' + line + '\n')
for (let i = cWaiters.length - 1; i >= 0; i--) {
if (cWaiters[i].predicate(line)) {
const waiter = cWaiters.splice(i, 1)[0]
clearTimeout(waiter.timer)
waiter.resolve(line)
}
}
}
function waitC (predicate, description, timeoutMs = 90000) {
const existing = cLines.find(predicate)
if (existing) return Promise.resolve(existing)
return new Promise((resolve, reject) => {
const waiter = { predicate, resolve, reject, timer: null }
waiter.timer = setTimeout(() => {
const index = cWaiters.indexOf(waiter)
if (index >= 0) cWaiters.splice(index, 1)
reject(new Error('timeout waiting for C client ' + description))
}, timeoutMs)
cWaiters.push(waiter)
})
}
async function main () {
fs.mkdirSync(cHome, { recursive: true })
fs.mkdirSync(jsHome, { recursive: true })
const port = await freeUdpPort()
const relayPort = await freeUdpPort()
const jsPort = await freeUdpPort()
const cPort = await freeUdpPort()
bootstrapper = DHT.bootstrapper(port, '127.0.0.1', { host: '127.0.0.1' })
await bootstrapper.ready()
relay = new DHT({ bootstrap: ['127.0.0.1:' + port], host: '127.0.0.1', port: relayPort,
ephemeral: false, firewalled: false })
await relay.ready()
const endpoint = '127.0.0.1:' + relayPort
process.env.CABLE_BOOTSTRAP = endpoint
process.env.CABLE_DHT_HOST = '127.0.0.1'
process.env.CABLE_DHT_ADVERTISE_HOST = '127.0.0.1'
process.env.CABLE_WAIT_DHT = '1'
console.log('Local Hyperswarm bootstrap listening at ' + endpoint)
cabal = Cabal(jsHome, roomKey)
await ready(cabal)
if (process.env.CABLE_DEBUG) {
const key = await new Promise((resolve, reject) => cabal.getLocalKey((err, value) => err ? reject(err) : resolve(value)))
console.log('JS test storage: ' + jsHome)
console.log('JS Cabal user key: ' + key)
}
const jsPeers = new Set()
const jsMessages = []
cabal.on('peer-added', key => { jsPeers.add(key); console.log('JS peer up: ' + key) })
cabal.on('peer-dropped', key => jsPeers.delete(key))
cabal.kcore.api.messages.events.on('message', message => {
const value = message && message.value
const content = value && value.content
if (value && value.type === 'chat/text' && content && typeof content.text === 'string') {
jsMessages.push(content.text)
console.log('JS chat: ' + content.text)
}
})
cClient = spawn(binary, [address], {
cwd: process.cwd(),
env: {
...process.env,
HOME: cHome,
SDL_VIDEODRIVER: 'dummy',
SDL_AUDIODRIVER: 'dummy',
CABLE_BOOTSTRAP: endpoint,
CABLE_DHT_HOST: '127.0.0.1',
CABLE_DHT_PORT: String(cPort)
},
stdio: ['pipe', 'pipe', 'inherit']
})
cClient.stdin.on('error', () => {})
cClient.once('error', err => {
for (const waiter of cWaiters) { clearTimeout(waiter.timer); waiter.reject(err) }
cWaiters.length = 0
})
readline.createInterface({ input: cClient.stdout }).on('line', cLine)
const cReady = await waitC(line => line.startsWith('C_READY '), 'S7-backed Cabal room startup')
const cFeedKey = cReady.slice('C_READY '.length)
await startHyperswarm(cabal, { host: '127.0.0.1', port: jsPort,
advertiseHost: '127.0.0.1', waitForBootstrap: true })
await Promise.all([
waitFor(() => jsPeers.size > 0, 'C client discovered by JS Cabal Core'),
waitC(line => line === 'PEER_UP', 'JS client discovered by C Cable')
])
const jsKey = await new Promise((resolve, reject) =>
cabal.getLocalKey((err, key) => err ? reject(err) : resolve(key)))
assert.notStrictEqual(jsKey, cFeedKey, 'independent Cabal users must have distinct feed keys')
assert(jsPeers.has(cFeedKey), 'JS Cabal Core must identify the C client feed')
await new Promise((resolve, reject) => cabal.publishNick('js-peer', err => err ? reject(err) : resolve()))
await new Promise((resolve, reject) => cabal.publish({
type: 'chat/text', content: { channel: 'default', text: 'message from cabal js' }
}, err => err ? reject(err) : resolve()))
await waitC(line => line.includes('message from cabal js'), 'JS-to-C message delivery')
cClient.stdin.write('SEND message from sdl c\n')
await waitFor(() => jsMessages.includes('message from sdl c'), 'C-to-JS Hypercore feed replication')
console.log('C SDL / Cabal JS Hyperswarm discovery and bidirectional chat: PASS')
}
main().catch(err => { console.error(err.stack || err); process.exitCode = 1 }).finally(async () => {
if (cClient && cClient.exitCode === null) {
try { cClient.stdin.write('QUIT\n') } catch (_) {}
await Promise.race([new Promise(resolve => cClient.once('exit', resolve)), new Promise(resolve => setTimeout(resolve, 2000))])
if (cClient.exitCode === null) cClient.kill('SIGTERM')
}
if (cabal) {
if (cabal._swarm && cabal._swarm.__shutdown) { try { await cabal._swarm.__shutdown() } catch (_) {} }
try { await cabal.close() } catch (_) {}
}
if (relay) { try { await relay.destroy() } catch (_) {} }
if (bootstrapper) { try { await bootstrapper.destroy() } catch (_) {} }
const resolved = path.resolve(root)
if (resolved.startsWith(path.resolve(os.tmpdir()) + path.sep) && path.basename(resolved).startsWith('cable-js-c-live-'))
fs.rmSync(resolved, { recursive: true, force: true })
})