The document discusses using Sinatra and Ruby to build web applications that utilize asynchronous JavaScript and XMLHttpRequest (AJAX) techniques. It demonstrates how to make HTTP requests to a Sinatra backend from JavaScript using XMLHttpRequest, Fetch API promises, and DOM manipulation. Various timing functions like setInterval and setTimeout are also explored. The document contains sample code for building a basic Sinatra API and incrementally enhancing the frontend JavaScript code to retrieve and display responses asynchronously.
2. pay attention!
all code is BSD 2-clause licensed
any resemblance to actual code &
conceptstm, living or dead, is probably
your imagination playing tricks on you
if you can make money from it you're
doing a damn sight better than we are!
5. require 'sinatra'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :message, ARGV[0] || "Hello World"
get '/' do
settings.message
end
6. require 'sinatra'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :message, ARGV[0] || "Hello World"
get '/' do
erb :html_02, locals: { message: settings.message }
end
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>TBE:Ruby HTML</title>
</head>
<body class='container'>
<h1>TBE:Ruby HTML</h1>
<div>
<%= message %>
</div>
</body>
</html>
9. AJAX
Asynchronous JavaScript and XML
•JavaScript is a single-threaded language
•but browsers are event-driven environments
•so JavaScript runtimes normally have three basic threads
•one to run the main script
•one to run scripts for high priority events
•one to run scripts for low priority events
•and each event can have callbacks de
fi
ned for it
10. AJAX
Asynchronous JavaScript and XML
•XMLHttpRequest
fi
rst appeared in MSXML
•available in IE5 as an ActiveX component from 1999
•similar functionality in other browsers from 2000 onwards
•fully supported in IE 7 2006
•despite its name it isn't restricted to XML
•most modern uses involve JSON
11. require 'sinatra'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :commands, ["A", "B", "C"]
get '/' do
erb :html_03, locals: { commands: settings.commands }
end
get '/:command' do
params[:command]
end
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>TBE:Ruby AJAX</title>
<script>
const element = (e = "event_log") => { return document.getElementById(e) };
const print = m => { element().innerHTML += `<div>${m}</div>` };
<% commands.each do |c, v| %>
function <%= c %>() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
print(this.responseText);
}
};
xhttp.open("GET", "<%= c %>", true);
xhttp.send();
}
<% end %>
</script>
</head>
<body>
<h1>TBE:Ruby AJAX</h1>
<h2>Actions</h2>
<div>
<% commands.each do |c, v| %>
<span>
<button type="button" onclick="<%= c %>();"><%= c %></button>
</span>
<% end %>
</div>
<h2>Server Output</h2>
<div id='event_log'></div>
</body>
</html>
12. require 'sinatra'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :commands, ["A", "B", "C"]
get '/' do
erb :html_04, locals: { commands: settings.commands }
end
get '/:command' do
if settings.commands.include? params[:command]
params[:command]
else
halt 404
end
end
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>TBE:Ruby AJAX</title>
<script>
const element = (e = "event_log") => { return document.getElementById(e) };
const print = m => { element().innerHTML += `<div>${m}</div>` };
function sendCommand(c) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
print(this.responseText);
} else {
print(`Request Failed: ${this.status}`);
}
}
};
xhttp.open("GET", c, true);
xhttp.send();
}
</script>
</head>
<body>
<h1>TBE:Ruby AJAX</h1>
<h2>Actions</h2>
<div>
<% commands.each do |c, v| %>
<span>
<button type="button" onclick="sendCommand('<%= c %>');"><%= c %></button>
</span>
<% end %>
</div>
<h2>Server Output</h2>
<div id='event_log'></div>
</body>
</html>
13. require 'sinatra'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :commands, ["A", "B", "C"]
get '/' do
erb :html_05, locals: { commands: settings.commands }
end
get '/:command' do
if settings.commands.include? params[:command]
params[:command]
else
halt 404
end
end
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>TBE:Ruby AJAX</title>
<script>
const element = (e = "event_log") => { return document.getElementById(e) };
const print = m => { element().innerHTML += `<div>${m}</div>` };
function sendCommand(c) {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", c, false);
xhttp.send();
if (xhttp.status == 200) {
print(xhttp.responseText);
} else {
print(`Request Failed: ${xhttp.status}`);
}
}
</script>
</head>
<body>
<h1>TBE:Ruby AJAX</h1>
<h2>Actions</h2>
<div>
<% commands.each do |c, v| %>
<span>
<button type="button" onclick="sendCommand('<%= c %>');"><%= c %></button>
</span>
<% end %>
<span>
<button type="button" onclick="sendCommand('D');">D</button>
</span>
</div>
<h2>Server Output</h2>
<div id='event_log'></div>
</body>
</html>
27. require 'sinatra'
require 'sinatra-websocket'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :socket_url, ENV['SOCKET'] || '/'
set :commands, ["A", "B", "C"]
get '/' do
if !request.websocket?
erb :html_15, locals: {
url: settings.socket_url,
commands: settings.commands }
else
request.websocket do |ws|
ws.onopen do
warn "websocket connected"
ws.send "connection established"
end
ws.onmessage do |m|
warn "received: #{m}n"
if settings.commands.include? m
ws.send m
else
ws.send "unknown request #{m}"
end
end
ws.onclose do
warn "socket closed #{ws}"
end
end
end
end
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>TBE:Ruby WebSocket</title>
<script>
const element = (e = "event_log") => { return document.getElementById(e) };
const print = m => { element().innerHTML += `<div>${m}</div>` };
const addButton = (n, c) => {
element("action_buttons").appendChild(newButton(n, c)) };
function newButton(n, c) {
let b = document.createElement("BUTTON");
b.onclick = c;
b.appendChild(document.createTextNode(n));
return b;
}
window.onload = () => {
var socket = new WebSocket(`ws://${location.host}<%= url %>`);
socket.onopen = (e) => print("opening socket: <%= url %>");
socket.onclose = (e) => print("closing socket: <%= url %>");
socket.onerror = (e) => print(e.message);
socket.onmessage = (m) => { print(m.data) };
<% commands.each do |c, v| %>
addButton('<%= c %>', () => socket.send('<%= c %>'));
<% end %>
addButton('D', () => socket.send('D'));
</script>
</head>
<body>
<h1>TBE:Ruby WebSocket</h1>
<h2>Actions</h2>
<div id="action_buttons"></div>
<h2>Server Output</h2>
<div id='event_log'></div>
</body>
</html>
28. require 'sinatra'
require 'sinatra-websocket'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :socket_url, ENV['SOCKET'] || '/'
set :commands, ["A", "B", "C"]
set :sockets, []
get '/' do
if !request.websocket?
erb :html_16, locals: { url: settings.socket_url, commands: settings.commands }
else
request.websocket do |ws|
ws.onopen do
warn "websocket #{ws} connected"
m = "connection opened: #{ws.object_id}"
settings.sockets.each { |s| s.send m }
settings.sockets << ws
ws.send "connection established: #{ws.object_id}"
end
ws.onmessage do |m|
if settings.commands.include? m
EM.next_tick do
ws.send "message sent: #{m}"
m = "message received from #{ws.object_id}: #{m}"
settings.sockets.each { |s| s.send m unless s == ws }
end
else
ws.send "unknown command: #{m}"
end
end
ws.onclose do
warn "socket closed #{ws}"
settings.sockets.delete ws
m = "connection closed: #{ws.object_id}"
settings.sockets.each { |s| s.send m }
end
end
end
end
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>TBE:Ruby WebSocket</title>
<script>
const element = (e = "event_log") => { return document.getElementById(e) };
const print = m => { element().innerHTML += `<div>${m}</div>` };
const addButton = (n, c) => {
element("action_buttons").appendChild(newButton(n, c)) };
function newButton(n, c) {
let b = document.createElement("BUTTON");
b.onclick = c;
b.appendChild(document.createTextNode(n));
return b;
}
window.onload = () => {
var socket = new WebSocket(`ws://${location.host}<%= url %>`);
socket.onopen = (e) => print("opening socket: <%= url %>");
socket.onclose = (e) => print("closing socket: <%= url %>");
socket.onerror = (e) => print(e.message);
socket.onmessage = (m) => { print(m.data) };
<% commands.each do |c, v| %>
addButton('<%= c %>', () => socket.send('<%= c %>'));
<% end %>
addButton('D', () => socket.send('D'));
</script>
</head>
<body>
<h1>TBE:Ruby WebSocket</h1>
<h2>Actions</h2>
<div id="action_buttons"></div>
<h2>Server Output</h2>
<div id='event_log'></div>
</body>
</html>
29. require 'sinatra'
require 'sinatra-websocket'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :socket_url, ENV['SOCKET'] || '/'
set :commands, ["A", "B", "C"]
set :sockets, []
get '/' do
if !request.websocket?
erb :html_17, locals: { url: settings.socket_url, commands: settings.commands }
else
request.websocket do |ws|
ws.onopen do
warn "websocket #{ws} connected"
m = "connection opened: #{ws.object_id}"
settings.sockets.each { |s| s.send m }
settings.sockets << ws
ws.send "connection established: #{ws.object_id}"
end
ws.onmessage do |m|
if settings.commands.include? m
EM.next_tick do
ws.send "message sent: #{m}"
m = "message received from #{ws.object_id}: #{m}"
settings.sockets.each { |s| s.send m unless s == ws }
end
else
ws.send "unknown command: #{m}"
end
end
ws.onclose do
warn "socket closed #{ws}"
settings.sockets.delete ws
m = "connection closed: #{ws.object_id}"
settings.sockets.each { |s| s.send m }
end
end
end
end
require 'websocket-eventmachine-client'
server = ENV['SERVER'] || '127.0.0.1:3000'
server_url = "ws://#{server}"
EM.run do
puts "connecting to server: #{server_url}"
ws = WebSocket::EventMachine::Client.connect :uri => server_url
puts ws
ws.onopen do
puts "connected: #{server}"
end
ws.onmessage do |m, t|
puts m
end
ws.onerror do |e|
puts "error: #{e}"
end
ws.onclose do |c, m|
puts "disconnected: #{server} (#{c}, #{m})"
exit
end
end
30. require 'sinatra'
require 'sinatra-websocket'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :socket_url, ENV['SOCKET'] || '/'
set :commands, ["A", "B", "C"]
set :sockets, []
get '/' do
if !request.websocket?
erb :html_18, locals: { url: settings.socket_url, commands: settings.commands }
else
request.websocket do |ws|
ws.onopen do
warn "websocket #{ws} connected"
m = "connection opened: #{ws.object_id}"
settings.sockets.each { |s| s.send m }
settings.sockets << ws
ws.send "connection established: #{ws.object_id}"
end
ws.onmessage do |m|
if settings.commands.include? m
EM.next_tick do
ws.send "message sent: #{m}"
m = "message received from #{ws.object_id}: #{m}"
settings.sockets.each { |s| s.send m unless s == ws }
end
else
ws.send "unknown command: #{m}"
end
end
ws.onclose do
warn "socket closed #{ws}"
settings.sockets.delete ws
m = "connection closed: #{ws.object_id}"
settings.sockets.each { |s| s.send m }
end
end
end
end
require 'websocket-eventmachine-client'
server = ENV['SERVER'] || 'localhost:3000'
heartbeat = ENV['HEARTBEAT'] || 2
commands = ["A", "B", "C", "D"]
server_url = "ws://#{server}"
connected = false
def rotate_buffer b
r = b.shift
b << r
r
end
EM.run do
puts "connecting to server: #{server_url}"
ws = WebSocket::EventMachine::Client.connect(:uri => "ws://127.0.0.1:3000")
puts ws
ws.onopen do
puts "connected: #{server}"
connected = true
end
ws.onmessage do |m, t|
puts m
end
ws.onerror do |e|
puts "error: #{e}"
end
ws.onclose do |c, m|
puts "disconnected: #{server} (#{c}, #{m})"
end
timer = EventMachine::PeriodicTimer.new(heartbeat) do
ws.send rotate_buffer(commands) if connected
end
end
31. require 'sinatra'
require 'sinatra-websocket'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :socket_url, ENV['SOCKET'] || '/'
set :commands, [:HEARTBEAT, :PRINT, :ERROR]
set :sockets, []
set :heartbeat, 0
def message id, a, m, e = nil
{ "id" => id, "action" => a, "message" => m, "error" => e }
end
get '/' do
if !request.websocket?
erb :html_19, locals: { url: settings.socket_url, commands: settings.commands }
else
request.websocket do |ws|
ws.onopen do
warn "websocket #{ws} connected"
m = message 0, :PRINT, "connection opened: #{ws.object_id}"
settings.sockets.each { |s| s.send m.to_json }
ws.send message(0, :PRINT, "connection established: #{ws.object_id}").to_json
ws.send message(0, :HEARTBEAT, settings.heartbeat).to_json
settings.sockets << ws
end
ws.onmessage do |m|
m = JSON.parse m
m.merge! "id" => ws.object_id
case m["action"].to_sym
when :HEARTBEAT
settings.heartbeat = m["message"].to_i
EM.next_tick do
settings.sockets.each { |s| s.send m.to_json }
end
when :PRINT
EM.next_tick do
settings.sockets.each { |s| s.send m.to_json if s != ws }
end
else
ws.send message(0, :ERROR, m["message"], m["action"]).to_json
end
end
ws.onclose do
warn "socket closed #{ws}"
settings.sockets.delete ws
m = message(0, :PRINT, "connection closed: #{ws.object_id}").to_json
settings.sockets.each { |s| s.send m }
end
end
end
end
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>WebSocket EXAMPLE</title>
<script>
const element = (e = "event_log") => { return document.getElementById(e) };
const print = m => { element().innerHTML += `<div>${m}</div>` };
const addButton = (n, c) => { element("action_buttons").appendChild(newButton(n, c)) };
const sendMessage = (s, a, m) => {
s.send(JSON.stringify({ id: null, action: a, message: m }))
};
function newButton(n, c) {
let b = document.createElement("BUTTON");
b.onclick = c;
b.appendChild(document.createTextNode(n));
return b;
}
window.onload = () => {
var heartbeat = 0;
var socket = new WebSocket(`ws://${location.host}<%= url %>`);
socket.onopen = (e) => print("opening socket: <%= url %>");
socket.onclose = (e) => print("closing socket: <%= url %>");
socket.onerror = (e) => print(e.message);
socket.onmessage = (m) => {
let d = JSON.parse(m.data);
switch (d.action.toUpperCase()) {
case 'HEARTBEAT':
heartbeat = d.message;
element("counter").innerHTML = heartbeat;
print(`${d.id}: ${d.action} ${heartbeat}`);
break;
case 'PRINT':
print(`${d.id}: ${d.message}`);
break;
}
};
[0, 10, 100].forEach(i => {
addButton(`HEARTBEAT ${i}`, () => sendMessage(socket, 'HEARTBEAT', i));
});
}
</script>
</head>
<body>
<h1>WebSocket EXAMPLE</h1>
<h2>Actions</h2>
<div id="heartbeat">
HEARTBEAT: <span id="counter">0</span>
</div>
<div id="action_buttons"></div>
<h2>Server Output</h2>
<div id='event_log'></div>
</body>
</html>
32. require 'sinatra'
require 'sinatra-websocket'
set :server, %w[ thin mongrel webrick ]
set :bind, ENV['IP_ADDRESS'] || '0.0.0.0'
set :port, ENV['PORT'] || 3000
set :socket_url, ENV['SOCKET'] || '/'
set :commands, [:HEARTBEAT, :PRINT, :ERROR]
set :sockets, []
set :heartbeat, 0
def message id, a, m, e = nil
{ "id" => id, "action" => a, "message" => m, "error" => e }
end
get '/' do
if !request.websocket?
erb :html_19, locals: { url: settings.socket_url, commands: settings.commands }
else
request.websocket do |ws|
ws.onopen do
warn "websocket #{ws} connected"
m = message 0, :PRINT, "connection opened: #{ws.object_id}"
settings.sockets.each { |s| s.send m.to_json }
ws.send message(0, :PRINT, "connection established: #{ws.object_id}").to_json
ws.send message(0, :HEARTBEAT, settings.heartbeat).to_json
settings.sockets << ws
end
ws.onmessage do |m|
m = JSON.parse m
m.merge! "id" => ws.object_id
case m["action"].to_sym
when :HEARTBEAT
settings.heartbeat = m["message"].to_i
EM.next_tick do
settings.sockets.each { |s| s.send m.to_json }
end
when :PRINT
EM.next_tick do
settings.sockets.each { |s| s.send m.to_json if s != ws }
end
else
ws.send message(0, :ERROR, m["message"], m["action"]).to_json
end
end
ws.onclose do
warn "socket closed #{ws}"
settings.sockets.delete ws
m = message(0, :PRINT, "connection closed: #{ws.object_id}").to_json
settings.sockets.each { |s| s.send m }
end
end
end
end
require 'websocket-eventmachine-client'
require 'json'
server = ENV['SERVER'] || '127.0.0.1:3000'
heartbeat = ENV['HEARTBEAT'] || 1
server_url = "ws://#{server}"
connected = false
count = 0
EM.run do
puts "connecting to server: #{server_url}"
ws = WebSocket::EventMachine::Client.connect(:uri => server_url)
puts ws
ws.onopen do
puts "connected: #{server}"
connected = true
end
ws.onmessage do |m, t|
v = JSON.parse(m)
case v["action"].to_sym
when :PRINT
puts "#{v["id"]}: #{v["message"]}"
when :HEARTBEAT
count = v["message"].to_i
puts "#{v["id"]}: HEARTBEAT ==> #{count}"
when :ERROR
puts "ERROR: #{v["error"]} ==> #{v["message"]}"
end
end
ws.onerror do |e|
puts "error: #{e}"
end
ws.onclose do |c, m|
puts "disconnected: #{server} (#{c}, #{m})"
exit
end
timer = EventMachine::PeriodicTimer.new(heartbeat) do
count += 1
ws.send({ "action" => 'HEARTBEAT', "message" => count }.to_json) if connected
end
end