feat(server): the relay joins modems of different projects by a code (K3)
A modem that offers a connection to another project is found by six characters derived from its project and itself (modemCode): a copy of the project has another code, so no two modems offer the same one. The room names its modems of other projects to F's relay at a start and none at a stop; the relay pairs each offer with the first modem that joins its code, one to one as Java's server modem takes one client, and passes the frames between the rooms' threads. Every client hears how each modem stands (`modems`: connected, waiting, taken, unpaired, ambiguous, detached, or unavailable without F). The document keeps the connection and the code. Measured on F with two threads: a ping from one project's notebook to the other's goes through; a third project that wants the same code reads taken, and takes it when the second stops; a deleted project's modem leaves with its room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ export * from './model/link.js';
|
||||
export * from './model/node.js';
|
||||
export * from './model/virtualFileSystem.js';
|
||||
export * from './runtime/config.js';
|
||||
export * from './runtime/modemCode.js';
|
||||
export * from './runtime/dns.js';
|
||||
export * from './runtime/engine.js';
|
||||
export * from './runtime/ipv4.js';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// A modem's code (plan FILIUS_MODEM_PLAN E1, E2): a modem that offers a
|
||||
// connection to another project is found by six characters instead of
|
||||
// Java's IP address and port. The code is derived from the project and the
|
||||
// modem, not stored: a copy of the project (handed out, duplicated) has
|
||||
// another project number and so another code, and no two modems offer the
|
||||
// same one. The editor shows it, server F pairs by it — both derive it here
|
||||
// (RemoteLinkRelay names the role; config.ts).
|
||||
//
|
||||
// The characters leave out what one misreads (0/O, 1/I); typed in, small
|
||||
// letters, spaces and dashes count as nothing.
|
||||
|
||||
export const MODEM_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
export const MODEM_CODE_LENGTH = 6;
|
||||
|
||||
const FNV_OFFSET = 0xcbf29ce484222325n;
|
||||
const FNV_PRIME = 0x100000001b3n;
|
||||
|
||||
/** The code a modem of `project` offers (FNV-1a over both, its upper 30 bits in five-bit steps). */
|
||||
export function modemCode(project: string, node: string): string {
|
||||
let hash = FNV_OFFSET;
|
||||
const text = `filius-modem\n${project}\n${node}`;
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
hash ^= BigInt(text.charCodeAt(index));
|
||||
hash = BigInt.asUintN(64, hash * FNV_PRIME);
|
||||
}
|
||||
let code = '';
|
||||
for (let index = 0; index < MODEM_CODE_LENGTH; index += 1) code += MODEM_CODE_ALPHABET[Number((hash >> BigInt(59 - 5 * index)) & 31n)];
|
||||
return code;
|
||||
}
|
||||
|
||||
const CODE_PATTERN = new RegExp(`^[${MODEM_CODE_ALPHABET}]{${MODEM_CODE_LENGTH}}$`);
|
||||
|
||||
/** A code as typed in, without spaces and dashes, in capitals; undefined when it is none. */
|
||||
export function modemCodeOf(text: string): string | undefined {
|
||||
const code = text.toUpperCase().replace(/[\s-]/g, '');
|
||||
return CODE_PATTERN.test(code) ? code : undefined;
|
||||
}
|
||||
|
||||
/** A code as people read it: two groups of three. */
|
||||
export function modemCodeText(code: string): string {
|
||||
return `${code.slice(0, 3)}-${code.slice(3)}`;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// A modem's code (modemCode.ts): six characters derived from the project and
|
||||
// the modem, the same wherever it is derived; read back as people type it.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MODEM_CODE_ALPHABET, MODEM_CODE_LENGTH, modemCode, modemCodeOf, modemCodeText } from '../src/index.js';
|
||||
|
||||
const uuid = (number: number) => `00000000-0000-4000-8000-${number.toString(16).padStart(12, '0')}`;
|
||||
|
||||
describe("a modem's code", () => {
|
||||
it('is six characters one does not misread, the same for the same modem', () => {
|
||||
const code = modemCode(uuid(1), uuid(2));
|
||||
expect(code).toMatch(new RegExp(`^[${MODEM_CODE_ALPHABET}]{${MODEM_CODE_LENGTH}}$`));
|
||||
expect(MODEM_CODE_ALPHABET).not.toMatch(/[01IO]/);
|
||||
expect(modemCode(uuid(1), uuid(2))).toBe(code);
|
||||
// Pinned: the editor and server F must derive the same code from the same project and modem.
|
||||
expect(code).toBe('VKUZ5Q');
|
||||
});
|
||||
|
||||
it('differs for a copy of the project and for another modem, and spreads over every character', () => {
|
||||
expect(modemCode(uuid(1), uuid(2))).not.toBe(modemCode(uuid(3), uuid(2)));
|
||||
expect(modemCode(uuid(1), uuid(2))).not.toBe(modemCode(uuid(1), uuid(3)));
|
||||
const codes = Array.from({ length: 20_000 }, (_, index) => modemCode(uuid(index), uuid(7)));
|
||||
// 2^30 codes: 20 000 projects meet a handful of times at most.
|
||||
expect(new Set(codes).size).toBeGreaterThan(19_990);
|
||||
for (let position = 0; position < MODEM_CODE_LENGTH; position += 1) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const code of codes) counts.set(code[position]!, (counts.get(code[position]!) ?? 0) + 1);
|
||||
expect(counts.size, `characters at ${position}`).toBe(MODEM_CODE_ALPHABET.length);
|
||||
// Each character about 625 times; none below half of that.
|
||||
expect(Math.min(...counts.values()), `rarest character at ${position}`).toBeGreaterThan(300);
|
||||
}
|
||||
});
|
||||
|
||||
it('is read as typed, without spaces, dashes and small letters, and written in two groups', () => {
|
||||
expect(modemCodeOf(' k7f-3qx ')).toBe('K7F3QX');
|
||||
expect(modemCodeOf('K7F 3QX')).toBe('K7F3QX');
|
||||
for (const text of ['', 'K7F3Q', 'K7F3QXY', 'K7F3Q0', 'K7F3QO', 'K7F3Q1', 'K7F3QI', 'K7F3Q!']) expect(modemCodeOf(text), text).toBeUndefined();
|
||||
expect(modemCodeText('K7F3QX')).toBe('K7F-3QX');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user