fix: memory leak in blog (#5097)
* fix: memory leak in blog * chore: pr feedback
This commit is contained in:
Родитель
9bbd06f5e0
Коммит
46b87a76cf
179
lib/blog.js
179
lib/blog.js
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Загрузка…
Ссылка в новой задаче