billdogeng 1.0.0-beta.1

Official BilldogEng server SDK for Rust — Analytics, Feature Flags (remote + local eval), Surveys, Messaging, and LLM observability.
Documentation
//! murmurhash3 (32-bit, x86, seed-able) — ported verbatim from the canonical
//! web reference `packages/ab-testing/src/ABTest.ts`.
//!
//! This is the cross-platform bucketing primitive for local feature-flag
//! evaluation. The exact same algorithm runs on web / iOS / Android / every
//! server SDK so that `murmurhash3("{key}.{distinctId}") % 100` produces an
//! identical bucket everywhere — a user is deterministically in or out of a
//! rollout regardless of which platform evaluated the flag.
//!
//! Implementation notes (must not change without re-pinning the vectors):
//! - UTF-8 bytes
//! - 32-bit wrapping multiply (the JS `Math.imul`)
//! - unsigned u32 result (the JS `h1 >>> 0`)

/// Compute the 32-bit murmurhash3 of `key` with the given `seed`.
///
/// Mirrors the reference JS implementation byte-for-byte. The default seed
/// is `0` for the cross-platform bucketing contract.
pub fn murmurhash3_seed(key: &str, seed: u32) -> u32 {
    let key_bytes = key.as_bytes();
    let mut h1: u32 = seed;
    let c1: u32 = 0xcc9e_2d51;
    let c2: u32 = 0x1b87_3593;
    let len = key_bytes.len();
    let blocks = len / 4;

    for i in 0..blocks {
        // little-endian 32-bit block; JS uses `<< 24` which is signed there but
        // the subsequent imul/`>>> 0` makes the whole pipeline behave as u32.
        let mut k1: u32 = (key_bytes[i * 4] as u32)
            | ((key_bytes[i * 4 + 1] as u32) << 8)
            | ((key_bytes[i * 4 + 2] as u32) << 16)
            | ((key_bytes[i * 4 + 3] as u32) << 24);
        k1 = k1.wrapping_mul(c1);
        k1 = (k1 << 15) | (k1 >> 17);
        k1 = k1.wrapping_mul(c2);
        h1 ^= k1;
        h1 = (h1 << 13) | (h1 >> 19);
        h1 = h1.wrapping_mul(5).wrapping_add(0xe654_6b64);
    }

    let mut k1: u32 = 0;
    let remainder = len % 4;
    let offset = blocks * 4;
    if remainder == 3 {
        k1 ^= (key_bytes[offset + 2] as u32) << 16;
    }
    if remainder >= 2 {
        k1 ^= (key_bytes[offset + 1] as u32) << 8;
    }
    if remainder >= 1 {
        k1 ^= key_bytes[offset] as u32;
        k1 = k1.wrapping_mul(c1);
        k1 = (k1 << 15) | (k1 >> 17);
        k1 = k1.wrapping_mul(c2);
        h1 ^= k1;
    }

    h1 ^= len as u32;
    h1 ^= h1 >> 16;
    h1 = h1.wrapping_mul(0x85eb_ca6b);
    h1 ^= h1 >> 13;
    h1 = h1.wrapping_mul(0xc2b2_ae35);
    h1 ^= h1 >> 16;
    h1
}

/// Compute the 32-bit murmurhash3 of `key` with seed `0`.
pub fn murmurhash3(key: &str) -> u32 {
    murmurhash3_seed(key, 0)
}

/// Deterministic `0..=99` bucket for a `"{key}.{distinctId}"` style seed.
pub fn bucket_of(seed: &str) -> u32 {
    murmurhash3(seed) % 100
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn twelve_canonical_vectors() {
        let vectors: &[(&str, u32, u32)] = &[
            ("", 0, 0),
            ("a", 1009084850, 50),
            ("ab", 2613040991, 91),
            ("abc", 3017643002, 2),
            ("abcd", 1139631978, 78),
            ("hello", 613153351, 51),
            ("feature_x.user-123", 1651817416, 16),
            ("new_checkout.user-42", 2069292701, 1),
            ("dark_mode.alice", 325027212, 12),
            ("beta.bob", 3544697655, 55),
            ("my_flag.00000000-0000-0000-0000-000000000000", 2457227110, 10),
            (
                "flag.user_with_a_fairly_long_identifier_string_1234567890",
                1645539598,
                98,
            ),
        ];
        for (input, hash, mod100) in vectors {
            assert_eq!(murmurhash3(input), *hash, "hash mismatch for {input:?}");
            assert_eq!(bucket_of(input), *mod100, "bucket mismatch for {input:?}");
        }
    }
}