Current section
Files
Jump to
Current section
Files
lib/chord.ex
defmodule Chord do
@moduledoc """
Chord: A powerful library for managing real-time contexts with efficiency and flexibility.
The `Chord` module serves as the library's entry point, providing a high-level API for managing contexts, tracking deltas, and integrating with various storage backends.
It is designed to simplify state synchronization and lifecycle management in real-time applications.
## Key Features
- **Context management**: Create, update, synchronize, and delete contexts seamlessly.
- **Delta tracking**: Efficiently track changes to minimize data transfer.
- **Flexible backends**: Built-in support for ETS (in-memory) and Redis (distributed).
- **Context export**: Easily export contexts to external storage systems.
- **Customizable cleanup**: Automate or manually clean up expired data.
- **Partial updates**: Update specific fields within a context.
- **External context provider**: Restore contexts from external sources as needed.
## Configuration
Chord is highly configurable via the application environment. Below are the available options:
```elixir
config :chord,
backend: Chord.Backend.ETS, # Backend for storing contexts and deltas (ETS or Redis)
context_auto_delete: true, # Enable automatic cleanup of expired contexts
context_ttl: 6 * 60 * 60, # Time-to-live for contexts
delta_ttl: 3 * 60 * 60, # Time-to-live for deltas
delta_threshold: 100, # Maximum number of deltas to retain per context
delta_formatter: Chord.Delta.Formatter.Default, # Delta formatter (default or custom)
time_provider: MyApp.TimeProvider, # Custom time provider for timestamps (optional)
time_unit: :second, # Unit for timestamps (:second or :millisecond)
export_callback: &MyApp.ContextExporter.export/1, # Callback for exporting contexts (optional)
context_external_provider: &MyApp.ExternalState.fetch_context/1 # Restore contexts from external storage (optional)
```
## Common Options
The following options are shared across several functions in this module and related modules:
- :context_id - The identifier for the context.
- :version - The version number for tracking state changes.
- :inserted_at - The timestamp when the context or delta was created.
- :limit - The maximum number of contexts or deltas to fetch.
- :offset - The number of entries to skip when fetching.
- :order - The sort order for results (:asc or :desc).
"""
alias Chord.{Context, Cleanup}
# Context Management API
@doc """
Sets the global context and records deltas.
This function allows you to set a new global context for a specific identifier
and automatically calculates deltas for any changes.
## Parameters
- `context_id` (any): The identifier for the context. This should uniquely represent the context.
- `new_context` (map): The new context to be stored. This should be a map containing the desired state.
## Returns
- `{:ok, %{context: map(), delta: map()}}` on success.
- `{:error, term()}` on failure.
## Examples
iex> Chord.set_context("user:369", %{status: "online", metadata: %{theme: "light", language: "en-US"}})
{:ok,
%{
context: %{
version: 1,
context: %{
status: "online",
metadata: %{language: "en-US", theme: "light"}
},
context_id: "user:369",
inserted_at: 1737892321
},
delta: %{
version: 1,
context_id: "user:369",
delta: %{
status: %{value: "online", action: :added},
metadata: %{
language: %{value: "en-US", action: :added},
theme: %{value: "light", action: :added}
}
},
inserted_at: 1737892321
}
}}
"""
@spec set_context(context_id :: any(), new_context :: map()) :: {:ok, map()} | {:error, term()}
defdelegate set_context(context_id, new_context), to: Context.Manager
@doc """
Retrieves the current global context for a given identifier.
This function fetches the context associated with the given `context_id`. If the context
does not exist or an invalid `context_id` is provided, an error is returned.
## Parameters
- `context_id` (any): The identifier for the context. This should uniquely represent the context.
## Returns
- `{:ok, map()}` on success, where the map contains the context data.
- `{:error, :not_found}` if the context does not exist.
- `{:error, term()}` for other types of failures.
## Examples
iex> Chord.get_context("user:369")
{:ok,
%{
version: 1,
context: %{status: "online", metadata: %{language: "en-US", theme: "light"}},
context_id: "user:369",
inserted_at: 1737892321
}}
iex> Chord.get_context("user:non-existent")
{:error, :not_found}
"""
@spec get_context(context_id :: any()) :: {:ok, map()} | {:error, term()}
defdelegate get_context(context_id), to: Context.Manager
@doc """
Partially updates the global context for a given identifier.
This function applies the given changes to the existing context identified by `context_id`.
It calculates the delta for the updated fields and returns the updated context and delta.
## Parameters
- `context_id` (any): The identifier for the context. This should uniquely represent the context.
- `changes` (map): A map of fields to be updated in the existing context.
The keys in this map must correspond to valid fields in the context, or,
if a specified path does not exist, it will be added as a new field at the root level of the context.
## Returns
- `{:ok, %{context: map(), delta: map()}}` on success, where:
- `context` is the updated context.
- `delta` is a map representing the changes made.
- `{:error, :not_found}` if the context does not exist.
- `{:error, term()}` for other types of failures.
## Examples
iex> Chord.update_context("user:369", %{metadata: %{theme: "dark"}})
{:ok,
%{
context: %{
version: 2,
context: %{status: "online", metadata: %{language: "en-US", theme: "dark"}},
context_id: "user:369",
inserted_at: 1737893007
},
delta: %{
version: 2,
context_id: "user:369",
delta: %{
metadata: %{
theme: %{value: "dark", action: :modified, old_value: "light"}
}
},
inserted_at: 1737893007
}
}}
iex> Chord.update_context("user:non-existent", %{metadata: %{theme: "dark"}})
{:error, :not_found}
"""
@spec update_context(context_id :: any(), changes :: map()) :: {:ok, map()} | {:error, term()}
defdelegate update_context(context_id, changes), to: Context.Manager
@doc """
Restores a context from an external provider to the current backend.
This function retrieves a context from an external storage provider (e.g., a database or cloud storage)
and stores it in the current backend for future use.
## Parameters
- `context_id` (any): The identifier for the context. This should uniquely represent the context.
## Returns
- `{:ok, map()}` on success, where the map represents the restored context.
- `{:error, :not_found}` if the context is not found in the external storage.
- `{:error, term()}` for other types of failures.
## Examples
iex> Chord.restore_context("user:369")
{:ok,
%{
version: 10,
context: %{source: "external storage provider"},
inserted_at: 1737464001,
context_id: "user:369"
}}
iex> Chord.restore_context("user:non-existent")
{:error, :not_found}
"""
@spec restore_context(context_id :: any()) :: {:ok, map()} | {:error, term()}
defdelegate restore_context(context_id), to: Context.Manager
@doc """
Deletes the global context, including all associated deltas.
This function removes the context identified by `context_id` from the backend, along with
all deltas related to that context. Once deleted, the context cannot be restored unless it is backed up externally.
## Parameters
- `context_id` (any): The identifier for the context. This should uniquely represent the context.
## Returns
- `:ok` on success, confirming that the context and its deltas were deleted.
- `{:error, term()}` for other types of failures.
## Examples
iex> Chord.delete_context("user:369")
:ok
iex> Chord.delete_context("user:non-existent")
{:error, :not_found}
"""
@spec delete_context(context_id :: any()) :: :ok | {:error, term()}
defdelegate delete_context(context_id), to: Context.Manager
@doc """
Exports the active context for a given identifier to external storage using the configured export callback.
This function retrieves the current context for the specified `context_id` and sends it
to an external storage provider via the callback defined in the application configuration.
## Parameters
- `context_id` (any): The identifier for the context. This should uniquely represent the context.
## Returns
- `:ok` if the context is successfully exported to the external storage.
- `{:error, :not_found}` if the specified context does not exist.
- `{:error, term()}` for other types of failures.
## Examples
iex> Chord.export_context("user:123")
:ok
iex> Chord.export_context("user:non-existent")
{:error, :not_found}
"""
@spec export_context(context_id :: any()) :: :ok | {:error, term()}
defdelegate export_context(context_id), to: Context.Manager
@doc """
Synchronizes the context for a client based on its current version.
This function ensures that the client has the latest version of the context identified by `context_id`.
Depending on the client's version, it will return either the full context, the delta of changes, or indicate
that no synchronization is necessary.
## Parameters
- `context_id` (any): The identifier for the context. This should uniquely represent the context.
- `client_version` (integer): The last known version of the context for the client.
## Returns
- `{:full_context, map()}`: Returns the entire context if the client is too far behind or has no version.
- `{:delta, map()}`: Returns only the changes (deltas) required to update the client to the latest version.
- `{:no_change, version}`: Indicates that the client's version is already up-to-date and no synchronization is needed.
## Examples
iex> Chord.sync_context("user:369", nil)
{:full_context,
%{
version: 2,
context: %{status: "online", metadata: %{language: "en-US", theme: "dark"}},
context_id: "user:369",
inserted_at: 1737893007
}}
iex> Chord.sync_context("user:369", 1)
{:delta,
%{
version: 2,
context_id: "user:369",
delta: %{
metadata: %{theme: %{value: "dark", action: :modified, old_value: "light"}}
},
inserted_at: 1737893007
}}
iex> Chord.sync_context("user:369", 2)
{:no_change, 2}
"""
@spec sync_context(context_id :: any(), client_version :: integer() | nil) ::
{:full_context, map()} | {:delta, map()} | {:no_change, integer()}
defdelegate sync_context(context_id, client_version), to: Context.Manager
# Cleanup API
@doc """
Triggers a one-time cleanup for stale contexts and deltas.
## Parameters
- `opts` (Keyword.t): Optional parameters for filtering contexts and deltas (see "Common Options").
## Returns
- `:ok` on success.
## Examples
iex> Chord.cleanup(limit: 10)
"""
@spec cleanup(opts :: Keyword.t()) :: :ok
def cleanup(opts \\ []) do
Cleanup.periodic_cleanup(opts)
end
# Cleanup.Server Management API
@doc """
Starts the Cleanup.Server for periodic cleanup.
## Parameters
- `opts` - Options for the Cleanup.Server. Examples include:
- `:interval` - The interval (in milliseconds) between cleanup executions.
- `:backend_opts` - A keyword list of options passed to backend listing functions (see "Common Options").
## Returns
- `{:ok, pid}` on success.
- `{:error, reason}` if an error occurs when starting the cleanup server process.
## Examples
iex> Chord.start_cleanup_server(interval: 60000)
{:ok, #PID<0.247.0>}
"""
@spec start_cleanup_server(opts :: Keyword.t()) :: {:ok, pid()} | {:error, term()}
def start_cleanup_server(opts \\ []) do
Cleanup.Server.start_link(opts)
end
@doc """
Stops the Cleanup.Server if running.
## Returns
- `:ok` on success.
## Examples
iex> Chord.stop_cleanup_server()
:ok
"""
@spec stop_cleanup_server() :: :ok
def stop_cleanup_server do
case Process.whereis(Cleanup.Server) do
nil -> :ok
pid -> GenServer.stop(pid, :normal)
end
end
@doc """
Updates the cleanup server's interval at runtime.
## Parameters
- `new_interval` (integer): The new interval in milliseconds.
## Returns
- `:ok` on success.
## Examples
iex> Chord.update_cleanup_interval(30000)
:ok
"""
@spec update_cleanup_interval(new_interval :: integer()) :: :ok
def update_cleanup_interval(new_interval) do
Cleanup.Server.update_interval(new_interval)
end
@doc """
Updates the backend options for the cleanup server at runtime.
## Parameters
- `new_opts` (Keyword.t): The new backend options.
## Returns
- `:ok` on success.
## Examples
iex> Chord.update_cleanup_backend_opts([limit: 100])
:ok
"""
@spec update_cleanup_backend_opts(new_opts :: Keyword.t()) :: :ok
def update_cleanup_backend_opts(new_opts) do
Cleanup.Server.update_backend_opts(new_opts)
end
end