// The two comparators the fold is allowed to order by. // // Every list `materialize()` produces is sorted, because two operators folding the same records // must produce the SAME index — a list in a different order is a different index, and the digest // (design §12) would report a divergence that is really just a sort. The sort keys are therefore // part of the wire contract, as much as the lexicons are, and they must mean the same thing in // every runtime, locale and language that implements it. That rules out both of JavaScript's // convenient defaults: lexical comparison of datetimes (one instant has several legal spellings) // and `String.prototype.localeCompare` (collation, which is not code-point order). // // A leaf module on purpose: `store.ts` needs these too, and it sits below `materializer.ts`. /** * Order two identifiers by UTF-16 code point — what `<` does, and what "lowest record URI" means. * * `localeCompare` is ICU COLLATION under the runtime's default locale: it ignores case at the * primary level and treats punctuation as variable, so `…/claim-A2` sorts before `…/claim-a2` in * one place and after it in another. Nothing in this repo diverges today — DIDs are lowercase and * rkeys are `claim-`, where collation and code-point order agree — but the claim tie-break * (design §6: earliest `createdAt`, lowest URI) is the one rule the design promises is * deterministic, and the URI half is what actually decides a tie between two operators that claimed * in the same millisecond. A rule whose answer depends on the daemon's locale is not that rule. */ export function compareCodePoints(left: string, right: string): number { return left === right ? 0 : left < right ? -1 : 1 } /** * The instant an RFC-3339 datetime names, in epoch milliseconds — `NaN` when it cannot be parsed. * * Validation (`validation.ts`) requires a `Z` suffix but allows OPTIONAL fractional seconds, so one * instant has several legal spellings: `2026-01-01T00:00:00Z` and `2026-01-01T00:00:00.000Z` are the * same moment, and lexically `".000Z" < "Z"`. Worse, `2026-01-01T00:00:01Z` sorts BEFORE * `2026-01-01T00:00:00.500Z`. Every record this repo writes carries millisecond precision * (`new Date().toISOString()`), so string comparison never disagreed with the clock — but a second * operator's implementation may legitimately write second-precision timestamps, and then the claim * tie-break silently diverges between the two materializers. `store.ts`'s lease-renewal guard has * always parsed rather than compared strings; this is core agreeing with itself. */ export const instant = (value: string): number => Date.parse(value) /** * Order two datetimes by the instant they name, falling back to code-point order when either is * unparseable. A value can be valid per the lexicon (`format: datetime`) and still defeat * `Date.parse`, and such a record must STILL order deterministically — every materializer has to * agree on the fold, including about records it cannot fully understand. */ export function compareInstant(left: string, right: string): number { const leftInstant = instant(left) const rightInstant = instant(right) if (Number.isNaN(leftInstant) || Number.isNaN(rightInstant)) return compareCodePoints(left, right) return leftInstant - rightInstant === 0 ? 0 : leftInstant < rightInstant ? -1 : 1 } /** `left` names an instant strictly before `right` — the lease-expiry test, on the same footing. */ export const beforeInstant = (left: string, right: string): boolean => compareInstant(left, right) < 0