// The rkey. ⛔ A TID, because bsky requires one and says nothing when it does not get one.
//
// MEASURED 2026-09-07: our PDS served 74 posts, three with non-TID rkeys; bsky's AppView showed the
// other 71 and dropped those three silently. `app.bsky.feed.post` is keyed "tid" in the lexicon.
//
// ⛔ PURE, unlike atproto-store's `tidNow`, which keeps module-level state to guarantee monotonicity
// and therefore returns a different rkey each time it is called for the same record. This bridge
// holds no record→rkey map — it asks the sink which rkeys it already has — so the mapping has to be
// a function of the record alone or every run posts everything again.
//
// A favela record name is milliseconds since the epoch, and a TID is microseconds shifted left by a
// 10-bit clock id, so the conversion is exact and the ordering survives.
const S32 = '234567abcdefghijklmnopqrstuvwxyz'
// favela-peer/lib/name.js: 10 base32hex characters of milliseconds, then 16 of randomness.
// ⛔ The digit-stripping path below is WRONG for this shape — a-v are dropped, so the remainder is a
// meaningless number and two different names can share one. Measured on the shape room.js minted:
// 65,536 distinct names collapsed to 11,111 distinct TIDs. That is not cosmetic, because
// index.js:101 reads `have.has(tidFor(name))` as "already there" and silently never sends the post.
const B32HEX = '0123456789abcdefghijklmnopqrstuv'
const NAME_RE = /^[0-9a-v]{26}$/
const b32hex = (s) => {
let n = 0n
for (const ch of s) n = n * 32n + BigInt(B32HEX.indexOf(ch))
return n
}
/** @param {string} name a favela record name */
export function tidFor (name) {
const s = String(name == null ? '' : name)
if (NAME_RE.test(s)) {
// The timestamp becomes the TID's microseconds, so order across milliseconds is preserved
// exactly. The random half fills BOTH remaining fields: the 999 sub-millisecond microseconds we
// would otherwise waste, and the 10-bit clock identifier — which is what that field is for.
// That is ~1.02M distinct keys per millisecond rather than the 1,024 the clock id alone gives.
//
// ⚠ AND IT IS LOSSY, unavoidably. A name carries 80 bits of randomness; a TID has room for about
// 20. Two records minted in the same millisecond still collide with probability ~1/1.02M, and at
// human posting rates that never happens — but the native name remains the identity, and this is
// a projection of it. Do not reason backwards from an rkey.
const r = b32hex(s.slice(10))
const micros = b32hex(s.slice(0, 10)) * 1000n + (r % 1000n)
return encode((micros << 10n) | ((r / 1000n) % 1024n))
}
// Legacy: names were milliseconds since the epoch. Records written before favela-peer 0.0.38 are
// live on three machines and must keep mapping to the rkeys bsky already holds.
const digits = s.replace(/\D/g, '')
// ⚠ A name that is neither still has to produce a conformant key, or the record is dropped by
// bsky with no error. Falling back to a hash of the name keeps it deterministic.
return encode(((digits ? BigInt(digits) : hash53(s)) * 1000n) << 10n)
}
function encode (n) {
let s = ''
for (let i = 0; i < 13; i++) { s = S32[Number(n % 32n)] + s; n /= 32n }
return s
}
// 53 bits, deterministic, no crypto — this only has to be stable and in range, never unguessable.
function hash53 (s) {
let h = 0n
for (let i = 0; i < s.length; i++) h = (h * 131n + BigInt(s.charCodeAt(i))) % 9007199254740993n
return h
}
export default { tidFor }