| 1 | // The `devport` convention, reimplemented so the panel can apply it offline. |
| 2 | // |
| 3 | // devport(1) is a shell script that hands a local project a stable block of ten |
| 4 | // ports: `BASE + (cksum(name) % BLOCKS) * SLOT`, where the name is the git |
| 5 | // repo's directory name. Nothing registers anything — the hash *is* the |
| 6 | // registry — which is what makes it reproducible here. |
| 7 | // |
| 8 | // The panel wants the question the other way round: a browser tab is sitting on |
| 9 | // http://localhost:12345, and it wants to know which tmux session that port |
| 10 | // belongs to. devport answers that with `-r`, by walking ~/Code and hashing |
| 11 | // every directory in it. We do not need the walk: the daemon already tells us |
| 12 | // each session's working directory, so hashing those few names forward and |
| 13 | // comparing blocks gives the same answer without a subprocess, a filesystem, or |
| 14 | // devport being installed at all. |
| 15 | // |
| 16 | // The one thing that must not drift is the hash. `portof()` in devport is |
| 17 | // `printf '%s' "$name" | cksum`, and POSIX cksum is CRC-32/CKSUM: the ordinary |
| 18 | // CRC-32 polynomial, unreflected, with the message length appended to the |
| 19 | // message. Any faster-looking CRC-32 (zlib's, reflected, no length) computes a |
| 20 | // different number and would point at the wrong project. |
| 21 | |
| 22 | /** First assignable port. Below this is the crowd of framework defaults. */ |
| 23 | const DEVPORT_BASE = 10240; |
| 24 | /** How many blocks exist: 2100 * 10 covers 10240-31239. */ |
| 25 | const DEVPORT_BLOCKS = 2100; |
| 26 | /** Ports per project — app, db, cache, debugger, whatever. */ |
| 27 | const DEVPORT_SLOT = 10; |
| 28 | |
| 29 | /** Lazily built, because this runs on every tab activation. @type {number[] | null} */ |
| 30 | let table = null; |
| 31 | |
| 32 | /** The CRC-32/CKSUM byte table, poly 0x04C11DB7, unreflected. */ |
| 33 | function crcTable() { |
| 34 | if (table) return table; |
| 35 | const t = new Array(256); |
| 36 | for (let i = 0; i < 256; i++) { |
| 37 | let c = i << 24; |
| 38 | for (let k = 0; k < 8; k++) c = c & 0x80000000 ? (c << 1) ^ 0x04c11db7 : c << 1; |
| 39 | t[i] = c >>> 0; |
| 40 | } |
| 41 | table = t; |
| 42 | return t; |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * POSIX `cksum`, as a number. |
| 47 | * |
| 48 | * The trailing length loop is not decoration: cksum feeds the byte count in |
| 49 | * after the data, low byte first, dropping the high zero bytes. Without it the |
| 50 | * checksums differ from the shell's for every input. |
| 51 | * |
| 52 | * @param {string} s |
| 53 | * @returns {number} |
| 54 | */ |
| 55 | function cksum(s) { |
| 56 | const t = crcTable(); |
| 57 | const bytes = new TextEncoder().encode(s); |
| 58 | let crc = 0; |
| 59 | for (const b of bytes) crc = ((crc << 8) ^ t[((crc >>> 24) ^ b) & 0xff]) >>> 0; |
| 60 | for (let n = bytes.length; n !== 0; n = Math.floor(n / 256)) { |
| 61 | crc = ((crc << 8) ^ t[((crc >>> 24) ^ (n & 0xff)) & 0xff]) >>> 0; |
| 62 | } |
| 63 | return (crc ^ 0xffffffff) >>> 0; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * The first port of the block a project name owns — what plain `devport` prints |
| 68 | * in a directory of that name. |
| 69 | * |
| 70 | * @param {string} name |
| 71 | * @returns {number} |
| 72 | */ |
| 73 | function portOf(name) { |
| 74 | return DEVPORT_BASE + (cksum(name) % DEVPORT_BLOCKS) * DEVPORT_SLOT; |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * The block a port falls in, or null when it is outside the assignable range — |
| 79 | * 3000, 5173, 8080 and every other hand-picked port land here, and they carry |
| 80 | * no project in them to find. |
| 81 | * |
| 82 | * @param {number} port |
| 83 | * @returns {number | null} |
| 84 | */ |
| 85 | function blockOf(port) { |
| 86 | if (!Number.isInteger(port)) return null; |
| 87 | if (port < DEVPORT_BASE || port >= DEVPORT_BASE + DEVPORT_BLOCKS * DEVPORT_SLOT) return null; |
| 88 | return port - ((port - DEVPORT_BASE) % DEVPORT_SLOT); |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Whether `port` is one of the ten this name owns. |
| 93 | * |
| 94 | * A block is ten ports wide and there are only 2100 blocks, so a match is |
| 95 | * evidence and not proof — two projects in ~/Code can share one. That is why |
| 96 | * this only ever *suggests* a pin rather than making one. |
| 97 | * |
| 98 | * @param {number} port |
| 99 | * @param {string} name |
| 100 | * @returns {boolean} |
| 101 | */ |
| 102 | function owns(port, name) { |
| 103 | if (!name) return false; |
| 104 | const block = blockOf(port); |
| 105 | return block !== null && block === portOf(name); |
| 106 | } |
| 107 | |
| 108 | const Devport = { BASE: DEVPORT_BASE, BLOCKS: DEVPORT_BLOCKS, SLOT: DEVPORT_SLOT, cksum, portOf, blockOf, owns }; |
| 109 | |
| 110 | if (typeof module !== "undefined" && module.exports) { |
| 111 | module.exports = Devport; |
| 112 | } |