| /** | |
| * Sharded-lock helper for the followed-countries watchlist primitive. | |
| * | |
| * The pre-seeded `followedCountriesShards` table holds one row per shard | |
| * id `0..SHARD_COUNT-1`. Every mutation that touches `followedCountries` | |
| * for a user maps the user's id to a shard via `userIdToShard()` below, | |
| * reads + patches that shard row, and lets Convex's per-document OCC | |
| * serialize concurrent same-user mutations. Because the shards are | |
| * pre-seeded (never lazily created), there is no nested TOCTOU window — | |
| * unlike the original `followedCountriesUserMeta` lazy-create approach | |
| * (Codex round-3 P0), which left two parallel first-ever mutations from | |
| * the same user free to both INSERT a fresh meta row. | |
| * | |
| * The hash is intentionally non-cryptographic (djb2). All it needs to do | |
| * is map distinct userIds to a roughly uniform shard distribution; a | |
| * collision between two different users is correctness-preserving (it | |
| * just adds an extra serialization point) and not a security concern. | |
| * | |
| * SHARD_COUNT is fixed at deploy time and CANNOT change without | |
| * re-seeding all rows AND draining in-flight mutations. See | |
| * `convex/constants.ts::SHARD_COUNT`. | |
| */ | |
| import { SHARD_COUNT } from "../constants"; | |
| /** | |
| * Deterministic non-cryptographic hash of a userId → shard id in | |
| * `[0, SHARD_COUNT)`. Uses djb2 with bitwise XOR to keep the hash inside | |
| * 32-bit signed range (matches the existing `hashUserIdForLog` shape in | |
| * `convex/followedCountries.ts`). | |
| * | |
| * MUST stay deterministic: changing the hash function would silently | |
| * remap every existing user to a new shard mid-deploy, briefly breaking | |
| * the OCC serialization for any user whose new shard differs from their | |
| * old shard. Treat as a frozen contract once shipped. | |
| */ | |
| export function userIdToShard(userId: string): number { | |
| let h = 5381; | |
| for (let i = 0; i < userId.length; i++) { | |
| h = ((h << 5) + h) ^ userId.charCodeAt(i); | |
| } | |
| return Math.abs(h) % SHARD_COUNT; | |
| } | |