-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 => <>, 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.