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>
185 lines
7.2 KiB
JavaScript
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 })
|
|
})
|