Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/elixir_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
--health-timeout 5s
--health-retries 5
ports:
- 55555:5432
- 54321:5432

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -83,7 +83,8 @@ jobs:
run: mix compile --force --all-warnings --warnings-as-errors

- name: Run tests
run: mix test --trace
# Include igniter tests since phx_new is installed in CI
run: mix test --trace --include igniter

test-as-dep-standalone:
name: Test installation as a dependency without Electric or Ecto
Expand Down Expand Up @@ -140,7 +141,7 @@ jobs:
--health-timeout 5s
--health-retries 5
ports:
- 55555:5432
- 54321:5432
steps:
- uses: actions/checkout@v4

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ electric_phoenix-*.tar

# Temporary files, for example, from tests.
/tmp/

# Claude Code local settings
.claude/
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.6.2] - 2026-01-06

### Changed

- **Breaking**: Updated `electric` dependency to `~> 1.2.4` (dropping support for Electric 1.1.x)
- **Breaking**: Updated `electric_client` dependency to `~> 0.8.0`
- Removed `http_api_num_acceptors` workaround (fixed upstream in Electric via [#2863](https://github.com/electric-sql/electric/pull/2863))
- Updated `Electric.StatusMonitor.mark_connection_pool_ready/2` calls to `/3` (Electric 1.2.x API change)
- Updated storage configuration to use keyword list format (Electric 1.2.x requirement)

### Deprecated

- **Sandbox mode** (`mode: :sandbox`) is deprecated and will be removed in a future version. Electric 1.2.x introduced architectural changes that make sandbox mode incompatible with the new internal structure. Use embedded mode with a test database instead.
- **LiveView streams** (`Phoenix.Sync.LiveView.sync_stream/4` and `sync_stream_update/3`) are deprecated and will be removed in a future version. Use `Phoenix.Sync.Shape` or client-side sync with TanStack DB instead.

### Added

- Implemented `Inspector.load_supported_features/1` callback (new in Electric 1.2.x)
- Implemented `PublicationManager.wait_for_restore/1` callback (new in Electric 1.2.x)
- Updated storage configuration tests to handle both keyword list and map formats for better forward compatibility

### Migration Guide

If upgrading from Phoenix.Sync 0.6.1 or earlier:

1. **Electric 1.2.x Required**: This version requires Electric 1.2.4 or later. Electric 1.1.x is no longer supported.

2. **Deprecated Features**:
- **Sandbox mode**: No longer compatible with Electric 1.2.x. Migrate to embedded mode with test database.
- **LiveView streams**: `sync_stream/4` and `sync_stream_update/3` will be removed in a future version. Migrate to `Phoenix.Sync.Shape` or client-side sync.

3. **Deprecated Configuration Options**:
- `experimental_live_sse` has been replaced by `live_sse` in Electric 1.2.x
- The `ELECTRIC_EXPERIMENTAL_MAX_SHAPES` environment variable has been retired; use the `max_shapes` configuration option instead

4. **New Configuration Options** (Electric 1.2.x):
- `live_sse`: Enable server-sent events for real-time updates (replaces `experimental_live_sse`)
- `replication_idle_timeout`: Automatically close database connections during idle replication streams (useful for scale-to-zero deployments)

## [0.6.1] - 2025-10-13

### Fixed
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ Example config:
# mix.exs
defp deps do
[
{:electric, "~> 1.0"},
{:electric, "~> 1.2"},
{:phoenix_sync, "~> 0.6"}
]
end
Expand Down Expand Up @@ -303,7 +303,7 @@ It is also possible to include Electric as an application dependency and configu
# mix.exs
defp deps do
[
{:electric, "~> 1.0"},
{:electric, "~> 1.2"},
{:phoenix_sync, "~> 0.6"}
]
end
Expand Down Expand Up @@ -342,7 +342,7 @@ With Electric only included and compiled as a dependency in `:dev` and `:test`.
# mix.exs
defp deps do
[
{:electric, "~> 1.0", only: [:dev, :test]},
{:electric, "~> 1.2", only: [:dev, :test]},
{:phoenix_sync, "~> 0.6"}
]
end
Expand Down
2 changes: 1 addition & 1 deletion apps/phoenix_sync_example/config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ config :phoenix_sync_example, PhoenixSyncExample.Repo,
password: "password",
hostname: "localhost",
database: "phoenix_sync_example_dev",
port: 55555,
port: 54321,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
Expand Down
5 changes: 3 additions & 2 deletions apps/phoenix_sync_example/config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ config :phoenix_sync_example, PhoenixSyncExample.Repo,
password: "password",
hostname: "localhost",
database: "phoenix_sync_example_test#{System.get_env("MIX_TEST_PARTITION")}",
port: 55555,
port: 54321,
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2

Expand All @@ -33,4 +33,5 @@ config :phoenix_live_view,

config :phoenix_sync,
env: config_env(),
mode: :sandbox
mode: :embedded,
repo: PhoenixSyncExample.Repo
4 changes: 1 addition & 3 deletions apps/phoenix_sync_example/lib/phoenix_sync_example/repo.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
defmodule PhoenixSyncExample.Repo do
use Phoenix.Sync.Sandbox.Postgres

use Ecto.Repo,
otp_app: :phoenix_sync_example,
adapter: Phoenix.Sync.Sandbox.Postgres.adapter()
adapter: Ecto.Adapters.Postgres
end
2 changes: 1 addition & 1 deletion apps/phoenix_sync_example/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule PhoenixSyncExample.MixProject do
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"},
{:phoenix_sync, path: "../.."},
{:electric, "~> 1.1.0"}
{:electric, "~> 1.2"}
]
end

Expand Down
1 change: 0 additions & 1 deletion apps/phoenix_sync_example/test/support/data_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ defmodule PhoenixSyncExample.DataCase do
Ecto.Adapters.SQL.Sandbox.start_owner!(PhoenixSyncExample.Repo, shared: not tags[:async])

on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
Phoenix.Sync.Sandbox.start!(PhoenixSyncExample.Repo, pid, shared: not tags[:async])
end

@doc """
Expand Down
2 changes: 1 addition & 1 deletion apps/plug_sync/config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ case System.get_env("PHOENIX_SYNC_MODE", "embedded") do
password: "password",
hostname: "localhost",
database: "plug_sync",
port: 55555
port: 54321
end
10 changes: 9 additions & 1 deletion apps/plug_sync/config/test.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import Config

config :phoenix_sync, mode: :sandbox, env: config_env()
config :phoenix_sync, mode: :embedded, env: config_env(), repo: PlugSync.Repo

config :plug_sync, PlugSync.Repo,
username: "postgres",
password: "password",
hostname: "localhost",
database: "phoenix_sync",
port: 54321,
pool: Ecto.Adapters.SQL.Sandbox
4 changes: 1 addition & 3 deletions apps/plug_sync/lib/plug_sync/repo.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
defmodule PlugSync.Repo do
use Phoenix.Sync.Sandbox.Postgres

use Ecto.Repo,
otp_app: :plug_sync,
adapter: Phoenix.Sync.Sandbox.Postgres.adapter()
adapter: Ecto.Adapters.Postgres
end
2 changes: 1 addition & 1 deletion apps/plug_sync/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ defmodule PlugSync.MixProject do
{:bandit, "~> 1.0"},
{:postgrex, "~> 0.21"},
{:ecto_sql, "~> 3.0"},
{:electric, "~> 1.1.2"},
{:electric, "~> 1.2"},
{:phoenix_sync, [path: "../..", override: true]},
{:igniter, "~> 0.6"}
]
Expand Down
2 changes: 1 addition & 1 deletion config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Config
if config_env() == :test do
# port = 3333
default_database_url =
"postgresql://postgres:password@localhost:55555/phoenix_sync?sslmode=disable"
"postgresql://postgres:password@localhost:54321/phoenix_sync?sslmode=disable"

database_url = System.get_env("DATABASE_URL", default_database_url)

Expand Down
6 changes: 4 additions & 2 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ db_config = [
password: "password",
hostname: "localhost",
database: "phoenix_sync",
port: 55555
port: 54321
]

# configure the support repo with random options so we can validate them in Phoenix.Sync.ConfigTest
Expand Down Expand Up @@ -51,7 +51,9 @@ config :phoenix_sync,
ownership_log: :warning
]

config :phoenix_sync, env: :test, mode: :sandbox
# Note: sandbox mode is deprecated and disabled in Electric 1.2.x
# Tests now run in embedded mode with a real test database
config :phoenix_sync, env: :test, mode: :embedded

config :phoenix_sync,
Phoenix.Sync.SandboxTest.Endpoint,
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "55555:5432"
- "54321:5432"
volumes:
- ./postgres.conf:/etc/postgresql.conf:ro
tmpfs:
Expand Down
3 changes: 0 additions & 3 deletions lib/phoenix/sync/electric.ex
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,6 @@ defmodule Phoenix.Sync.Electric do
:error ->
opts
end
# TODO: remove this once https://github.com/electric-sql/electric/pull/2863
# is released
|> Keyword.put_new(:http_api_num_acceptors, nil)
end
else
defp start_embedded(_env, _mode, _db_config_fun, _message) do
Expand Down
17 changes: 17 additions & 0 deletions lib/phoenix/sync/live_view.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
if Code.ensure_loaded?(Phoenix.Component) do
defmodule Phoenix.Sync.LiveView do
@moduledoc """
> #### Deprecated {: .warning}
>
> LiveView streams support (`sync_stream/4`) is deprecated and will be removed
> in a future version. This feature is incompatible with planned future changes
> to the Electric sync architecture.
>
> For real-time data synchronization in LiveView, consider using:
> - `Phoenix.Sync.Shape` for maintaining in-memory shape state
> - Direct Electric client integration with `Phoenix.PubSub`
> - Client-side sync with TanStack DB

Swap out `Phoenix.LiveView.stream/3` for `Phoenix.Sync.LiveView.sync_stream/4` to
automatically keep a LiveView up-to-date with the state of your Postgres database:

Expand Down Expand Up @@ -228,6 +239,7 @@ if Code.ensure_loaded?(Phoenix.Component) do
</div>

"""
@doc since: "0.5.0", deprecated: "Use Phoenix.Sync.Shape or client-side sync instead"
@spec sync_stream(
socket :: Phoenix.LiveView.Socket.t(),
name :: atom() | String.t(),
Expand Down Expand Up @@ -302,7 +314,12 @@ if Code.ensure_loaded?(Phoenix.Component) do
end

The `opts` are passed to the `Phoenix.LiveView.stream_insert/4` call.

> #### Deprecated {: .warning}
>
> This function is deprecated along with `sync_stream/4`.
"""
@doc since: "0.5.0", deprecated: "Use Phoenix.Sync.Shape or client-side sync instead"
@spec sync_stream_update(Phoenix.LiveView.Socket.t(), event(), Keyword.t()) ::
Phoenix.LiveView.Socket.t()
def sync_stream_update(socket, event, opts \\ [])
Expand Down
17 changes: 15 additions & 2 deletions lib/phoenix/sync/sandbox.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
if Phoenix.Sync.sandbox_enabled?() do
defmodule Phoenix.Sync.Sandbox do
@moduledoc """
> #### Deprecated {: .warning}
>
> Sandbox mode is deprecated and will be removed in a future version.
> Electric 1.2.x introduced architectural changes that make sandbox mode
> incompatible with the new internal structure. Use embedded mode with
> a test database and proper cleanup instead.
>
> See the [Testing Guide](guides/testing.md) for recommended testing strategies.

Integration between `Ecto.Adapters.SQL.Sandbox` and `Electric` that produces
replication events from Ecto operations within a sandboxed connection.

Expand Down Expand Up @@ -256,10 +265,14 @@ if Phoenix.Sync.sandbox_enabled?() do
# give the inspector access to the sandboxed connection
Ecto.Adapters.SQL.Sandbox.allow(repo, owner, Sandbox.Inspector.name(stack_id))

# mark the stack as ready
# mark the stack as ready - Electric 1.2.x requires all these conditions
Electric.StatusMonitor.mark_pg_lock_acquired(stack_id, owner)
Electric.StatusMonitor.mark_replication_client_ready(stack_id, owner)
Electric.StatusMonitor.mark_connection_pool_ready(stack_id, owner)
# Electric 1.2.x requires pool type (:admin or :snapshot) as second argument
Electric.StatusMonitor.mark_connection_pool_ready(stack_id, :admin, owner)
Electric.StatusMonitor.mark_connection_pool_ready(stack_id, :snapshot, owner)
# Electric 1.2.x requires integrity checks to pass (note: typo is in Electric's code)
Electric.StatusMonitor.mark_integrety_checks_passed(stack_id, owner)

api_config = Sandbox.Stack.config(stack_id, repo)
api = Electric.Application.api(api_config)
Expand Down
37 changes: 37 additions & 0 deletions lib/phoenix/sync/sandbox/expiry_manager.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
if Phoenix.Sync.sandbox_enabled?() do
defmodule Phoenix.Sync.Sandbox.ExpiryManager do
@moduledoc false
# Stub implementation for Electric.Shapes.Supervisor
# The sandbox doesn't need actual shape expiry management

use GenServer

def child_spec(opts) do
{:ok, stack_id} = Keyword.fetch(opts, :stack_id)

%{
id: {__MODULE__, stack_id},
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :transient
}
end

def start_link(opts) do
stack_id = Keyword.fetch!(opts, :stack_id)
GenServer.start_link(__MODULE__, stack_id, name: name(stack_id))
end

def name(stack_id) do
Phoenix.Sync.Sandbox.name({__MODULE__, stack_id})
end

def init(stack_id) do
{:ok, %{stack_id: stack_id}}
end

# No-op implementations for expiry manager behavior
def handle_cast(_msg, state), do: {:noreply, state}
def handle_call(_msg, _from, state), do: {:reply, :ok, state}
end
end
6 changes: 6 additions & 0 deletions lib/phoenix/sync/sandbox/inspector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ if Phoenix.Sync.sandbox_enabled?() do
@impl Electric.Postgres.Inspector
def list_relations_with_stale_cache(_), do: {:ok, []}

@impl Electric.Postgres.Inspector
def load_supported_features(_stack_id) do
# Electric 1.2.x requires supports_generated_column_replication feature flag
{:ok, %{supports_generated_column_replication: false}}
end

def start_link(args) do
GenServer.start_link(__MODULE__, args, name: name(args[:stack_id]))
end
Expand Down
5 changes: 5 additions & 0 deletions lib/phoenix/sync/sandbox/publication_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,10 @@ if Phoenix.Sync.sandbox_enabled?() do
def refresh_publication(_opts) do
:ok
end

# Electric 1.2.x: New callback for waiting for restore completion
def wait_for_restore(_opts) do
:ok
end
end
end
Loading
Loading