openvibers/openvibe.blog Commits
Blog collections load their posts' tags in parallel, not one query each (#169)
Changes
@@@ -41,33 +41,31 @@ function createReading({ store, blogs, posts: postsApi, publication, people, med};}-- /** Blogs of these rows, one lookup per distinct blog. */++ /** Blogs of these rows, one lookup per distinct blog, all at once. */async function blogsFor(rows) {-- const out = new Map();-- for (const id of new Set(rows.map((p) => p.blog_id))) out.set(id, await blogs.get(id));-- return out;++ const ids = [...new Set(rows.map((p) => p.blog_id))];++ return new Map(await Promise.all(ids.map(async (id) => [id, await blogs.get(id)])));}const revRefs = (rows) => rows.map((p) => ({ entityId: p.id, revision: p.published_revision }));-- /** List items for rows of published posts (possibly from several blogs): revisions in one query (no N+1). */++ /** List items for rows of published posts (possibly from several blogs): revisions in one query (no N+1), each post's tags in parallel. */async function listItems(rows, { perBlogLinks = true } = {}) {const [who, revs, blogById] = await Promise.all([people.many(rows.map((p) => p.author_subject)), store.revisions.getMany(revRefs(rows)), blogsFor(rows)]);-- const out = [];-- for (const [i, post] of rows.entries()) {++ const out = await Promise.all(rows.map(async (post, i) => {const rev = revs[i];-- if (!rev) continue;++ if (!rev) return null;const blog = blogById.get(post.blog_id);const person = who.get(post.author_subject);const { tags } = await termsView(post, blog, { networkTags: !perBlogLinks });-- out.push({++ return {post, rev, blog,url: publication.postPath(blog, post),author: person ? { name: person.name, url: urls.author(person, blog) } : null,tags,badge: { members: 'Members only', private: 'Private' }[post.visibility] || null,-- });-- }-- return out;++ };++ }));++ return out.filter(Boolean);}async function categoriesTree(blog) {@@@ -84,6 +84,13 @@ const ld = (text) => [...text.matchAll(/<script type="application\/ld\+json">([\for (const q of faq.mainEntity) assert.ok(text.includes(`<summary>${q.name.replace(/'/g, ''')}</summary>`), q.name);});++ await check('a tag page: every post tagged so, newest first, each carrying its tag links', async () => {++ const r = await t.get('/tags/patch-notes');++ assert.strictEqual(r.status, 200);++ assert.ok(r.text.indexOf('Patch notes: The Rundown') < r.text.indexOf('Patch notes: Voice channels'), 'newest first');++ assert.ok(r.text.includes('<a href="/tags/patch-notes">#patch-notes</a>') && r.text.includes('<a href="/tags/chat">#chat</a>'), 'each post keeps its tags');++ });++await check('an empty network: sections with nothing to show are left out', async () => {await db.prepare('DELETE FROM changelog_entries').run();await db.prepare('DELETE FROM blog_desk_posts').run();
@@ -41,33 +41,31 @@ function createReading({ store, blogs, posts: postsApi, publication, people, med
@@ -41,33 +41,31 @@ function createReading({ store, blogs, posts: postsApi, publication, people, med
};
};
}
}
- /** Blogs of these rows, one lookup per distinct blog. */
+ /** Blogs of these rows, one lookup per distinct blog, all at once. */
async function blogsFor(rows) {
async function blogsFor(rows) {
- const out = new Map();
- for (const id of new Set(rows.map((p) => p.blog_id))) out.set(id, await blogs.get(id));
- return out;
+ const ids = [...new Set(rows.map((p) => p.blog_id))];
+ return new Map(await Promise.all(ids.map(async (id) => [id, await blogs.get(id)])));
}
}
const revRefs = (rows) => rows.map((p) => ({ entityId: p.id, revision: p.published_revision }));
const revRefs = (rows) => rows.map((p) => ({ entityId: p.id, revision: p.published_revision }));
- /** List items for rows of published posts (possibly from several blogs): revisions in one query (no N+1). */
+ /** List items for rows of published posts (possibly from several blogs): revisions in one query (no N+1), each post's tags in parallel. */
async function listItems(rows, { perBlogLinks = true } = {}) {
async function listItems(rows, { perBlogLinks = true } = {}) {
const [who, revs, blogById] = await Promise.all([people.many(rows.map((p) => p.author_subject)), store.revisions.getMany(revRefs(rows)), blogsFor(rows)]);
const [who, revs, blogById] = await Promise.all([people.many(rows.map((p) => p.author_subject)), store.revisions.getMany(revRefs(rows)), blogsFor(rows)]);
- const out = [];
- for (const [i, post] of rows.entries()) {
+ const out = await Promise.all(rows.map(async (post, i) => {
const rev = revs[i];
const rev = revs[i];
- if (!rev) continue;
+ if (!rev) return null;
const blog = blogById.get(post.blog_id);
const blog = blogById.get(post.blog_id);
const person = who.get(post.author_subject);
const person = who.get(post.author_subject);
const { tags } = await termsView(post, blog, { networkTags: !perBlogLinks });
const { tags } = await termsView(post, blog, { networkTags: !perBlogLinks });
- out.push({
+ return {
post, rev, blog,
post, rev, blog,
url: publication.postPath(blog, post),
url: publication.postPath(blog, post),
author: person ? { name: person.name, url: urls.author(person, blog) } : null,
author: person ? { name: person.name, url: urls.author(person, blog) } : null,
tags,
tags,
badge: { members: 'Members only', private: 'Private' }[post.visibility] || null,
badge: { members: 'Members only', private: 'Private' }[post.visibility] || null,
- });
- }
- return out;
+ };
+ }));
+ return out.filter(Boolean);
}
}
async function categoriesTree(blog) {
async function categoriesTree(blog) {
@@ -84,6 +84,13 @@ const ld = (text) => [...text.matchAll(/<script type="application\/ld\+json">([\
@@ -84,6 +84,13 @@ const ld = (text) => [...text.matchAll(/<script type="application\/ld\+json">([\
for (const q of faq.mainEntity) assert.ok(text.includes(`<summary>${q.name.replace(/'/g, ''')}</summary>`), q.name);
for (const q of faq.mainEntity) assert.ok(text.includes(`<summary>${q.name.replace(/'/g, ''')}</summary>`), q.name);
});
});
+ await check('a tag page: every post tagged so, newest first, each carrying its tag links', async () => {
+ const r = await t.get('/tags/patch-notes');
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.text.indexOf('Patch notes: The Rundown') < r.text.indexOf('Patch notes: Voice channels'), 'newest first');
+ assert.ok(r.text.includes('<a href="/tags/patch-notes">#patch-notes</a>') && r.text.includes('<a href="/tags/chat">#chat</a>'), 'each post keeps its tags');
+ });
+
await check('an empty network: sections with nothing to show are left out', async () => {
await check('an empty network: sections with nothing to show are left out', async () => {
await db.prepare('DELETE FROM changelog_entries').run();
await db.prepare('DELETE FROM changelog_entries').run();
await db.prepare('DELETE FROM blog_desk_posts').run();
await db.prepare('DELETE FROM blog_desk_posts').run();