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)
Changes
@@@ -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 };nnew file mode 100644@@@ -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); });
@@ -10,6 +10,10 @@
@@ -10,6 +10,10 @@
*
*
* The window is kept only LOCAL_TTL_MS — long enough that one page's latest/trending/languages
* The window is kept only LOCAL_TTL_MS — long enough that one page's latest/trending/languages
* share a single query.
* 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');
const source = require('./source');
@@ -18,6 +22,7 @@ const LOCAL_TTL_MS = 2000;
@@ -18,6 +22,7 @@ const LOCAL_TTL_MS = 2000;
const PER_PAGE = 24;
const PER_PAGE = 24;
let _recent = { at: 0, promise: null, pastes: [] };
let _recent = { at: 0, promise: null, pastes: [] };
+const _byType = new Map(); // 'paste' | 'screenshot' → { at, promise, pastes }
function _textOnlyPublic(rows) {
function _textOnlyPublic(rows) {
return (rows || []).filter((p) => p && p.slug && (p.visibility === 'public' || p.visibility == null));
return (rows || []).filter((p) => p && p.slug && (p.visibility === 'public' || p.visibility == null));
@@ -34,6 +39,19 @@ async function recent() {
@@ -34,6 +39,19 @@ async function recent() {
return _recent.promise;
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 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 || '')); }
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
@@ -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. */
/** Pastes in the same language first, then the newest, never the paste itself. */
async function related(paste, n = 6) {
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 lang = paste.language && paste.language !== 'text' ? paste.language : null;
const same = lang ? rows.filter((p) => p.language === lang) : [];
const same = lang ? rows.filter((p) => p.language === lang) : [];
const rest = rows.filter((p) => !same.includes(p));
const rest = rows.filter((p) => !same.includes(p));
@@ -52,7 +70,8 @@ async function related(paste, n = 6) {
@@ -52,7 +70,8 @@ async function related(paste, n = 6) {
/** [{ language, count }] over the recent window, most common first. */
/** [{ language, count }] over the recent window, most common first. */
async function languages() {
async function languages() {
const counts = new Map();
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');
const l = p.type === 'screenshot' ? 'image' : (p.language || 'text');
counts.set(l, (counts.get(l) || 0) + 1);
counts.set(l, (counts.get(l) || 0) + 1);
}
}
@@ -89,8 +108,11 @@ async function browse(query = {}, ctx = {}) {
@@ -89,8 +108,11 @@ async function browse(query = {}, ctx = {}) {
if (q) {
if (q) {
const out = await source.listPastes({ limit: WINDOW, offset: 0, search: q, type: 'paste' }, ctx);
const out = await source.listPastes({ limit: WINDOW, offset: 0, search: q, type: 'paste' }, ctx);
rows = _textOnlyPublic(out && out.pastes);
rows = _textOnlyPublic(out && out.pastes);
+ } else if (type === 'all') {
+ const [text, images] = await Promise.all([recentOf('paste'), recentOf('screenshot')]);
+ rows = [...text, ...images];
} else {
} else {
- rows = await recent();
+ rows = await recentOf(type === 'images' ? 'screenshot' : 'paste');
}
}
rows = rows.filter(_ofType(type));
rows = rows.filter(_ofType(type));
if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);
if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);
@@ -104,6 +126,6 @@ async function browse(query = {}, ctx = {}) {
@@ -104,6 +126,6 @@ async function browse(query = {}, ctx = {}) {
}
}
/** For tests and a hot reload after a write. */
/** 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 };
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -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); });