diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ffbf6..4e9d570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2.0.0] - 2023-06-30 +## ** Breaking for Erlang applications (not affecting Elixir applications) ** + +- Due to a new dependency on a murmur3 hashing library implemented in Elixir, the following is now required to use the SDK in Erlang applications: + - Elixir is now required to be installed on your build system when compiling your application. Version 1.13.4 and above is required. + - Rebar3 `rebar_mix` plugin installed in your Rebar3 plugins + - For full details, see the [Erlang SDK reference](https://developer.harness.io/docs/feature-flags/ff-sdks/server-sdks/erlang-sdk-reference/#for-erlang-applications) + +### Enhancements +- Implemented retry logic for authentication, polling, and metrics services for resilience and fault tolerance. +- Changes supervisor +### Fixes +- Swaps out murmur3 nif library which was giving unpredictable runtime behaviour in favour of pure Elixir implementation + + ## [1.2.1] - 2023-06-29 ### Fixes The optional configuration option introduced in 1.2.0 would only work if the application level was set to `info` - this change now sets the `cfclient_evaluation` module to `info` level if `verbose_evaluation_logs` is enabled. diff --git a/README.md b/README.md index dff2a97..0c8fce8 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,18 @@ For a sample FF Erlang SDK Project for Elixir, see our ![FeatureFlags](https://github.com/harness/ff-erlang-server-sdk/raw/main/docs/images/ff-gui.png) -## Requirements +* **For Erlang** applications, install: + + * Erlang/OTP 24 or later + * Rebar3 3.20.0 or later + * Important, since version 2.0.0 the SDK depends on an Elixir hashing library, so the following is also required for Erlang applications: + * Elixir 1.13.4 or later available on your build system + * Rebar3 `rebar_mix` plugin installed in your Rebar3 plugins + +* **For Elixir** applications, install: + * Elixir version 1.13.4 or later + * OTP 24 or later -Erlang OTP 22 or newer. ## Quickstart @@ -52,17 +61,26 @@ To install the SDK for Erlang based applications: 1. Add the SDK as a dependency to your `rebar.config` file: ``` - {deps, [{cfclient, "1.2.0", {pkg, harness_ff_erlang_server_sdk}}]}. + {deps, [{cfclient, "2.0.0", {pkg, harness_ff_erlang_server_sdk}}]}. ``` 2. Add the dependency to your project's `app.src`. ```erlang {applications, - [kernel, stdlib, cfclient] - }, +[kernel, stdlib, cfclient] +}, ``` +2. Add the `rebar_mix` plugin to your `rebar.config` file: + + ```erlang + {project_plugins, [rebar_mix]}. + ``` + +Imporatant: for this plugin to work ensure you have Elixir 1.13.4 or later installed onto your build system + + ### For Elixir applications To install the SDK for Elixir based applications: @@ -72,7 +90,7 @@ To install the SDK for Elixir based applications: ``` defp deps do [ - {:cfclient, "~> 1.1.0", hex: :harness_ff_erlang_server_sdk} + {:cfclient, "~> 2.0.0", hex: :harness_ff_erlang_server_sdk} ] ``` @@ -84,9 +102,9 @@ Provide your API key in `sys.config` using an environment variable: ```erlang [ - {cfclient, [ - {api_key, {environment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, - ]} +{cfclient, [ +{api_key, {environment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, +]} ]. ``` @@ -94,9 +112,9 @@ Or you may provide the API key directly if required: ```erlang [ - {cfclient, [ - {api_key, "YOUR_API_KEY"}, - ]} +{cfclient, [ +{api_key, "YOUR_API_KEY"}, +]} ]. ``` @@ -140,21 +158,21 @@ config :cfclient, ```erlang [{cfclient, [ - %% Set the log level of the SDK to debug - {log_level, debug}, - {api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, - {config, [ - {config_url, "https://config.ff.harness.io/api/1.0"}, - {events_url, "https://config.ff.harness.io/api/1.0"}, - {poll_interval, 60}, - {analytics_enabled, true}, - ]}, - ]}] +%% Set the log level of the SDK to debug +{log_level, debug}, +{api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"}, +{config, [ +{config_url, "https://config.ff.harness.io/api/1.0"}, +{events_url, "https://config.ff.harness.io/api/1.0"}, +{poll_interval, 60}, +{analytics_enabled, true}, +]}, +]}] ``` ### Enable Verbose Evaluation Logs -Evaluation logs are `debug` level by default. If required, they can be changed to `info` level. This is useful if production environments do not use `debug` level, but there is a requirement to check low level evaluation logs. +Evaluation logs are `debug` level by default. If required, they can be changed to `info` level. This is useful if production environments do not use `debug` level, but there is a requirement to check low level evaluation logs. Note that this will only affect evaluation log statements. #### Elixir @@ -174,7 +192,7 @@ config :cfclient, ]] ``` -#### Erlang +#### Erlang ```erlang [{cfclient, [ @@ -194,11 +212,11 @@ config :cfclient, ## Run multiple instances of the SDK The SDK by default starts up a single instance called `default` which is configured with your project API key. -If different parts of your application need to use specific [projects](https://developer.harness.io/docs/feature-flags/ff-using-flags/ff-creating-flag/create-a-project/), you can start up additional client instances using by defining additional configuration for each unique project. +If different parts of your application need to use specific [projects](https://developer.harness.io/docs/feature-flags/ff-using-flags/ff-creating-flag/create-a-project/), you can start up additional client instances using by defining additional configuration for each unique project. -### Erlang Project Config +### Erlang Project Config -The additional project config is defined in `sys.config` +The additional project config is defined in `sys.config` The following `sys.config` snippet starts up two additional instances as well along with the default instance: @@ -259,7 +277,7 @@ If you don't require the default instance to be started up, you can do: ``` In your application supervisor, e.g. `src/myapp_sup.erl`, start up a `cfclient_instance` -for each additional project. As the default instance is booted when your application starts, you cannot (and don't need to) start it here. +for each additional project. As the default instance is booted when your application starts, you cannot (and don't need to) start it here. ```erlang init(Args) -> @@ -334,8 +352,8 @@ multi_instance_evaluations() -> ] ``` -2. In your application supervisor, e.g. `lib/myapp/supervisor.ex`, start up `cfclient_instance` -for each of the additional project configurations you provided above. As the default instance is booted when your application starts, you cannot (and don't need to) start it here: +2. In your application supervisor, e.g. `lib/myapp/supervisor.ex`, start up `cfclient_instance` + for each of the additional project configurations you provided above. As the default instance is booted when your application starts, you cannot (and don't need to) start it here: ```elixir def init(_opts) do @@ -533,4 +551,4 @@ In order to run the tests, pull the submodules: ```command git submodule update --init -``` +``` \ No newline at end of file diff --git a/doc/.build b/doc/.build deleted file mode 100644 index 56e9298..0000000 --- a/doc/.build +++ /dev/null @@ -1,43 +0,0 @@ -404.html -api-reference.html -cfclient.html -cfclient_app.html -cfclient_cache.html -cfclient_config.html -cfclient_ets.html -cfclient_evaluator.html -cfclient_instance.html -cfclient_metrics.html -cfclient_retrieve.html -cfclient_sup.html -dist/handlebars.runtime-NWIB6V2M.js -dist/handlebars.templates-IV5W3OL2.js -dist/html-XN2TSG4M.js -dist/html-erlang-6FXMBT73.css -dist/inconsolata-latin-400-normal-RGKDDNDD.woff2 -dist/inconsolata-latin-700-normal-DTS2D7TO.woff2 -dist/inconsolata-latin-ext-400-normal-K7HVGTP7.woff2 -dist/inconsolata-latin-ext-700-normal-4MPBLFZC.woff2 -dist/inconsolata-vietnamese-400-normal-IGQPHHJH.woff2 -dist/inconsolata-vietnamese-700-normal-LHEGSN35.woff2 -dist/lato-latin-300-normal-YUMVEFOL.woff2 -dist/lato-latin-700-normal-2XVSBPG4.woff2 -dist/lato-latin-ext-300-normal-VPGGJKJL.woff2 -dist/lato-latin-ext-700-normal-Q2L5DVMW.woff2 -dist/merriweather-cyrillic-300-italic-M6KMXZSZ.woff2 -dist/merriweather-cyrillic-300-normal-7PAAHU3N.woff2 -dist/merriweather-cyrillic-ext-300-italic-JP3ZEV2P.woff2 -dist/merriweather-cyrillic-ext-300-normal-5LF5LCEK.woff2 -dist/merriweather-latin-300-italic-353COS6Q.woff2 -dist/merriweather-latin-300-normal-RWDJH4FN.woff2 -dist/merriweather-latin-ext-300-italic-MWCA36KE.woff2 -dist/merriweather-latin-ext-300-normal-K6L27CZ5.woff2 -dist/merriweather-vietnamese-300-italic-EHHNZPUO.woff2 -dist/merriweather-vietnamese-300-normal-U376L4Z4.woff2 -dist/remixicon-NKANDIL5.woff2 -dist/search_items-9462DBD2.js -dist/sidebar_items-32BCB229.js -index.html -license.html -readme.html -search.html diff --git a/doc/404.html b/doc/404.html deleted file mode 100644 index 14567d8..0000000 --- a/doc/404.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - 404 — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - Page not found -

- -

Sorry, but the page you were trying to get to, does not exist. You -may want to try searching this site using the sidebar - - or using our API Reference page - -to find what you were looking for.

- -
-
-
-
- - - - diff --git a/doc/api-reference.html b/doc/api-reference.html deleted file mode 100644 index 986f312..0000000 --- a/doc/api-reference.html +++ /dev/null @@ -1,262 +0,0 @@ - - - - - - - - - - API Reference — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - API Reference cfclient v1.2.1 -

- - -
-

- -

modules

-
- Modules -

- -
-
-
- cfclient - -
- -
Public interface for client.
- -
-
-
- cfclient_app - -
- -
cfclient application.
- -
-
- - -
Functions to manage cache of Flag and Segment data from server.
- -
-
- - -
Functions to manage client configuration.
- -
-
-
- cfclient_ets - -
- -
Functions to make it easier to mock ETS
- -
-
- - -
Functions to evaluate flag rules.
- -
-
- - -

Feature flags client instance.

- -
-
- - -
Functions to record, process, and send cached metric data.
- -
-
- - -
Funcctions to pull feature and target configuration from server via the API.
- -
-
-
- cfclient_sup - -
- -

Top level supervisor for cfclient.

- -
- -
-
- - - - -
-
-
-
- - - - diff --git a/doc/cfclient.epub b/doc/cfclient.epub deleted file mode 100644 index 35a7df7..0000000 Binary files a/doc/cfclient.epub and /dev/null differ diff --git a/doc/cfclient.html b/doc/cfclient.html deleted file mode 100644 index 8f5c323..0000000 --- a/doc/cfclient.html +++ /dev/null @@ -1,636 +0,0 @@ - - - - - - - - - - cfclient — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient - (cfclient v1.2.1) - -

- - -
-Public interface for client. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Types -

- -
-
- config/0 - -
- -
- -
-
- target/0 - -
- -
- -
-
-

- Functions -

- -
- - -
Evaluate variation which returns a boolean.
- -
- - - -
-
- close() - -
- -
- -
-
- close(Name) - -
- -
- -
- - -
Evaluate variation which returns a JSON object.
- -
- - - -
- - -
Evaluate variation which returns a number.
- -
- - - -
- - -
Evaluate variation which returns a string.
- -
- - - -
- -
- - -
-

- - - Link to this section - -Types -

-
-
- - - -
- -
- -
-type config() :: map().
- -
- - -
-
-
- - - -
- -
- -
-type target() ::
-    #{identifier := binary(),
-      name := binary(),
-      anonymous => boolean(),
-      attributes := #{atom() := binary() | atom() | list()} | null}.
- -
- - -
-
- -
-
- -
-

- - - Link to this section - -Functions -

-
-
- -
- - - Link to this function - -

bool_variation(FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec bool_variation(binary() | string(), target(), boolean()) -> boolean().
- -
- -Evaluate variation which returns a boolean. -
-
-
- -
- - - Link to this function - -

bool_variation(Config, FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec bool_variation(atom() | config(), binary() | string(), target(), boolean()) -> boolean().
- -
- - -
-
-
- - - -
- - -
-
-
- - - -
- - -
-
-
- -
- - - Link to this function - -

json_variation(FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec json_variation(binary() | string(), target(), map()) -> map().
- -
- -Evaluate variation which returns a JSON object. -
-
-
- -
- - - Link to this function - -

json_variation(Config, FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec json_variation(atom() | config(), binary() | list(), target(), map()) -> map().
- -
- - -
-
-
- -
- - - Link to this function - -

number_variation(FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec number_variation(binary() | list(), target(), number()) -> number().
- -
- -Evaluate variation which returns a number. -
-
-
- -
- - - Link to this function - -

number_variation(Config, FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec number_variation(atom() | config(), binary() | list(), target(), number()) -> number().
- -
- - -
-
-
- -
- - - Link to this function - -

string_variation(FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec string_variation(binary() | string(), target(), binary()) -> binary().
- -
- -Evaluate variation which returns a string. -
-
-
- -
- - - Link to this function - -

string_variation(Config, FlagKey, Target, Default)

- - - - View Source - - - -
- -
- -
- -
-spec string_variation(atom() | config(), binary() | list(), target(), binary()) -> binary().
- -
- - -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_app.html b/doc/cfclient_app.html deleted file mode 100644 index c74de45..0000000 --- a/doc/cfclient_app.html +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - - - - - cfclient_app — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_app - (cfclient v1.2.1) - -

- - -
-cfclient application. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Functions -

- - - -
-
- stop(State) - -
- -
- -
- -
- - -
-

- - - Link to this section - -Functions -

-
-
- -
- - - Link to this function - -

start(StartType, StartArgs)

- - - - View Source - - - -
- -
- - -
-
-
- - - -
- - -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_cache.html b/doc/cfclient_cache.html deleted file mode 100644 index d4b9157..0000000 --- a/doc/cfclient_cache.html +++ /dev/null @@ -1,605 +0,0 @@ - - - - - - - - - - cfclient_cache — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_cache - (cfclient v1.2.1) - -

- - -
-Functions to manage cache of Flag and Segment data from server. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Types -

- -
-
- config/0 - -
- -
- -
-
- flag/0 - -
- -
- -
-
- segment/0 - -
- -
- -
-
-

- Functions -

- -
- - -
- - - -
- - -
- - - -
-
- get_value(_) - -
- -
Get Flag or Segment from cache.
- -
- -
- - -
- -
-
- set_pid(_) - -
- -
- -
- - -
Store flag or segment into cache with new value.
- -
- -
- -
- - -
-

- - - Link to this section - -Types -

-
-
- - - -
- -
- -
-type config() :: cfclient:config().
- -
- - -
-
-
- - - -
- -
- -
-type flag() :: cfclient_evaluator:flag().
- -
- - -
-
-
- - - -
- -
- -
-type segment() :: cfapi_segment:cfapi_segment().
- -
- - -
-
- -
-
- -
-

- - - Link to this section - -Functions -

-
-
- - - -
- -
- -
-spec cache_flag(flag()) -> ok | {error, outdated}.
- -
- - -
-
-
- -
- - - Link to this function - -

cache_flag(Value, Config)

- - - - View Source - - - -
- -
- -
- -
-spec cache_flag(flag(), config()) -> ok | {error, outdated}.
- -
- - -
-
-
- -
- - - Link to this function - -

cache_segment(Value)

- - - - View Source - - - -
- -
- -
- -
-spec cache_segment(segment()) -> ok | {error, outdated}.
- -
- - -
-
-
- -
- - - Link to this function - -

cache_segment(Value, Config)

- - - - View Source - - - -
- -
- -
- -
-spec cache_segment(segment(), config()) -> ok | {error, outdated}.
- -
- - -
-
-
- - - -
- -
- -
-spec get_value({flag, binary()} | {segment, binary()}) -> {ok, flag() | segment()} | {error, undefined}.
- -
- -Get Flag or Segment from cache. -
-
-
- -
- - - Link to this function - -

get_value(_, Config)

- - - - View Source - - - -
- -
- -
- -
-spec get_value({flag, binary()} | {segment, binary()}, config()) ->
-             {ok, flag() | segment()} | {error, undefined}.
- -
- - -
-
-
- - - -
- -
- -
-spec set_pid(pid()) -> ok.
- -
- - -
-
-
- -
- - - Link to this function - -

set_value(_, Value)

- - - - View Source - - - -
- -
- -
- -
-spec set_value({flag, binary()} | {segment, binary()}, flag() | segment()) -> ok | {error, outdated}.
- -
- -Store flag or segment into cache with new value. -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_config.html b/doc/cfclient_config.html deleted file mode 100644 index 1789fdf..0000000 --- a/doc/cfclient_config.html +++ /dev/null @@ -1,741 +0,0 @@ - - - - - - - - - - cfclient_config — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_config - (cfclient v1.2.1) - -

- - -
-Functions to manage client configuration. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Types -

- -
-
- config/0 - -
- -
- -
-
-

- Functions -

- -
- - -
with Authenticate with server and merge project attributes into config
- -
- -
- - -
- -
-
- defaults() - -
- -
- -
- - -
- -
-
- get_config() - -
- -
- -
- - -
- - - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - -
- -
- - -
-

- - - Link to this section - -Types -

-
-
- - - -
- -
- -
-type config() :: map().
- -
- - -
-
- -
-
- -
-

- - - Link to this section - -Functions -

-
-
- -
- - - Link to this function - -

authenticate(ApiKey, Config)

- - - - View Source - - - -
- -
- -
- -
-spec authenticate(binary() | string() | undefined | nil, map()) ->
-                {ok, Config :: map()} | {error, Response :: term()}.
- -
- -with Authenticate with server and merge project attributes into config -
-
-
- -
- - - Link to this function - -

create_tables(Config)

- - - - View Source - - - -
- -
- -
- -
-spec create_tables(config()) -> ok.
- -
- - -
-
-
- - - -
- -
- -
-spec defaults() -> map().
- -
- - -
-
-
- - - -
- -
- -
-spec delete_tables(list()) -> ok.
- -
- - -
-
-
- - - -
- -
- -
-spec get_config() -> config().
- -
- - -
-
-
- - - -
- -
- -
-spec get_config(atom()) -> config().
- -
- - -
-
-
- -
- - - Link to this function - -

get_table_names(Config)

- - - - View Source - - - -
- -
- - -
-
-
- - - -
- -
- -
-spec get_value(atom() | binary() | string()) -> term().
- -
- - -
-
-
- -
- - - Link to this function - -

get_value(Key, Opts)

- - - - View Source - - - -
- -
- -
- -
-spec get_value(atom(), map()) -> term().
- -
- - -
-
-
- - - -
- -
- -
-spec init(proplists:proplist()) -> ok.
- -
- - -
-
-
- -
- - - Link to this function - -

normalize(Config0)

- - - - View Source - - - -
- -
- -
- -
-spec normalize(proplists:proplist()) -> map().
- -
- - -
-
-
- -
- - - Link to this function - -

parse_jwt(JwtToken)

- - - - View Source - - - -
- -
- -
- -
-spec parse_jwt(binary()) -> {ok, map()} | {error, Reason :: term()}.
- -
- - -
-
-
- -
- - - Link to this function - -

set_config(Config)

- - - - View Source - - - -
- -
- -
- -
-spec set_config(config()) -> ok.
- -
- - -
-
-
- -
- - - Link to this function - -

set_config(Name, Config)

- - - - View Source - - - -
- -
- -
- -
-spec set_config(atom(), config()) -> ok.
- -
- - -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_ets.html b/doc/cfclient_ets.html deleted file mode 100644 index a2bdc91..0000000 --- a/doc/cfclient_ets.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - cfclient_ets — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_ets - (cfclient v1.2.1) - -

- - -
-Functions to make it easier to mock ETS -
- - -
-

- - - Link to this section - - Summary -

-
-

- Functions -

- -
- - -
- -
- - -
- -
- -
- - -
-

- - - Link to this section - -Functions -

-
-
- - - -
- -
- -
-spec get(atom(), binary()) -> term().
- -
- - -
-
-
- -
- - - Link to this function - -

lookup(Table, Key)

- - - - View Source - - - -
- -
- -
- -
-spec lookup(atom(), term()) -> list().
- -
- - -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_evaluator.html b/doc/cfclient_evaluator.html deleted file mode 100644 index 35ff491..0000000 --- a/doc/cfclient_evaluator.html +++ /dev/null @@ -1,739 +0,0 @@ - - - - - - - - - - cfclient_evaluator — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_evaluator - (cfclient v1.2.1) - -

- - -
-Functions to evaluate flag rules. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Types -

- -
-
- config/0 - -
- -
- -
-
- flag/0 - -
- -
- -
-
- rule/0 - -
- -
- -
- - -
- -
-
- rule_serve/0 - -
- -
- -
-
- segment/0 - -
- -
- -
-
- target/0 - -
- -
- -
- - -
- -
- - -
- - -
-

- - - Link to this section - -Types -

-
-
- - - -
- -
- -
-type config() :: map().
- -
- - -
-
-
- - - -
- -
- -
-type flag() ::
-    #{createdAt => integer(),
-      defaultServe := cfapi_serve:cfapi_serve(),
-      environment := binary(),
-      excluded => list(),
-      feature := binary(),
-      identifier => binary(),
-      included => list(),
-      kind := binary(),
-      modifiedAt => integer(),
-      name => binary(),
-      offVariation := binary(),
-      prerequisites => list(),
-      project := binary(),
-      rules => [map()],
-      state := binary() | map(),
-      tags => list(),
-      variationToTargetMap => [variation_map()] | null,
-      variations := list(),
-      version => integer()}.
- -
- - -
-
-
- - - -
- -
- -
-type rule() ::
-    #{priority := non_neg_integer(),
-      clauses := [rule_clause()],
-      serve => rule_serve(),
-      op => binary(),
-      values => [binary()],
-      excluded => [map()] | null,
-      included => [map()] | null}.
- -
- - -
-
-
- - - -
- -
- -
-type rule_clause() :: #{op := binary(), values := [binary()]}.
- -
- - -
-
-
- - - -
- -
- -
-type rule_serve() :: #{variation := binary(), distribution => boolean()}.
- -
- - -
-
-
- - - -
- -
- -
-type segment() :: cfapi_segment:cfapi_segment().
- -
- - -
-
-
- - - -
- -
- -
-type target() :: cfclient:target().
- -
- - -
-
-
- - - -
- -
- -
-type variation_map() ::
-    #{variation := binary(), targets := [cfapi_variation_map:cfapi_variation_map()]}.
- -
- - -
-
- -
-
- -
-

- - - Link to this section - -Functions -

-
-
- -
- - - Link to this function - -

bool_variation(FlagId, Target, Config)

- - - - View Source - - - -
- -
- -
- -
-spec bool_variation(binary(), target(), config()) ->
-                  {ok, Id :: binary(), Value :: boolean()} | {error, Reason :: atom()}.
- -
- - -
-
-
- -
- - - Link to this function - -

custom_attribute_to_binary(Value)

- - - - View Source - - - -
- -
- -
- -
-spec custom_attribute_to_binary(binary() | atom() | number() | string()) -> binary() | [binary()].
- -
- - -
-
-
- -
- - - Link to this function - -

is_rule_included_or_excluded(Clauses, Target)

- - - - View Source - - - -
- -
- -
- -
-spec is_rule_included_or_excluded([rule_clause()], target()) -> included | excluded | false.
- -
- - -
-
-
- -
- - - Link to this function - -

json_variation(FlagId, Target, Config)

- - - - View Source - - - -
- -
- -
- -
-spec json_variation(binary(), target(), config()) ->
-                  {ok, Id :: binary(), Value :: map()} | {error, Reason :: atom()}.
- -
- - -
-
-
- -
- - - Link to this function - -

number_variation(FlagId, Target, Config)

- - - - View Source - - - -
- -
- -
- -
-spec number_variation(binary(), target(), config()) ->
-                    {ok, Id :: binary(), Value :: number()} | {error, Reason :: atom()}.
- -
- - -
-
-
- -
- - - Link to this function - -

string_variation(FlagId, Target, Config)

- - - - View Source - - - -
- -
- -
- -
-spec string_variation(binary(), target(), config()) ->
-                    {ok, Id :: binary(), Value :: binary()} | {error, Reason :: atom()}.
- -
- - -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_instance.html b/doc/cfclient_instance.html deleted file mode 100644 index 96e0f96..0000000 --- a/doc/cfclient_instance.html +++ /dev/null @@ -1,377 +0,0 @@ - - - - - - - - - - cfclient_instance — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_instance - (cfclient v1.2.1) - -

- - -
-

Feature flags client instance.

It creates the ETS tables used to cache flag data from the server and flag usage metrics. It runs periodic tasks to pull data from the server and send metrics to it.

An default instance is started by the cfclient application. Additional instances can be started if multiple Harness projects need to be used. project. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Functions -

- - - -
- - -
- - - -
-
- init(Args) - -
- -
- -
- - -
- -
-
- stop(Config) - -
- -
- -
- -
- - -
-

- - - Link to this section - -Functions -

-
-
- -
- - - Link to this function - -

handle_call(_, From, State)

- - - - View Source - - - -
- -
- - -
-
-
- -
- - - Link to this function - -

handle_cast(_, State)

- - - - View Source - - - -
- -
- - -
-
-
- -
- - - Link to this function - -

handle_info(_, Config)

- - - - View Source - - - -
- -
- - -
-
-
- - - -
- - -
-
-
- - - -
- -
- -
-spec start_link(proplists:proplist()) -> {ok, pid()} | ignore | {error, term()}.
- -
- - -
-
-
- - - -
- -
- -
-spec stop(map()) -> ok | {error, not_found, term()}.
- -
- - -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_metrics.html b/doc/cfclient_metrics.html deleted file mode 100644 index a1ada39..0000000 --- a/doc/cfclient_metrics.html +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - - - - cfclient_metrics — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_metrics - (cfclient v1.2.1) - -

- - -
-Functions to record, process, and send cached metric data. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Types -

- -
-
- config/0 - -
- -
- -
-
-

- Functions -

- -
- - -
Gather metrics and send them to server. Called periodically by cfclient_instance.
- -
- -
- - -
Record metrics for request.
- -
- -
- -
- - -
-

- - - Link to this section - -Types -

-
-
- - - -
- -
- -
-type config() :: map().
- -
- - -
-
- -
-
- -
-

- - - Link to this section - -Functions -

-
-
- -
- - - Link to this function - -

process_metrics(Config)

- - - - View Source - - - -
- -
- -
- -
-spec process_metrics(config()) -> ok | {error, api}.
- -
- -Gather metrics and send them to server. Called periodically by cfclient_instance. -
-
-
- -
- - - Link to this function - -

record(FlagId, Target, VariationId, VariationValue, Config)

- - - - View Source - - - -
- -
- -
- -
-spec record(binary(), cfclient:target(), binary(), binary(), config()) -> atom().
- -
- -Record metrics for request. -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_retrieve.html b/doc/cfclient_retrieve.html deleted file mode 100644 index db3ea85..0000000 --- a/doc/cfclient_retrieve.html +++ /dev/null @@ -1,388 +0,0 @@ - - - - - - - - - - cfclient_retrieve — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_retrieve - (cfclient v1.2.1) - -

- - -
-Funcctions to pull feature and target configuration from server via the API. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Types -

- -
-
- config/0 - -
- -
- -
-
- flag/0 - -
- -
- -
-
- segment/0 - -
- -
- -
-
-

- Functions -

- -
- - -
Retrieve all features from Feature Flags API.
- -
- -
- - -
Retrieve all segments from Feature Flags API.
- -
- -
- -
- - -
-

- - - Link to this section - -Types -

-
-
- - - -
- -
- -
-type config() :: map().
- -
- - -
-
-
- - - -
- -
- -
-type flag() :: cfapi_feature_config:cfapi_feature_config().
- -
- - -
-
-
- - - -
- -
- -
-type segment() :: cfapi_segment:cfapi_segment().
- -
- - -
-
- -
-
- -
-

- - - Link to this section - -Functions -

-
-
- -
- - - Link to this function - -

retrieve_flags(Config)

- - - - View Source - - - -
- -
- -
- -
-spec retrieve_flags(config()) -> {ok, [flag()]} | {error, Reason :: term()}.
- -
- -Retrieve all features from Feature Flags API. -
-
-
- -
- - - Link to this function - -

retrieve_segments(Config)

- - - - View Source - - - -
- -
- -
- -
-spec retrieve_segments(config()) -> {ok, [segment()]} | {error, Reason :: term()}.
- -
- -Retrieve all segments from Feature Flags API. -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/cfclient_sup.html b/doc/cfclient_sup.html deleted file mode 100644 index c9b1390..0000000 --- a/doc/cfclient_sup.html +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - - - - cfclient_sup — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
- - - - - -
- -
-
- -

- - - - - - View Source - - - cfclient_sup - (cfclient v1.2.1) - -

- - -
-

Top level supervisor for cfclient.

Called by application, starting up the default client instance. -
- - -
-

- - - Link to this section - - Summary -

-
-

- Functions -

- -
-
- init(Args) - -
- -
- -
- - -
- -
- -
- - -
-

- - - Link to this section - -Functions -

-
-
- - - -
- - -
-
-
- - - -
- -
- -
-spec start_link(proplists:proplist()) -> supervisor:startlink_ret().
- -
- - -
-
- -
-
- - -
-
-
-
- - - - diff --git a/doc/dist/handlebars.runtime-NWIB6V2M.js b/doc/dist/handlebars.runtime-NWIB6V2M.js deleted file mode 100644 index 117dc6c..0000000 --- a/doc/dist/handlebars.runtime-NWIB6V2M.js +++ /dev/null @@ -1,30 +0,0 @@ -/**! - - @license - handlebars v4.7.7 - -Copyright (C) 2011-2019 by Yehuda Katz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -*/(function(r,e){typeof exports=="object"&&typeof module=="object"?module.exports=e():typeof define=="function"&&define.amd?define([],e):typeof exports=="object"?exports.Handlebars=e():r.Handlebars=e()})(this,function(){return function(u){var r={};function e(n){if(r[n])return r[n].exports;var t=r[n]={exports:{},id:n,loaded:!1};return u[n].call(t.exports,t,t.exports,e),t.loaded=!0,t.exports}return e.m=u,e.c=r,e.p="",e(0)}([function(u,r,e){"use strict";var n=e(1).default,t=e(2).default;r.__esModule=!0;var f=e(3),a=n(f),i=e(36),l=t(i),h=e(5),v=t(h),P=e(4),H=n(P),C=e(37),E=n(C),I=e(43),o=t(I);function g(){var y=new a.HandlebarsEnvironment;return H.extend(y,a),y.SafeString=l.default,y.Exception=v.default,y.Utils=H,y.escapeExpression=H.escapeExpression,y.VM=E,y.template=function(p){return E.template(p,y)},y}var w=g();w.create=g,o.default(w),w.default=w,r.default=w,u.exports=r.default},function(u,r){"use strict";r.default=function(e){if(e&&e.__esModule)return e;var n={};if(e!=null)for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(n[t]=e[t]);return n.default=e,n},r.__esModule=!0},function(u,r){"use strict";r.default=function(e){return e&&e.__esModule?e:{default:e}},r.__esModule=!0},function(u,r,e){"use strict";var n=e(2).default;r.__esModule=!0,r.HandlebarsEnvironment=g;var t=e(4),f=e(5),a=n(f),i=e(9),l=e(29),h=e(31),v=n(h),P=e(32),H="4.7.7";r.VERSION=H;var C=8;r.COMPILER_REVISION=C;var E=7;r.LAST_COMPATIBLE_COMPILER_REVISION=E;var I={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:"== 1.x.x",5:"== 2.0.0-alpha.x",6:">= 2.0.0-beta.1",7:">= 4.0.0 <4.3.0",8:">= 4.3.0"};r.REVISION_CHANGES=I;var o="[object Object]";function g(y,p,R){this.helpers=y||{},this.partials=p||{},this.decorators=R||{},i.registerDefaultHelpers(this),l.registerDefaultDecorators(this)}g.prototype={constructor:g,logger:v.default,log:v.default.log,registerHelper:function(p,R){if(t.toString.call(p)===o){if(R)throw new a.default("Arg not supported with multiple helpers");t.extend(this.helpers,p)}else this.helpers[p]=R},unregisterHelper:function(p){delete this.helpers[p]},registerPartial:function(p,R){if(t.toString.call(p)===o)t.extend(this.partials,p);else{if(typeof R>"u")throw new a.default('Attempting to register a partial called "'+p+'" as undefined');this.partials[p]=R}},unregisterPartial:function(p){delete this.partials[p]},registerDecorator:function(p,R){if(t.toString.call(p)===o){if(R)throw new a.default("Arg not supported with multiple decorators");t.extend(this.decorators,p)}else this.decorators[p]=R},unregisterDecorator:function(p){delete this.decorators[p]},resetLoggedPropertyAccesses:function(){P.resetLoggedProperties()}};var w=v.default.log;r.log=w,r.createFrame=t.createFrame,r.logger=v.default},function(u,r){"use strict";r.__esModule=!0,r.extend=a,r.indexOf=v,r.escapeExpression=P,r.isEmpty=H,r.createFrame=C,r.blockParams=E,r.appendContextPath=I;var e={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"="},n=/[&<>"'`=]/g,t=/[&<>"'`=]/;function f(o){return e[o]}function a(o){for(var g=1;g0?(a.ids&&(a.ids=[a.name]),t.helpers.each(f,a)):i(this);if(a.data&&a.ids){var h=n.createFrame(a.data);h.contextPath=n.appendContextPath(a.data.contextPath,a.name),a={data:h}}return l(f,a)})},u.exports=r.default},function(u,r,e){(function(n){"use strict";var t=e(12).default,f=e(2).default;r.__esModule=!0;var a=e(4),i=e(5),l=f(i);r.default=function(h){h.registerHelper("each",function(v,P){if(!P)throw new l.default("Must pass iterator to #each");var H=P.fn,C=P.inverse,E=0,I="",o=void 0,g=void 0;P.data&&P.ids&&(g=a.appendContextPath(P.data.contextPath,P.ids[0])+"."),a.isFunction(v)&&(v=v.call(this)),P.data&&(o=a.createFrame(P.data));function w(b,F,c){o&&(o.key=b,o.index=F,o.first=F===0,o.last=!!c,g&&(o.contextPath=g+b)),I=I+H(v[b],{data:o,blockParams:a.blockParams([v[b],b],[g+b,null])})}if(v&&typeof v=="object")if(a.isArray(v))for(var y=v.length;E=0?a=i:a=parseInt(a,10)}return a},log:function(a){if(a=t.lookupLevel(a),typeof console<"u"&&t.lookupLevel(t.level)<=a){var i=t.methodMap[a];console[i]||(i="log");for(var l=arguments.length,h=Array(l>1?l-1:0),v=1;v=P.LAST_COMPATIBLE_COMPILER_REVISION&&O<=P.COMPILER_REVISION))if(O{(function(){var d=Handlebars.template,y=Handlebars.templates=Handlebars.templates||{};y["autocomplete-suggestions"]=d({1:function(n,l,a,c,s){var e,o,u=l??(n.nullContext||{}),r=n.hooks.helperMissing,i="function",t=n.escapeExpression,f=n.lookupProperty||function(p,m){if(Object.prototype.hasOwnProperty.call(p,m))return p[m]};return' -
- `+((e=(o=(o=f(a,"title")||(l!=null?f(l,"title"):l))!=null?o:r,typeof o===i?o.call(u,{name:"title",hash:{},data:s,loc:{start:{line:9,column:29},end:{line:9,column:40}}}):o))!=null?e:"")+` -`+((e=f(a,"if").call(u,l!=null?f(l,"label"):l,{name:"if",hash:{},fn:n.program(2,s,0),inverse:n.noop,data:s,loc:{start:{line:10,column:8},end:{line:12,column:15}}}))!=null?e:"")+`
- -`+((e=f(a,"if").call(u,l!=null?f(l,"description"):l,{name:"if",hash:{},fn:n.program(4,s,0),inverse:n.noop,data:s,loc:{start:{line:15,column:6},end:{line:19,column:13}}}))!=null?e:"")+`
-`},2:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return' ('+n.escapeExpression((e=(e=o(a,"label")||(l!=null?o(l,"label"):l))!=null?e:n.hooks.helperMissing,typeof e=="function"?e.call(l??(n.nullContext||{}),{name:"label",hash:{},data:s,loc:{start:{line:11,column:31},end:{line:11,column:40}}}):e))+`) -`},4:function(n,l,a,c,s){var e,o,u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return`
- `+((e=(o=(o=u(a,"description")||(l!=null?u(l,"description"):l))!=null?o:n.hooks.helperMissing,typeof o=="function"?o.call(l??(n.nullContext||{}),{name:"description",hash:{},data:s,loc:{start:{line:17,column:10},end:{line:17,column:27}}}):o))!=null?e:"")+` -
-`},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){var e,o,u=l??(n.nullContext||{}),r=n.hooks.helperMissing,i="function",t=n.escapeExpression,f=n.lookupProperty||function(p,m){if(Object.prototype.hasOwnProperty.call(p,m))return p[m]};return`
- -
"`+t((o=(o=f(a,"term")||(l!=null?f(l,"term"):l))!=null?o:r,typeof o===i?o.call(u,{name:"term",hash:{},data:s,loc:{start:{line:3,column:28},end:{line:3,column:36}}}):o))+`"
-
Search the documentation
-
-`+((e=f(a,"each").call(u,l!=null?f(l,"suggestions"):l,{name:"each",hash:{},fn:n.program(1,s,0),inverse:n.noop,data:s,loc:{start:{line:6,column:2},end:{line:21,column:11}}}))!=null?e:"")+`
-`},useData:!0}),y["modal-layout"]=d({compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){return` -`},useData:!0}),y["quick-switch-modal-body"]=d({compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){return`
- - -
-
-`},useData:!0}),y["quick-switch-results"]=d({1:function(n,l,a,c,s){var e,o=l??(n.nullContext||{}),u=n.hooks.helperMissing,r="function",i=n.escapeExpression,t=n.lookupProperty||function(f,p){if(Object.prototype.hasOwnProperty.call(f,p))return f[p]};return'
- `+i((e=(e=t(a,"name")||(l!=null?t(l,"name"):l))!=null?e:u,typeof e===r?e.call(o,{name:"name",hash:{},data:s,loc:{start:{line:3,column:4},end:{line:3,column:12}}}):e))+` -
-`},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return(e=o(a,"each").call(l??(n.nullContext||{}),l!=null?o(l,"results"):l,{name:"each",hash:{},fn:n.program(1,s,0),inverse:n.noop,data:s,loc:{start:{line:1,column:0},end:{line:5,column:9}}}))!=null?e:""},useData:!0}),y["search-results"]=d({1:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return" Search results for "+n.escapeExpression((e=(e=o(a,"value")||(l!=null?o(l,"value"):l))!=null?e:n.hooks.helperMissing,typeof e=="function"?e.call(l??(n.nullContext||{}),{name:"value",hash:{},data:s,loc:{start:{line:3,column:27},end:{line:3,column:36}}}):e))+` -`},3:function(n,l,a,c,s){return` Invalid search -`},5:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return(e=o(a,"each").call(l??(n.nullContext||{}),l!=null?o(l,"results"):l,{name:"each",hash:{},fn:n.program(6,s,0),inverse:n.noop,data:s,loc:{start:{line:15,column:2},end:{line:26,column:11}}}))!=null?e:""},6:function(n,l,a,c,s){var e,o=n.lambda,u=n.escapeExpression,r=n.lookupProperty||function(i,t){if(Object.prototype.hasOwnProperty.call(i,t))return i[t]};return`
-

- - `+u(o(l!=null?r(l,"title"):l,l))+" ("+u(o(l!=null?r(l,"type"):l,l))+`) - -

-`+((e=r(a,"each").call(l??(n.nullContext||{}),l!=null?r(l,"excerpts"):l,{name:"each",hash:{},fn:n.program(7,s,0),inverse:n.noop,data:s,loc:{start:{line:22,column:8},end:{line:24,column:17}}}))!=null?e:"")+`
-`},7:function(n,l,a,c,s){var e;return'

'+((e=n.lambda(l,l))!=null?e:"")+`

-`},9:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return((e=(o(a,"isArray")||l&&o(l,"isArray")||n.hooks.helperMissing).call(l??(n.nullContext||{}),l!=null?o(l,"results"):l,{name:"isArray",hash:{},fn:n.program(10,s,0),inverse:n.program(12,s,0),data:s,loc:{start:{line:28,column:2},end:{line:34,column:14}}}))!=null?e:"")+` -

The search functionality is full-text based. Here are some tips:

- -
    -
  • Multiple words (such as foo bar) are searched as OR
  • -
  • Use * anywhere (such as fo*) as wildcard
  • -
  • Use + before a word (such as +foo) to make its presence required
  • -
  • Use - before a word (such as -foo) to make its absence required
  • -
  • Use : to search on a particular field (such as field:word). The available fields are title and doc
  • -
  • Use WORD^NUMBER (such as foo^2) to boost the given word
  • -
  • Use WORD~NUMBER (such as foo~2) to do a search with edit distance on word
  • -
- -

To quickly go to a module, type, or function, use the autocompletion feature in the sidebar search.

-`},10:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return"

Sorry, we couldn't find anything for "+n.escapeExpression((e=(e=o(a,"value")||(l!=null?o(l,"value"):l))!=null?e:n.hooks.helperMissing,typeof e=="function"?e.call(l??(n.nullContext||{}),{name:"value",hash:{},data:s,loc:{start:{line:29,column:48},end:{line:29,column:57}}}):e))+`.

-`},12:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return(e=o(a,"if").call(l??(n.nullContext||{}),l!=null?o(l,"value"):l,{name:"if",hash:{},fn:n.program(13,s,0),inverse:n.program(15,s,0),data:s,loc:{start:{line:30,column:2},end:{line:34,column:2}}}))!=null?e:""},13:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return"

Invalid search: "+n.escapeExpression((e=(e=o(a,"errorMessage")||(l!=null?o(l,"errorMessage"):l))!=null?e:n.hooks.helperMissing,typeof e=="function"?e.call(l??(n.nullContext||{}),{name:"errorMessage",hash:{},data:s,loc:{start:{line:31,column:23},end:{line:31,column:39}}}):e))+`.

-`},15:function(n,l,a,c,s){return`

Please type something into the search bar to perform a search.

- `},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){var e,o=l??(n.nullContext||{}),u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return`

-`+((e=u(a,"if").call(o,l!=null?u(l,"value"):l,{name:"if",hash:{},fn:n.program(1,s,0),inverse:n.program(3,s,0),data:s,loc:{start:{line:2,column:2},end:{line:6,column:9}}}))!=null?e:"")+` - -

- -`+((e=(u(a,"isNonEmptyArray")||l&&u(l,"isNonEmptyArray")||n.hooks.helperMissing).call(o,l!=null?u(l,"results"):l,{name:"isNonEmptyArray",hash:{},fn:n.program(5,s,0),inverse:n.program(9,s,0),data:s,loc:{start:{line:14,column:0},end:{line:49,column:20}}}))!=null?e:"")},useData:!0}),y["settings-modal-body"]=d({1:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return(e=o(a,"if").call(l??(n.nullContext||{}),l!=null?o(l,"description"):l,{name:"if",hash:{},fn:n.program(2,s,0),inverse:n.noop,data:s,loc:{start:{line:40,column:6},end:{line:53,column:13}}}))!=null?e:""},2:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return`
-
- `+n.escapeExpression(n.lambda(l!=null?o(l,"description"):l,l))+` -
-
-`+((e=o(a,"if").call(l??(n.nullContext||{}),l!=null?o(l,"displayAs"):l,{name:"if",hash:{},fn:n.program(3,s,0),inverse:n.program(5,s,0),data:s,loc:{start:{line:46,column:12},end:{line:50,column:19}}}))!=null?e:"")+`
-
-`},3:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return" "+((e=n.lambda(l!=null?o(l,"displayAs"):l,l))!=null?e:"")+` -`},5:function(n,l,a,c,s){var e=n.lookupProperty||function(o,u){if(Object.prototype.hasOwnProperty.call(o,u))return o[u]};return" "+n.escapeExpression(n.lambda(l!=null?e(l,"key"):l,l))+` -`},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return`
-
- - - - -
- -
-`},useData:!0}),y["sidebar-items"]=d({1:function(n,l,a,c,s,e,o){var u,r=l??(n.nullContext||{}),i=n.hooks.helperMissing,t=n.lookupProperty||function(f,p){if(Object.prototype.hasOwnProperty.call(f,p))return f[p]};return((u=(t(a,"groupChanged")||l&&t(l,"groupChanged")||i).call(r,o[1],(u=e[0][0])!=null?t(u,"group"):u,{name:"groupChanged",hash:{},fn:n.program(2,s,0,e,o),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:2,column:2},end:{line:6,column:19}}}))!=null?u:"")+` -`+((u=(t(a,"nestingChanged")||l&&t(l,"nestingChanged")||i).call(r,o[1],e[0][0],{name:"nestingChanged",hash:{},fn:n.program(7,s,0,e,o),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:8,column:2},end:{line:10,column:21}}}))!=null?u:"")+` -
  • - -`+((u=t(a,"if").call(r,(u=e[0][0])!=null?t(u,"nested_title"):u,{name:"if",hash:{},fn:n.program(13,s,0,e,o),inverse:n.program(15,s,0,e,o),data:s,blockParams:e,loc:{start:{line:14,column:6},end:{line:18,column:13}}}))!=null?u:"")+` - - -
      -`+((u=(t(a,"isArray")||l&&t(l,"isArray")||i).call(r,(u=e[0][0])!=null?t(u,"headers"):u,{name:"isArray",hash:{},fn:n.program(17,s,0,e,o),inverse:n.program(20,s,0,e,o),data:s,blockParams:e,loc:{start:{line:23,column:6},end:{line:65,column:18}}}))!=null?u:"")+`
    -
  • -`},2:function(n,l,a,c,s,e){var o,u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return'
  • - `+n.escapeExpression(n.lambda((o=e[1][0])!=null?u(o,"group"):o,l))+` -
  • -`},3:function(n,l,a,c,s){return""},5:function(n,l,a,c,s){return'translate="no"'},7:function(n,l,a,c,s,e){var o,u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return' -`},9:function(n,l,a,c,s){return"current-page open"},11:function(n,l,a,c,s){return"#content"},13:function(n,l,a,c,s,e){var o,u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return" "+((o=n.lambda((o=e[1][0])!=null?u(o,"nested_title"):o,l))!=null?o:"")+` -`},15:function(n,l,a,c,s,e){var o,u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return" "+((o=n.lambda((o=e[1][0])!=null?u(o,"title"):o,l))!=null?o:"")+` -`},17:function(n,l,a,c,s,e){var o,u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return(o=u(a,"each").call(l??(n.nullContext||{}),(o=e[1][0])!=null?u(o,"headers"):o,{name:"each",hash:{},fn:n.program(18,s,0,e),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:24,column:8},end:{line:28,column:17}}}))!=null?o:""},18:function(n,l,a,c,s,e){var o,u,r=l??(n.nullContext||{}),i=n.hooks.helperMissing,t="function",f=n.lookupProperty||function(p,m){if(Object.prototype.hasOwnProperty.call(p,m))return p[m]};return`
  • - '+((o=(u=(u=f(a,"id")||(l!=null?f(l,"id"):l))!=null?u:i,typeof u===t?u.call(r,{name:"id",hash:{},data:s,blockParams:e,loc:{start:{line:26,column:52},end:{line:26,column:60}}}):u))!=null?o:"")+` -
  • -`},20:function(n,l,a,c,s,e){var o,u=l??(n.nullContext||{}),r=n.hooks.helperMissing,i=n.lookupProperty||function(t,f){if(Object.prototype.hasOwnProperty.call(t,f))return t[f]};return((o=(i(a,"showSections")||l&&i(l,"showSections")||r).call(u,e[1][0],{name:"showSections",hash:{},fn:n.program(21,s,0,e),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:30,column:8},end:{line:44,column:25}}}))!=null?o:"")+((o=(i(a,"showSummary")||l&&i(l,"showSummary")||r).call(u,e[1][0],{name:"showSummary",hash:{},fn:n.program(26,s,0,e),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:45,column:8},end:{line:49,column:24}}}))!=null?o:"")+((o=i(a,"each").call(u,(o=e[1][0])!=null?i(o,"nodeGroups"):o,{name:"each",hash:{},fn:n.program(28,s,1,e),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:50,column:8},end:{line:64,column:17}}}))!=null?o:"")},21:function(n,l,a,c,s,e){var o,u=l??(n.nullContext||{}),r=n.lookupProperty||function(i,t){if(Object.prototype.hasOwnProperty.call(i,t))return i[t]};return'
  • - - Sections - - -
      -`+((o=r(a,"each").call(u,l!=null?r(l,"sections"):l,{name:"each",hash:{},fn:n.program(24,s,0,e),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:37,column:14},end:{line:41,column:23}}}))!=null?o:"")+`
    -
  • -`},22:function(n,l,a,c,s){return"open"},24:function(n,l,a,c,s,e){var o,u,r=n.escapeExpression,i=l??(n.nullContext||{}),t=n.hooks.helperMissing,f="function",p=n.lookupProperty||function(m,v){if(Object.prototype.hasOwnProperty.call(m,v))return m[v]};return`
  • - '+((o=(u=(u=p(a,"id")||(l!=null?p(l,"id"):l))!=null?u:t,typeof u===f?u.call(i,{name:"id",hash:{},data:s,blockParams:e,loc:{start:{line:39,column:56},end:{line:39,column:64}}}):u))!=null?o:"")+` -
  • -`},26:function(n,l,a,c,s,e){var o,u=n.lookupProperty||function(r,i){if(Object.prototype.hasOwnProperty.call(r,i))return r[i]};return`
  • - Summary -
  • -`},28:function(n,l,a,c,s,e){var o,u=n.lambda,r=n.escapeExpression,i=n.lookupProperty||function(t,f){if(Object.prototype.hasOwnProperty.call(t,f))return t[f]};return`
  • - - `+r(u((o=e[0][0])!=null?i(o,"name"):o,l))+` - - -
      -`+((o=i(a,"each").call(l??(n.nullContext||{}),(o=e[0][0])!=null?i(o,"nodes"):o,{name:"each",hash:{},fn:n.program(29,s,0,e),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:57,column:14},end:{line:61,column:23}}}))!=null?o:"")+`
    -
  • -`},29:function(n,l,a,c,s,e){var o,u,r=n.escapeExpression,i=l??(n.nullContext||{}),t=n.hooks.helperMissing,f="function",p=n.lookupProperty||function(m,v){if(Object.prototype.hasOwnProperty.call(m,v))return m[v]};return`
  • - '+r((u=(u=p(a,"id")||(l!=null?p(l,"id"):l))!=null?u:t,typeof u===f?u.call(i,{name:"id",hash:{},data:s,blockParams:e,loc:{start:{line:59,column:89},end:{line:59,column:95}}}):u))+` -
  • -`},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s,e,o){var u,r=n.lookupProperty||function(i,t){if(Object.prototype.hasOwnProperty.call(i,t))return i[t]};return(u=r(a,"each").call(l??(n.nullContext||{}),l!=null?r(l,"nodes"):l,{name:"each",hash:{},fn:n.program(1,s,2,e,o),inverse:n.noop,data:s,blockParams:e,loc:{start:{line:1,column:0},end:{line:68,column:9}}}))!=null?u:""},useData:!0,useDepths:!0,useBlockParams:!0}),y["tooltip-body"]=d({1:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return`
    - `+n.escapeExpression(n.lambda((e=l!=null?o(l,"hint"):l)!=null?o(e,"description"):e,l))+` -
    -`},3:function(n,l,a,c,s){var e,o=n.lambda,u=n.escapeExpression,r=n.lookupProperty||function(i,t){if(Object.prototype.hasOwnProperty.call(i,t))return i[t]};return`
    -

    - `+u(o((e=l!=null?r(l,"hint"):l)!=null?r(e,"title"):e,l))+` -
    `+u(o((e=l!=null?r(l,"hint"):l)!=null?r(e,"version"):e,l))+`
    -

    -
    -`+((e=r(a,"if").call(l??(n.nullContext||{}),(e=l!=null?r(l,"hint"):l)!=null?r(e,"description"):e,{name:"if",hash:{},fn:n.program(4,s,0),inverse:n.noop,data:s,loc:{start:{line:12,column:2},end:{line:16,column:9}}}))!=null?e:"")},4:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return`
    - `+n.escapeExpression(n.lambda((e=l!=null?o(l,"hint"):l)!=null?o(e,"description"):e,l))+` -
    -`},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return(e=o(a,"if").call(l??(n.nullContext||{}),l!=null?o(l,"isPlain"):l,{name:"if",hash:{},fn:n.program(1,s,0),inverse:n.program(3,s,0),data:s,loc:{start:{line:1,column:0},end:{line:17,column:7}}}))!=null?e:""},useData:!0}),y["tooltip-layout"]=d({compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){return`
    -
    -
    -`},useData:!0}),y["versions-dropdown"]=d({1:function(n,l,a,c,s){var e,o,u=l??(n.nullContext||{}),r=n.hooks.helperMissing,i="function",t=n.escapeExpression,f=n.lookupProperty||function(p,m){if(Object.prototype.hasOwnProperty.call(p,m))return p[m]};return' -`},2:function(n,l,a,c,s){return" selected disabled"},compiler:[8,">= 4.3.0"],main:function(n,l,a,c,s){var e,o=n.lookupProperty||function(u,r){if(Object.prototype.hasOwnProperty.call(u,r))return u[r]};return`
    - -
    -`},useData:!0})})();})(); diff --git a/doc/dist/html-XN2TSG4M.js b/doc/dist/html-XN2TSG4M.js deleted file mode 100644 index 48d9711..0000000 --- a/doc/dist/html-XN2TSG4M.js +++ /dev/null @@ -1,52 +0,0 @@ -(()=>{var Cn=Object.create;var Ze=Object.defineProperty;var Pn=Object.getOwnPropertyDescriptor;var Rn=Object.getOwnPropertyNames;var An=Object.getPrototypeOf,Qn=Object.prototype.hasOwnProperty;var et=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Nn=(e,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of Rn(t))!Qn.call(e,r)&&r!==n&&Ze(e,r,{get:()=>t[r],enumerable:!(i=Pn(t,r))||i.enumerable});return e};var tt=(e,t,n)=>(n=e!=null?Cn(An(e)):{},Nn(t||!e||!e.__esModule?Ze(n,"default",{value:e,enumerable:!0}):n,e));var mt=et((ds,pt)=>{var ht="Expected a function",dt=NaN,$n="[object Symbol]",Un=/^\s+|\s+$/g,Wn=/^[-+]0x[0-9a-f]+$/i,Gn=/^0b[01]+$/i,Kn=/^0o[0-7]+$/i,Yn=parseInt,Jn=typeof global=="object"&&global&&global.Object===Object&&global,Xn=typeof self=="object"&&self&&self.Object===Object&&self,Zn=Jn||Xn||Function("return this")(),ei=Object.prototype,ti=ei.toString,ni=Math.max,ii=Math.min,ke=function(){return Zn.Date.now()};function ri(e,t,n){var i,r,s,o,a,u,l=0,f=!1,y=!1,g=!0;if(typeof e!="function")throw new TypeError(ht);t=ft(t)||0,pe(n)&&(f=!!n.leading,y="maxWait"in n,s=y?ni(ft(n.maxWait)||0,t):s,g="trailing"in n?!!n.trailing:g);function L(x){var C=i,D=r;return i=r=void 0,l=x,o=e.apply(D,C),o}function S(x){return l=x,a=setTimeout(p,t),f?L(x):o}function T(x){var C=x-u,D=x-l,V=t-C;return y?ii(V,s-D):V}function m(x){var C=x-u,D=x-l;return u===void 0||C>=t||C<0||y&&D>=s}function p(){var x=ke();if(m(x))return _(x);a=setTimeout(p,T(x))}function _(x){return a=void 0,g&&i?L(x):(i=r=void 0,o)}function w(){a!==void 0&&clearTimeout(a),l=0,i=u=r=a=void 0}function Q(){return a===void 0?o:_(ke())}function N(){var x=ke(),C=m(x);if(i=arguments,r=this,u=x,C){if(a===void 0)return S(u);if(y)return a=setTimeout(p,t),L(u)}return a===void 0&&(a=setTimeout(p,t)),o}return N.cancel=w,N.flush=Q,N}function si(e,t,n){var i=!0,r=!0;if(typeof e!="function")throw new TypeError(ht);return pe(n)&&(i="leading"in n?!!n.leading:i,r="trailing"in n?!!n.trailing:r),ri(e,t,{leading:i,maxWait:t,trailing:r})}function pe(e){var t=typeof e;return!!e&&(t=="object"||t=="function")}function oi(e){return!!e&&typeof e=="object"}function ai(e){return typeof e=="symbol"||oi(e)&&ti.call(e)==$n}function ft(e){if(typeof e=="number")return e;if(ai(e))return dt;if(pe(e)){var t=typeof e.valueOf=="function"?e.valueOf():e;e=pe(t)?t+"":t}if(typeof e!="string")return e===0?e:+e;e=e.replace(Un,"");var n=Gn.test(e);return n||Kn.test(e)?Yn(e.slice(2),n?2:8):Wn.test(e)?dt:+e}pt.exports=si});var Vt=et((Mt,Bt)=>{(function(){var e=function(t){var n=new e.Builder;return n.pipeline.add(e.trimmer,e.stopWordFilter,e.stemmer),n.searchPipeline.add(e.stemmer),t.call(n,n),n.build()};e.version="2.3.9";e.utils={},e.utils.warn=function(t){return function(n){t.console&&console.warn&&console.warn(n)}}(this),e.utils.asString=function(t){return t==null?"":t.toString()},e.utils.clone=function(t){if(t==null)return t;for(var n=Object.create(null),i=Object.keys(t),r=0;r0){var f=e.utils.clone(n)||{};f.position=[a,l],f.index=s.length,s.push(new e.Token(i.slice(a,o),f))}a=o+1}}return s},e.tokenizer.separator=/[\s\-]+/;e.Pipeline=function(){this._stack=[]},e.Pipeline.registeredFunctions=Object.create(null),e.Pipeline.registerFunction=function(t,n){n in this.registeredFunctions&&e.utils.warn("Overwriting existing registered function: "+n),t.label=n,e.Pipeline.registeredFunctions[t.label]=t},e.Pipeline.warnIfFunctionNotRegistered=function(t){var n=t.label&&t.label in this.registeredFunctions;n||e.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. -`,t)},e.Pipeline.load=function(t){var n=new e.Pipeline;return t.forEach(function(i){var r=e.Pipeline.registeredFunctions[i];if(r)n.add(r);else throw new Error("Cannot load unregistered function: "+i)}),n},e.Pipeline.prototype.add=function(){var t=Array.prototype.slice.call(arguments);t.forEach(function(n){e.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},e.Pipeline.prototype.after=function(t,n){e.Pipeline.warnIfFunctionNotRegistered(n);var i=this._stack.indexOf(t);if(i==-1)throw new Error("Cannot find existingFn");i=i+1,this._stack.splice(i,0,n)},e.Pipeline.prototype.before=function(t,n){e.Pipeline.warnIfFunctionNotRegistered(n);var i=this._stack.indexOf(t);if(i==-1)throw new Error("Cannot find existingFn");this._stack.splice(i,0,n)},e.Pipeline.prototype.remove=function(t){var n=this._stack.indexOf(t);n!=-1&&this._stack.splice(n,1)},e.Pipeline.prototype.run=function(t){for(var n=this._stack.length,i=0;i1&&(ot&&(i=s),o!=t);)r=i-n,s=n+Math.floor(r/2),o=this.elements[s*2];if(o==t||o>t)return s*2;if(ou?f+=2:a==u&&(n+=i[l+1]*r[f+1],l+=2,f+=2);return n},e.Vector.prototype.similarity=function(t){return this.dot(t)/this.magnitude()||0},e.Vector.prototype.toArray=function(){for(var t=new Array(this.elements.length/2),n=1,i=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new e.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),r.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new e.TokenSet;s.node.edges["*"]=u}if(s.str.length==0&&(u.final=!0),r.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&r.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new e.TokenSet;s.node.edges["*"]=l}s.str.length==1&&(l.final=!0),r.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var f=s.str.charAt(0),y=s.str.charAt(1),g;y in s.node.edges?g=s.node.edges[y]:(g=new e.TokenSet,s.node.edges[y]=g),s.str.length==1&&(g.final=!0),r.push({node:g,editsRemaining:s.editsRemaining-1,str:f+s.str.slice(2)})}}}return i},e.TokenSet.fromString=function(t){for(var n=new e.TokenSet,i=n,r=0,s=t.length;r=t;n--){var i=this.uncheckedNodes[n],r=i.child.toString();r in this.minimizedNodes?i.parent.edges[i.char]=this.minimizedNodes[r]:(i.child._str=r,this.minimizedNodes[r]=i.child),this.uncheckedNodes.pop()}};e.Index=function(t){this.invertedIndex=t.invertedIndex,this.fieldVectors=t.fieldVectors,this.tokenSet=t.tokenSet,this.fields=t.fields,this.pipeline=t.pipeline},e.Index.prototype.search=function(t){return this.query(function(n){var i=new e.QueryParser(t,n);i.parse()})},e.Index.prototype.query=function(t){for(var n=new e.Query(this.fields),i=Object.create(null),r=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=t},e.Builder.prototype.k1=function(t){this._k1=t},e.Builder.prototype.add=function(t,n){var i=t[this._ref],r=Object.keys(this._fields);this._documents[i]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return e.QueryLexer.EOS;var t=this.str.charAt(this.pos);return this.pos+=1,t},e.QueryLexer.prototype.width=function(){return this.pos-this.start},e.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},e.QueryLexer.prototype.backup=function(){this.pos-=1},e.QueryLexer.prototype.acceptDigitRun=function(){var t,n;do t=this.next(),n=t.charCodeAt(0);while(n>47&&n<58);t!=e.QueryLexer.EOS&&this.backup()},e.QueryLexer.prototype.more=function(){return this.pos1&&(t.backup(),t.emit(e.QueryLexer.TERM)),t.ignore(),t.more())return e.QueryLexer.lexText},e.QueryLexer.lexEditDistance=function(t){return t.ignore(),t.acceptDigitRun(),t.emit(e.QueryLexer.EDIT_DISTANCE),e.QueryLexer.lexText},e.QueryLexer.lexBoost=function(t){return t.ignore(),t.acceptDigitRun(),t.emit(e.QueryLexer.BOOST),e.QueryLexer.lexText},e.QueryLexer.lexEOS=function(t){t.width()>0&&t.emit(e.QueryLexer.TERM)},e.QueryLexer.termSeparator=e.tokenizer.separator,e.QueryLexer.lexText=function(t){for(;;){var n=t.next();if(n==e.QueryLexer.EOS)return e.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){t.escapeCharacter();continue}if(n==":")return e.QueryLexer.lexField;if(n=="~")return t.backup(),t.width()>0&&t.emit(e.QueryLexer.TERM),e.QueryLexer.lexEditDistance;if(n=="^")return t.backup(),t.width()>0&&t.emit(e.QueryLexer.TERM),e.QueryLexer.lexBoost;if(n=="+"&&t.width()===1||n=="-"&&t.width()===1)return t.emit(e.QueryLexer.PRESENCE),e.QueryLexer.lexText;if(n.match(e.QueryLexer.termSeparator))return e.QueryLexer.lexTerm}},e.QueryParser=function(t,n){this.lexer=new e.QueryLexer(t),this.query=n,this.currentClause={},this.lexemeIdx=0},e.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var t=e.QueryParser.parseClause;t;)t=t(this);return this.query},e.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},e.QueryParser.prototype.consumeLexeme=function(){var t=this.peekLexeme();return this.lexemeIdx+=1,t},e.QueryParser.prototype.nextClause=function(){var t=this.currentClause;this.query.clause(t),this.currentClause={}},e.QueryParser.parseClause=function(t){var n=t.peekLexeme();if(n!=null)switch(n.type){case e.QueryLexer.PRESENCE:return e.QueryParser.parsePresence;case e.QueryLexer.FIELD:return e.QueryParser.parseField;case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var i="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(i+=" with value '"+n.str+"'"),new e.QueryParseError(i,n.start,n.end)}},e.QueryParser.parsePresence=function(t){var n=t.consumeLexeme();if(n!=null){switch(n.str){case"-":t.currentClause.presence=e.Query.presence.PROHIBITED;break;case"+":t.currentClause.presence=e.Query.presence.REQUIRED;break;default:var i="unrecognised presence operator'"+n.str+"'";throw new e.QueryParseError(i,n.start,n.end)}var r=t.peekLexeme();if(r==null){var i="expecting term or field, found nothing";throw new e.QueryParseError(i,n.start,n.end)}switch(r.type){case e.QueryLexer.FIELD:return e.QueryParser.parseField;case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var i="expecting term or field, found '"+r.type+"'";throw new e.QueryParseError(i,r.start,r.end)}}},e.QueryParser.parseField=function(t){var n=t.consumeLexeme();if(n!=null){if(t.query.allFields.indexOf(n.str)==-1){var i=t.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),r="unrecognised field '"+n.str+"', possible fields: "+i;throw new e.QueryParseError(r,n.start,n.end)}t.currentClause.fields=[n.str];var s=t.peekLexeme();if(s==null){var r="expecting term, found nothing";throw new e.QueryParseError(r,n.start,n.end)}switch(s.type){case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var r="expecting term, found '"+s.type+"'";throw new e.QueryParseError(r,s.start,s.end)}}},e.QueryParser.parseTerm=function(t){var n=t.consumeLexeme();if(n!=null){t.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(t.currentClause.usePipeline=!1);var i=t.peekLexeme();if(i==null){t.nextClause();return}switch(i.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+i.type+"'";throw new e.QueryParseError(r,i.start,i.end)}}},e.QueryParser.parseEditDistance=function(t){var n=t.consumeLexeme();if(n!=null){var i=parseInt(n.str,10);if(isNaN(i)){var r="edit distance must be numeric";throw new e.QueryParseError(r,n.start,n.end)}t.currentClause.editDistance=i;var s=t.peekLexeme();if(s==null){t.nextClause();return}switch(s.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+s.type+"'";throw new e.QueryParseError(r,s.start,s.end)}}},e.QueryParser.parseBoost=function(t){var n=t.consumeLexeme();if(n!=null){var i=parseInt(n.str,10);if(isNaN(i)){var r="boost must be numeric";throw new e.QueryParseError(r,n.start,n.end)}t.currentClause.boost=i;var s=t.peekLexeme();if(s==null){t.nextClause();return}switch(s.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+s.type+"'";throw new e.QueryParseError(r,s.start,s.end)}}},function(t,n){typeof define=="function"&&define.amd?define(n):typeof Mt=="object"?Bt.exports=n():t.lunr=n()}(this,function(){return e})})()});Handlebars.registerHelper("groupChanged",function(e,t,n){let i=t||"";if(e.group!==i)return delete e.nestedContext,e.group=i,n.fn(this)});Handlebars.registerHelper("nestingChanged",function(e,t,n){if(t.nested_context&&t.nested_context!==e.nestedContext){if(e.nestedContext=t.nested_context,e.lastModuleSeenInGroup!==t.nested_context)return n.fn(this)}else e.lastModuleSeenInGroup=t.title});Handlebars.registerHelper("showSections",function(e,t){if(e.sections.length>0)return t.fn(this)});Handlebars.registerHelper("showSummary",function(e,t){if(e.nodeGroups)return t.fn(this)});Handlebars.registerHelper("isArray",function(e,t){return Array.isArray(e)?t.fn(this):t.inverse(this)});Handlebars.registerHelper("isNonEmptyArray",function(e,t){return Array.isArray(e)&&e.length>0?t.fn(this):t.inverse(this)});Handlebars.registerHelper("isLocal",function(e,t){let n=window.location.pathname.split("/").pop();return e+".html"===n?t.fn(this):t.inverse(this)});var c=document.querySelector.bind(document),k=document.querySelectorAll.bind(document);function nt(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function de(e){return String(e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function re(){return document.body.dataset.type}function it(e,t){if(!!e){for(let n of e){let i=n.nodeGroups&&n.nodeGroups.find(r=>r.nodes.some(s=>s.anchor===t));if(i)return i.key}return null}}function fe(){return window.location.hash.replace(/^#/,"")}function rt(e){return new URLSearchParams(window.location.search).get(e)}function st(e){return fetch(e).then(t=>t.ok).catch(()=>!1)}function ot(e){document.readyState!=="loading"?e():document.addEventListener("DOMContentLoaded",e)}function K(e){return!e||e.trim()===""}function at(e,t){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>{n=null,e(...r)},t)}}function he(){return document.head.querySelector("meta[name=project][content]").content}var ct="ex_doc:settings",Dn={tooltips:!0,theme:null,livebookUrl:null},we=class{constructor(){this._subscribers=[],this._settings=Dn,this._loadSettings()}get(){return this._settings}update(t){let n=this._settings;this._settings={...this._settings,...t},this._subscribers.forEach(i=>i(this._settings,n)),this._storeSettings()}getAndSubscribe(t){this._subscribers.push(t),t(this._settings)}_loadSettings(){try{let t=localStorage.getItem(ct);if(t){let n=JSON.parse(t);this._settings={...this._settings,...n}}this._loadSettingsLegacy()}catch(t){console.error(`Failed to load settings: ${t}`)}}_storeSettings(){try{this._storeSettingsLegacy(),localStorage.setItem(ct,JSON.stringify(this._settings))}catch(t){console.error(`Failed to persist settings: ${t}`)}}_loadSettingsLegacy(){localStorage.getItem("tooltipsDisabled")!==null&&(this._settings={...this._settings,tooltips:!1}),localStorage.getItem("night-mode")==="true"&&(this._settings={...this._settings,nightMode:!0}),this._settings.nightMode===!0&&(this._settings={...this._settings,theme:"dark"})}_storeSettingsLegacy(){this._settings.tooltips?localStorage.removeItem("tooltipsDisabled"):localStorage.setItem("tooltipsDisabled","true"),this._settings.nightMode!==null?localStorage.setItem("night-mode",this._settings.nightMode===!0?"true":"false"):localStorage.removeItem("night-mode"),this._settings.theme!==null?(localStorage.setItem("night-mode",this._settings.theme==="dark"?"true":"false"),this._settings.nightMode=this._settings.theme==="dark"):(delete this._settings.nightMode,localStorage.removeItem("night-mode"))}},O=new we;var Hn=".content",lt=".content-inner",Fn=".livebook-badge";function ut(){Mn(),Vn(),jn(),Bn()}function Mn(){c(Hn).querySelectorAll("a").forEach(e=>{e.querySelector("code, img")&&e.classList.add("no-underline")})}function Bn(){["warning","info","error","neutral","tip"].forEach(t=>{k(`blockquote h3.${t}, blockquote h4.${t}`).forEach(n=>{n.closest("blockquote").classList.add(t)})})}function Vn(){c(lt).setAttribute("tabindex",-1),c(lt).focus()}function jn(){let t=window.location.pathname.replace(/\.html$/,".livemd"),n=new URL(t,window.location.href).toString();O.getAndSubscribe(i=>{let r=i.livebookUrl?zn(i.livebookUrl,n):qn(n);for(let s of k(Fn))s.href=r})}function qn(e){return`https://livebook.dev/run?url=${encodeURIComponent(e)}`}function zn(e,t){return`${e}/import?url=${encodeURIComponent(t)}`}var gt=tt(mt());var ci=768,yt=300,li=".sidebar-toggle",ui=".content",H={CLOSED:"closed",OPEN:"open",NO_PREF:"no_pref"},F={opened:"sidebar-opened",opening:"sidebar-opening",closed:"sidebar-closed",closing:"sidebar-closing"},di=Object.values(F),P={togglingTimeout:null,lastWindowWidth:window.innerWidth,sidebarPreference:H.NO_PREF};function vt(){St(),fi()}function St(){if(sessionStorage.getItem("sidebar_state")==="closed")return Y(F.closed);Y(xt()?F.closed:F.opened)}function xt(){return window.matchMedia(`screen and (max-width: ${ci}px)`).matches}function Y(...e){document.body.classList.remove(...di),document.body.classList.add(...e)}function fi(){c(li).addEventListener("click",e=>{_e(),mi()}),c(ui).addEventListener("click",e=>{pi()}),window.addEventListener("resize",(0,gt.default)(e=>{hi()},100))}function _e(){return Oe()?Et():Ie()}function Oe(){return document.body.classList.contains(F.opened)||document.body.classList.contains(F.opening)}function Ie(){return bt(),Y(F.opening),sessionStorage.setItem("sidebar_state","opened"),new Promise((e,t)=>{P.togglingTimeout=setTimeout(()=>{Y(F.opened),e()},yt)})}function Et(){return bt(),Y(F.closing),sessionStorage.setItem("sidebar_state","closed"),new Promise((e,t)=>{P.togglingTimeout=setTimeout(()=>{Y(F.closed),e()},yt)})}function bt(){P.togglingTimeout&&(clearTimeout(P.togglingTimeout),P.togglingTimeout=null)}function hi(){P.lastWindowWidth!==window.innerWidth&&(P.lastWindowWidth=window.innerWidth,(P.sidebarPreference===H.OPEN||P.sidebarPreference===H.NO_PREF)&&St())}function pi(){xt()&&Oe()&&Et()}function mi(){switch(P.sidebarPreference){case H.OPEN:P.sidebarPreference=H.CLOSED;break;case H.CLOSED:P.sidebarPreference=H.OPEN;break;case H.NO_PREF:Oe()?P.sidebarPreference=H.OPEN:P.sidebarPreference=H.CLOSED}}function J(){return window.sidebarNodes||{}}function Lt(){return window.versionNodes||[]}var Ce={search:"search",extras:"extras",modules:"modules",tasks:"tasks"},Tt=[Ce.extras,Ce.modules,Ce.tasks],me="#full-list";function wt(){kt(J(),re()),Ot(),_t(),yi()}function kt(e,t){let n=e[t]||[],i=c(me),r=Handlebars.templates["sidebar-items"]({nodes:n,group:""});i.innerHTML=r,gi(t),i.querySelectorAll("ul").forEach(s=>{if(s.innerHTML.trim()===""){let o=s.previousElementSibling;o.classList.contains("expand")&&o.classList.remove("expand")}}),i.querySelectorAll("li a").forEach(s=>{s.addEventListener("click",o=>{let a=o.target,u=a.closest("li"),l=i.querySelector(".current-section");if(a.matches(".icon-expand")){o.preventDefault(),u.classList.toggle("open");return}l&&l.classList.remove("current-section"),s.matches(".expand")&&s.pathname===window.location.pathname&&u.classList.add("open")})})}function gi(e){Tt.forEach(t=>{let n=c(`#${t}-list-link`);n&&n.parentElement.classList.toggle("selected",t===e)})}function _t(){let e=c(me),t=e.querySelector("li.current-page");t&&(t.scrollIntoView(),e.scrollTop-=40)}function Ot(){let e=fe()||"content",n=J()[re()]||[],i=it(n,e),r=c(me),s=r.querySelector(`li.current-page a.expand[href$="#${i}"]`);s&&s.closest("li").classList.add("open");let o=r.querySelector(`li.current-page a[href$="#${e}"]`);if(o){let a=o.closest("ul");a.classList.contains("deflist")&&a.closest("li").classList.add("current-section"),o.closest("li").classList.add("current-hash")}}function yi(){Tt.forEach(e=>{let t=c(`#${e}-list-link`);t&&t.addEventListener("click",n=>{n.preventDefault(),kt(J(),e),_t()})}),window.addEventListener("hashchange",e=>{let n=c(me).querySelector("li.current-page li.current-hash");n&&n.classList.remove("current-hash"),Ot()})}var $={module:"module",moduleChild:"module-child",mixTask:"mix-task",extra:"extra"};function Ct(e,t=5){if(K(e))return[];let n=J(),i=[...Pe(n.modules,e,$.module),...vi(n.modules,e,$.moduleChild),...Pe(n.tasks,e,$.mixTask),...Pe(n.extras,e,$.extra)];return Li(i).slice(0,t)}function Pe(e,t,n){return e.map(i=>Si(i,t,n)).filter(i=>i!==null)}function vi(e,t,n){return e.filter(i=>i.nodeGroups).flatMap(i=>i.nodeGroups.flatMap(({key:r,nodes:s})=>{let o=bi(r);return s.map(a=>xi(a,i.id,t,n,o)||Ei(a,i.id,t,n,o))})).filter(i=>i!==null)}function Si(e,t,n){return Re(e.title,t)?{link:`${e.id}.html`,title:Qe(e.title,t),label:null,description:null,matchQuality:Ae(e.title,t),category:n}:null}function xi(e,t,n,i,r){return Re(e.id,n)?{link:`${t}.html#${e.anchor}`,title:Qe(e.id,n),label:r,description:t,matchQuality:Ae(e.id,n),category:i}:null}function Ei(e,t,n,i,r){let s=`${t}.${e.id}`;if(!Re(s,n))return null;let o=n.replace(/\./g," ");return Ti(e.id,o)?{link:`${t}.html#${e.anchor}`,title:Qe(e.id,o),label:r,description:t,matchQuality:Ae(s,n),category:i}:null}function bi(e){switch(e){case"callbacks":return"callback";case"types":return"type";default:return null}}function Li(e){return e.slice().sort((t,n)=>t.matchQuality!==n.matchQuality?n.matchQuality-t.matchQuality:It(t.category)-It(n.category))}function It(e){switch(e){case $.module:return 1;case $.moduleChild:return 2;case $.mixTask:return 3;default:return 4}}function Ti(e,t){return ye(t).some(i=>Pt(e,i))}function Re(e,t){return ye(t).every(i=>Pt(e,i))}function Pt(e,t){return e.toLowerCase().includes(t.toLowerCase())}function Ae(e,t){let n=ye(t),r=n.map(o=>o.length).reduce((o,a)=>o+a,0)/e.length,s=wi(e,n[0])?1:0;return r+s}function wi(e,t){return e.toLowerCase().startsWith(t.toLowerCase())}function ye(e){return e.trim().split(/\s+/)}function Qe(e,t){let n=ye(t).sort((i,r)=>r.length-i.length);return ge(e,n)}function ge(e,t){if(t.length===0)return e;let[n,...i]=t,r=e.match(new RegExp(`(.*)(${nt(n)})(.*)`,"i"));if(r){let[,s,o,a]=r;return ge(s,t)+""+de(o)+""+ge(a,t)}else return ge(e,i)}var X=".autocomplete",ve=".autocomplete-suggestion",M={autocompleteSuggestions:[],selectedIdx:-1};function ki(){c(X).classList.add("shown")}function Ne(){c(X).classList.remove("shown")}function Rt(){return c(X).classList.contains("shown")}function De(e){M.autocompleteSuggestions=Ct(e),M.selectedIdx=-1,K(e)?Ne():(_i({term:e,suggestions:M.autocompleteSuggestions}),Se(0),ki())}function _i({term:e,suggestions:t}){let n=Handlebars.templates["autocomplete-suggestions"]({suggestions:t,term:e}),i=c(X);i.innerHTML=n}function At(){return M.selectedIdx===-1?null:M.autocompleteSuggestions[M.selectedIdx]}function Se(e){M.selectedIdx=Oi(e);let t=c(`${ve}.selected`),n=c(`${ve}[data-index="${M.selectedIdx}"]`);t&&t.classList.remove("selected"),n&&n.classList.add("selected")}function Oi(e){let t=M.autocompleteSuggestions.length+1;return(M.selectedIdx+e+1+t)%t-1}var se="form.sidebar-search input",Ii="form.sidebar-search .search-close-button";function Qt(){Ci()}function Nt(e){let t=c(se);t.value=e}function Dt(){c(se).focus()}function Ci(){let e=c(se);e.addEventListener("keydown",t=>{t.key==="Escape"?(xe(),e.blur()):t.key==="Enter"?Pi(t):t.key==="ArrowUp"?(Se(-1),t.preventDefault()):t.key==="ArrowDown"&&(Se(1),t.preventDefault())}),e.addEventListener("input",t=>{De(t.target.value)}),e.addEventListener("focus",t=>{document.body.classList.add("search-focused"),De(t.target.value)}),e.addEventListener("blur",t=>{let n=t.relatedTarget;if(n){if(n.matches(ve))return setTimeout(()=>{Rt()&&e.focus()},1e3),null;n.matches(Ii)&&xe()}He()}),c(X).addEventListener("click",t=>{t.shiftKey||t.ctrlKey?e.focus():(xe(),He())})}function Pi(e){let t=c(se),n=e.shiftKey||e.ctrlKey,i=At();e.preventDefault();let r=n?"_blank":"_self",s=document.createElement("a");s.setAttribute("target",r),i?s.setAttribute("href",i.link):s.setAttribute("href",`search.html?q=${encodeURIComponent(t.value)}`),s.click(),n||(xe(),He())}function xe(){let e=c(se);e.value=""}function He(){document.body.classList.remove("search-focused"),Ne()}var Ht=".sidebar-projectVersion",Ri=".sidebar-projectVersionsDropdown";function Ft(){let e=Lt();if(e.length>0){let n=c(Ht).textContent.trim(),i=Qi(e,n);Ai({nodes:i})}}function Ai({nodes:e}){let t=c(Ht),n=Handlebars.templates["versions-dropdown"]({nodes:e});t.innerHTML=n,c(Ri).addEventListener("change",Di)}function Qi(e,t){return Ni(e,t).map(i=>({...i,isCurrentVersion:i.version===t}))}function Ni(e,t){return e.some(i=>i.version===t)?e:[{version:t,url:"#"},...e]}function Di(e){let t=e.target.value,n=window.location.pathname.split("/").pop()+window.location.hash,i=`${t}/${n}`;st(i).then(r=>{r?window.location.href=i:window.location.href=t})}var I=tt(Vt());var Ee=80,Hi="#search";function qt(){if(window.location.pathname.endsWith("/search.html")){let e=rt("q");Fi(e)}}function Fi(e){if(K(e))Fe({value:e});else{Nt(e);let t=Mi();try{let n=Ui(t.search(e));Fe({value:e,results:n})}catch(n){Fe({value:e,errorMessage:n.message})}}}function Fe({value:e,results:t,errorMessage:n}){let i=c(Hi),r=Handlebars.templates["search-results"]({value:e,results:t,errorMessage:n});i.innerHTML=r}function Mi(){I.default.QueryLexer.termSeparator=/\s+/,I.default.Pipeline.registerFunction(Me,"elixirTokenSplitter"),I.default.Pipeline.registerFunction(Be,"elixirTrimmer"),I.default.Pipeline.registerFunction(Ve,"hyphenSearch");let e=Bi();if(e)return e;let t=ji();return Vi(t),t}function Bi(){try{let e=sessionStorage.getItem(zt());return e?I.default.Index.load(JSON.parse(e)):null}catch(e){return console.error("Failed to load index: ",e),null}}function Vi(e){try{let t=JSON.stringify(e);sessionStorage.setItem(zt(),t)}catch(t){console.error("Failed to save index: ",t)}}function zt(){return`index:${he()}`}function ji(){return(0,I.default)(function(){this.tokenizer.separator=/\s+/,this.ref("ref"),this.field("title",{boost:3}),this.field("doc"),this.metadataWhitelist=["position"],this.pipeline.remove(I.default.stopWordFilter),this.use($i),this.use(qi),this.pipeline.remove(I.default.trimmer),this.use(zi),searchNodes.forEach(e=>{this.add(e)})})}function qi(e){e.pipeline.before(I.default.stemmer,Me),e.searchPipeline.before(I.default.stemmer,Me)}function Me(e){let t=e.toString().split(/\.|\/|_/).map(n=>e.clone().update(()=>n));return t.length>1?[...t,e]:t}function zi(e){e.pipeline.after(I.default.stemmer,Be),e.searchPipeline.after(I.default.stemmer,Be)}function Be(e){return e.update(function(t){return t.replace(/^@?\W+/,"").replace(/\W+$/,"")})}function Ve(e){if(e.toString().indexOf("-")<0)return e;let n=[];return n.push(e.clone(function(i){return i.replace("-","")})),n.push(e),n}function $i(e){e.pipeline.before(I.default.stemmer,Ve),e.searchPipeline.before(I.default.stemmer,Ve)}function Ui(e){return e.filter(t=>jt(t.ref)).map(t=>{let n=jt(t.ref),i=t.matchData.metadata;return{...n,metadata:i,excerpts:Wi(n,i)}})}function jt(e){return searchNodes.find(t=>t.ref===e)||null}function Wi(e,t){let{doc:n}=e,r=Object.keys(t).filter(s=>"doc"in t[s]).map(s=>t[s].doc.position.map(([o,a])=>Gi(n,o,a))).reduce((s,o)=>s.concat(o),[]);return r.length===0?[n.slice(0,Ee*2)+(Ee*20?"...":"",e.slice(i,t),""+de(e.slice(t,t+n))+"",e.slice(t+n,r),r{clearTimeout(be),e.target.classList.remove("show")});function je(e){Z&&(clearTimeout(be),Z.innerText=e,Z.classList.add("show"),be=setTimeout(()=>{Z.classList.remove("show"),be=setTimeout(function(){Z.innerText=""},1e3)},5e3))}var $t="dark",qe=["system","dark","light"];function Ut(){O.getAndSubscribe(e=>{document.body.classList.toggle($t,Gt(e))}),Yi()}function Wt(){let t=O.get().theme||"system",n=qe[qe.indexOf(t)+1]||qe[0];O.update({theme:n}),je(`Set theme to "${n}"`)}function Gt(e){return e.theme==="dark"||Ki()&&(e.theme==null||e.theme==="system")}function Ki(){return window.matchMedia("(prefers-color-scheme: dark)").matches}function Yi(){window.matchMedia("(prefers-color-scheme: dark)").addListener(e=>{let t=O.get(),n=Gt(t);(t.theme==null||t.theme==="system")&&(document.body.classList.toggle($t,n),je(`Browser changed theme to "${n?"dark":"light"}"`))})}var Ji="hll";function Yt(){Xi()}function Xi(){k("[data-group-id]").forEach(t=>{let n=t.getAttribute("data-group-id");t.addEventListener("mouseenter",i=>{Kt(n,!0)}),t.addEventListener("mouseleave",i=>{Kt(n,!1)})})}function Kt(e,t){k(`[data-group-id="${e}"]`).forEach(i=>{i.classList.toggle(Ji,t)})}var ee="#modal",Zi="#modal .modal-close",er="#modal .modal-title",tr="#modal .modal-body",Jt='button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',B={prevFocus:null,lastFocus:null,ignoreFocusChanges:!1};function Xt(){nr()}function nr(){let e=Handlebars.templates["modal-layout"]();document.body.insertAdjacentHTML("beforeend",e),c(ee).addEventListener("keydown",t=>{t.key==="Escape"&&oe()}),c(Zi).addEventListener("click",t=>{oe()})}function Zt(e){if(B.ignoreFocusChanges)return;let t=c(ee);if(t.contains(e.target))B.lastFocus=e.target;else{B.ignoreFocusChanges=!0;let n=ir(t);B.lastFocus===n?rr(t).focus():n.focus(),B.ignoreFocusChanges=!1,B.lastFocus=document.activeElement}}function ir(e){return e.querySelector(Jt)}function rr(e){let t=e.querySelectorAll(Jt);return t[t.length-1]}function Le({title:e,body:t}){B.prevFocus=document.activeElement,document.addEventListener("focus",Zt,!0),c(er).innerHTML=e,c(tr).innerHTML=t,c(ee).classList.add("shown"),c(ee).focus()}function oe(){c(ee).classList.remove("shown"),document.addEventListener("focus",Zt,!0),B.prevFocus&&B.prevFocus.focus(),B.prevFocus=null}function en(){return c(ee).classList.contains("shown")}var sr="https://hexdocs.pm/%%",or="https://hex.pm/api/packages?search=name:%%*",ar=".display-quick-switch",ze="#quick-switch-input",nn="#quick-switch-results",cr=".quick-switch-result",lr=300,ur=9,dr=["elixir","eex","ex_unit","hex","iex","logger","mix"].map(e=>({name:e})),rn=2,R={autocompleteResults:[],selectedIdx:null};function sn(){fr()}function fr(){k(ar).forEach(e=>{e.addEventListener("click",t=>{Ue()})})}function hr(e){if(e.key==="Enter"){let t=e.target.value;mr(t),e.preventDefault()}else e.key==="ArrowUp"?(tn(-1),e.preventDefault()):e.key==="ArrowDown"&&(tn(1),e.preventDefault())}function pr(e){let t=e.target.value;if(t.lengthn.json()).then(n=>{Array.isArray(n)&&(R.autocompleteResults=Sr(e,n),R.selectedIdx=null,c(ze).value.length>=rn&&vr({results:R.autocompleteResults}))})}function vr({results:e}){let t=c(nn),n=Handlebars.templates["quick-switch-results"]({results:e});t.innerHTML=n,k(cr).forEach(i=>{i.addEventListener("click",r=>{let s=i.getAttribute("data-index"),o=R.autocompleteResults[s];$e(o.name)})})}function Sr(e,t){return dr.concat(t).filter(n=>n.name.toLowerCase().includes(e.toLowerCase())).filter(n=>n.releases===void 0||n.releases[0].has_docs===!0).slice(0,ur)}function tn(e){R.selectedIdx=xr(e);let t=c(".quick-switch-result.selected"),n=c(`.quick-switch-result[data-index="${R.selectedIdx}"]`);t&&t.classList.remove("selected"),n&&n.classList.add("selected")}function xr(e){let t=R.autocompleteResults.length;if(R.selectedIdx===null){if(e>=0)return 0;if(e<0)return t-1}return(R.selectedIdx+e+t)%t}var Er=".display-settings",br="#settings-modal-content",We="#modal-settings-tab",Ge="#modal-keyboard-shortcuts-tab",an="#settings-content",cn="#keyboard-shortcuts-content",Lr=[{title:"Settings",id:"modal-settings-tab"},{title:"Keyboard shortcuts",id:"modal-keyboard-shortcuts-tab"}];function ln(){Tr()}function Tr(){k(Er).forEach(e=>{e.addEventListener("click",t=>{Ke()})})}function on(){c(Ge).classList.remove("active"),c(We).classList.add("active"),c(an).classList.remove("hidden"),c(cn).classList.add("hidden")}function wr(){c(Ge).classList.add("active"),c(We).classList.remove("active"),c(cn).classList.remove("hidden"),c(an).classList.add("hidden")}function Ke(){Le({title:Lr.map(({id:s,title:o})=>``).join(""),body:Handlebars.templates["settings-modal-body"]({shortcuts:Ye})});let e=c(br),t=e.querySelector('[name="theme"]'),n=e.querySelector('[name="tooltips"]'),i=e.querySelector('[name="direct_livebook_url"]'),r=e.querySelector('[name="livebook_url"]');O.getAndSubscribe(s=>{t.value=s.theme||"system",n.checked=s.tooltips,s.livebookUrl===null?(i.checked=!1,r.classList.add("hidden"),r.tabIndex=-1):(i.checked=!0,r.classList.remove("hidden"),r.tabIndex=0,r.value=s.livebookUrl)}),t.addEventListener("change",s=>{O.update({theme:s.target.value})}),n.addEventListener("change",s=>{O.update({tooltips:s.target.checked})}),i.addEventListener("change",s=>{let o=s.target.checked?r.value:null;O.update({livebookUrl:o})}),r.addEventListener("input",s=>{O.update({livebookUrl:s.target.value})}),c(We).addEventListener("click",s=>{on()}),c(Ge).addEventListener("click",s=>{wr()}),on()}var kr="#settings-modal-content",Ye=[{key:"c",description:"Toggle sidebar",action:_e},{key:"n",description:"Cycle themes",action:Wt},{key:"s",description:"Focus search bar",displayAs:"/ or s",action:un},{key:"/",action:un},{key:"g",description:"Search HexDocs package",displayAs:"g",action:Ue},{key:"?",displayAs:"?",description:"Bring up this modal",action:Cr}],Je={shortcutBeingPressed:null};function dn(){_r()}function _r(){document.addEventListener("keydown",Or),document.addEventListener("keyup",Ir)}function Or(e){if(Je.shortcutBeingPressed||e.target.matches("input, textarea")||e.ctrlKey||e.metaKey||e.altKey)return;let t=Ye.find(n=>n.key===e.key);!t||(Je.shortcutBeingPressed=t,e.preventDefault(),t.action(e))}function Ir(e){Je.shortcutBeingPressed=null}function un(e){oe(),Ie().then(()=>{Dt()})}function Cr(){Pr()?oe():Ke()}function Pr(){return en()&&c(kr)}var U={plain:"plain",function:"function",module:"module"},Rr=[{href:"typespecs.html#basic-types",hint:{kind:U.plain,description:"Basic type"}},{href:"typespecs.html#literals",hint:{kind:U.plain,description:"Literal"}},{href:"typespecs.html#built-in-types",hint:{kind:U.plain,description:"Built-in type"}}],Te={cancelHintFetching:null};function fn(e){if(pn(e))return!0;let t=/#.*\//;return e.includes("#")&&!t.test(e)?!1:e.includes(".html")}function hn(e){let t=pn(e);return t?Promise.resolve(t):Ar(e)}function pn(e){let t=Rr.find(n=>e.includes(n.href));return t?t.hint:null}function Ar(e){let t=e.replace(".html",".html?hint=true");return new Promise((n,i)=>{let r=document.createElement("iframe");r.setAttribute("sandbox","allow-scripts allow-same-origin"),r.setAttribute("src",t),r.style.display="none";function s(a){let{href:u,hint:l}=a.data;t===u&&(o(),n(l))}Te.cancelHintFetching=()=>{o(),i(new Error("cancelled"))};function o(){r.remove(),window.removeEventListener("message",s),Te.cancelHintFetching=null}window.addEventListener("message",s),document.body.appendChild(r)})}function mn(){Te.cancelHintFetching&&Te.cancelHintFetching()}function gn(e){let n=e.querySelector("h1").textContent,i=e.querySelector(".docstring > p"),r=i?i.textContent:"";return{kind:U.function,title:n.trim(),description:r.trim()}}function yn(e){let n=e.querySelector("h1 > span").textContent,i=e.querySelector("#moduledoc p"),r=i?i.textContent:"";return{kind:U.module,title:n.trim(),description:r.trim()}}var Qr=".content a",Xe="#tooltip",Nr="#tooltip .tooltip-body",Sn="body .content-inner",Dr="#content",xn="tooltip-shown",ae=10,Hr=ae*4,vn={height:450,width:768},Fr=100,te={currentLinkElement:null,hoverDelayTimeout:null};function En(){Mr(),Br()}function Mr(){let e=Handlebars.templates["tooltip-layout"]();c(Sn).insertAdjacentHTML("beforeend",e)}function Br(){k(Qr).forEach(e=>{!Vr(e)||(e.addEventListener("mouseenter",t=>{qr(e)}),e.addEventListener("mouseleave",t=>{Wr(e)}))})}function Vr(e){return!(e.classList.contains("detail-link")||jr(e.href)||!fn(e.href))}function jr(e){let t=e.replace(Dr,"");return window.location.href.split("#")[0]===t}function qr(e){!zr()||(te.currentLinkElement=e,te.hoverDelayTimeout=setTimeout(()=>{hn(e.href).then(t=>{$r(t),Ur()}).catch(()=>{})},Fr))}function zr(){let e=window.innerWidthe.firstElementChild&&e.firstElementChild.tagName==="CODE").forEach(e=>e.insertAdjacentHTML("beforeend",is)),Array.from(k(".copy-button")).forEach(e=>{let t;e.addEventListener("click",()=>{t&&clearTimeout(t);let n=Array.from(e.parentElement.querySelector("code").childNodes).filter(i=>!(i.tagName==="SPAN"&&i.classList.contains("unselectable"))).map(i=>i.textContent).join("");navigator.clipboard.writeText(n),e.classList.add("clicked"),t=setTimeout(()=>e.classList.remove("clicked"),3e3)})})}ot(()=>{Ut(),vt(),wt(),Qt(),Ft(),ut(),Yt(),Xt(),dn(),sn(),En(),Tn(),qt(),wn(),ln()});})(); -/*! - * lunr.Builder - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Index - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Pipeline - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Set - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.TokenSet - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.Vector - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.stemmer - * Copyright (C) 2020 Oliver Nightingale - * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt - */ -/*! - * lunr.stopWordFilter - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.tokenizer - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.trimmer - * Copyright (C) 2020 Oliver Nightingale - */ -/*! - * lunr.utils - * Copyright (C) 2020 Oliver Nightingale - */ -/** - * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 - * Copyright (C) 2020 Oliver Nightingale - * @license MIT - */ diff --git a/doc/dist/html-erlang-6FXMBT73.css b/doc/dist/html-erlang-6FXMBT73.css deleted file mode 100644 index 391e389..0000000 --- a/doc/dist/html-erlang-6FXMBT73.css +++ /dev/null @@ -1,2 +0,0 @@ -:root{--main: hsl(0, 100%, 64%);--main-darkened-10: hsl(0, 100%, 54%);--main-darkened-20: hsl(0, 100%, 44%);--main-lightened-05: hsl(0, 100%, 69%);--main-lightened-10: hsl(0, 100%, 74%)}@font-face{font-family:Lato;font-style:normal;font-display:swap;font-weight:300;src:url(./lato-latin-ext-300-normal-VPGGJKJL.woff2) format("woff2"),url(./lato-all-300-normal-GIV56FBX.woff) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Lato;font-style:normal;font-display:swap;font-weight:300;src:url(./lato-latin-300-normal-YUMVEFOL.woff2) format("woff2"),url(./lato-all-300-normal-GIV56FBX.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Lato;font-style:normal;font-display:swap;font-weight:700;src:url(./lato-latin-ext-700-normal-Q2L5DVMW.woff2) format("woff2"),url(./lato-all-700-normal-XMT5XFBS.woff) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Lato;font-style:normal;font-display:swap;font-weight:700;src:url(./lato-latin-700-normal-2XVSBPG4.woff2) format("woff2"),url(./lato-all-700-normal-XMT5XFBS.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Merriweather;font-style:normal;font-display:swap;font-weight:300;src:url(./merriweather-cyrillic-ext-300-normal-5LF5LCEK.woff2) format("woff2"),url(./merriweather-all-300-normal-VL6BT3UN.woff) format("woff");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Merriweather;font-style:normal;font-display:swap;font-weight:300;src:url(./merriweather-cyrillic-300-normal-7PAAHU3N.woff2) format("woff2"),url(./merriweather-all-300-normal-VL6BT3UN.woff) format("woff");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Merriweather;font-style:normal;font-display:swap;font-weight:300;src:url(./merriweather-vietnamese-300-normal-U376L4Z4.woff2) format("woff2"),url(./merriweather-all-300-normal-VL6BT3UN.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Merriweather;font-style:normal;font-display:swap;font-weight:300;src:url(./merriweather-latin-ext-300-normal-K6L27CZ5.woff2) format("woff2"),url(./merriweather-all-300-normal-VL6BT3UN.woff) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Merriweather;font-style:normal;font-display:swap;font-weight:300;src:url(./merriweather-latin-300-normal-RWDJH4FN.woff2) format("woff2"),url(./merriweather-all-300-normal-VL6BT3UN.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Merriweather;font-style:italic;font-display:swap;font-weight:300;src:url(./merriweather-cyrillic-ext-300-italic-JP3ZEV2P.woff2) format("woff2"),url(./merriweather-all-300-italic-CMQNB6FA.woff) format("woff");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Merriweather;font-style:italic;font-display:swap;font-weight:300;src:url(./merriweather-cyrillic-300-italic-M6KMXZSZ.woff2) format("woff2"),url(./merriweather-all-300-italic-CMQNB6FA.woff) format("woff");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Merriweather;font-style:italic;font-display:swap;font-weight:300;src:url(./merriweather-vietnamese-300-italic-EHHNZPUO.woff2) format("woff2"),url(./merriweather-all-300-italic-CMQNB6FA.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Merriweather;font-style:italic;font-display:swap;font-weight:300;src:url(./merriweather-latin-ext-300-italic-MWCA36KE.woff2) format("woff2"),url(./merriweather-all-300-italic-CMQNB6FA.woff) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Merriweather;font-style:italic;font-display:swap;font-weight:300;src:url(./merriweather-latin-300-italic-353COS6Q.woff2) format("woff2"),url(./merriweather-all-300-italic-CMQNB6FA.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inconsolata;font-style:normal;font-display:swap;font-weight:400;src:url(./inconsolata-vietnamese-400-normal-IGQPHHJH.woff2) format("woff2"),url(./inconsolata-all-400-normal-HMVRHNDU.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inconsolata;font-style:normal;font-display:swap;font-weight:400;src:url(./inconsolata-latin-ext-400-normal-K7HVGTP7.woff2) format("woff2"),url(./inconsolata-all-400-normal-HMVRHNDU.woff) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inconsolata;font-style:normal;font-display:swap;font-weight:400;src:url(./inconsolata-latin-400-normal-RGKDDNDD.woff2) format("woff2"),url(./inconsolata-all-400-normal-HMVRHNDU.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inconsolata;font-style:normal;font-display:swap;font-weight:700;src:url(./inconsolata-vietnamese-700-normal-LHEGSN35.woff2) format("woff2"),url(./inconsolata-all-700-normal-WFUKXZPS.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inconsolata;font-style:normal;font-display:swap;font-weight:700;src:url(./inconsolata-latin-ext-700-normal-4MPBLFZC.woff2) format("woff2"),url(./inconsolata-all-700-normal-WFUKXZPS.woff) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inconsolata;font-style:normal;font-display:swap;font-weight:700;src:url(./inconsolata-latin-700-normal-DTS2D7TO.woff2) format("woff2"),url(./inconsolata-all-700-normal-WFUKXZPS.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--serifFontFamily: "Merriweather", "Book Antiqua", Georgia, "Century Schoolbook", serif;--sansFontFamily: "Lato", sans-serif;--monoFontFamily: "Inconsolata", Menlo, Courier, monospace;--baseFontSize: 18px;--baseLineHeight: 1.5em;--gray50-lightened-2: hsl(207, 43%, 98% );--gray50: hsl(207, 43%, 96% );--gray100: hsl(212, 33%, 91% );--gray200: hsl(210, 26%, 84% );--gray300: hsl(210, 21%, 64% );--gray500: hsl(210, 21%, 34% );--gray600: hsl(210, 27%, 26% );--gray700: hsl(212, 35%, 17% );--gray800: hsl(216, 52%, 11% );--gray800-opacity-0: hsla(216, 52%, 11%, 0%);--gray900: hsl(218, 73%, 4% );--coldGrayFaint: hsl(240, 5%, 97% );--coldGrayLight: hsl(240, 5%, 88% );--coldGray-lightened-10: hsl(240, 5%, 56% );--coldGray: hsl(240, 5%, 46% );--coldGrayDark: hsl(240, 5%, 28% );--coldGrayDim: hsl(240, 5%, 18% );--yellowLight: hsl( 60, 100%, 81% );--yellowDark: hsl(60deg 100% 43% / 62%);--yellow: hsl( 60, 100%, 43% );--green-lightened-10: hsl( 90, 100%, 45% );--green: hsl( 90, 100%, 35% );--white: hsl( 0, 0%, 100% );--white-opacity-10: hsla( 0, 0%, 100%, 10%);--black: hsl( 0, 0%, 0% );--black-opacity-10: hsla( 0, 0%, 0%, 10%);--black-opacity-50: hsla( 0, 0%, 0%, 50%)}:root{--background: var(--white);--contrast: var(--black);--textBody: var(--gray700);--textHeaders: var(--gray800);--textDetailBackground: var(--coldGrayFaint);--textFooter: var(--coldGray);--links: var(--black);--linksVisited: var(--black);--linksNoUnderline: var(--main-darkened-10);--linksNoUnderlineVisited: var(--main-darkened-20);--iconAction: var(--coldGray);--iconActionHover: var(--gray800);--blockquoteBackground: var(--coldGrayFaint);--blockquoteBorder: var(--coldGrayLight);--warningBackground: hsl( 33, 100%, 97%);--warningHeadingBackground: hsl( 33, 87%, 64%);--warningHeading: var(--black);--errorBackground: hsl( 7, 81%, 96%);--errorHeadingBackground: hsl( 6, 80%, 60%);--errorHeading: var(--white);--infoBackground: hsl(206, 91%, 96%);--infoHeadingBackground: hsl(213, 92%, 62%);--infoHeading: var(--white);--neutralBackground: hsl(212, 29%, 92%);--neutralHeadingBackground: hsl(220, 43%, 11%);--neutralHeading: var(--white);--tipBackground: hsl(142, 31%, 93%);--tipHeadingBackground: hsl(134, 39%, 36%);--tipHeading: var(--white);--fnSpecAttr: var(--coldGray);--fnDeprecated: var(--yellowLight);--blink: var(--yellowLight);--codeBackground: var(--gray50-lightened-2);--codeBorder: var(--gray100);--inlineCodeBackground: var(--codeBackground);--inlineCodeBorder: var(--codeBorder);--codeScrollThumb: var(--gray300);--codeScrollBackground: var(--codeBorder);--bottomActionsBtnBorder: var(--black-opacity-10);--bottomActionsBtnSubheader: var(--main-darkened-10);--modalBackground: var(--white);--settingsInput: var(--gray500);--settingsInputBackground: var(--white);--settingsInputBorder: var(--gray200);--settingsSectionBorder: var(--gray200);--quickSwitchInput: var(--gray500);--quickSwitchContour: var(--coldGray);--success: var(--green);--sidebarButton: var(--gray100)}body.dark{--background: var(--gray900);--contrast: var(--white);--textBody: var(--gray200);--textHeaders: var(--gray100);--textDetailBackground: var(--gray700);--textFooter: var(--gray300);--links: var(--gray100);--linksVisited: var(--gray100);--linksNoUnderline: var(--main-lightened-10);--linksNoUnderlineVisited: var(--main-lightened-05);--iconAction: var(--coldGray-lightened-10);--iconActionHover: var(--white);--blockquoteBackground: var(--coldGrayDim);--blockquoteBorder: var(--coldGrayDark);--warningBackground: hsl( 40, 67%, 79%);--warningHeadingBackground: hsl( 27, 66%, 29%);--warningHeading: var(--white);--errorBackground: hsl(358, 52%, 78%);--errorHeadingBackground: hsl(349, 55%, 34%);--errorHeading: var(--white);--infoBackground: hsl(222, 57%, 77%);--infoHeadingBackground: hsl(243, 65%, 26%);--infoHeading: var(--white);--neutralBackground: hsl(220, 23%, 82%);--neutralHeadingBackground: hsl(224, 24%, 16%);--neutralHeading: var(--white);--tipBackground: hsl(139, 26%, 69%);--tipHeadingBackground: hsl(158, 35%, 17%);--tipHeading: var(--white);--fnSpecAttr: var(--gray500);--fnDeprecated: var(--yellowDark);--blink: var(--gray600);--codeBackground: var(--gray800);--codeBorder: var(--gray700);--inlineCodeBackground: var(--gray50);--inlineCodeBorder: var(--gray100);--codeScrollThumb: var(--gray500);--codeScrollBackground: var(--codeBorder);--bottomActionsBtnBorder: var(--white-opacity-10);--bottomActionsBtnSubheader: var(--main);--modalBackground: var(--gray800);--settingsInput: var(--white);--settingsInputBackground: var(--gray700);--settingsInputBorder: var(--gray700);--settingsSectionBorder: var(--gray700);--quickSwitchInput: var(--gray200);--quickSwitchContour: var(--gray500);--success: var(--green-lightened-10);--sidebarButton: var(--gray50)}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}@font-face{font-family:remixicon;src:url(./remixicon-NKANDIL5.woff2) format("woff2");font-display:swap}[class^=ri-],[class*=" ri-"],.remix-icon{font-family:remixicon;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:root{--icon-arrow-up-s: "\ea78";--icon-arrow-down-s: "\ea4e";--icon-arrow-right-s: "\ea6e";--icon-add: "\ea13";--icon-subtract: "\f1af";--icon-error-warning: "\eca1";--icon-information: "\ee59";--icon-alert: "\ea21";--icon-double-quotes-l: "\ec51";--icon-link-m: "\eeaf";--icon-close-line: "\eb99";--icon-code-s-slash-line: "\ebad";--icon-menu-line: "\ef3e";--icon-search-2-line: "\f0cd";--icon-settings-3-line: "\f0e6";--icon-printer-line: "\f029"}.ri-lg{font-size:1.3333em;line-height:.75em;vertical-align:-.0667em}.ri-settings-3-line:before{content:var(--icon-settings-3-line)}.ri-add-line:before{content:var(--icon-add)}.ri-subtract-line:before{content:var(--icon-subtract)}.ri-arrow-up-s-line:before{content:var(--icon-arrow-up-s)}.ri-arrow-down-s-line:before{content:var(--icon-arrow-down-s)}.ri-arrow-right-s-line:before{content:var(--icon-arrow-right-s)}.ri-search-2-line:before{content:var(--icon-search-2-line)}.ri-menu-line:before{content:var(--icon-menu-line)}.ri-close-line:before{content:var(--icon-close-line)}.ri-link-m:before{content:var(--icon-link-m)}.ri-code-s-slash-line:before{content:var(--icon-code-s-slash-line)}.ri-error-warning-line:before{content:var(--icon-error-warning)}.ri-information-line:before{content:var(--icon-information)}.ri-alert-line:before{content:var(--icon-alert)}.ri-double-quotes-l:before{content:var(--icon-double-quotes-l)}.ri-printer-line:before{content:var(--icon-printer-line)}html,body{box-sizing:border-box;height:100%;width:100%}body{background-color:var(--background);color:var(--textBody);font-size:16px;font-family:var(--sansFontFamily);line-height:1.6875em}*,*:before,*:after{box-sizing:inherit}.main{display:flex;justify-content:flex-end;height:100%}.sidebar{display:flex;flex-direction:column;width:300px;height:100%;position:fixed;top:0;left:0;z-index:4}.sidebar-button{position:fixed;z-index:99;left:0;top:7px;transition:color .3s ease-in-out,transform .15s ease-out .1s,opacity .15s ease-out .1s;will-change:transform;transform:translate(250px)}.content{width:calc(100% - 300px);left:300px;height:100%;position:absolute;z-index:3}body.sidebar-opening .sidebar-button{transition:transform .3s ease-in-out}body.sidebar-opening .sidebar{left:0;transition:left .3s ease-in-out}body.sidebar-opening .content{width:calc(100% - 300px);left:300px;transition:all .3s ease-in-out}body.sidebar-closing .sidebar-button{transform:translate(0)}body.sidebar-closing .sidebar{left:-300px;transition:left .3s ease-in-out}body.sidebar-closing .content{width:100%;left:0;transition:all .3s ease-in-out}body.sidebar-closed .sidebar-button{transition:transform .3s ease-in-out;transform:translateY(-8px)}body.sidebar-closed .sidebar{left:-300px}body.sidebar-closed .content{width:100%;left:0}body.search-focused .sidebar-button{transform:translate(250px) scaleY(0);transition:all .15s ease-out;opacity:0}body.search-focused .sidebar-search .search-close-button{transform:scaleY(1);transition:transform .15s ease-out .15s}.content-inner{max-width:949px;margin:0 auto;padding:3px 60px}.content-inner:focus{outline:none}.content-outer{min-height:100%}@media screen and (max-width: 768px){.content,body.sidebar-opening .content{left:0;width:100%}.content-inner{padding:27px 20px 27px 40px;max-width:100%;overflow-x:auto}}.sidebar{font-family:var(--sansFontFamily);font-size:16px;line-height:18px;background-color:var(--gray800);color:var(--gray50);overflow:hidden;scrollbar-color:var(--coldGray) var(--gray800)}.sidebar .gradient{background:linear-gradient(var(--gray800),var(--gray800-opacity-0));height:20px;margin-top:-20px;pointer-events:none;position:relative;top:20px;z-index:100}.sidebar ul{list-style:none}.sidebar ul li{margin:0;padding:0 10px}.sidebar a{color:var(--gray50);text-decoration:none;transition:color .3s ease-in-out}.sidebar a:hover{color:var(--white)}.sidebar .sidebar-header{margin:12px;border-radius:4px;background-color:var(--gray700);width:276px}.sidebar .sidebar-projectDetails{display:inline-block;text-align:left;vertical-align:top;margin:6px 0 0 10px}.sidebar .sidebar-projectImage{display:inline-block;max-width:48px;max-height:48px;margin:0 0 0 10px;vertical-align:bottom}.sidebar .sidebar-projectName{font-weight:700;font-size:20px;line-height:30px;color:var(--gray50);margin:0;padding:0;max-width:230px;word-wrap:break-word}.sidebar .sidebar-projectVersion{display:block;position:relative;margin:0;padding:0;font-weight:300;font-size:16px;line-height:20px;color:var(--gray300)}.sidebar .sidebar-projectVersionsDropdown{cursor:pointer;position:relative;margin:0;padding:0 0 0 12px;border:none;-webkit-appearance:none;appearance:none;background-color:transparent;color:var(--white);z-index:2}.sidebar .sidebar-projectVersionsDropdown option{color:initial}.sidebar .sidebar-projectVersionsDropdown:focus{outline:none}.sidebar .sidebar-projectVersion form:after{position:absolute;left:0;top:2px;content:"\25bc";z-index:1;font-size:8px;color:var(--white)}.sidebar .sidebar-projectVersionsDropdown::-ms-expand{display:none}.sidebar .sidebar-listNav{padding:12px 0 0;margin:0}.sidebar .sidebar-listNav :is(li,li a){text-transform:uppercase;font-weight:300;font-size:14px;color:var(--gray300)}.sidebar .sidebar-listNav li{display:inline-block;border-bottom:3px solid transparent;line-height:27px}.sidebar .sidebar-listNav li:is(:hover,.selected){border-color:var(--main)}.sidebar .sidebar-listNav li:is(:hover,.selected) a{color:var(--gray50)}.sidebar .sidebar-search{margin-top:12px}.sidebar .sidebar-search.selected .search-button,.sidebar .sidebar-search .search-button:hover{color:var(--main);opacity:1}.sidebar .sidebar-search .search-label{position:relative;width:100%}.sidebar .sidebar-search .search-button{font-size:14px;color:var(--gray50);background-color:transparent;border:none;cursor:pointer;left:22px;margin:0;opacity:.5;padding:3px 1px 3px 0;position:absolute;top:18px;z-index:2}.sidebar .sidebar-search .search-close-button{font-size:16px;color:var(--gray50);background-color:transparent;border:none;cursor:pointer;right:18px;margin:0;opacity:.5;padding:5px 1px 5px 0;position:absolute;transform:scaleY(0);top:17px;transition:.15s transform ease-out;z-index:2}.sidebar .sidebar-search .search-close-button:hover{opacity:.7}.sidebar .sidebar-search .search-close-button:is(:focus,:hover){outline:none}.sidebar .sidebar-search .search-input{background-color:var(--gray700);border:none;border-radius:4px;color:var(--gray50);margin-left:12px;padding:8px 6px 8px 38px;width:276px}.sidebar .sidebar-search .search-input:is(:focus,:hover){outline:none}.sidebar .sidebar-search .ri-search-2-line{font-weight:700}.sidebar #full-list{margin:0;padding:20px 0;overflow-y:auto;position:relative;-webkit-overflow-scrolling:touch;flex:1 1 .01%}.sidebar #full-list li{padding:0;margin-right:30px;line-height:27px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar #full-list li.docs{margin-right:0}.sidebar #full-list li.open>ul{display:block;margin-left:10px}.sidebar #full-list li a span.icon-expand:after{font-family:remixicon;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.sidebar #full-list li a.expand>span.icon-expand:after{content:var(--icon-arrow-down-s);margin-right:10px;font-size:1.2em;position:absolute;right:0}.sidebar #full-list li.open>a.expand>span.icon-expand:after{content:var(--icon-arrow-up-s)}.sidebar #full-list li.docs>a>span.icon-expand:after{margin-right:12px;content:var(--icon-add);font-size:1em;position:absolute;right:0}.sidebar #full-list li.docs.open>a>span.icon-expand:after{content:var(--icon-subtract)}.sidebar #full-list li.nesting-context{font-weight:700;font-size:.9em;line-height:1.8em;color:var(--gray300);padding-left:15px}.sidebar #full-list li.group{text-transform:uppercase;font-weight:700;font-size:.8em;margin:2em 0 0;line-height:1.8em;color:var(--gray300);padding-left:15px}.sidebar #full-list li a{padding:3px 0 3px 15px;color:var(--gray200)}.sidebar #full-list li a.expand,.sidebar #full-list li .sections-list>li{text-overflow:ellipsis}.sidebar #full-list li .current-section>a{color:var(--main)}.sidebar #full-list>li.current-page>a{color:var(--main);border-left:3px solid var(--main);padding-left:12px}.sidebar #full-list>li.current-page>a:after,.sidebar #full-list>li.current-page{color:var(--main)}.sidebar #full-list>li:last-child{margin-bottom:30px}.sidebar #full-list>li.group:first-child{margin-top:0}.sidebar #full-list ul{display:none;margin:9px 0 9px 15px;padding:0}.sidebar #full-list ul li{font-weight:300;line-height:20px;padding:2px 8px;margin-right:0;color:var(--gray50)}.sidebar #full-list ul li.current-hash{color:var(--main)}.sidebar #full-list ul li.current-hash:before{content:"\2022";color:var(--main)}.sidebar #full-list ul li.current-hash>a{color:var(--main);margin-left:-12px}.sidebar #full-list ul li a{padding-left:15px}.sidebar #full-list ul li ul{display:none;margin:9px 0 9px 20px}.sidebar #full-list ul li ul li{border-left:1px solid var(--gray600);padding:0 10px;margin-left:8px;margin-right:0;color:var(--gray50)}.sidebar #full-list ul li ul li a{padding-left:0}.sidebar #full-list ul li ul li.current-hash:before{content:none}.sidebar #full-list ul li ul li.current-hash{color:var(--main);border-color:var(--main)}.sidebar #full-list ul li ul li.current-hash>a{color:var(--main);margin-left:0}.sidebar ::-webkit-scrollbar{width:14px}::-webkit-scrollbar-track{background-color:var(--gray800)}.sidebar ::-webkit-scrollbar-thumb{background-color:var(--coldGray);border-radius:10px;border:3px solid var(--gray800)}.sidebar-button{cursor:pointer;background-color:transparent;border:none;padding:15px 11px;font-size:16px}.sidebar-button:hover{color:var(--white)}.sidebar-button:is(:active,:hover,:focus){outline:none}.sidebar-button{color:var(--sidebarButton)}.sidebar-closed .sidebar-button{color:var(--contrast)}@media screen and (max-height: 500px){.sidebar{overflow-y:auto}.sidebar #full-list{overflow:visible}}.content-inner{font-family:var(--serifFontFamily);font-size:1em;line-height:1.6875em;position:relative;background-color:var(--background);color:var(--textBody)}.content-inner :is(h1,h2,h3,h4,h5,h6){font-family:var(--sansFontFamily);font-weight:700;line-height:1.5em;word-wrap:break-word;color:var(--textHeaders)}.content-inner h1{font-size:2em;margin:1em 0 .5em}.content-inner h1.signature{margin:0}.content-inner h1.section-heading{margin:1.5em 0 .5em}.content-inner h1 small{font-weight:300}.content-inner h1 .icon-action{font-size:1.2rem;font-weight:400}.content-inner h2{font-size:1.6em;margin:1em 0 .5em;font-weight:700}.content-inner h3{font-size:1.375em;margin:1em 0 .5em;font-weight:700}.content-inner :is(a,.a-main){color:var(--links);text-decoration:underline;text-decoration-skip-ink:auto}.content-inner :is(a:visited,.a-main:visited){color:var(--linksVisited)}.content-inner .icon-action{float:right;color:var(--iconAction);text-decoration:none;border:none;transition:color .3s ease-in-out;background-color:transparent;cursor:pointer;padding:0 0 0 6px}.content-inner button.icon-action{margin-top:12px}.content-inner .icon-action:hover{color:var(--iconActionHover)}.content-inner .icon-action:visited{color:var(--iconAction)}.content-inner .livebook-badge-container{display:flex}.content-inner a.livebook-badge{display:inline-flex}.content-inner .note{color:var(--iconAction);margin-right:5px;font-size:14px;font-weight:400}.content-inner h1 .note{float:right}.content-inner blockquote{border-left:3px solid var(--blockquoteBorder);position:relative;margin:1.5625em 0;padding:0 1.2rem;overflow:auto;background-color:var(--blockquoteBackground);border-radius:3px}.content-inner blockquote p:last-child{padding-bottom:1em;margin-bottom:0}.content-inner table{margin:2em 0}.content-inner th{text-align:left;font-family:var(--sansFontFamily);text-transform:uppercase;font-weight:700;padding-bottom:.5em}.content-inner tr{border-bottom:1px solid var(--gray50);vertical-align:bottom;height:2.5em}.content-inner :is(td,th){padding-left:1em;line-height:2em;vertical-align:top}.content-inner .section-heading:hover a.hover-link{opacity:1;text-decoration:none}.content-inner .section-heading a.hover-link{transition:opacity .3s ease-in-out;display:inline-block;opacity:0;padding:.3em .6em .6em;line-height:1em;margin-left:-2.7em;text-decoration:none;border:none;font-size:16px;vertical-align:middle}.content-inner .detail :is(h1,h2,h3,h4,h5,h6).section-heading{margin-left:.3em}.content-inner .app-vsn{display:none!important;font-size:.6em;line-height:1.5em}@media screen and (max-width: 768px){.content-inner .app-vsn{display:block!important}}.content-inner img{max-width:100%}.content-inner code{font-family:var(--monoFontFamily);font-style:normal;line-height:24px;font-weight:400}.content-inner blockquote:is(.warning,.error,.info,.neutral,.tip){color:var(--black);border-radius:10px;border-left:0}.content-inner blockquote:is(.warning,.error,.info,.neutral,.tip) a{color:var(--black)}.content-inner blockquote.warning{background-color:var(--warningBackground)}.content-inner blockquote.error{background-color:var(--errorBackground)}.content-inner blockquote.info{background-color:var(--infoBackground)}.content-inner blockquote.neutral{background-color:var(--neutralBackground)}.content-inner blockquote.tip{background-color:var(--tipBackground)}.content-inner blockquote :is(h3,h4):is(.warning,.error,.info,.neutral,.tip){color:var(--contrast);margin:0 -1.2rem;padding:.7rem 1.2rem .7rem 3.3rem;font-weight:700;font-style:normal}.content-inner blockquote :is(h3,h4):is(.warning,.error,.info,.neutral,.tip):before{color:var(--contrast);position:absolute;left:1rem;font-size:1.8rem;font-family:remixicon;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.content-inner blockquote :is(h3,h4).warning{background-color:var(--warningHeadingBackground);color:var(--warningHeading)}.content-inner blockquote :is(h3,h4).warning:before{content:var(--icon-error-warning);color:var(--warningHeading)}.content-inner blockquote :is(h3,h4).error{background-color:var(--errorHeadingBackground);color:var(--errorHeading)}.content-inner blockquote :is(h3,h4).error:before{content:var(--icon-error-warning);color:var(--errorHeading)}.content-inner blockquote :is(h3,h4).info{background-color:var(--infoHeadingBackground);color:var(--infoHeading)}.content-inner blockquote :is(h3,h4).info:before{content:var(--icon-information);color:var(--infoHeading)}.content-inner blockquote :is(h3,h4).neutral{background-color:var(--neutralHeadingBackground);color:var(--neutralHeading)}.content-inner blockquote :is(h3,h4).neutral:before{content:var(--icon-double-quotes-l);color:var(--neutralHeading)}.content-inner blockquote :is(h3,h4).tip{background-color:var(--tipHeadingBackground);color:var(--tipHeading)}.content-inner blockquote :is(h3,h4).tip:before{content:var(--icon-information);color:var(--tipHeading)}.content-inner blockquote:is(.warning,.error,.info,.neutral,.tip) code{background-color:var(--inlineCodeBackground);border:1px solid var(--inlineCodeBorder);color:var(--black)}.content-inner .summary h2{font-weight:700}.content-inner .summary h2 a{text-decoration:none;border:none}.content-inner .summary span.deprecated{color:var(--darkDeprecated);font-weight:400;font-style:italic}.content-inner .summary .summary-row .summary-signature{font-family:var(--monoFontFamily);font-weight:700}.content-inner .summary .summary-row .summary-signature a{text-decoration:none;border:none}.content-inner .summary .summary-row .summary-synopsis{font-family:var(--serifFontFamily);font-style:italic;padding:0 1.2em;margin:0 0 .5em}.content-inner .summary .summary-row .summary-synopsis p{margin:0;padding:0}.content-inner :is(a.no-underline,pre a){color:var(--linksNoUnderline);text-shadow:none;text-decoration:none;background-image:none}.content-inner :is(a.no-underline,pre a):is(:visited,:active,:focus,:hover){color:var(--linksNoUnderlineVisited)}.content-inner code{background-color:var(--codeBackground);vertical-align:baseline;border-radius:2px;padding:.1em .2em;border:1px solid var(--codeBorder);text-transform:none}.content-inner pre{margin:var(--baseLineHeight) 0}.content-inner pre code{display:block;overflow-x:auto;white-space:inherit;padding:.5em 1em;background-color:var(--codeBackground)}.content-inner pre code::-webkit-scrollbar{width:.4rem;height:.4rem}.content-inner pre code::-webkit-scrollbar-thumb{border-radius:.25rem;background-color:var(--codeScrollThumb)}.content-inner pre code::-webkit-scrollbar-track{background-color:var(--codeScrollBackground)}.content-inner pre code::-webkit-scrollbar-corner{background-color:var(--codeScrollBackground)}.content-inner pre code.output{margin:0 12px;max-height:400px;overflow:auto}.content-inner pre code.output+.copy-button{margin-right:12px}.content-inner pre code.output:before{content:"Output";display:block;position:absolute;top:-16px;left:12px;padding:2px 4px;font-size:12px;font-family:var(--monoFontFamily);line-height:1;color:var(--textHeaders);background-color:var(--codeBackground);border:1px solid var(--codeBorder);border-bottom:0;border-radius:2px}@keyframes blink-background{0%{background-color:var(--textDetailBackground)}to{background-color:var(--blink)}}.content-inner .detail:target .detail-header{animation-duration:.55s;animation-name:blink-background;animation-iteration-count:1;animation-timing-function:ease-in-out}.content-inner .detail-header{margin:2em 0 1em;padding:.5em 1em;background-color:var(--textDetailBackground);border-left:3px solid var(--main);font-size:1em;font-family:var(--monoFontFamily);position:relative}.content-inner .detail-header .note{float:right}.content-inner .detail-header .signature{display:inline-block;font-family:var(--monoFontFamily);font-size:1rem;font-weight:700}.content-inner .detail-header:hover a.detail-link{opacity:1;text-decoration:none}.content-inner .detail-header a.detail-link{transition:opacity .3s ease-in-out;position:absolute;top:0;left:0;display:block;opacity:0;padding:.6em;line-height:1.5em;margin-left:-2.5em;text-decoration:none;border:none}.content-inner .specs pre{font-family:var(--monoFontFamily);font-size:.9em;font-style:normal;line-height:24px;white-space:pre-wrap;margin:0;padding:0}.content-inner .specs .attribute{color:var(--fnSpecAttr)}.content-inner .docstring{margin:1.2em 0 3em 1.2em}.content-inner .docstring:is(h2,h3,h4,h5){font-weight:700}.content-inner .docstring h2{font-size:1.1em}.content-inner .docstring h3{font-size:1em}.content-inner .docstring h4{font-size:.95em}.content-inner .docstring h5{font-size:.9em}.content-inner div.deprecated{display:block;padding:9px 15px;background-color:var(--fnDeprecated)}.content-inner .footer{margin:4em auto 1em;text-align:center;font-style:italic;font-size:14px}.content-inner .footer,.content-inner .footer :is(a,.footer-button){color:var(--textFooter)}.content-inner .footer .line{display:inline-block}.content-inner .footer .footer-button{background-color:transparent;border:0;cursor:pointer;font-style:italic;outline:none;padding:0 4px}.content-inner .footer .footer-hex-package{margin-right:4px}.content-inner .bottom-actions{display:flex;justify-content:space-between;margin-top:4em}.content-inner .bottom-actions .bottom-actions-button{display:flex;text-decoration:none;flex-direction:column;border-radius:4px;border:1px solid var(--bottomActionsBtnBorder);padding:8px 16px;min-width:150px}.content-inner .bottom-actions .bottom-actions-button .subheader{font-size:.8em;color:var(--bottomActionsBtnSubheader);white-space:nowrap}.content-inner .bottom-actions .bottom-actions-button[rel=prev] .subheader{text-align:right}@media screen and (max-width: 768px){.content-inner .bottom-actions{flex-direction:column-reverse}.content-inner .bottom-actions .bottom-actions-item:not(:first-child){margin-bottom:16px}}@media screen{.page-cheatmd .content-inner{max-width:1200px}.page-cheatmd h1{margin-bottom:1em}.page-cheatmd h2{margin:1em 0;column-span:all;padding-left:3px;color:var(--gray700);font-weight:500}.page-cheatmd.dark h2{color:var(--gray200)}.page-cheatmd h3{white-space:nowrap;overflow:hidden;margin:0 0 1em;padding-left:5px;color:var(--main);font-weight:400}.page-cheatmd section.h3{min-width:300px;margin:0 0 2em;break-inside:avoid;-webkit-column-break-inside:avoid}.page-cheatmd h3:after{margin-left:24px;content:"";vertical-align:middle;display:inline-block;width:100%;height:1px;background:linear-gradient(to right,rgba(116,95,181,.2),transparent 80%)}.page-cheatmd h4{display:block;margin:0;padding:.25em 1.5em;font-weight:400;background:var(--gray100);color:#567;border:solid 1px 1px 0 1px var(--gray100)}.page-cheatmd.dark h4{background:#192f50;color:var(--textBody);border:1px solid #192f50;border-bottom:0}.page-cheatmd .h2 p{margin:0;display:block;background:var(--gray50);padding:1.5em}.page-cheatmd.dark .h2 p{background:var(--gray700)}.page-cheatmd .h2 p>code{color:#eb5757;border-radius:3px;padding:.2em .4em}.page-cheatmd pre code{padding:1em 1.5em}.page-cheatmd pre code::-webkit-scrollbar{width:.4rem;height:.6rem}.page-cheatmd .h2 pre{margin:0}.page-cheatmd pre.wrap{white-space:break-spaces}.page-cheatmd .h2 table{display:table;box-sizing:border-box;width:100%;border-collapse:collapse;margin:0}.page-cheatmd .h2 table th{padding:.75em 1.5em;line-height:2em;margin-bottom:-1px;vertical-align:middle;border-bottom:1px solid var(--codeBorder)}.page-cheatmd .h2 table td{padding:.75em 1.5em;border:0;border-bottom:1px solid var(--codeBorder)}.page-cheatmd .h2 table tr:first-child{border-top:1px solid var(--codeBorder)}.page-cheatmd .h2 table td code{color:#eb5757;border-radius:3px;padding:.2em .4em}.page-cheatmd .h2 thead{background-color:var(--gray50)}.page-cheatmd.dark .h2 thead{background-color:var(--gray700)}.page-cheatmd .h2 tbody{background-color:var(--codeBackground)}.page-cheatmd .h2 ul,.page-cheatmd .h2 ol{margin:0;padding:0}.page-cheatmd .h2 li{list-style-position:inside;padding:.5em 1.5em;line-height:2em;vertical-align:middle;background-color:var(--codeBackground);border-bottom:1px solid var(--codeBorder)}.page-cheatmd .h2 ul+pre code,.page-cheatmd .h2 ol+pre code{border-top:0}.page-cheatmd .h2 li>code{color:#eb5757;border-radius:3px;padding:.2em .4em}.page-cheatmd section.width-50{display:block;width:50%;margin:0}.page-cheatmd section.width-50>section>table{width:100%}.page-cheatmd section.col-2{column-count:2;column-gap:40px;height:auto}.page-cheatmd section.col-2-left{display:grid;grid-template-columns:33% 63.2%;column-gap:40px}.page-cheatmd section.col-2-left>h2{display:block;grid-column-end:span 2}.page-cheatmd section.col-3{column-count:3;column-gap:40px;height:auto}.page-cheatmd section.list-4>ul{display:flex;flex-wrap:wrap}.page-cheatmd section.list-4>ul>li{flex:0 0 25%}.page-cheatmd section.list-6>ul{display:flex;flex-wrap:wrap}.page-cheatmd section.list-6>ul>li{flex:0 0 16.6667%}@media (max-width: 1400px){.page-cheatmd section.col-3{column-count:2;column-gap:40px}.page-cheatmd section.col-2-left{display:block;column-count:1}}@media (max-width: 1200px){.page-cheatmd section.col-3,.page-cheatmd section.col-2{column-count:1}.page-cheatmd section.list-6>ul>li{flex:0 0 25%}}@media (max-width: 1000px){.page-cheatmd section.list-4>ul>li{flex:0 0 33%}.page-cheatmd section.list-6>ul>li{flex:0 0 33%}}@media (max-width: 600px){.page-cheatmd section.list-4>ul>li{flex:0 0 50%}.page-cheatmd section.list-6>ul>li{flex:0 0 50%}}}#search{min-height:200px;position:relative}#search .loading{height:64px;width:64px;vertical-align:middle;position:absolute;top:50%;left:18%}#search .loading div{box-sizing:border-box;display:block;position:absolute;width:51px;height:51px;margin:6px;border:6px solid var(--coldGray);border-radius:50%;animation:loading 1.2s cubic-bezier(.5,0,.5,1) infinite;border-color:var(--coldGray) transparent transparent transparent}#search .loading div:nth-child(1){animation-delay:-.45s}#search .loading div:nth-child(2){animation-delay:-.3s}#search .loading div:nth-child(3){animation-delay:-.15s}@keyframes loading{0%{transform:rotate(0)}to{transform:rotate(360deg)}}#search .result{margin:2em 0 2.5em}#search .result p{margin:0}#search .result-id{font-size:1.4em;margin:0}#search .result-id a{text-decoration:none;transition:color .1s ease-in-out;color:var(--links)}#search .result-id a:is(:visited,:active,:focus){color:var(--linksVisited)}#search .result-id a:hover{color:var(--main)}#search :is(.result-id,.result-elem) em{font-style:normal;color:var(--main)}#search .result-id small{font-weight:400}@keyframes keyboard-shortcuts-show{0%{opacity:0}to{opacity:1}}.modal{animation-duration:.15s;animation-name:keyboard-shortcuts-show;animation-iteration-count:1;animation-timing-function:ease-in-out;display:none;background-color:#000000bf;position:fixed;inset:0;z-index:300}.modal.shown{display:block}.modal .modal-contents{margin:75px auto 0;max-width:500px;background-color:var(--modalBackground);border-radius:3px;box-shadow:2px 2px 8px #0003;padding:25px 35px 35px}@media screen and (max-width: 768px){.modal .modal-contents{padding:20px}}.modal .modal-header{display:flex;align-items:start}.modal .modal-title{display:inline-block;flex-grow:1;font-size:1.2rem;font-weight:700;margin-bottom:20px}.modal .modal-title button{border:none;background-color:transparent;color:var(--textHeaders);font-weight:700;margin-right:30px;padding-left:0;text-align:left;transition:color .15s}.modal .modal-title button:hover{color:var(--main);cursor:pointer}.modal .modal-title button.active{color:var(--main)}.modal .modal-close{cursor:pointer;display:block;font-size:1.5rem;margin:-8px -8px 0 0;padding:8px;opacity:.7;background-color:transparent;color:var(--textHeaders);border:none;transition:opacity .15s}.modal .modal-close:hover{opacity:1}#keyboard-shortcuts-content dl.shortcut-row{display:flex;align-items:center;justify-content:space-between;margin:0;padding:6px 0 8px;border-bottom:1px solid var(--settingsSectionBorder)}#keyboard-shortcuts-content dl.shortcut-row:last-of-type{border-bottom-style:none}#keyboard-shortcuts-content dl.shortcut-row:first-child{padding-top:0}#keyboard-shortcuts-content :is(.shortcut-keys,.shortcut-description){display:inline-block}#keyboard-shortcuts-content kbd>kbd{background-color:var(--settingsInputBorder);color:var(--contrast);border-radius:3px;font-family:inherit;font-weight:700;display:inline-block;line-height:1;padding:4px 7px 6px;min-width:26px;text-align:center}#keyboard-shortcuts-content :is(.shortcut-keys,.shortcut-description){margin:0}#quick-switch-modal-body{width:100%;position:relative}#quick-switch-modal-body .ri-search-2-line{position:absolute;left:0;top:0;padding:4px 10px;color:var(--quickSwitchContour);font-weight:700}#quick-switch-modal-body #quick-switch-input{width:100%;padding:8px 6px 8px 38px;border:none;color:var(--quickSwitchInput);background-color:transparent;border-bottom:1px solid var(--quickSwitchContour);box-sizing:border-box;transition:all .12s ease-out}#quick-switch-modal-body #quick-switch-input:focus{outline:none}#quick-switch-modal-body #quick-switch-results{margin:0}#quick-switch-modal-body .quick-switch-result{padding:2px 5px;border-bottom:1px dotted var(--quickSwitchContour);transition:all .12s ease-out}#quick-switch-modal-body .quick-switch-result:last-child{border-bottom:none}#quick-switch-modal-body .quick-switch-result:hover{cursor:pointer}#quick-switch-modal-body .quick-switch-result:is(:hover,.selected){border-left:4px solid var(--main);background-color:var(--codeBackground)}.autocomplete{display:none;height:0;margin:0 5px 0 12px;overflow:visible;position:relative;width:100%}.autocomplete.shown{display:block}.autocomplete-suggestions{box-shadow:2px 2px 10px #00000040;background-color:var(--gray700);border-top:1px solid var(--gray800);left:0;position:absolute;top:-2px;width:276px;z-index:200}.autocomplete-suggestion{color:inherit;display:block;padding:10px;text-decoration:none}.autocomplete-suggestion:hover,.autocomplete-suggestion.selected{background-color:var(--gray600);border-left:3px solid var(--main)}.autocomplete-suggestion em{font-style:normal;font-weight:700}.autocomplete-suggestion .description{opacity:.6;padding-top:3px}.autocomplete-suggestion .label{padding-left:2px;opacity:.75}.autocomplete-suggestion .title,.autocomplete-suggestion .description{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}#tooltip{box-shadow:0 0 10px var(--black-opacity-10);max-height:300px;max-width:500px;padding:0;position:absolute;pointer-events:none;margin:0;z-index:99;top:0;left:0;visibility:hidden;transform:translateY(20px);opacity:0;transition:.2s visibility ease-out,.2s transform ease-out,.2s opacity ease-out}#tooltip.tooltip-shown{visibility:visible;transform:translateY(0);opacity:1}#tooltip .tooltip-body{border:1px solid var(--codeBorder)}#tooltip .tooltip-body .signature{min-width:320px;width:100%}#tooltip .tooltip-body .detail-header{border-left:0;margin-bottom:0;margin-top:0}#tooltip .tooltip-body .docstring{background-color:var(--background);padding:1.2em;margin:0;width:498px}#tooltip .tooltip-body .docstring-plain{max-width:498px;width:auto}#tooltip .tooltip-body .version-info{float:right;line-height:1.6rem;font-family:var(--monoFontFamily);font-size:.9rem;font-weight:400;margin-bottom:-6px;opacity:.3;padding-left:.3em}pre{position:relative}pre:hover .copy-button{display:block}.copy-button{display:none;position:absolute;top:9px;right:9px;background-color:transparent;border:none;cursor:pointer;padding:0;opacity:.5;transition:all .15s;font-family:var(--serifFontFamily);font-size:14px;line-height:24px;color:currentColor}.copy-button:hover{opacity:1}.copy-button svg{width:20px}.copy-button.clicked{display:block;opacity:1;color:var(--success)}.copy-button.clicked:after{content:"Copied! \2713"}.copy-button.clicked svg{display:none;color:currentColor}#settings-modal-content{margin-top:10px}#settings-modal-content .hidden{display:none}#settings-modal-content .input{box-sizing:border-box;width:80%;padding:8px;font-size:14px;background-color:var(--settingsInputBackground);color:var(--settingsInput);border:1px solid var(--settingsInputBorder);border-radius:8px;transition:border-color .15s}#settings-modal-content .input:focus{outline:none;border-color:var(--main)}#settings-modal-content .input::placeholder{color:var(--gray300)}#settings-modal-content .switch-button-container{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--settingsSectionBorder);padding:10px 0}#settings-modal-content .switch-button-container:first-of-type{border-top-style:none;padding-top:0}#settings-modal-content .switch-button-container>div>span{font-size:18px}#settings-modal-content .switch-button-container>div>p{font-size:14px;line-height:1.4;margin:0;padding-bottom:6px;padding-right:10px}#settings-modal-content .switch-button{position:relative;display:inline-block;flex-shrink:0;width:40px;height:20px;user-select:none;transition:all .15s}#settings-modal-content .switch-button__checkbox{appearance:none;position:absolute;display:block;width:20px;height:20px;border-radius:1000px;background-color:#91a4b7;border:3px solid #e5edf5;cursor:pointer;transition:all .3s}#settings-modal-content .switch-button__bg{display:block;width:100%;height:100%;border-radius:1000px;background-color:#e5edf5;cursor:pointer;transition:all .3s}#settings-modal-content .switch-button__checkbox:checked{background-color:#fff;border-color:var(--main);transform:translate(100%)}#settings-modal-content .switch-button__checkbox:checked+.switch-button__bg{background-color:var(--main)}#settings-modal-content .settings-select{cursor:pointer;position:relative;border:none;background-color:transparent;color:var(--textBody)}#settings-modal-content .settings-select option{color:initial}#settings-modal-content .settings-select:focus{outline:none}#toast{opacity:0;position:fixed;z-index:1;left:50%;bottom:1rem;min-width:3rem;margin:0 -1.2rem;padding:.7rem 1.2rem;text-align:center;font-weight:700;border-radius:10px;border:1px solid var(--codeBorder);background-color:var(--codeBackground);color:var(--textBody);transition:opacity .4s ease-in-out,transform .3s ease-out}#toast.show{opacity:1;transform:translateY(-.75rem)}@media (prefers-reduced-motion: reduce){#toast{transition:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0;user-select:none}@media print{.main{display:block}.sidebar,.sidebar-button{display:none}.content{padding-left:0;overflow:visible;left:0;width:100%}.summary-row{page-break-inside:avoid}#toast,.content-inner .section-heading a.hover-link,.content-inner button.icon-action,.content-inner a.icon-action,.content-inner .bottom-actions{display:none}.footer p:first-of-type{display:none}.content-inner blockquote:is(.warning,.error,.info,.neutral,.tip){border:2px solid var(--gray300)}.content-inner blockquote :is(h3,h4):is(.warning,.error,.info,.neutral,.tip){color:var(--textHeaders);border-bottom:2px solid var(--gray300)}.content-inner pre code.makeup{border-color:var(--gray300);white-space:break-spaces;page-break-inside:avoid;break-inside:avoid}.content-inner blockquote code.inline,.content-inner code.inline{border-color:var(--gray300)}}@media print{.page-cheatmd .content-inner{max-width:100%;width:100%;padding:0;font-size:.85em}.page-cheatmd section.col-2{column-count:2;column-gap:20px;height:auto}.page-cheatmd section.col-2-left{display:grid;grid-template-columns:33% 63.2%;column-gap:20px}.page-cheatmd section.col-2-left>h2{display:block;grid-column-end:span 2}.page-cheatmd section.col-3{column-count:3;column-gap:10px;height:auto}.page-cheatmd section.list-4>ul>li{flex:0 0 25%}.page-cheatmd section.list-6>ul>li{flex:0 0 16.6667%}.page-cheatmd section.list-4>ul{display:flex;flex-wrap:wrap}.page-cheatmd section.list-6>ul{display:flex;flex-wrap:wrap}.page-cheatmd section.width-50{display:block;width:50%;margin:0}.page-cheatmd section.width-50>section>table{width:100%}.page-cheatmd h1{margin-top:0;margin-bottom:.5em}.page-cheatmd h2.section-heading{margin:1em 0 .25em;column-span:all}.page-cheatmd h2.section-heading:before{border-left:solid 6px var(--gray100);margin-right:8px;content:" "}.page-cheatmd section.h2{page-break-inside:avoid}.page-cheatmd h3{white-space:nowrap;overflow:hidden;margin:0 0 .25em;padding-left:5px}.page-cheatmd h3.section-heading{overflow:hidden}.page-cheatmd section.h3{min-width:300px;margin:0 0 .75em;break-inside:avoid;page-break-inside:avoid;-webkit-column-break-inside:avoid}.page-cheatmd h4{display:block;margin:-1px 0;padding:.5em;border:solid 1px var(--gray300)}.page-cheatmd .content-inner p{font-size:.95em;line-height:1.5em;padding:.5em}.page-cheatmd .content-inner section p{display:block;margin:-1px 0;font-size:.95em;line-height:1.5em;padding:.5em;border:solid;border-color:var(--gray300);border-width:1px 1px 0px 1px}.page-cheatmd .content-inner section p+p{border-width:0px 1px 0px 1px}.page-cheatmd .content-inner section p:last-of-type{border-width:0px 1px 1px 1px}.page-cheatmd .content-inner section p:only-of-type{border-width:1px}.page-cheatmd table{width:100%;border-collapse:collapse;margin:0;font-variant-numeric:tabular-nums;page-break-inside:avoid}.page-cheatmd th,.page-cheatmd td{text-align:left;vertical-align:top;padding:.5em;font-size:.95em}.page-cheatmd thead{border:1px solid var(--gray300)}.page-cheatmd .content-inner tbody tr{border-width:0px 1px 1px 1px;border-style:solid;border-color:var(--gray200)}.page-cheatmd .content-inner thead tr{border-bottom:none}.page-cheatmd th{font-weight:700}.page-cheatmd td{text-align:left}.page-cheatmd pre{margin:-1px 0}.page-cheatmd ul,.page-cheatmd ol{margin:0;padding:0;list-style-position:inside}.page-cheatmd .h2 li{padding:.5em .75em;vertical-align:middle;border-bottom:1px solid var(--gray200)}.page-cheatmd .h2 li:last-of-type{border-bottom:0}pre:hover button.copy-button,.page-cheatmd div#tooltip{display:none}.page-cheatmd footer p{display:none}.page-cheatmd footer p.built-using{display:block}}code.makeup .unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.makeup .hll{background-color:#ffc}.makeup .bp{color:#3465a4}.makeup .c,.makeup .c1,.makeup .ch,.makeup .cm,.makeup .cp,.makeup .cpf,.makeup .cs{color:#4d4d4d}.makeup .dl{color:#408200}.makeup .err{color:#a40000;border:#ef2929}.makeup .fm,.makeup .g{color:#4d4d4c}.makeup .gd{color:#a40000}.makeup .ge{color:#4d4d4c;font-style:italic}.makeup .gh{color:navy;font-weight:700}.makeup .gi{color:#00a000}.makeup .go{color:#4d4d4c;font-style:italic}.makeup .gp{color:#4d4d4d}.makeup .gr{color:#ef2929}.makeup .gs{color:#4d4d4c;font-weight:700}.makeup .gt{color:#a40000;font-weight:700}.makeup .gu{color:purple;font-weight:700}.makeup .il{color:#0000cf;font-weight:700}.makeup .k,.makeup .kc,.makeup .kd,.makeup .kn,.makeup .kp,.makeup .kr,.makeup .kt{color:#204a87}.makeup .l{color:#4d4d4c}.makeup .ld{color:#c00}.makeup .m,.makeup .mb,.makeup .mf,.makeup .mh,.makeup .mi,.makeup .mo{color:#2937ab}.makeup .n{color:#4d4d4c}.makeup .na{color:#8a7000}.makeup .nb{color:#204a87}.makeup .nc{color:#0000cf}.makeup .nd{color:#5c35cc;font-weight:700}.makeup .ne{color:#c00;font-weight:700}.makeup .nf{color:#b65800}.makeup .ni{color:#bc5400}.makeup .nl{color:#b65800}.makeup .nn{color:#4d4d4c}.makeup .no{color:#a06600}.makeup .nt{color:#204a87;font-weight:700}.makeup .nv,.makeup .nx{color:#4d4d4c}.makeup .o{color:#bc5400}.makeup .ow{color:#204a87}.makeup .p,.makeup .py{color:#4d4d4c}.makeup .s,.makeup .s1,.makeup .s2,.makeup .sa,.makeup .sb,.makeup .sc{color:#408200}.makeup .sd{color:#8f5902;font-style:italic}.makeup .se{color:#204a87}.makeup .sh{color:#408200}.makeup .si{color:#204a87}.makeup .sr{color:#c00}.makeup .ss{color:#a06600}.makeup .sx{color:#408200}.makeup .vc,.makeup .vg,.makeup .vi,.makeup .vm,.makeup .x{color:#4d4d4c}.dark .makeup{color:#dce1e6}.dark .makeup .hll{background-color:#49483e}.dark .makeup .bp{color:#dce1e6}.dark .makeup .c,.dark .makeup .c1,.dark .makeup .ch,.dark .makeup .cm,.dark .makeup .cp,.dark .makeup .cpf,.dark .makeup .cs{color:#969386}.dark .makeup .dl{color:#e6db74}.dark .makeup .err{color:#960050;background-color:#1e0010}.dark .makeup .fm{color:#a6e22e}.dark .makeup .gd{color:#ff5385}.dark .makeup .ge{font-style:italic}.dark .makeup .gi{color:#a6e22e}.dark .makeup .gp{color:#969386}.dark .makeup .gs{font-weight:700}.dark .makeup .gu{color:#969386}.dark .makeup .gt{color:#ff5385;font-weight:700}.dark .makeup .il{color:#ae81ff}.dark .makeup .k,.dark .makeup .kc,.dark .makeup .kd{color:#66d9ef}.dark .makeup .kn{color:#ff5385}.dark .makeup .kp,.dark .makeup .kr,.dark .makeup .kt{color:#66d9ef}.dark .makeup .l,.dark .makeup .ld,.dark .makeup .m,.dark .makeup .mb,.dark .makeup .mf,.dark .makeup .mh,.dark .makeup .mi,.dark .makeup .mo{color:#ae81ff}.dark .makeup .n{color:#dce1e6}.dark .makeup .na{color:#a6e22e}.dark .makeup .nb{color:#dce1e6}.dark .makeup .nc,.dark .makeup .nd,.dark .makeup .ne,.dark .makeup .nf{color:#a6e22e}.dark .makeup .ni,.dark .makeup .nl,.dark .makeup .nn{color:#dce1e6}.dark .makeup .no{color:#66d9ef}.dark .makeup .nt{color:#ff5385}.dark .makeup .nv{color:#dce1e6}.dark .makeup .nx{color:#a6e22e}.dark .makeup .o,.dark .makeup .ow{color:#ff5385}.dark .makeup .p,.dark .makeup .py{color:#dce1e6}.dark .makeup .s,.dark .makeup .s1,.dark .makeup .s2,.dark .makeup .sa,.dark .makeup .sb,.dark .makeup .sc,.dark .makeup .sd{color:#e6db74}.dark .makeup .se{color:#ae81ff}.dark .makeup .sh,.dark .makeup .si,.dark .makeup .sr,.dark .makeup .ss,.dark .makeup .sx{color:#e6db74}.dark .makeup .vc,.dark .makeup .vg,.dark .makeup .vi,.dark .makeup .vm{color:#dce1e6}body:not(.dark) .content-inner img[src*="#gh-dark-mode-only"],body.dark .content-inner img[src*="#gh-light-mode-only"]{display:none} -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ diff --git a/doc/dist/inconsolata-latin-400-normal-RGKDDNDD.woff2 b/doc/dist/inconsolata-latin-400-normal-RGKDDNDD.woff2 deleted file mode 100644 index 6119e59..0000000 Binary files a/doc/dist/inconsolata-latin-400-normal-RGKDDNDD.woff2 and /dev/null differ diff --git a/doc/dist/inconsolata-latin-700-normal-DTS2D7TO.woff2 b/doc/dist/inconsolata-latin-700-normal-DTS2D7TO.woff2 deleted file mode 100644 index 010da61..0000000 Binary files a/doc/dist/inconsolata-latin-700-normal-DTS2D7TO.woff2 and /dev/null differ diff --git a/doc/dist/inconsolata-latin-ext-400-normal-K7HVGTP7.woff2 b/doc/dist/inconsolata-latin-ext-400-normal-K7HVGTP7.woff2 deleted file mode 100644 index 50cb547..0000000 Binary files a/doc/dist/inconsolata-latin-ext-400-normal-K7HVGTP7.woff2 and /dev/null differ diff --git a/doc/dist/inconsolata-latin-ext-700-normal-4MPBLFZC.woff2 b/doc/dist/inconsolata-latin-ext-700-normal-4MPBLFZC.woff2 deleted file mode 100644 index 8a20d54..0000000 Binary files a/doc/dist/inconsolata-latin-ext-700-normal-4MPBLFZC.woff2 and /dev/null differ diff --git a/doc/dist/inconsolata-vietnamese-400-normal-IGQPHHJH.woff2 b/doc/dist/inconsolata-vietnamese-400-normal-IGQPHHJH.woff2 deleted file mode 100644 index ab70f18..0000000 Binary files a/doc/dist/inconsolata-vietnamese-400-normal-IGQPHHJH.woff2 and /dev/null differ diff --git a/doc/dist/inconsolata-vietnamese-700-normal-LHEGSN35.woff2 b/doc/dist/inconsolata-vietnamese-700-normal-LHEGSN35.woff2 deleted file mode 100644 index a814008..0000000 Binary files a/doc/dist/inconsolata-vietnamese-700-normal-LHEGSN35.woff2 and /dev/null differ diff --git a/doc/dist/lato-latin-300-normal-YUMVEFOL.woff2 b/doc/dist/lato-latin-300-normal-YUMVEFOL.woff2 deleted file mode 100644 index aad98a3..0000000 Binary files a/doc/dist/lato-latin-300-normal-YUMVEFOL.woff2 and /dev/null differ diff --git a/doc/dist/lato-latin-700-normal-2XVSBPG4.woff2 b/doc/dist/lato-latin-700-normal-2XVSBPG4.woff2 deleted file mode 100644 index 11de83f..0000000 Binary files a/doc/dist/lato-latin-700-normal-2XVSBPG4.woff2 and /dev/null differ diff --git a/doc/dist/lato-latin-ext-300-normal-VPGGJKJL.woff2 b/doc/dist/lato-latin-ext-300-normal-VPGGJKJL.woff2 deleted file mode 100644 index 486d3ec..0000000 Binary files a/doc/dist/lato-latin-ext-300-normal-VPGGJKJL.woff2 and /dev/null differ diff --git a/doc/dist/lato-latin-ext-700-normal-Q2L5DVMW.woff2 b/doc/dist/lato-latin-ext-700-normal-Q2L5DVMW.woff2 deleted file mode 100644 index 2c8aaa8..0000000 Binary files a/doc/dist/lato-latin-ext-700-normal-Q2L5DVMW.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-cyrillic-300-italic-M6KMXZSZ.woff2 b/doc/dist/merriweather-cyrillic-300-italic-M6KMXZSZ.woff2 deleted file mode 100644 index 71e8f7e..0000000 Binary files a/doc/dist/merriweather-cyrillic-300-italic-M6KMXZSZ.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-cyrillic-300-normal-7PAAHU3N.woff2 b/doc/dist/merriweather-cyrillic-300-normal-7PAAHU3N.woff2 deleted file mode 100644 index 11d8cdd..0000000 Binary files a/doc/dist/merriweather-cyrillic-300-normal-7PAAHU3N.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-cyrillic-ext-300-italic-JP3ZEV2P.woff2 b/doc/dist/merriweather-cyrillic-ext-300-italic-JP3ZEV2P.woff2 deleted file mode 100644 index 0bc4aac..0000000 Binary files a/doc/dist/merriweather-cyrillic-ext-300-italic-JP3ZEV2P.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-cyrillic-ext-300-normal-5LF5LCEK.woff2 b/doc/dist/merriweather-cyrillic-ext-300-normal-5LF5LCEK.woff2 deleted file mode 100644 index 8874d2c..0000000 Binary files a/doc/dist/merriweather-cyrillic-ext-300-normal-5LF5LCEK.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-latin-300-italic-353COS6Q.woff2 b/doc/dist/merriweather-latin-300-italic-353COS6Q.woff2 deleted file mode 100644 index 0c7b8ed..0000000 Binary files a/doc/dist/merriweather-latin-300-italic-353COS6Q.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-latin-300-normal-RWDJH4FN.woff2 b/doc/dist/merriweather-latin-300-normal-RWDJH4FN.woff2 deleted file mode 100644 index a119883..0000000 Binary files a/doc/dist/merriweather-latin-300-normal-RWDJH4FN.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-latin-ext-300-italic-MWCA36KE.woff2 b/doc/dist/merriweather-latin-ext-300-italic-MWCA36KE.woff2 deleted file mode 100644 index 8a6b26f..0000000 Binary files a/doc/dist/merriweather-latin-ext-300-italic-MWCA36KE.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-latin-ext-300-normal-K6L27CZ5.woff2 b/doc/dist/merriweather-latin-ext-300-normal-K6L27CZ5.woff2 deleted file mode 100644 index ff2d483..0000000 Binary files a/doc/dist/merriweather-latin-ext-300-normal-K6L27CZ5.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-vietnamese-300-italic-EHHNZPUO.woff2 b/doc/dist/merriweather-vietnamese-300-italic-EHHNZPUO.woff2 deleted file mode 100644 index ff20a5b..0000000 Binary files a/doc/dist/merriweather-vietnamese-300-italic-EHHNZPUO.woff2 and /dev/null differ diff --git a/doc/dist/merriweather-vietnamese-300-normal-U376L4Z4.woff2 b/doc/dist/merriweather-vietnamese-300-normal-U376L4Z4.woff2 deleted file mode 100644 index b250a2d..0000000 Binary files a/doc/dist/merriweather-vietnamese-300-normal-U376L4Z4.woff2 and /dev/null differ diff --git a/doc/dist/remixicon-NKANDIL5.woff2 b/doc/dist/remixicon-NKANDIL5.woff2 deleted file mode 100644 index 57e6c7c..0000000 Binary files a/doc/dist/remixicon-NKANDIL5.woff2 and /dev/null differ diff --git a/doc/dist/search_items-9462DBD2.js b/doc/dist/search_items-9462DBD2.js deleted file mode 100644 index 4e89e84..0000000 --- a/doc/dist/search_items-9462DBD2.js +++ /dev/null @@ -1 +0,0 @@ -searchNodes=[{"doc":"Public interface for client.","ref":"cfclient.html","title":"cfclient","type":"module"},{"doc":"Evaluate variation which returns a boolean.","ref":"cfclient.html#bool_variation/3","title":"cfclient.bool_variation/3","type":"function"},{"doc":"","ref":"cfclient.html#bool_variation/4","title":"cfclient.bool_variation/4","type":"function"},{"doc":"","ref":"cfclient.html#close/0","title":"cfclient.close/0","type":"function"},{"doc":"","ref":"cfclient.html#close/1","title":"cfclient.close/1","type":"function"},{"doc":"Evaluate variation which returns a JSON object.","ref":"cfclient.html#json_variation/3","title":"cfclient.json_variation/3","type":"function"},{"doc":"","ref":"cfclient.html#json_variation/4","title":"cfclient.json_variation/4","type":"function"},{"doc":"Evaluate variation which returns a number.","ref":"cfclient.html#number_variation/3","title":"cfclient.number_variation/3","type":"function"},{"doc":"","ref":"cfclient.html#number_variation/4","title":"cfclient.number_variation/4","type":"function"},{"doc":"Evaluate variation which returns a string.","ref":"cfclient.html#string_variation/3","title":"cfclient.string_variation/3","type":"function"},{"doc":"","ref":"cfclient.html#string_variation/4","title":"cfclient.string_variation/4","type":"function"},{"doc":"","ref":"cfclient.html#t:config/0","title":"cfclient.config/0","type":"type"},{"doc":"","ref":"cfclient.html#t:target/0","title":"cfclient.target/0","type":"type"},{"doc":"cfclient application.","ref":"cfclient_app.html","title":"cfclient_app","type":"module"},{"doc":"","ref":"cfclient_app.html#start/2","title":"cfclient_app.start/2","type":"function"},{"doc":"","ref":"cfclient_app.html#stop/1","title":"cfclient_app.stop/1","type":"function"},{"doc":"Functions to manage cache of Flag and Segment data from server.","ref":"cfclient_cache.html","title":"cfclient_cache","type":"module"},{"doc":"","ref":"cfclient_cache.html#cache_flag/1","title":"cfclient_cache.cache_flag/1","type":"function"},{"doc":"","ref":"cfclient_cache.html#cache_flag/2","title":"cfclient_cache.cache_flag/2","type":"function"},{"doc":"","ref":"cfclient_cache.html#cache_segment/1","title":"cfclient_cache.cache_segment/1","type":"function"},{"doc":"","ref":"cfclient_cache.html#cache_segment/2","title":"cfclient_cache.cache_segment/2","type":"function"},{"doc":"Get Flag or Segment from cache.","ref":"cfclient_cache.html#get_value/1","title":"cfclient_cache.get_value/1","type":"function"},{"doc":"","ref":"cfclient_cache.html#get_value/2","title":"cfclient_cache.get_value/2","type":"function"},{"doc":"","ref":"cfclient_cache.html#set_pid/1","title":"cfclient_cache.set_pid/1","type":"function"},{"doc":"Store flag or segment into cache with new value.","ref":"cfclient_cache.html#set_value/2","title":"cfclient_cache.set_value/2","type":"function"},{"doc":"","ref":"cfclient_cache.html#t:config/0","title":"cfclient_cache.config/0","type":"type"},{"doc":"","ref":"cfclient_cache.html#t:flag/0","title":"cfclient_cache.flag/0","type":"type"},{"doc":"","ref":"cfclient_cache.html#t:segment/0","title":"cfclient_cache.segment/0","type":"type"},{"doc":"Functions to manage client configuration.","ref":"cfclient_config.html","title":"cfclient_config","type":"module"},{"doc":"with Authenticate with server and merge project attributes into config","ref":"cfclient_config.html#authenticate/2","title":"cfclient_config.authenticate/2","type":"function"},{"doc":"","ref":"cfclient_config.html#create_tables/1","title":"cfclient_config.create_tables/1","type":"function"},{"doc":"","ref":"cfclient_config.html#defaults/0","title":"cfclient_config.defaults/0","type":"function"},{"doc":"","ref":"cfclient_config.html#delete_tables/1","title":"cfclient_config.delete_tables/1","type":"function"},{"doc":"","ref":"cfclient_config.html#get_config/0","title":"cfclient_config.get_config/0","type":"function"},{"doc":"","ref":"cfclient_config.html#get_config/1","title":"cfclient_config.get_config/1","type":"function"},{"doc":"","ref":"cfclient_config.html#get_table_names/1","title":"cfclient_config.get_table_names/1","type":"function"},{"doc":"","ref":"cfclient_config.html#get_value/1","title":"cfclient_config.get_value/1","type":"function"},{"doc":"","ref":"cfclient_config.html#get_value/2","title":"cfclient_config.get_value/2","type":"function"},{"doc":"","ref":"cfclient_config.html#init/1","title":"cfclient_config.init/1","type":"function"},{"doc":"","ref":"cfclient_config.html#normalize/1","title":"cfclient_config.normalize/1","type":"function"},{"doc":"","ref":"cfclient_config.html#parse_jwt/1","title":"cfclient_config.parse_jwt/1","type":"function"},{"doc":"","ref":"cfclient_config.html#set_config/1","title":"cfclient_config.set_config/1","type":"function"},{"doc":"","ref":"cfclient_config.html#set_config/2","title":"cfclient_config.set_config/2","type":"function"},{"doc":"","ref":"cfclient_config.html#t:config/0","title":"cfclient_config.config/0","type":"type"},{"doc":"Functions to make it easier to mock ETS","ref":"cfclient_ets.html","title":"cfclient_ets","type":"module"},{"doc":"","ref":"cfclient_ets.html#get/2","title":"cfclient_ets.get/2","type":"function"},{"doc":"","ref":"cfclient_ets.html#lookup/2","title":"cfclient_ets.lookup/2","type":"function"},{"doc":"Functions to evaluate flag rules.","ref":"cfclient_evaluator.html","title":"cfclient_evaluator","type":"module"},{"doc":"","ref":"cfclient_evaluator.html#bool_variation/3","title":"cfclient_evaluator.bool_variation/3","type":"function"},{"doc":"","ref":"cfclient_evaluator.html#custom_attribute_to_binary/1","title":"cfclient_evaluator.custom_attribute_to_binary/1","type":"function"},{"doc":"","ref":"cfclient_evaluator.html#is_rule_included_or_excluded/2","title":"cfclient_evaluator.is_rule_included_or_excluded/2","type":"function"},{"doc":"","ref":"cfclient_evaluator.html#json_variation/3","title":"cfclient_evaluator.json_variation/3","type":"function"},{"doc":"","ref":"cfclient_evaluator.html#number_variation/3","title":"cfclient_evaluator.number_variation/3","type":"function"},{"doc":"","ref":"cfclient_evaluator.html#string_variation/3","title":"cfclient_evaluator.string_variation/3","type":"function"},{"doc":"","ref":"cfclient_evaluator.html#t:config/0","title":"cfclient_evaluator.config/0","type":"type"},{"doc":"","ref":"cfclient_evaluator.html#t:flag/0","title":"cfclient_evaluator.flag/0","type":"type"},{"doc":"","ref":"cfclient_evaluator.html#t:rule/0","title":"cfclient_evaluator.rule/0","type":"type"},{"doc":"","ref":"cfclient_evaluator.html#t:rule_clause/0","title":"cfclient_evaluator.rule_clause/0","type":"type"},{"doc":"","ref":"cfclient_evaluator.html#t:rule_serve/0","title":"cfclient_evaluator.rule_serve/0","type":"type"},{"doc":"","ref":"cfclient_evaluator.html#t:segment/0","title":"cfclient_evaluator.segment/0","type":"type"},{"doc":"","ref":"cfclient_evaluator.html#t:target/0","title":"cfclient_evaluator.target/0","type":"type"},{"doc":"","ref":"cfclient_evaluator.html#t:variation_map/0","title":"cfclient_evaluator.variation_map/0","type":"type"},{"doc":"Feature flags client instance. It creates the ETS tables used to cache flag data from the server and flag usage metrics. It runs periodic tasks to pull data from the server and send metrics to it. An default instance is started by the cfclient application. Additional instances can be started if multiple Harness projects need to be used. project.","ref":"cfclient_instance.html","title":"cfclient_instance","type":"module"},{"doc":"","ref":"cfclient_instance.html#handle_call/3","title":"cfclient_instance.handle_call/3","type":"function"},{"doc":"","ref":"cfclient_instance.html#handle_cast/2","title":"cfclient_instance.handle_cast/2","type":"function"},{"doc":"","ref":"cfclient_instance.html#handle_info/2","title":"cfclient_instance.handle_info/2","type":"function"},{"doc":"","ref":"cfclient_instance.html#init/1","title":"cfclient_instance.init/1","type":"function"},{"doc":"","ref":"cfclient_instance.html#start_link/1","title":"cfclient_instance.start_link/1","type":"function"},{"doc":"","ref":"cfclient_instance.html#stop/1","title":"cfclient_instance.stop/1","type":"function"},{"doc":"Functions to record, process, and send cached metric data.","ref":"cfclient_metrics.html","title":"cfclient_metrics","type":"module"},{"doc":"Gather metrics and send them to server. Called periodically by cfclient_instance.","ref":"cfclient_metrics.html#process_metrics/1","title":"cfclient_metrics.process_metrics/1","type":"function"},{"doc":"Record metrics for request.","ref":"cfclient_metrics.html#record/5","title":"cfclient_metrics.record/5","type":"function"},{"doc":"","ref":"cfclient_metrics.html#t:config/0","title":"cfclient_metrics.config/0","type":"type"},{"doc":"Funcctions to pull feature and target configuration from server via the API.","ref":"cfclient_retrieve.html","title":"cfclient_retrieve","type":"module"},{"doc":"Retrieve all features from Feature Flags API.","ref":"cfclient_retrieve.html#retrieve_flags/1","title":"cfclient_retrieve.retrieve_flags/1","type":"function"},{"doc":"Retrieve all segments from Feature Flags API.","ref":"cfclient_retrieve.html#retrieve_segments/1","title":"cfclient_retrieve.retrieve_segments/1","type":"function"},{"doc":"","ref":"cfclient_retrieve.html#t:config/0","title":"cfclient_retrieve.config/0","type":"type"},{"doc":"","ref":"cfclient_retrieve.html#t:flag/0","title":"cfclient_retrieve.flag/0","type":"type"},{"doc":"","ref":"cfclient_retrieve.html#t:segment/0","title":"cfclient_retrieve.segment/0","type":"type"},{"doc":"Top level supervisor for cfclient. Called by application, starting up the default client instance.","ref":"cfclient_sup.html","title":"cfclient_sup","type":"module"},{"doc":"","ref":"cfclient_sup.html#init/1","title":"cfclient_sup.init/1","type":"function"},{"doc":"","ref":"cfclient_sup.html#start_link/1","title":"cfclient_sup.start_link/1","type":"function"},{"doc":"Harness is a feature management platform that helps teams to build better software and to test features quicker. This repository contains our Feature Flags SDK for Erlang and other BEAM languages such as Elixir.","ref":"readme.html","title":"Erlang SDK For Harness Feature Flags","type":"extras"},{"doc":"Intro <br> Requirements <br> Quickstart <br> Further Reading <br> Build Instructions <br>","ref":"readme.html#table-of-contents","title":"Erlang SDK For Harness Feature Flags - Table of Contents","type":"extras"},{"doc":"This sample doesn’t include configuration options. For in depth steps and configuring the SDK, e.g. disabling streaming or using our Relay Proxy, see the Erlang SDK Reference For a sample FF Erlang SDK project, see our test Erlang project . For a sample FF Erlang SDK Project for Elixir, see our test Elixir Project .","ref":"readme.html#intro","title":"Erlang SDK For Harness Feature Flags - Intro","type":"extras"},{"doc":"Erlang OTP 22 or newer.","ref":"readme.html#requirements","title":"Erlang SDK For Harness Feature Flags - Requirements","type":"extras"},{"doc":"To follow along with our test code sample, make sure you have: Created a Feature Flag on the Harness Platform called harnessappdemodarkmode Created a server SDK key and made a copy of it","ref":"readme.html#quickstart","title":"Erlang SDK For Harness Feature Flags - Quickstart","type":"extras"},{"doc":"For Erlang applications To install the SDK for Erlang based applications: Add the SDK as a dependency to your rebar.config file: { deps , [ { cfclient , "1.2.0" , { pkg , harness_ff_erlang_server_sdk } } ] } . Add the dependency to your project's app.src . { applications , [ kernel , stdlib , cfclient ] } , For Elixir applications To install the SDK for Elixir based applications: Add the SDK as a dependency to mix.exs deps() : defp deps do [ { : cfclient , "~> 1.1.0" , hex : : harness_ff_erlang_server_sdk } ]","ref":"readme.html#install-the-sdk","title":"Erlang SDK For Harness Feature Flags - Install the SDK","type":"extras"},{"doc":"Erlang Provide your API key in sys.config using an environment variable: [ { cfclient , [ { api_key , { environment_variable , "YOUR_API_KEY_ENV_VARIABLE" } , ] } ] . Or you may provide the API key directly if required: [ { cfclient , [ { api_key , "YOUR_API_KEY" } , ] } ] . Elixir Provide your API key in config/prod.exs using an environment variable: : config :cfclient , api_key : System . get_env ( "YOUR_API_KEY_ENVIRONMENT_VARIABLE" ) Or you may provide the API key directly if required: config :cfclient , api_key : "YOUR_API_KEY"","ref":"readme.html#configuration","title":"Erlang SDK For Harness Feature Flags - Configuration","type":"extras"},{"doc":"Optionally you may set the required log level of the SDK. If not provided, the SDK will default to warning . Elixir logging configuration example config :cfclient , # Set the log level of the SDK to debug log_level : :debug [ api_key : System . get_env ( "FF_API_KEY_0" ) , # For additional config you can pass in, see Erlang SDK docs: https://github.com/harness/ff-erlang-server-sdk/blob/main/docs/further_reading.md#further-reading # we are just using the main config url here as an example. config : [ config_url : "https://config.ff.harness.io/api/1.0" , events_url : "https://events.ff.harness.io/api/1.0" , poll_interval : 60000 , analytics_enabled : true ] ] Erlang logging configuration example [ { cfclient , [ %% Set the log level of the SDK to debug { log_level , debug } , { api_key , { envrionment_variable , "YOUR_API_KEY_ENV_VARIABLE" } , { config , [ { config_url , "https://config.ff.harness.io/api/1.0" } , { events_url , "https://config.ff.harness.io/api/1.0" } , { poll_interval , 60 } , { analytics_enabled , true } , ] } , ] } ] Enable Verbose Evaluation Logs Evaluation logs are debug level by default. If required, they can be changed to info level. This is useful if production environments do not use debug level, but there is a requirement to check low level evaluation logs. Note that this will only affect evaluation log statements. Elixir config :cfclient , log_level : :error [ api_key : System . get_env ( "FF_API_KEY_0" ) , config : [ verbose_evaluation_logs : true ] ] Erlang [ { cfclient , [ { log_level , error } , { api_key , { envrionment_variable , "YOUR_API_KEY_ENV_VARIABLE" } , { config , [ { verbose_evaluation_logs , true } , ] } , ] } ]","ref":"readme.html#set-logging-level","title":"Erlang SDK For Harness Feature Flags - Set logging level","type":"extras"},{"doc":"The SDK by default starts up a single instance called default which is configured with your project API key. If different parts of your application need to use specific projects , you can start up additional client instances using by defining additional configuration for each unique project. Erlang Project Config The additional project config is defined in sys.config The following sys.config snippet starts up two additional instances as well along with the default instance: [ %% Project config name: This is an arbitrary identifier, but it must be unique per project config you define. { harness_project_1_config , [ { cfclient , [ { config , [ %% Instance name: This must be unique across all of the project configs. E.g. it cannot be the same as an instance name %% in another project config. %% It will be the name you use when calling SDK API functions like `bool_variation/4`, { name , instance_name_1 } ] } , %% The API key for the Harness project you want to use with this SDK instance. { api_key , { environment_variable , "PROJECT_1_API_KEY" } } ] } ] } , { harness_project_2_config , [ { cfclient , [ { config , [ { name , instance_name_2 } ] } , { api_key , { environment_variable , "PROJECT_2_API_KEY" } } ] } ] } , { cfclient , [ { api_key , { environment_variable , "FF_API_KEY" } } , { config , [ { config_url , "https://config.ff.harness.io/api/1.0" } , { events_url , "https://config.ff.harness.io/api/1.0" } ] } , { analytics_push_interval , 60000 } ] } ] . Note: if the default instance fails to start, for example due to an authentication error with the API key, then the SDK will fail to boot and the additional instances won't start. If you don't require the default instance to be started up, you can do: % ... additional project config { cfclient , [ { start_default_instance , false } , %% The remaining tuples will be ignored, so you can choose to include or omit them. { api_key , { environment_variable , "FF_API_KEY" } } , { config , [ { config_url , "https://config.ff.harness.io/api/1.0" } , { events_url , "https://config.ff.harness.io/api/1.0" } ] } , { analytics_push_interval , 60000 } ] } , In your application supervisor, e.g. src/myapp_sup.erl , start up a cfclient_instance for each additional project. As the default instance is booted when your application starts, you cannot (and don't need to) start it here. init ( Args ) -> HarnessProject1Args = application : get_env ( harness_project_1_config , cfclient , [ ] ) , HarnessProject2Args = application : get_env ( harness_project_2_config , cfclient , [ ] ) , ChildSpec1 = \#{ id => project1_cfclient_instance , start => { cfclient_instance , start_link , [ HarnessProject1Args ] } } , ChildSpec2 = \#{ id => project2_cfclient_instance , start => { cfclient_instance , start_link , [ HarnessProject2Args ] } } , MaxRestarts = 1000 , MaxSecondsBetweenRestarts = 3600 , SupFlags = \#{ strategy => one_for_one , intensity => MaxRestarts , period => MaxSecondsBetweenRestarts } , { ok , { SupFlags , [ ChildSpec1 , ChildSpec2 ] } } . Using a specific instance of the SDK To use a specific SDK instance, you provide the instance name to the public function you are calling. For example bool_variation/4 . The following is an example of referencing the instances we have created above: - module ( multi_instance_example ) . - export ( [ multi_instance_evaluations / 0 ] ) . multi_instance_evaluations ( ) -> Target = \#{ identifier => "Harness_Target_1" , name => "HT_1" , attributes => \#{ email => << "demo@harness.io" >> } } , %% Instance 1 Project1Flag = << "harnessappdemodarkmodeproject1" >> , Project1Result = cfclient : bool_variation ( instance_name_1 , Project1Flag , Target , false ) , logger : info ( "Instance Name 1 : Variation for Flag ~p with Target ~p is: ~p ~n " , [ Project1Flag , maps : get ( identifier , Target ) , Project1Result ] ) , %% Instance 2 Project2Flag = << "harnessappdemodarkmodeproject2" >> , Project2Result = cfclient : bool_variation ( instance_name_2 , Project2Flag , Target , false ) , logger : info ( "Instance name 2 Variation for Flag ~p with Target ~p is: ~p ~n " , [ Project2Flag , maps : get ( identifier , Target ) , Project2Result ] ) . %% Default instance DefaultProjectFlag = << "harnessappdemodarkmodeprojectdefault" >> , DefaultProjectResult = cfclient : bool_variation ( Project2Flag , Target , false ) , logger : info ( "Default instance Variation for Flag ~p with Target ~p is: ~p ~n " , [ DefaultProjectFlag , maps : get ( identifier , Target ) , DefaultProjectResult ] ) . Elixir Create project configurations for each new instance you would like to start in your config/config.exs file: # Config for "project 1" config :elixirsample , project1 : [ api_key : System . get_env ( "FF_API_KEY_1" ) , config : [ name : :project1 ] ] # Config for "project 2" config :elixirsample , project2 : [ api_key : System . get_env ( "FF_API_KEY_2" ) , config : [ name : :project2 ] ] In your application supervisor, e.g. lib/myapp/supervisor.ex , start up cfclient_instance for each of the additional project configurations you provided above. As the default instance is booted when your application starts, you cannot (and don't need to) start it here: def init ( _opts ) do project_1_config = Application . get_env ( :elixirsample , :project1 , [ ] ) project_2_config = Application . get_env ( :elixirsample , :project2 , [ ] ) children = [ %{ id : :project1_cfclient_instance , start : { :cfclient_instance , :start_link , [ project_1_config ] } , type : :supervisor } , %{ id : :project2_cfclient_instance , start : { :cfclient_instance , :start_link , [ project_2_config ] } , type : :supervisor } , ] Supervisor . init ( children , strategy : :one_for_one ) end To use a specific SDK instance, you provide the instance name to the public function you are calling. For example use bool_variation/4 instead of bool_variation/3 - see the following code sample: defmodule ElixirSample.EvaluationSample do require Logger def getFlagLoop ( ) do target = %{ identifier : "harness" , name : "Harness" , anonymous : false , attributes : %{ } } # Default instance flag = "projectflag" result = :cfclient . bool_variation ( flag , target , false ) Logger . info ( "SVariation for Flag \#{ flag } with Target \#{ inspect ( target ) } is: \#{ result } " ) # Instance 1 project_1_flag = "project1flag" project_1_result = :cfclient . number_variation ( :project1 , project_1_flag , target , 3 ) Logger . info ( "SDK instance 1: Variation for Flag \#{ project_1_flag } with Target \#{ inspect ( target ) } is: \#{ project_1_result } " ) # Instance 2 project_2_flag = "project2flag" project_2_result = :cfclient . bool_variation ( :project2 , project_2_flag , target , false ) Logger . info ( "SDK instance 2: Variation for Flag \#{ project_2_flag } with Target \#{ inspect ( target ) } is: \#{ project_2_result } " ) Process . sleep ( 10000 ) getFlagLoop ( ) # Default instance default_project_flag = "defaultflag" default_project_result = :cfclient . bool_variation ( default_project_flag , target , false ) Logger . info ( "Default instance: Variation for Flag \#{ default_project_flag } with Target \#{ inspect ( target ) } is: \#{ default_project_result } " ) Process . sleep ( 10000 ) getFlagLoop ( ) end end","ref":"readme.html#run-multiple-instances-of-the-sdk","title":"Erlang SDK For Harness Feature Flags - Run multiple instances of the SDK","type":"extras"},{"doc":"Erlang Call the API to get the value of the harnessappdemodarkmode flag you created via https://www.harness.io/ . get_flag_loop ( ) -> Target = \#{ identifier => "Harness_Target_1" , name => "HT_1" , %% Attribute keys must be atoms. %% Values must be either bitstrings, atoms, or a list of bitstrings/atoms - see Targets with custom attributes section below. attributes => \#{ email => << "demo@harness.io" >> } } , FlagIdentifier = "harnessappdemodarkmode" , Result = cfclient : bool_variation ( FlagIdentifier , Target , false ) , logger : info ( "Variation for Flag ~p witih Target ~p is: ~p ~n " , [ FlagIdentifier , maps : get ( identifier , Target ) , Result ] ) , timer : sleep ( 10000 ) , get_flag_loop ( ) . Elixir Call the API to get the value of the harnessappdemodarkmode flag you created via https://www.harness.io/ . def getFlagLoop ( ) do target = %{ identifier : "Harness_Target_1" , name : "HT_1" # Attribute keys must be atoms. # Values must be either binaries, atoms, or a list of binaries/atoms. # See "targets with custom attributes" below. attributes : %{ email : "demo@harness.io" } } flag_identifier = "harnessappdemodarkmode" result = :cfclient . bool_variation ( flag_identifier , target , false ) Logger . info ( "Variation for Flag \#{ flag_identifier } with Target \#{ inspect ( target ) } is: \#{result)" ) Process . sleep ( 10000 ) getFlagLoop ( )","ref":"readme.html#code-sample","title":"Erlang SDK For Harness Feature Flags - Code Sample","type":"extras"},{"doc":"You can use the attributes map to provide custom attributes. If the target isn't anonymous, the attributes will shortly appear in the Harness UI after an evaluation using the target. You can create Group Rules based on these attributes. Note: attribute keys must be atoms and the values must either be binaries or atoms or a list of binaries or atoms . Erlang: TargetBetaGroup = \#{ 'identifier' => << "my_target" >> , name => << "my_target_name" >> , anonymous => << "" >> , attributes => \#{ beta => << "beta_group_1" >> } } , TargetBetaGroups = \#{ 'identifier' => << "my_other_target" >> , name => << "my_other_target_name" >> , anonymous => << "" >> , attributes => \#{ beta => [ << "beta_group_1" >> , 'beta_group_2' ] } } } , TargetAlphaGroup = \#{ 'identifier' => << "my_alpha_target" >> , name => << "my_alpha_target_name" >> , anonymous => << "" >> , attributes => \#{ alpha => 'alpha_group_1' } } , Elixir target_beta_group = %{ identifier : "my_target" , name : "my_target_name" , anonymous : "" , attributes : %{ beta : "beta_group_1" } } target_beta_groups = %{ identifier : "my_other_target" , name : "my_other_target_name" , anonymous : "" , attributes : %{ beta : [ "beta_group_1" , :beta_group_2 ] } } target_alpha_group = %{ identifier : "my_alpha_target" , name : "my_alpha_target_name" , anonymous : "" , attributes : %{ alpha : :alpha_group_1 } }","ref":"readme.html#targets-with-custom-attributes","title":"Erlang SDK For Harness Feature Flags - Targets with custom attributes","type":"extras"},{"doc":"For further examples and config options, see the Erlang SDK Further Reading . For more information about Feature Flags, see our Feature Flags documentation .","ref":"readme.html#additional-reading","title":"Erlang SDK For Harness Feature Flags - Additional Reading","type":"extras"},{"doc":"In order to run the tests, pull the submodules: git submodule update --init","ref":"readme.html#contributing","title":"Erlang SDK For Harness Feature Flags - Contributing","type":"extras"},{"doc":"Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","ref":"license.html","title":"LICENSE","type":"extras"}] \ No newline at end of file diff --git a/doc/dist/sidebar_items-32BCB229.js b/doc/dist/sidebar_items-32BCB229.js deleted file mode 100644 index 39a2a4f..0000000 --- a/doc/dist/sidebar_items-32BCB229.js +++ /dev/null @@ -1 +0,0 @@ -sidebarNodes={"extras":[{"group":"","headers":[{"anchor":"modules","id":"Modules"}],"id":"api-reference","title":"API Reference"},{"group":"","headers":[{"anchor":"table-of-contents","id":"Table of Contents"},{"anchor":"intro","id":"Intro"},{"anchor":"requirements","id":"Requirements"},{"anchor":"quickstart","id":"Quickstart"},{"anchor":"install-the-sdk","id":"Install the SDK"},{"anchor":"configuration","id":"Configuration"},{"anchor":"set-logging-level","id":"Set logging level"},{"anchor":"run-multiple-instances-of-the-sdk","id":"Run multiple instances of the SDK"},{"anchor":"code-sample","id":"Code Sample"},{"anchor":"targets-with-custom-attributes","id":"Targets with custom attributes"},{"anchor":"additional-reading","id":"Additional Reading"},{"anchor":"contributing","id":"Contributing"}],"id":"readme","title":"Erlang SDK For Harness Feature Flags"},{"group":"","headers":[],"id":"license","title":"LICENSE"}],"modules":[{"group":"","id":"cfclient","nodeGroups":[{"key":"types","name":"Types","nodes":[{"anchor":"t:config/0","id":"config/0","title":"config/0"},{"anchor":"t:target/0","id":"target/0","title":"target/0"}]},{"key":"functions","name":"Functions","nodes":[{"anchor":"bool_variation/3","id":"bool_variation/3","title":"bool_variation(FlagKey, Target, Default)"},{"anchor":"bool_variation/4","id":"bool_variation/4","title":"bool_variation(Config, FlagKey, Target, Default)"},{"anchor":"close/0","id":"close/0","title":"close()"},{"anchor":"close/1","id":"close/1","title":"close(Name)"},{"anchor":"json_variation/3","id":"json_variation/3","title":"json_variation(FlagKey, Target, Default)"},{"anchor":"json_variation/4","id":"json_variation/4","title":"json_variation(Config, FlagKey, Target, Default)"},{"anchor":"number_variation/3","id":"number_variation/3","title":"number_variation(FlagKey, Target, Default)"},{"anchor":"number_variation/4","id":"number_variation/4","title":"number_variation(Config, FlagKey, Target, Default)"},{"anchor":"string_variation/3","id":"string_variation/3","title":"string_variation(FlagKey, Target, Default)"},{"anchor":"string_variation/4","id":"string_variation/4","title":"string_variation(Config, FlagKey, Target, Default)"}]}],"sections":[],"title":"cfclient"},{"group":"","id":"cfclient_app","nodeGroups":[{"key":"functions","name":"Functions","nodes":[{"anchor":"start/2","id":"start/2","title":"start(StartType, StartArgs)"},{"anchor":"stop/1","id":"stop/1","title":"stop(State)"}]}],"sections":[],"title":"cfclient_app"},{"group":"","id":"cfclient_cache","nodeGroups":[{"key":"types","name":"Types","nodes":[{"anchor":"t:config/0","id":"config/0","title":"config/0"},{"anchor":"t:flag/0","id":"flag/0","title":"flag/0"},{"anchor":"t:segment/0","id":"segment/0","title":"segment/0"}]},{"key":"functions","name":"Functions","nodes":[{"anchor":"cache_flag/1","id":"cache_flag/1","title":"cache_flag(Value)"},{"anchor":"cache_flag/2","id":"cache_flag/2","title":"cache_flag(Value, Config)"},{"anchor":"cache_segment/1","id":"cache_segment/1","title":"cache_segment(Value)"},{"anchor":"cache_segment/2","id":"cache_segment/2","title":"cache_segment(Value, Config)"},{"anchor":"get_value/1","id":"get_value/1","title":"get_value(_)"},{"anchor":"get_value/2","id":"get_value/2","title":"get_value(_, Config)"},{"anchor":"set_pid/1","id":"set_pid/1","title":"set_pid(_)"},{"anchor":"set_value/2","id":"set_value/2","title":"set_value(_, Value)"}]}],"sections":[],"title":"cfclient_cache"},{"group":"","id":"cfclient_config","nodeGroups":[{"key":"types","name":"Types","nodes":[{"anchor":"t:config/0","id":"config/0","title":"config/0"}]},{"key":"functions","name":"Functions","nodes":[{"anchor":"authenticate/2","id":"authenticate/2","title":"authenticate(ApiKey, Config)"},{"anchor":"create_tables/1","id":"create_tables/1","title":"create_tables(Config)"},{"anchor":"defaults/0","id":"defaults/0","title":"defaults()"},{"anchor":"delete_tables/1","id":"delete_tables/1","title":"delete_tables(T)"},{"anchor":"get_config/0","id":"get_config/0","title":"get_config()"},{"anchor":"get_config/1","id":"get_config/1","title":"get_config(Name)"},{"anchor":"get_table_names/1","id":"get_table_names/1","title":"get_table_names(Config)"},{"anchor":"get_value/1","id":"get_value/1","title":"get_value(Key)"},{"anchor":"get_value/2","id":"get_value/2","title":"get_value(Key, Opts)"},{"anchor":"init/1","id":"init/1","title":"init(Config0)"},{"anchor":"normalize/1","id":"normalize/1","title":"normalize(Config0)"},{"anchor":"parse_jwt/1","id":"parse_jwt/1","title":"parse_jwt(JwtToken)"},{"anchor":"set_config/1","id":"set_config/1","title":"set_config(Config)"},{"anchor":"set_config/2","id":"set_config/2","title":"set_config(Name, Config)"}]}],"sections":[],"title":"cfclient_config"},{"group":"","id":"cfclient_ets","nodeGroups":[{"key":"functions","name":"Functions","nodes":[{"anchor":"get/2","id":"get/2","title":"get(Table, Key)"},{"anchor":"lookup/2","id":"lookup/2","title":"lookup(Table, Key)"}]}],"sections":[],"title":"cfclient_ets"},{"group":"","id":"cfclient_evaluator","nodeGroups":[{"key":"types","name":"Types","nodes":[{"anchor":"t:config/0","id":"config/0","title":"config/0"},{"anchor":"t:flag/0","id":"flag/0","title":"flag/0"},{"anchor":"t:rule/0","id":"rule/0","title":"rule/0"},{"anchor":"t:rule_clause/0","id":"rule_clause/0","title":"rule_clause/0"},{"anchor":"t:rule_serve/0","id":"rule_serve/0","title":"rule_serve/0"},{"anchor":"t:segment/0","id":"segment/0","title":"segment/0"},{"anchor":"t:target/0","id":"target/0","title":"target/0"},{"anchor":"t:variation_map/0","id":"variation_map/0","title":"variation_map/0"}]},{"key":"functions","name":"Functions","nodes":[{"anchor":"bool_variation/3","id":"bool_variation/3","title":"bool_variation(FlagId, Target, Config)"},{"anchor":"custom_attribute_to_binary/1","id":"custom_attribute_to_binary/1","title":"custom_attribute_to_binary(Value)"},{"anchor":"is_rule_included_or_excluded/2","id":"is_rule_included_or_excluded/2","title":"is_rule_included_or_excluded(Clauses, Target)"},{"anchor":"json_variation/3","id":"json_variation/3","title":"json_variation(FlagId, Target, Config)"},{"anchor":"number_variation/3","id":"number_variation/3","title":"number_variation(FlagId, Target, Config)"},{"anchor":"string_variation/3","id":"string_variation/3","title":"string_variation(FlagId, Target, Config)"}]}],"sections":[],"title":"cfclient_evaluator"},{"group":"","id":"cfclient_instance","nodeGroups":[{"key":"functions","name":"Functions","nodes":[{"anchor":"handle_call/3","id":"handle_call/3","title":"handle_call(_, From, State)"},{"anchor":"handle_cast/2","id":"handle_cast/2","title":"handle_cast(_, State)"},{"anchor":"handle_info/2","id":"handle_info/2","title":"handle_info(_, Config)"},{"anchor":"init/1","id":"init/1","title":"init(Args)"},{"anchor":"start_link/1","id":"start_link/1","title":"start_link(Args)"},{"anchor":"stop/1","id":"stop/1","title":"stop(Config)"}]}],"sections":[],"title":"cfclient_instance"},{"group":"","id":"cfclient_metrics","nodeGroups":[{"key":"types","name":"Types","nodes":[{"anchor":"t:config/0","id":"config/0","title":"config/0"}]},{"key":"functions","name":"Functions","nodes":[{"anchor":"process_metrics/1","id":"process_metrics/1","title":"process_metrics(Config)"},{"anchor":"record/5","id":"record/5","title":"record(FlagId, Target, VariationId, VariationValue, Config)"}]}],"sections":[],"title":"cfclient_metrics"},{"group":"","id":"cfclient_retrieve","nodeGroups":[{"key":"types","name":"Types","nodes":[{"anchor":"t:config/0","id":"config/0","title":"config/0"},{"anchor":"t:flag/0","id":"flag/0","title":"flag/0"},{"anchor":"t:segment/0","id":"segment/0","title":"segment/0"}]},{"key":"functions","name":"Functions","nodes":[{"anchor":"retrieve_flags/1","id":"retrieve_flags/1","title":"retrieve_flags(Config)"},{"anchor":"retrieve_segments/1","id":"retrieve_segments/1","title":"retrieve_segments(Config)"}]}],"sections":[],"title":"cfclient_retrieve"},{"group":"","id":"cfclient_sup","nodeGroups":[{"key":"functions","name":"Functions","nodes":[{"anchor":"init/1","id":"init/1","title":"init(Args)"},{"anchor":"start_link/1","id":"start_link/1","title":"start_link(Args)"}]}],"sections":[],"title":"cfclient_sup"}],"tasks":[]} \ No newline at end of file diff --git a/doc/index.html b/doc/index.html deleted file mode 100644 index 666d183..0000000 --- a/doc/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - cfclient v1.2.1 — Documentation - - - - - diff --git a/doc/license.html b/doc/license.html deleted file mode 100644 index 88aca0b..0000000 --- a/doc/license.html +++ /dev/null @@ -1,366 +0,0 @@ - - - - - - - - - - LICENSE — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
    - - - - - -
    - -
    -
    - -

    - - - - - - View Source - - - - LICENSE -

    - -
    -                                 Apache License
    -                           Version 2.0, January 2004
    -                        http://www.apache.org/licenses/
    -
    -   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    -
    -   1. Definitions.
    -
    -      "License" shall mean the terms and conditions for use, reproduction,
    -      and distribution as defined by Sections 1 through 9 of this document.
    -
    -      "Licensor" shall mean the copyright owner or entity authorized by
    -      the copyright owner that is granting the License.
    -
    -      "Legal Entity" shall mean the union of the acting entity and all
    -      other entities that control, are controlled by, or are under common
    -      control with that entity. For the purposes of this definition,
    -      "control" means (i) the power, direct or indirect, to cause the
    -      direction or management of such entity, whether by contract or
    -      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    -      outstanding shares, or (iii) beneficial ownership of such entity.
    -
    -      "You" (or "Your") shall mean an individual or Legal Entity
    -      exercising permissions granted by this License.
    -
    -      "Source" form shall mean the preferred form for making modifications,
    -      including but not limited to software source code, documentation
    -      source, and configuration files.
    -
    -      "Object" form shall mean any form resulting from mechanical
    -      transformation or translation of a Source form, including but
    -      not limited to compiled object code, generated documentation,
    -      and conversions to other media types.
    -
    -      "Work" shall mean the work of authorship, whether in Source or
    -      Object form, made available under the License, as indicated by a
    -      copyright notice that is included in or attached to the work
    -      (an example is provided in the Appendix below).
    -
    -      "Derivative Works" shall mean any work, whether in Source or Object
    -      form, that is based on (or derived from) the Work and for which the
    -      editorial revisions, annotations, elaborations, or other modifications
    -      represent, as a whole, an original work of authorship. For the purposes
    -      of this License, Derivative Works shall not include works that remain
    -      separable from, or merely link (or bind by name) to the interfaces of,
    -      the Work and Derivative Works thereof.
    -
    -      "Contribution" shall mean any work of authorship, including
    -      the original version of the Work and any modifications or additions
    -      to that Work or Derivative Works thereof, that is intentionally
    -      submitted to Licensor for inclusion in the Work by the copyright owner
    -      or by an individual or Legal Entity authorized to submit on behalf of
    -      the copyright owner. For the purposes of this definition, "submitted"
    -      means any form of electronic, verbal, or written communication sent
    -      to the Licensor or its representatives, including but not limited to
    -      communication on electronic mailing lists, source code control systems,
    -      and issue tracking systems that are managed by, or on behalf of, the
    -      Licensor for the purpose of discussing and improving the Work, but
    -      excluding communication that is conspicuously marked or otherwise
    -      designated in writing by the copyright owner as "Not a Contribution."
    -
    -      "Contributor" shall mean Licensor and any individual or Legal Entity
    -      on behalf of whom a Contribution has been received by Licensor and
    -      subsequently incorporated within the Work.
    -
    -   2. Grant of Copyright License. Subject to the terms and conditions of
    -      this License, each Contributor hereby grants to You a perpetual,
    -      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -      copyright license to reproduce, prepare Derivative Works of,
    -      publicly display, publicly perform, sublicense, and distribute the
    -      Work and such Derivative Works in Source or Object form.
    -
    -   3. Grant of Patent License. Subject to the terms and conditions of
    -      this License, each Contributor hereby grants to You a perpetual,
    -      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    -      (except as stated in this section) patent license to make, have made,
    -      use, offer to sell, sell, import, and otherwise transfer the Work,
    -      where such license applies only to those patent claims licensable
    -      by such Contributor that are necessarily infringed by their
    -      Contribution(s) alone or by combination of their Contribution(s)
    -      with the Work to which such Contribution(s) was submitted. If You
    -      institute patent litigation against any entity (including a
    -      cross-claim or counterclaim in a lawsuit) alleging that the Work
    -      or a Contribution incorporated within the Work constitutes direct
    -      or contributory patent infringement, then any patent licenses
    -      granted to You under this License for that Work shall terminate
    -      as of the date such litigation is filed.
    -
    -   4. Redistribution. You may reproduce and distribute copies of the
    -      Work or Derivative Works thereof in any medium, with or without
    -      modifications, and in Source or Object form, provided that You
    -      meet the following conditions:
    -
    -      (a) You must give any other recipients of the Work or
    -          Derivative Works a copy of this License; and
    -
    -      (b) You must cause any modified files to carry prominent notices
    -          stating that You changed the files; and
    -
    -      (c) You must retain, in the Source form of any Derivative Works
    -          that You distribute, all copyright, patent, trademark, and
    -          attribution notices from the Source form of the Work,
    -          excluding those notices that do not pertain to any part of
    -          the Derivative Works; and
    -
    -      (d) If the Work includes a "NOTICE" text file as part of its
    -          distribution, then any Derivative Works that You distribute must
    -          include a readable copy of the attribution notices contained
    -          within such NOTICE file, excluding those notices that do not
    -          pertain to any part of the Derivative Works, in at least one
    -          of the following places: within a NOTICE text file distributed
    -          as part of the Derivative Works; within the Source form or
    -          documentation, if provided along with the Derivative Works; or,
    -          within a display generated by the Derivative Works, if and
    -          wherever such third-party notices normally appear. The contents
    -          of the NOTICE file are for informational purposes only and
    -          do not modify the License. You may add Your own attribution
    -          notices within Derivative Works that You distribute, alongside
    -          or as an addendum to the NOTICE text from the Work, provided
    -          that such additional attribution notices cannot be construed
    -          as modifying the License.
    -
    -      You may add Your own copyright statement to Your modifications and
    -      may provide additional or different license terms and conditions
    -      for use, reproduction, or distribution of Your modifications, or
    -      for any such Derivative Works as a whole, provided Your use,
    -      reproduction, and distribution of the Work otherwise complies with
    -      the conditions stated in this License.
    -
    -   5. Submission of Contributions. Unless You explicitly state otherwise,
    -      any Contribution intentionally submitted for inclusion in the Work
    -      by You to the Licensor shall be under the terms and conditions of
    -      this License, without any additional terms or conditions.
    -      Notwithstanding the above, nothing herein shall supersede or modify
    -      the terms of any separate license agreement you may have executed
    -      with Licensor regarding such Contributions.
    -
    -   6. Trademarks. This License does not grant permission to use the trade
    -      names, trademarks, service marks, or product names of the Licensor,
    -      except as required for reasonable and customary use in describing the
    -      origin of the Work and reproducing the content of the NOTICE file.
    -
    -   7. Disclaimer of Warranty. Unless required by applicable law or
    -      agreed to in writing, Licensor provides the Work (and each
    -      Contributor provides its Contributions) on an "AS IS" BASIS,
    -      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    -      implied, including, without limitation, any warranties or conditions
    -      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    -      PARTICULAR PURPOSE. You are solely responsible for determining the
    -      appropriateness of using or redistributing the Work and assume any
    -      risks associated with Your exercise of permissions under this License.
    -
    -   8. Limitation of Liability. In no event and under no legal theory,
    -      whether in tort (including negligence), contract, or otherwise,
    -      unless required by applicable law (such as deliberate and grossly
    -      negligent acts) or agreed to in writing, shall any Contributor be
    -      liable to You for damages, including any direct, indirect, special,
    -      incidental, or consequential damages of any character arising as a
    -      result of this License or out of the use or inability to use the
    -      Work (including but not limited to damages for loss of goodwill,
    -      work stoppage, computer failure or malfunction, or any and all
    -      other commercial damages or losses), even if such Contributor
    -      has been advised of the possibility of such damages.
    -
    -   9. Accepting Warranty or Additional Liability. While redistributing
    -      the Work or Derivative Works thereof, You may choose to offer,
    -      and charge a fee for, acceptance of support, warranty, indemnity,
    -      or other liability obligations and/or rights consistent with this
    -      License. However, in accepting such obligations, You may act only
    -      on Your own behalf and on Your sole responsibility, not on behalf
    -      of any other Contributor, and only if You agree to indemnify,
    -      defend, and hold each Contributor harmless for any liability
    -      incurred by, or claims asserted against, such Contributor by reason
    -      of your accepting any such warranty or additional liability.
    -
    -   END OF TERMS AND CONDITIONS
    -
    -   APPENDIX: How to apply the Apache License to your work.
    -
    -      To apply the Apache License to your work, attach the following
    -      boilerplate notice, with the fields enclosed by brackets "[]"
    -      replaced with your own identifying information. (Don't include
    -      the brackets!)  The text should be enclosed in the appropriate
    -      comment syntax for the file format. We also recommend that a
    -      file or class name and description of purpose be included on the
    -      same "printed page" as the copyright notice for easier
    -      identification within third-party archives.
    -
    -   Copyright [yyyy] [name of copyright owner]
    -
    -   Licensed under the Apache License, Version 2.0 (the "License");
    -   you may not use this file except in compliance with the License.
    -   You may obtain a copy of the License at
    -
    -       http://www.apache.org/licenses/LICENSE-2.0
    -
    -   Unless required by applicable law or agreed to in writing, software
    -   distributed under the License is distributed on an "AS IS" BASIS,
    -   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    -   See the License for the specific language governing permissions and
    -   limitations under the License.
    - - -
    -
    -
    -
    - - - - diff --git a/doc/readme.html b/doc/readme.html deleted file mode 100644 index bf29d8e..0000000 --- a/doc/readme.html +++ /dev/null @@ -1,614 +0,0 @@ - - - - - - - - - - Erlang SDK For Harness Feature Flags — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
    - - - - - -
    - -
    -
    - -

    - - - - - - View Source - - - - Erlang SDK For Harness Feature Flags -

    - -

    Harness is a feature management platform that helps -teams to build better software and to test features quicker.

    This repository contains our Feature Flags SDK for Erlang and other BEAM -languages such as Elixir.

    - -

    table-of-contents

    -
    - Table of Contents -

    -

    Intro<br> -Requirements<br> -Quickstart<br> -Further Reading<br> -Build Instructions<br>

    - -

    intro

    -
    - Intro -

    -

    This sample doesn’t include configuration -options. For in depth steps and configuring the SDK, e.g. disabling -streaming or using our Relay Proxy, see the -Erlang SDK Reference

    For a sample FF Erlang SDK project, see our -test Erlang project.

    For a sample FF Erlang SDK Project for Elixir, see our -test Elixir Project.

    FeatureFlags

    - -

    requirements

    -
    - Requirements -

    -

    Erlang OTP 22 or newer.

    - -

    quickstart

    -
    - Quickstart -

    -

    To follow along with our test code sample, make sure you have:

    - -

    install-the-sdk

    -
    - Install the SDK -

    -

    - -

    for-erlang-applications

    -
    - For Erlang applications -

    -

    To install the SDK for Erlang based applications:

    1. Add the SDK as a dependency to your rebar.config file:
      {deps, [{cfclient, "1.2.0", {pkg, harness_ff_erlang_server_sdk}}]}.
    1. Add the dependency to your project's app.src.
      {applications,
    -    [kernel, stdlib, cfclient]
    -  },

    - -

    for-elixir-applications

    -
    - For Elixir applications -

    -

    To install the SDK for Elixir based applications:

    • Add the SDK as a dependency to mix.exs deps():

        defp deps do
      -    [
      -        {:cfclient, "~> 1.1.0", hex: :harness_ff_erlang_server_sdk}
      -    ]

    - -

    configuration

    -
    - Configuration -

    -

    - -

    erlang

    -
    - Erlang -

    -

    Provide your API key in sys.config using an environment variable:

    [
    -  {cfclient, [
    -    {api_key, {environment_variable, "YOUR_API_KEY_ENV_VARIABLE"},
    -  ]}
    -].

    Or you may provide the API key directly if required:

    [
    -  {cfclient, [
    -      {api_key, "YOUR_API_KEY"},
    -  ]}
    -].

    - -

    elixir

    -
    - Elixir -

    -

    Provide your API key in config/prod.exs using an environment variable: :

    config :cfclient,
    -  api_key: System.get_env("YOUR_API_KEY_ENVIRONMENT_VARIABLE")

    Or you may provide the API key directly if required:

    config :cfclient,
    -  api_key: "YOUR_API_KEY"

    - -

    set-logging-level

    -
    - Set logging level -

    -

    Optionally you may set the required log level of the SDK. If not provided, the SDK will default to warning.

    Elixir logging configuration example

    config :cfclient,
    -  # Set the log level of the SDK to debug
    -    log_level: :debug
    -    [api_key: System.get_env("FF_API_KEY_0"),
    -    # For additional config you can pass in, see Erlang SDK docs: https://github.com/harness/ff-erlang-server-sdk/blob/main/docs/further_reading.md#further-reading
    -    # we are just using the main config url here as an example.
    -    config: [
    -      config_url: "https://config.ff.harness.io/api/1.0",
    -      events_url: "https://events.ff.harness.io/api/1.0",
    -      poll_interval: 60000,
    -      analytics_enabled: true
    -    ]]

    Erlang logging configuration example

    [{cfclient, [
    -    %% Set the log level of the SDK to debug
    -    {log_level, debug},
    -    {api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"},
    -    {config, [
    -        {config_url, "https://config.ff.harness.io/api/1.0"},
    -        {events_url, "https://config.ff.harness.io/api/1.0"},
    -        {poll_interval, 60},
    -        {analytics_enabled, true},
    -    ]},
    -    ]}]

    - -

    enable-verbose-evaluation-logs

    -
    - Enable Verbose Evaluation Logs -

    -

    Evaluation logs are debug level by default. If required, they can be changed to info level. This is useful if production environments do not use debug level, but there is a requirement to check low level evaluation logs. -Note that this will only affect evaluation log statements.

    Elixir

    config :cfclient,
    -    log_level: :error
    -    [api_key: System.get_env("FF_API_KEY_0"),
    -    config: [
    -      verbose_evaluation_logs: true
    -    ]]

    Erlang

    [{cfclient, [
    -    {log_level, error},
    -    {api_key, {envrionment_variable, "YOUR_API_KEY_ENV_VARIABLE"},
    -    {config, [
    -        {verbose_evaluation_logs, true},
    -    ]},
    -    ]}]

    - -

    run-multiple-instances-of-the-sdk

    -
    - Run multiple instances of the SDK -

    -

    The SDK by default starts up a single instance called default which is configured with your project API key. -If different parts of your application need to use specific projects, you can start up additional client instances using by defining additional configuration for each unique project.

    - -

    erlang-project-config

    -
    - Erlang Project Config -

    -

    The additional project config is defined in sys.config

    The following sys.config snippet starts up two additional instances as well along with the default instance:

    [
    -  %% Project config name: This is an arbitrary identifier, but it must be unique per project config you define.
    -  {harness_project_1_config, [
    -    {cfclient, [
    -      {config, [
    -        %% Instance name: This must be unique across all of the project configs. E.g. it cannot be the same as an instance name
    -        %% in another project config.
    -        %% It will be the name you use when calling SDK API functions like `bool_variation/4`, 
    -        {name, instance_name_1}
    -      ]},
    -      %% The API key for the Harness project you want to use with this SDK instance.
    -      {api_key, {environment_variable, "PROJECT_1_API_KEY"}}]
    -    }
    -  ]},
    -  
    -  {harness_project_2_config, [
    -    {cfclient, [
    -      {config, [
    -        {name, instance_name_2}
    -      ]},
    -      {api_key, {environment_variable, "PROJECT_2_API_KEY"}}]
    -    }
    -  ]},
    -
    -  {cfclient, [
    -    {api_key, {environment_variable, "FF_API_KEY"}},
    -    {config, [
    -      {config_url, "https://config.ff.harness.io/api/1.0"},
    -      {events_url, "https://config.ff.harness.io/api/1.0"}
    -    ]},
    -    {analytics_push_interval, 60000}
    -  ]
    -}].

    Note: if the default instance fails to start, for example due to an authentication error with the API key, then the SDK -will fail to boot and the additional instances won't start.

    If you don't require the default instance to be started up, you can do:

      % ... additional project config
    -
    -  {cfclient, [
    -    {start_default_instance, false},
    -    %% The remaining tuples will be ignored, so you can choose to include or omit them.
    -    {api_key, {environment_variable, "FF_API_KEY"}},
    -    {config, [
    -      {config_url, "https://config.ff.harness.io/api/1.0"},
    -      {events_url, "https://config.ff.harness.io/api/1.0"}
    -    ]},
    -    {analytics_push_interval, 60000}
    -  ]
    -},

    In your application supervisor, e.g. src/myapp_sup.erl, start up a cfclient_instance -for each additional project. As the default instance is booted when your application starts, you cannot (and don't need to) start it here.

    init(Args) ->
    -  HarnessProject1Args = application:get_env(harness_project_1_config, cfclient, []),
    -  HarnessProject2Args = application:get_env(harness_project_2_config, cfclient, []),
    -  
    -  ChildSpec1 = #{id => project1_cfclient_instance, start => {cfclient_instance, start_link, [HarnessProject1Args]}},
    -  ChildSpec2 = #{id => project2_cfclient_instance, start => {cfclient_instance, start_link, [HarnessProject2Args]}},
    -
    -  MaxRestarts = 1000,
    -  MaxSecondsBetweenRestarts = 3600,
    -  SupFlags = #{strategy => one_for_one,
    -    intensity => MaxRestarts,
    -    period => MaxSecondsBetweenRestarts},
    -
    -  {ok, {SupFlags, [ChildSpec1, ChildSpec2]}}.

    - -

    using-a-specific-instance-of-the-sdk

    -
    - Using a specific instance of the SDK -

    -

    To use a specific SDK instance, you provide the instance name to the public function you are calling. For example bool_variation/4.

    The following is an example of referencing the instances we have created above:

    -module(multi_instance_example).
    -
    --export([multi_instance_evaluations/0]).
    -
    -multi_instance_evaluations() ->
    -  Target = #{
    -    identifier => "Harness_Target_1",
    -    name => "HT_1",
    -    attributes => #{email => <<"demo@harness.io">>}
    -  },
    -
    -  %% Instance 1
    -  Project1Flag = <<"harnessappdemodarkmodeproject1">>,
    -  Project1Result = cfclient:bool_variation(instance_name_1, Project1Flag, Target, false),
    -  logger:info("Instance Name 1 : Variation for Flag ~p with Target ~p is: ~p~n",
    -    [Project1Flag, maps:get(identifier, Target), Project1Result]),
    -
    -  %% Instance 2
    -  Project2Flag = <<"harnessappdemodarkmodeproject2">>,
    -  Project2Result = cfclient:bool_variation(instance_name_2, Project2Flag, Target, false),
    -  logger:info("Instance name 2 Variation for Flag ~p with Target ~p is: ~p~n",
    -  [Project2Flag, maps:get(identifier, Target), Project2Result]).
    -
    -  %% Default instance
    -  DefaultProjectFlag = <<"harnessappdemodarkmodeprojectdefault">>,
    -  DefaultProjectResult = cfclient:bool_variation(Project2Flag, Target, false),
    -  logger:info("Default instance Variation for Flag ~p with Target ~p is: ~p~n",
    -  [DefaultProjectFlag, maps:get(identifier, Target), DefaultProjectResult]).

    - -

    elixir-1

    -
    - Elixir -

    -
    1. Create project configurations for each new instance you would like to start in your config/config.exs file:

       # Config for "project 1"
      - config :elixirsample,  project1:
      -        [
      -         api_key: System.get_env("FF_API_KEY_1"),
      -         config: [name: :project1]
      -        ]
      - 
      - # Config for "project 2"
      - config :elixirsample,  project2:
      -   [
      -   api_key: System.get_env("FF_API_KEY_2"),
      -   config: [name: :project2]
      - ]
    2. In your application supervisor, e.g. lib/myapp/supervisor.ex, start up cfclient_instance -for each of the additional project configurations you provided above. As the default instance is booted when your application starts, you cannot (and don't need to) start it here:

         def init(_opts) do
      -     project_1_config = Application.get_env(:elixirsample, :project1, [])
      -     project_2_config = Application.get_env(:elixirsample, :project2, [])
      -     children = [
      -       %{
      -         id: :project1_cfclient_instance,
      -         start: {:cfclient_instance, :start_link, [project_1_config]},
      -         type: :supervisor
      -       },
      -       %{
      -         id: :project2_cfclient_instance,
      -         start: {:cfclient_instance, :start_link, [project_2_config]},
      -         type: :supervisor
      -       },
      -     ]
      -     Supervisor.init(children, strategy: :one_for_one)
      -   end
    3. To use a specific SDK instance, you provide the instance name to the public function you are calling. For example use bool_variation/4 instead of bool_variation/3 - see the following code sample:

       defmodule ElixirSample.EvaluationSample do
      -   require Logger
      - 
      -   def getFlagLoop() do
      -     target = %{
      -       identifier: "harness",
      -       name: "Harness",
      -       anonymous: false,
      -       attributes: %{}
      -     }
      - 
      -     # Default instance
      -     flag = "projectflag"
      -     result = :cfclient.bool_variation(flag, target, false)
      - 
      -     Logger.info(
      -       "SVariation for Flag #{flag} with Target #{inspect(target)} is: #{result}"
      -     )
      - 
      -     # Instance 1
      -     project_1_flag = "project1flag"
      -     project_1_result = :cfclient.number_variation(:project1, project_1_flag, target, 3)
      - 
      -     Logger.info(
      -       "SDK instance 1: Variation for Flag #{project_1_flag} with Target #{inspect(target)} is: #{project_1_result}"
      -     )
      - 
      -     # Instance 2
      -     project_2_flag = "project2flag"
      -     project_2_result = :cfclient.bool_variation(:project2, project_2_flag, target, false)
      - 
      -     Logger.info(
      -       "SDK instance 2: Variation for Flag #{project_2_flag} with Target #{inspect(target)} is: #{project_2_result}"
      -     )
      - 
      -     Process.sleep(10000)
      -     getFlagLoop()
      -
      -     # Default instance
      -     default_project_flag = "defaultflag"
      -     default_project_result = :cfclient.bool_variation(default_project_flag, target, false)
      - 
      -     Logger.info(
      -       "Default instance: Variation for Flag #{default_project_flag} with Target #{inspect(target)} is: #{default_project_result}"
      -     )
      - 
      -     Process.sleep(10000)
      -     getFlagLoop()
      -   end
      - end

    - -

    code-sample

    -
    - Code Sample -

    -

    - -

    erlang-1

    -
    - Erlang -

    -

    Call the API to get the value of the harnessappdemodarkmode flag you created -via https://www.harness.io/.

    get_flag_loop() ->
    -  Target = #{identifier => "Harness_Target_1",
    -    name => "HT_1",
    -    %% Attribute keys must be atoms. 
    -    %% Values must be either bitstrings, atoms, or a list of bitstrings/atoms - see Targets with custom attributes section below.
    -    attributes => #{email => <<"demo@harness.io">>}
    -  },
    -  FlagIdentifier = "harnessappdemodarkmode",
    -  Result = cfclient:bool_variation(FlagIdentifier, Target, false),
    -  logger:info("Variation for Flag ~p witih Target ~p is: ~p~n", [FlagIdentifier, maps:get(identifier, Target), Result]),
    -  timer:sleep(10000),
    -  get_flag_loop().

    - -

    elixir-2

    -
    - Elixir -

    -

    Call the API to get the value of the harnessappdemodarkmode flag you created -via https://www.harness.io/.

    def getFlagLoop() do
    -  target = %{
    -    identifier: "Harness_Target_1",
    -    name: "HT_1"
    -  
    -    # Attribute keys must be atoms. 
    -    # Values must be either binaries, atoms, or a list of binaries/atoms.
    -    # See "targets with custom attributes" below.
    -    attributes: %{email: "demo@harness.io"}
    -  }
    -  
    -  flag_identifier = "harnessappdemodarkmode"
    -  
    -  result = :cfclient.bool_variation(flag_identifier, target, false)
    -  Logger.info("Variation for Flag #{flag_identifier} with Target #{inspect(target)} is: #{result)")
    -  Process.sleep(10000)
    -  getFlagLoop()
    -

    - -

    targets-with-custom-attributes

    -
    - Targets with custom attributes -

    -

    You can use the attributes map to provide custom attributes. If the target -isn't anonymous, the attributes will shortly appear in the Harness UI after an -evaluation using the target.

    You can create Group Rules -based on these attributes.

    Note: attribute keys must be atoms and the values must either be binaries -or atoms or a list of binaries or atoms.

    - -

    erlang-2

    -
    - Erlang: -

    -
      TargetBetaGroup = #{'identifier' => <<"my_target">>,
    -    name => <<"my_target_name">>,
    -    anonymous => <<"">>,
    -    attributes => #{beta => <<"beta_group_1">>}
    -    },
    -  TargetBetaGroups = #{'identifier' => <<"my_other_target">>,
    -    name => <<"my_other_target_name">>,
    -    anonymous => <<"">>,
    -    attributes => #{beta => [<<"beta_group_1">>, 'beta_group_2']}}
    -    },
    -  TargetAlphaGroup = #{'identifier' => <<"my_alpha_target">>,
    -    name => <<"my_alpha_target_name">>,
    -    anonymous => <<"">>,
    -    attributes => #{alpha => 'alpha_group_1'}
    -    },

    - -

    elixir-3

    -
    - Elixir -

    -
    target_beta_group = %{
    -  identifier: "my_target",
    -  name: "my_target_name",
    -  anonymous: "",
    -  attributes: %{beta: "beta_group_1"}
    -}
    -
    -target_beta_groups = %{
    -  identifier: "my_other_target",
    -  name: "my_other_target_name",
    -  anonymous: "",
    -  attributes: %{
    -    beta: ["beta_group_1", :beta_group_2]
    -  }
    -}
    -
    -target_alpha_group = %{
    -  identifier: "my_alpha_target",
    -  name: "my_alpha_target_name",
    -  anonymous: "",
    -  attributes: %{alpha: :alpha_group_1}
    -}

    - -

    additional-reading

    -
    - Additional Reading -

    -

    For further examples and config options, see the Erlang SDK Further -Reading.

    For more information about Feature Flags, see our Feature Flags -documentation.

    - -

    contributing

    -
    - Contributing -

    -

    In order to run the tests, pull the submodules:

    git submodule update --init
    - - -
    -
    -
    -
    - - - - diff --git a/doc/search.html b/doc/search.html deleted file mode 100644 index eab2059..0000000 --- a/doc/search.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - Search — cfclient v1.2.1 - - - - - - - - - - - - - - - - -
    - - - - - -
    - -
    -
    - - - - -
    -
    -
    -
    - - - - diff --git a/rebar.config b/rebar.config index 8bfe3ea..8589f6c 100644 --- a/rebar.config +++ b/rebar.config @@ -3,15 +3,15 @@ { deps, [ - {erlang_murmurhash, "1.0.0", {pkg, harness_erlang_murmurhash}}, - {cfapi, "1.0.0", {pkg, harness_ff_erlang_client_api}}, + {cfapi, "1.0.1", {pkg, harness_ff_erlang_client_api}}, + {murmur, "1.0.3"}, {ctx, "0.6.0"}, {mochiweb, "3.1.1"}, {base64url, "1.0.1"} ] }. -{project_plugins, [steamroller, rebar3_hex, rebar3_ex_doc]}. +{project_plugins, [steamroller, rebar3_hex, rebar3_ex_doc, rebar_mix]}. { profiles, @@ -33,11 +33,14 @@ {dialyzer, [all_deps]}. -{ex_doc, [ - {extras, ["README.md", "LICENSE"]}, - {main, "README.md"}, - {source_url, "https://github.com/harness/ff-erlang-server-sdk"} -]}. +{ + ex_doc, + [ + {extras, ["README.md", "LICENSE"]}, + {main, "README.md"}, + {source_url, "https://github.com/harness/ff-erlang-server-sdk"} + ] +}. {hex, [{doc, ex_doc}]}. diff --git a/rebar.lock b/rebar.lock index ff0bf5c..fe0a372 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,21 +1,15 @@ {"1.2.0", [{<<"base64url">>,{pkg,<<"base64url">>,<<"1.0.1">>},0}, {<<"certifi">>,{pkg,<<"certifi">>,<<"2.9.0">>},2}, - {<<"cfapi">>,{pkg,<<"harness_ff_erlang_client_api">>,<<"1.0.0">>},0}, + {<<"cfapi">>,{pkg,<<"harness_ff_erlang_client_api">>,<<"1.0.1">>},0}, {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},0}, - {<<"erlang_murmurhash">>,{pkg,<<"harness_erlang_murmurhash">>,<<"1.0.0">>},0}, {<<"hackney">>,{pkg,<<"hackney">>,<<"1.18.1">>},1}, - {<<"harness_erlang_murmurhash">>, - {pkg,<<"harness_erlang_murmurhash">>,<<"1.0.0">>}, - 0}, - {<<"harness_ff_erlang_client_api">>, - {pkg,<<"harness_ff_erlang_client_api">>,<<"1.0.0">>}, - 0}, {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2}, {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2}, {<<"mochiweb">>,{pkg,<<"mochiweb">>,<<"3.1.1">>},0}, + {<<"murmur">>,{pkg,<<"murmur">>,<<"1.0.3">>},0}, {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2}, {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},2}, {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2}]}. @@ -23,34 +17,30 @@ {pkg_hash,[ {<<"base64url">>, <<"F8C7F2DA04CA9A5D0F5F50258F055E1D699F0E8BF4CFDB30B750865368403CF6">>}, {<<"certifi">>, <<"6F2A475689DD47F19FB74334859D460A2DC4E3252A3324BD2111B8F0429E7E21">>}, - {<<"cfapi">>, <<"3FECA60093209AE774E2CC01F89D007668EF98F50FCC797F45C941B966956465">>}, + {<<"cfapi">>, <<"FAB3C9796A3B5AC35E3BF4C5925613A665C39810390AF2BD1F5331FA0AFFBC5E">>}, {<<"ctx">>, <<"8FF88B70E6400C4DF90142E7F130625B82086077A45364A78D208ED3ED53C7FE">>}, - {<<"erlang_murmurhash">>, <<"6044EF140C1F001CCD17BE4CFE707582526EB33C0FFE3DCAEFF4C4BDC79B0636">>}, {<<"hackney">>, <<"F48BF88F521F2A229FC7BAE88CF4F85ADC9CD9BCF23B5DC8EB6A1788C662C4F6">>}, - {<<"harness_erlang_murmurhash">>, <<"6044EF140C1F001CCD17BE4CFE707582526EB33C0FFE3DCAEFF4C4BDC79B0636">>}, - {<<"harness_ff_erlang_client_api">>, <<"3FECA60093209AE774E2CC01F89D007668EF98F50FCC797F45C941B966956465">>}, {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, {<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>}, {<<"mochiweb">>, <<"C628CC4518A3CA0F2FB6B8973DCBDC9135635F834B64AEE846337583AFD42084">>}, + {<<"murmur">>, <<"ADE5E9CDB86300645ABF72ABBE064A87BC943678DF2567986D720A70C8F48214">>}, {<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>}, {<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>}, {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, {pkg_hash_ext,[ {<<"base64url">>, <<"F9B3ADD4731A02A9B0410398B475B33E7566A695365237A6BDEE1BB447719F5C">>}, {<<"certifi">>, <<"266DA46BDB06D6C6D35FDE799BCB28D36D985D424AD7C08B5BB48F5B5CDD4641">>}, - {<<"cfapi">>, <<"F6DE5DAA6DC8950787F9530D31042FAA76A9AEA7E4675246E4664CA8F9E93B4E">>}, + {<<"cfapi">>, <<"34177648F4BC938D7B836135CAA07B877AC422C3F8A255FF86B01F1575B1DEBB">>}, {<<"ctx">>, <<"A14ED2D1B67723DBEBBE423B28D7615EB0BDCBA6FF28F2D1F1B0A7E1D4AA5FC2">>}, - {<<"erlang_murmurhash">>, <<"BF831D4FCC76B830B2EF27C8277477A85ACAD076F45223D7277A714ACA5421D6">>}, {<<"hackney">>, <<"A4ECDAFF44297E9B5894AE499E9A070EA1888C84AFDD1FD9B7B2BC384950128E">>}, - {<<"harness_erlang_murmurhash">>, <<"BF831D4FCC76B830B2EF27C8277477A85ACAD076F45223D7277A714ACA5421D6">>}, - {<<"harness_ff_erlang_client_api">>, <<"F6DE5DAA6DC8950787F9530D31042FAA76A9AEA7E4675246E4664CA8F9E93B4E">>}, {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, {<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>}, {<<"mochiweb">>, <<"8B5ECFECC26D5D083F9BEC5CE969D717676B6A4FACBDF5915E0498CDFE035A26">>}, + {<<"murmur">>, <<"02B4DDA9327AC6650CC9A8D007EC5244B609397B9110C5AD8549224EEF9AFC60">>}, {<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>}, {<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>}, {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} diff --git a/src/cfclient.app.src b/src/cfclient.app.src index 67aae6d..fde8ed7 100644 --- a/src/cfclient.app.src +++ b/src/cfclient.app.src @@ -4,7 +4,7 @@ [ {description, "Harness Feature Flags Server SDK"}, {pkg_name, "harness_ff_erlang_server_sdk"}, - {vsn, "1.2.1"}, + {vsn, "2.0.0"}, {registered, []}, {mod, {cfclient_app, []}}, {applications, [kernel, stdlib, cfapi]}, diff --git a/src/cfclient.erl b/src/cfclient.erl index 293ddbb..85119c2 100644 --- a/src/cfclient.erl +++ b/src/cfclient.erl @@ -8,17 +8,20 @@ -include("cfclient_config.hrl"). --export([ - bool_variation/3, - bool_variation/4, - string_variation/3, - string_variation/4, - number_variation/3, - number_variation/4, - json_variation/3, - json_variation/4, - close/0, - close/1]). +-export( + [ + bool_variation/3, + bool_variation/4, + string_variation/3, + string_variation/4, + number_variation/3, + number_variation/4, + json_variation/3, + json_variation/4, + close/0, + close/1 + ] +). -type target() :: #{ identifier := binary(), @@ -181,11 +184,14 @@ json_variation(Config, FlagKey, Target0, Default) when is_binary(FlagKey) -> Default end. + close() -> close(default). + close(Name) when is_atom(Name) -> Config = cfclient_config:get_config(Name), cfclient_instance:stop(Config). + % Convert target identifier to binary, as users can provide it as a string, % binary, or atom, but client API works in binary. normalize_target(#{identifier := Id} = Target) when is_binary(Id) -> Target; diff --git a/src/cfclient_app.erl b/src/cfclient_app.erl index 5adc89e..46fdfda 100644 --- a/src/cfclient_app.erl +++ b/src/cfclient_app.erl @@ -18,12 +18,13 @@ start(_StartType, _StartArgs) -> true -> logger:set_module_level(cfclient_evaluator, info), [{verbose_evaluation_logs, true}] ++ Config; - false -> - Config - end, + false -> Config + end, StartDefaultInstance = application:get_env(cfclient, start_default_instance, true), - cfclient_sup:start_link([{api_key, ApiKey}, {config, Config2}, {start_default_instance, StartDefaultInstance}]). + cfclient_sup:start_link( + [{api_key, ApiKey}, {config, Config2}, {start_default_instance, StartDefaultInstance}] + ). -stop(_State) -> ok. \ No newline at end of file +stop(_State) -> ok. diff --git a/src/cfclient_config.erl b/src/cfclient_config.erl index 8623324..a955191 100644 --- a/src/cfclient_config.erl +++ b/src/cfclient_config.erl @@ -8,20 +8,25 @@ -include("cfclient_config.hrl"). --export([ - authenticate/2, - create_tables/1, - defaults/0, - get_config/0, - get_config/1, - get_value/1, - get_value/2, - init/1, - normalize/1, - parse_jwt/1, - set_config/1, - set_config/2 - , delete_tables/1, get_table_names/1]). +-export( + [ + authenticate/2, + create_tables/1, + defaults/0, + get_config/0, + get_config/1, + get_value/1, + get_value/2, + init/1, + normalize/1, + parse_jwt/1, + set_config/1, + set_config/2, + delete_tables/1, + get_table_names/1, + is_retry_code/1 + ] +). -type config() :: map(). @@ -92,7 +97,7 @@ defaults() -> metrics_counter_table => ?METRICS_COUNTER_TABLE, % Enable to info log evaluation related logs - useful if customer production systems don't use debug logs verbose_evaluation_logs => ?DEFAULT_VERBOSE_EVALUATION_LOGS - }. + }. -spec normalize(proplists:proplist()) -> map(). @@ -159,8 +164,8 @@ authenticate({environment_variable, APIKeyEnvVar}, Config) -> false -> ?LOG_ERROR("Environment variable for API Key not found"), {error, not_configured}; - APIKey -> - authenticate(APIKey, Config) + + APIKey -> authenticate(APIKey, Config) end; authenticate(ApiKey, Config) when is_list(ApiKey) -> authenticate(list_to_binary(ApiKey), Config); @@ -168,6 +173,12 @@ authenticate(ApiKey, Config) when is_list(ApiKey) -> authenticate(list_to_binary authenticate(ApiKey, Config) -> #{config_url := ConfigUrl} = Config, Opts = #{cfg => #{host => ConfigUrl}, params => #{apiKey => ApiKey}}, + RetryLimit = 5, + RetryDelay = 1000, + authenticate_with_retry(Opts, Config, ApiKey, RetryLimit, RetryDelay). + + +authenticate_with_retry(Opts, Config, ApiKey, RetryLimit, RetryDelay) -> case cfapi_client_api:authenticate(ctx:new(), Opts) of {ok, #{authToken := AuthToken}, _} -> {ok, Project} = cfclient_config:parse_jwt(AuthToken), @@ -175,9 +186,37 @@ authenticate(ApiKey, Config) -> maps:merge(Config, #{api_key => ApiKey, auth_token => AuthToken, project => Project}), {ok, MergedConfig}; - {error, Response, _} -> {error, Response} + % Non-200 status codes + {error, Reason, Response} -> + case cfclient_config:is_retry_code(Response) of + true when RetryLimit > 0 -> + timer:sleep(RetryDelay), + NewRetryLimit = RetryLimit - 1, + NewRetryDelay = RetryDelay * 2, + ?LOG_WARNING( + "Error when authenticating cfclient: ~p retrying with ~p: attempts left", + [Reason, NewRetryLimit] + ), + authenticate_with_retry(Opts, Config, ApiKey, NewRetryLimit, NewRetryDelay); + + _ -> {error, Reason} + end; + + % Other request related errors from the hackney client + {error, Reason} when RetryLimit > 0 -> + timer:sleep(RetryDelay), + NewRetryLimit = RetryLimit - 1, + NewRetryDelay = RetryDelay * 2, + ?LOG_WARNING( + "Error when authenticating cfclient: ~p retrying with ~p: attempts left", + [Reason, NewRetryLimit] + ), + authenticate_with_retry(Opts, Config, ApiKey, NewRetryLimit, NewRetryDelay); + + {error, Reason} when RetryLimit == 0 -> {error, Reason} end. + % TODO: validate the JWT -spec parse_jwt(binary()) -> {ok, map()} | {error, Reason :: term()}. parse_jwt(JwtToken) -> @@ -204,8 +243,8 @@ create_tables(Config) -> case ets:whereis(ConfigTable) of undefined -> ConfigTable = ets:new(ConfigTable, [named_table, set, public, {read_concurrency, true}]); - _TID -> - noop + + _TID -> noop end, CacheTable = ets:new(CacheTable, [named_table, set, public, {read_concurrency, true}]), MetricsTargetTable = ets:new(MetricsTargetTable, [named_table, set, public]), @@ -213,13 +252,15 @@ create_tables(Config) -> MetricsCounterTable = ets:new(MetricsCounterTable, [named_table, set, public]), ok. + -spec delete_tables(list()) -> ok. delete_tables([H | T]) -> logger:debug("Deleting table ~s ", [H]), ets:delete(H), delete_tables(T); -delete_tables([]) -> - ok. + +delete_tables([]) -> ok. + get_table_names(Config) -> #{ @@ -268,3 +309,7 @@ get_value(Key, Opts) -> Config = get_config(), maps:get(Key, Config) end. + + +% Helper function for retryable http codes +is_retry_code(#{status := Status}) -> lists:member(Status, [408, 425, 429, 500, 502, 503, 504]). diff --git a/src/cfclient_evaluator.erl b/src/cfclient_evaluator.erl index 1c04212..5e62605 100644 --- a/src/cfclient_evaluator.erl +++ b/src/cfclient_evaluator.erl @@ -28,7 +28,6 @@ excluded => [map()] | null, included => [map()] | null }. - -type target() :: cfclient:target(). -type variation_map() :: #{ variation := binary(), @@ -82,13 +81,13 @@ -include("cfclient_evaluator_operators.hrl"). --define(LOG_EVALUATION_STATE(IsVerboseEvaluationEnabled, Message, Args), +-define( + LOG_EVALUATION_STATE(IsVerboseEvaluationEnabled, Message, Args), case IsVerboseEvaluationEnabled of - false -> - ?LOG_DEBUG(Message, Args); - true -> - ?LOG_INFO(Message, Args) - end). + false -> ?LOG_DEBUG(Message, Args); + true -> ?LOG_INFO(Message, Args) + end +). %% Public API @@ -159,17 +158,25 @@ evaluate(FlagId, Target, Config, Kind) -> {ok, Id :: binary(), Value :: term()} | {error, atom()}. evaluate_flag(off, #{state := <<"off">>} = Flag, _Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Flag state off for flag ~p, returning default 'off' variation", [Flag]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Flag state off for flag ~p, returning default 'off' variation", + [Flag] + ), return_default_off_variation(Flag, Config); evaluate_flag(off, #{state := <<"on">>} = Flag, Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging,"Flag state on for flag ~p", [Flag]), + ?LOG_EVALUATION_STATE(IsVerboseLogging, "Flag state on for flag ~p", [Flag]), evaluate_flag(prerequisites, Flag, Target, Config); evaluate_flag(prerequisites, #{prerequisites := []} = Flag, Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Prerequisites not set for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Prerequisites not set for flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(target_rules, Flag, Target, Config); evaluate_flag(prerequisites, #{prerequisites := Prereqs} = Flag, Target, Config) @@ -177,40 +184,68 @@ when is_list(Prereqs) -> case search_prerequisites(Prereqs, Target, Config) of true -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Prerequisites met for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Prerequisites met for flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(target_rules, Flag, Target, Config); _ -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Prerequisites not met for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Prerequisites not met for flag ~p, target ~p", + [Flag, Target] + ), return_default_off_variation(Flag, Config) end; evaluate_flag(prerequisites, Flag, Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Prerequisites not set for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Prerequisites not set for flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(target_rules, Flag, Target, Config); evaluate_flag(target_rules, #{variationToTargetMap := []} = Flag, Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Target rules not set for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Target rules not set for flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(group_rules, Flag, Target, Config); evaluate_flag(target_rules, #{variationToTargetMap := null} = Flag, Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Target rules not set for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Target rules not set for flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(group_rules, Flag, Target, Config); evaluate_flag(target_rules, #{variationToTargetMap := TM} = Flag, Target, Config) when TM /= null -> case evaluate_target_rule(TM, Target) of false -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Target rules map did not match flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Target rules map did not match flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(group_rules, Flag, Target, Config); TargetVariationId -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Target rules map matched flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Target rules map matched flag ~p, target ~p", + [Flag, Target] + ), % Return both variation identifier and value, as prerequisites % compare on identifier return_target_or_group_variation(Flag, TargetVariationId) @@ -218,29 +253,49 @@ evaluate_flag(target_rules, #{variationToTargetMap := TM} = Flag, Target, Config evaluate_flag(target_rules, Flag, Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Target rules not set for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Target rules not set for flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(group_rules, Flag, Target, Config); evaluate_flag(group_rules, #{rules := []} = Flag, Target, Config) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Group rules not set for flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Group rules not set for flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(default_on, Flag, Target, Config); evaluate_flag(group_rules, #{rules := Rules} = Flag, Target, Config) when Rules /= null -> case search_rules_for_inclusion(sort_by_priority(Rules), Target, Config) of false -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Group rules did not match flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Group rules did not match flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(default_on, Flag, Target, Config); excluded -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Group rules excluded flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Group rules excluded flag ~p, target ~p", + [Flag, Target] + ), evaluate_flag(default_on, Flag, Target, Config); GroupVariationId when is_binary(GroupVariationId) -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Group rules matched flag ~p, target ~p", [Flag, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Group rules matched flag ~p, target ~p", + [Flag, Target] + ), return_target_or_group_variation(Flag, GroupVariationId) end; @@ -255,12 +310,17 @@ evaluate_flag(default_on, Flag, Target, Config) -> {value, #{value := Value}} -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Default on variation returned for flag ~p, target ~p, id ~s: ~p", [Flag, Target, Id, Value]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Default on variation returned for flag ~p, target ~p, id ~s: ~p", + [Flag, Target, Id, Value] + ), {ok, Id, Value} end. --spec return_default_off_variation(flag(), config()) -> {ok, Id :: binary(), term()} | {error, not_found}. +-spec return_default_off_variation(flag(), config()) -> + {ok, Id :: binary(), term()} | {error, not_found}. return_default_off_variation(Flag, Config) -> #{variations := Variations, offVariation := Id} = Flag, case search_by_id(Variations, Id) of @@ -270,7 +330,11 @@ return_default_off_variation(Flag, Config) -> {value, #{value := Value}} -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Default off variation returned for flag ~p, id ~s: ~p", [Flag, Id, Value]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Default off variation returned for flag ~p, id ~s: ~p", + [Flag, Id, Value] + ), {ok, Id, Value} end. @@ -493,7 +557,9 @@ apply_percentage_rollout([], _, _, _) -> excluded. -spec should_rollout(binary(), binary(), integer()) -> boolean(). should_rollout(BucketBy, TargetValue, Percentage) -> - Hash = erlang_murmurhash:murmurhash3_32(<>), + Concatenated = <>, + % Using a pure Elixir library for murmur3 + Hash = 'Elixir.Murmur':hash_x86_32(Concatenated), BucketID = (Hash rem 100) + 1, (Percentage > 0) andalso (BucketID =< Percentage). @@ -526,9 +592,12 @@ check_prerequisite(PrerequisiteFlag, PrerequisiteFlagId, Prerequisite, Target, C case evaluate_flag(off, PrerequisiteFlag, Target, Config) of {ok, VariationId, _} -> #{verbose_evaluation_logs := IsVerboseLogging} = Config, - ?LOG_EVALUATION_STATE(IsVerboseLogging, "Prerequisite flag ~p has variation ~p, target ~p", [PrerequisiteFlagId, VariationId, Target]), + ?LOG_EVALUATION_STATE( + IsVerboseLogging, + "Prerequisite flag ~p has variation ~p, target ~p", + [PrerequisiteFlagId, VariationId, Target] + ), PrerequisiteVariations = maps:get(variations, Prerequisite), - ?LOG_EVALUATION_STATE(IsVerboseLogging,"Prerequisite flag ~p should have variations ~p", [PrerequisiteFlagId, PrerequisiteVariations]), lists:member(VariationId, PrerequisiteVariations); {error, Reason} -> diff --git a/src/cfclient_instance.erl b/src/cfclient_instance.erl index 35140dc..1c3ac6a 100644 --- a/src/cfclient_instance.erl +++ b/src/cfclient_instance.erl @@ -29,7 +29,7 @@ start_link(Args) -> gen_server:start_link(?MODULE, Args, []). init(Args) -> case proplists:get_value(start_default_instance, Args, true) of - true -> + true -> ApiKey = proplists:get_value(api_key, Args), Config0 = proplists:get_value(config, Args, []), Config1 = cfclient_config:normalize(Config0), @@ -40,11 +40,10 @@ init(Args) -> % Used during testing to start up cfclient instances % without a valid API key. case maps:get(unit_test_mode, Config1, undefined) of - undefined -> - {stop, authenticate}; - _UnitTestMode -> - {ok, Config1} + undefined -> {stop, authenticate}; + _UnitTestMode -> {ok, Config1} end; + {error, Reason} -> InstanceName = maps:get(name, Config1), ?LOG_ERROR("Authentication failed for cflient instance '~p': ~p", [InstanceName, Reason]), @@ -59,13 +58,13 @@ init(Args) -> ?LOG_INFO("Started unique instance of cfclient: ~p", [maps:get(name, Config1)]), {ok, Config} end; + false -> ?LOG_INFO("Default cfclient instance not started"), {ok, default_instance_not_started} end. - handle_info(metrics, Config) -> ?LOG_DEBUG("Metrics triggered"), #{analytics_push_interval := AnalyticsPushInterval} = Config, @@ -103,16 +102,24 @@ start_analytics(_) -> ok. retrieve_flags(#{poll_enabled := true} = Config) -> case cfclient_retrieve:retrieve_flags(Config) of {ok, Flags} -> [cfclient_cache:cache_flag(F, Config) || F <- Flags]; - {error, Reason} -> ?LOG_ERROR("Could not retrive flags from API: ~p", [Reason]) + + {error, Reason} -> + ?LOG_ERROR("Could not retrive flags from API for this poll interval, reason: ~p", [Reason]) end, case cfclient_retrieve:retrieve_segments(Config) of {ok, Segments} -> [cfclient_cache:cache_segment(S, Config) || S <- Segments]; - {error, Reason1} -> ?LOG_ERROR("Could not retrive segments from API: ~p", [Reason1]) + + {error, Reason1} -> + ?LOG_ERROR( + "Could not retrive segments from API for this poll interval, reason: ~p", + [Reason1] + ) end, ok; retrieve_flags(_) -> ok. + -spec stop(map()) -> ok | {error, not_found, term()}. stop(Config) -> #{name := Name} = Config, @@ -124,9 +131,11 @@ stop(Config) -> default -> supervisor:terminate_child(cfclient_sup, cfclient_instance), logger:debug("Terminating cfclient_instance default process "); + _InstanceName -> - logger:debug("User has started cfclient instance in their own supervision tree, please ensure you terminiate - the child process") + logger:debug( + "User has started cfclient instance in their own supervision tree, please ensure you terminiate\n the child process" + ) end, logger:debug("Stopped cfclient instance ~s ", [Name]), - ok. \ No newline at end of file + ok. diff --git a/src/cfclient_metrics.erl b/src/cfclient_metrics.erl index 3ccc184..ec81f77 100644 --- a/src/cfclient_metrics.erl +++ b/src/cfclient_metrics.erl @@ -37,7 +37,7 @@ process_metrics(Config) -> ok; {ok, Response} -> - ?LOG_DEBUG("Posted metrics to server: ~p", [Response]), + ?LOG_INFO("Posted metrics to server: ~p", [Response]), % TODO: race condition, will lose any metrics made during call to % post_metrics clear_caches(Config), @@ -61,9 +61,38 @@ post_metrics(MetricsData, MetricsTargetData, Config) -> cfg => #{auth => #{'BearerAuth' => <<"Bearer ", AuthToken/binary>>}, host => EventsUrl}, params => #{metricsData => MetricsData, targetData => MetricsTargetData} }, + RetryLimit = 5, + RetryDelay = 1000, + post_metrics_with_retry(Cluster, Environment, Opts, RetryLimit, RetryDelay). + + +post_metrics_with_retry(Cluster, Environment, Opts, RetryLimit, RetryDelay) -> case cfapi_metrics_api:post_metrics(ctx:new(), #{cluster => Cluster}, Environment, Opts) of {ok, Response, _} -> {ok, Response}; - {error, Response, _} -> {error, Response} + + {error, Reason} when RetryLimit > 0 -> + timer:sleep(RetryDelay), + NewRetryLimit = RetryLimit - 1, + NewRetryDelay = RetryDelay * 2, + ?LOG_WARNING( + "Error posting metrics: ~p retrying with ~p: attempts left", + [Reason, NewRetryLimit] + ), + post_metrics_with_retry(Cluster, Environment, Opts, NewRetryLimit, NewRetryDelay); + + {error, Reason} when RetryLimit == 0 -> {error, Reason}; + + {error, Reason, _} when RetryLimit > 0 -> + timer:sleep(RetryDelay), + NewRetryLimit = RetryLimit - 1, + NewRetryDelay = RetryDelay * 2, + ?LOG_WARNING( + "Error posting metrics: ~p retrying with ~p: attempts left", + [Reason, NewRetryLimit] + ), + post_metrics_with_retry(Cluster, Environment, Opts, NewRetryLimit, NewRetryDelay); + + {error, Reason, _} when RetryLimit == 0 -> {error, Reason} end. @@ -181,15 +210,12 @@ format_target(Target) -> SanitisedIdentifier = target_field_to_binary(maps:get(identifier, Target)), SanitisedName = target_field_to_binary(maps:get(name, Target, SanitisedIdentifier)), SanitisedAttributes = target_attributes_to_metrics(Target), - #{identifier => SanitisedIdentifier, name => SanitisedName, attributes => SanitisedAttributes}. -target_field_to_binary(TargetName) when is_binary(TargetName) -> - TargetName; -target_field_to_binary(TargetName) when is_atom(TargetName) -> - atom_to_binary(TargetName); -target_field_to_binary(TargetName) when is_list(TargetName) -> - list_to_binary(TargetName). + +target_field_to_binary(TargetName) when is_binary(TargetName) -> TargetName; +target_field_to_binary(TargetName) when is_atom(TargetName) -> atom_to_binary(TargetName); +target_field_to_binary(TargetName) when is_list(TargetName) -> list_to_binary(TargetName). -spec target_attributes_to_metrics(cfclient:target()) -> [map()]. target_attributes_to_metrics(#{attributes := Values}) when is_map(Values) -> diff --git a/src/cfclient_retrieve.erl b/src/cfclient_retrieve.erl index fee07c8..a4ec022 100644 --- a/src/cfclient_retrieve.erl +++ b/src/cfclient_retrieve.erl @@ -10,6 +10,8 @@ -type segment() :: cfapi_segment:cfapi_segment(). -type config() :: map(). +-include_lib("kernel/include/logger.hrl"). + % @doc Retrieve all features from Feature Flags API. -spec retrieve_flags(config()) -> {ok, [flag()]} | {error, Reason :: term()}. retrieve_flags(Config) -> @@ -17,13 +19,26 @@ retrieve_flags(Config) -> #{environment := Env, clusterIdentifier := Cluster} = Project, Opts = #{ - cfg => #{auth => #{'BearerAuth' => <<"Bearer ", AuthToken/binary>>}, host => ConfigUrl}, + cfg + => + #{ + auth => #{'BearerAuth' => <<"Bearer ", AuthToken/binary>>}, + host => ConfigUrl, + hackney_opts => [{timeout, 1}] + }, params => #{cluster => Cluster} }, - case cfapi_client_api:get_feature_config(ctx:new(), Env, Opts) of - {ok, Values, _} -> {ok, Values}; - {error, Reason, _} -> {error, Reason} - end. + % Maximum number of retries + RetryLimit = 5, + % Initial delay between retries in milliseconds + RetryDelay = 1000, + retrieve_with_retry( + fun cfapi_client_api:get_feature_config/3, + [ctx:new(), Env, Opts], + RetryLimit, + RetryDelay, + flags + ). % @doc Retrieve all segments from Feature Flags API. @@ -31,12 +46,59 @@ retrieve_flags(Config) -> retrieve_segments(Config) -> #{auth_token := AuthToken, project := Project, config_url := ConfigUrl} = Config, #{environment := Env, clusterIdentifier := Cluster} = Project, + RetryLimit = 5, + RetryDelay = 1000, Opts = #{ - cfg => #{auth => #{'BearerAuth' => <<"Bearer ", AuthToken/binary>>}, host => ConfigUrl}, + cfg + => + #{ + auth => #{'BearerAuth' => <<"Bearer ", AuthToken/binary>>}, + host => ConfigUrl, + hackney_opts => [{timeout, 20000}] + }, params => #{cluster => Cluster} }, - case cfapi_client_api:get_all_segments(ctx:new(), Env, Opts) of + retrieve_with_retry( + fun cfapi_client_api:get_all_segments/3, + [ctx:new(), Env, Opts], + RetryLimit, + RetryDelay, + segments + ). + + +% Recursive function for retrying the request +retrieve_with_retry(Func, Args, RetryLimit, RetryDelay, Endpoint) -> + case apply(Func, Args) of {ok, Values, _} -> {ok, Values}; - {error, Reason, _} -> {error, Reason} + % Retry on certain status codes + {error, Reason, Response} -> + case cfclient_config:is_retry_code(Response) of + true when RetryLimit > 0 -> + timer:sleep(RetryDelay), + NewRetryLimit = RetryLimit - 1, + NewRetryDelay = RetryDelay * 2, + ?LOG_WARNING( + "Error retrieving ~p: ~p, retrying with ~p: attempts left", + [Endpoint, Reason, NewRetryLimit] + ), + retrieve_with_retry(Func, Args, NewRetryLimit, NewRetryDelay, Endpoint); + + _ -> {error, Reason} + end; + + % Retry on request errors + {error, Reason} when RetryLimit > 0 -> + timer:sleep(RetryDelay), + NewRetryLimit = RetryLimit - 1, + % Exponential backoff + NewRetryDelay = RetryDelay * 2, + ?LOG_WARNING( + "Error retrieving ~p: ~p, retrying with: ~p attempts left", + [Endpoint, Reason, NewRetryLimit] + ), + retrieve_with_retry(Func, Args, NewRetryLimit, NewRetryDelay, Endpoint); + + {error, Reason} when RetryLimit == 0 -> {error, Reason} end. diff --git a/src/cfclient_sup.erl b/src/cfclient_sup.erl index 979af82..cd1876e 100644 --- a/src/cfclient_sup.erl +++ b/src/cfclient_sup.erl @@ -18,5 +18,5 @@ start_link(Args) -> supervisor:start_link({local, ?MODULE}, ?MODULE, Args). init(Args) -> ChildSpecs = [#{id => cfclient_instance, start => {cfclient_instance, start_link, [Args]}}], - SupFlags = #{strategy => one_for_one, intensity => 1, period => 5}, + SupFlags = #{strategy => one_for_one, intensity => 4, period => 5}, {ok, {SupFlags, lists:flatten(ChildSpecs)}}. diff --git a/test/cfclient_evaluator_tests.erl b/test/cfclient_evaluator_tests.erl index 7d1368b..97dfb0c 100644 --- a/test/cfclient_evaluator_tests.erl +++ b/test/cfclient_evaluator_tests.erl @@ -161,7 +161,7 @@ variations_bool() -> ) end, ?_assertEqual( - {error,flag_type_mismatch}, + {error, flag_type_mismatch}, cfclient_evaluator:bool_variation(<<"My_string_flag">>, existing_target_a(), config()) ) }, @@ -360,7 +360,7 @@ variations_string() -> ) end, ?_assertEqual( - {error,flag_type_mismatch}, + {error, flag_type_mismatch}, cfclient_evaluator:string_variation(<<"My_number_flag">>, existing_target_a(), config()) ) }, @@ -546,7 +546,7 @@ variations_number() -> ) end, ?_assertEqual( - {error,flag_type_mismatch}, + {error, flag_type_mismatch}, cfclient_evaluator:number_variation(<<"My_boolean_flag">>, existing_target_a(), config()) ) }, @@ -742,7 +742,7 @@ variations_json() -> ) end, ?_assertEqual( - {error,flag_type_mismatch}, + {error, flag_type_mismatch}, cfclient_evaluator:json_variation(<<"My_json_flag">>, existing_target_a(), config()) ) }, diff --git a/test/cfclient_ff_test_cases.erl b/test/cfclient_ff_test_cases.erl index f473ead..1e639bb 100644 --- a/test/cfclient_ff_test_cases.erl +++ b/test/cfclient_ff_test_cases.erl @@ -63,7 +63,13 @@ evaluate_file(Path) -> setup, fun () -> - Config = [{name, ?MODULE}, {analytics_enabled, false}, {poll_enabled, false}, {unit_test_mode, true}], + Config = + [ + {name, ?MODULE}, + {analytics_enabled, false}, + {poll_enabled, false}, + {unit_test_mode, true} + ], {ok, Pid} = cfclient_instance:start_link([{config, Config}]), [cfclient_cache:cache_flag(F) || F <- Flags], [cfclient_cache:cache_segment(S) || S <- Segments],