/** * NTP short format (RFC 5905 §6): an unsigned 32-bit, 16.16 fixed-point count * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 §9.2.2) * carries each packet's UTC sending instant in this format, so the value wraps * to 0 every 2^16 s (~18.2 h). * * Plain numeric order on these values breaks at every wrap: a retained * `Math.max` stays at the pre-wrap value and every later instant compares as * older. Order send times only with {@link compareNtpShort}, which applies * RFC 1982 serial-number arithmetic over the field's width. */ /** Width in bits of the MMTP `timestamp` field (NTP short). */ export const NTP_SHORT_BITS = 32 /** Number of distinct NTP-short values; the field wraps to 0 here. */ export const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS /** * RFC 1982 §3.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is * below this are ordered; exactly this far apart they are not ordered at all. * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time) * for the comparison to give their true order. */ export const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1) /** * Serial order of one NTP-short value relative to another. * * `'unordered'` is RFC 1982's undefined case: the values are exactly * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is * never folded into `'before'` or `'after'`; each caller decides what an * unorderable pair means for it. */ export type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered' /** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */ export function isNtpShort(value: number): boolean { return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS } /** * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a` * is the later instant, `'before'` when it is the earlier one. * * The answer is the true order only while the two instants are less than * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell * later from earlier, and the result names whichever direction is nearer. * This is not a total order (it is not transitive across the whole space), so * it must never be used to sort. * * @throws {RangeError} if either operand is not an NTP-short value */ export function compareNtpShort(a: number, b: number): NtpShortOrder { if (!isNtpShort(a) || !isNtpShort(b)) { throw new RangeError( `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`, ) } if (a === b) return 'equal' // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both // operands are below 2^32, so every step is exact in a double. const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS if (forward < NTP_SHORT_HALF_RANGE) return 'after' if (forward > NTP_SHORT_HALF_RANGE) return 'before' return 'unordered' }