snowflake

A snowflake ID generator library for MoonBit

moon add RabitLogic/snowflake@0.1.2
Download zip
Version
0.1.2
License
Apache-2.0
Last updated
27 days ago
Downloads
8
README

#RabitLogic/snowflake

A Snowflake ID generator library for MoonBit.

#ID Structure

0 | 41 bits timestamp | 10 bits node | 12 bits sequence

FieldSizeDescription
Unused1 bitMSB, always 0 (ensures a positive Int64)
Timestamp41 bitsMilliseconds since a custom epoch (≈69 years)
Node ID10 bitsWorker/node identifier (0–1023)
Sequence12 bitsMonotonic sequence within the same ms (0–4095)

Customise bit allocation via Node::new(node, epoch, nb=?, sb=?) as long as nb + sb ≤ 22.

#Quick Start

///|
let node = Node::new_default(1L).unwrap()

///|
let id : Snowflake = node.generate()

///|
println(id) // 67817059108864
println(id.to_base36()) // 1w7p0bgwwb28

#API Reference

#Types

TypeDescription
NodeID generator — call .generate() to produce IDs
SnowflakeA snowflake ID value — supports encoding, decoding, field extraction

#Generator — Node

FunctionDescription
Node::new(node, epoch, nb?, sb?)Custom epoch (2026-01-01 default) & bit widths
Node::new_default(node)Create with default epoch (2026-01-01)
node.generate()SnowflakeGenerate a new monotonically increasing ID
node.extract_time(id)Int64Extract timestamp using this node's epoch & layout
node.extract_node_id(id)Int64Extract node ID using this node's bit layout
node.extract_step(id)Int64Extract sequence using this node's bit layout

Thread safety: Node is not Send/Sync. Wrap in Mutex(Node) for shared access.

#ID value — Snowflake

Construction:

///|
let id = Snowflake::new(67817059108864L) // from raw Int64

Field extraction (uses default 10‑node / 12‑step layout):

MethodReturns
id.time()Unix‑ms timestamp (Int64)
id.node_id()Node/worker ID (Int64)
id.step()Sequence number (Int64)

For custom bit widths, use node.extract_*(id) instead.

Encoding (instance methods):

FormatMethod
Binary (base‑2)id.to_base2()
[z‑base‑32]id.to_base32()
Base‑36id.to_base36()
[Bitcoin Base‑58]id.to_base58()
Base‑64id.to_base64()
UTF‑8 bytes (decimal)id.to_bytes()
Big‑endian 8‑bytesid.to_int_bytes()

Parsing (static methods, return Snowflake?):

FormatMethod
Decimal stringSnowflake::from_string(s)
BinarySnowflake::from_base2(s)
z‑base‑32Snowflake::from_base32(s)
Base‑36Snowflake::from_base36(s)
Base‑58Snowflake::from_base58(s)
Base‑64Snowflake::from_base64(s)
UTF‑8 bytesSnowflake::from_bytes(b)
8‑byte big‑endianSnowflake::from_int_bytes(b)

Conversions:

///|
let raw : Int64 = id.to_int64()

///|
let id2 = Snowflake::new(raw)

Traits:

TraitBehaviour
Eq / CompareCompares by numeric value — supports ==, <, >, etc.
HashUsable in HashSet / HashMap
Show\{id} → decimal string
ToJson / FromJsonSerialises as JSON string (e.g. "67817059108864")

#Constants

///|
default_epoch // 1767225600000L (2026-01-01 00:00:00 UTC)

///|
node_bits // 10

///|
step_bits // 12

#Complete Example

///|
fn run {
let node = Node::new_default(1L).unwrap()
let id = node.generate()
let id2 = node.generate()

// Field extraction (default layout)
println("\{id.time()} \{id.node_id()} \{id.step()}")

// Field extraction (custom layout)
println(
"\{node.extract_time(id)} \{node.extract_node_id(id)} \{node.extract_step(id)}",
)

// Encoding
println(id.to_base36())
println(id.to_base58())

// Parsing
let parsed = Snowflake::from_base36(id.to_base36())
println(parsed == Some(id)) // true

// JSON serialisation
let json_str = id.to_json().stringify()
println(json_str) // "67817059108864"
}

#Production Guide

#Node ID management

Each Node must have a unique node ID (0–1023 for the default 10-bit layout). If two Node instances share the same node ID and timestamp, they will produce duplicate IDs. Strategies for assigning node IDs:

  • Static config: assign a unique ID per process at deployment time.
  • Database sequence: use an AUTO_INCREMENT column or Redis INCR.
  • Consensus: use etcd or ZooKeeper for automatic allocation.

#Clock accuracy

The generator uses @env.now() (wall‑clock time). A backward NTP adjustment may cause a brief stall while the clock catches up (the library waits for time to recover). To minimise risk:

  • Use ntpd or chronyd with minpoll no smaller than 6 (64 s).
  • Avoid manual date commands or large step adjustments.
  • Monitor system clock synchronisation in your observability pipeline.

Note: Unlike libraries that use an OS monotonic clock, this library will pause briefly if the clock jumps backwards. This is a deliberate trade‑off for portability across all MoonBit targets (Wasm, JS, native).

#Thread safety

Node is not Send/Sync. For concurrent access, wrap it:

///|
let node = Mutex(Node::new_default(1L).unwrap())

///|
let id = node.lock().generate()

#Bit width planning

The default layout (10‑node / 12‑step) supports 1024 nodes × 4096 IDs/ms. For different workloads, use Node::new(…, nb=?, sb=?):

nbsbMax nodesMax IDs/msUse case
10121 0244 096General purpose
71512832 768Many IDs, few nodes
1398 192512Many nodes, few IDs
6166465 536High‑throughput

#Epoch planning

The default epoch (2026-01-01) gives ~69 years of ID space (until ≈ 2095). If you need IDs beyond that, shift the epoch forward:

///|
let node = Node::new(1L, 1893456000000L).unwrap() // 2030-01-01

#Limitations

  • Clock monotonicity: see Clock accuracy above.
  • Max timestamp: 41‑bit field + epoch (2026-01-01) → space lasts until ≈ 2095 (~69 years). Adjust the epoch to shift the window.

#Development

# Run all tests (40 tests) moon test # Update snapshots (if any) moon test --update # Run the demo moon run cmd/main # Check coverage moon coverage analyze > uncovered.log

#Test Coverage

CategoryTests
Node creation4
ID generation5
Extraction (default)3
Extraction (Node-based)2
String / Base2 / Base364
Base32 (z-base-32)3
Base583
Base642
Big-endian bytes2
Bytes (UTF-8)1
Stress (10k IDs)1
Boundary & edge cases5
Overflow & validation4
Custom bit widths2
Total40

#License

Apache 2.0

#
Node

pub struct Node {
step : Int64
last_time : Int64
node : Int64
epoch : Int64
node_max : Int64
node_mask : Int64
step_mask : Int64
time_shift : Int
node_shift : Int
}

A generator that produces unique, time‑sorted Snowflake IDs.

#
Node::extract_node_id

fn Node::extract_node_id(self : Node, id : Snowflake) -> Int64

Extract the node/worker ID from a Snowflake using this node's bit layout.

#
Node::extract_step

fn Node::extract_step(self : Node, id : Snowflake) -> Int64

Extract the sequence number from a Snowflake using this node's bit layout.

#
Node::extract_time

fn Node::extract_time(self : Node, id : Snowflake) -> Int64

Extract the Unix‑ms timestamp from a Snowflake using this node's epoch and bit layout.

#
Node::generate

fn Node::generate(self : Node) -> Snowflake

Generate a new unique Snowflake.

To guarantee uniqueness:
  • Keep accurate system time.
  • Never run multiple nodes with the same node ID.

#
Node::new

fn Node::new(node : Int64, epoch : Int64, nb? : Int, sb? : Int) -> Node?

Create a new Node.

Parameters:
  • node : Worker ID (0 … node_max).
  • epoch : Custom epoch in ms since Unix epoch.
  • nb : Bits for the node field (default 10).
  • sb : Bits for the sequence field (default 12).

Returns None if parameters are out of range.

#
Node::new_default

fn Node::new_default(node : Int64) -> Node?

Create a Node with the default epoch and default bit widths.

#
Snowflake

pub struct Snowflake {
val : Int64
} derive(
Debug
)

A snowflake ID — a 63-bit positive integer with embedded timestamp, node ID, and sequence number.
impl Eq for Snowflake
impl Hash for Snowflake
impl Show for Snowflake
impl ToJson for Snowflake

#
Snowflake::from_base2

fn Snowflake::from_base2(s : String) -> Snowflake?

Parse from a binary string.

#
Snowflake::from_base32

fn Snowflake::from_base32(s : String) -> Snowflake?

Parse from a [z‑base‑32] string.

#
Snowflake::from_base36

fn Snowflake::from_base36(s : String) -> Snowflake?

Parse from a base‑36 string.

#
Snowflake::from_base58

fn Snowflake::from_base58(s : String) -> Snowflake?

Parse from a [Bitcoin Base‑58] string.

#
Snowflake::from_base64

fn Snowflake::from_base64(s : String) -> Snowflake?

Parse from a base‑64 string.

#
Snowflake::from_bytes

fn Snowflake::from_bytes(bytes : Bytes) -> Snowflake?

Parse from UTF‑8 bytes containing a decimal number.

#
Snowflake::from_int_bytes

fn Snowflake::from_int_bytes(bytes : Bytes) -> Snowflake?

Parse from an 8‑byte big‑endian byte array. Returns None if bytes is not exactly 8 bytes.

#
Snowflake::from_string

fn Snowflake::from_string(s : String) -> Snowflake?

Parse from a decimal string.

#
Snowflake::new

fn Snowflake::new(val : Int64) -> Snowflake

Create a Snowflake from a raw Int64.

#
Snowflake::node_id

fn Snowflake::node_id(self : Snowflake) -> Int64

Extract the node/worker ID. Uses the default 10‑bit node / 12‑bit step layout.

#
Snowflake::step

fn Snowflake::step(self : Snowflake) -> Int64

Extract the sequence number. Uses the default 12‑bit step layout.

#
Snowflake::time

fn Snowflake::time(self : Snowflake) -> Int64

Extract the Unix‑ms timestamp. Uses the default 10‑bit node / 12‑bit step layout.

#
Snowflake::to_base2

fn Snowflake::to_base2(self : Snowflake) -> String

Return a binary (base‑2) string.

#
Snowflake::to_base32

fn Snowflake::to_base32(self : Snowflake) -> String

Return a [z‑base‑32] string.

#
Snowflake::to_base36

fn Snowflake::to_base36(self : Snowflake) -> String

Return a base‑36 string.

#
Snowflake::to_base58

fn Snowflake::to_base58(self : Snowflake) -> String

Return a [Bitcoin Base‑58] string.

#
Snowflake::to_base64

fn Snowflake::to_base64(self : Snowflake) -> String

Return a base‑64 string.

#
Snowflake::to_bytes

fn Snowflake::to_bytes(self : Snowflake) -> Bytes

Return the decimal string as UTF‑8 bytes.

#
Snowflake::to_int64

fn Snowflake::to_int64(self : Snowflake) -> Int64

Convert to a raw Int64.

#
Snowflake::to_int_bytes

fn Snowflake::to_int_bytes(self : Snowflake) -> Bytes

Return an 8‑byte big‑endian encoding.

#
default_epoch

let default_epoch : Int64

Default epoch: January 1, 2026 00:00:00 UTC (in milliseconds).

The 41-bit timestamp field provides ≈69 years of ID space.

2026-01-01 → ≈2095 ← IDs remain unique until roughly here

You can pick any epoch via Node::new(node_id, your_epoch) — for example:

EpochValue (ms)ExpiresUse case
2026-01-0117672256000002095-09default
2025-01-0117356896000002094-09alternative
2010-11-04 (Twitter)12888349746572080-07interop

#
node_bits

let node_bits : Int

Number of bits allocated for the node ID.

#
step_bits

let step_bits : Int

Number of bits allocated for the sequence number.

Source Files