perf(api): Store user and post metrics directly in database instead of recalculating them on-the-fly
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s

This commit is contained in:
Jesse Wierzbinski 2025-05-04 16:38:37 +02:00
parent cd12ccd6c1
commit ddb3cfc978
No known key found for this signature in database
16 changed files with 2676 additions and 106 deletions

View file

@ -106,12 +106,8 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
return this.update(this.data);
}
public async delete(ids?: string[]): Promise<void> {
if (Array.isArray(ids)) {
await db.delete(Likes).where(inArray(Likes.id, ids));
} else {
await db.delete(Likes).where(eq(Likes.id, this.id));
}
public async delete(): Promise<void> {
await db.delete(Likes).where(eq(Likes.id, this.id));
}
public static async insert(
@ -119,13 +115,13 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
): Promise<Like> {
const inserted = (await db.insert(Likes).values(data).returning())[0];
const role = await Like.fromId(inserted.id);
const like = await Like.fromId(inserted.id);
if (!role) {
if (!like) {
throw new Error("Failed to insert like");
}
return role;
return like;
}
public get id(): string {

View file

@ -2,6 +2,7 @@ import type { Status } from "@versia/client/schemas";
import { db, Instance } from "@versia/kit/db";
import {
EmojiToNote,
Likes,
MediasToNotes,
Notes,
NoteToMentions,
@ -49,9 +50,6 @@ type NoteTypeWithRelations = NoteType & {
reply: NoteType | null;
quote: NoteType | null;
application: typeof Application.$type | null;
reblogCount: number;
likeCount: number;
replyCount: number;
pinned: boolean;
reblogged: boolean;
muted: boolean;
@ -104,6 +102,16 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
throw new Error("Failed to insert status");
}
// Update author's status count
await note.author.recalculateStatusCount();
if (note.data.replyId) {
// Update the reply's reply count
await new Note(
note.data.reply as typeof Note.$type,
).recalculateReplyCount();
}
return note;
}
@ -308,6 +316,24 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
return this.author.local;
}
public async recalculateReblogCount(): Promise<void> {
const reblogCount = await db.$count(Notes, eq(Notes.reblogId, this.id));
await this.update({ reblogCount });
}
public async recalculateLikeCount(): Promise<void> {
const likeCount = await db.$count(Likes, eq(Likes.likedId, this.id));
await this.update({ likeCount });
}
public async recalculateReplyCount(): Promise<void> {
const replyCount = await db.$count(Notes, eq(Notes.replyId, this.id));
await this.update({ replyCount });
}
/**
* Updates the emojis associated with this note in the database
*
@ -531,12 +557,11 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
return note;
}
public async delete(ids?: string[]): Promise<void> {
if (Array.isArray(ids)) {
await db.delete(Notes).where(inArray(Notes.id, ids));
} else {
await db.delete(Notes).where(eq(Notes.id, this.id));
}
public async delete(): Promise<void> {
await db.delete(Notes).where(eq(Notes.id, this.id));
// Update author's status count
await this.author.recalculateStatusCount();
}
public async update(

View file

@ -12,7 +12,6 @@ import {
import type { z } from "zod";
import {
transformOutputToUserWithRelations,
userExtrasTemplate,
userRelations,
} from "../functions/user.ts";
import { BaseInterface } from "./base.ts";
@ -78,7 +77,6 @@ export class Notification extends BaseInterface<
with: {
...userRelations,
},
extras: userExtrasTemplate("Notifications_account"),
},
},
});
@ -112,7 +110,6 @@ export class Notification extends BaseInterface<
with: {
...userRelations,
},
extras: userExtrasTemplate("Notifications_account"),
},
},
extras: extra?.extras,

View file

@ -1,6 +1,6 @@
import type { Relationship as RelationshipSchema } from "@versia/client/schemas";
import { db } from "@versia/kit/db";
import { Relationships } from "@versia/kit/tables";
import { Relationships, Users } from "@versia/kit/tables";
import { randomUUIDv7 } from "bun";
import {
and,
@ -10,6 +10,7 @@ import {
type InferSelectModel,
inArray,
type SQL,
sql,
} from "drizzle-orm";
import { z } from "zod";
import { BaseInterface } from "./base.ts";
@ -246,6 +247,40 @@ export class Relationship extends BaseInterface<
.set(newRelationship)
.where(eq(Relationships.id, this.id));
// If a user follows another user, update followerCount and followingCount
if (newRelationship.following && !this.data.following) {
await db
.update(Users)
.set({
followingCount: sql`${Users.followingCount} + 1`,
})
.where(eq(Users.id, this.data.ownerId));
await db
.update(Users)
.set({
followerCount: sql`${Users.followerCount} + 1`,
})
.where(eq(Users.id, this.data.subjectId));
}
// If a user unfollows another user, update followerCount and followingCount
if (!newRelationship.following && this.data.following) {
await db
.update(Users)
.set({
followingCount: sql`${Users.followingCount} - 1`,
})
.where(eq(Users.id, this.data.ownerId));
await db
.update(Users)
.set({
followerCount: sql`${Users.followerCount} - 1`,
})
.where(eq(Users.id, this.data.subjectId));
}
const updated = await Relationship.fromId(this.data.id);
if (!updated) {

View file

@ -13,6 +13,7 @@ import {
Notes,
NoteToMentions,
Notifications,
Relationships,
Users,
UserToPinnedNotes,
} from "@versia/kit/tables";
@ -203,6 +204,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
languages: options?.languages,
});
if (!otherUser.data.isLocked) {
// Update the follower count
await otherUser.recalculateFollowerCount();
await this.recalculateFollowingCount();
}
if (otherUser.remote) {
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
entity: {
@ -237,6 +244,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
});
}
await this.recalculateFollowingCount();
await followee.recalculateFollowerCount();
await relationship.update({
following: false,
});
@ -262,6 +272,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
throw new Error("Followee must be a local user");
}
await follower.recalculateFollowerCount();
await this.recalculateFollowingCount();
const entity = new VersiaEntities.FollowAccept({
type: "FollowAccept",
id: crypto.randomUUID(),
@ -504,6 +517,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
uri: uri?.href,
});
await note.recalculateReblogCount();
// Refetch the note *again* to get the proper value of .reblogged
const finalNewReblog = await Note.fromId(newReblog.id, this?.id);
@ -555,6 +570,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
await reblogToDelete.delete();
await note.recalculateReblogCount();
if (note.author.local) {
// Remove any eventual notifications for this reblog
await db
@ -586,6 +603,45 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
}
}
public async recalculateFollowerCount(): Promise<void> {
const followerCount = await db.$count(
Relationships,
and(
eq(Relationships.subjectId, this.id),
eq(Relationships.following, true),
),
);
await this.update({
followerCount,
});
}
public async recalculateFollowingCount(): Promise<void> {
const followingCount = await db.$count(
Relationships,
and(
eq(Relationships.ownerId, this.id),
eq(Relationships.following, true),
),
);
await this.update({
followingCount,
});
}
public async recalculateStatusCount(): Promise<void> {
const statusCount = await db.$count(
Notes,
and(eq(Notes.authorId, this.id)),
);
await this.update({
statusCount,
});
}
/**
* Like a note.
*
@ -611,6 +667,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
uri: uri?.href,
});
await note.recalculateLikeCount();
if (note.author.local) {
// Notify the user that their post has been favourited
await note.author.notify("favourite", this, note);
@ -650,6 +708,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
await likeToDelete.delete();
await note.recalculateLikeCount();
if (note.author.local) {
// Remove any eventual notifications for this like
await likeToDelete.clearRelatedNotifications();