* fix: memory leak in blog

* chore: pr feedback
This commit is contained in:
Antón Molleda 2021-01-25 13:29:19 -08:00 коммит произвёл GitHub
Родитель 9bbd06f5e0
Коммит 46b87a76cf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 98 добавлений и 171 удалений

Просмотреть файл

@ -1,9 +1,15 @@
const markdownToHtml = require('electron-markdown')
const i18n = require('../lib/i18n')
const graymatter = require('gray-matter')
const fs = require('fs-extra')
const path = require('path')
const blogPosts = new Map()
const POSTS_PATH = path.join(__dirname, '..', 'data', 'blog')
/**
* @param {string} markdown
* @returns {Promise<string>}
*/
function parseMarkdown(markdown) {
// Using unsafe options as we trust the blog content from this repo
return markdownToHtml(markdown, {
@ -16,115 +22,66 @@ function parseMarkdown(markdown) {
})
}
const POSTS_PATH = path.join(__dirname, '..', 'data', 'blog')
class BlogPost {
static async getAll(language) {
const files = await fs.readdir(POSTS_PATH)
const slugs = files.map((file) => path.basename(file, '.md'))
return slugs.map((slug) => BlogPost.get(slug, language))
}
static get(slug, language) {
return new BlogPost(slug, language)
}
constructor(slug, language) {
this._cache = new Map()
this._slug = slug
this._language = language
}
exists() {
return fs.stat(path.join(POSTS_PATH, this.filename()))
}
async cache(key, computation) {
if (this._cache.has(key)) {
return this._cache.get(key)
} else {
const value = await computation()
this._cache.set(key, value)
return value
}
}
_getRawMarkdown() {
// This block looks not very good, but they give confidence,
// and security that we do not get the crash when the content
// not synchronized in the i18n, e.g. when we publish a new post,
// languages that don't have the copy on the own language get the
// version from the disk.
//
// Why not use the English version from the i18n module?
// We get the desync between this repo and i18n, the save
// and simple way it's using the good saved copy from the disk.
//
// drink some water, be kind to yourself and others.
// If you ask why this code block is disabled?
// So, the Electron translations for blogs is so bad,
// that it's better to currently disable this and not search
// for fixes. The comment above this still actual for this code block.
// if (this._language) {
// const blog = i18n.blogs[this._language][this.href()]
// if (blog) {
// return this.cache('_getRawMarkdown', () => blog.content)
// }
// }
const fullPath = path.join(POSTS_PATH, this.filename())
return this.cache('_getRawMarkdown', () => fs.readFile(fullPath))
}
async _frontmatter() {
const markdown = await this._getRawMarkdown()
return this.cache('frontmatter', () => {
return graymatter(markdown, {
excerpt: true,
excerpt_separator: '---',
})
})
}
async frontmatter(key) {
const fm = await this._frontmatter()
return fm.data[key]
}
slug() {
return this._slug
}
filename() {
return `${this.slug()}.md`
}
title() {
return this.frontmatter('title')
}
date() {
return this.frontmatter('date')
}
href() {
return `/blog/${this.slug()}`
}
async authors() {
const authors = await this.frontmatter('author')
return Array.isArray(authors) ? authors : [authors]
}
async excerpt() {
const { excerpt } = await this._frontmatter()
return this.cache('excerpt', () => parseMarkdown(excerpt))
}
async content() {
const { content } = await this._frontmatter()
return this.cache('content', () => parseMarkdown(content))
}
/**
* @param {string} markdown
* @returns {graymatter.GrayMatterFile<string>}
*/
function parseFrontMatter(markdown) {
return graymatter(markdown, {
excerpt: true,
excerpt_separator: '---',
})
}
module.exports = BlogPost
/**
* @returns {Map}
*/
async function getAllPosts() {
if (blogPosts.size !== 0) {
return blogPosts
}
const files = await fs.readdir(POSTS_PATH)
const slugs = files.map((file) => path.basename(file, '.md'))
for (const slug of slugs) {
blogPosts.set(slug, await loadPost(slug))
}
return blogPosts
}
/**
* @param {string} slug
*/
async function loadPost(slug) {
let markdown = await fs.readFile(path.join(POSTS_PATH, `${slug}.md`))
const frontmatter = parseFrontMatter(markdown)
const authors = Array.isArray(frontmatter.data.author)
? frontmatter.data.author
: [frontmatter.data.author]
const post = {
title: frontmatter.data.title,
href: `/blog/${slug}`,
date: frontmatter.data.date,
authors,
excerpt: await parseMarkdown(frontmatter.excerpt),
content: await parseMarkdown(frontmatter.content),
}
return post
}
/**
* @param {string} slug
*/
async function getPost(slug) {
return (await getAllPosts()).get(slug)
}
module.exports = {
getAllPosts,
getPost,
}

8
package-lock.json сгенерированный
Просмотреть файл

@ -8508,8 +8508,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true,
"optional": true
"dev": true
},
"is-finite": {
"version": "1.1.0",
@ -17038,11 +17037,6 @@
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
"dev": true
},
"yubikiri": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/yubikiri/-/yubikiri-2.0.0.tgz",
"integrity": "sha512-gPLdm8Om6zZn6lsjQGZf3OdB+3OnxEX46S+TP6slcgLOArydrZan/OtEemyBmC73SG2Y0QYzYts3+5p2VzqvKw=="
},
"zwitch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz",

Просмотреть файл

@ -83,8 +83,7 @@
"search-with-your-keyboard": "^2.0.0",
"semver": "^7.3.2",
"set-query-string": "^2.2.0",
"ultimate-pagination": "^1.0.0",
"yubikiri": "^2.0.0"
"ultimate-pagination": "^1.0.0"
},
"devDependencies": {
"browser-sync": "^2.26.14",

Просмотреть файл

@ -1,20 +1,14 @@
const yubikiri = require('yubikiri')
const BlogPost = require('../../lib/blog')
const { getAllPosts } = require('../../lib/blog')
function hydrateViewModel(blogPost) {
return yubikiri({
title: blogPost.title(),
href: blogPost.href(),
date: blogPost.date(),
authors: blogPost.authors(),
excerpt: blogPost.excerpt(),
})
}
let postsInOrder = []
module.exports = async (req, res) => {
const blogPosts = await BlogPost.getAll(req.language)
const posts = await Promise.all(blogPosts.map(hydrateViewModel))
const postsInOrder = posts.sort((a, b) => b.date.localeCompare(a.date))
if (postsInOrder.length === 0) {
const blogPosts = await getAllPosts()
const posts = Array.from(blogPosts.values())
postsInOrder = posts.sort((a, b) => b.date.localeCompare(a.date))
}
Object.assign(req.context, {
posts: postsInOrder,
})

Просмотреть файл

@ -1,27 +1,12 @@
const yubikiri = require('yubikiri')
const BlogPost = require('../../lib/blog')
function hydrateViewModel(blogPost) {
return yubikiri({
title: blogPost.title(),
href: blogPost.href(),
date: blogPost.date(),
authors: blogPost.authors(),
excerpt: blogPost.excerpt(),
content: blogPost.content(),
})
}
const { getPost } = require('../../lib/blog')
module.exports = async (req, res, next) => {
const blogPost = BlogPost.get(req.params.slug, req.language)
try {
await blogPost.exists()
} catch (e) {
const post = await getPost(req.params.slug)
if (!post) {
return next()
}
const post = await hydrateViewModel(blogPost)
// redirect /blog/2016/09/27/foo to /blog/foo
if (req.path !== post.href) {
return res.redirect(post.href)

Просмотреть файл

@ -1,8 +1,12 @@
const { setupFeed } = require('./mainFeed')
const BlogPost = require('../../lib/blog')
const { getAllPosts } = require('../../lib/blog')
let feed
module.exports = async function feedHandler(req, res, next) {
const feed = await setupFeed('blog', await BlogPost.getAll())
if (!feed) {
const posts = await getAllPosts()
feed = await setupFeed('blog', Array.from(posts.values()))
}
if (req.path === '/blog.xml') {
res.set('content-type', 'text/xml')

Просмотреть файл

@ -1,7 +1,6 @@
const Feed = require('feed').Feed
const description = require('description')
const memoize = require('fast-memoize')
const yubikiri = require('yubikiri')
const types = {
blog: 'blog',
@ -37,21 +36,16 @@ module.exports.setupFeed = memoize(async (type, items) => {
})
break
case types.blog: {
const posts = await Promise.all(
items.map((post) =>
yubikiri({
href: post.href(),
title: post.title(),
content: post.content(),
date: post.date(),
author: async () => {
const authors = await post.authors()
return { name: authors[0] }
},
image: null, // TODO
})
)
)
const posts = items.map((post) => {
return {
href: post.href,
title: post.title,
content: post.content,
date: post.date,
author: post.authors[0],
image: null, // TODO
}
})
posts
.sort((a, b) => b.date.localeCompare(a.date))
.forEach((post) => {