Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ route do |r|
end
```


### Options

All options are optional.

| Option | Default | Type | Description |
|---|---|---|---|
| `watch` | `[]` | Array of String | Directories to watch for changes |
| `skip` | `[]` | Array of String/Regexp | Paths to ignore |
| `debounce` | `300` | Integer | Debounce interval in milliseconds before broadcasting changes |
| `retry` | not set | Integer | SSE retry interval in milliseconds (not set = browser default ~3s, maximum clamped at 30s) |


## Used in

This gem powers live reloading in [Perron](https://github.com/rails-designer/perron), a Rails-based static site generator. It can be enabled with `config.live_reload = true` in your Perron initializer.
Expand Down
4 changes: 2 additions & 2 deletions lib/mata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ def initialize(app, options = {})
return if ENV["MATA_DISABLED"] == "true"

@watch_tower = WatchTower.new(options)
@broadcaster = Broadcaster.new
@agent = Agent.new
@broadcaster = Broadcaster.new(options)
@agent = Agent.new(options)

@watch_tower.on_change { |files| @broadcaster.broadcast_to_all(files) }

Expand Down
10 changes: 9 additions & 1 deletion lib/mata/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

class Mata
class Agent
def initialize(options = {})
@retry = options[:retry]
end

def insert(status, headers, body)
return [status, headers, body] unless html_response?(headers)

content = extract(body)
return [status, headers, [content]] unless content.include?("</head>")

script_tag = '<script src="/__mata/client.js"></script>'
script_tag = if @retry
%(<script src="/__mata/client.js" data-mata-retry="#{@retry}"></script>)
else
'<script src="/__mata/client.js"></script>'
end
modified_content = content.sub("</head>", "#{script_tag}\n</head>")

headers["Content-Length"] = modified_content.bytesize.to_s if headers["Content-Length"]
Expand Down
4 changes: 3 additions & 1 deletion lib/mata/broadcaster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

class Mata
class Broadcaster
def initialize
def initialize(options = {})
@clients = []
@clients_mutex = Mutex.new
@retry = options[:retry]
@cleanup_thread = cleanup_periodically
end

Expand All @@ -26,6 +27,7 @@ def establish_contact(env)
end

begin
stream << "retry: #{@retry}\n\n" if @retry
stream << "data: {\"type\":\"connected\"}\n\n"
rescue
@clients_mutex.synchronize { @clients.delete(stream) }
Expand Down
47 changes: 31 additions & 16 deletions lib/mata/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,41 @@
initMata();

function initMata() {
const eventSource = new EventSource("/__mata/events");
const retryDelayBase = parseInt(document.currentScript.dataset.mataRetry) || 1000;
const maximumRetryDelay = 30000;
let retryDelay = retryDelayBase;

eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
connect();

switch(data.type) {
case "reload":
morphPage();
function connect() {
const eventSource = new EventSource("/__mata/events");

break;
case "connected":
console.log("[Mata] Connected with DOM morphing");
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);

break;
}
};
switch(data.type) {
case "reload":
morphPage();

break;
case "connected":
retryDelay = retryDelayBase;

console.log("[Mata] Connected with DOM morphing");

break;
}
};

eventSource.onerror = function() {
eventSource.close();

console.log(`[Mata] Connection lost, retrying in ${retryDelay}ms…`);

setTimeout(connect, retryDelay);
retryDelay = Math.min(retryDelay * 2, maximumRetryDelay);
};
}

async function morphPage() {
try {
Expand Down Expand Up @@ -57,9 +76,5 @@
window.location.reload();
}
}

eventSource.onerror = function() {
console.log("[Mata] Connection lost, retrying…");
};
}
})();
6 changes: 4 additions & 2 deletions lib/mata/watch_tower.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class WatchTower
def initialize(options)
@watch_paths = Array(options[:watch] || [])
@skip_paths = skipped_patterns(options[:skip] || options[:ignore] || [])
@debounce = options[:debounce] || 300
@on_change = nil
@listener = nil

Expand Down Expand Up @@ -42,8 +43,9 @@ def observe!
@last_change_time = Time.now

Thread.new do
sleep 0.15
if Time.now - @last_change_time >= 0.15
sleep(@debounce / 1000.0)

if Time.now - @last_change_time >= (@debounce / 1000.0)
@on_change&.call(modified + added + removed)
end
end
Expand Down
8 changes: 8 additions & 0 deletions test/test_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ def test_inserts_script_into_html
assert_includes body.first, "</head>"
end

def test_injects_data_attributes_when_retry_is_set
agent = Mata::Agent.new(retry: 5000)
html = "<html><head></head><body>Content</body></html>"
_, _, body = agent.insert(200, {"Content-Type" => "text/html"}, [html])

assert_includes body.first, 'data-mata-retry="5000"'
end

def test_skips_non_html_responses
json = '{"data": "value"}'
_, _, body = @agent.insert(200, {"Content-Type" => "application/json"}, [json])
Expand Down
21 changes: 21 additions & 0 deletions test/test_broadcaster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ def test_handles_sse_connection
assert_equal "keep-alive", headers["connection"]
end

def test_sends_retry_directive_when_configured
broadcaster = Mata::Broadcaster.new(retry: 3000)
env = {"REQUEST_METHOD" => "GET"}
_, _, stream_proc = broadcaster.establish_contact(env)

output = ""
stream_proc.call(output)

assert_includes output, "retry: 3000\n\n"
end

def test_omits_retry_directive_when_not_configured
env = {"REQUEST_METHOD" => "GET"}
_, _, stream_proc = @broadcaster.establish_contact(env)

output = ""
stream_proc.call(output)

refute_includes output, "retry:"
end

def test_rejects_non_get_sse_requests
env = {"REQUEST_METHOD" => "POST"}
status, _, _ = @broadcaster.establish_contact(env)
Expand Down
6 changes: 3 additions & 3 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

module TestHelpers
def setup_temp_directory
@temp_dir = Dir.mktmpdir
@watch_dir = Dir.mktmpdir
end

def teardown_temp_directory
FileUtils.rm_rf(@temp_dir) if @temp_dir
FileUtils.rm_rf(@watch_dir) if @watch_dir
end

def create_test_file(name, content = "test content")
File.join(@temp_dir, name).tap { File.write(it, content) }
File.join(@watch_dir, name).tap { File.write(it, content) }
end

def wait_for_file_watcher(seconds = 0.3)
Expand Down
6 changes: 3 additions & 3 deletions test/test_mata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def setup
setup_temp_directory

@app = lambda { |env| [200, {"Content-Type" => "text/html"}, ["<html><head></head><body>Hello</body></html>"]] }
@mata = Mata.new(@app, watch: [@temp_dir], skip: ["tmp"])
@mata = Mata.new(@app, watch: [@watch_dir], skip: ["tmp"], debounce: 300, retry: 3000)
end

def teardown
Expand All @@ -24,7 +24,7 @@ def app
def test_injects_script_into_html_responses
get "/"

assert_includes last_response.body, '<script src="/__mata/client.js"></script>'
assert_includes last_response.body, '<script src="/__mata/client.js" data-mata-retry="3000"></script>'
end

def test_serves_client_script
Expand All @@ -46,6 +46,6 @@ def test_passes_through_other_requests
get "/other"

assert_includes last_response.body, "Hello"
assert_includes last_response.body, '<script src="/__mata/client.js"></script>'
assert_includes last_response.body, '<script src="/__mata/client.js" data-mata-retry="3000"></script>'
end
end
14 changes: 11 additions & 3 deletions test/test_watch_tower.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ class TestWatchTower < Minitest::Test
include TestHelpers

def setup
@temp_dir = Dir.mktmpdir
@watch_tower = Mata::WatchTower.new(watch: [@temp_dir])
@watch_dir = Dir.mktmpdir
@watch_tower = Mata::WatchTower.new(watch: [@watch_dir])
@changes = []

@watch_tower.on_change { |files| @changes.concat(files) }
Expand All @@ -15,7 +15,15 @@ def setup
def teardown
@watch_tower.shutdown

FileUtils.rm_rf(@temp_dir)
FileUtils.rm_rf(@watch_dir)
end

def test_accepts_custom_debounce
watch_tower = Mata::WatchTower.new(watch: [@watch_dir], debounce: 500)

assert_equal 500, watch_tower.instance_variable_get(:@debounce)
ensure
watch_tower.shutdown
end

def test_on_change_callback_fires
Expand Down