@@ -56,6 +56,7 @@ defmodule Electric.Shapes.Api do
5656 stale_age: 300 ,
5757 send_cache_headers?: true ,
5858 encoder: Electric.Shapes.Api.Encoder.JSON ,
59+ sse_encoder: Electric.Shapes.Api.Encoder.SSE ,
5960 configured: false
6061 ]
6162
@@ -484,7 +485,7 @@ defmodule Electric.Shapes.Api do
484485 if live? && Enum . take ( log , 1 ) == [ ] do
485486 request
486487 |> update_attrs ( % { ot_is_immediate_response: false } )
487- |> hold_until_change ( )
488+ |> handle_live_request ( )
488489 else
489490 up_to_date_lsn =
490491 if live? do
@@ -497,9 +498,9 @@ defmodule Electric.Shapes.Api do
497498 max ( global_last_seen_lsn , chunk_end_offset . tx_offset )
498499 end
499500
500- body = Stream . concat ( [ log , maybe_up_to_date ( request , up_to_date_lsn ) ] )
501+ log_stream = Stream . concat ( log , maybe_up_to_date ( request , up_to_date_lsn ) )
501502
502- % { response | chunked: true , body: encode_log ( request , body ) }
503+ % { response | chunked: true , body: encode_log ( request , log_stream ) }
503504 end
504505
505506 { :error , error } ->
@@ -513,6 +514,13 @@ defmodule Electric.Shapes.Api do
513514 end
514515 end
515516
517+ defp handle_live_request ( % Request { params: % { experimental_live_sse: true } } = request ) do
518+ stream_sse_events ( request )
519+ end
520+ defp handle_live_request ( % Request { } = request ) do
521+ hold_until_change ( request )
522+ end
523+
516524 defp hold_until_change ( % Request { } = request ) do
517525 % {
518526 new_changes_ref: ref ,
@@ -549,10 +557,107 @@ defmodule Electric.Shapes.Api do
549557 end
550558 end
551559
552- defp clean_up_change_listener ( % Request { handle: shape_handle } = request )
553- when not is_nil ( shape_handle ) do
554- % { api: % { registry: registry } } = request
555- Registry . unregister ( registry , shape_handle )
560+ defp stream_sse_events ( % Request { } = request ) do
561+ % {
562+ new_changes_ref: ref ,
563+ handle: shape_handle ,
564+ api: % { sse_timeout: sse_timeout }
565+ } = request
566+
567+ Logger . debug ( "Client #{ inspect ( self ( ) ) } is streaming SSE for changes to #{ shape_handle } " )
568+
569+ # Set up timer for SSE timeout
570+ timer_ref = Process . send_after ( self ( ) , { :sse_timeout , ref } , sse_timeout )
571+
572+ # Stream changes as SSE events for the duration of the timer.
573+ sse_event_stream = Stream . resource (
574+ fn ->
575+ request
576+ end ,
577+ & next_sse_event / 1 ,
578+ fn _ ->
579+ Process . cancel_timer ( timer_ref )
580+ end
581+ )
582+
583+ response = % { request . response | chunked: true , body: sse_event_stream }
584+
585+ % { response | trace_attrs: Map . put ( response . trace_attrs || % { } , :ot_is_sse_response , true ) }
586+ end
587+
588+ defp next_sse_event ( :done ) , do: { :halt , :done }
589+
590+ defp next_sse_event ( % Request { } = request ) do
591+ % {
592+ api: api ,
593+ handle: shape_handle ,
594+ new_changes_ref: ref
595+ } = request
596+
597+ receive do
598+ { ^ ref , :new_changes , latest_log_offset } ->
599+ updated_request =
600+ % { request | last_offset: latest_log_offset }
601+ |> determine_global_last_seen_lsn ( )
602+ |> determine_log_chunk_offset ( )
603+ |> determine_up_to_date ( )
604+
605+ case Shapes . get_merged_log_stream (
606+ updated_request . api ,
607+ shape_handle ,
608+ since: updated_request . params . offset ,
609+ up_to: updated_request . chunk_end_offset
610+ ) do
611+ { :ok , log } ->
612+ up_to_date_lsn = updated_request . chunk_end_offset . tx_offset
613+ up_to_date_messages = maybe_up_to_date ( updated_request , up_to_date_lsn )
614+
615+ message_stream = Stream . concat ( log , up_to_date_messages )
616+ messages = Enum . to_list ( encode_log ( updated_request , message_stream ) )
617+
618+ { messages , updated_request }
619+
620+ { :error , _error } ->
621+ { [ ] , request }
622+ end
623+
624+ { ^ ref , :shape_rotation } ->
625+ must_refetch = % { headers: % { control: "must-refetch" } }
626+ message = encode_message ( api , must_refetch )
627+
628+ { message , :done }
629+
630+ { :sse_timeout , ^ ref } ->
631+ { [ ] , :done }
632+ end
633+ end
634+
635+ defp clean_up_change_listener ( % Request { handle: shape_handle } = request ) when not is_nil ( shape_handle ) do
636+ % {
637+ api: % {
638+ registry: registry ,
639+ sse_timeout: sse_timeout
640+ } ,
641+ params: % {
642+ live: live? ,
643+ experimental_live_sse: live_sse?
644+ }
645+ } = request
646+
647+ # When handling SSE requests, the response body is a stream that listens for
648+ # :new_changes events. If we unregister the shape_handle event listener immediately,
649+ # we don't receive the events. So, in this case, we unregister the shape_handle
650+ # listener after the sse_timeout, when we can be sure that the request is over.
651+ if live? and live_sse? do
652+ spawn ( fn ->
653+ :timer . sleep ( sse_timeout )
654+
655+ Registry . unregister ( registry , shape_handle )
656+ end )
657+ else
658+ Registry . unregister ( registry , shape_handle )
659+ end
660+
556661 request
557662 end
558663
@@ -600,6 +705,10 @@ defmodule Electric.Shapes.Api do
600705 def stack_id ( % Api { stack_id: stack_id } ) , do: stack_id
601706 def stack_id ( % { api: % { stack_id: stack_id } } ) , do: stack_id
602707
708+ defp encode_log ( % Request { api: api , params: % { live: true , experimental_live_sse: true } } , stream ) do
709+ encode_sse ( api , :log , stream )
710+ end
711+
603712 defp encode_log ( % Request { api: api } , stream ) do
604713 encode ( api , :log , stream )
605714 end
@@ -609,6 +718,10 @@ defmodule Electric.Shapes.Api do
609718 encode ( api , :message , message )
610719 end
611720
721+ def encode_message ( % Request { api: api , params: % { live: true , experimental_live_sse: true } } , message ) do
722+ encode_sse ( api , :message , message )
723+ end
724+
612725 def encode_message ( % Request { api: api } , message ) do
613726 encode ( api , :message , message )
614727 end
@@ -617,6 +730,10 @@ defmodule Electric.Shapes.Api do
617730 apply ( encoder , type , [ message ] )
618731 end
619732
733+ defp encode_sse ( % Api { sse_encoder: sse_encoder } , type , message ) when type in [ :message , :log ] do
734+ apply ( sse_encoder , type , [ message ] )
735+ end
736+
620737 def schema ( % Response {
621738 api: % Api { inspector: inspector } ,
622739 shape_definition: % Shapes.Shape { } = shape
0 commit comments