mbeditor 0.4.2 → 0.4.3

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: d378fdda8fc933a3c3d1f5b1fc5eb1622ca4ebee91db435378dd069827f708e2
4
- data.tar.gz: 2c58b8db51c1df5e7f746af61550dc2ccf62ba33a515cb374c79931aefed1beb
3
+ metadata.gz: cf6000382b991d7d35aa850c38fc80c743089c60f4a2277310f552e5a3b85477
4
+ data.tar.gz: 4d9c42bc71a9db592172eb09a601b134628f3c7f4d619a271349dda9b675c5ac
5
5
  SHA512:
6
- metadata.gz: 43753349cc14d328dbb45c4d6413cb0a56b9ac49979e7cd0bafe884de0889ede353decdd8552da38d872c278fa3c70162a1730df5827f45d0f629aa0d44a1f6f
7
- data.tar.gz: 3547eec20751a3c7279765e79429dda30398dfd6aa64c7b70d7e2f70cda6e8de6d9ae8c30a0c671a9750dcce1422d115d2b42f1a3d35b6679e81d795ba134c3e
6
+ metadata.gz: 9e4b14f3d506a23c1f157a7dd91d4c522a568623fd555b3ef10e6e83e561b4bdcaf040b822ea1c2ec86ca1eed8f47992f7fa1060cb18a9600ee2f43b10de4730
7
+ data.tar.gz: '0911b0a22050d16d96a1e841ae16e73673fd0579549efd5590d538efe405ca66e7f1470597680e0102954b4d4ce6ef9337d02a22dc736c487998f990808ff34f'
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.3] - 2026-04-22
9
+
10
+ ### Added
11
+ - Documented optional Action Cable host-app setup and fallback behavior for realtime editor updates.
12
+
13
+ ### Changed
14
+ - `bundle exec rake test` now includes `test/lib/**/*_test.rb` so release validation covers library-level regression tests.
15
+
16
+ ### Fixed
17
+ - Action Cable availability detection now respects whether `/cable` is actually mounted, and websocket handshake failures fall back to polling without noisy console errors.
18
+ - Circular loading indicators keep animating even when global reduced-motion styles are active in production.
19
+ - `CableLogFilter` now remains compatible with untagged logger stacks used outside full Action Cable setups.
20
+
8
21
  ## [0.4.2] - 2026-04-22
9
22
 
10
23
  ### Fixed
data/README.md CHANGED
@@ -121,9 +121,27 @@ The gem keeps host/tooling responsibilities in the host app:
121
121
  - `haml_lint` gem (optional, required for HAML lint — add to your app's Gemfile if needed)
122
122
  - `git` installed in environment (for Git panel data)
123
123
  - `minitest` or `rspec` in the host app's bundle (required for the test runner)
124
+ - `actioncable` framework/gem (optional, required only for realtime file-change push + websocket state saves)
124
125
 
125
126
  All lint and test tools are auto-detected at runtime. The engine gracefully disables features if the tools are not available. Neither `rubocop`, `haml_lint`, nor any test framework are runtime dependencies of the gem itself — they are discovered from the host app's environment.
126
127
 
128
+ ### Realtime via Action Cable (Optional)
129
+
130
+ Mbeditor works without Action Cable. If Action Cable is unavailable, unreachable, or returns transient errors, the editor automatically falls back to polling.
131
+
132
+ To enable realtime features in a host app:
133
+
134
+ 1. Ensure Action Cable is enabled in the host app (for apps that do not load it by default, add the framework/gem explicitly).
135
+ 2. Mount cable in host routes:
136
+
137
+ ```ruby
138
+ mount ActionCable.server => '/cable'
139
+ ```
140
+
141
+ 3. Make Action Cable JavaScript available to the page (for asset-pipeline apps, `actioncable.js` is typically sufficient).
142
+
143
+ If any of these are missing, mbeditor still runs in polling mode.
144
+
127
145
  ### Syntax Highlighting Support
128
146
  Monaco runtime assets are served from the engine route namespace (`/mbeditor/monaco-editor/*` and `/mbeditor/monaco_worker.js`).
129
147
  The gem includes syntax highlighting for common Rails and React development file types:
@@ -11,6 +11,10 @@ var WebSocketService = (function () {
11
11
  var _subscription = null;
12
12
  var _connected = false;
13
13
  var _filesChangedCallbacks = [];
14
+ var _serverSupportsWs = false;
15
+ var _reconnectTimer = null;
16
+ var _lastCableAttemptAt = 0;
17
+ var RECONNECT_INTERVAL_MS = 30000;
14
18
 
15
19
  // ---------------------------------------------------------------------------
16
20
  // Internal helpers
@@ -20,33 +24,78 @@ var WebSocketService = (function () {
20
24
  return typeof window.ActionCable !== 'undefined';
21
25
  }
22
26
 
27
+ function _cableUrl() {
28
+ return window.MBEDITOR_CABLE_URL || '/cable';
29
+ }
30
+
23
31
  function _getConsumer() {
24
32
  // Reuse an existing consumer the host app may have already created (App.cable
25
33
  // is the Rails default). Fall back to creating our own.
26
34
  if (typeof window.App !== 'undefined' && window.App.cable) {
27
35
  return window.App.cable;
28
36
  }
29
- var cableUrl = window.MBEDITOR_CABLE_URL || '/cable';
30
- return window.ActionCable.createConsumer(cableUrl);
37
+ return window.ActionCable.createConsumer(_cableUrl());
31
38
  }
32
39
 
33
- function _emitFilesChanged(data) {
34
- _filesChangedCallbacks.forEach(function (fn) {
35
- try { fn(data); } catch (e) { /* ignore */ }
40
+ function _cleanupConsumer() {
41
+ if (_subscription) {
42
+ try { _subscription.unsubscribe(); } catch (e) { /* ignore */ }
43
+ _subscription = null;
44
+ }
45
+ if (_consumer && typeof _consumer.disconnect === 'function') {
46
+ try { _consumer.disconnect(); } catch (e) { /* ignore */ }
47
+ }
48
+ _consumer = null;
49
+ _connected = false;
50
+ }
51
+
52
+ function _isCableRelatedRejection(reason) {
53
+ var url = _cableUrl();
54
+ if (!reason) return false;
55
+
56
+ // Common browser shape: ErrorEvent with target/currentTarget = WebSocket.
57
+ var target = reason.target || reason.currentTarget;
58
+ if (target && typeof target.url === 'string' && target.url.indexOf(url) !== -1) {
59
+ return true;
60
+ }
61
+
62
+ // Some environments stringify this as "[object Event]"; only suppress if
63
+ // it happened right after a cable connection attempt.
64
+ var ageMs = Date.now() - _lastCableAttemptAt;
65
+ var reasonText = String(reason);
66
+ if (reasonText.indexOf('[object Event]') !== -1 && ageMs >= 0 && ageMs < 3000) {
67
+ return true;
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ function _installUnhandledRejectionGuard() {
74
+ if (window.__mbeditorCableRejectionGuardInstalled) return;
75
+ window.__mbeditorCableRejectionGuardInstalled = true;
76
+ window.addEventListener('unhandledrejection', function (event) {
77
+ if (_isCableRelatedRejection(event.reason)) {
78
+ // Keep cable handshake failures internal; polling fallback remains active.
79
+ event.preventDefault();
80
+ }
36
81
  });
37
82
  }
38
83
 
39
- // ---------------------------------------------------------------------------
40
- // Public API
41
- // ---------------------------------------------------------------------------
84
+ function _scheduleReconnect() {
85
+ if (_reconnectTimer || !_serverSupportsWs) return;
86
+ _reconnectTimer = setTimeout(function () {
87
+ _reconnectTimer = null;
88
+ _attemptConnect();
89
+ }, RECONNECT_INTERVAL_MS);
90
+ }
42
91
 
43
- // Call once after the workspace response is received.
44
- // serverSupportsWs: boolean from workspace.actionCableEnabled
45
- function connect(serverSupportsWs) {
46
- if (!serverSupportsWs || !_isActionCableAvailable()) {
47
- return; // polling remains the only refresh mechanism
92
+ function _attemptConnect() {
93
+ if (!_serverSupportsWs || !_isActionCableAvailable() || _connected || _subscription) {
94
+ return;
48
95
  }
49
96
 
97
+ _lastCableAttemptAt = Date.now();
98
+
50
99
  try {
51
100
  _consumer = _getConsumer();
52
101
  _subscription = _consumer.subscriptions.create(
@@ -56,15 +105,12 @@ var WebSocketService = (function () {
56
105
  _connected = true;
57
106
  },
58
107
  disconnected: function () {
59
- _connected = false;
108
+ _cleanupConsumer();
109
+ _scheduleReconnect();
60
110
  },
61
111
  rejected: function () {
62
- _connected = false;
63
- // Channel was rejected — unsubscribe so we stop trying.
64
- if (_subscription) {
65
- _subscription.unsubscribe();
66
- _subscription = null;
67
- }
112
+ _cleanupConsumer();
113
+ _scheduleReconnect();
68
114
  },
69
115
  received: function (data) {
70
116
  if (data && data.type === 'files_changed') {
@@ -74,18 +120,41 @@ var WebSocketService = (function () {
74
120
  }
75
121
  );
76
122
  } catch (e) {
77
- // Any setup error means we silently stay in polling-only mode.
78
- _connected = false;
79
- _subscription = null;
123
+ // Any setup error means we silently stay in polling-only mode for now.
124
+ _cleanupConsumer();
125
+ _scheduleReconnect();
126
+ }
127
+ }
128
+
129
+ function _emitFilesChanged(data) {
130
+ _filesChangedCallbacks.forEach(function (fn) {
131
+ try { fn(data); } catch (e) { /* ignore */ }
132
+ });
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Public API
137
+ // ---------------------------------------------------------------------------
138
+
139
+ // Call once after the workspace response is received.
140
+ // serverSupportsWs: boolean from workspace.actionCableEnabled
141
+ function connect(serverSupportsWs) {
142
+ _serverSupportsWs = !!serverSupportsWs;
143
+ if (!_serverSupportsWs || !_isActionCableAvailable()) {
144
+ return; // polling remains the only refresh mechanism
80
145
  }
146
+ _installUnhandledRejectionGuard();
147
+ _attemptConnect();
148
+ _scheduleReconnect();
81
149
  }
82
150
 
83
151
  function disconnect() {
84
- if (_subscription) {
85
- _subscription.unsubscribe();
86
- _subscription = null;
152
+ _serverSupportsWs = false;
153
+ if (_reconnectTimer) {
154
+ clearTimeout(_reconnectTimer);
155
+ _reconnectTimer = null;
87
156
  }
88
- _connected = false;
157
+ _cleanupConsumer();
89
158
  }
90
159
 
91
160
  // Returns true only when the WebSocket is currently live.
@@ -841,4 +841,23 @@
841
841
  @keyframes mbeditor-spin {
842
842
  to { transform: rotate(360deg); }
843
843
  }
844
+
845
+ @media (prefers-reduced-motion: reduce) {
846
+ .mbeditor-spinner,
847
+ .editor-loading-spinner,
848
+ .search-loading-spinner {
849
+ animation-duration: 0.75s !important;
850
+ animation-delay: 0s !important;
851
+ animation-iteration-count: infinite !important;
852
+ }
853
+
854
+ .editor-loading-spinner {
855
+ animation-duration: 0.65s !important;
856
+ }
857
+
858
+ .search-loading-spinner {
859
+ animation-duration: 0.7s !important;
860
+ }
861
+ }
862
+
844
863
  .cdiff-ctx { color: var(--ide-text-muted); }
@@ -4,12 +4,14 @@ require "fileutils"
4
4
  require "pathname"
5
5
 
6
6
  module Mbeditor
7
- class EditorChannel < ActionCable::Channel::Base
7
+ CableBaseClass = defined?(ActionCable::Channel::Base) ? ActionCable::Channel::Base : Object
8
+
9
+ class EditorChannel < CableBaseClass
8
10
  STATE_MAX_BYTES = 1 * 1024 * 1024
9
11
  SAFE_BRANCH_NAME = /\A[a-zA-Z0-9._\-\/]+\z/
10
12
 
11
13
  def subscribed
12
- stream_from "mbeditor_editor"
14
+ stream_from "mbeditor_editor" if respond_to?(:stream_from)
13
15
  end
14
16
 
15
17
  def unsubscribed
@@ -39,7 +39,7 @@ module Mbeditor
39
39
  blameAvailable: git_blame_available?,
40
40
  redmineEnabled: Mbeditor.configuration.redmine_enabled == true,
41
41
  testAvailable: test_available?,
42
- actionCableEnabled: defined?(ActionCable::Channel::Base) ? true : false
42
+ actionCableEnabled: action_cable_enabled?
43
43
  }
44
44
  end
45
45
 
@@ -691,6 +691,23 @@ module Mbeditor
691
691
  str.match?(SAFE_BRANCH_NAME) ? str : nil
692
692
  end
693
693
 
694
+ def action_cable_enabled?
695
+ return false unless defined?(ActionCable::Channel::Base)
696
+
697
+ mount_path = begin
698
+ ActionCable.server.config.mount_path
699
+ rescue StandardError
700
+ nil
701
+ end
702
+ mount_path = '/cable' if mount_path.blank?
703
+
704
+ Rails.application.routes.routes.any? do |route|
705
+ route.path.spec.to_s.start_with?(mount_path)
706
+ end
707
+ rescue StandardError
708
+ false
709
+ end
710
+
694
711
  def allow_missing_file?
695
712
  %w[1 true yes on].include?(params[:allow_missing].to_s.downcase)
696
713
  end
@@ -19,10 +19,38 @@ module Mbeditor
19
19
  # Tagged-logging compat — the block body still passes through the filter.
20
20
  def tagged(*tags, &block)
21
21
  if __getobj__.respond_to?(:tagged)
22
- __getobj__.tagged(*tags) { block.call }
23
- else
22
+ __getobj__.tagged(*tags, &block)
23
+ elsif block
24
24
  block.call
25
+ else
26
+ self
25
27
  end
26
28
  end
29
+
30
+ # Rails/ActiveSupport logger compatibility. Some logger stacks call these
31
+ # methods even when the underlying logger is not TaggedLogging.
32
+ def current_tags
33
+ return __getobj__.current_tags if __getobj__.respond_to?(:current_tags)
34
+
35
+ []
36
+ end
37
+
38
+ def push_tags(*tags)
39
+ return __getobj__.push_tags(*tags) if __getobj__.respond_to?(:push_tags)
40
+
41
+ tags
42
+ end
43
+
44
+ def pop_tags(count = 1)
45
+ return __getobj__.pop_tags(count) if __getobj__.respond_to?(:pop_tags)
46
+
47
+ []
48
+ end
49
+
50
+ def clear_tags!
51
+ return __getobj__.clear_tags! if __getobj__.respond_to?(:clear_tags!)
52
+
53
+ nil
54
+ end
27
55
  end
28
56
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.4.2"
4
+ VERSION = "0.4.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan