'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 })