34UNGHWEZK2J2VHCULNFD5AQPZQPD6MXU7ZAKY4DLJ5TQSROQ4BAC AXV6OFM6O2PMOAKKIP3MLLXCPBHBDGLPIV2PU3SG3SACXXB4TVKQC 2E4H4QPHKUDVTUDO335LRPAAU7374N5EX6TJW2NKDW7JK3N4HRTQC JR3F3TQ3A7I2K5AWRK7SKAOZI5XE3ZGAS3VK4O7FKODUONQSY35QC IHBNW3GI2XB6KAWUYRLL6KDOBUNUOU3N7RRLS6BFNW6SE7ZDHLWQC 55WLMLEEVBRSTAFRZ5RGF7TOGUF5OPVCPA2TMHAQK45OUO7PA3YQC 6RQQDL46IO2ZFTJSEJREWJIMTNHOH4UBSO2VXAYNLEWNUR72OWHQC BSISJB2O2HKYGSCX6HIIMLBIXCZ66BRCZH3622G2NOQJRJ5S3HLAC M4TNRFRPHEH6T673JAMJ3CHABASCWMAJVU57HH2XEMDJCB3QPT5QC NBMKIBO6UJKXCOXXVPPENLEBYI4YOU2VCHH5KIOUGH7WJG47N4PQC J32TNYTRQJ3YTGFSI5FXW63R5ACT5SZQ6C4YSHLAZ4HXYAHAN7YQC DJ7EM5ZXZRSOBHEAA5EVZNVULJCQ7EX4DQBSK2PKPXITWJDPIUXQC EDLKGFB5NWTZTHEO6IAR5M4W4533KJI4O6673MKJNTW3JPHQZP5AC MONVI5STEDKY5ALVMEXJJXDUX6XQRKTFLP7BBNOQML3VSJEM2JAAC 2R3WFEOT3WWS6NFBBABSVRUNUPTXHFFMGPZZQOCPLTD2WB3U55HQC EVG6AOW4UUH7C6COH5XHYLPBXC3QEZR2372SE3LJCUPPEZQ3BUQQC QTLCENKPK4QOQJTHEAWAJYTJWVH7ZI5KQ2CQTMJRMA4TOMCHTVBAC R4JDMB7LL3FLA4NJEAV2DQEXII5XS5KIMG3H4YS5P6W7ZZUE7FIQC CBHKQGLDCAH2E4ZNACITBSMADOKPERFCWQPUGMH7UN5TLJXLYI4QC FAFY7PLMQPWUIKHTG7YANSBHDBWXMNUOIFBS5MUW2ALNSXDLI42QC %% manage persistent peer connectionspersistent_peer_add(Pid, Address) ->gen_server:call(Pid, {persistentPeerAdd, Address}).persistent_peer_remove(Pid, Address) ->gen_server:call(Pid, {persistentPeerRemove, Address}).persistent_peer_list(Pid) ->gen_server:call(Pid, {persistentPeerList}).
handle_call({persistentPeerRemove, Address}, _From, State) ->Db = proplists:get_value(database, State),Result = db:peer_delete(Db, Address),{reply, Result, State};handle_call({persistentPeerList}, _From, State) ->Db = proplists:get_value(database, State),Result = db:peer_list(Db),{reply, Result, State}.
{startReconnectTimer} ->%% Start periodic timer (10 seconds)TimerRef = erlang:send_after(10000, self(), {reconnectTimer}),event_loop(State#state{reconnectTimer = TimerRef});{reconnectTimer} ->%% Handle periodic reconnection attemptsNewState = attempt_peer_reconnections(State),%% Reschedule timerTimerRef = erlang:send_after(10000, self(), {reconnectTimer}),event_loop(NewState#state{reconnectTimer = TimerRef});
%% Update persistent peer database if this peer is in the listDb = State#state.db,case db:peer_get(Db, PeerAddr) of{ok, PeerInfo} ->%% Peer is in persistent list - decrease scoreCurrentScore = maps:get(score, PeerInfo, 0),Now = os:system_time(millisecond),db:peer_update(Db, PeerAddr, [{score, max(0, CurrentScore - 5)},{last_seen, Now}]),io:format("[Peer] Updated persistent peer ~s: score -5~n", [PeerAddr]);not_found ->ok % Not a persistent peer, no action neededend,
%% Update persistent peer database if this peer is in the listDb = State#state.db,case db:peer_get(Db, PeerAddr) of{ok, PeerInfo} ->%% Peer is in persistent list - reset backoff and increase scoreCurrentScore = maps:get(score, PeerInfo, 0),Now = os:system_time(millisecond),db:peer_update(Db, PeerAddr, [{score, CurrentScore + 10},{last_seen, Now},{attempt_count, 0}]),io:format("[Peer] Updated persistent peer ~s: score +10, reset backoff~n", [PeerAddr]);not_found ->ok % Not a persistent peer, no action neededend,
%% Persistent peer reconnection helpers%% Calculate exponential backoff delay in milliseconds%% Base delay is 5 seconds, max is 5 minutescalculate_backoff_delay(AttemptCount) ->BaseDelayMs = 5000, % 5 secondsMaxDelayMs = 300000, % 5 minutes%% Exponential backoff: base * 2^attempts, capped at maxDelayMs = BaseDelayMs * math:pow(2, AttemptCount),min(trunc(DelayMs), MaxDelayMs).%% Check if a peer should be reconnected based on last attempt and backoffshould_reconnect_peer(PeerInfo) ->Now = os:system_time(millisecond),LastAttempt = maps:get(last_attempt, PeerInfo, 0),AttemptCount = maps:get(attempt_count, PeerInfo, 0),BackoffDelay = calculate_backoff_delay(AttemptCount),%% Reconnect if enough time has passed since last attempt(Now - LastAttempt) >= BackoffDelay.%% Get the address string for a peer from connected peers mapget_peer_address_string(ConnPid, Peers) ->case maps:get(ConnPid, Peers, undefined) ofundefined -> undefined;PeerMeta ->case maps:get(address, PeerMeta, undefined) ofundefined -> undefined;Addr -> Addrendend.%% Check if peer address is already connectedis_peer_connected(Address, Peers) ->%% Check if any connected peer has this addresslists:any(fun({_ConnPid, PeerMeta}) ->maps:get(address, PeerMeta, undefined) =:= Addressend,maps:to_list(Peers)).%% Attempt to reconnect to persistent peers based on exponential backoffattempt_peer_reconnections(State = #state{db = Db, transportPid = TransportPid, peers = Peers}) ->%% Get all persistent peers{ok, PersistentPeers} = db:peer_list(Db),%% Filter and attempt reconnectionslists:foreach(fun(PeerInfo) ->Address = maps:get(address, PeerInfo),%% Only attempt if not already connected and backoff period has passedcase is_peer_connected(Address, Peers) oftrue ->ok; % Already connected, skipfalse ->case should_reconnect_peer(PeerInfo) oftrue ->%% Attempt reconnectionio:format("[Peer] Attempting reconnection to ~s~n", [Address]),try%% Parse address and dial[HostStr, PortStr] = string:split(Address, ":"),{ok, Host} = inet:getaddr(HostStr, inet),Port = list_to_integer(PortStr),transport:dial(TransportPid, Host, Port),%% Update last_attempt and increment attempt_countNow = os:system_time(millisecond),AttemptCount = maps:get(attempt_count, PeerInfo, 0),db:peer_update(Db, Address, [{last_attempt, Now},{attempt_count, AttemptCount + 1}])catch_:Error ->io:format("[Peer] Failed to dial ~s: ~p~n", [Address, Error])end;false ->ok % Backoff period not elapsed, skipendendend,PersistentPeers),State.
{reply, {ok, Chans}, State}.
{reply, {ok, Chans}, State};%%%%%%%%%%%%% Peers %%%%%%%%%%%%%handle_call({peerAdd, Address}, _From, [{sql, Db}, _] = State) ->Now = os:system_time(millisecond),Row = [{address, Address},{score, 0},{attempt_count, 0},{created_at, Now}],Res = case sqlite3:write(Db, peers, Row) of{rowid, RowId} -> {ok, RowId};{error, 19, _Msg} -> {error, already_exists}end,{reply, Res, State};handle_call({peerUpdate, Address, Updates}, _From, [{sql, Db}, _] = State) ->%% Updates is a proplist like [{score, 10}, {last_seen, Timestamp}]Res = case sqlite3:update(Db, peers, {address, Address}, Updates) ofok -> ok;Error -> {error, Error}end,{reply, Res, State};handle_call({peerList}, _From, [{sql, Db}, _] = State) ->[{columns, _Cols}, {rows, Rows}] = sqlite3:read_all(Db, peers),%% Expected columns: ["id", "address", "score", "last_seen", "last_attempt", "attempt_count", "created_at", "notes"]Peers = lists:map(fun({Id, Address, Score, LastSeen, LastAttempt, AttemptCount, CreatedAt, Notes}) ->#{id => Id,address => binary_to_list(Address),score => Score,last_seen => LastSeen,last_attempt => LastAttempt,attempt_count => AttemptCount,created_at => CreatedAt,notes => case Notes ofnull -> undefined;N -> binary_to_list(N)end}end,Rows),{reply, {ok, Peers}, State};handle_call({peerDelete, Address}, _From, [{sql, Db}, _] = State) ->ok = sqlite3:delete(Db, peers, {address, Address}),{reply, ok, State};handle_call({peerGet, Address}, _From, [{sql, Db}, _] = State) ->Result = case sqlite3:read(Db, peers, {address, Address}) of[{columns, _Cols}, {rows, [{Id, Addr, Score, LastSeen, LastAttempt, AttemptCount, CreatedAt, Notes}]}] ->{ok, #{id => Id,address => binary_to_list(Addr),score => Score,last_seen => LastSeen,last_attempt => LastAttempt,attempt_count => AttemptCount,created_at => CreatedAt,notes => case Notes ofnull -> undefined;N -> binary_to_list(N)end}};_ ->not_foundend,{reply, Result, State}.
users => [ColId, {public_key, blob, [unique, not_null]}, {name, text, not_null}]
users => [ColId, {public_key, blob, [unique, not_null]}, {name, text, not_null}],peers => [ColId,{address, text, [unique, not_null]},{score, integer, {default, 0}},{last_seen, integer},{last_attempt, integer},{attempt_count, integer, {default, 0}},{created_at, integer, not_null},{notes, text}]
nativeBuildInputs = with pkgs.buildPackages; [ erlang_27 rebar3 libsodium sqlite] ++ [erlang-language-platform erlfmt erlang-language-platform clang];
nativeBuildInputs = with pkgs.buildPackages; [ erlang_27 rebar3 libsodium sqlite] ++ [erlang-language-platform erlfmt clang];