Class: DuoRuby::Server

Inherits:
Channel show all
Defined in:
lib/duoruby/server.rb,
lib/duoruby/server/frontend_compiler.rb

Overview

Application server built on Falcon and Async.

Server handles three HTTP routes:

  • GET / — serves an HTML shell page that loads the frontend script

  • GET /duoruby/app.js — compiles and serves the Opal frontend on demand

  • GET /duoruby/socket — upgrades to a WebSocket and drives message handlers

Subclasses can declare message handlers with on and can override #call for custom HTTP routes before delegating to super.

Examples:

Starting the server from application code

DuoRuby::Server.build(root: __dir__, port: 3000).run

Defined Under Namespace

Classes: FrontendCompiler

Constant Summary collapse

SOCKET_PATH =

Path that the browser WebSocket connects to.

"/duoruby/socket"
SCRIPT_PATH =

Path from which the compiled frontend JavaScript is served.

"/duoruby/app.js"

Instance Attribute Summary collapse

Attributes inherited from Channel

#handlers

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Channel

#channel, #dispatch, #handler_for, inherited, #off, #on, #one

Methods included from Channel::HandlerMethods

#channel, #handlers, included, #included, #off, #on, #one

Constructor Details

#initialize(root: Dir.pwd, host: "127.0.0.1", port: 9292) ⇒ Server

Returns a new instance of Server.

Parameters:

  • root (String) (defaults to: Dir.pwd)

    the application root directory

  • host (String) (defaults to: "127.0.0.1")

    the hostname or IP to bind to (default: “127.0.0.1”)

  • port (Integer, String) (defaults to: 9292)

    the port to listen on (default: 9292)



53
54
55
56
57
58
# File 'lib/duoruby/server.rb', line 53

def initialize(root: Dir.pwd, host: "127.0.0.1", port: 9292)
  super()
  configure(root: root, host: host, port: port)
  @groups = {}
  @next_client_id = 0
end

Instance Attribute Details

#groupsHash{Symbol => Group} (readonly)

Returns all groups that have been accessed on this server.

Returns:

  • (Hash{Symbol => Group})

    all groups that have been accessed on this server



48
49
50
# File 'lib/duoruby/server.rb', line 48

def groups
  @groups
end

#hostString (readonly)

Returns the bind host.

Returns:

  • (String)

    the bind host



42
43
44
# File 'lib/duoruby/server.rb', line 42

def host
  @host
end

#portInteger (readonly)

Returns the bind port.

Returns:

  • (Integer)

    the bind port



45
46
47
# File 'lib/duoruby/server.rb', line 45

def port
  @port
end

#rootString (readonly)

Returns the expanded application root directory.

Returns:

  • (String)

    the expanded application root directory



39
40
41
# File 'lib/duoruby/server.rb', line 39

def root
  @root
end

Class Method Details

.build(root: Dir.pwd, host: "127.0.0.1", port: 9292) ⇒ Object



60
61
62
63
64
65
# File 'lib/duoruby/server.rb', line 60

def self.build(root: Dir.pwd, host: "127.0.0.1", port: 9292)
  root = File.expand_path(root)
  server = DuoRuby.load_app(:backend, root: root) || new(root: root, host: host, port: port)
  server.configure(root: root, host: host, port: port)
  server
end

Instance Method Details

#authenticate(_client) ⇒ Object



136
137
138
# File 'lib/duoruby/server.rb', line 136

def authenticate(_client)
  true
end

#broadcast(group_name, event, **params) ⇒ Object



151
152
153
# File 'lib/duoruby/server.rb', line 151

def broadcast(group_name, event, **params)
  group(group_name).send(event, **params)
end

#call(request) ⇒ Protocol::HTTP::Response

Rack-compatible request handler. Routes to the appropriate private handler or returns a 404. Catches StandardError and responds with a 500.

Parameters:

  • request (Protocol::HTTP::Request)

Returns:

  • (Protocol::HTTP::Response)


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/duoruby/server.rb', line 83

def call(request)
  path = request.path.to_s.split("?", 2).first

  case path
  when SOCKET_PATH
    websocket(request) || not_found("websocket endpoint")
  when SCRIPT_PATH
    javascript
  when "/", ""
    html
  else
    not_found(path)
  end
rescue StandardError => error
  text(500, "#{error.class}: #{error.message}\n")
end

#configure(root:, host:, port:) ⇒ Object



67
68
69
70
71
72
73
74
75
76
# File 'lib/duoruby/server.rb', line 67

def configure(root:, host:, port:)
  @root = File.expand_path(root)
  config_path = File.join(@root, "duoruby.rb")
  load config_path if File.file?(config_path)
  @host = host
  @port = Integer(port)
  DuoRuby.config.host = @host
  DuoRuby.config.port = @port
  self
end

#connect(id:, writer: nil, metadata: {}, &writer_block) ⇒ Object



128
129
130
131
132
133
134
# File 'lib/duoruby/server.rb', line 128

def connect(id:, writer: nil, metadata: {}, &writer_block)
  client = Client.new(id: id, writer: writer, metadata: , &writer_block)
  return client.reject unless authenticate(client)

  dispatch(:$connect, client)
  client
end

#disconnect(client) ⇒ Object



140
141
142
143
144
145
# File 'lib/duoruby/server.rb', line 140

def disconnect(client)
  dispatch(:$disconnect, client)
  client.cancel_pending_calls
  client.groups.values.each { |group| group.remove(client) }
  client
end

#frontend_javascriptString

Compiles the Opal frontend to a JavaScript string.

Resets Opal’s global path state, adds configured frontend gems, then builds the opal runtime followed by setup/frontend.

Note: this method mutates global Opal state (Opal.reset_paths!) and is not safe to call concurrently.

Returns:

  • (String)

    the concatenated JavaScript



124
125
126
# File 'lib/duoruby/server.rb', line 124

def frontend_javascript
  FrontendCompiler.new(root).call
end

#group(name) ⇒ Object



147
148
149
# File 'lib/duoruby/server.rb', line 147

def group(name)
  groups[name.to_sym] ||= Group.new(name)
end

#receive(client, message) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/duoruby/server.rb', line 155

def receive(client, message)
  message = Message.coerce(message)
  return client.resolve_call(message) if message.event == Message::REPLY_EVENT
  return client.reject_call(message) if message.event == Message::ERROR_EVENT && message.reply_to

  results = dispatch(message.event, client, **message.params)
  client.deliver(Message.reply(message.id, results.last)) if message.id
  results
rescue StandardError => error
  raise unless message&.id

  client.deliver(Message.error(code: error.class.name, message: error.message, reply_to: message.id))
end

#run(output: $stdout) ⇒ Object

Starts the Falcon server and blocks until it exits.

Parameters:

  • output (IO) (defaults to: $stdout)

    where to print the “serving …” banner (default: $stdout)



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/duoruby/server.rb', line 103

def run(output: $stdout)
  endpoint = Async::HTTP::Endpoint.parse("http://#{host}:#{port}")

  Sync do
    task = Falcon::Server.new(self, endpoint).run
    output.puts "serving http://#{host}:#{port}"
    task.wait
  ensure
    task&.stop
  end
end