wip: timeline query

This commit is contained in:
Namekuji 2023-07-30 17:35:34 -04:00
parent 76b383dee1
commit 41930bda52
No known key found for this signature in database
GPG key ID: 1D62332C07FBA532
11 changed files with 83 additions and 34 deletions

View file

@ -1,7 +1,7 @@
DROP MATERIALIZED VIEW IF EXISTS reaction_by_user_id; DROP MATERIALIZED VIEW IF EXISTS reaction_by_userid;
DROP INDEX IF EXISTS reaction_by_id; DROP INDEX IF EXISTS reaction_by_id;
DROP TABLE IF EXISTS reaction; DROP TABLE IF EXISTS reaction;
DROP MATERIALIZED VIEW IF EXISTS note_by_user_id; DROP MATERIALIZED VIEW IF EXISTS note_by_userid;
DROP INDEX IF EXISTS note_by_id; DROP INDEX IF EXISTS note_by_id;
DROP INDEX IF EXISTS note_by_uri; DROP INDEX IF EXISTS note_by_uri;
DROP INDEX IF EXISTS note_by_url; DROP INDEX IF EXISTS note_by_url;

View file

@ -49,7 +49,6 @@ CREATE TABLE IF NOT EXISTS note ( -- Models timeline
"hasPoll" boolean, "hasPoll" boolean,
"threadId" ascii, "threadId" ascii,
"channelId" ascii, -- Channel "channelId" ascii, -- Channel
"channelName" text,
"userId" ascii, -- User "userId" ascii, -- User
"userHost" text, "userHost" text,
"replyId" ascii, -- Reply "replyId" ascii, -- Reply

View file

@ -44,7 +44,6 @@ export const prepared = {
"hasPoll", "hasPoll",
"threadId", "threadId",
"channelId", "channelId",
"channelName",
"userId", "userId",
"userHost", "userHost",
"replyId", "replyId",
@ -58,9 +57,9 @@ export const prepared = {
"updatedAt" "updatedAt"
) )
VALUES VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
select: { select: {
byDate: `SELECT * FROM note WHERE "createdAtDate" IN ?`, byDate: `SELECT * FROM note WHERE "createdAtDate" = ?`,
byId: `SELECT * FROM note WHERE "id" IN ?`, byId: `SELECT * FROM note WHERE "id" IN ?`,
byUri: `SELECT * FROM note WHERE "uri" IN ?`, byUri: `SELECT * FROM note WHERE "uri" IN ?`,
byUrl: `SELECT * FROM note WHERE "url" IN ?`, byUrl: `SELECT * FROM note WHERE "url" IN ?`,
@ -118,7 +117,6 @@ export interface ScyllaNoteEditHistory {
export type ScyllaNote = Note & { export type ScyllaNote = Note & {
createdAtDate: Date; createdAtDate: Date;
files: ScyllaDriveFile[]; files: ScyllaDriveFile[];
channelName: string;
noteEdit: ScyllaNoteEditHistory[]; noteEdit: ScyllaNoteEditHistory[];
}; };
@ -148,7 +146,6 @@ export function parseScyllaNote(row: types.Row): ScyllaNote {
hasPoll: row.get("hasPoll"), hasPoll: row.get("hasPoll"),
threadId: row.get("threadId"), threadId: row.get("threadId"),
channelId: row.get("channelId"), channelId: row.get("channelId"),
channelName: row.get("channelName"),
userId: row.get("userId"), userId: row.get("userId"),
userHost: row.get("userHost"), userHost: row.get("userHost"),
replyId: row.get("replyId"), replyId: row.get("replyId"),

View file

@ -140,11 +140,11 @@ export class LocalFollowingsCache {
this.key = `follow:${userId}`; this.key = `follow:${userId}`;
} }
public static async init(userId: string) { public static async init(userId: string): Promise<LocalFollowingsCache> {
const cache = new LocalFollowingsCache(userId); const cache = new LocalFollowingsCache(userId);
// Sync from DB if no relationships is cached // Sync from DB if no followings are cached
if ((await redisClient.scard(cache.key)) === 0) { if (!(await cache.hasFollowing())) {
const rel = await Followings.find({ const rel = await Followings.find({
select: { followeeId: true }, select: { followeeId: true },
where: { followerId: cache.myId }, where: { followerId: cache.myId },
@ -172,4 +172,12 @@ export class LocalFollowingsCache {
public async isFollowing(targetId: string): Promise<boolean> { public async isFollowing(targetId: string): Promise<boolean> {
return (await redisClient.sismember(this.key, targetId)) === 1; return (await redisClient.sismember(this.key, targetId)) === 1;
} }
public async hasFollowing(): Promise<boolean> {
return (await redisClient.scard(this.key)) !== 0;
}
public async getAll(): Promise<string[]> {
return (await redisClient.smembers(this.key))
}
} }

View file

@ -296,8 +296,8 @@ export const NoteRepository = db.getRepository(Note).extend({
const myReactionsMap = new Map<Note["id"], NoteReaction | null>(); const myReactionsMap = new Map<Note["id"], NoteReaction | null>();
if (meId) { if (meId) {
const renoteIds = notes const renoteIds = notes
.filter((n) => n.renoteId != null) .filter((n) => !!n.renoteId)
.map((n) => n.renoteId!); .map((n) => n.renoteId) as string[];
const targets = [...notes.map((n) => n.id), ...renoteIds]; const targets = [...notes.map((n) => n.id), ...renoteIds];
const myReactions = await NoteReactions.findBy({ const myReactions = await NoteReactions.findBy({
userId: meId, userId: meId,

View file

@ -37,6 +37,7 @@ import {
UserSecurityKeys, UserSecurityKeys,
} from "../index.js"; } from "../index.js";
import type { Instance } from "../entities/instance.js"; import type { Instance } from "../entities/instance.js";
import { userByIdCache, userDenormalizedCache } from "@/services/user-cache.js";
const userInstanceCache = new Cache<Instance | null>( const userInstanceCache = new Cache<Instance | null>(
"userInstance", "userInstance",
@ -391,13 +392,15 @@ export const UserRepository = db.getRepository(User).extend({
if (src.banner === undefined && src.bannerId) if (src.banner === undefined && src.bannerId)
src.banner = (await DriveFiles.findOneBy({ id: src.bannerId })) ?? null; src.banner = (await DriveFiles.findOneBy({ id: src.bannerId })) ?? null;
} else { } else {
user = await this.findOneOrFail({ user = await userDenormalizedCache.fetch(src, () =>
this.findOneOrFail({
where: { id: src }, where: { id: src },
relations: { relations: {
avatar: true, avatar: true,
banner: true, banner: true,
}, },
}); }),
);
} }
const meId = me ? me.id : null; const meId = me ? me.id : null;

View file

@ -28,7 +28,11 @@ import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js";
import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import { normalizeForSearch } from "@/misc/normalize-for-search.js";
import { truncate } from "@/misc/truncate.js"; import { truncate } from "@/misc/truncate.js";
import { StatusError } from "@/misc/fetch.js"; import { StatusError } from "@/misc/fetch.js";
import { uriPersonCache, userByIdCache } from "@/services/user-cache.js"; import {
uriPersonCache,
userByIdCache,
userDenormalizedCache,
} from "@/services/user-cache.js";
import { publishInternalEvent } from "@/services/stream.js"; import { publishInternalEvent } from "@/services/stream.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import { apLogger } from "../logger.js"; import { apLogger } from "../logger.js";
@ -373,6 +377,10 @@ export async function createPerson(
await updateFeatured(user!.id, resolver).catch((err) => logger.error(err)); await updateFeatured(user!.id, resolver).catch((err) => logger.error(err));
user!.avatar = avatar;
user!.banner = banner;
await userDenormalizedCache.set(user!.id, user!);
return user!; return user!;
} }
@ -518,6 +526,13 @@ export async function updatePerson(
// Update user // Update user
await Users.update(user.id, updates); await Users.update(user.id, updates);
const updatedUser = await Users.findOneByOrFail({ id: user.id });
updatedUser.avatarId = avatar?.id ?? null;
updatedUser.avatar = avatar;
updatedUser.bannerId = banner?.id ?? null;
updatedUser.banner = banner;
await userDenormalizedCache.set(updatedUser.id, updatedUser);
if (person.publicKey) { if (person.publicKey) {
await UserPublickeys.update( await UserPublickeys.update(
{ userId: user.id }, { userId: user.id },

View file

@ -16,7 +16,7 @@ import { verifyLink } from "@/services/fetch-rel-me.js";
import { ApiError } from "../../error.js"; import { ApiError } from "../../error.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import define from "../../define.js"; import define from "../../define.js";
import { userByIdCache } from "@/services/user-cache.js"; import { userByIdCache, userDenormalizedCache } from "@/services/user-cache.js";
export const meta = { export const meta = {
tags: ["account"], tags: ["account"],
@ -308,10 +308,18 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
await Users.update(user.id, updates); await Users.update(user.id, updates);
const data = await Users.findOneByOrFail({ id: user.id });
await userByIdCache.set( await userByIdCache.set(
user.id, data.id,
await Users.findOneByOrFail({ id: user.id }), data,
); );
if (data.avatarId) {
data.avatar = await DriveFiles.findOneBy({ id: data.avatarId });
}
if (data.bannerId) {
data.banner = await DriveFiles.findOneBy({ id: data.bannerId });
}
await userDenormalizedCache.set(data.id, data);
} }
if (Object.keys(profileUpdates).length > 0) if (Object.keys(profileUpdates).length > 0)
await UserProfiles.update(user.id, profileUpdates); await UserProfiles.update(user.id, profileUpdates);

View file

@ -11,6 +11,8 @@ import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js"; import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
import { ApiError } from "../../error.js"; import { ApiError } from "../../error.js";
import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js";
import { LocalFollowingsCache } from "@/misc/cache.js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],
@ -64,13 +66,31 @@ export const paramDef = {
} as const; } as const;
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const hasFollowing = const followingsCache = await LocalFollowingsCache.init(user.id);
(await Followings.count({
where: { if (scyllaClient) {
followerId: user.id, const untilDate = ps.untilDate ? new Date(ps.untilDate) : new Date();
}, const query = [`${prepared.note.select.byDate} AND "createdAt" <= ?`];
take: 1, const params: (Date | string | string[])[] = [untilDate, untilDate];
})) !== 0; if (ps.sinceDate) {
query.push(`AND "createdAt" >= ?`);
params.push(new Date(ps.sinceDate));
}
if (ps.untilId) {
query.push(`AND "id" <= ?`);
params.push(ps.untilId);
}
if (ps.sinceId) {
query.push(`AND "id" >= ?`);
params.push(ps.sinceId);
}
const result = await scyllaClient.execute(query.join(" "), params, { prepare: true });
const notes = result.rows.map(parseScyllaNote);
return Notes.packMany(notes, user);
}
const hasFollowing = await followingsCache.hasFollowing();
//#region Construct query //#region Construct query
const followingQuery = Followings.createQueryBuilder("following") const followingQuery = Followings.createQueryBuilder("following")

View file

@ -794,8 +794,7 @@ async function insertNote(
insert.tags, insert.tags,
insert.hasPoll, insert.hasPoll,
insert.threadId, insert.threadId,
data.channel?.id, insert.channelId,
data.channel?.name,
insert.userId, insert.userId,
insert.userHost, insert.userHost,
insert.replyId, insert.replyId,

View file

@ -8,7 +8,7 @@ import type { User, IRemoteUser } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import { NoteReactions, Users, Notes } from "@/models/index.js"; import { NoteReactions, Users, Notes } from "@/models/index.js";
import { decodeReaction } from "@/misc/reaction-lib.js"; import { decodeReaction } from "@/misc/reaction-lib.js";
import { parseScyllaReaction, prepared, scyllaClient } from "@/db/scylla"; import { parseScyllaReaction, prepared, scyllaClient } from "@/db/scylla.js";
import type { NoteReaction } from "@/models/entities/note-reaction.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js";
export default async ( export default async (