/
snowflake.gleam
106 lines (93 loc) · 3.15 KB
/
snowflake.gleam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//// A module for generating Snowflake IDs.
import gleam/int
import gleam/string
import gleam/result
import gleam/erlang
import gleam/otp/actor.{type Next}
import gleam/erlang/process.{type Subject}
@external(erlang, "binary", "encode_unsigned")
fn encode_unsigned(i: Int) -> BitArray
/// The messages handled by the actor.
/// The actor shouldn't be called directly so this type is opaque.
pub opaque type Message {
Generate(reply_with: Subject(Int))
}
/// The internal state of the actor.
/// The state keeps track of the Snowflake parts.
pub opaque type State {
State(epoch: Int, last_time: Int, machine_id: Int, idx: Int)
}
/// Starts a Snowflake generator.
pub fn start(machine_id: Int) -> Result(Subject(Message), String) {
start_with_epoch(machine_id, 0)
}
/// Starts a Snowflake generator with an epoch offset.
pub fn start_with_epoch(
machine_id: Int,
epoch: Int,
) -> Result(Subject(Message), String) {
case epoch > erlang.system_time(erlang.Millisecond) {
True -> Error("Error: Epoch can't be larger than current time.")
False ->
State(epoch: epoch, last_time: 0, machine_id: machine_id, idx: 0)
|> actor.start(handle_msg)
|> result.map_error(fn(err) {
"Error: Couldn't start actor. Reason: " <> string.inspect(err)
})
}
}
/// Generates a Snowflake ID using the given channel.
///
/// ### Usage
/// ```gleam
/// import ids/snowflake
///
/// let assert Ok(channel) = snowflake.start(machine_id: 1)
/// let id: Int = snowflake.generate(channel)
///
/// let discord_epoch = 1_420_070_400_000
/// let assert Ok(d_channel) = snowflake.start_with_epoch(machine_id: 1, epoch: discord_epoch)
/// let discord_id: Int = snowflake.generate(d_channel)
/// ```
pub fn generate(channel: Subject(Message)) -> Int {
actor.call(channel, Generate, 1000)
}
/// Decodes a Snowflake ID into #(timestamp, machine_id, idx).
pub fn decode(snowflake: Int) -> Result(#(Int, Int, Int), String) {
case encode_unsigned(snowflake) {
<<timestamp:int-size(42), machine_id:int-size(10), idx:int-size(12)>> ->
Ok(#(timestamp, machine_id, idx))
_other -> Error("Error: Couldn't decode snowflake id.")
}
}
/// Actor message handler.
fn handle_msg(msg: Message, state: State) -> Next(Message, State) {
case msg {
Generate(reply) -> {
let new_state = update_state(state)
let snowflake =
new_state.last_time
|> int.bitwise_shift_left(22)
|> int.bitwise_or({
new_state.machine_id
|> int.bitwise_shift_left(12)
|> int.bitwise_or(new_state.idx)
})
actor.send(reply, snowflake)
actor.continue(new_state)
}
}
}
/// Prepares the state for generation.
/// Handles incrementing if id is being generated in the same millisecond.
/// Calls itself recursively to make a millisecond pass if all 4096 ids have been generated in the past millisecond.
fn update_state(state: State) -> State {
let now =
erlang.system_time(erlang.Millisecond)
|> int.subtract(state.epoch)
case state.last_time {
lt if lt == now && state.idx < 4095 -> State(..state, idx: state.idx + 1)
lt if lt == now -> update_state(state)
_other -> State(..state, last_time: now)
}
}