Skip to content

Instantly share code, notes, and snippets.

@julik
Last active August 31, 2017 13:41
Show Gist options
  • Save julik/05d50e54c2d586a58be37bdfa666d247 to your computer and use it in GitHub Desktop.
Save julik/05d50e54c2d586a58be37bdfa666d247 to your computer and use it in GitHub Desktop.
For the guard-tired
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