dev_node_process.erl - Node Singleton Process Management
Overview
Purpose: Singleton pattern implementation for node-specific processes
Module: dev_node_process
Device Name: node-process@1.0
Pattern: Lazy initialization with persistence via local-name@1.0
This device implements the singleton pattern for processes specific to an individual node. Process definitions are configured in the node message and automatically spawned, initialized, and registered on first access.
Dependencies
- HyperBEAM:
hb_ao,hb_cache,hb_message,hb_opts,hb_util - Devices:
dev_local_name - Arweave:
ar_wallet - Includes:
include/hb.hrl
Public Functions Overview
%% Device Information
-spec info(Opts) -> DeviceInfo.
%% Process Lookup
-spec lookup(Name, Base, Req, Opts) -> {ok, Process} | {error, not_found}.Public Functions
1. info/1
-spec info(Opts) -> DeviceInfo
when
Opts :: map(),
DeviceInfo :: #{
default => HandlerFun,
excludes => [binary()]
},
HandlerFun :: fun((Name, Base, Req, Opts) -> Result).Description: Configure device to handle all requests except set and keys.
-module(dev_node_process_info_test).
-include_lib("eunit/include/eunit.hrl").
info_structure_test() ->
Info = dev_node_process:info(#{}),
?assert(maps:is_key(default, Info)),
?assert(maps:is_key(excludes, Info)),
Excludes = maps:get(excludes, Info),
?assert(lists:member(<<"set">>, Excludes)),
?assert(lists:member(<<"keys">>, Excludes)).2. lookup/4
-spec lookup(Name, Base, Req, Opts) -> {ok, Process} | {error, not_found}
when
Name :: binary(),
Base :: map(),
Req :: map(),
Opts :: map(),
Process :: map().Description: Lookup a process by name. If not found and spawn=true, spawns and registers a new process from the definition in node_processes.
- Query
local-name@1.0for registered process - If found: Load and return process from cache
- If not found and
spawn=true: Spawn new process - If not found and
spawn=false: Return error
- Create process from
node_processes/{Name}definition - Augment with node's address as scheduler/authority
- Sign with node wallet
- Initialize via POST to
/schedule - Register with
local-name@1.0
-module(dev_node_process_lookup_test).
-include_lib("eunit/include/eunit.hrl").
lookup_no_spawn_test() ->
% lookup/4 is an internal function not exported
% Verify info/1 returns expected structure
Info = dev_node_process:info(#{}),
?assert(maps:is_key(default, Info)).
lookup_spawn_test() ->
{ok, Module} = file:read_file("test/test.lua"),
Opts = #{
node_processes => #{
<<"test-process">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"scheduler-device">> => <<"scheduler@1.0">>,
<<"module">> => #{
<<"content-type">> => <<"text/x-lua">>,
<<"body">> => Module
}
}
},
priv_wallet => ar_wallet:new()
},
{ok, Process} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"test-process">>,
Opts
),
?assert(is_map(Process)),
?assert(maps:is_key(<<"device">>, Process)).
singleton_behavior_test() ->
{ok, Module} = file:read_file("test/test.lua"),
Opts = #{
node_processes => #{
<<"singleton">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => #{
<<"content-type">> => <<"text/x-lua">>,
<<"body">> => Module
}
}
},
priv_wallet => ar_wallet:new()
},
% First access spawns process
{ok, Process1} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"singleton">>,
Opts
),
% Second access returns same process
{ok, Process2} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"singleton">>,
Opts
),
?assertEqual(
hb_cache:ensure_all_loaded(Process1, Opts),
hb_cache:ensure_all_loaded(Process2, Opts)
).Process Definition Format
Basic Definition
#{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"scheduler-device">> => <<"scheduler@1.0">>,
<<"module">> => #{
<<"content-type">> => <<"text/x-lua">>,
<<"body">> => LuaCode
}
}With Additional Fields
#{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"scheduler-device">> => <<"scheduler@1.0">>,
<<"module">> => ModuleSpec,
<<"balance">> => InitialBalances,
<<"custom-field">> => CustomValue
}Node Configuration
Configuring Node Processes
NodeMsg = #{
node_processes => #{
<<"database">> => DatabaseProcessDef,
<<"cache">> => CacheProcessDef,
<<"logger">> => LoggerProcessDef
},
priv_wallet => NodeWallet
}Process Name Registration
Processes are registered with their names in local-name@1.0:
- Name: Process name (e.g.,
<<"database">>) - Value: Process ID (signed ID of spawned process)
Address Augmentation
Automatic Field Addition
The device automatically adds the node's address to:
Scheduler List:% Before augmentation
<<"scheduler">> => [<<"other-scheduler">>]
% After augmentation
<<"scheduler">> => [<<"other-scheduler">>, NodeAddress]% Before augmentation
<<"authority">> => [<<"other-authority">>]
% After augmentation
<<"authority">> => [<<"other-authority">>, NodeAddress]- Node address is moved to end of list
- Duplicates are removed
Spawn Control
Default Behavior (spawn=true)
% Automatically spawns if not found
{ok, Process} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"myprocess">>,
Opts
)Disable Spawn (spawn=false)
% Returns error if not found
Result = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
#{ <<"path">> => <<"myprocess">>, <<"spawn">> => false },
Opts
)
% Result: {error, not_found} if process doesn't existCommon Patterns
%% Configure node processes
NodeMsg = #{
node_processes => #{
<<"ledger">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"scheduler-device">> => <<"scheduler@1.0">>,
<<"module">> => #{
<<"content-type">> => <<"text/x-lua">>,
<<"body">> => LedgerCode
},
<<"balance">> => #{ Alice => 1000 }
},
<<"cache">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => CacheModule
}
},
priv_wallet => ar_wallet:new()
}.
%% Access singleton process (auto-spawns if needed)
{ok, Ledger} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"ledger">>,
NodeMsg
).
%% Execute on singleton process
{ok, Result} = hb_ao:resolve_many(
[
#{ <<"device">> => <<"node-process@1.0">> },
<<"ledger">>,
#{
<<"path">> => <<"schedule">>,
<<"method">> => <<"POST">>,
<<"body">> => TransferMessage
}
],
NodeMsg
).
%% Check process state
Balance = hb_ao:get(
<<"ledger~node-process@1.0/now/balance/", Address/binary>>,
#{ <<"device">> => <<"node-process@1.0">> },
NodeMsg
).
%% Disable auto-spawn
Result = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
#{ <<"path">> => <<"unknown">>, <<"spawn">> => false },
NodeMsg
).
% Result: {error, not_found}HTTP Integration
URL Pattern
GET /{ProcessName}~node-process@1.0
GET /{ProcessName}~node-process@1.0/{Path}
POST /{ProcessName}~node-process@1.0/{Path}Examples
# Access singleton process
GET /ledger~node-process@1.0
# Query process state
GET /ledger~node-process@1.0/now/balance
# Send message to process
POST /ledger~node-process@1.0/scheduleInitialization Flow
Spawn and Initialize
- Load Definition: Get from
node_processes/{Name} - Augment: Add node address to scheduler/authority
- Sign: Commit with node wallet
- Initialize: POST to process's
/scheduleendpoint - Register: Store ID in
local-name@1.0 - Return: Return signed process
Example Flow
% User requests: GET /ledger~node-process@1.0
% 1. Lookup in local-name: not found
% 2. Get definition from node_processes
% 3. Augment with node address
% 4. Sign with node wallet
% 5. POST to ledger/schedule to initialize
% 6. Register ledger -> ProcessID
% 7. Return processProcess Persistence
Registration
Processes are registered with dev_local_name:
dev_local_name:direct_register(
#{
<<"key">> => ProcessName,
<<"value">> => ProcessID
},
Opts
)Persistence Across Restarts
- Process IDs stored in
local-name@1.0(persistent) - Process definitions in node message
- On restart: Existing processes found via lookup
- New processes spawned on first access
Codec Configuration
Custom Spawn Codec
Opts = #{
node_process_spawn_codec => <<"ans104@1.0">>
}Default: <<"httpsig@1.0">>
Error Handling
Process Not Defined
% No definition in node_processes
{error, not_found}Registration Failure
{error, #{
<<"status">> => 500,
<<"body">> => <<"Failed to register process.">>,
<<"details">> => ErrorDetails
}}No Wallet
If priv_wallet not in options, process cannot be signed/spawned.
Use Cases
1. Ledger Service
NodeMsg = #{
node_processes => #{
<<"ledger">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => LedgerModule,
<<"balance">> => InitialBalances
}
}
}2. Logging Service
NodeMsg = #{
node_processes => #{
<<"logger">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => LoggerModule
}
}
}3. Configuration Service
NodeMsg = #{
node_processes => #{
<<"config">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => ConfigModule,
<<"settings">> => DefaultSettings
}
}
}Testing Patterns
%% Setup test environment
setup_test_node() ->
{ok, Module} = file:read_file("test/test.lua"),
#{
node_processes => #{
<<"test">> => #{
<<"device">> => <<"process@1.0">>,
<<"execution-device">> => <<"lua@5.3a">>,
<<"module">> => #{
<<"content-type">> => <<"text/x-lua">>,
<<"body">> => Module
}
}
},
priv_wallet => ar_wallet:new()
}.
%% Test process access
test_process_access() ->
Opts = setup_test_node(),
{ok, Process} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"test">>,
Opts
),
?assert(is_map(Process)).
%% Test singleton behavior
test_singleton() ->
Opts = setup_test_node(),
{ok, P1} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"test">>,
Opts
),
{ok, P2} = hb_ao:resolve(
#{ <<"device">> => <<"node-process@1.0">> },
<<"test">>,
Opts
),
?assertEqual(hb_message:id(P1, all), hb_message:id(P2, all)).References
- dev_local_name.erl - Name registration and lookup
- hb_message.erl - Message signing and ID generation
- hb_cache.erl - Process storage
- hb_ao.erl - Process initialization
- ar_wallet.erl - Wallet operations
Notes
- Singleton Pattern: One instance per name per node
- Lazy Initialization: Processes spawned on first access
- Persistence: Process IDs persist across restarts
- Auto-Augmentation: Node address automatically added
- Local Name Storage: Uses
local-name@1.0for registration - Spawn Control: Can disable auto-spawn via
spawn=false - Custom Codec: Configurable signing codec
- Initialization: Processes initialized via POST to /schedule
- Error Handling: Structured error responses
- HTTP Compatible: Works with HTTP endpoints
- Address Deduplication: Removes duplicate addresses
- Definition Required: Must exist in
node_processes - Wallet Required: Node wallet required for signing
- Cache Integration: Process stored and retrieved from cache
- Path Resolution: Supports nested path access on processes