Class: Datastar::Dispatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/datastar/dispatcher.rb

Overview

The Dispatcher encapsulates the logic of handling a request and building a response with streaming datastar messages. You’ll normally instantiate a Dispatcher in your controller action of Rack handler via Datastar.new.

Examples:


datastar = Datastar.new(request:, response:, view_context: self)

# One-off fragment response
datastar.patch_elements(template)

# Streaming response with multiple messages
datastar.stream do |sse|
  sse.patch_elements(template)
  10.times do |i|
    sleep 0.1
    sse.patch_signals(count: i)
  end
end

Constant Summary collapse

BLANK_BODY =
[].freeze
SSE_CONTENT_TYPE =
'text/event-stream'
SSE_ACCEPT_EXP =
/text\/event-stream/
HTTP_ACCEPT =
'HTTP_ACCEPT'
HTTP1 =
'HTTP/1.1'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(request:, response: nil, view_context: nil, executor: Datastar.config.executor, error_callback: Datastar.config.error_callback, finalize: Datastar.config.finalize, heartbeat: Datastar.config.heartbeat) ⇒ Dispatcher

Returns a new instance of Dispatcher.

Parameters:

  • request (Hash)

    a customizable set of options

  • response (Hash) (defaults to: nil)

    a customizable set of options

  • view_context (Hash) (defaults to: nil)

    a customizable set of options

  • executor (Hash) (defaults to: Datastar.config.executor)

    a customizable set of options

  • error_callback (Hash) (defaults to: Datastar.config.error_callback)

    a customizable set of options

  • finalize (Hash) (defaults to: Datastar.config.finalize)

    a customizable set of options

  • heartbeat (Hash) (defaults to: Datastar.config.heartbeat)

    a customizable set of options

Options Hash (request:):

  • the (Rack::Request)

    request object

Options Hash (response:):

  • the (Rack::Response, nil)

    response object

Options Hash (view_context:):

  • the (Object, nil)

    view context object, to use when rendering templates. Ie. a controller, or Sinatra app.

Options Hash (executor:):

  • the (Object)

    executor object to use for managing threads and queues

Options Hash (error_callback:):

  • the (Proc)

    callback to call when an error occurs

Options Hash (finalize:):

  • the (Proc)

    callback to call when the response is finalized

Options Hash (heartbeat:):

  • the (Integer, nil, FalseClass)

    heartbeat interval in seconds

Raises:

  • (ArgumentError)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/datastar/dispatcher.rb', line 40

def initialize(
  request:,
  response: nil,
  view_context: nil,
  executor: Datastar.config.executor,
  error_callback: Datastar.config.error_callback,
  finalize: Datastar.config.finalize,
  heartbeat: Datastar.config.heartbeat
)
  @on_connect = []
  @on_client_disconnect = []
  @on_server_disconnect = []
  @on_error = [error_callback]
  @finalize = finalize
  @streamers = []
  @queue = nil
  @executor = executor
  @view_context = view_context
  @request = request
  @response = Rack::Response.new(BLANK_BODY, 200, response&.headers || {})
  @response.content_type = SSE_CONTENT_TYPE
  @response.headers['Cache-Control'] = 'no-cache'
  @response.headers['Connection'] = 'keep-alive' if @request.env['SERVER_PROTOCOL'] == HTTP1
  # Disable response buffering in NGinx and other proxies
  @response.headers['X-Accel-Buffering'] = 'no'
  @response.delete_header 'Content-Length'
  @executor.prepare(@response)
  raise ArgumentError, ':heartbeat must be a number' if heartbeat && !heartbeat.is_a?(Numeric)

  @heartbeat = heartbeat
  @heartbeat_on = false
end

Instance Attribute Details

#requestObject (readonly)

Returns the value of attribute request.



31
32
33
# File 'lib/datastar/dispatcher.rb', line 31

def request
  @request
end

#responseObject (readonly)

Returns the value of attribute response.



31
32
33
# File 'lib/datastar/dispatcher.rb', line 31

def response
  @response
end

Instance Method Details

#execute_script(script, options = BLANK_OPTIONS) ⇒ Object

One-off execute script in the UI See data-star.dev/reference/sse_events#datastar-execute-script

Examples:


datastar.execute_scriprt(%(alert('Hello World!'))

Parameters:

  • script (String)

    the script to execute

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



190
191
192
193
194
# File 'lib/datastar/dispatcher.rb', line 190

def execute_script(script, options = BLANK_OPTIONS)
  stream_no_heartbeat do |sse|
    sse.execute_script(script, options)
  end
end

#on_client_disconnect(callable = nil, &block) ⇒ self

Register a callback for client disconnection Ex. when the browser is closed mid-stream

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Returns:

  • (self)


93
94
95
96
# File 'lib/datastar/dispatcher.rb', line 93

def on_client_disconnect(callable = nil, &block)
  @on_client_disconnect << (callable || block)
  self
end

#on_connect(callable = nil) {|sse| ... } ⇒ self

Register an on-connect callback Triggered when the request is handled

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Yield Parameters:

Returns:

  • (self)


84
85
86
87
# File 'lib/datastar/dispatcher.rb', line 84

def on_connect(callable = nil, &block)
  @on_connect << (callable || block)
  self
end

#on_error(callable = nil, &block) ⇒ self

Register a callback server-side exceptions Ex. when one of the server threads raises an exception

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Returns:

  • (self)


111
112
113
114
# File 'lib/datastar/dispatcher.rb', line 111

def on_error(callable = nil, &block)
  @on_error << (callable || block)
  self
end

#on_server_disconnect(callable = nil, &block) ⇒ self

Register a callback for server disconnection Ex. when the server finishes serving the request

Parameters:

  • callable (Proc, nil) (defaults to: nil)

    the callback to call

Returns:

  • (self)


102
103
104
105
# File 'lib/datastar/dispatcher.rb', line 102

def on_server_disconnect(callable = nil, &block)
  @on_server_disconnect << (callable || block)
  self
end

#patch_elements(elements, options = BLANK_OPTIONS) ⇒ Object

Examples:


datastar.patch_elements(%(<div id="foo">\n<span>hello</span>\n</div>\n))
# or a Phlex view object
datastar.patch_elements(UserComponet.new)

Parameters:

  • elements (String, #call(view_context: Object) => Object)

    the HTML elements or object

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



133
134
135
136
137
# File 'lib/datastar/dispatcher.rb', line 133

def patch_elements(elements, options = BLANK_OPTIONS)
  stream_no_heartbeat do |sse|
    sse.patch_elements(elements, options)
  end
end

#patch_signals(signals, options = BLANK_OPTIONS) ⇒ Object

One-off patch signals in the UI See data-star.dev/reference/sse_events#datastar-patch-signals

Examples:


datastar.patch_signals(count: 1, toggle: true)

Parameters:

  • signals (Hash, String)

    signals to merge

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



162
163
164
165
166
# File 'lib/datastar/dispatcher.rb', line 162

def patch_signals(signals, options = BLANK_OPTIONS)
  stream_no_heartbeat do |sse|
    sse.patch_signals(signals, options)
  end
end

#redirect(url) ⇒ Object

Send an execute_script event to change window.location

Parameters:

  • url (String)

    the URL or path to redirect to



200
201
202
203
204
# File 'lib/datastar/dispatcher.rb', line 200

def redirect(url)
  stream_no_heartbeat do |sse|
    sse.redirect(url)
  end
end

#remove_elements(selector, options = BLANK_OPTIONS) ⇒ Object

One-off remove elements from the UI Sugar on top of patch-elements with mode: ‘remove’ See data-star.dev/reference/sse_events#datastar-patch-elements

Examples:


datastar.remove_elements('#users')

Parameters:

  • selector (String)

    a CSS selector for the fragment to remove

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



148
149
150
151
152
# File 'lib/datastar/dispatcher.rb', line 148

def remove_elements(selector, options = BLANK_OPTIONS)
  stream_no_heartbeat do |sse|
    sse.remove_elements(selector, options)
  end
end

#remove_signals(paths, options = BLANK_OPTIONS) ⇒ Object

One-off remove signals from the UI See data-star.dev/reference/sse_events#datastar-remove-signals

Examples:


datastar.remove_signals(['user.name', 'user.email'])

Parameters:

  • paths (Array<String>)

    object paths to the signals to remove

  • options (Hash) (defaults to: BLANK_OPTIONS)

    the options to send with the message



176
177
178
179
180
# File 'lib/datastar/dispatcher.rb', line 176

def remove_signals(paths, options = BLANK_OPTIONS)
  stream_no_heartbeat do |sse|
    sse.remove_signals(paths, options)
  end
end

#signalsHash

Parse and returns Datastar signals sent by the client. See data-star.dev/guide/getting_started#data-signals

Returns:

  • (Hash)


119
120
121
# File 'lib/datastar/dispatcher.rb', line 119

def signals
  @signals ||= parse_signals(request).freeze
end

#sse?Boolean

Check if the request accepts SSE responses

Returns:

  • (Boolean)


75
76
77
# File 'lib/datastar/dispatcher.rb', line 75

def sse?
  !!(@request.get_header(HTTP_ACCEPT).to_s =~ SSE_ACCEPT_EXP)
end

#stream(streamer = nil) {|sse| ... } ⇒ Object

Start a streaming response A generator object is passed to the block The generator supports all the Datastar methods listed above (it’s the same type) But you can call them multiple times to send multiple messages down an open SSE connection. This methods also captures exceptions raised in the block and triggers any error callbacks. Client disconnection errors trigger the @on_client_disconnect callbacks. Finally, when the block is done streaming, the @on_server_disconnect callbacks are triggered.

When multiple streams are scheduled this way, this SDK will spawn each block in separate threads (or fibers, depending on executor) and linearize their writes to the connection socket As a last step, the finalize callback is called with the view context and the response This is so that different frameworks can setup their responses correctly. By default, the built-in Rack finalzer just returns the resposne Array which can be used by any Rack handler. On Rails, the Rails controller response is set to this objects streaming response.

Examples:


datastar.stream do |sse|
  total = 300
  sse.patch_elements(%(<progress data-signal-progress="0" id="progress" max="#{total}" data-attr-value="$progress">0</progress>))
  total.times do |i|
    sse.patch_signals(progress: i)
  end
end

datastar.stream do |sse|
  # update things here
end

datastar.stream do |sse|
  # more concurrent updates here
end

Parameters:

Yield Parameters:

Returns:

  • (Object)

    depends on the finalize callback



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/datastar/dispatcher.rb', line 245

def stream(streamer = nil, &block)
  streamer ||= block
  @streamers << streamer
  if @heartbeat && !@heartbeat_on
    @heartbeat_on = true
    @streamers << proc do |sse|
      while true
        sleep @heartbeat
        sse.check_connection!
      end
    end
  end

  body = if @streamers.size == 1
    stream_one(streamer) 
  else
    stream_many(streamer) 
  end

  @response.body = body
  @finalize.call(@view_context, @response)
end