Skip to content

Commit 6dd21b2

Browse files
committed
rewrite nodejs-snowflake in Rust
Signed-off-by: Utkarsh Srivastava <srivastavautkarsh8097@gmail.com>
1 parent 6bc6b5c commit 6dd21b2

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed

src/lib.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
mod utils;
2+
3+
use crate::utils::{current_time, random};
4+
5+
use serde::{Serialize, Deserialize};
6+
use wasm_bindgen::prelude::*;
7+
8+
//// GLOBAL CONSTANTS ///////////////////////////////////////////////////////////////////////
9+
10+
/// TOTAL_BITS is the total number of bits that
11+
/// the ID can have
12+
const TOTAL_BITS: u64 = 64;
13+
14+
/// EPOCH_BITS is the total number of bits that
15+
/// are occupied by the UNIX timestamp
16+
const EPOCH_BITS: u64 = 42;
17+
18+
/// INSTANCE_ID_BITS is the total number of bits that
19+
/// are occupied by the node id
20+
const INSTANCE_ID_BITS: u64 = 12;
21+
22+
/// SEQUENCE_BITS is the total number of bits that
23+
/// are occupied by the sequence ids
24+
const SEQUENCE_BITS: u64 = 10;
25+
26+
const MAX_INSTANCE_ID: u16 = (1 << INSTANCE_ID_BITS) - 1;
27+
const MAX_SEQUENCE: u16 = (1 << SEQUENCE_BITS) - 1;
28+
29+
//// CUSTOM TYPESCRIPT EXPORTS //////////////////////////////////////////////////////////////
30+
#[wasm_bindgen(typescript_custom_section)]
31+
const CUSTOM_TS: &'static str = r#"
32+
export interface SnowflakeOpts {
33+
custom_epoch?: number;
34+
instance_id?: number;
35+
};
36+
"#;
37+
38+
//// HELPER TYPES //////////////////////////////////////////////////////////////////////////
39+
#[wasm_bindgen]
40+
extern "C" {
41+
#[wasm_bindgen(typescript_type = "SnowflakeOpts")]
42+
pub type SnowflakeOpts;
43+
}
44+
45+
impl From<SnowflakeConfig> for SnowflakeOpts {
46+
fn from(cfg: SnowflakeConfig) -> Self {
47+
Self::from(JsValue::from_serde(&cfg).unwrap())
48+
}
49+
}
50+
51+
#[derive(Serialize, Deserialize)]
52+
pub struct SnowflakeConfig {
53+
pub custom_epoch: Option<u64>,
54+
pub instance_id: Option<u16>,
55+
}
56+
57+
//// CORE IMPLEMENTATION ///////////////////////////////////////////////////////////////////
58+
#[wasm_bindgen]
59+
#[derive(Debug)]
60+
pub struct Snowflake {
61+
last_timestamp: u64,
62+
custom_epoch: u64,
63+
sequence: u16,
64+
instance_id: u16,
65+
}
66+
67+
#[wasm_bindgen]
68+
impl Snowflake {
69+
#[wasm_bindgen(constructor)]
70+
/// Constructs a Snowflake object which stores method for generation
71+
/// of a unique 64 bit time sortable ID
72+
pub fn new(opts: Option<SnowflakeOpts>) -> Result<Snowflake, JsValue> {
73+
match opts {
74+
Some(opts) => {
75+
match opts.into_serde::<SnowflakeConfig>() {
76+
Ok(opts) => {
77+
let epoch = opts.custom_epoch.unwrap_or_else(||current_time(0));
78+
let instance_id = opts.instance_id.unwrap_or_else(||random(MAX_INSTANCE_ID as f64) as u16);
79+
80+
// If passed instance ID is greater than the max then return error
81+
if instance_id > MAX_INSTANCE_ID {
82+
return Err(JsValue::from_str(&format!("instance_id must be between 0 and {}", MAX_INSTANCE_ID)));
83+
}
84+
85+
Ok(Self {
86+
last_timestamp: 0,
87+
custom_epoch: epoch,
88+
sequence: 0,
89+
instance_id: instance_id & MAX_INSTANCE_ID,
90+
})
91+
}
92+
Err(_) => {
93+
Err(JsValue::from_str("[NATIVE]: failed to parse object into SnowflakeOpts"))
94+
}
95+
}
96+
}
97+
None => {
98+
Ok(Self{
99+
last_timestamp: 0,
100+
custom_epoch: current_time(0),
101+
sequence: 0,
102+
instance_id: random(MAX_INSTANCE_ID as f64) as u16,
103+
})
104+
}
105+
}
106+
}
107+
108+
#[wasm_bindgen(js_name = getUniqueID)]
109+
/// getUniqueID generates a 64 bit unique ID
110+
///
111+
/// NOTE: This method is blocking in nature, the function also
112+
/// has theorotical limit of generating 1,024,000 IDs/sec
113+
pub fn get_unique_id(&mut self) -> u64 {
114+
let mut current_timestamp = current_time(self.custom_epoch);
115+
116+
if current_timestamp == self.last_timestamp {
117+
self.sequence = (self.sequence + 1) & MAX_SEQUENCE;
118+
119+
// If we have exhausted all of the sequence number as well
120+
if self.sequence == 0 {
121+
// Wait for roughly a millisecond
122+
while current_time(self.custom_epoch) - current_timestamp < 1 {}
123+
124+
// Update timestamp by one
125+
current_timestamp += 1;
126+
}
127+
} else {
128+
// Reset the sequence
129+
self.sequence = 0;
130+
}
131+
132+
self.last_timestamp = current_timestamp;
133+
134+
let mut id: u64 = current_timestamp << (TOTAL_BITS - EPOCH_BITS);
135+
id |= (self.instance_id as u64) << (TOTAL_BITS - EPOCH_BITS - INSTANCE_ID_BITS);
136+
id |= self.sequence as u64;
137+
138+
id
139+
}
140+
141+
#[wasm_bindgen(js_name = idFromTimestamp)]
142+
/// idFromTimestamp takes a UNIX timestamp without any offset
143+
/// and returns an ID that has timestamp set to the given timestamp
144+
pub fn id_from_timestamp(&self, timestamp: f64) -> u64 {
145+
let timestamp = timestamp.round() as u64 - self.custom_epoch;
146+
147+
let mut id: u64 = timestamp << (TOTAL_BITS - EPOCH_BITS);
148+
id |= u64::from(self.instance_id) << (TOTAL_BITS - EPOCH_BITS - INSTANCE_ID_BITS);
149+
150+
id
151+
}
152+
153+
#[wasm_bindgen(js_name = instanceID)]
154+
/// instanceID returns the current node id
155+
pub fn instance_id(&self) -> f64 {
156+
self.instance_id as f64
157+
}
158+
159+
#[wasm_bindgen(js_name = customEpoch)]
160+
/// customEpoch returns the current custom epoch
161+
pub fn custom_epoch(&self) -> f64 {
162+
self.custom_epoch as f64
163+
}
164+
}
165+
166+
#[wasm_bindgen]
167+
impl Snowflake {
168+
#[wasm_bindgen(js_name = timestampFromID)]
169+
/// timestampFromID takes a unique ID and returns the timestamp
170+
/// when the Unique ID was created
171+
pub fn timestamp_from_id(unique_id: u64, epoch_offset: f64) -> f64 {
172+
((unique_id >> (TOTAL_BITS - EPOCH_BITS)) as f64) + (epoch_offset as f64)
173+
}
174+
175+
#[wasm_bindgen(js_name = instanceIDFromID)]
176+
/// instanceIDFromID takes a unique ID and returns the instance
177+
/// ID where the unique ID was created
178+
///
179+
/// NOTE: The unique ID could be created on ANY instance
180+
pub fn instance_id_from_id(unique_id: u64) -> i32 {
181+
let bits = TOTAL_BITS - INSTANCE_ID_BITS - SEQUENCE_BITS;
182+
((unique_id << bits) >> (bits + SEQUENCE_BITS)) as i32
183+
}
184+
}

src/utils.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use js_sys::{Date, Math};
2+
3+
/// current_time takes an `adjust` parameter which is
4+
/// subtracted from the current UNIX epoch timestamp
5+
pub fn current_time(adjust: u64) -> u64 {
6+
Date::now().round() as u64 - adjust
7+
}
8+
9+
/// random generates a random number between [0, scale]
10+
pub fn random(scale: f64) -> u64 {
11+
(Math::random() * scale).round() as u64
12+
}
13+

0 commit comments

Comments
 (0)