Current section
Files
Jump to
Current section
Files
src/snowglake.erl
-module(snowglake).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/snowglake.gleam").
-export([with_epoch/2, with_worker_id/2, with_process_id/2, with_timestamp/2, generate/1, generate_lazy/1, generate_many/2, generate_many_lazy/2, stop/1, timestamp/2, worker_id/1, process_id/1, get_now_milliseconds/0, new_generator/0, start/1]).
-export_type([generator/0, message/0, node_/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(" A module for generating unique IDs using the Twitter Snowflake algorithm.\n").
-opaque generator() :: {generator, gleam@erlang@process:subject(message())}.
-opaque message() :: {generate, gleam@erlang@process:subject(integer())} |
{generate_lazy, gleam@erlang@process:subject(integer())} |
{generate_many, integer(), gleam@erlang@process:subject(list(integer()))} |
{generate_many_lazy,
integer(),
gleam@erlang@process:subject(list(integer()))} |
shutdown.
-opaque node_() :: {node, integer(), integer(), integer(), integer(), integer()}.
-file("src/snowglake.gleam", 58).
?DOC(" Sets the epoch for the generator.\n").
-spec with_epoch(node_(), integer()) -> node_().
with_epoch(Node, Epoch) ->
{node,
Epoch,
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node),
erlang:element(6, Node)}.
-file("src/snowglake.gleam", 63).
?DOC(" Sets the worker ID for the generator.\n").
-spec with_worker_id(node_(), integer()) -> node_().
with_worker_id(Node, Worker_id) ->
{node,
erlang:element(2, Node),
Worker_id,
erlang:element(4, Node),
erlang:element(5, Node),
erlang:element(6, Node)}.
-file("src/snowglake.gleam", 68).
?DOC(" Sets the process ID for the generator.\n").
-spec with_process_id(node_(), integer()) -> node_().
with_process_id(Node, Process_id) ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
Process_id,
erlang:element(5, Node),
erlang:element(6, Node)}.
-file("src/snowglake.gleam", 74).
?DOC(
" Sets timestamp for the generator. Useful for lazy generation. It should not\n"
" be used along with normal generation.\n"
).
-spec with_timestamp(node_(), integer()) -> node_().
with_timestamp(Node, Last_ts) ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
Last_ts,
erlang:element(6, Node)}.
-file("src/snowglake.gleam", 98).
-spec format_start_error(gleam@otp@actor:start_error()) -> binary().
format_start_error(Error) ->
Detail = case Error of
init_timeout ->
<<"initialisation timed out"/utf8>>;
{init_failed, Reason} ->
<<"initialisation failed: "/utf8, Reason/binary>>;
{init_exited, Reason@1} ->
<<"initialisation exited: "/utf8,
(gleam@string:inspect(Reason@1))/binary>>
end,
<<"could not start actor: "/utf8, Detail/binary>>.
-file("src/snowglake.gleam", 128).
?DOC(
" Generates a new Snowflake ID.\n"
"\n"
" # Examples\n"
" ```gleam\n"
" import gleam/snowglake\n"
"\n"
" let epoch = 1_420_070_400_000\n"
" let worker_id = 12\n"
" let process_id = 1\n"
"\n"
" let assert Ok(generator) =\n"
" snowglake.new_generator()\n"
" |> snowglake.with_epoch(epoch)\n"
" |> snowglake.with_worker_id(worker_id)\n"
" |> snowglake.with_process_id(process_id)\n"
" |> snowglake.start()\n"
"\n"
" let id = snowglake.generate(generator)\n"
" ```\n"
).
-spec generate(generator()) -> integer().
generate(Generator) ->
gleam@otp@actor:call(
erlang:element(2, Generator),
10,
fun(Field@0) -> {generate, Field@0} end
).
-file("src/snowglake.gleam", 138).
?DOC(
" Generates a new Snowflake ID lazily.\n"
" It works like the `generate` function but it does not uses the current\n"
" timestamp, instead it consumes all the 4096 IDs of every millisecond.\n"
" It may be faster and useful in some cases than the `generate` function.\n"
" For example, to generate a batch of IDs or to generate IDs for a particular\n"
" time.\n"
).
-spec generate_lazy(generator()) -> integer().
generate_lazy(Generator) ->
gleam@otp@actor:call(
erlang:element(2, Generator),
10,
fun(Field@0) -> {generate_lazy, Field@0} end
).
-file("src/snowglake.gleam", 151).
?DOC(
" Generates many Snowflake IDs.\n"
"\n"
" # Examples\n"
" ```gleam\n"
" import gleam/snowglake\n"
"\n"
" let assert Ok(generator) = snowglake.new_generator() |> snowglake.start()\n"
" let ids = snowglake.generate_many(generator, 5000)\n"
" ```\n"
).
-spec generate_many(generator(), integer()) -> list(integer()).
generate_many(Generator, Count) ->
gleam@otp@actor:call(
erlang:element(2, Generator),
100,
fun(Reply) -> {generate_many, Count, Reply} end
).
-file("src/snowglake.gleam", 164).
?DOC(
" Generates many Snowflake IDs lazily.\n"
"\n"
" # Examples\n"
" ```gleam\n"
" import gleam/snowglake\n"
"\n"
" let assert Ok(generator) = snowglake.new_generator() |> snowglake.start()\n"
" let ids = snowglake.generate_many_lazy(generator, 5000)\n"
" ```\n"
).
-spec generate_many_lazy(generator(), integer()) -> list(integer()).
generate_many_lazy(Generator, Count) ->
gleam@otp@actor:call(
erlang:element(2, Generator),
100,
fun(Reply) -> {generate_many_lazy, Count, Reply} end
).
-file("src/snowglake.gleam", 171).
?DOC(" Stops the generator.\n").
-spec stop(generator()) -> nil.
stop(Generator) ->
gleam@otp@actor:send(erlang:element(2, Generator), shutdown).
-file("src/snowglake.gleam", 206).
?DOC(" Generates a new Snowflake ID.\n").
-spec generate_id(node_()) -> integer().
generate_id(Node) ->
_pipe = erlang:'bsl'(erlang:element(5, Node), 22),
_pipe@1 = erlang:'bor'(_pipe, erlang:'bsl'(erlang:element(3, Node), 17)),
_pipe@2 = erlang:'bor'(_pipe@1, erlang:'bsl'(erlang:element(4, Node), 12)),
erlang:'bor'(_pipe@2, erlang:element(6, Node)).
-file("src/snowglake.gleam", 254).
?DOC(" Extracts the timestamp from a Snowflake ID using the provided epoch.\n").
-spec timestamp(integer(), integer()) -> integer().
timestamp(Id, Epoch) ->
_pipe = Id,
_pipe@1 = erlang:'bsr'(_pipe, 22),
gleam@int:add(_pipe@1, Epoch).
-file("src/snowglake.gleam", 259).
?DOC(" Extracts the worker ID from a Snowflake ID.\n").
-spec worker_id(integer()) -> integer().
worker_id(Id) ->
_pipe = Id,
_pipe@1 = erlang:'band'(_pipe, 16#3E0000),
erlang:'bsr'(_pipe@1, 17).
-file("src/snowglake.gleam", 264).
?DOC(" Extracts the process ID from a Snowflake ID.\n").
-spec process_id(integer()) -> integer().
process_id(Id) ->
_pipe = Id,
_pipe@1 = erlang:'band'(_pipe, 16#1F000),
erlang:'bsr'(_pipe@1, 12).
-file("src/snowglake.gleam", 273).
-spec get_now_milliseconds() -> integer().
get_now_milliseconds() ->
Now = gleam@time@timestamp:system_time(),
{Seconds, Nanoseconds} = gleam@time@timestamp:to_unix_seconds_and_nanoseconds(
Now
),
Milliseconds_from_nanoseconds = case gleam@int:divide(Nanoseconds, 1000000) of
{ok, Value} ->
Value;
{error, _} ->
erlang:error(#{gleam_error => panic,
message => <<"unexpected divide-by-zero while computing milliseconds"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"snowglake"/utf8>>,
function => <<"get_now_milliseconds"/utf8>>,
line => 280})
end,
Ts_ms_int = (Seconds * 1000) + Milliseconds_from_nanoseconds,
Ts_ms_int.
-file("src/snowglake.gleam", 269).
?DOC(" Gets the current timestamp using erlang os:system_time/1.\n").
-spec get_timestamp(node_()) -> integer().
get_timestamp(Node) ->
_pipe = get_now_milliseconds(),
gleam@int:subtract(_pipe, erlang:element(2, Node)).
-file("src/snowglake.gleam", 53).
?DOC(" Creates a new Snowflake ID generator with default settings.\n").
-spec new_generator() -> node_().
new_generator() ->
{node, 1288834974657, 0, 0, 0, -1}.
-file("src/snowglake.gleam", 229).
?DOC(
" Sets up the node before generating a new ID.\n"
" Handles the case where multiple IDs are generated in the same millisecond.\n"
" It wait for the next millisecond if the 4096 were already generated.\n"
).
-spec setup(node_()) -> node_().
setup(Node) ->
Timestamp = begin
_pipe = Node,
get_timestamp(_pipe)
end,
case Node of
{node, _, _, _, Lts, I} when (Lts =:= Timestamp) andalso (I < 4096) ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node),
I + 1};
{node, _, _, _, Lts@1, _} when Lts@1 =:= Timestamp ->
_pipe@1 = Node,
setup(_pipe@1);
_ ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
Timestamp,
0}
end.
-file("src/snowglake.gleam", 244).
?DOC(
" Lazily sets up the node before generating a new ID.\n"
" It does not uses current timestamp to generate the next ID. It consumes\n"
" all the 4096 of every millisecond before moving to the next one.\n"
" It may be faster and useful in some cases.\n"
).
-spec lazy_setup(node_()) -> node_().
lazy_setup(Node) ->
I = case 4096 of
0 -> 0;
Gleam@denominator -> (erlang:element(6, Node) + 1) rem Gleam@denominator
end,
case I of
I@1 when (I@1 =:= 0) andalso (erlang:element(6, Node) =/= -1) ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node) + 1,
I@1};
_ ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node),
I}
end.
-file("src/snowglake.gleam", 214).
?DOC(" Generates many Snowflake IDs.\n").
-spec generate_ids(node_(), integer()) -> {node_(), list(integer())}.
generate_ids(Node, Count) ->
case Count of
0 ->
{Node, []};
_ ->
Node@1 = begin
_pipe = Node,
lazy_setup(_pipe)
end,
Id = begin
_pipe@1 = Node@1,
generate_id(_pipe@1)
end,
{Node@2, Ids} = begin
_pipe@2 = Node@1,
generate_ids(_pipe@2, Count - 1)
end,
{Node@2, lists:append(Ids, [Id])}
end.
-file("src/snowglake.gleam", 176).
?DOC(" Actor message handler.\n").
-spec handle_message(node_(), message()) -> gleam@otp@actor:next(node_(), message()).
handle_message(Node, Message) ->
case Message of
{generate, Reply} ->
Node@1 = begin
_pipe = Node,
setup(_pipe)
end,
Id = begin
_pipe@1 = Node@1,
generate_id(_pipe@1)
end,
gleam@otp@actor:send(Reply, Id),
gleam@otp@actor:continue(Node@1);
{generate_lazy, Reply@1} ->
Node@2 = begin
_pipe@2 = Node,
lazy_setup(_pipe@2)
end,
Id@1 = begin
_pipe@3 = Node@2,
generate_id(_pipe@3)
end,
gleam@otp@actor:send(Reply@1, Id@1),
gleam@otp@actor:continue(Node@2);
{generate_many, Count, Reply@2} ->
Node@3 = begin
_pipe@4 = Node,
setup(_pipe@4)
end,
{Node@4, Ids} = begin
_pipe@5 = Node@3,
generate_ids(_pipe@5, Count)
end,
gleam@otp@actor:send(
Reply@2,
begin
_pipe@6 = Ids,
lists:reverse(_pipe@6)
end
),
gleam@otp@actor:continue(Node@4);
{generate_many_lazy, Count@1, Reply@3} ->
{Node@5, Ids@1} = begin
_pipe@7 = Node,
generate_ids(_pipe@7, Count@1)
end,
gleam@otp@actor:send(
Reply@3,
begin
_pipe@8 = Ids@1,
lists:reverse(_pipe@8)
end
),
gleam@otp@actor:continue(Node@5);
shutdown ->
gleam@otp@actor:stop()
end.
-file("src/snowglake.gleam", 79).
?DOC(" Starts the generator.\n").
-spec start(node_()) -> {ok, generator()} | {error, binary()}.
start(Node) ->
case erlang:element(2, Node) > get_now_milliseconds() of
true ->
{error, <<"epoch must be in the past"/utf8>>};
false ->
Node@1 = case erlang:element(5, Node) of
0 ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
begin
_pipe = Node,
get_timestamp(_pipe)
end,
erlang:element(6, Node)};
_ ->
{node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
begin
_pipe@1 = erlang:element(5, Node),
gleam@int:subtract(_pipe@1, erlang:element(2, Node))
end,
erlang:element(6, Node)}
end,
_pipe@2 = Node@1,
_pipe@3 = gleam@otp@actor:new(_pipe@2),
_pipe@4 = gleam@otp@actor:on_message(_pipe@3, fun handle_message/2),
_pipe@5 = gleam@otp@actor:start(_pipe@4),
_pipe@6 = gleam@result:map_error(_pipe@5, fun format_start_error/1),
gleam@result:map(
_pipe@6,
fun(Started) -> {generator, erlang:element(3, Started)} end
)
end.