### setup(options) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Initializes the store. Called once when IdempotencyPlug.RequestTracker starts. Use this to prepare the store for use, e.g. create table or verify database connection. Returns {:error, reason} if initialization fails, which will cause IdempotencyPlug.RequestTracker to fail to start. ```APIDOC ## setup(options) ### Description Initializes the store. Called once when `IdempotencyPlug.RequestTracker` starts. Use this to prepare the store for use, e.g. create table or verify database connection. Returns `{:error, reason}` if initialization fails, which will cause `IdempotencyPlug.RequestTracker` to fail to start. ### Callback ``` @callback setup(options()) :: :ok | {:error, term()} ``` ### Parameters - **options** (keyword()) - Required - Options for the store, typically containing configuration like `:redix`. ### Returns - `:ok` on success - `{:error, term()}` on failure ``` -------------------------------- ### Start RequestTracker with custom options in an Elixir supervision tree Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.RequestTracker.html Configures and starts a IdempotencyPlug.RequestTracker within a supervisor. The example specifies a 6-hour cache TTL, 1-minute pruning interval, and an Ecto store using MyApp.Repo. This setup is used to track request processes for idempotency handling. ```elixir children = [ { IdempotencyPlug.RequestTracker, cache_ttl: :timer.hours(6), prune: :timer.minutes(1), store: {IdempotencyPlug.EctoStore, repo: MyApp.Repo} } ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) ``` -------------------------------- ### Callback specification: setup/1 Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Specifies the setup callback. Returns :ok or {:error, term()}. ```elixir @callback setup(options()) :: :ok | {:error, term()} ``` -------------------------------- ### start_link(opts) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.RequestTracker.html Starts the RequestTracker GenServer with the given options. This is typically called by a supervisor. ```APIDOC ## start_link(opts) ### Description Starts the RequestTracker GenServer with the given options. This is typically called by a supervisor. ### Method start_link ### Parameters - **opts** (keyword()) - Required - Options for the tracker. Supported options: `:cache_ttl` (integer, milliseconds, default 24 hours), `:prune` (integer, milliseconds, default 60 seconds), `:store` (module and options, default `{IdempotencyPlug.ETSStore, [table: Elixir.IdempotencyPlug.RequestTracker]}`). ### Returns - **{:ok, pid()}** - The PID of the started process. - **{:error, term()}** - If the process fails to start. ### Example ```elixir {:ok, pid} = IdempotencyPlug.RequestTracker.start_link(cache_ttl: :timer.hours(6), prune: :timer.minutes(1)) ``` ``` -------------------------------- ### RedixStore implementation of IdempotencyPlug.Store Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Example module implementing the IdempotencyPlug.Store behaviour using Redix. It defines setup, lookup, insert, update, and prune callbacks. The store uses Redis commands and handles errors by raising or returning error tuples. ```elixir defmodule RedixStore do @behaviour IdempotencyPlug.Store @impl true def setup(options) do name = Keyword.fetch!(options, :redix) {:ok, _conn} = Redix.start_link(name: name) :ok end @impl true def lookup(request_id, options) do redix = Keyword.fetch!(options, :redix) case Redix.command(redix, ["GET", request_id]) do {:ok, nil} -> :not_found {:ok, encoded} -> {data, fingerprint, expires_at} = :erlang.binary_to_term(encoded, [:safe]) {data, fingerprint, expires_at} {:error, reason} -> raise reason end end @impl true def insert(request_id, data, fingerprint, expires_at, options) do redix = Keyword.fetch!(options, :redix) value = :erlang.term_to_binary({data, fingerprint, expires_at}) expires_in = DateTime.diff(expires_at, DateTime.utc_now(), :second) case Redix.command(redix, ["SET", request_id, value, "EX", Integer.to_string(expires_in), "NX"]) do {:ok, "OK"} -> :ok {:ok, nil} -> {:error, "key #{request_id} already exists in store"} {:error, reason} -> {:error, reason} end end @impl true def update(request_id, data, expires_at, options) do redix = Keyword.fetch!(options, :redix) case lookup(request_id, options) do :not_found -> {:error, "key #{request_id} not found in store"} {_data, fingerprint, _expires_at} -> value = :erlang.term_to_binary({data, fingerprint, expires_at}) expires_in = DateTime.diff(expires_at, DateTime.utc_now(), :second) case Redix.command(redix, ["SET", request_id, value, "EX", Integer.to_string(expires_in)]) do {:ok, "OK"} -> :ok {:error, reason} -> {:error, reason} end end end @impl true def prune(options), do: :ok end ``` -------------------------------- ### Configure ETSStore in Application Supervisor Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.ETSStore.html Example of setting up the IdempotencyPlug.ETSStore as the store for IdempotencyPlug.RequestTracker in an application's supervision tree. The ETS table is not persisted and will be flushed on restart. ```elixir defmodule MyApp.Application do # .. def start(_type, _args) do children = [ {IdempotencyPlug.RequestTracker, [ store: {IdempotencyPlug.ETSStore, table: MyAppWeb.RequestTrackerStore}]} # ... ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) end end ``` -------------------------------- ### Configure IdempotencyPlug.EctoStore in application Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.EctoStore.html Example of configuring the Ecto store in a supervision tree. The store is passed to the RequestTracker as a tuple with the repo module. ```elixir defmodule MyApp.Application do # .. def start(_type, _args) do children = [ {IdempotencyPlug.RequestTracker, [ store: {IdempotencyPlug.EctoStore, repo: MyApp.Repo}]} # ... ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) end end ``` -------------------------------- ### Add idempotency_plug dependency in mix.exs Source: https://hexdocs.pm/idempotency_plug/index.html Add the idempotency_plug dependency to your mix.exs file to install the library. ```elixir def deps do [ {:idempotency_plug, "~> 0.2.2"} ] end ``` -------------------------------- ### child_spec(init_arg) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.RequestTracker.html Returns a specification to start this module under a supervisor. This is used when adding the RequestTracker to a supervision tree. ```APIDOC ## child_spec(init_arg) ### Description Returns a specification to start this module under a supervisor. ### Method child_spec ### Parameters - **init_arg** (term) - Required - The initialization arguments for the GenServer. ### Returns - **spec** (Supervisor.child_spec()) - The child specification. ### Example ```elixir children = [ {IdempotencyPlug.RequestTracker, cache_ttl: :timer.hours(6), prune: :timer.minutes(1), store: {IdempotencyPlug.EctoStore, repo: MyApp.Repo}} ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) ``` ``` -------------------------------- ### Migrate to IdempotencyPlug v0.2.0: plug configuration and handler module Source: https://hexdocs.pm/idempotency_plug/CHANGELOG.html Migration guide for v0.2.0 breaking changes. The first block shows the new plug configuration using the `:tracker`, `:idempotency_key`, and `:with` options. The second block is the required handler module that implements `scope_idempotency_key/2` and `handle_error/2`. ```elixir plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker, idempotency_key: {MyAppWeb.IdempotencyPlugHandler, :scope_idempotency_key}, with: {MyAppWeb.IdempotencyPlugHandler, :handle_error} ``` ```elixir defmodule MyAppWeb.IdempotencyPlugHandler do import Phoenix.Controller import Plug.Conn def scope_idempotency_key(conn, key), do: {conn.assigns.current_user.id, key} def handle_error(conn, error) do conn |> put_status(Plug.Exception.status(error)) |> json(%{error: error.message}) |> halt() end end ``` -------------------------------- ### Configure IdempotencyPlug with custom callbacks Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.html Complete configuration and callback functions for IdempotencyPlug. The plug is configured with a tracker, custom functions for scoping the idempotency key, limiting the request payload, hashing, and error handling. The callbacks show how to scope the key by user ID, drop the "value" parameter from the payload, compute a SHA-512 hash, and return a problem+json error response. ```elixir plug IdempotencyPlug, tracker: IdempotencyPlug.RequestTracker, idempotency_key: {__MODULE__, :scope_idempotency_key}, request_payload: {__MODULE__, :limit_request_payload}, hash: {__MODULE__, :sha512_hash}, with: {__MODULE__, :handle_error}, cached_headers: [{"idempotent-replayed", "true"}] ``` ```elixir def scope_idempotency_key(conn, key) do {conn.assigns.user.id, key} end ``` ```elixir def limit_request_payload(conn) do Map.drop(conn.params, ["value"]) end ``` ```elixir def sha512_hash(_type, value) do :sha512 |> :crypto.hash(:erlang.term_to_binary(value)) |> Base.encode16() |> String.downcase() end ``` ```elixir def handle_error(conn, error) do conn |> put_status(error.plug_status) |> put_resp_content_type("application/problem+json") |> json(%{ type: "https://example.com/errors/idempotency", title: error.message, status: error.plug_status }) |> halt() end ``` -------------------------------- ### lookup(request_id, options) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Fetches the entry for `request_id/0`. Returns a tuple of `data/0`, `fingerprint/0`, and `expires_at/0` that was inserted in `insert/5` or updated in `update/4`. Returns `:not_found` if no entry exists. ```APIDOC ## lookup(request_id, options) ### Description Fetches the entry for `request_id/0`. Returns a tuple of `data/0`, `fingerprint/0`, and `expires_at/0` that was inserted in `insert/5` or updated in `update/4`. Returns `:not_found` if no entry exists. ### Callback ``` @callback lookup(request_id(), options()) :: {data(), fingerprint(), expires_at()} | :not_found ``` ### Parameters - **request_id** (binary()) - Required - The unique identifier for the request. - **options** (keyword()) - Required - Options for the store. ### Returns - `{data(), fingerprint(), expires_at()}` - The stored entry. - `:not_found` - If no entry exists. ``` -------------------------------- ### Configure request tracker with Ecto store Source: https://hexdocs.pm/idempotency_plug/index.html Configure the request tracker to use the Ecto store for persistence, after running the migration generator. ```elixir {IdempotencyPlug.RequestTracker, [store: {IdempotencyPlug.EctoStore, repo: MyApp.Repo}]} ``` -------------------------------- ### Type definition: options() Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Defines the options type as a keyword list. ```elixir @type options() :: keyword() ``` -------------------------------- ### insert(request_id, data, fingerprint, expires_at, options) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Inserts a new entry for `request_id/0`. Returns `{:error, reason}` if an entry already exists for `request_id/0`, or if any other error occurs. ```APIDOC ## insert(request_id, data, fingerprint, expires_at, options) ### Description Inserts a new entry for `request_id/0`. Returns `{:error, reason}` if an entry already exists for `request_id/0`, or if any other error occurs. ### Callback ``` @callback insert(request_id(), data(), fingerprint(), expires_at(), options()) :: :ok | {:error, term()} ``` ### Parameters - **request_id** (binary()) - Required - The unique identifier for the request. - **data** (term()) - Required - The data to store. - **fingerprint** (binary()) - Required - A fingerprint of the request. - **expires_at** (DateTime.t()) - Required - The expiration time for the entry. - **options** (keyword()) - Required - Options for the store. ### Returns - `:ok` on success - `{:error, term()}` if an entry already exists or another error occurs. ``` -------------------------------- ### Callback specification: prune/1 Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Specifies the prune callback. Returns :ok. ```elixir @callback prune(options()) :: :ok ``` -------------------------------- ### Callback specification: insert/5 Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Specifies the insert callback. Returns :ok or {:error, term()}. ```elixir @callback insert(request_id(), data(), fingerprint(), expires_at(), options()) :: :ok | {:error, term()} ``` -------------------------------- ### Customize error response with :with option Source: https://hexdocs.pm/idempotency_plug/index.html Customize the error response by setting the :with option to handle errors yourself. ```elixir plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker, with: {__MODULE__, :handle_error} def handle_error(conn, error) do conn |> put_status(Plug.Exception.status(error)) |> json(%{error: error.message}) |> halt() end ``` -------------------------------- ### Add IdempotencyPlug.RequestTracker to supervision tree Source: https://hexdocs.pm/idempotency_plug/index.html Add the request tracker to your supervision tree in the application module. ```elixir defmodule MyApp.Application do # .. def start(_type, _args) do children = [ {IdempotencyPlug.RequestTracker, [name: MyAppWeb.RequestTracker]} # ... ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) end end ``` -------------------------------- ### Set table to UNLOGGED in Postgres Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.EctoStore.html For high-write database workloads, WAL pressure may become a bottleneck. With Postgres, set the idempotency_plug_requests table to UNLOGGED to skip WAL entirely. This is only appropriate when losing the idempotency cache during recovery is acceptable, not for payments or critical cases. ```sql ALTER TABLE idempotency_plug_requests SET UNLOGGED ``` -------------------------------- ### Callback specification: update/4 Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Specifies the update callback. Returns :ok or {:error, term()}. ```elixir @callback update(request_id(), data(), expires_at(), options()) :: :ok | {:error, term()} ``` -------------------------------- ### Add idempotency key helper for Phoenix tests Source: https://hexdocs.pm/idempotency_plug/index.html Helper for Phoenix controller tests that sets a unique idempotency key on each request, ensuring tests succeed. ```elixir def setup_with_idempotency_key(%{conn: conn}) do conn = Plug.Conn.put_req_header(conn, "idempotency-key", Ecto.UUID.generate()) {:ok, conn: conn} end ``` ```elixir setup :setup_with_idempotency_key ``` -------------------------------- ### Callback specification: lookup/2 Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Specifies the lookup callback. Returns a tuple of data, fingerprint, and expires_at, or :not_found. ```elixir @callback lookup(request_id(), options()) :: {data(), fingerprint(), expires_at()} | :not_found ``` -------------------------------- ### track(name_or_pid, request_id, fingerprint) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.RequestTracker.html Tracks a request ID. Returns `{:init, key, expires_at}` for first-time requests. Subsequent requests return the request state. If the request payload fingerprint differs from what was stored, an error is returned. Emits `:cache_hit` and `:cache_miss` telemetry events. ```APIDOC ## track(name_or_pid, request_id, fingerprint) ### Description Tracks a request ID. Returns `{:init, key, expires_at}` for first-time requests. Subsequent requests return the request state. If the request payload fingerprint differs from what was stored, an error is returned. Emits `:cache_hit` and `:cache_miss` telemetry events. ### Method Function call ### Endpoint `track(name_or_pid, request_id, fingerprint)` ### Parameters - **name_or_pid** (`GenServer.server()`) - Required - The server name or PID. - **request_id** (`binary()`) - Required - The request ID. - **fingerprint** (`binary()`) - Required - The fingerprint of the request payload. ### Returns - `{:error, term()}` - Error with reason. - `{:init, binary(), DateTime.t()}` - First-time request with key and expiration. - `{:mismatch, {:fingerprint, binary()}, DateTime.t()}` - Fingerprint mismatch with stored fingerprint and expiration. - `{:processing, {node(), pid()}, DateTime.t()}` - Request is being processed by another node/process. - `{:cache, {:ok, term()}, DateTime.t()}` - Cache hit with stored response. - `{:cache, {:halted, term()}, DateTime.t()}` - Cache hit with halted response. ``` -------------------------------- ### Add IdempotencyPlug to router pipeline Source: https://hexdocs.pm/idempotency_plug/index.html Add the plug to your API pipeline to make POST and PATCH requests idempotent using the Idempotency-Key header. ```elixir defmodule MyAppWeb.Router do # ... pipeline :api do plug :accepts, ["json"] plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker end # ... end ``` -------------------------------- ### update(request_id, data, expires_at, options) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Updates the entry for `request_id/0`. Returns `{:error, reason}` if no entry exists for `request_id/0`, or if any other error occurs. ```APIDOC ## update(request_id, data, expires_at, options) ### Description Updates the entry for `request_id/0`. Returns `{:error, reason}` if no entry exists for `request_id/0`, or if any other error occurs. ### Callback ``` @callback update(request_id(), data(), expires_at(), options()) :: :ok | {:error, term()} ``` ### Parameters - **request_id** (binary()) - Required - The unique identifier for the request. - **data** (term()) - Required - The new data to store. - **expires_at** (DateTime.t()) - Required - The new expiration time. - **options** (keyword()) - Required - Options for the store. ### Returns - `:ok` on success - `{:error, term()}` if no entry exists or another error occurs. ``` -------------------------------- ### idempotency_key(conn, key) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.html Default :idempotency_key callback. Returns the key unchanged. ```APIDOC ## idempotency_key(conn, key) ### Description Default `:idempotency_key` callback. Returns the key unchanged. ### Spec ```elixir @spec idempotency_key(Plug.Conn.t(), term()) :: term() ``` ### Parameters - **conn** (Plug.Conn.t()) - The connection. - **key** (term()) - The idempotency key. ### Returns - (term()) - The key unchanged. ``` -------------------------------- ### plug IdempotencyPlug Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.html The IdempotencyPlug is a plug that can be used in a Phoenix pipeline to provide idempotent request handling. It requires a tracker and supports options for customizing the idempotency key, request payload hashing, error handling, and cached response headers. ```APIDOC ## plug IdempotencyPlug ### Description The IdempotencyPlug is a plug that provides idempotent request handling. It caches responses for requests with the same idempotency key and replays them for subsequent identical requests. It requires a tracker and supports options for customizing the idempotency key, request payload hashing, error handling, and cached response headers. ### Usage ```elixir plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker, idempotency_key: {__MODULE__, :scope_idempotency_key}, cached_headers: [{"idempotent-replayed", "true"}] ``` ### Options - **tracker** (GenServer.server/0) - Required - Reference for the `IdempotencyPlug.RequestTracker` GenServer. - **idempotency_key** (mfa_tuple/0) - Optional - Callback to process idempotency key. Defaults to `{Elixir.IdempotencyPlug, :idempotency_key}`. - **request_payload** (mfa_tuple/0) - Optional - Callback to shape request payload. Defaults to `{Elixir.IdempotencyPlug, :request_payload}`. - **hash** (mfa_tuple/0) - Optional - Callback to hash an Erlang term. Receives `(type, value)` where `type` is `:idempotency_key` or `:request_payload`. Defaults to `{Elixir.IdempotencyPlug, :sha256_hash}`. - **with** (atom or mfa_tuple/0) - Optional - Error handling mode. Defaults to `:exception`. `:exception` raises an error; an MFA tuple calls the MFA to process the conn with error, and the connection MUST be halted. - **cached_headers** (list of {name, value} tuples) - Optional - Response headers to add on top of the cached response. ### Cached Responses Cached responses use the original status, body, and headers. Any response headers already set on the conn by upstream plugs are dropped. An `Expires` header is added on top. See `IdempotencyPlug.RequestTracker` for more on expiration. ### Authenticated Requests When authenticating users, scope the key to the user via the `:idempotency_key` option to prevent cross-user cache hits: ```elixir plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker, idempotency_key: {__MODULE__, :scope_idempotency_key} def scope_idempotency_key(conn, key), do: {conn.assigns.current_user.id, key} ``` ### Replay Header Stripe uses an `Idempotent-Replayed` response header to indicate that a response is a replay of a cached response. This can be added to cached responses with the `:cached_headers` option: ```elixir plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker, cached_headers: [{"idempotent-replayed", "true"}] ``` ``` -------------------------------- ### track(name_or_pid, request_id, fingerprint) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.RequestTracker.html Tracks a request ID with a fingerprint. This is used to check if a request has been seen before and to store the initial state. ```APIDOC ## track(name_or_pid, request_id, fingerprint) ### Description Tracks a request ID with a fingerprint. This is used to check if a request has been seen before and to store the initial state. ### Method track ### Parameters - **name_or_pid** (atom() | pid()) - Required - The name or PID of the RequestTracker process. - **request_id** (binary()) - Required - The unique identifier for the request. - **fingerprint** (binary()) - Required - A fingerprint of the request payload. ### Returns - **{:cache, {:ok, data}, expires_at}** - If a cached response exists with matching fingerprint. - **{:mismatch, {:fingerprint, fingerprint}, expires_at}** - If a cached response exists but the fingerprint differs. - **{:processing, {node, pid}, expires_at}** - If the request is still being processed. - **{:cache, {:halted, reason}, expires_at}** - If the original request process terminated unexpectedly. ### Example ```elixir case IdempotencyPlug.RequestTracker.track(tracker, request_id, fingerprint) do {:cache, {:ok, data}, _} -> data {:processing, _, _} -> :wait {:mismatch, _, _} -> :error {:cache, {:halted, reason}, _} -> {:error, reason} end ``` ``` -------------------------------- ### Type definition: data() Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Defines the data type as any term. ```elixir @type data() :: term() ``` -------------------------------- ### request_payload(conn) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.html Default :request_payload callback. Returns request params as a sorted list so the resulting hash is deterministic. ```APIDOC ## request_payload(conn) ### Description Default `:request_payload` callback. Returns request params as a sorted list so the resulting hash is deterministic. ### Spec ```elixir @spec request_payload(Plug.Conn.t()) :: [{binary(), term()}] ``` ### Parameters - **conn** (Plug.Conn.t()) - The connection. ### Returns - (list of {binary(), term()}) - Sorted request params. ``` -------------------------------- ### prune(options) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Deletes expired entries. Called periodically by `IdempotencyPlug.RequestTracker` per its `:prune` option. ```APIDOC ## prune(options) ### Description Deletes expired entries. Called periodically by `IdempotencyPlug.RequestTracker` per its `:prune` option. ### Callback ``` @callback prune(options()) :: :ok ``` ### Parameters - **options** (keyword()) - Required - Options for the store. ### Returns - `:ok` ``` -------------------------------- ### Type definition: fingerprint() Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Defines the fingerprint type as a binary. ```elixir @type fingerprint() :: binary() ``` -------------------------------- ### put_response(name_or_pid, request_id, response) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.RequestTracker.html Updates the state for a given request ID. ```APIDOC ## put_response(name_or_pid, request_id, response) ### Description Updates the state for a given request ID. ### Method Function call ### Endpoint `put_response(name_or_pid, request_id, response)` ### Parameters - **name_or_pid** (`GenServer.server()`) - Required - The server name or PID. - **request_id** (`binary()`) - Required - The request ID. - **response** (`term()`) - Required - The response to store. ### Returns - `{:ok, DateTime.t()}` - Success with expiration timestamp. - `{:error, term()}` - Error with reason. ``` -------------------------------- ### Scope idempotency key to user with :idempotency_key option Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.html Scopes the idempotency key to the current user to prevent cross-user cache hits. The callback receives the conn and the key, and returns a tuple with the user id and the key. ```elixir plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker, idempotency_key: {__MODULE__, :scope_idempotency_key} def scope_idempotency_key(conn, key), do: {conn.assigns.current_user.id, key} ``` -------------------------------- ### Type definition: request_id() Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Defines the request_id type as a binary. ```elixir @type request_id() :: binary() ``` -------------------------------- ### sha256_hash(type, value) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.html Default :hash callback. Returns a hex encoded sha256 hash of `value`. `type` is ignored. ```APIDOC ## sha256_hash(type, value) ### Description Default `:hash` callback. Returns a hex encoded sha256 hash of `value`. `type` is ignored. ### Spec ```elixir @spec sha256_hash(:idempotency_key | :request_payload, term()) :: binary() ``` ### Parameters - **type** (:idempotency_key | :request_payload) - The type of value being hashed (ignored). - **value** (term()) - The value to hash. ### Returns - (binary()) - Hex encoded SHA-256 hash. ``` -------------------------------- ### put_response(name_or_pid, request_id, response) Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.RequestTracker.html Updates the state for a given request ID with the response data. This should be called after a request has completed to cache the response. ```APIDOC ## put_response(name_or_pid, request_id, response) ### Description Updates the state for a given request ID with the response data. This should be called after a request has completed to cache the response. ### Method put_response ### Parameters - **name_or_pid** (atom() | pid()) - Required - The name or PID of the RequestTracker process. - **request_id** (binary()) - Required - The unique identifier for the request. - **response** (term()) - Required - The response data to cache. ### Returns - **:ok** - The response was successfully stored. ### Example ```elixir IdempotencyPlug.RequestTracker.put_response(tracker, request_id, response) ``` ``` -------------------------------- ### Type definition: expires_at() Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html Defines the expires_at type as a DateTime. ```elixir @type expires_at() :: DateTime.t() ``` -------------------------------- ### Add replay header with :cached_headers option Source: https://hexdocs.pm/idempotency_plug/IdempotencyPlug.html Adds an Idempotent-Replayed response header to cached responses, similar to Stripe's behavior. The :cached_headers option accepts a list of {name, value} tuples. ```elixir plug IdempotencyPlug, tracker: MyAppWeb.RequestTracker, cached_headers: [{"idempotent-replayed", "true"}] ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.