mata 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 307e9528d8950443b07d7ebcf1f036d400fa2198f4f97e79959826011d6b543c
4
+ data.tar.gz: c3f3999e6d1c5dad7d224b34595ece06cb42c832cb25aa34b948cf2723083d2d
5
+ SHA512:
6
+ metadata.gz: 42d6e90cd1c19cece9a011056cf04c095673f9552dd0944f3c77bd4f807ac8575505cf889ed0d6d9fec7aa65bec23c7437d0d2d2ecab7206bab0fe2a89e10dbc
7
+ data.tar.gz: 29163a7985e9a8e863e374cfe4525902f7fe5861bb457b724444464c22b6159058ad8262ad62f7d33c509f14bf11887c29b4398ea86144d70b07e3d63cfe8a8f
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Mata
2
+
3
+ Hot module reloading for Rack applications using Server-Sent Events and DOM morphing.
4
+
5
+
6
+ **Sponsored By [Rails Designer](https://railsdesigner.com/)**
7
+
8
+ <a href="https://railsdesigner.com/" target="_blank">
9
+ <picture>
10
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-dark.svg">
11
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-light.svg">
12
+ <img alt="Rails Designer" src="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-light.svg" width="240" style="max-width: 100%;">
13
+ </picture>
14
+ </a>
15
+
16
+
17
+ ## Installation
18
+
19
+ ```ruby
20
+ gem "mata"
21
+ ```
22
+
23
+
24
+ ## Usage
25
+
26
+ ### Rails
27
+
28
+ ```ruby
29
+ # config/environments/development.rb
30
+ Rails.application.configure do
31
+ config.middleware.insert_before(
32
+ ActionDispatch::Static,
33
+ Mata,
34
+ watch: %w[app/views app/assets],
35
+ skip: %w[tmp log node_modules]
36
+ )
37
+ end
38
+ ```
39
+
40
+ ### Sinatra
41
+
42
+ ```ruby
43
+ require "sinatra"
44
+ require "mata"
45
+
46
+ configure :development do
47
+ use Mata, watch: %w[views public]
48
+ end
49
+ ```
50
+
51
+ ### Hanami
52
+
53
+ ```ruby
54
+ # config.ru
55
+ require "hanami/boot"
56
+ require "mata"
57
+
58
+ use Mata, watch: %w[apps lib] if Hanami.env?(:development)
59
+
60
+ run Hanami.app
61
+ ```
62
+
63
+ ### Basic Rack app
64
+
65
+ ```ruby
66
+ # config.ru
67
+ require "mata"
68
+
69
+ use Mata, watch: %w[views assets], skip: %w[tmp log]
70
+
71
+ run YourApp
72
+ ```
73
+
74
+
75
+ ## Options
76
+
77
+ - **watch**; array of paths to monitor (default: `%w[app views assets]`)
78
+ - **skip**; array of paths to ignore (default: `%w[tmp log]`)
79
+
80
+
81
+ ## Used in
82
+
83
+ This gem powers hot reloading in [Perron](https://github.com/rails-designer/perron), a Rails-based static site generator. It can be enabled with `config.hrm = true` in your Perron initializer.
84
+
85
+
86
+ ## Why this gem?
87
+
88
+ I needed hot module reloading for Perron-powered Rails applications. These are minimal Rails apps typically without Hotwire or ActionCable dependencies. Existing solutions either required ActionCable (adding unnecessary complexity) or provided only basic full-page reloads without state preservation.
89
+
90
+
91
+ ## Who is Mata?
92
+
93
+ In the smoky cabarets of Belle Époque Paris, a dancer captivated audiences with her exotic performances and mysterious allure. Born Margaretha Geertruida Zelle in the Netherlands, she had reinvented herself as an Indonesian princess, weaving tales of sacred temple dances and Eastern mystique.
94
+
95
+ She moved through the salons of Europe's elite with remarkable ease, speaking multiple languages and charming diplomats, military officers and aristocrats alike. Her lovers included high-ranking officials from opposing sides of the Great War, giving her access to secrets that others could only dream of obtaining.
96
+
97
+ Whether she was truly the master spy of legend or simply a woman caught in the wrong place at the wrong time remains a mystery. What's certain is her ability to observe, adapt and operate seamlessly across boundaries that others found impermeable.
98
+
99
+ History remembers **Mata Hari** as the ultimate double agent. She was someone who could watch, listen and report back with precision, all while maintaining perfect cover.
100
+
101
+
102
+ ## Contributing
103
+
104
+ This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `be standardrb` before submitting pull requests. Run tests via `rails test`.
105
+
106
+
107
+ ## License
108
+
109
+ Mata is released under the [MIT License](https://opensource.org/licenses/MIT).
data/lib/mata/agent.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Mata
4
+ class Agent
5
+ def insert(status, headers, body)
6
+ return [status, headers, body] unless html_response?(headers)
7
+
8
+ content = extract(body)
9
+ return [status, headers, [content]] unless content.include?("</head>")
10
+
11
+ script_tag = '<script src="/__mata/client.js"></script>'
12
+ modified_content = content.sub("</head>", "#{script_tag}\n</head>")
13
+
14
+ headers["Content-Length"] = modified_content.bytesize.to_s if headers["Content-Length"]
15
+
16
+ [status, headers, [modified_content]]
17
+ end
18
+
19
+ private
20
+
21
+ def html_response?(headers)
22
+ content_type = headers["Content-Type"] || headers["content-type"]
23
+
24
+ content_type&.include?("text/html")
25
+ end
26
+
27
+ def extract(body)
28
+ if body.respond_to?(:each)
29
+ body.to_a.join
30
+ else
31
+ body.to_s
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Mata
4
+ class Broadcaster
5
+ def initialize
6
+ @clients = []
7
+ @clients_mutex = Mutex.new
8
+ @cleanup_thread = cleanup_periodically
9
+ end
10
+
11
+ def establish_contact(env)
12
+ if env["REQUEST_METHOD"] != "GET"
13
+ return [405, {}, []]
14
+ end
15
+
16
+ headers = {
17
+ "Content-Type" => "text/event-stream",
18
+ "Cache-Control" => "no-cache",
19
+ "Connection" => "keep-alive",
20
+ "Access-Control-Allow-Origin" => "*"
21
+ }
22
+
23
+ [200, headers, proc { |stream|
24
+ @clients_mutex.synchronize do
25
+ @clients << stream
26
+ end
27
+
28
+ begin
29
+ stream << "data: {\"type\":\"connected\"}\n\n"
30
+
31
+ Thread.new do
32
+ loop { sleep 30 }
33
+ rescue
34
+ @clients_mutex.synchronize { @clients.delete(stream) }
35
+ end
36
+ rescue
37
+ @clients_mutex.synchronize { @clients.delete(stream) }
38
+ end
39
+ }]
40
+ end
41
+
42
+ def deliver_payload
43
+ idiomorph_js = File.read(File.join(__dir__, "idiomorph.min.js"))
44
+ client_js = File.read(File.join(__dir__, "client.js"))
45
+
46
+ script = "#{idiomorph_js}\n\n#{client_js}"
47
+
48
+ [200, {"Content-Type" => "application/javascript"}, [script]]
49
+ end
50
+
51
+ def broadcast_to_all(files)
52
+ clients_copy = @clients_mutex.synchronize { @clients.dup }
53
+ return if clients_copy.empty?
54
+
55
+ files.each do |file|
56
+ event_data = {type: "reload"}
57
+ message = "data: #{event_data.to_json}\n\n"
58
+
59
+ clients_copy.each do |stream|
60
+ stream << message
61
+ rescue
62
+ @clients_mutex.synchronize { @clients.delete(stream) }
63
+ end
64
+ end
65
+ end
66
+
67
+ def stand_down
68
+ @cleanup_thread&.kill
69
+
70
+ @clients_mutex.synchronize { @clients.clear }
71
+ end
72
+
73
+ private
74
+
75
+ def cleanup_periodically
76
+ Thread.new do
77
+ loop do
78
+ sleep 60
79
+
80
+ cleanup_dead_clients
81
+ end
82
+ rescue
83
+ end
84
+ end
85
+
86
+ def cleanup_dead_clients
87
+ @clients_mutex.synchronize do
88
+ @clients.reject! do |client|
89
+ client << ""
90
+ false
91
+ rescue
92
+ true
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,65 @@
1
+ (function() {
2
+ initMata();
3
+
4
+ function initMata() {
5
+ const eventSource = new EventSource("/__mata/events");
6
+
7
+ eventSource.onmessage = function(event) {
8
+ const data = JSON.parse(event.data);
9
+
10
+ switch(data.type) {
11
+ case "reload":
12
+ morphPage();
13
+
14
+ break;
15
+ case "connected":
16
+ console.log("[Mata] Connected with DOM morphing");
17
+
18
+ break;
19
+ }
20
+ };
21
+
22
+ async function morphPage() {
23
+ try {
24
+ console.log("[Mata] Fetching updated page…");
25
+ const response = await fetch(window.location.href);
26
+
27
+ if (!response.ok) {
28
+ throw new Error(`HTTP ${response.status}`);
29
+ }
30
+
31
+ const html = await response.text();
32
+ const parser = new DOMParser();
33
+ const updatedDocument = parser.parseFromString(html, "text/html");
34
+
35
+ if (!updatedDocument.body) {
36
+ throw new Error("Invalid HTML response");
37
+ }
38
+
39
+ Idiomorph.morph(document.documentElement, updatedDocument.documentElement, {
40
+ ignoreActiveValue: true,
41
+ callbacks: {
42
+ beforeNodeMorphed: function(oldNode, _) {
43
+ if (oldNode.tagName && oldNode.tagName.includes("-")) { // skip custom elements
44
+ return false;
45
+ }
46
+
47
+ return true;
48
+ }
49
+ }
50
+ });
51
+
52
+ console.log("[Mata] Page morphed successfully");
53
+ } catch (error) {
54
+ console.error("[Mata] Morph failed:", error.message);
55
+ console.log("[Mata] Falling back to full reload");
56
+
57
+ window.location.reload();
58
+ }
59
+ }
60
+
61
+ eventSource.onerror = function() {
62
+ console.log("[Mata] Connection lost, retrying…");
63
+ };
64
+ }
65
+ })();
@@ -0,0 +1 @@
1
+ var Idiomorph=function(){"use strict";const e=()=>{};const n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:e,afterNodeAdded:e,beforeNodeMorphed:e,afterNodeMorphed:e,beforeNodeRemoved:e,afterNodeRemoved:e,beforeAttributeUpdated:e},head:{style:"merge",shouldPreserve:e=>e.getAttribute("im-preserve")==="true",shouldReAppend:e=>e.getAttribute("im-re-append")==="true",shouldRemove:e,afterHeadMorphed:e},restoreFocus:true};function t(t,e,n={}){t=d(t);const r=f(e);const i=u(t,r,n);const o=a(i,()=>{return c(i,t,r,e=>{if(e.morphStyle==="innerHTML"){s(e,t,r);return Array.from(t.childNodes)}else{return l(e,t,r)}})});i.pantry.remove();return o}function l(e,t,n){const r=f(t);s(e,r,n,t,t.nextSibling);return Array.from(r.childNodes)}function a(e,t){if(!e.config.restoreFocus)return t();let n=document.activeElement;if(!(n instanceof HTMLInputElement||n instanceof HTMLTextAreaElement)){return t()}const{id:r,selectionStart:i,selectionEnd:o}=n;const l=t();if(r&&r!==document.activeElement?.getAttribute("id")){n=e.target.querySelector(`[id="${r}"]`);n?.focus()}if(n&&!n.selectionEnd&&o){n.setSelectionRange(i,o)}return l}const s=function(){function e(e,t,n,r=null,i=null){if(t instanceof HTMLTemplateElement&&n instanceof HTMLTemplateElement){t=t.content;n=n.content}r||=t.firstChild;for(const o of n.childNodes){if(r&&r!=i){const a=f(e,o,r,i);if(a){if(a!==r){h(e,r,a)}b(a,o,e);r=a.nextSibling;continue}}if(o instanceof Element){const s=o.getAttribute("id");if(e.persistentIds.has(s)){const c=p(t,s,r,e);b(c,o,e);r=c.nextSibling;continue}}const l=d(t,o,r,e);if(l){r=l.nextSibling}}while(r&&r!=i){const u=r;r=r.nextSibling;m(e,u)}}function d(e,t,n,r){if(r.callbacks.beforeNodeAdded(t)===false)return null;if(r.idMap.has(t)){const i=document.createElement(t.tagName);e.insertBefore(i,n);b(i,t,r);r.callbacks.afterNodeAdded(i);return i}else{const o=document.importNode(t,true);e.insertBefore(o,n);r.callbacks.afterNodeAdded(o);return o}}const f=function(){function e(e,t,n,r){let i=null;let o=t.nextSibling;let l=0;let a=n;while(a&&a!=r){if(c(a,t)){if(s(e,a,t)){return a}if(i===null){if(!e.idMap.has(a)){i=a}}}if(i===null&&o&&c(a,o)){l++;o=o.nextSibling;if(l>=2){i=undefined}}if(e.activeElementAndParents.includes(a))break;a=a.nextSibling}return i||null}function s(e,t,n){let r=e.idMap.get(t);let i=e.idMap.get(n);if(!i||!r)return false;for(const o of r){if(i.has(o)){return true}}return false}function c(e,t){const n=e;const r=t;return n.nodeType===r.nodeType&&n.tagName===r.tagName&&(!n.getAttribute?.("id")||n.getAttribute?.("id")===r.getAttribute?.("id"))}return e}();function m(e,t){if(e.idMap.has(t)){l(e.pantry,t,null)}else{if(e.callbacks.beforeNodeRemoved(t)===false)return;t.parentNode?.removeChild(t);e.callbacks.afterNodeRemoved(t)}}function h(t,e,n){let r=e;while(r&&r!==n){let e=r;r=r.nextSibling;m(t,e)}return r}function p(e,t,n,r){const i=r.target.getAttribute?.("id")===t&&r.target||r.target.querySelector(`[id="${t}"]`)||r.pantry.querySelector(`[id="${t}"]`);o(i,r);l(e,i,n);return i}function o(t,n){const r=t.getAttribute("id");while(t=t.parentNode){let e=n.idMap.get(t);if(e){e.delete(r);if(!e.size){n.idMap.delete(t)}}}}function l(t,n,r){if(t.moveBefore){try{t.moveBefore(n,r)}catch(e){t.insertBefore(n,r)}}else{t.insertBefore(n,r)}}return e}();const b=function(){function e(e,t,n){if(n.ignoreActive&&e===document.activeElement){return null}if(n.callbacks.beforeNodeMorphed(e,t)===false){return e}if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){m(e,t,n)}else{r(e,t,n);if(!f(e,n)){s(n,e,t)}}n.callbacks.afterNodeMorphed(e,t);return e}function r(e,t,n){let r=t.nodeType;if(r===1){const i=e;const o=t;const l=i.attributes;const a=o.attributes;for(const s of a){if(d(s.name,i,"update",n)){continue}if(i.getAttribute(s.name)!==s.value){i.setAttribute(s.name,s.value)}}for(let e=l.length-1;0<=e;e--){const c=l[e];if(!c)continue;if(!o.hasAttribute(c.name)){if(d(c.name,i,"remove",n)){continue}i.removeAttribute(c.name)}}if(!f(i,n)){u(i,o,n)}}if(r===8||r===3){if(e.nodeValue!==t.nodeValue){e.nodeValue=t.nodeValue}}}function u(n,r,i){if(n instanceof HTMLInputElement&&r instanceof HTMLInputElement&&r.type!=="file"){let e=r.value;let t=n.value;o(n,r,"checked",i);o(n,r,"disabled",i);if(!r.hasAttribute("value")){if(!d("value",n,"remove",i)){n.value="";n.removeAttribute("value")}}else if(t!==e){if(!d("value",n,"update",i)){n.setAttribute("value",e);n.value=e}}}else if(n instanceof HTMLOptionElement&&r instanceof HTMLOptionElement){o(n,r,"selected",i)}else if(n instanceof HTMLTextAreaElement&&r instanceof HTMLTextAreaElement){let e=r.value;let t=n.value;if(d("value",n,"update",i)){return}if(e!==t){n.value=e}if(n.firstChild&&n.firstChild.nodeValue!==e){n.firstChild.nodeValue=e}}}function o(e,t,n,r){const i=t[n],o=e[n];if(i!==o){const l=d(n,e,"update",r);if(!l){e[n]=t[n]}if(i){if(!l){e.setAttribute(n,"")}}else{if(!d(n,e,"remove",r)){e.removeAttribute(n)}}}}function d(e,t,n,r){if(e==="value"&&r.ignoreActiveValue&&t===document.activeElement){return true}return r.callbacks.beforeAttributeUpdated(e,t,n)===false}function f(e,t){return!!t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}return e}();function c(t,e,n,r){if(t.head.block){const i=e.querySelector("head");const o=n.querySelector("head");if(i&&o){const l=m(i,o,t);return Promise.all(l).then(()=>{const e=Object.assign(t,{head:{block:false,ignore:true}});return r(e)})}}return r(t)}function m(e,t,r){let i=[];let o=[];let l=[];let a=[];let s=new Map;for(const n of t.children){s.set(n.outerHTML,n)}for(const u of e.children){let e=s.has(u.outerHTML);let t=r.head.shouldReAppend(u);let n=r.head.shouldPreserve(u);if(e||n){if(t){o.push(u)}else{s.delete(u.outerHTML);l.push(u)}}else{if(r.head.style==="append"){if(t){o.push(u);a.push(u)}}else{if(r.head.shouldRemove(u)!==false){o.push(u)}}}}a.push(...s.values());let c=[];for(const d of a){let n=document.createRange().createContextualFragment(d.outerHTML).firstChild;if(r.callbacks.beforeNodeAdded(n)!==false){if("href"in n&&n.href||"src"in n&&n.src){let t;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});c.push(e)}e.appendChild(n);r.callbacks.afterNodeAdded(n);i.push(n)}}for(const f of o){if(r.callbacks.beforeNodeRemoved(f)!==false){e.removeChild(f);r.callbacks.afterNodeRemoved(f)}}r.head.afterHeadMorphed(e,{added:i,kept:l,removed:o});return c}const u=function(){function e(e,t,n){const{persistentIds:r,idMap:i}=f(e,t);const o=a(n);const l=o.morphStyle||"outerHTML";if(!["innerHTML","outerHTML"].includes(l)){throw`Do not understand how to morph style ${l}`}return{target:e,newContent:t,config:o,morphStyle:l,ignoreActive:o.ignoreActive,ignoreActiveValue:o.ignoreActiveValue,restoreFocus:o.restoreFocus,idMap:i,persistentIds:r,pantry:s(),activeElementAndParents:c(e),callbacks:o.callbacks,head:o.head}}function a(e){let t=Object.assign({},n);Object.assign(t,e);t.callbacks=Object.assign({},n.callbacks,e.callbacks);t.head=Object.assign({},n.head,e.head);return t}function s(){const e=document.createElement("div");e.hidden=true;document.body.insertAdjacentElement("afterend",e);return e}function c(e){let t=[];let n=document.activeElement;if(n?.tagName!=="BODY"&&e.contains(n)){while(n){t.push(n);if(n===e)break;n=n.parentElement}}return t}function u(e){let t=Array.from(e.querySelectorAll("[id]"));if(e.getAttribute?.("id")){t.push(e)}return t}function d(n,e,r,t){for(const i of t){const o=i.getAttribute("id");if(e.has(o)){let t=i;while(t){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(o);if(t===r)break;t=t.parentElement}}}}function f(e,t){const n=u(e);const r=u(t);const i=m(n,r);let o=new Map;d(o,i,e,n);const l=t.__idiomorphRoot||t;d(o,i,l,r);return{persistentIds:i,idMap:o}}function m(e,t){let n=new Set;let r=new Map;for(const{id:o,tagName:l}of e){if(r.has(o)){n.add(o)}else{r.set(o,l)}}let i=new Set;for(const{id:o,tagName:l}of t){if(i.has(o)){n.add(o)}else if(r.get(o)===l){i.add(o)}}for(const o of n){i.delete(o)}return i}return e}();const{normalizeElement:d,normalizeParent:f}=function(){const i=new WeakSet;function e(e){if(e instanceof Document){return e.documentElement}else{return e}}function r(e){if(e==null){return document.createElement("div")}else if(typeof e==="string"){return r(l(e))}else if(i.has(e)){return e}else if(e instanceof Node){if(e.parentNode){return new o(e)}else{const t=document.createElement("div");t.append(e);return t}}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}class o{constructor(e){this.originalNode=e;this.realParentNode=e.parentNode;this.previousSibling=e.previousSibling;this.nextSibling=e.nextSibling}get childNodes(){const e=[];let t=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;while(t&&t!=this.nextSibling){e.push(t);t=t.nextSibling}return e}querySelectorAll(r){return this.childNodes.reduce((t,e)=>{if(e instanceof Element){if(e.matches(r))t.push(e);const n=e.querySelectorAll(r);for(let e=0;e<n.length;e++){t.push(n[e])}}return t},[])}insertBefore(e,t){return this.realParentNode.insertBefore(e,t)}moveBefore(e,t){return this.realParentNode.moveBefore(e,t)}get __idiomorphRoot(){return this.originalNode}}function l(n){let r=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=r.parseFromString(n,"text/html");if(e.match(/<\/html>/)){i.add(t);return t}else{let e=t.firstChild;if(e){i.add(e)}return e}}else{let e=r.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;i.add(t);return t}}return{normalizeElement:e,normalizeParent:r}}();return{morph:t,defaults:n}}();
@@ -0,0 +1,3 @@
1
+ module Mata
2
+ VERSION = "0.5.0"
3
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Mata
4
+ class WatchTower
5
+ def initialize(options)
6
+ @watch_paths = Array(options[:watch] || %w[app views assets])
7
+ @skip_paths = skipped_patterns(options[:skip] || options[:ignore] || %w[tmp log])
8
+ @on_change = nil
9
+ @listener = nil
10
+
11
+ observe!
12
+ end
13
+
14
+ def on_change(&block)
15
+ @on_change = block
16
+ end
17
+
18
+ def shutdown
19
+ @listener&.stop
20
+ end
21
+
22
+ private
23
+
24
+ def skipped_patterns(skip_paths)
25
+ skip_paths.map do |path|
26
+ case path
27
+ when Regexp
28
+ path
29
+ when String
30
+ escaped = Regexp.escape(path)
31
+ /#{escaped}/
32
+ else
33
+ /#{Regexp.escape(path.to_s)}/
34
+ end
35
+ end
36
+ end
37
+
38
+ def observe!
39
+ @last_change_time = nil
40
+
41
+ @listener = Listen.to(*@watch_paths, ignore: @skip_paths) do |modified, added, removed|
42
+ @last_change_time = Time.now
43
+
44
+ Thread.new do
45
+ sleep 0.15
46
+ if Time.now - @last_change_time >= 0.15
47
+ @on_change&.call(modified + added + removed)
48
+ end
49
+ end
50
+ end
51
+
52
+ @listener.start
53
+ end
54
+ end
55
+ end
data/lib/mata.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "listen"
4
+ require "json"
5
+
6
+ require "mata/agent"
7
+ require "mata/broadcaster"
8
+ require "mata/watch_tower"
9
+
10
+ class Mata
11
+ def initialize(app, options = {})
12
+ @app = app
13
+
14
+ @watch_tower = WatchTower.new(options)
15
+ @broadcaster = Broadcaster.new
16
+ @agent = Agent.new
17
+
18
+ @watch_tower.on_change { |files| @broadcaster.broadcast_to_all(files) }
19
+
20
+ at_exit { stand_down }
21
+ end
22
+
23
+ def call(environment)
24
+ request = Rack::Request.new(environment)
25
+
26
+ case request.path
27
+ when "/__mata/events"
28
+ @broadcaster.establish_contact(environment)
29
+ when "/__mata/client.js"
30
+ @broadcaster.deliver_payload
31
+ else
32
+ @agent.insert(*@app.call(environment))
33
+ end
34
+ end
35
+
36
+ def stand_down
37
+ @watch_tower&.shutdown
38
+ @broadcaster&.stand_down
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mata
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Rails Designer
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: listen
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ description: SSE-based hot reloading middleware with DOM morphing
41
+ email:
42
+ - devs@railsdesigner.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - lib/mata.rb
49
+ - lib/mata/agent.rb
50
+ - lib/mata/broadcaster.rb
51
+ - lib/mata/client.js
52
+ - lib/mata/idiomorph.min.js
53
+ - lib/mata/version.rb
54
+ - lib/mata/watch_tower.rb
55
+ homepage: https://railsdesigner.com/mata/
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://railsdesigner.com/mata/
60
+ source_code_uri: https://github.com/Rails-Designer/mata
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 4.0.4
76
+ specification_version: 4
77
+ summary: Hot module reloading for Rack applications
78
+ test_files: []