// The relay between modems of different projects (modemRelay.ts): pairs by // the offering modem's code, one to one, the first that joins keeps it; // frames go to the partner; each room hears how its modems stand when that // changes. import { describe, expect, it } from 'vitest'; import type { EthernetFrame } from '@mdg-filius/core'; import { modemCode } from '@mdg-filius/core'; import type { RelayState } from '@mdg-filius/sim'; import { ModemRelay, type ModemAddress } from '../src/modemRelay.js'; const frame: EthernetFrame = { identity: 1, sourceMacAddress: '02:00:00:00:00:01', destinationMacAddress: 'FF:FF:FF:FF:FF:FF', payload: { kind: 'arp', packet: { operation: 'request', senderMacAddress: '02:00:00:00:00:01', senderIPAddress: '10.0.0.1', targetMacAddress: 'FF:FF:FF:FF:FF:FF', targetIPAddress: '10.0.0.2' } }, }; function relay() { const told: { project: string; states: Record }[] = []; const delivered: { to: ModemAddress; frame: EthernetFrame }[] = []; const log: string[] = []; const modems = new ModemRelay({ states: (project, states) => told.push({ project, states: { ...states } }), frame: (to, each) => delivered.push({ to, frame: each }), paired: (offer, join, paired) => log.push(`${paired ? 'paired' : 'parted'} ${offer.project}/${offer.node} ${join.project}/${join.node}`), }); return { modems, told, delivered, log }; } const code = modemCode('teacher', 'm1'); describe('the relay between modems of different projects', () => { it('pairs a modem that joins a code with the modem that offers it, and passes frames both ways', () => { const { modems, told, delivered, log } = relay(); modems.setModems('teacher', [{ node: 'm1', relay: { role: 'offer' } }]); expect(told).toEqual([{ project: 'teacher', states: { m1: 'waiting' } }]); modems.setModems('group', [{ node: 'g1', relay: { role: 'join', code } }]); expect(told.slice(1)).toEqual([ { project: 'teacher', states: { m1: 'connected' } }, { project: 'group', states: { g1: 'connected' } }, ]); expect(log).toEqual(['paired teacher/m1 group/g1']); modems.frame({ project: 'group', node: 'g1' }, frame); modems.frame({ project: 'teacher', node: 'm1' }, frame); expect(delivered.map(({ to }) => to)).toEqual([ { project: 'teacher', node: 'm1' }, { project: 'group', node: 'g1' }, ]); expect(modems.stats()).toEqual({ modems: 2, pairs: 1 }); }); it('waits for a code no running modem offers, and loses what the waiting modem passes', () => { const { modems, told, delivered } = relay(); modems.setModems('group', [{ node: 'g1', relay: { role: 'join', code: 'ZZZZZZ' } }]); modems.frame({ project: 'group', node: 'g1' }, frame); expect(told).toEqual([{ project: 'group', states: { g1: 'waiting' } }]); expect(delivered).toEqual([]); }); it('takes one modem per code, as Java takes one client: the first keeps it, the next is told it is taken', () => { const { modems, told, log } = relay(); modems.setModems('teacher', [{ node: 'm1', relay: { role: 'offer' } }]); modems.setModems('first', [{ node: 'a', relay: { role: 'join', code } }]); modems.setModems('second', [{ node: 'b', relay: { role: 'join', code } }]); expect(told.at(-1)).toEqual({ project: 'second', states: { b: 'taken' } }); // The first group starts anew: its pair goes, the waiting one takes the code; the first now waits behind it. modems.setModems('first', []); modems.setModems('first', [{ node: 'a', relay: { role: 'join', code } }]); expect(log).toEqual(['paired teacher/m1 first/a', 'parted teacher/m1 first/a', 'paired teacher/m1 second/b']); expect(told.slice(-1)).toEqual([{ project: 'first', states: { a: 'taken' } }]); }); it('keeps a pair standing when another modem joins the same code', () => { const { modems, log } = relay(); modems.setModems('first', [{ node: 'a', relay: { role: 'join', code } }]); modems.setModems('teacher', [{ node: 'm1', relay: { role: 'offer' } }]); modems.setModems('second', [{ node: 'b', relay: { role: 'join', code } }]); modems.setModems('teacher', [{ node: 'm1', relay: { role: 'offer' } }]); expect(log).toEqual(['paired teacher/m1 first/a']); }); it('parts a pair when the offering simulation stops; the joining modem waits again', () => { const { modems, told, delivered, log } = relay(); modems.setModems('teacher', [{ node: 'm1', relay: { role: 'offer' } }]); modems.setModems('group', [{ node: 'g1', relay: { role: 'join', code } }]); modems.setModems('teacher', []); expect(told.at(-1)).toEqual({ project: 'group', states: { g1: 'waiting' } }); expect(log.at(-1)).toBe('parted teacher/m1 group/g1'); modems.frame({ project: 'group', node: 'g1' }, frame); expect(delivered).toEqual([]); expect(modems.stats()).toEqual({ modems: 1, pairs: 0 }); }); it('tells a room only what changed', () => { const { modems, told } = relay(); modems.setModems('teacher', [ { node: 'm1', relay: { role: 'offer' } }, { node: 'm2', relay: { role: 'offer' } }, ]); modems.setModems('group', [{ node: 'g1', relay: { role: 'join', code } }]); expect(told).toEqual([ { project: 'teacher', states: { m1: 'waiting', m2: 'waiting' } }, { project: 'teacher', states: { m1: 'connected' } }, { project: 'group', states: { g1: 'connected' } }, ]); }); it('pairs two modems of one project, one offering, one joining its code', () => { const { modems, told } = relay(); modems.setModems('one', [ { node: 'm1', relay: { role: 'offer' } }, { node: 'm2', relay: { role: 'join', code: modemCode('one', 'm1') } }, ]); expect(told).toEqual([{ project: 'one', states: { m1: 'connected', m2: 'connected' } }]); }); });