Comments & Updated caching
This commit is contained in:
parent
9983720bd1
commit
2781724f8f
16
src/comp/comment.svelte
Normal file
16
src/comp/comment.svelte
Normal 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>
|
||||
41
src/lib/client/nodetree.js
Normal file
41
src/lib/client/nodetree.js
Normal 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);
|
||||
}) || []
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
return categories.get(category_id) || {
|
||||
error: true,
|
||||
msg: `Could not find Category of ID ${category_id}`
|
||||
};
|
||||
}
|
||||
|
||||
return categories[category_id];
|
||||
}
|
||||
48
src/lib/server/db/comment.js
Normal file
48
src/lib/server/db/comment.js
Normal 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
|
||||
));
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
return users.get(user_id) || {
|
||||
error: true,
|
||||
msg: `Could not find user of ID ${user_id}`
|
||||
};
|
||||
}
|
||||
|
||||
return users[user_id];
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)[]
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user