openvibers/openvibe.live Commits

Clicking a name in chat never waits on a Loading box (#589)

OP OpenVibers committed 10 days ago · 6c18926ee4 · 4 files, +66 −18

* Clicking a name in chat never waits on a Loading box

Maticus clicked a name and got 'Loading...' for minutes: the card waited on its profile with no time limit while
Chat restarted in a deploy round. The card now shows at once with what it always had (the name, Message, Channel)
and upgrades when the profile arrives, within eight seconds; the chat logs give up after ten and offer Try again.

* Every read through api() gives up after 20 seconds unless the caller sets its own signal

A request held at a restarting service during a deploy round kept boxes on
"Loading..." for minutes; with a limit the caller's error path shows instead.

Changes

ddiff --git a/public/js/app.js b/public/js/app.js
iindex 4c6effa2..fca950b3 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -237,6 +237,10 @@ function authHeaders() {
async function api(path, opts = {}) {
// Caller headers add to the defaults (spreading opts after them used to replace the lot, auth included).
const { headers, body, _retried, ...init } = opts;
++ // A read gives up after 20 seconds unless the caller set its own signal: during a deploy round a request held at a
++ // restarting service kept "Loading..." boxes up for minutes (11-03). The caller's error path takes it from there.
++ const method = String(init.method || 'GET').toUpperCase();
++ if (!init.signal && (method === 'GET' || method === 'HEAD') && typeof AbortSignal !== 'undefined' && AbortSignal.timeout) init.signal = AbortSignal.timeout(20000);
const res = await fetch(`${API}/api${path}`, {
...init,
headers: { 'Content-Type': 'application/json', ...authHeaders(), ...headers },
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex 68abd49e..dcccdd3c 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -3885,9 +3885,9 @@ function showChatContextMenu(event) {
menu.dataset.msgId = msgId || '';
menu.dataset.isRelay = isRelay ? '1' : '';
menu.dataset.sourcePlatform = sourcePlatform;
-- menu.innerHTML = `
-- <div class="ctx-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>
-- `;
++ // The basic card at once (the name, Message, Channel), upgraded when the profile arrives: a click never waits on a
++ // "Loading..." box (11-03: Maticus's sat for minutes while Chat restarted in a deploy round).
++ menu.innerHTML = basicContextCard(username);
// Position near the click
document.body.appendChild(menu);
@@@ -3991,22 +3991,30 @@ async function loadContextMenuProfile(menu, username, userId, isAnon) {
return;
}
try {
-- const profile = await api(`/chat/user/${encodeURIComponent(username)}/profile`);
++ // At most eight seconds: a profile that takes longer leaves the basic card, which already works.
++ const profile = await api(`/chat/user/${encodeURIComponent(username)}/profile`, { signal: AbortSignal.timeout(8000) });
++ if (!menu.isConnected) return; // dismissed meanwhile
renderContextMenu(menu, profile, username);
} catch {
-- menu.innerHTML = `
++ if (menu.isConnected) menu.innerHTML = basicContextCard(username);
++ }
++}
++
++/** The name card before (or without) the profile: the name, Message and Channel. Shown at once, never a spinner. */
++function basicContextCard(username) {
++ const name = String(username || '?');
++ return `
<div class="ctx-header">
-- <span class="ctx-avatar-letter">${esc(username)[0].toUpperCase()}</span>
++ <span class="ctx-avatar-letter">${esc(name)[0].toUpperCase()}</span>
<div class="ctx-info">
-- <span class="ctx-name">${esc(username)}</span>
++ <span class="ctx-name">${esc(name)}</span>
</div>
</div>
<div class="ctx-actions">
-- <button class="ctx-btn" data-username="${esc(username)}" onclick="ctxWhisper(this.dataset.username)"><i class="fa-solid fa-comment"></i> Message</button>
-- <button class="ctx-btn" data-username="${esc(username)}" onclick="ctxViewChannel(this.dataset.username)"><i class="fa-solid fa-user"></i> Channel</button>
++ <button class="ctx-btn" data-username="${esc(name)}" onclick="ctxWhisper(this.dataset.username)"><i class="fa-solid fa-comment"></i> Message</button>
++ <button class="ctx-btn" data-username="${esc(name)}" onclick="ctxViewChannel(this.dataset.username)"><i class="fa-solid fa-user"></i> Channel</button>
</div>
`;
-- }
}
// Reserved usernames that represent the site itself (system/changelog messages), not a
@@@ -4778,7 +4786,7 @@ function openChatLogsModal(username, userId, relay) {
</div>
</div>
<div class="chat-logs-body" id="chat-logs-body">
-- <div class="chat-logs-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>
++ <div class="chat-logs-loading" role="status">Loading…</div>
</div>
<div class="chat-logs-footer" id="chat-logs-footer"></div>
</div>
@@@ -4806,7 +4814,9 @@ async function loadChatLogs() {
const footer = document.getElementById('chat-logs-footer');
if (!body) return;
-- body.innerHTML = '<div class="chat-logs-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>';
++ body.innerHTML = '<div class="chat-logs-loading" role="status">Loading…</div>';
++ // At most ten seconds for a page of logs (11-03: a request held while Chat restarted kept this up for minutes).
++ const signal = AbortSignal.timeout(10000);
try {
let url, result;
@@@ -4815,24 +4825,24 @@ async function loadChatLogs() {
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
if (ctx.query) params.set('q', ctx.query);
url = `/chat/anon/${encodeURIComponent(ctx.anon)}/logs?${params}`;
-- result = await api(url);
++ result = await api(url, { signal });
} else if (ctx.relay) {
// Relay (external-platform) user history — matched by platform + username.
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
if (ctx.query) params.set('q', ctx.query);
url = `/chat/relay-user/${encodeURIComponent(ctx.relay.platform)}/${encodeURIComponent(ctx.relay.username)}/logs?${params}`;
-- result = await api(url);
++ result = await api(url, { signal });
} else if (ctx.userId && !ctx.query) {
// User history mode
url = `/chat/user/${ctx.userId}/history?limit=50&offset=${ctx.offset}`;
-- result = await api(url);
++ result = await api(url, { signal });
} else {
// Search mode
const params = new URLSearchParams({ limit: '50', offset: String(ctx.offset) });
if (ctx.query) params.set('q', ctx.query);
if (ctx.userId) params.set('user_id', String(ctx.userId));
url = `/chat/search?${params}`;
-- result = await api(url);
++ result = await api(url, { signal });
}
const msgs = result.messages || [];
@@@ -4866,7 +4876,7 @@ async function loadChatLogs() {
footer.innerHTML = total > 0 ? `<span class="log-page-info">${total} messages</span>` : '';
}
} catch (err) {
-- body.innerHTML = '<p class="muted" style="text-align:center;padding:20px">Failed to load logs</p>';
++ body.innerHTML = `<p class="muted" style="text-align:center;padding:20px">${err && err.name === 'TimeoutError' ? 'The logs are taking too long right now.' : 'Failed to load logs.'} <button type="button" class="btn btn-sm" onclick="loadChatLogs()">Try again</button></p>`;
}
}
ddiff --git a/scripts/perf/check-budgets.js b/scripts/perf/check-budgets.js
iindex 3809f4f8..820368cd 100644
---- a/scripts/perf/check-budgets.js
++++ b/scripts/perf/check-budgets.js
@@@ -22,7 +22,7 @@ const BUDGETS = {
htmlRawKB: 128, // 119.3 with the icons drawn on the server (2026-11-01, plan L54 K4: the OpenVibe glyphs in the HTML, 20.8 KB brotli); measured 94.9 before
htmlBrotliKB: 30,
jsFiles: 26, // measured 23 (incl. route scripts for "/")
-- jsRawKB: 1280, // 1279.2 (2026-11-03: openvibe-shared 4.107.0, open.<tld> brands; the home's reveal scroll listener hooked once); 1278.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack so the site's name reads whole on a phone); 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)
++ jsRawKB: 1290, // 1280.1 (2026-11-03: the chat name card shows at once, the logs give up after ten seconds; headroom for openvibe-shared 4.107.3); 1279.2 (2026-11-03: openvibe-shared 4.107.0, open.<tld> brands; the home's reveal scroll listener hooked once); 1278.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack so the site's name reads whole on a phone); 1277.1 (2026-11-02: the home's money four as one 'None yet' chip while all four are zero, open item 135); 1275.2 (2026-11-02: route placeholders in ov-skeleton.js, L52 step 6: a loading route shows its shape, not an empty box); 1268.4 (2026-11-02: openvibe-shared 4.103.0, the puzzle glyph, two-line sidebar rows, boost's fix and eight solids; the 404 offers the channel for a bare /name); 1266.3 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 1262.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 1259.8 (2026-11-02: content labels on cards and the player, L1); 1257.5 (2026-11-01: the Catch up rails load by themselves as you swipe, no Load more on a phone); 1254.2 (2026-11-01: openvibe-shared 4.87.0), about 1255 with #534 (feed and card avatars at their size); 1253.8 (2026-11-01: avatars ask Media for their drawn width, ovImgW); 1252.3 (2026-11-01: Following in the topics row, the picked topic remembered); 1250.2 (2026-11-01: topics live now, readable chat names on light themes); 1247.7 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 1244.3 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 1243.5 with openvibe-shared 4.64.0: the look travels between sites, the Look panel in the menu (2026-10-31); 1242.7 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face, and the ring on page moves (2026-10-31); 1238.8 with the home's featured stream once and the intro closed on phones (2026-10-31); 1237.4 with the home's two more tab blocks on phones (2026-10-30); 1233.9 with the home's Catch up tabs on phones (2026-10-30); 1230.8 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 1227.8 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 1223.5 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 1215.4 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); measured 1203.9 (2026-10-28: openvibe-shared 4.30.0, the bell center: tabs with counts, the Unread filter, thumbnails, swipe, the phone sheet; 4.31.0's ✦ adds about 3 KB); 1187.2 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0: opaque themed toasts held while a menu is open, grouping, priority, System cards and the rich bell panel); 1158.8 (2026-10-28: openvibe-shared 4.25.0, the menu's orb, messages, the phone sheet and auto-hide in the navbar; the messenger opens from the bar)
jsBrotliKB: 300, // 299.3 (2026-11-02: openvibe-shared 4.106.2, the balances stack); 298.4 (2026-11-02: route placeholders, L52 step 6); 296.7 (2026-11-02: openvibe-shared 4.103.0 and the 404's channel offer); 296.2 (2026-11-02: openvibe-shared 4.95.0, keyboard-reachable scrollers; the phone Pastes rail); 295.1 (2026-11-02: openvibe-shared 4.94.0, the Audio chip, the go-live card's labels); 294.6 (2026-11-02: content labels on cards and the player, L1); 294.1 (2026-11-01: the Catch up rails load by themselves as you swipe); 292.6 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 291.1 with openvibe-shared 4.57.1 (the Dock's async token and scope-aware controls, 4.57.0; the tool frame's facts, 4.56.0) (2026-10-31); 290.6 with the home's two more tab blocks on phones (2026-10-30); 289.8 with the home's Catch up tabs on phones (2026-10-30); 288.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 287.4 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); measured 283.2 (2026-10-28: openvibe-shared 4.30.0, the bell center as above); 279.6 (2026-10-28: openvibe-shared 4.27.0, Notifications 2.0 as above); 273.1 (2026-10-28: openvibe-shared 4.25.0 as above); 270.5 (2026-10-28: openvibe-shared 4.22.0, the navbar's search magnifier with / and Ctrl+K and the More menu; search.js and loading.js load only when used); 269.6 (2026-10-28: openvibe-shared 4.19.0, the account menu's gear and people-group icons inline; 268 on 2026-10-27: openvibe-shared 4.16.0, the folded balance chip, Across OpenVibe with site marks and the icon preload in the navbar); 265.3 (2026-10-27: openvibe-shared 4.15.0, the compact footer opens into the whole one, and the pulse line); 263.3 on 10-23 (openvibe-shared 4.9.0, status text twins in every theme); 262.5 on 10-17 (openvibe-shared 4.4.0, every site in the launcher)
cssRawKB: 270, // measured ~243 since the first-paint subset (style.core.css) replaced style.css (~717 before)
cssBrotliKB: 55, // measured ~47.5 (was ~130)
ddiff --git a/test/chat-card-never-waits.test.js b/test/chat-card-never-waits.test.js
nnew file mode 100644
iindex 00000000..4200b8f0
---- /dev/null
++++ b/test/chat-card-never-waits.test.js
@@@ -0,0 +1,34 @@
++'use strict';
++// The chat name card never waits (11-03: Maticus's "Loading..." box sat for minutes while Chat restarted in a deploy
++// round). Clicking a name shows the basic card at once (the name, Message, Channel) and upgrades it when the profile
++// arrives, within eight seconds; the chat logs give up after ten with a way to try again.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'chat.js'), 'utf8');
++assert.ok(!src.includes('<div class="ctx-loading">'), 'no "Loading..." box in the name card');
++assert.match(src, /menu\.innerHTML = basicContextCard\(username\);/, 'the basic card is shown at once');
++assert.match(src, /api\(`\/chat\/user\/\$\{encodeURIComponent\(username\)\}\/profile`, \{ signal: AbortSignal\.timeout\(8000\) \}\)/, 'the profile waits eight seconds at most');
++assert.match(src, /if \(!menu\.isConnected\) return;/, 'a dismissed card is left alone');
++
++const fn = /function basicContextCard\(username\) \{[\s\S]*?\n\}/.exec(src);
++assert.ok(fn, 'basicContextCard exists');
++const ctx = { esc: (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') };
++vm.createContext(ctx);
++vm.runInContext(`${fn[0]}\nthis.card = basicContextCard;`, ctx);
++const html = ctx.card('maticus');
++assert.match(html, /<span class="ctx-avatar-letter">M<\/span>/);
++assert.match(html, /ctxWhisper\(this\.dataset\.username\)[\s\S]*Message/);
++assert.match(html, /ctxViewChannel\(this\.dataset\.username\)[\s\S]*Channel/);
++assert.ok(!/spin|Loading/.test(html), 'nothing loading in it');
++assert.ok(!ctx.card('<b>x</b>').includes('<b>'), 'the name is escaped');
++
++const logs = /async function loadChatLogs\(\) \{[\s\S]*?\n\}/.exec(src)[0];
++assert.match(logs, /const signal = AbortSignal\.timeout\(10000\);/);
++assert.strictEqual((logs.match(/await api\(url, \{ signal \}\)/g) || []).length, 4, 'every logs request has the limit');
++assert.match(logs, /taking too long right now[\s\S]*onclick="loadChatLogs\(\)">Try again/);
++const app = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
++assert.match(app, /if \(!init\.signal && \(method === 'GET' \|\| method === 'HEAD'\)[^\n]*AbortSignal\.timeout\(20000\)/, 'every read through api() gives up after 20 s unless the caller set a signal');
++console.log('chat card never waits: all checks passed');