Added in-memory caching

- added class for caching management
- added cache parameter to sql query object
- implemented caching for sql querys with cache = true
- added loglevel to config
- improved some methods of dataaccess classes to enable better caching
pull/1/head
Trivernis 5 years ago
parent 47d775fd2c
commit 7b94e4e3da

@ -17,3 +17,6 @@ session:
markdown:
plugins:
- 'markdown-it-emoji'
logging:
level: info

@ -0,0 +1,73 @@
import {EventEmitter} from "events";
import * as crypto from "crypto";
export class MemoryCache extends EventEmitter {
private cacheItems: any = {};
private cacheExpires: any = {};
private expireCheck: NodeJS.Timeout;
/**
* Creates interval function.
* @param ttl
*/
constructor(private ttl: number = 500) {
super();
this.expireCheck = setInterval(() => this.checkExpires(), ttl / 2);
}
/**
* Creates a md5 hash of the given key.
* @param key
*/
public hashKey(key: string): string {
const hash = crypto.createHash("md5");
const data = hash.update(key, "utf8");
return data.digest("hex");
}
/**
* Sets an entry.
* @param key
* @param value
*/
public set(key: string, value: any) {
this.cacheItems[key] = value;
this.cacheExpires[key] = Date.now() + this.ttl;
this.emit("set", key, value);
}
/**
* Returns the entry stored with the given key.
* @param key
*/
public get(key: string) {
if (this.cacheItems.hasOwnProperty(key)) {
this.emit("hit", key, this.cacheItems[key]);
return this.cacheItems[key];
} else {
this.emit("miss", key);
}
}
/**
* Deletes a cache item.
* @param key
*/
public delete(key: string) {
this.emit("delete", key);
delete this.cacheItems[key];
}
/**
* Checks expires and clears items that are over the expire value.
*/
private checkExpires() {
for (const [key, value] of Object.entries(this.cacheExpires)) {
if (value < Date.now()) {
this.emit("delete", key);
delete this.cacheItems[key];
delete this.cacheExpires[key];
}
}
}
}

@ -11,6 +11,10 @@ import globals from "./globals";
const logger = globals.logger;
export interface IAdvancedQueryConfig extends QueryConfig {
cache?: boolean;
}
/**
* Transaction class to wrap SQL transactions.
*/
@ -101,7 +105,7 @@ export class QueryHelper {
* executes the sql query with values and returns all results.
* @param query
*/
public async all(query: QueryConfig): Promise<any[]> {
public async all(query: IAdvancedQueryConfig): Promise<any[]> {
const result = await this.query(query);
return result.rows;
}
@ -110,7 +114,7 @@ export class QueryHelper {
* executes the sql query with values and returns the first result.
* @param query
*/
public async first(query: QueryConfig): Promise<any> {
public async first(query: IAdvancedQueryConfig): Promise<any> {
const result = await this.query(query);
if (result.rows && result.rows.length > 0) {
return result.rows[0];
@ -129,9 +133,24 @@ export class QueryHelper {
* Queries the database with error handling.
* @param query - the sql and values to execute
*/
private async query(query: QueryConfig): Promise<QueryResult|{rows: any}> {
private async query(query: IAdvancedQueryConfig): Promise<QueryResult|{rows: any}> {
try {
return await this.pool.query(query);
query.text = query.text.replace(/[\r\n]/g, " ");
globals.logger.silly(`Executing sql '${JSON.stringify(query)}'`);
if (query.cache) {
const key = globals.cache.hashKey(JSON.stringify(query));
const cacheResult = globals.cache.get(key);
if (cacheResult) {
return cacheResult;
} else {
const result = await this.pool.query(query);
globals.cache.set(key, result);
return result;
}
} else {
return await this.pool.query(query);
}
} catch (err) {
logger.debug(`Error on query "${JSON.stringify(query)}".`);
logger.error(`Sql query failed: ${err}`);

@ -1,3 +1,4 @@
import globals from "../globals";
import {ChatMessage} from "./ChatMessage";
import {queryHelper} from "./index";
import {User} from "./User";
@ -24,6 +25,7 @@ export class Chatroom {
*/
public async members(): Promise<User[]> {
const result = await queryHelper.all({
cache: true,
text: `SELECT * FROM chat_members
JOIN users ON (chat_members.member = users.id)
WHERE chat_members.chat = $1;`,
@ -31,7 +33,8 @@ export class Chatroom {
});
const chatMembers = [];
for (const row of result) {
chatMembers.push(new User(row.id, row));
const user = new User(row.id, row);
chatMembers.push(user);
}
return chatMembers;
}
@ -47,13 +50,20 @@ export class Chatroom {
const offs = offset || 0;
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM chat_messages WHERE chat = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
values: [this.id, lim, offs],
});
const messages = [];
const users: any = {};
for (const row of result) {
messages.push(new ChatMessage(new User(row.author), this, row.created_at, row.content));
if (!users[row.author]) {
const user = new User(row.author);
await user.exists();
users[row.author] = user;
}
messages.push(new ChatMessage(users[row.author], this, row.created_at, row.content));
}
if (containing) {
return messages.filter((x) => x.content.includes(containing));

@ -6,10 +6,14 @@ export class Profile extends User {
/**
* Returns all chatrooms (with pagination).
* Skips the query if the user doesn't exist.
* @param first
* @param offset
*/
public async chats({first, offset}: {first: number, offset?: number}) {
public async chats({first, offset}: {first: number, offset?: number}): Promise<Chatroom[]> {
if (!(await this.exists())) {
return [];
}
first = first || 10;
offset = offset || 0;

@ -1,3 +1,4 @@
import globals from "../globals";
import {DataObject} from "./DataObject";
import {queryHelper} from "./index";
import {Post} from "./Post";
@ -47,6 +48,7 @@ export class User extends DataObject {
*/
public async numberOfPosts(): Promise<number> {
const result = await queryHelper.first({
cache: true,
text: "SELECT COUNT(*) count FROM posts WHERE author = $1",
values: [this.id],
});
@ -66,6 +68,7 @@ export class User extends DataObject {
*/
public async friends(): Promise<User[]> {
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1",
values: [this.id],
});
@ -87,6 +90,7 @@ export class User extends DataObject {
first = first || 10;
offset = offset || 0;
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
values: [this.id, first, offset],
});
@ -107,6 +111,7 @@ export class User extends DataObject {
result = this.row;
} else {
result = await queryHelper.first({
cache: true,
text: "SELECT * FROM users WHERE users.id = $1",
values: [this.id],
});

@ -8,6 +8,7 @@
import * as fsx from "fs-extra";
import * as yaml from "js-yaml";
import * as winston from "winston";
import {MemoryCache} from "./Cache";
const configPath = "config.yaml";
const defaultConfig = __dirname + "/../default-config.yaml";
@ -26,6 +27,7 @@ if (!(fsx.pathExistsSync(configPath))) {
*/
namespace globals {
export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8"));
export const cache = new MemoryCache(1200);
export const logger = winston.createLogger({
transports: [
new winston.transports.Console({
@ -36,10 +38,13 @@ namespace globals {
return `${timestamp} ${level}: ${message}`;
}),
),
level: "debug",
level: config.logging.level,
}),
],
});
cache.on("set", (key) => logger.debug(`Caching '${key}'.`));
cache.on("miss", (key) => logger.debug(`Cache miss for '${key}'`));
cache.on("hit", (key) => logger.debug(`Cache hit for '${key}'`));
}
export default globals;

@ -3,7 +3,7 @@ type Query {
getUser(userId: ID, handle: String): User
"returns the logged in user"
getSelf: User
getSelf: Profile
"returns the post object for a post id"
getPost(postId: ID!): Post
@ -26,10 +26,10 @@ type Mutation {
acceptCookies: Boolean
"Login of the user. The passwordHash should be a sha512 hash of the password."
login(email: String, passwordHash: String): User
login(email: String, passwordHash: String): Profile
"Registers the user."
register(username: String, email: String, passwordHash: String): User
register(username: String, email: String, passwordHash: String): Profile
"Logout of the user."
logout: Boolean
@ -62,8 +62,60 @@ type Mutation {
createChat(members: [ID!]): ChatRoom
}
interface UserData {
"url for the Profile picture of the User"
profilePicture: String
"name of the User"
name: String!
"unique identifier name from the User"
handle: String!
"Id of the User"
id: ID!
"the total number of posts the user posted"
numberOfPosts: Int
"returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post]
"creation date of the user account"
joinedAt: String!
"all friends of the user"
friends: [User]
}
"represents a single user account"
type User {
type User implements UserData{
"url for the Profile picture of the User"
profilePicture: String
"name of the User"
name: String!
"unique identifier name from the User"
handle: String!
"Id of the User"
id: ID!
"the total number of posts the user posted"
numberOfPosts: Int
"returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post]
"creation date of the user account"
joinedAt: String!
"all friends of the user"
friends: [User]
}
type Profile implements UserData {
"url for the Profile picture of the User"
profilePicture: String

Loading…
Cancel
Save