-module(ids@ulid). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([monotonic_generate/1, monotonic_from_timestamp/2, decode/1, from_parts/2, from_timestamp/1, generate/0, start/0]). -export_type([message/0, state/0]). -opaque message() :: {generate, gleam@erlang@process:subject({ok, binary()} | {error, binary()})} | {generate_from_timestamp, integer(), gleam@erlang@process:subject({ok, binary()} | {error, binary()})}. -opaque state() :: {state, integer(), bitstring()}. -spec monotonic_generate(gleam@erlang@process:subject(message())) -> {ok, binary()} | {error, binary()}. monotonic_generate(Channel) -> gleam@otp@actor:call(Channel, fun(Field@0) -> {generate, Field@0} end, 1000). -spec monotonic_from_timestamp( gleam@erlang@process:subject(message()), integer() ) -> {ok, binary()} | {error, binary()}. monotonic_from_timestamp(Channel, Timestamp) -> gleam@otp@actor:call( Channel, fun(Subject) -> {generate_from_timestamp, Timestamp, Subject} end, 1000 ). -spec encode_bytes(bitstring()) -> binary(). encode_bytes(Binary) -> case Binary of <> -> _pipe = <<"0123456789ABCDEFGHJKMNPQRSTVWXYZ"/utf8>>, _pipe@1 = gleam@string:to_graphemes(_pipe), _pipe@2 = gleam@list:at(_pipe@1, Index), _pipe@3 = gleam@result:unwrap(_pipe@2, <<"0"/utf8>>), gleam@string:append(_pipe@3, encode_bytes(Rest)); _ -> <<""/utf8>> end. -spec encode_base32(bitstring()) -> binary(). encode_base32(Bytes) -> To_pad = begin _pipe = Bytes, _pipe@1 = erlang:bit_size(_pipe), _pipe@2 = gleam@int:modulo(_pipe@1, 5), _pipe@3 = gleam@result:unwrap(_pipe@2, 5), _pipe@4 = gleam@int:subtract(_pipe@3, 5), _pipe@5 = gleam@int:absolute_value(_pipe@4), _pipe@6 = gleam@int:modulo(_pipe@5, 5), gleam@result:unwrap(_pipe@6, 0) end, encode_bytes(<<0:(lists:max([(To_pad), 0])), Bytes/bitstring>>). -spec decode_base32(binary()) -> {ok, bitstring()} | {error, nil}. decode_base32(Binary) -> Crockford_with_index = begin _pipe = <<"0123456789ABCDEFGHJKMNPQRSTVWXYZ"/utf8>>, _pipe@1 = gleam@string:to_graphemes(_pipe), gleam@list:index_map(_pipe@1, fun(X, I) -> {X, I} end) end, Bits = begin _pipe@2 = Binary, _pipe@3 = gleam@string:to_graphemes(_pipe@2), gleam@list:fold( _pipe@3, <<>>, fun(Acc, C) -> Index = begin _pipe@4 = Crockford_with_index, _pipe@5 = gleam@list:key_find(_pipe@4, C), gleam@result:unwrap(_pipe@5, 0) end, <> end ) end, Padding = begin _pipe@6 = Bits, _pipe@7 = erlang:bit_size(_pipe@6), _pipe@8 = gleam@int:modulo(_pipe@7, 8), gleam@result:unwrap(_pipe@8, 0) end, case Bits of <<0:Padding, Res/bitstring>> -> {ok, Res}; _ -> {error, nil} end. -spec decode(binary()) -> {ok, {integer(), bitstring()}} | {error, binary()}. decode(Ulid) -> case decode_base32(Ulid) of {ok, <>} -> {ok, {Timestamp, Randomness}}; _ -> {error, <<"Error: Decoding failed. Is a valid ULID being supplied?"/utf8>>} end. -spec from_parts(integer(), bitstring()) -> {ok, binary()} | {error, binary()}. from_parts(Timestamp, Randomness) -> case {Timestamp, Randomness} of {Time, <>} when Time =< 281474976710655 -> _pipe = <>, _pipe@1 = encode_base32(_pipe), {ok, _pipe@1}; _ -> Error = gleam@string:concat( [<<"Error: The timestamp is too large or randomness isn't 80 bits. Please use an Unix timestamp smaller than "/utf8>>, gleam@int:to_string(281474976710655), <<"."/utf8>>] ), {error, Error} end. -spec from_timestamp(integer()) -> {ok, binary()} | {error, binary()}. from_timestamp(Timestamp) -> from_parts(Timestamp, crypto:strong_rand_bytes(10)). -spec generate() -> binary(). generate() -> Timestamp = os:system_time(millisecond), case from_timestamp(Timestamp) of {ok, Ulid} -> Ulid; _ -> erlang:error(#{gleam_error => panic, message => <<"Error: Couldn't generate ULID."/utf8>>, module => <<"ids/ulid"/utf8>>, function => <<"generate"/utf8>>, line => 89}) end. -spec generate_response_ulid( integer(), gleam@erlang@process:subject({ok, binary()} | {error, binary()}), state() ) -> gleam@otp@actor:next(message(), state()). generate_response_ulid(Timestamp, Reply, State) -> case erlang:element(2, State) =:= Timestamp of true -> Randomness = begin _pipe = erlang:element(3, State), _pipe@1 = binary:decode_unsigned(_pipe), _pipe@2 = gleam@int:add(_pipe@1, 1), binary:encode_unsigned(_pipe@2) end, case from_parts(Timestamp, Randomness) of {ok, Ulid} -> gleam@otp@actor:send(Reply, {ok, Ulid}); _ -> gleam@otp@actor:send( Reply, {error, <<"Error: Couldn't generate ULID."/utf8>>} ) end, gleam@otp@actor:continue({state, Timestamp, Randomness}); false -> Randomness@1 = crypto:strong_rand_bytes(10), case from_parts(Timestamp, Randomness@1) of {ok, Ulid@1} -> gleam@otp@actor:send(Reply, {ok, Ulid@1}); _ -> gleam@otp@actor:send( Reply, {error, <<"Error: Couldn't generate ULID."/utf8>>} ) end, gleam@otp@actor:continue({state, Timestamp, Randomness@1}) end. -spec handle_msg(message(), state()) -> gleam@otp@actor:next(message(), state()). handle_msg(Msg, State) -> case Msg of {generate, Reply} -> _pipe = os:system_time(millisecond), generate_response_ulid(_pipe, Reply, State); {generate_from_timestamp, Timestamp, Reply@1} -> _pipe@1 = Timestamp, generate_response_ulid(_pipe@1, Reply@1, State) end. -spec start() -> {ok, gleam@erlang@process:subject(message())} | {error, gleam@otp@actor:start_error()}. start() -> gleam@otp@actor:start({state, 0, <<>>}, fun handle_msg/2).