@@ -39,6 +39,8 @@ defmodule Backpex.Preferences do
3939 * `get/3` — read a single preference.
4040 * `get_map/3` — read every value under a prefix as a nested map.
4141 * `put_async/4` — write from a LiveView socket or `%Plug.Conn{}`.
42+ * `put_batch/3` — dispatch a list of writes (best-effort, first-error-wins;
43+ see the function docs for the partial-success caveat).
4244 """
4345
4446 alias Backpex.Preferences.Adapters
@@ -174,16 +176,25 @@ defmodule Backpex.Preferences do
174176
175177 @ doc """
176178 Dispatches a batch of writes through their adapters and returns the
177- collected side effects, or an error list (all-or-nothing) .
179+ collected side effects, or the first error encountered .
178180
179- Used by `Backpex.PreferencesController` to implement cross-adapter batch
180- writes with a clean failure mode .
181+ Used by `Backpex.PreferencesController` to dispatch cross-adapter batch
182+ writes.
181183
182184 Threads the accumulated session state through each adapter call so that
183185 writes under the same session key compose correctly. The caller applies
184186 the returned effects in order; for `:put_session` effects targeting the
185187 same key, the last effect holds the fully-merged value.
186188
189+ ## Semantics
190+
191+ This is **best-effort, first-error-wins**. On the first adapter that
192+ returns `{:error, reason}` the loop halts and returns
193+ `{:error, {key, reason}}` — subsequent entries are not dispatched. Earlier
194+ successful writes may already have been committed by their adapters (e.g.
195+ a DB-backed adapter that writes eagerly). The adapter behaviour has no
196+ rollback primitive, so callers should treat partial success as possible.
197+
187198 ## Examples
188199
189200 ctx = Backpex.Preferences.Context.from_conn(conn)
@@ -195,26 +206,29 @@ defmodule Backpex.Preferences do
195206 #=> {:ok, [{:put_session, "backpex_preferences", %{...}}]}
196207 """
197208 @ spec put_batch ( Context . t ( ) , [ { String . t ( ) , term ( ) } ] , keyword ( ) ) ::
198- { :ok , [ Backpex.Preferences.Adapter . side_effect ( ) ] } | { :error , [ term ( ) ] }
209+ { :ok , [ Backpex.Preferences.Adapter . side_effect ( ) ] } | { :error , { String . t ( ) , term ( ) } }
199210 def put_batch ( % Context { } = ctx , entries , opts \\ [ ] ) when is_list ( entries ) do
200211 ctx = resolve_identity ( ctx )
201212
202- { effects , errors , _final_ctx } =
203- Enum . reduce ( entries , { [ ] , [ ] , ctx } , fn { key , value } , { effects_acc , errors_acc , current_ctx } ->
213+ # Accumulate effects by prepending each adapter's effects in reverse, then
214+ # reverse the whole list at the end — preserves the original left-to-right
215+ # order while staying O(n) in batch size.
216+ result =
217+ Enum . reduce_while ( entries , { [ ] , ctx } , fn { key , value } , { reversed_acc , current_ctx } ->
204218 { module , adapter_opts } = Router . resolve ( key )
205219
206220 case module . put ( current_ctx , key , value , merge_opts ( adapter_opts , opts ) ) do
207221 { :ok , fx } ->
208- { effects_acc ++ fx , errors_acc , apply_effects_to_ctx ( current_ctx , fx ) }
222+ { :cont , { :lists . reverse ( fx , reversed_acc ) , apply_effects_to_ctx ( current_ctx , fx ) } }
209223
210224 { :error , reason } ->
211- { effects_acc , [ { key , reason } | errors_acc ] , current_ctx }
225+ { :halt , { :error , { key , reason } } }
212226 end
213227 end )
214228
215- case errors do
216- [ ] -> { :ok , effects }
217- errs -> { :error , Enum . reverse ( errs ) }
229+ case result do
230+ { :error , _reason } = err -> err
231+ { reversed_acc , _ctx } -> { :ok , Enum . reverse ( reversed_acc ) }
218232 end
219233 end
220234
0 commit comments