Skip to content

hb_store_opts.erl - Store Configuration Defaults Manager

Overview

Purpose: Apply default configuration values to store options based on store type
Module: hb_store_opts
Pattern: Recursive map merger with module-specific defaults

This module takes store options and store defaults, applying type-specific default properties based on the store-module field. Supports recursive application to nested store configurations, allowing complex multi-layered store hierarchies to inherit appropriate defaults.

Dependencies

  • Erlang/OTP: lists, maps
  • Records: None

Public Functions Overview

%% Configuration Management
-spec apply(StoreOpts, Defaults) -> UpdatedStoreOpts
    when
        StoreOpts :: [map()],
        Defaults :: map(),
        UpdatedStoreOpts :: [map()].

Public Functions

1. apply/2

-spec apply(StoreOpts, Defaults) -> UpdatedStoreOpts
    when
        StoreOpts :: [map()],
        Defaults :: map(),
        UpdatedStoreOpts :: [map()].

Description: Apply store defaults to a list of store configuration maps. For each store in the list, applies defaults based on its store-module type and recursively processes any nested stores in the <<"store">> field. Store options take precedence over defaults.

Supported Store Modules:
  • hb_store_lmdb → Uses <<"lmdb">> defaults
  • hb_store_fs → Uses <<"fs">> defaults
  • hb_store_rocksdb → Uses <<"rocksdb">> defaults
  • hb_store_gateway → Uses <<"gateway">> defaults
Test Code:
-module(test_hb_store_opts).
-include_lib("eunit/include/eunit.hrl").
 
%% Basic apply tests
apply_single_store_test() ->
    StoreOpts = [
        #{
            <<"name">> => <<"cache-mainnet/lmdb">>,
            <<"store-module">> => hb_store_lmdb
        }
    ],
    Defaults = #{
        <<"lmdb">> => #{
            <<"capacity">> => 1073741824
        }
    },
    Result = hb_store_opts:apply(StoreOpts, Defaults),
    [Updated] = Result,
    ?assertEqual(1073741824, maps:get(<<"capacity">>, Updated)),
    ?assertEqual(<<"cache-mainnet/lmdb">>, maps:get(<<"name">>, Updated)).
 
apply_empty_defaults_test() ->
    StoreOpts = [
        #{
            <<"name">> => <<"test">>,
            <<"store-module">> => hb_store_lmdb
        }
    ],
    Defaults = #{},
    Result = hb_store_opts:apply(StoreOpts, Defaults),
    ?assertEqual(StoreOpts, Result).
 
apply_empty_store_opts_test() ->
    StoreOpts = [],
    Defaults = #{<<"lmdb">> => #{<<"capacity">> => 1000}},
    Result = hb_store_opts:apply(StoreOpts, Defaults),
    ?assertEqual([], Result).
 
apply_multiple_stores_test() ->
    StoreOpts = [
        #{<<"name">> => <<"lmdb1">>, <<"store-module">> => hb_store_lmdb},
        #{<<"name">> => <<"lmdb2">>, <<"store-module">> => hb_store_lmdb},
        #{<<"name">> => <<"fs1">>, <<"store-module">> => hb_store_fs}
    ],
    Defaults = #{
        <<"lmdb">> => #{<<"capacity">> => 5000},
        <<"fs">> => #{<<"buffer-size">> => 1024}
    },
    Result = hb_store_opts:apply(StoreOpts, Defaults),
    [Lmdb1, Lmdb2, Fs1] = Result,
    
    ?assertEqual(5000, maps:get(<<"capacity">>, Lmdb1)),
    ?assertEqual(5000, maps:get(<<"capacity">>, Lmdb2)),
    ?assertEqual(1024, maps:get(<<"buffer-size">>, Fs1)),
    ?assertEqual(false, maps:is_key(<<"capacity">>, Fs1)).
 
%% Module type tests
apply_lmdb_defaults_test() ->
    StoreOpt = #{
        <<"name">> => <<"test">>,
        <<"store-module">> => hb_store_lmdb
    },
    Defaults = #{
        <<"lmdb">> => #{
            <<"capacity">> => 1000,
            <<"sync">> => true
        }
    },
    Result = hb_store_opts:apply([StoreOpt], Defaults),
    [Updated] = Result,
    ?assertEqual(1000, maps:get(<<"capacity">>, Updated)),
    ?assertEqual(true, maps:get(<<"sync">>, Updated)).
 
apply_fs_defaults_test() ->
    StoreOpt = #{
        <<"name">> => <<"test">>,
        <<"store-module">> => hb_store_fs
    },
    Defaults = #{
        <<"fs">> => #{<<"buffer-size">> => 2048}
    },
    Result = hb_store_opts:apply([StoreOpt], Defaults),
    [Updated] = Result,
    ?assertEqual(2048, maps:get(<<"buffer-size">>, Updated)).
 
apply_rocksdb_defaults_test() ->
    StoreOpt = #{
        <<"name">> => <<"test">>,
        <<"store-module">> => hb_store_rocksdb
    },
    Defaults = #{
        <<"rocksdb">> => #{<<"block-size">> => 8192}
    },
    Result = hb_store_opts:apply([StoreOpt], Defaults),
    [Updated] = Result,
    ?assertEqual(8192, maps:get(<<"block-size">>, Updated)).
 
apply_gateway_defaults_test() ->
    StoreOpt = #{
        <<"name">> => <<"test">>,
        <<"store-module">> => hb_store_gateway
    },
    Defaults = #{
        <<"gateway">> => #{<<"timeout">> => 30000}
    },
    Result = hb_store_opts:apply([StoreOpt], Defaults),
    [Updated] = Result,
    ?assertEqual(30000, maps:get(<<"timeout">>, Updated)).
 
apply_unknown_module_test() ->
    StoreOpt = #{
        <<"name">> => <<"test">>,
        <<"store-module">> => unknown_module
    },
    Defaults = #{<<"lmdb">> => #{<<"capacity">> => 1000}},
    Result = hb_store_opts:apply([StoreOpt], Defaults),
    [Updated] = Result,
    ?assertEqual(StoreOpt, Updated).
 
%% Store options precedence
apply_store_opts_precedence_test() ->
    StoreOpt = #{
        <<"name">> => <<"test">>,
        <<"store-module">> => hb_store_lmdb,
        <<"capacity">> => 9999  %% User-specified
    },
    Defaults = #{
        <<"lmdb">> => #{
            <<"capacity">> => 1000,  %% Default
            <<"sync">> => true       %% Additional default
        }
    },
    Result = hb_store_opts:apply([StoreOpt], Defaults),
    [Updated] = Result,
    ?assertEqual(9999, maps:get(<<"capacity">>, Updated)),  %% Kept from store
    ?assertEqual(true, maps:get(<<"sync">>, Updated)).      %% Added from defaults
 
%% Nested store tests
apply_nested_stores_test() ->
    StoreOpts = [
        #{
            <<"store-module">> => hb_store_gateway,
            <<"store">> => [
                #{
                    <<"name">> => <<"cache-mainnet/lmdb">>,
                    <<"store-module">> => hb_store_lmdb
                }
            ]
        }
    ],
    Defaults = #{
        <<"lmdb">> => #{<<"capacity">> => 1073741824}
    },
    Result = hb_store_opts:apply(StoreOpts, Defaults),
    [Gateway] = Result,
    [NestedLmdb] = maps:get(<<"store">>, Gateway),
    ?assertEqual(1073741824, maps:get(<<"capacity">>, NestedLmdb)).
 
apply_deeply_nested_stores_test() ->
    StoreOpts = [
        #{
            <<"store-module">> => hb_store_gateway,
            <<"store">> => [
                #{
                    <<"store-module">> => hb_store_lru,
                    <<"store">> => [
                        #{
                            <<"name">> => <<"lmdb">>,
                            <<"store-module">> => hb_store_lmdb
                        }
                    ]
                }
            ]
        }
    ],
    Defaults = #{<<"lmdb">> => #{<<"capacity">> => 5000}},
    Result = hb_store_opts:apply(StoreOpts, Defaults),
    [Gateway] = Result,
    [Lru] = maps:get(<<"store">>, Gateway),
    [Lmdb] = maps:get(<<"store">>, Lru),
    ?assertEqual(5000, maps:get(<<"capacity">>, Lmdb)).
 
apply_nested_with_parent_defaults_test() ->
    StoreOpts = [
        #{
            <<"store-module">> => hb_store_gateway,
            <<"store">> => [
                #{
                    <<"name">> => <<"nested">>,
                    <<"store-module">> => hb_store_lmdb
                }
            ]
        }
    ],
    Defaults = #{
        <<"gateway">> => #{<<"timeout">> => 30000},
        <<"lmdb">> => #{<<"capacity">> => 5000}
    },
    Result = hb_store_opts:apply(StoreOpts, Defaults),
    [Gateway] = Result,
    ?assertEqual(30000, maps:get(<<"timeout">>, Gateway)),
    [NestedLmdb] = maps:get(<<"store">>, Gateway),
    ?assertEqual(5000, maps:get(<<"capacity">>, NestedLmdb)).

Internal Functions

apply_defaults_to_store/2

-spec apply_defaults_to_store(StoreOpt, Defaults) -> UpdatedStoreOpt
    when
        StoreOpt :: map(),
        Defaults :: map(),
        UpdatedStoreOpt :: map().

Description: Apply defaults to a single store configuration by first applying module-type defaults, then recursively processing any sub-stores.


apply_defaults_by_module_type/2

-spec apply_defaults_by_module_type(StoreOpt, Defaults) -> UpdatedStoreOpt
    when
        StoreOpt :: map(),
        Defaults :: map(),
        UpdatedStoreOpt :: map().

Description: Apply type-specific defaults based on the <<"store-module">> field. If the module type has defaults defined, merges them with the store options (store options take precedence).


apply_type_defaults/3

-spec apply_type_defaults(StoreOpt, TypeKey, Defaults) -> UpdatedStoreOpt
    when
        StoreOpt :: map(),
        TypeKey :: binary(),
        Defaults :: map(),
        UpdatedStoreOpt :: map().

Description: Apply defaults for a specific type key by merging type-specific defaults with the store options. Store options take precedence over defaults.


apply_defaults_to_substores/2

-spec apply_defaults_to_substores(StoreOpt, Defaults) -> UpdatedStoreOpt
    when
        StoreOpt :: map(),
        Defaults :: map(),
        UpdatedStoreOpt :: map().

Description: Recursively apply defaults to nested stores found in the <<"store">> field. Processes each sub-store independently.


Common Patterns

%% Basic store defaults application
DefaultStoreOpts = [
    #{
        <<"name">> => <<"cache-mainnet/lmdb">>,
        <<"store-module">> => hb_store_lmdb
    }
],
StoreDefaults = #{
    <<"lmdb">> => #{
        <<"capacity">> => 16_000_000_000
    }
},
UpdatedStoreOpts = hb_store_opts:apply(DefaultStoreOpts, StoreDefaults).
 
%% Multiple store types with different defaults
MultiStoreOpts = [
    #{
        <<"name">> => <<"cache/lmdb">>,
        <<"store-module">> => hb_store_lmdb
    },
    #{
        <<"name">> => <<"cache/fs">>,
        <<"store-module">> => hb_store_fs
    },
    #{
        <<"name">> => <<"cache/rocks">>,
        <<"store-module">> => hb_store_rocksdb
    }
],
Defaults = #{
    <<"lmdb">> => #{
        <<"capacity">> => 16_000_000_000,
        <<"no-sync">> => true
    },
    <<"fs">> => #{
        <<"buffer-size">> => 4096
    },
    <<"rocksdb">> => #{
        <<"block-size">> => 8192
    }
},
UpdatedMultiStoreOpts = hb_store_opts:apply(MultiStoreOpts, Defaults).
 
%% Nested store configuration (gateway → lmdb)
NestedStoreOpts = [
    #{
        <<"store-module">> => hb_store_gateway,
        <<"gateway">> => <<"https://arweave.net">>,
        <<"store">> => [
            #{
                <<"name">> => <<"cache-mainnet/lmdb">>,
                <<"store-module">> => hb_store_lmdb
            }
        ]
    }
],
NestedDefaults = #{
    <<"lmdb">> => #{
        <<"capacity">> => 10_000_000_000
    },
    <<"gateway">> => #{
        <<"timeout">> => 30000
    }
},
UpdatedNestedOpts = hb_store_opts:apply(NestedStoreOpts, NestedDefaults).
 
%% Complex multi-layer hierarchy (gateway → lru → lmdb)
ComplexStoreOpts = [
    #{
        <<"store-module">> => hb_store_gateway,
        <<"store">> => [
            #{
                <<"store-module">> => hb_store_lru,
                <<"name">> => <<"main-cache">>,
                <<"store">> => [
                    #{
                        <<"name">> => <<"persistent">>,
                        <<"store-module">> => hb_store_lmdb
                    }
                ]
            }
        ]
    }
],
ComplexDefaults = #{
    <<"lmdb">> => #{<<"capacity">> => 5_000_000_000},
    <<"lru">> => #{<<"capacity">> => 1_000_000_000}
},
UpdatedComplexOpts = hb_store_opts:apply(ComplexStoreOpts, ComplexDefaults).
 
%% Integration with hb_http_server configuration
LoadedConfig = #{
    <<"store_defaults">> => #{
        <<"lmdb">> => #{<<"capacity">> => 5000}
    }
},
DefaultStores = [
    #{
        <<"name">> => <<"cache-mainnet/lmdb">>,
        <<"store-module">> => hb_store_lmdb
    }
],
MergedConfig = maps:merge(
    #{<<"store">> => DefaultStores},
    LoadedConfig
),
FinalStoreOpts = hb_store_opts:apply(
    maps:get(<<"store">>, MergedConfig),
    maps:get(<<"store_defaults">>, MergedConfig, #{})
).

Module Type Mappings

Supported Mappings

Store ModuleDefault KeyExample Defaults
hb_store_lmdb<<"lmdb">>#{<<"capacity">> => 16GB}
hb_store_fs<<"fs">>#{<<"buffer-size">> => 4096}
hb_store_rocksdb<<"rocksdb">>#{<<"block-size">> => 8192}
hb_store_gateway<<"gateway">>#{<<"timeout">> => 30000}
Other modulesNoneNo defaults applied

Merge Behavior

Priority Rules

When merging defaults with store options:

% Store options take precedence over defaults
StoreOpt = #{
    <<"name">> => <<"test">>,
    <<"capacity">> => 1000  % User-specified
},
Defaults = #{
    <<"lmdb">> => #{
        <<"capacity">> => 5000,  % Default value
        <<"sync">> => true       % Additional default
    }
},
% Result:
#{
    <<"name">> => <<"test">>,
    <<"capacity">> => 1000,  % Kept from StoreOpt
    <<"sync">> => true       % Added from Defaults
}

Recursive Application

Level 1: Gateway Store
  ├─ Apply gateway defaults
  └─ Process <<"store">> field recursively

      Level 2: LRU Store
        ├─ Apply lru defaults
        └─ Process <<"store">> field recursively

            Level 3: LMDB Store
              └─ Apply lmdb defaults

Configuration Examples

Typical Server Configuration

% In hb_http_server.erl
DefaultStoreOpts = [
    #{
        <<"name">> => <<"cache-mainnet/lmdb">>,
        <<"store-module">> => hb_store_lmdb
    },
    #{
        <<"store-module">> => hb_store_fs,
        <<"name">> => <<"cache-mainnet">>
    }
],
 
StoreDefaults = #{
    <<"lmdb">> => #{
        <<"capacity">> => 16 * 1024 * 1024 * 1024
    },
    <<"fs">> => #{
        <<"buffer-size">> => 4096
    }
},
 
FinalStoreOpts = hb_store_opts:apply(DefaultStoreOpts, StoreDefaults).

Custom Capacity Configuration

% Load from configuration file
LoadedDefaults = #{
    <<"store_defaults">> => #{
        <<"lmdb">> => #{
            <<"capacity">> => 5_000_000_000  % 5GB custom
        }
    }
},
 
% Apply to store options
UpdatedOpts = hb_store_opts:apply(
    StoreOpts,
    maps:get(<<"store_defaults">>, LoadedDefaults, #{})
).

References

  • hb_store - HyperBEAM store interface
  • hb_http_server - Uses this module for configuration
  • Store Modules - hb_store_lmdb, hb_store_fs, hb_store_rocksdb, etc.

Notes

  1. No Auto-Import: Module disables auto-import for apply/2 to avoid conflicts
  2. Recursive Processing: Handles arbitrarily nested store hierarchies
  3. Type-Based Defaults: Different stores get different default configurations
  4. Merge Strategy: Store options always take precedence over defaults
  5. Unknown Modules: Silently skips modules without defined defaults
  6. List Processing: Always operates on lists of store configurations
  7. Immutable Operations: Returns new maps, doesn't modify originals
  8. Empty Handling: Gracefully handles empty store lists and default maps
  9. Configuration Files: Designed for integration with external configuration
  10. Server Integration: Used by hb_http_server during initialization