Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 28s
next/kernel/actor_state.erl mirrors define_registry's structure: a
2-arity fold_fn that plugs into projection:start_link/3, an
Erlang-fun stand-in for the genesis actor-state.sx projection body.
State shape:
[{ActorId, Profile}, ...]
Profile is a property list with :type, :name, :preferredUsername,
:summary, :icon, :public_keys, :moved_to, :created. Maps #{} aren't
registered in this substrate, so this matches the kernel bucket /
registry shape convention.
Folding rules per design §9.1-§9.4:
- Create{Person|Service|Group}: register profile, capturing object
fields + :published seq as :created. Duplicate Create no-overwrite.
- Update{Person|Service|Group, patch}: deep-merge :patch into
profile last-write-wins per key.
- Move: record :moved_to.
Other activity types and non-actor object Creates pass through.
Local find_keyed/has_keyed/set_keyed helpers (same gap as Step 1a:
no lists:keyfind/keymember in this substrate).
19/19 in next/tests/actor_state_pure.sh covering:
- new/0/has/2/lookup/2/actors/1 base cases
- Create for Person/Service/Group all three actor types
- Profile field capture (name, preferredUsername, public_keys, created)
- Duplicate Create no-overwrite
- Two independent actors
- Update field merge + per-key last-write-wins
- Update for unknown actor pass-through
- Move :moved_to
- Non-actor Creates pass through
- Activities without :actor pass through
- fold_fn/0 returns is_function(F, 2)
Conformance 761/761. Step-2-adjacent no-regression gate 106/106
across 6 suites (define_registry_pure, projection_pure,
projection_server, nx_kernel_multi, bootstrap_start, smoke_app_pure).
179 lines
6.0 KiB
Erlang
179 lines
6.0 KiB
Erlang
-module(actor_state).
|
|
-export([fold/2, fold_fn/0, new/0, lookup/2, has/2, actors/1,
|
|
profile_type/1, profile_name/1, profile_field/2]).
|
|
|
|
%% Actor-state projection fold — Erlang-fun stand-in for the
|
|
%% genesis `actor-state.sx` projection body. Tracks per-actor
|
|
%% profiles, key-history, and Move pointers per design §9.1-§9.4.
|
|
%%
|
|
%% State shape:
|
|
%% [{ActorId, Profile}, ...]
|
|
%%
|
|
%% Profile = [{type, person|service|group},
|
|
%% {name, Bin},
|
|
%% {preferredUsername, Bin},
|
|
%% {summary, Bin},
|
|
%% {icon, Bin},
|
|
%% {public_keys, [Key]},
|
|
%% {moved_to, ActorIdOrUrl},
|
|
%% {created, N}]
|
|
%%
|
|
%% Bridge note: the SX-source eval bridge would replace this fold
|
|
%% body once available (same gap as Step 5d-pure / Step 6c-schema-pure).
|
|
%% define_registry.erl is the structural twin.
|
|
%%
|
|
%% lists:keyfind/keymember aren't in this substrate (Step 1a noted
|
|
%% same gap), so local `find_keyed`/`has_keyed`/`set_keyed` helpers
|
|
%% handle the keyed-list ops.
|
|
|
|
new() -> [].
|
|
|
|
actors(State) -> [Id || {Id, _Profile} <- State].
|
|
|
|
has(ActorId, State) -> has_keyed(ActorId, State).
|
|
|
|
lookup(ActorId, State) ->
|
|
case find_keyed(ActorId, State) of
|
|
{ok, Profile} -> {ok, Profile};
|
|
{error, _} -> not_found
|
|
end.
|
|
|
|
%% ── Fold dispatch ───────────────────────────────────────────────
|
|
|
|
fold(Activity, State) ->
|
|
case envelope:get_field(type, Activity) of
|
|
{ok, create} -> fold_create(Activity, State);
|
|
{ok, update} -> fold_update(Activity, State);
|
|
{ok, move} -> fold_move(Activity, State);
|
|
_ -> State
|
|
end.
|
|
|
|
fold_create(Activity, State) ->
|
|
case envelope:get_field(object, Activity) of
|
|
{ok, Obj} ->
|
|
case envelope:get_field(type, Obj) of
|
|
{ok, ObjType} ->
|
|
case is_actor_type(ObjType) of
|
|
true -> register_actor(Activity, Obj, ObjType, State);
|
|
false -> State
|
|
end;
|
|
_ -> State
|
|
end;
|
|
_ -> State
|
|
end.
|
|
|
|
register_actor(Activity, Obj, ObjType, State) ->
|
|
case envelope:get_field(actor, Activity) of
|
|
{ok, ActorId} ->
|
|
case has_keyed(ActorId, State) of
|
|
true ->
|
|
State;
|
|
false ->
|
|
Created = published_seq(Activity),
|
|
Profile = build_profile(ObjType, Obj, Created),
|
|
State ++ [{ActorId, Profile}]
|
|
end;
|
|
_ -> State
|
|
end.
|
|
|
|
fold_update(Activity, State) ->
|
|
case envelope:get_field(actor, Activity) of
|
|
{ok, ActorId} ->
|
|
case find_keyed(ActorId, State) of
|
|
{ok, Profile} ->
|
|
case envelope:get_field(patch, Activity) of
|
|
{ok, Patch} ->
|
|
NewProfile = merge_patch(Profile, Patch),
|
|
set_keyed(ActorId, NewProfile, State);
|
|
_ -> State
|
|
end;
|
|
_ -> State
|
|
end;
|
|
_ -> State
|
|
end.
|
|
|
|
fold_move(Activity, State) ->
|
|
case envelope:get_field(actor, Activity) of
|
|
{ok, ActorId} ->
|
|
case find_keyed(ActorId, State) of
|
|
{ok, Profile} ->
|
|
case envelope:get_field(moved_to, Activity) of
|
|
{ok, Target} ->
|
|
NewProfile = set_keyed(moved_to, Target, Profile),
|
|
set_keyed(ActorId, NewProfile, State);
|
|
_ -> State
|
|
end;
|
|
_ -> State
|
|
end;
|
|
_ -> State
|
|
end.
|
|
|
|
%% ── Profile assembly ────────────────────────────────────────────
|
|
|
|
build_profile(ObjType, Obj, Created) ->
|
|
Base = [{type, ObjType}, {created, Created}],
|
|
Fields = [name, preferredUsername, summary, icon, public_keys],
|
|
Base ++ collect_fields(Fields, Obj).
|
|
|
|
collect_fields([], _) -> [];
|
|
collect_fields([F | Rest], Obj) ->
|
|
case envelope:get_field(F, Obj) of
|
|
{ok, V} -> [{F, V} | collect_fields(Rest, Obj)];
|
|
_ -> collect_fields(Rest, Obj)
|
|
end.
|
|
|
|
merge_patch(Profile, []) -> Profile;
|
|
merge_patch(Profile, [{K, V} | Rest]) ->
|
|
merge_patch(set_keyed(K, V, Profile), Rest);
|
|
merge_patch(Profile, _) -> Profile.
|
|
|
|
published_seq(Activity) ->
|
|
case envelope:get_field(published, Activity) of
|
|
{ok, P} -> P;
|
|
_ -> 0
|
|
end.
|
|
|
|
is_actor_type(person) -> true;
|
|
is_actor_type(service) -> true;
|
|
is_actor_type(group) -> true;
|
|
is_actor_type(_) -> false.
|
|
|
|
%% ── Profile accessors ───────────────────────────────────────────
|
|
|
|
profile_type(Profile) ->
|
|
case find_keyed(type, Profile) of
|
|
{ok, T} -> T;
|
|
_ -> nil
|
|
end.
|
|
|
|
profile_name(Profile) ->
|
|
case find_keyed(name, Profile) of
|
|
{ok, N} -> N;
|
|
_ -> nil
|
|
end.
|
|
|
|
profile_field(F, Profile) ->
|
|
case find_keyed(F, Profile) of
|
|
{ok, V} -> {ok, V};
|
|
_ -> not_found
|
|
end.
|
|
|
|
%% ── Projection integration ──────────────────────────────────────
|
|
|
|
fold_fn() ->
|
|
fun (Activity, State) -> fold(Activity, State) end.
|
|
|
|
%% ── Internal ────────────────────────────────────────────────────
|
|
|
|
has_keyed(_, []) -> false;
|
|
has_keyed(K, [{K, _} | _]) -> true;
|
|
has_keyed(K, [_ | Rest]) -> has_keyed(K, Rest).
|
|
|
|
find_keyed(_, []) -> {error, not_found};
|
|
find_keyed(K, [{K, V} | _]) -> {ok, V};
|
|
find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest).
|
|
|
|
set_keyed(K, V, []) -> [{K, V}];
|
|
set_keyed(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
|
set_keyed(K, V, [P | Rest]) -> [P | set_keyed(K, V, Rest)].
|