openvibers/openvibe.community Commits

Community: most viewed pastes and the language list see text pastes again (each kind has its own window; screenshots had filled the shared one) (#72)

OP OpenVibers committed 30 days ago · 636b9303f3 · 2 files, +84 −5

On openvibe.community the pastes page was empty in its "Most viewed" view: "0 pastes in the latest 200 … No pastes yet", beside 59 pastes on the Newest view. The language list showed only "image (200)".

**Cause.** The catalog answered "most viewed", the language filter, the language list and related pastes from one window: the latest 200 public pastes of every kind. The latest 200 are all screenshots, so no text paste was left in it.

**Fix.** Text pastes and images each get their own window (`recentOf('paste' | 'screenshot')`), using the store's own `type` filter and the same short cache.
- The Pastes view (the default) reads the text window.
- The Images view reads the image window.
- All reads both.
- The language list counts both, so the text languages show up beside "image".
- Related pastes come from the paste's own kind.

**Tests.** The new `test/paste-catalog.test.js` covers 200 screenshots newer than five text pastes:
- most viewed lists the four public text pastes (never the unlisted one);
- the language filter, images and all;
- the language counts;
- related order.

The full suite passes (58/58).

Changes

ddiff --git a/server/pastes/catalog.js b/server/pastes/catalog.js
iindex b4c8863..b33fed7 100644
---- a/server/pastes/catalog.js
++++ b/server/pastes/catalog.js
@@@ -10,6 +10,10 @@
*
* The window is kept only LOCAL_TTL_MS — long enough that one page's latest/trending/languages
* share a single query.
++ *
++ * Text pastes and images (screenshots) each have their own window too (recentOf): a run of screenshots
++ * filled the shared latest 200, and "most viewed", the language list and related pastes then showed no
++ * text paste at all ("0 pastes in the latest 200" beside 59 on the newest page).
*/
const source = require('./source');
@@@ -18,6 +22,7 @@ const LOCAL_TTL_MS = 2000;
const PER_PAGE = 24;
let _recent = { at: 0, promise: null, pastes: [] };
++const _byType = new Map(); // 'paste' | 'screenshot' → { at, promise, pastes }
function _textOnlyPublic(rows) {
return (rows || []).filter((p) => p && p.slug && (p.visibility === 'public' || p.visibility == null));
@@@ -34,6 +39,19 @@ async function recent() {
return _recent.promise;
}
++/** The latest public pastes of one type, 'paste' (text) or 'screenshot' (cached like recent()). Never throws. */
++async function recentOf(type) {
++ const now = Date.now();
++ const cur = _byType.get(type) || { at: 0, promise: null, pastes: [] };
++ if (cur.promise) return cur.promise;
++ if (now - cur.at < LOCAL_TTL_MS) return cur.pastes;
++ cur.promise = source.listPastes({ limit: WINDOW, offset: 0, type })
++ .then((out) => { _byType.set(type, { at: Date.now(), promise: null, pastes: _textOnlyPublic(out && out.pastes) }); return _byType.get(type).pastes; })
++ .catch((err) => { console.warn(`[Catalog] recent ${type}s unavailable:`, err.message); _byType.set(type, { at: Date.now() - LOCAL_TTL_MS + 5000, promise: null, pastes: cur.pastes }); return cur.pastes; });
++ _byType.set(type, cur);
++ return cur.promise;
++}
++
function byViews(a, b) { return (b.views || 0) - (a.views || 0) || (b.likes || 0) - (a.likes || 0) || String(b.created_at).localeCompare(String(a.created_at)); }
function byNewest(a, b) { return String(b.created_at || '').localeCompare(String(a.created_at || '')); }
@@@ -42,7 +60,7 @@ async function trending(n = 8) { return (await recent()).slice().sort(byViews).s
/** Pastes in the same language first, then the newest, never the paste itself. */
async function related(paste, n = 6) {
-- const rows = (await recent()).filter((p) => p.slug !== paste.slug);
++ const rows = (await recentOf(paste.type === 'screenshot' ? 'screenshot' : 'paste')).filter((p) => p.slug !== paste.slug);
const lang = paste.language && paste.language !== 'text' ? paste.language : null;
const same = lang ? rows.filter((p) => p.language === lang) : [];
const rest = rows.filter((p) => !same.includes(p));
@@@ -52,7 +70,8 @@ async function related(paste, n = 6) {
/** [{ language, count }] over the recent window, most common first. */
async function languages() {
const counts = new Map();
-- for (const p of await recent()) {
++ const [text, images] = await Promise.all([recentOf('paste'), recentOf('screenshot')]);
++ for (const p of [...text, ...images]) {
const l = p.type === 'screenshot' ? 'image' : (p.language || 'text');
counts.set(l, (counts.get(l) || 0) + 1);
}
@@@ -89,8 +108,11 @@ async function browse(query = {}, ctx = {}) {
if (q) {
const out = await source.listPastes({ limit: WINDOW, offset: 0, search: q, type: 'paste' }, ctx);
rows = _textOnlyPublic(out && out.pastes);
++ } else if (type === 'all') {
++ const [text, images] = await Promise.all([recentOf('paste'), recentOf('screenshot')]);
++ rows = [...text, ...images];
} else {
-- rows = await recent();
++ rows = await recentOf(type === 'images' ? 'screenshot' : 'paste');
}
rows = rows.filter(_ofType(type));
if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);
@@@ -104,6 +126,6 @@ async function browse(query = {}, ctx = {}) {
}
/** For tests and a hot reload after a write. */
--function reset() { _recent = { at: 0, promise: null, pastes: [] }; }
++function reset() { _recent = { at: 0, promise: null, pastes: [] }; _byType.clear(); }
--module.exports = { recent, latest, trending, related, languages, browse, normalizeBrowseQuery, reset, PER_PAGE, WINDOW };
++module.exports = { recent, recentOf, latest, trending, related, languages, browse, normalizeBrowseQuery, reset, PER_PAGE, WINDOW };
ddiff --git a/test/paste-catalog.test.js b/test/paste-catalog.test.js
nnew file mode 100644
iindex 0000000..33256c9
---- /dev/null
++++ b/test/paste-catalog.test.js
@@@ -0,0 +1,57 @@
++'use strict';
++// The paste catalog's windows: text pastes and images each get their own latest window, so a run of screenshots never
++// hides every text paste from "most viewed", the language list or related pastes (production 2026-10-14: the latest 200
++// public pastes were all screenshots and "most viewed" said "0 pastes in the latest 200" beside 59 on the newest page).
++const assert = require('assert');
++const source = require('../server/pastes/source');
++const catalog = require('../server/pastes/catalog');
++
++const at = (i) => new Date(Date.UTC(2026, 9, 1) + i * 60_000).toISOString();
++const shots = Array.from({ length: 200 }, (_, i) => ({ slug: `shot-${i}`, type: 'screenshot', visibility: 'public', views: i, created_at: at(1000 + i) }));
++const text = [
++ { slug: 'bash-1', type: 'paste', language: 'bash', visibility: 'public', views: 76, created_at: at(10) },
++ { slug: 'js-1', type: 'paste', language: 'javascript', visibility: 'public', views: 12, created_at: at(11) },
++ { slug: 'txt-1', type: 'paste', language: 'text', visibility: 'public', views: 60, created_at: at(12) },
++ { slug: 'bash-2', type: 'paste', language: 'bash', visibility: 'public', views: 3, created_at: at(13) },
++ { slug: 'secret', type: 'paste', language: 'bash', visibility: 'unlisted', views: 999, created_at: at(14) },
++];
++const all = [...shots, ...text].sort((a, b) => b.created_at.localeCompare(a.created_at));
++const asked = [];
++source.use({
++ list: async (_viewer, q) => {
++ asked.push(q.type || 'any');
++ const rows = all.filter((p) => !q.type || p.type === q.type);
++ return { pastes: rows.slice(q.offset || 0, (q.offset || 0) + q.limit), total: rows.length };
++ },
++});
++
++let passed = 0;
++const check = async (name, fn) => { catalog.reset(); await fn(); passed++; console.log(' ✓', name); };
++
++(async () => {
++ await check('most viewed lists the text pastes even when images fill the latest 200', async () => {
++ const r = await catalog.browse({ sort: 'views' });
++ assert.deepStrictEqual(r.pastes.map((p) => p.slug), ['bash-1', 'txt-1', 'js-1', 'bash-2'], 'public text pastes by views; never an unlisted one');
++ assert.strictEqual(r.total, 4);
++ });
++ await check('a language filter finds its pastes; images and all have their own windows', async () => {
++ assert.deepStrictEqual((await catalog.browse({ lang: 'bash' })).pastes.map((p) => p.slug), ['bash-2', 'bash-1']);
++ const images = await catalog.browse({ type: 'images', sort: 'views' });
++ assert.strictEqual(images.pastes[0].slug, 'shot-199');
++ assert.strictEqual(images.total, 200);
++ const both = await catalog.browse({ type: 'all', sort: 'views' });
++ assert.strictEqual(both.total, 204);
++ assert.strictEqual(both.pastes[0].slug, 'shot-199', 'all, by views: the most viewed of either kind');
++ assert.deepStrictEqual((await catalog.browse({ type: 'all', lang: 'bash' })).pastes.map((p) => p.slug), ['bash-2', 'bash-1'], 'and the text pastes are in it');
++ });
++ await check('the language list counts the text pastes beside the images', async () => {
++ const langs = Object.fromEntries((await catalog.languages()).map((l) => [l.language, l.count]));
++ assert.deepStrictEqual(langs, { image: 200, bash: 2, javascript: 1, text: 1 });
++ });
++ await check('related pastes of a text paste are text pastes, its own language first', async () => {
++ const rel = await catalog.related(text[0], 3);
++ assert.deepStrictEqual(rel.map((p) => p.slug), ['bash-2', 'txt-1', 'js-1'], 'its language first, then the newest');
++ assert.ok(asked.includes('paste'), 'asked the store for text pastes, not the shared window');
++ });
++ console.log(`paste catalog: ${passed} checks passed`);
++})().catch((err) => { console.error(err); process.exit(1); });