Comments & Updated caching

This commit is contained in:
Donatas Kirda 2024-05-10 09:44:20 +03:00
parent 9983720bd1
commit 2781724f8f
Signed by: bloodwiing
GPG Key ID: 63020D8D3F4A164F
11 changed files with 244 additions and 84 deletions

16
src/comp/comment.svelte Normal file
View File

@ -0,0 +1,16 @@
<script>
/**
* @type {import("$types/base").CommentTreeNode}
*/
export let commentNode;
</script>
<div>
<h5>{commentNode.parent.author?.name}</h5>
<p>{commentNode.parent.content}</p>
<div>
{#each commentNode.children as reply}
<svelte:self commentNode={reply}></svelte:self>
{/each}
</div>
</div>

View File

@ -0,0 +1,41 @@
/**
* @typedef {import('$types/base').Comment} Comment
*/
/**
* @param {import('$types/base').Result<Comment>} comments
* @returns {import('$types/base').CommentTreeNode[]}
*/
export function buildCommentTree(comments) {
/** @type {Comment[]} */
let roots = [];
/** @type {Map<number, Comment[]>} */
let refs = new Map();
comments.forEach((comment, id) => {
if (comment.parentCommentId == null) {
roots.push(comment);
} else {
if (!refs.has(comment.parentCommentId)) {
refs.set(comment.parentCommentId, []);
}
refs.get(comment.parentCommentId)?.push(comment);
}
});
return roots.map(r => buildFromRoot(refs, r));
}
/**
* @param {Map<number, Comment[]>} refs
* @param {Comment} comment
* @returns {import('$types/base').CommentTreeNode}
*/
function buildFromRoot(refs, comment) {
return {
parent: comment,
children: refs.get(comment.id)?.map(c => {
return buildFromRoot(refs, c);
}) || []
}
}

View File

@ -1,5 +1,5 @@
import { createCache } from '$lib/cache.server'; import { createCache } from '$lib/cache.server';
import { cacheUpdater, cachedMethod } from './root'; import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root';
const cache = createCache(); const cache = createCache();
@ -12,6 +12,17 @@ const cache = createCache();
* @typedef {import('$types/base').Category} Category * @typedef {import('$types/base').Category} Category
*/ */
/**
* @param {import('postgres').Row} row
* @returns {Category}
*/
function parseCategoryFromRow(row) {
return {
id: row['id'],
name: row['name']
};
}
/** /**
* @param {Result<Category>} categories * @param {Result<Category>} categories
* @returns {Result<Category>} * @returns {Result<Category>}
@ -25,13 +36,15 @@ const updateCategoryCache = cacheUpdater(cache);
*/ */
export const getCategoriesCached = cachedMethod(cache, getCategories); export const getCategoriesCached = cachedMethod(cache, getCategories);
export const getCategoriesCachedByRef = refExtendCachedMethod(getCategoriesCached);
/** /**
* @param {import('postgres').Sql} sql * @param {import('postgres').Sql} sql
* @param {number[]} category_ids * @param {number[]} category_ids
* @returns {Promise<Result<Category>>} * @returns {Promise<Result<Category>>}
*/ */
export async function getCategories(sql, category_ids) { export async function getCategories(sql, category_ids) {
if (category_ids.length == 0) return {}; if (category_ids.length == 0) return new Map();
const query = sql` const query = sql`
SELECT id, name SELECT id, name
@ -43,13 +56,10 @@ export async function getCategories(sql, category_ids) {
/** /**
* @type {Result<Category>} * @type {Result<Category>}
*/ */
let result = {}; let result = new Map();
categories.forEach(row => { categories.forEach(row => {
result[row['id']] = { result.set(row['id'], parseCategoryFromRow(row));
id: row['id'],
name: row['name']
}
}) })
return updateCategoryCache(result); return updateCategoryCache(result);
@ -64,12 +74,8 @@ export async function getCategories(sql, category_ids) {
export async function getCategoryCached(sql, category_id) { export async function getCategoryCached(sql, category_id) {
const categories = await getCategoriesCached(sql, [category_id]); const categories = await getCategoriesCached(sql, [category_id]);
if (Object.keys(categories).length == 0) { return categories.get(category_id) || {
return { error: true,
error: true, msg: `Could not find Category of ID ${category_id}`
msg: `Could not find Category of ID ${category_id}` };
};
}
return categories[category_id];
} }

View File

@ -0,0 +1,48 @@
import { getUser, getUsersCached, getUsersCachedByRef } from './user';
/**
* @typedef {import('$types/base').Comment} Comment
*/
/**
* @param {import('$types/base').User | null} author
* @param {import('postgres').Row} row
* @returns {Comment}
*/
function parseCommentFromRow(author, row) {
return {
id: row['id'],
author: author,
parentCommentId: row['parent_comment_id'],
content: row['latest_content'],
commentDate: row['created_date'],
rating: {
likes: BigInt(row['likes']),
dislikes: BigInt(row['dislikes']),
}
};
}
/**
* @param {import('postgres').Sql} sql
* @param {number} post_id
* @returns {Promise<Comment[]>}
*/
export async function getCommentsForPost(sql, post_id) {
const query = sql`
SELECT id, author_id, latest_content, parent_comment_id, created_date, likes, dislikes
FROM doki8902.message_comment
WHERE post_id = ${ post_id };`;
const comments = await query;
const users = await getUsersCachedByRef(sql, comments, c => c['author_id']);
/**
* @type {Comment[]}
*/
return comments.map(row => parseCommentFromRow(
users.get(row['author_id']) || null,
row
));
}

View File

@ -1,10 +1,31 @@
import { getCategoriesCached, getCategoryCached } from './category'; import { getCategoriesCached, getCategoriesCachedByRef, getCategoryCached } from './category';
import { getUser, getUsersCached } from './user'; import { getUser, getUsersCached, getUsersCachedByRef } from './user';
/** /**
* @typedef {import('$types/base').Post} Post * @typedef {import('$types/base').Post} Post
*/ */
/**
* @param {import('$types/base').User | null} author
* @param {import('$types/base').Category} category
* @param {import('postgres').Row} row
* @returns {Post}
*/
function parsePostFromRow(author, category, row) {
return {
id: row['id'],
author: author,
name: row['name'],
category: category,
content: row['latest_content'],
postDate: row['created_date'],
rating: {
likes: BigInt(row['likes']),
dislikes: BigInt(row['dislikes']),
}
};
}
/** /**
* @param {import('postgres').Sql} sql * @param {import('postgres').Sql} sql
* @param {import('$types/base').Category | undefined} category * @param {import('$types/base').Category | undefined} category
@ -30,31 +51,17 @@ export async function getPosts(sql, category = undefined, limit = 10, offset = 0
const posts = await query; const posts = await query;
const users = await getUsersCached(sql, posts.map(row => { const users = await getUsersCachedByRef(sql, posts, p => p['author_id']);
return row['author_id']; const categories = await getCategoriesCachedByRef(sql, posts, p => p['category_id']);
}));
const categories = await getCategoriesCached(sql, posts.map(row => {
return row['category_id'];
}));
/** /**
* @type {Post[]} * @type {Post[]}
*/ */
return posts.map(row => { return posts.map(row => parsePostFromRow(
return { users.get(row['author_id']) || null,
id: row['id'], /** @type {import('$types/base').Category} */ (categories.get(row['category_id'])),
author: users[row['author_id']] || null, row
name: row['name'], ));
category: categories[row['category_id']],
content: row['latest_content'],
post_date: row['created_date'],
rating: {
likes: BigInt(row['likes']),
dislikes: BigInt(row['dislikes']),
}
};
});
} }
/** /**
@ -108,16 +115,5 @@ export async function getPost(sql, post_id) {
/** /**
* @type {Post} * @type {Post}
*/ */
return { return parsePostFromRow(author, category, post);
id: post['id'],
author: author,
name: post['name'],
category: category,
content: post['latest_content'],
post_date: post['created_date'],
rating: {
likes: BigInt(post['likes']),
dislikes: BigInt(post['dislikes']),
}
};
} }

View File

@ -1,12 +1,12 @@
/** /**
* @template T * @template T
* @param {import('node-cache')} cache * @param {import('node-cache')} cache
* @returns {function({[id: number]: T})} * @returns {function(import('$types/base').Result<T>)}
*/ */
export const cacheUpdater = (cache) => { export const cacheUpdater = (cache) => {
return function updateUserCache(data) { return function updateUserCache(data) {
Object.keys(data).forEach(id => { data.forEach((val, id) => {
cache.set(parseInt(id), data[parseInt(id)]); cache.set(id, val);
}); });
return data; return data;
} }
@ -15,15 +15,15 @@ export const cacheUpdater = (cache) => {
/** /**
* @template T * @template T
* @param {import('node-cache')} cache * @param {import('node-cache')} cache
* @param {function(import('postgres').Sql, number[]): Promise<{[id: number]: T}>} method * @param {function(import('postgres').Sql, number[]): Promise<import('$types/base').Result<T>>} method
* @returns {function(import('postgres').Sql, number[]): Promise<{[id: number]: T}>} * @returns {function(import('postgres').Sql, number[]): Promise<import('$types/base').Result<T>>}
*/ */
export const cachedMethod = (cache, method) => { export const cachedMethod = (cache, method) => {
return async function(sql, ids) { return async function(sql, ids) {
/** /**
* @type {{[id: number]: T}} * @type {import('$types/base').Result<T>}
*/ */
let results = {}; let results = new Map();
/** /**
* @type {number[]} * @type {number[]}
*/ */
@ -34,13 +34,24 @@ export const cachedMethod = (cache, method) => {
return; return;
let user = cache.get(id); let user = cache.get(id);
if (user) if (user)
results[id] = user; results.set(id, user);
else else
missing.push(id); missing.push(id);
}); });
const remaining = await method(sql, missing); const remaining = await method(sql, missing);
return Object.assign({}, results, remaining); return new Map([...results, ...remaining]);
}; };
} }
/**
* @template T
* @param {function(import('postgres').Sql, number[]): Promise<import('$types/base').Result<T>>} cachedMethod
* @returns {function(import('postgres').Sql, import('postgres').RowList<import('postgres').Row[]>, function(import('postgres').Row): number): Promise<import('$types/base').Result<T>>}
*/
export const refExtendCachedMethod = (cachedMethod) => {
return async function(sql, rows, getRef) {
return await cachedMethod(sql, rows.map(r => getRef(r)));
}
}

View File

@ -1,5 +1,5 @@
import { createCache } from '$lib/cache.server'; import { createCache } from '$lib/cache.server';
import { cacheUpdater, cachedMethod } from './root'; import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root';
const cache = createCache(); const cache = createCache();
@ -12,6 +12,18 @@ const cache = createCache();
* @typedef {import('$types/base').User} User * @typedef {import('$types/base').User} User
*/ */
/**
* @param {import('postgres').Row} row
* @returns {User}
*/
function parseUserFromRow(row) {
return {
id: row['id'],
name: row['username'],
joinDate: row['join_time']
};
}
/** /**
* @param {Result<User>} users * @param {Result<User>} users
* @returns {Result<User>} * @returns {Result<User>}
@ -25,13 +37,15 @@ const updateUserCache = cacheUpdater(cache);
*/ */
export const getUsersCached = cachedMethod(cache, getUsers); export const getUsersCached = cachedMethod(cache, getUsers);
export const getUsersCachedByRef = refExtendCachedMethod(getUsersCached);
/** /**
* @param {import('postgres').Sql} sql * @param {import('postgres').Sql} sql
* @param {number[]} user_ids * @param {number[]} user_ids
* @returns {Promise<Result<User>>} * @returns {Promise<Result<User>>}
*/ */
export async function getUsers(sql, user_ids) { export async function getUsers(sql, user_ids) {
if (user_ids.length == 0) return {}; if (user_ids.length == 0) return new Map();
const query = sql` const query = sql`
SELECT id, username, join_time SELECT id, username, join_time
@ -43,14 +57,10 @@ export async function getUsers(sql, user_ids) {
/** /**
* @type {Result<User>} * @type {Result<User>}
*/ */
let result = {}; let result = new Map();
users.forEach(row => { users.forEach(row => {
result[row['id']] = { result.set(row['id'], parseUserFromRow(row));
id: row['id'],
name: row['username'],
join_date: row['join_time']
}
}) })
return updateUserCache(result); return updateUserCache(result);
@ -64,12 +74,8 @@ export async function getUsers(sql, user_ids) {
export async function getUser(sql, user_id) { export async function getUser(sql, user_id) {
const users = await getUsers(sql, [user_id]); const users = await getUsers(sql, [user_id]);
if (Object.keys(users).length == 0) { return users.get(user_id) || {
return { error: true,
error: true, msg: `Could not find user of ID ${user_id}`
msg: `Could not find user of ID ${user_id}` };
};
}
return users[user_id];
} }

View File

@ -5,8 +5,6 @@ import { getPosts } from '$lib/server/db/post';
export async function load({ locals }) { export async function load({ locals }) {
let result = await getPosts(locals.sql); let result = await getPosts(locals.sql);
console.log(result);
return { return {
posts: result posts: result
}; };

View File

@ -1,11 +1,18 @@
import { getCommentsForPost } from "$lib/server/db/comment";
import { getPost } from "$lib/server/db/post"; import { getPost } from "$lib/server/db/post";
/** @type {import("@sveltejs/kit").ServerLoad} */ /** @type {import("@sveltejs/kit").ServerLoad} */
export async function load({ params, locals }) { export async function load({ params, locals }) {
/** @type {import("$types/base").Post | import("$types/error").Error} */ const post_id = Number(params.id);
const post = await getPost(locals.sql, Number(params.id));
const post = await getPost(locals.sql, post_id);
const comments = await getCommentsForPost(locals.sql, post_id);
console.log(comments);
return { return {
post: post post: post,
comments: comments
}; };
} }

View File

@ -1,12 +1,29 @@
<script> <script>
import Comment from "$comp/comment.svelte";
import { buildCommentTree } from "$lib/client/nodetree";
/** /**
* @type {{post: import("$types/base").Post}} * @type {{
* post: import("$types/base").Post,
* comments: import("$types/base").Result<import("$types/base").Comment>
* }}
*/ */
export let data; export let data;
console.log(data); console.log(data);
/**
* @type {import('$types/base').CommentTreeNode[]}
*/
let commentTree;
$: commentTree = buildCommentTree(data.comments);
</script> </script>
<h1>{data.post.name}</h1> <h1>{data.post.name}</h1>
<a href="#">{data.post.author?.name}</a> <a href="#">{data.post.author?.name}</a>
<p>{data.post.content}</p> <p>{data.post.content}</p>
<div>
{#each commentTree as reply}
<Comment commentNode={reply}></Comment>
{/each}
</div>

View File

@ -1,9 +1,9 @@
export type Result<T> = {[id: number]: T}; export type Result<T> = Map<number, T>;
export type User = { export type User = {
id: number, id: number,
name: string, name: string,
join_date: Date joinDate: Date
}; };
export type Rating = { export type Rating = {
@ -22,6 +22,20 @@ export type Post = {
name: string, name: string,
category: Category, category: Category,
content: string, content: string,
post_date: Date, postDate: Date,
rating: Rating rating: Rating
}; };
export type Comment = {
id: number,
author: User | null,
content: string,
commentDate: Date,
rating: Rating,
parentCommentId: number
};
export type CommentTreeNode = {
parent: Comment,
children: (CommentTreeNode | number)[]
};