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>
163 lines
6.1 KiB
JavaScript
163 lines
6.1 KiB
JavaScript
'use strict'
|
|
|
|
// Exercise the APIs that cabal-cli 15.x uses, with two independent local
|
|
// Cabal clients connected by the same live Hypercore replication streams.
|
|
const assert = require('assert')
|
|
const fs = require('fs')
|
|
const os = require('os')
|
|
const path = require('path')
|
|
const EventEmitter = require('events')
|
|
const Module = require('module')
|
|
const Client = require(process.env.CABAL_CLIENT_MODULE || 'cabal-client')
|
|
|
|
function once (emitter, event, predicate = () => true, timeoutMs = 12000) {
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
emitter.removeListener(event, listener)
|
|
reject(new Error(`timed out waiting for Cabal ${event}`))
|
|
}, timeoutMs)
|
|
function listener (value) {
|
|
if (!predicate(value)) return
|
|
clearTimeout(timer)
|
|
emitter.removeListener(event, listener)
|
|
resolve(value)
|
|
}
|
|
emitter.on(event, listener)
|
|
})
|
|
}
|
|
|
|
function close (client, details) {
|
|
return new Promise((resolve, reject) => client.removeCabal(details.key, err => err ? reject(err) : resolve()))
|
|
}
|
|
|
|
function waitForUserName (details, name, timeoutMs = 12000) {
|
|
return new Promise((resolve, reject) => {
|
|
const deadline = Date.now() + timeoutMs
|
|
const poll = () => {
|
|
const user = Object.values(details.getUsers()).find(candidate => candidate.name === name)
|
|
if (user) return resolve(user)
|
|
if (Date.now() >= deadline) return reject(new Error(`${name} nickname did not replicate`))
|
|
setTimeout(poll, 25)
|
|
}
|
|
poll()
|
|
})
|
|
}
|
|
|
|
function createCliScreen (client) {
|
|
const clientPath = require.resolve(process.env.CABAL_CLIENT_MODULE || 'cabal-client')
|
|
const cliDirectory = path.resolve(path.dirname(clientPath), '..', '..')
|
|
const screenPath = path.join(cliDirectory, 'neat-screen.js')
|
|
const originalLoad = Module._load
|
|
function fakeNeatLog () {
|
|
const input = new EventEmitter()
|
|
input.cursor = 0
|
|
input.rawLine = () => ''
|
|
input.set = () => {}
|
|
return {
|
|
input,
|
|
render: () => {},
|
|
clear: () => {},
|
|
use: plugin => {
|
|
const state = {}
|
|
plugin(state, new EventEmitter())
|
|
}
|
|
}
|
|
}
|
|
Module._load = function (request, parent, isMain) {
|
|
if (request === 'neat-log') return fakeNeatLog
|
|
return originalLoad.call(this, request, parent, isMain)
|
|
}
|
|
try {
|
|
const NeatScreen = require(screenPath)
|
|
return new NeatScreen({
|
|
client,
|
|
frontendConfig: { messageTimeformat: '%H:%M:%S', messageIndent: 'nick' }
|
|
})
|
|
} finally {
|
|
Module._load = originalLoad
|
|
}
|
|
}
|
|
|
|
async function main () {
|
|
const root = fs.mkdtempSync(path.join(process.env.CABAL_TEST_DIR || os.tmpdir(), 'cabal-cli-features-'))
|
|
const sourceClient = new Client({ config: { dbdir: path.join(root, 'alice'), preferredPort: 0 } })
|
|
const peerClient = new Client({ config: { dbdir: path.join(root, 'bob'), preferredPort: 0 } })
|
|
let alice, bob, aliceStream, bobStream, screen
|
|
try {
|
|
alice = await sourceClient.createCabal()
|
|
const roomKey = alice.key
|
|
assert.match(roomKey, /^[0-9a-f]{64}$/i, 'create room returns a Cabal room key')
|
|
|
|
bob = await peerClient.addCabal(`cabal://${roomKey}`)
|
|
aliceStream = alice.core.replicate(true)
|
|
bobStream = bob.core.replicate(false)
|
|
aliceStream.on('error', () => {})
|
|
bobStream.on('error', () => {})
|
|
aliceStream.pipe(bobStream).pipe(aliceStream)
|
|
|
|
const aliceNick = once(alice, 'publish-nick', update => update.name === 'alice')
|
|
alice.processLine('/nick alice')
|
|
await aliceNick
|
|
const aliceChannel = once(alice, 'channel-join', update => update.channel === 'lobby')
|
|
alice.processLine('/join lobby')
|
|
await aliceChannel
|
|
const secondChannel = once(alice, 'channel-join', update => update.channel === 'garden')
|
|
alice.processLine('/join garden')
|
|
await secondChannel
|
|
assert(alice.getJoinedChannels().includes('lobby'))
|
|
assert(alice.getJoinedChannels().includes('garden'))
|
|
|
|
screen = createCliScreen(sourceClient)
|
|
const cliChannels = alice.getChannels({ includePM: true, onlyJoined: true })
|
|
alice.focusChannel(cliChannels[0])
|
|
screen.neat.input.emit('ctrl-n')
|
|
assert.strictEqual(alice.getCurrentChannel(), cliChannels[1], 'the cabal-cli Ctrl+N handler advances channels')
|
|
screen.neat.input.emit('ctrl-n')
|
|
assert.strictEqual(alice.getCurrentChannel(), cliChannels[2], 'the cabal-cli Ctrl+N handler continues the cycle')
|
|
alice.focusChannel('lobby')
|
|
|
|
const bobJoined = once(alice, 'channel-join', update => update.channel === 'lobby' && !update.isLocal)
|
|
const bobChannel = once(bob, 'channel-join', update => update.channel === 'lobby')
|
|
bob.processLine('/join lobby')
|
|
await bobChannel
|
|
await bobJoined
|
|
const bobNick = once(bob, 'publish-nick', update => update.name === 'bob')
|
|
bob.processLine('/nick bob')
|
|
await bobNick
|
|
await waitForUserName(alice, 'bob')
|
|
|
|
const fromAlice = 'hello from Alice in the lobby'
|
|
const gotAlice = once(bob, 'new-message', update =>
|
|
update.channel === 'lobby' && update.message.value.content.text === fromAlice)
|
|
alice.processLine(fromAlice)
|
|
await gotAlice
|
|
|
|
// Verify the actual room stream carries the published nickname record to Bob.
|
|
await waitForUserName(bob, 'alice')
|
|
|
|
const fromBob = 'hello back from Bob'
|
|
const gotBob = once(alice, 'new-message', update =>
|
|
update.channel === 'lobby' && update.message.value.content.text === fromBob)
|
|
bob.processLine(fromBob)
|
|
await gotBob
|
|
|
|
aliceStream.destroy()
|
|
bobStream.destroy()
|
|
aliceStream = bobStream = null
|
|
process.stdout.write(`cabal-cli 15.x room, /join, /nick, Ctrl+N channel cycle, and two-peer chat: PASS room=cabal://${roomKey}\n`)
|
|
} finally {
|
|
if (aliceStream) aliceStream.destroy()
|
|
if (bobStream) bobStream.destroy()
|
|
if (screen) {
|
|
if (screen.updateTimer) clearTimeout(screen.updateTimer)
|
|
if (alice && screen._updateHandler && screen._updateHandler[alice.key])
|
|
alice.removeListener('update', screen._updateHandler[alice.key])
|
|
}
|
|
if (alice) await close(sourceClient, alice)
|
|
if (bob) await close(peerClient, bob)
|
|
fs.rmSync(root, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
main().catch(err => { console.error(err); process.exitCode = 1 })
|