Last active
August 31, 2017 13:41
-
-
Save julik/05d50e54c2d586a58be37bdfa666d247 to your computer and use it in GitHub Desktop.
For the guard-tired
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'thread' | |
# I am very Watchist. Use me in your config.ru like so: | |
# Watchist.start_and_mount_in_rack(self) | |
class Watchist | |
CHMUX = Mutex.new | |
$watchist_channels = [] | |
$watchist_logger = Logger.new($stderr) | |
$watchist_logger.progname = "watchist" | |
def start(dir_path=Dir.pwd) | |
dir_path = File.expand_path(dir_path) | |
require 'rb-fsevent' | |
Thread.new do | |
begin | |
ctr = 0 | |
fsevent = FSEvent.new | |
fsevent.watch(dir_path, file_events: true) do |file_paths| | |
destinations = CHMUX.synchronize { $watchist_channels.dup } | |
file_paths.each do |file_path| | |
next if file_path.include?('/.') # Dirty check for anything hidden | |
ctr += 1 | |
$watchist_logger.debug { "Dispatching change in %s to %d listeners" % [file_path, destinations.length] } | |
destinations.each { |q| q << {seq: ctr, path: file_path} } | |
end | |
end | |
fsevent.run | |
rescue StandardError => e | |
$stderr.puts e.inspect | |
raise e | |
end | |
end | |
true | |
end | |
def self.event_source_script | |
return '' if ENV['RACK_ENV'] == 'production' | |
return <<-EOF | |
<script> | |
function replaceQSParams(urlStr, overrides) { | |
var url = new URL(urlStr, window.location); | |
var qsp = new URLSearchParams(url.search); | |
Object.keys(overrides).forEach(function(key) { | |
qsp.set(key, overrides[key]); | |
}); | |
url.search = qsp.toString(); | |
return url.toString(); | |
} | |
function reloadLinkElem(linkElem, seqNumber) { | |
var replacementLinkElem = linkElem.cloneNode() | |
replacementLinkElem.href = replaceQSParams(replacementLinkElem.href, {watchist: seqNumber}) | |
linkElem.parentNode.insertBefore(replacementLinkElem, linkElem.nextSibling); | |
window.setTimeout(function() { | |
linkElem.parentNode.removeChild(linkElem); | |
}, 100); | |
} | |
function reloadStyles(seqNumber) { | |
var links = document.querySelectorAll("link"); | |
links.forEach(reloadLinkElem); | |
} | |
var evtSource = new EventSource("/_watchist_"); | |
evtSource.addEventListener("file_change", function(e) { | |
var obj = JSON.parse(e.data); | |
var changed_path = obj.path; | |
if(changed_path.match(/css$/)) { | |
console.debug("Filesystem CSS change, reloading stylesheets"); | |
reloadStyles(obj.seq); | |
} | |
}, false); | |
</script> | |
EOF | |
end | |
class Hijacker | |
TERM = "\r\n" | |
def initialize(q) | |
@q = q | |
end | |
def write_with_chunked_encoding(socket, chunk) | |
chunk_payload = [chunk.bytesize.to_s(16), TERM, chunk, TERM].join | |
socket.write(chunk_payload) | |
end | |
def call(socket) | |
$watchist_logger.debug { "Listener joined" } | |
write_with_chunked_encoding socket, "event: attach\n" | |
write_with_chunked_encoding socket, "data: {hello: 1}" | |
write_with_chunked_encoding socket, "\n\n" | |
socket.flush | |
loop do | |
begin | |
change_event = @q.pop(nb=true) | |
write_with_chunked_encoding socket, "event: file_change\n" | |
write_with_chunked_encoding socket, "data: %s" % JSON.dump(change_event) | |
write_with_chunked_encoding socket, "\n\n" | |
socket.flush | |
rescue ThreadError # empty | |
sleep 0.1 | |
end | |
end | |
rescue Errno::EPIPE | |
ensure | |
io.close rescue nil | |
$watchist_logger.debug { "Listener leaving, detaching the queue" } | |
CHMUX.synchronize do | |
$watchist_channels.delete(@q) | |
end | |
end | |
end | |
def initialize(app) | |
@app = app | |
end | |
def call(env) | |
req = Rack::Request.new(env) | |
if req.fullpath.start_with?('/_watchist_') && ENV['RACK_ENV'] != 'production' | |
$watchist_started ||= start | |
watchist_event_stream | |
else | |
@app.call(env) | |
end | |
end | |
def watchist_event_stream | |
q = Queue.new | |
$watchist_channels << q | |
[200, {"Content-Type" => "text/event-stream", "Transfer-Encoding" => "chunked", "rack.hijack" => Hijacker.new(q)}, []] | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment