mata 0.10.0 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab070e4aa36426eec7fdc324a6f493665bb6fbda68280cca8d8e98bbbd15df4d
4
- data.tar.gz: 372274d8821279df66b0ddbf8b5c8f59254aa4e61f7f0ed7b343fbbc5891269e
3
+ metadata.gz: 886a7db4fb956cd1191b502d73b582569e3dc680ca3c375fdf42296fb5d6197b
4
+ data.tar.gz: d87cd56304b9eb144d3b9a997598f09df16d94292c9724cf3312f0e9b6298411
5
5
  SHA512:
6
- metadata.gz: 3343f58cd5a1cc9bef0aaa7fdb8d8eb102839631c1b5ae1cb141b340f387b00e5f44743ea5742d5eac53dfdb26816231d5643a33158a6a465b11fabd154366d1
7
- data.tar.gz: 3dc12dbb74eaac619279b2d92c0721913f71f46a9cb184370861b7945c84610856466d2a14c24c0ee7595046dc603f249b3f96a2d7e43ebf82eaf2308d08b266
6
+ metadata.gz: 7e82ce5b92c7ae0ee99758373be421ac03abbc0546af2ccab1f51f74584b4d13fbeaa4efbf0c8af1c3efc6e6da9f9703ccf74cae7dc46e3fb3605364c3a0d7ff
7
+ data.tar.gz: 59b37d520084e2afed7d2237cf6d99d42f5a15bd690958e65f66397237131bdda23cf62e0909b3456ae5a3247a2b37a52fb2ceb2b5470206f2d76a245ebcaefc
data/README.md CHANGED
@@ -10,6 +10,7 @@ Live Reload with DOM Morphing for Rack applications using Server-Sent Events (SS
10
10
  <picture>
11
11
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-dark.svg">
12
12
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-light.svg">
13
+
13
14
  <img alt="Rails Designer" src="https://raw.githubusercontent.com/Rails-Designer/mata/HEAD/.github/logo-light.svg" width="240" style="max-width: 100%;">
14
15
  </picture>
15
16
  </a>
@@ -36,20 +37,6 @@ config.middleware.insert_before(
36
37
  ```
37
38
 
38
39
 
39
- ### Other rack-based apps
40
- ```ruby
41
- # config.ru
42
- require "mata"
43
-
44
- use Mata, watch: %w[views assets], skip: %w[views/tmp]
45
-
46
- run YourApp
47
- ```
48
-
49
- > [!NOTE]
50
- > Do share your snippets on how to add Mata into your specific Rack-based apps 💙
51
-
52
-
53
40
  ### Hanami
54
41
  ```ruby
55
42
  # config.ru
@@ -87,6 +74,31 @@ route do |r|
87
74
  end
88
75
  ```
89
76
 
77
+ ### Other rack-based apps
78
+ ```ruby
79
+ # config.ru
80
+ require "mata"
81
+
82
+ use Mata, watch: %w[views assets], skip: %w[views/tmp]
83
+
84
+ run YourApp
85
+ ```
86
+
87
+
88
+ ### Options
89
+
90
+ All options are optional.
91
+
92
+ | Option | Default | Type | Description |
93
+ |---|---|---|---|
94
+ | `watch` | `[]` | Array of String | Directories to watch for changes |
95
+ | `skip` | `[]` | Array of String/Regexp | Paths to ignore |
96
+ | `debounce` | `300` | Integer | Debounce interval in milliseconds before broadcasting changes |
97
+ | `retry` | not set | Integer | SSE retry interval in milliseconds (not set = browser default ~3s, maximum clamped at 30s) |
98
+ | `log_level` | `:verbose` | Symbol | `:verbose` shows all console messages; `:silent` suppresses them in the browser console (`[Mata] Connected with DOM morphing`, etc.) |
99
+ | `idiomorph_options` | `nil` | String | A raw JS object literal passed **verbatim** as the `options` argument to [`Idiomorph.morph`](https://github.com/bigskysoftware/idiomorph#options), fully replacing Mata's built-in defaults. Example: `idiomorph_options: '{ morphStyle: "innerHTML" }'` |
100
+
101
+
90
102
  ## Used in
91
103
 
92
104
  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.
data/lib/mata/agent.rb CHANGED
@@ -2,13 +2,21 @@
2
2
 
3
3
  class Mata
4
4
  class Agent
5
+ def initialize(options = {})
6
+ @retry = options[:retry]
7
+ @log_level = options[:log_level]
8
+ end
9
+
5
10
  def insert(status, headers, body)
6
11
  return [status, headers, body] unless html_response?(headers)
7
12
 
8
13
  content = extract(body)
9
14
  return [status, headers, [content]] unless content.include?("</head>")
10
15
 
11
- script_tag = '<script src="/__mata/client.js"></script>'
16
+ script_tag = '<script src="/__mata/client.js"'
17
+ script_tag += %( data-mata-retry="#{@retry}") if @retry
18
+ script_tag += %( data-mata-log-level="#{@log_level}") if @log_level
19
+ script_tag += "></script>"
12
20
  modified_content = content.sub("</head>", "#{script_tag}\n</head>")
13
21
 
14
22
  headers["Content-Length"] = modified_content.bytesize.to_s if headers["Content-Length"]
@@ -2,9 +2,26 @@
2
2
 
3
3
  class Mata
4
4
  class Broadcaster
5
- def initialize
5
+ DEFAULT_IDIOMORPH_OPTIONS = <<~JS
6
+ {
7
+ ignoreActiveValue: true,
8
+ callbacks: {
9
+ beforeNodeMorphed: function(oldNode, _) {
10
+ if (oldNode.tagName && oldNode.tagName.includes("-")) {
11
+ return false;
12
+ }
13
+
14
+ return true;
15
+ }
16
+ }
17
+ }
18
+ JS
19
+
20
+ def initialize(options = {})
6
21
  @clients = []
7
22
  @clients_mutex = Mutex.new
23
+ @retry = options[:retry]
24
+ @idiomorph_options = options[:idiomorph_options]
8
25
  @cleanup_thread = cleanup_periodically
9
26
  end
10
27
 
@@ -26,6 +43,7 @@ class Mata
26
43
  end
27
44
 
28
45
  begin
46
+ stream << "retry: #{@retry}\n\n" if @retry
29
47
  stream << "data: {\"type\":\"connected\"}\n\n"
30
48
  rescue
31
49
  @clients_mutex.synchronize { @clients.delete(stream) }
@@ -34,9 +52,13 @@ class Mata
34
52
  end
35
53
 
36
54
  def deliver_payload
55
+ raise ArgumentError, "idiomorph_options must be a String containing a raw JS object literal" if @idiomorph_options && !@idiomorph_options.is_a?(String)
56
+
37
57
  idiomorph_js = File.read(File.join(__dir__, "idiomorph.min.js"))
38
58
  client_js = File.read(File.join(__dir__, "client.js"))
39
59
 
60
+ client_js = client_js.gsub("__MATA_IDIOMORPH_OPTIONS__", @idiomorph_options || DEFAULT_IDIOMORPH_OPTIONS)
61
+
40
62
  script = "#{idiomorph_js}\n\n#{client_js}"
41
63
 
42
64
  [200, {"content-type" => "application/javascript"}, [script]]
@@ -46,15 +68,13 @@ class Mata
46
68
  clients_copy = @clients_mutex.synchronize { @clients.dup }
47
69
  return if clients_copy.empty?
48
70
 
49
- files.each do |file|
50
- event_data = {type: "reload"}
51
- message = "data: #{event_data.to_json}\n\n"
71
+ event_data = {type: "reload", files: files}
72
+ message = "data: #{event_data.to_json}\n\n"
52
73
 
53
- clients_copy.each do |stream|
54
- stream << message
55
- rescue
56
- @clients_mutex.synchronize { @clients.delete(stream) }
57
- end
74
+ clients_copy.each do |stream|
75
+ stream << message
76
+ rescue
77
+ @clients_mutex.synchronize { @clients.delete(stream) }
58
78
  end
59
79
  end
60
80
 
data/lib/mata/client.js CHANGED
@@ -2,26 +2,61 @@
2
2
  initMata();
3
3
 
4
4
  function initMata() {
5
- const eventSource = new EventSource("/__mata/events");
5
+ const retryDelayBase = parseInt(document.currentScript.dataset.mataRetry) || 1000;
6
+ const maximumRetryDelay = 30000;
7
+ let retryDelay = retryDelayBase;
6
8
 
7
- eventSource.onmessage = function(event) {
8
- const data = JSON.parse(event.data);
9
+ const silent = (document.currentScript.dataset.mataLogLevel || "verbose").toLowerCase() === "silent";
9
10
 
10
- switch(data.type) {
11
- case "reload":
12
- morphPage();
11
+ function log(...arguments) {
12
+ if (!silent) console.log("[Mata]", ...arguments);
13
+ }
13
14
 
14
- break;
15
- case "connected":
16
- console.log("[Mata] Connected with DOM morphing");
15
+ function error(...arguments) {
16
+ if (!silent) console.error("[Mata]", ...arguments);
17
+ }
17
18
 
18
- break;
19
- }
20
- };
19
+ connect();
20
+
21
+ function connect() {
22
+ const eventSource = new EventSource("/__mata/events");
23
+
24
+ eventSource.onmessage = function(event) {
25
+ const data = JSON.parse(event.data);
26
+
27
+ switch(data.type) {
28
+ case "reload":
29
+ if (data.files?.some((file) => file.endsWith(".js"))) {
30
+ log("JS file changed, reloading…");
31
+
32
+ window.location.reload();
33
+ } else {
34
+ morphPage();
35
+ }
36
+
37
+ break;
38
+ case "connected":
39
+ retryDelay = retryDelayBase;
40
+
41
+ log("Connected with DOM morphing");
42
+
43
+ break;
44
+ }
45
+ };
46
+
47
+ eventSource.onerror = function() {
48
+ eventSource.close();
49
+
50
+ log(`Connection lost, retrying in ${retryDelay}ms…`);
51
+
52
+ setTimeout(connect, retryDelay);
53
+ retryDelay = Math.min(retryDelay * 2, maximumRetryDelay);
54
+ };
55
+ }
21
56
 
22
57
  async function morphPage() {
23
58
  try {
24
- console.log("[Mata] Fetching updated page…");
59
+ log("Fetching updated page…");
25
60
  const response = await fetch(window.location.href);
26
61
 
27
62
  if (!response.ok) {
@@ -36,30 +71,15 @@
36
71
  throw new Error("Invalid HTML response");
37
72
  }
38
73
 
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
- });
74
+ Idiomorph.morph(document.documentElement, updatedDocument.documentElement, __MATA_IDIOMORPH_OPTIONS__);
51
75
 
52
- console.log("[Mata] Page morphed successfully");
76
+ log("Page morphed successfully");
53
77
  } catch (error) {
54
- console.error("[Mata] Morph failed:", error.message);
55
- console.log("[Mata] Falling back to full reload");
78
+ error("Morph failed:", error.message);
79
+ log("Falling back to full reload");
56
80
 
57
81
  window.location.reload();
58
82
  }
59
83
  }
60
-
61
- eventSource.onerror = function() {
62
- console.log("[Mata] Connection lost, retrying…");
63
- };
64
84
  }
65
85
  })();
data/lib/mata/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Mata
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.0"
3
3
  end
@@ -5,6 +5,7 @@ class Mata
5
5
  def initialize(options)
6
6
  @watch_paths = Array(options[:watch] || [])
7
7
  @skip_paths = skipped_patterns(options[:skip] || options[:ignore] || [])
8
+ @debounce = options[:debounce] || 300
8
9
  @on_change = nil
9
10
  @listener = nil
10
11
 
@@ -42,8 +43,9 @@ class Mata
42
43
  @last_change_time = Time.now
43
44
 
44
45
  Thread.new do
45
- sleep 0.15
46
- if Time.now - @last_change_time >= 0.15
46
+ sleep(@debounce / 1000.0)
47
+
48
+ if Time.now - @last_change_time >= (@debounce / 1000.0)
47
49
  @on_change&.call(modified + added + removed)
48
50
  end
49
51
  end
data/lib/mata.rb CHANGED
@@ -14,8 +14,8 @@ class Mata
14
14
  return if ENV["MATA_DISABLED"] == "true"
15
15
 
16
16
  @watch_tower = WatchTower.new(options)
17
- @broadcaster = Broadcaster.new
18
- @agent = Agent.new
17
+ @broadcaster = Broadcaster.new(options)
18
+ @agent = Agent.new(options)
19
19
 
20
20
  @watch_tower.on_change { |files| @broadcaster.broadcast_to_all(files) }
21
21
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer
@@ -72,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
72
  - !ruby/object:Gem::Version
73
73
  version: '0'
74
74
  requirements: []
75
- rubygems_version: 4.0.14
75
+ rubygems_version: 4.0.22
76
76
  specification_version: 4
77
77
  summary: Hot module reloading for Rack applications
78
78
  test_files: []