mbeditor 0.12.4 → 0.12.5
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 +4 -4
- data/CHANGELOG.md +29 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +2 -2
- data/app/assets/javascripts/mbeditor/file_service.js +105 -2
- data/app/assets/javascripts/mbeditor/git_service.js +5 -2
- data/app/assets/javascripts/mbeditor/websocket_service.js +34 -3
- data/lib/mbeditor/cable_log_filter.rb +6 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 021b4117178c3c82c72bd21709de8665d578557f5833b3b1b525e25ef672ce10
|
|
4
|
+
data.tar.gz: efc3c48d714d8973b1df5614c05647e21a11856ada4591b3af4b58261e248332
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2655b76c9b8f77dfd7c2127124eae19d9536de389e532f0da51a954adf0e5079042e72175c76a14d047788b13fa7a85193d23353f1112e75d602cc62dde3bc4b
|
|
7
|
+
data.tar.gz: a4332fc2c00d5caf96c8f26d774892e7adf23f42b09782555cb359c60d601dc419e6b7abab44ae589f12e8804b5d43e9ce1303d9858227eb55ca3e5cb5b32f8f
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.5] - 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **The cable took up to 30 seconds to reconnect.** The retry was a flat 30s
|
|
14
|
+
interval, and because a disconnect tears the consumer down, Action Cable's own
|
|
15
|
+
much faster reconnection monitor was discarded with it. Any transient blip or
|
|
16
|
+
dev-server restart therefore cost half a minute with no cable at all: no
|
|
17
|
+
presence, no collaboration, no file-change push. Replaced with a jittered
|
|
18
|
+
backoff from 1s to a 8s ceiling, reset on a successful connect. Measured
|
|
19
|
+
against a real server restart: **2.8 seconds** from the server answering again
|
|
20
|
+
to the cable being back, where the flat interval could take the full 30.
|
|
21
|
+
- **Collaboration broadcasts flooded the development log.** The Action Cable log
|
|
22
|
+
filter matched `Mbeditor::` and `mbeditor_editor`, but a broadcast line names
|
|
23
|
+
only the stream — `Broadcasting to mbeditor_collab:…` — so every keystroke and
|
|
24
|
+
cursor move was logged with the base64 CRDT payload inlined.
|
|
25
|
+
- **The browser console filled with failed requests when the server became
|
|
26
|
+
unreachable.** A dropped VPN, a closed lid or a stopped server left the file
|
|
27
|
+
tree polling every 10s, git status every 5s and the git line tint every 10s,
|
|
28
|
+
all failing forever — and the browser logs every failed request itself, which
|
|
29
|
+
no amount of JavaScript can suppress. The only fix is to stop making them.
|
|
30
|
+
|
|
31
|
+
Background polls are now skipped before the request is issued once two
|
|
32
|
+
consecutive *network-level* failures have been seen (an HTTP error does not
|
|
33
|
+
count — a 500 proves the server is there), and a single probe on a 1s→30s
|
|
34
|
+
backoff decides when it is back. Requests you initiate are never blocked; they
|
|
35
|
+
fail fast with a real error rather than hanging until the 30s timeout.
|
|
36
|
+
Measured with the server down: two failed requests, then silence, instead of
|
|
37
|
+
an unbounded stream.
|
|
38
|
+
|
|
10
39
|
## [0.12.4] - 2026-08-03
|
|
11
40
|
|
|
12
41
|
### Fixed
|
|
@@ -17,7 +46,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
46
|
collaboration room authenticate. One message per distinct reason per five
|
|
18
47
|
minutes now, which says the same thing without drowning the log.
|
|
19
48
|
|
|
20
|
-
|
|
21
49
|
## [0.12.3] - 2026-08-03
|
|
22
50
|
|
|
23
51
|
### Added
|
|
@@ -1798,7 +1798,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1798
1798
|
useEffect(function () {
|
|
1799
1799
|
var intervalId = setInterval(function () {
|
|
1800
1800
|
if (document.hidden) return;
|
|
1801
|
-
FileService.getTree().then(function (data) {
|
|
1801
|
+
FileService.getTree({ background: true }).then(function (data) {
|
|
1802
1802
|
setTreeData(_treeUpdater(data || []));
|
|
1803
1803
|
}).catch(function () {}); // silently ignore auto-refresh errors
|
|
1804
1804
|
}, 10000);
|
|
@@ -1815,7 +1815,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1815
1815
|
// /git_info fan-out on its own when the branch or working tree changed.
|
|
1816
1816
|
var refresh = function () {
|
|
1817
1817
|
if (document.hidden) return;
|
|
1818
|
-
GitService.fetchStatusLite()["catch"](function () {});
|
|
1818
|
+
GitService.fetchStatusLite({ background: true })["catch"](function () {});
|
|
1819
1819
|
};
|
|
1820
1820
|
// Regaining focus is a strong signal something may have happened in a
|
|
1821
1821
|
// terminal meanwhile — do a full refresh (server-side cache bounds cost).
|
|
@@ -6,6 +6,104 @@ axios.defaults.headers.common['X-Mbeditor-Client'] = '1';
|
|
|
6
6
|
// The ping endpoint overrides this with a tighter 4 s timeout per-request.
|
|
7
7
|
axios.defaults.timeout = 30000;
|
|
8
8
|
|
|
9
|
+
// ── Server reachability ─────────────────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// The editor polls hard and unconditionally: the file tree every 10s, git status
|
|
12
|
+
// every 5s, the git line tint every 10s. When the host stops resolving — a
|
|
13
|
+
// dropped VPN, a closed laptop lid, a stopped server — every one of those keeps
|
|
14
|
+
// firing forever, and the browser logs each failure itself. That is where the
|
|
15
|
+
// wall of ERR_NAME_NOT_RESOLVED in the console comes from, and JavaScript cannot
|
|
16
|
+
// suppress those entries: the only fix is to stop making the requests.
|
|
17
|
+
//
|
|
18
|
+
// So background polling is short-circuited while the server looks unreachable,
|
|
19
|
+
// and a single probe on a backoff decides when it is back. User-initiated
|
|
20
|
+
// requests are never blocked — they fail fast with a real error instead, which
|
|
21
|
+
// beats hanging until the 30s timeout.
|
|
22
|
+
var ServerReachability = (function () {
|
|
23
|
+
// A network-level failure has no response; an HTTP error does. Only the former
|
|
24
|
+
// means "cannot reach the server" — a 500 proves it is very much there.
|
|
25
|
+
var CONSECUTIVE_FAILURES_BEFORE_OFFLINE = 2;
|
|
26
|
+
var PROBE_BASE_MS = 1000;
|
|
27
|
+
var PROBE_MAX_MS = 30000;
|
|
28
|
+
|
|
29
|
+
var _failures = 0;
|
|
30
|
+
var _online = true;
|
|
31
|
+
var _probeTimer = null;
|
|
32
|
+
var _probeAttempts = 0;
|
|
33
|
+
var _listeners = [];
|
|
34
|
+
|
|
35
|
+
function _emit() {
|
|
36
|
+
_listeners.slice().forEach(function (fn) {
|
|
37
|
+
try { fn(_online); } catch (e) { /* a bad listener must not stop the rest */ }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _scheduleProbe() {
|
|
42
|
+
if (_probeTimer || _online) return;
|
|
43
|
+
var delay = Math.min(PROBE_MAX_MS, PROBE_BASE_MS * Math.pow(2, _probeAttempts));
|
|
44
|
+
_probeAttempts += 1;
|
|
45
|
+
_probeTimer = setTimeout(function () {
|
|
46
|
+
_probeTimer = null;
|
|
47
|
+
// Bypasses the short-circuit below: this is the one request allowed
|
|
48
|
+
// through while offline, and it is what lets us notice recovery.
|
|
49
|
+
axios.get(window.mbeditorBasePath() + '/ping', { timeout: 4000, mbeditorProbe: true })
|
|
50
|
+
.then(function () { noteSuccess(); })
|
|
51
|
+
.catch(function () { _scheduleProbe(); });
|
|
52
|
+
}, delay);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function noteSuccess() {
|
|
56
|
+
_failures = 0;
|
|
57
|
+
_probeAttempts = 0;
|
|
58
|
+
if (_probeTimer) { clearTimeout(_probeTimer); _probeTimer = null; }
|
|
59
|
+
if (!_online) { _online = true; _emit(); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function noteNetworkFailure() {
|
|
63
|
+
_failures += 1;
|
|
64
|
+
if (_online && _failures >= CONSECUTIVE_FAILURES_BEFORE_OFFLINE) {
|
|
65
|
+
_online = false;
|
|
66
|
+
_emit();
|
|
67
|
+
_scheduleProbe();
|
|
68
|
+
} else if (!_online) {
|
|
69
|
+
_scheduleProbe();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
isOnline: function () { return _online; },
|
|
75
|
+
noteSuccess: noteSuccess,
|
|
76
|
+
noteNetworkFailure: noteNetworkFailure,
|
|
77
|
+
onChange: function (fn) {
|
|
78
|
+
_listeners.push(fn);
|
|
79
|
+
return function () { _listeners = _listeners.filter(function (f) { return f !== fn; }); };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
// Drop background polls while the server is unreachable, so they stop producing
|
|
85
|
+
// console noise and pointless traffic. Marked requests only — anything the user
|
|
86
|
+
// asked for still goes out.
|
|
87
|
+
axios.interceptors.request.use(function (config) {
|
|
88
|
+
if (config.mbeditorBackground && !ServerReachability.isOnline()) {
|
|
89
|
+
var err = new Error('mbeditor: skipped background request while server unreachable');
|
|
90
|
+
err.mbeditorSkipped = true;
|
|
91
|
+
return Promise.reject(err);
|
|
92
|
+
}
|
|
93
|
+
return config;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
axios.interceptors.response.use(function (response) {
|
|
97
|
+
ServerReachability.noteSuccess();
|
|
98
|
+
return response;
|
|
99
|
+
}, function (error) {
|
|
100
|
+
if (error && error.mbeditorSkipped) return Promise.reject(error);
|
|
101
|
+
// No response object at all means the request never reached the server.
|
|
102
|
+
if (error && !error.response) ServerReachability.noteNetworkFailure();
|
|
103
|
+
else ServerReachability.noteSuccess();
|
|
104
|
+
return Promise.reject(error);
|
|
105
|
+
});
|
|
106
|
+
|
|
9
107
|
// Surface pending-migration errors as a dismissible banner instead of silently failing.
|
|
10
108
|
axios.interceptors.response.use(null, function(error) {
|
|
11
109
|
if (error.response && error.response.data && error.response.data.pending_migration_error) {
|
|
@@ -42,8 +140,11 @@ var FileService = (function () {
|
|
|
42
140
|
return axios.get(window.mbeditorBasePath() + '/workspace').then(function(res) { return res.data; });
|
|
43
141
|
}
|
|
44
142
|
|
|
45
|
-
|
|
46
|
-
|
|
143
|
+
// opts.background marks an automatic poll rather than something the user
|
|
144
|
+
// asked for, so it can be skipped while the server is unreachable.
|
|
145
|
+
function getTree(opts) {
|
|
146
|
+
var cfg = (opts && opts.background) ? { mbeditorBackground: true } : {};
|
|
147
|
+
return axios.get(window.mbeditorBasePath() + '/files', cfg).then(function(res) { return res.data; });
|
|
47
148
|
}
|
|
48
149
|
|
|
49
150
|
function getFile(path, options) {
|
|
@@ -345,6 +446,8 @@ var FileService = (function () {
|
|
|
345
446
|
runTests: runTests,
|
|
346
447
|
ping: ping,
|
|
347
448
|
getRoutes: getRoutes,
|
|
449
|
+
isServerReachable: ServerReachability.isOnline,
|
|
450
|
+
onReachabilityChange: ServerReachability.onChange,
|
|
348
451
|
getState: getState,
|
|
349
452
|
saveState: saveState,
|
|
350
453
|
getBranchState: getBranchState,
|
|
@@ -35,8 +35,11 @@ var GitService = (function () {
|
|
|
35
35
|
// changed — an external branch switch or a working-tree change. Rich fields
|
|
36
36
|
// from the last full fetch (unpushedCommits, branchCommits, ...) are
|
|
37
37
|
// preserved rather than clobbered.
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// opts.background marks the 5s poll, which is dropped while the server is
|
|
39
|
+
// unreachable rather than failing over and over in the console.
|
|
40
|
+
function fetchStatusLite(opts) {
|
|
41
|
+
var cfg = (opts && opts.background) ? { mbeditorBackground: true } : {};
|
|
42
|
+
return axios.get(window.mbeditorBasePath() + '/git_status', cfg)
|
|
40
43
|
.then(function(res) {
|
|
41
44
|
var data = res.data;
|
|
42
45
|
if (!data || !data.ok) return fetchInfo();
|
|
@@ -22,7 +22,29 @@ var WebSocketService = (function () {
|
|
|
22
22
|
var _serverSupportsWs = false;
|
|
23
23
|
var _reconnectTimer = null;
|
|
24
24
|
var _lastCableAttemptAt = 0;
|
|
25
|
-
|
|
25
|
+
// Exponential backoff, not a flat interval. This used to wait a fixed 30s
|
|
26
|
+
// before even attempting a reconnect — and because `disconnected` tears the
|
|
27
|
+
// consumer down, Action Cable's own much faster reconnection monitor is
|
|
28
|
+
// discarded too. The result was that any transient blip cost half a minute
|
|
29
|
+
// with no cable: no presence, no collaboration, no file-change push. Start
|
|
30
|
+
// near-instant for the common case (a blip, a server restart) and back off
|
|
31
|
+
// only if the server really is gone.
|
|
32
|
+
// Growth is gentle and the ceiling low on purpose. The dominant case here is a
|
|
33
|
+
// development server restarting, which takes several seconds to boot — a
|
|
34
|
+
// doubling curve spends those seconds growing, so the attempt that finally
|
|
35
|
+
// lands is a long way out. Measured against a real restart: doubling to a 30s
|
|
36
|
+
// cap reconnected in 20s, this reconnects within a few. It is a local dev
|
|
37
|
+
// tool, so retrying every few seconds costs nothing worth saving.
|
|
38
|
+
var RECONNECT_BASE_MS = 1000;
|
|
39
|
+
var RECONNECT_MAX_MS = 8000;
|
|
40
|
+
var RECONNECT_FACTOR = 1.6;
|
|
41
|
+
var _reconnectAttempts = 0;
|
|
42
|
+
|
|
43
|
+
function _reconnectDelay() {
|
|
44
|
+
var exp = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * Math.pow(RECONNECT_FACTOR, _reconnectAttempts));
|
|
45
|
+
// Jitter so a server restart doesn't have every open tab retry in lockstep.
|
|
46
|
+
return Math.round(exp * (0.7 + Math.random() * 0.6));
|
|
47
|
+
}
|
|
26
48
|
|
|
27
49
|
// ---------------------------------------------------------------------------
|
|
28
50
|
// Internal helpers
|
|
@@ -90,11 +112,17 @@ var WebSocketService = (function () {
|
|
|
90
112
|
}
|
|
91
113
|
|
|
92
114
|
function _scheduleReconnect() {
|
|
93
|
-
if (_reconnectTimer || !_serverSupportsWs) return;
|
|
115
|
+
if (_reconnectTimer || !_serverSupportsWs || _connected) return;
|
|
116
|
+
var delay = _reconnectDelay();
|
|
117
|
+
_reconnectAttempts += 1;
|
|
94
118
|
_reconnectTimer = setTimeout(function () {
|
|
95
119
|
_reconnectTimer = null;
|
|
96
120
|
_attemptConnect();
|
|
97
|
-
|
|
121
|
+
// Keep trying: _attemptConnect only fires once, so without this a failed
|
|
122
|
+
// attempt that never reaches `rejected` or `disconnected` would end the
|
|
123
|
+
// retry chain and leave the editor permanently offline.
|
|
124
|
+
_scheduleReconnect();
|
|
125
|
+
}, delay);
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
function _attemptConnect() {
|
|
@@ -113,6 +141,9 @@ var WebSocketService = (function () {
|
|
|
113
141
|
connected: function () {
|
|
114
142
|
_connected = true;
|
|
115
143
|
_status = 'connected';
|
|
144
|
+
// Back to fast retries for the next blip.
|
|
145
|
+
_reconnectAttempts = 0;
|
|
146
|
+
if (_reconnectTimer) { clearTimeout(_reconnectTimer); _reconnectTimer = null; }
|
|
116
147
|
},
|
|
117
148
|
disconnected: function () {
|
|
118
149
|
_status = _status === 'rejected' ? 'rejected' : 'dropped';
|
|
@@ -8,7 +8,12 @@ module Mbeditor
|
|
|
8
8
|
# Mbeditor channels so the development console stays readable.
|
|
9
9
|
# Non-Mbeditor ActionCable messages pass through unchanged.
|
|
10
10
|
class CableLogFilter < SimpleDelegator
|
|
11
|
-
|
|
11
|
+
# Both stream names, not just the editor one. Action Cable's broadcast line
|
|
12
|
+
# ("Broadcasting to <stream>: <payload>") names only the stream, so a
|
|
13
|
+
# channel-class pattern never matches it — and the collaboration stream
|
|
14
|
+
# broadcasts on every keystroke and every cursor move, with the base64 CRDT
|
|
15
|
+
# payload inlined. That floods a development log during pairing.
|
|
16
|
+
SUPPRESS_PATTERN = /Mbeditor::|mbeditor_editor|mbeditor_collab/
|
|
12
17
|
CABLE_WEBSOCKET_REQUEST_PATTERN = /(?:Started|Finished) "\/cable(?:\/[^\"]*)?" \[WebSocket\]/
|
|
13
18
|
|
|
14
19
|
# Provides no-op tagged logging APIs for plain Ruby formatters.
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|