@@ -17,12 +17,15 @@ if Code.ensure_loaded?(Phoenix.Component) do
1717 {:noreply, sync_stream_update(socket, event)}
1818 end
1919 end
20+
21+ See `sync_stream/4` for more details.
2022 ```
2123 """
2224
2325 use Phoenix.Component
2426
2527 alias Electric.Client.Message
28+ alias Phoenix.Sync.PredefinedShape
2629
2730 require Record
2831
@@ -190,6 +193,40 @@ if Code.ensure_loaded?(Phoenix.Component) do
190193 {:ok, Phoenix.Sync.LiveView.sync_stream(socket, :users, User)}
191194 end
192195 end
196+
197+ ## Keyword-based Shapes
198+
199+ `Ecto` is not required to use `sync_stream/4`. [Keyword-based
200+ shapes](../../../README.md#using-a-keyword-list) are possible but
201+ work a little differently.
202+
203+ def mount(_params, _session, socket) do
204+ socket =
205+ Phoenix.Sync.LiveView.sync_stream(
206+ socket,
207+ :admins,
208+ table: "users",
209+ where: "admin = true"
210+ )
211+ {:ok, socket}
212+ end
213+
214+ Without an underlying `Ecto.Schema` module to map the stream values, you
215+ will receive simple map values with string keys (plus a special
216+ `__sync_key__` value used for the live view
217+ [`dom_id`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream_configure/3)
218+ function.
219+
220+ So to use these in your template, you should be careful to use the
221+ `value["key"]` syntax to retrieve values. Using a keyword-based shape, The
222+ first example above becomes:
223+
224+ <div phx-update="stream">
225+ <div :for={{id, item} <- @streams.items} id={id}>
226+ <%= item["value"] %>
227+ </div>
228+ </div>
229+
193230 """
194231 @ spec sync_stream (
195232 socket :: Phoenix.LiveView.Socket . t ( ) ,
@@ -215,8 +252,9 @@ if Code.ensure_loaded?(Phoenix.Component) do
215252 end
216253 end )
217254
218- Phoenix.LiveView . stream (
219- socket ,
255+ socket
256+ |> configure_live_stream ( name , query )
257+ |> Phoenix.LiveView . stream (
220258 name ,
221259 client_live_stream ( client , name , query , component ) ,
222260 stream_opts
@@ -226,6 +264,36 @@ if Code.ensure_loaded?(Phoenix.Component) do
226264 end
227265 end
228266
267+ defp configure_live_stream ( socket , name , query ) do
268+ if PredefinedShape . is_queryable? ( query ) do
269+ # if the shape is an Ecto.Struct then we can just fallback to the default
270+ # dom_id function (which is `value.id`)
271+ socket
272+ else
273+ # since we don't have control over the `mount` callback, but want to use
274+ # `stream_configure` correctly which means that the stream should only be
275+ # configured once, we do our own tracking of configured streams in the
276+ # socket assigns
277+ case socket . assigns do
278+ % { __sync_stream_config__: % { ^ name => true } } ->
279+ socket
280+
281+ assigns ->
282+ assigns =
283+ Map . update (
284+ assigns ,
285+ :__sync_stream_config__ ,
286+ % { name => true } ,
287+ & Map . put ( & 1 , name , true )
288+ )
289+
290+ Phoenix.LiveView . stream_configure ( % { socket | assigns: assigns } , name ,
291+ dom_id: & Map . fetch! ( & 1 , :__sync_key__ )
292+ )
293+ end
294+ end
295+ end
296+
229297 @ doc """
230298 Handle Electric events within a LiveView.
231299
@@ -277,15 +345,42 @@ if Code.ensure_loaded?(Phoenix.Component) do
277345 defp client_live_stream ( client , name , query , component ) do
278346 pid = self ( )
279347
348+ shape =
349+ query
350+ |> PredefinedShape . new! ( )
351+ |> PredefinedShape . to_shape ( )
352+
280353 client
281- |> Electric.Client . stream ( query , live: false , replica: :full , errors: :stream )
354+ |> Electric.Client . stream ( shape , live: false , replica: :full , errors: :stream )
355+ |> keyed_stream ( query )
282356 |> Stream . transform (
283357 fn -> { [ ] , nil } end ,
284358 & live_stream_message / 2 ,
285- & update_mode ( & 1 , { client , name , query , pid , component } )
359+ & update_mode ( & 1 , { client , name , query , shape , pid , component } )
286360 )
287361 end
288362
363+ # when the stream is based off an Ecto.Schema struct then the default id
364+ # based dom-id function works. otherwise we can use the message key because
365+ # we **know** that is unique for a shape
366+ defp keyed_stream ( base_stream , query ) do
367+ if PredefinedShape . is_queryable? ( query ) do
368+ base_stream
369+ else
370+ Stream . map ( base_stream , fn
371+ % Message.ChangeMessage { key: key , value: value } = msg ->
372+ % { msg | value: Map . put ( value , :__sync_key__ , sanitise_key ( key ) ) }
373+
374+ msg ->
375+ msg
376+ end )
377+ end
378+ end
379+
380+ defp sanitise_key ( key ) do
381+ key |> String . replace ( "\" " , "" ) |> String . replace ( " " , "%20" )
382+ end
383+
289384 defp live_stream_message (
290385 % Message.ChangeMessage { headers: % { operation: :insert } , value: value } ,
291386 acc
@@ -313,7 +408,7 @@ if Code.ensure_loaded?(Phoenix.Component) do
313408 raise error
314409 end
315410
316- defp update_mode ( { updates , resume } , { client , name , query , pid , component } ) do
411+ defp update_mode ( { updates , resume } , { client , name , query , shape , pid , component } ) do
317412 # need to send every update as a separate message.
318413
319414 for event <- updates |> Enum . reverse ( ) |> Enum . map ( & wrap_msg ( & 1 , name , component ) ) ,
@@ -323,7 +418,8 @@ if Code.ensure_loaded?(Phoenix.Component) do
323418
324419 Task . start_link ( fn ->
325420 client
326- |> Electric.Client . stream ( query , resume: resume , replica: :full )
421+ |> Electric.Client . stream ( shape , resume: resume , replica: :full )
422+ |> keyed_stream ( query )
327423 |> Stream . each ( & send_live_event ( & 1 , pid , name , component ) )
328424 |> Stream . run ( )
329425 end )
0 commit comments