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 { cacheUpdater, cachedMethod } from './root';
import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root';
const cache = createCache();
@ -12,6 +12,17 @@ const cache = createCache();
* @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
* @returns {Result<Category>}
@ -25,13 +36,15 @@ const updateCategoryCache = cacheUpdater(cache);
*/
export const getCategoriesCached = cachedMethod(cache, getCategories);
export const getCategoriesCachedByRef = refExtendCachedMethod(getCategoriesCached);
/**
* @param {import('postgres').Sql} sql
* @param {number[]} category_ids
* @returns {Promise<Result<Category>>}
*/
export async function getCategories(sql, category_ids) {
if (category_ids.length == 0) return {};
if (category_ids.length == 0) return new Map();
const query = sql`
SELECT id, name
@ -43,13 +56,10 @@ export async function getCategories(sql, category_ids) {
/**
* @type {Result<Category>}
*/
let result = {};
let result = new Map();
categories.forEach(row => {
result[row['id']] = {
id: row['id'],
name: row['name']
}
result.set(row['id'], parseCategoryFromRow(row));
})
return updateCategoryCache(result);
@ -64,12 +74,8 @@ export async function getCategories(sql, category_ids) {
export async function getCategoryCached(sql, category_id) {
const categories = await getCategoriesCached(sql, [category_id]);
if (Object.keys(categories).length == 0) {
return {
error: true,
msg: `Could not find Category of ID ${category_id}`
};
}
return categories[category_id];
return categories.get(category_id) || {
error: true,
msg: `Could not find Category of ID ${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 { getUser, getUsersCached } from './user';
import { getCategoriesCached, getCategoriesCachedByRef, getCategoryCached } from './category';
import { getUser, getUsersCached, getUsersCachedByRef } from './user';
/**
* @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('$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 users = await getUsersCached(sql, posts.map(row => {
return row['author_id'];
}));
const categories = await getCategoriesCached(sql, posts.map(row => {
return row['category_id'];
}));
const users = await getUsersCachedByRef(sql, posts, p => p['author_id']);
const categories = await getCategoriesCachedByRef(sql, posts, p => p['category_id']);
/**
* @type {Post[]}
*/
return posts.map(row => {
return {
id: row['id'],
author: users[row['author_id']] || null,
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']),
}
};
});
return posts.map(row => parsePostFromRow(
users.get(row['author_id']) || null,
/** @type {import('$types/base').Category} */ (categories.get(row['category_id'])),
row
));
}
/**
@ -108,16 +115,5 @@ export async function getPost(sql, post_id) {
/**
* @type {Post}
*/
return {
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']),
}
};
return parsePostFromRow(author, category, post);
}

View File

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

View File

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

View File

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

View File

@ -1,12 +1,29 @@
<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;
console.log(data);
/**
* @type {import('$types/base').CommentTreeNode[]}
*/
let commentTree;
$: commentTree = buildCommentTree(data.comments);
</script>
<h1>{data.post.name}</h1>
<a href="#">{data.post.author?.name}</a>
<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 = {
id: number,
name: string,
join_date: Date
joinDate: Date
};
export type Rating = {
@ -22,6 +22,20 @@ export type Post = {
name: string,
category: Category,
content: string,
post_date: Date,
postDate: Date,
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)[]
};