修复远程缓存

This commit is contained in:
浪子
2026-05-14 15:36:25 +08:00
parent 5365a3569f
commit a2badc2d4f
6 changed files with 225 additions and 33 deletions
+79 -12
View File
@@ -59,6 +59,8 @@ import type { ParsedBody } from "./http";
import type {
ActorCache,
CachedStatus,
CachedStatusMention,
CachedStatusTag,
Follow,
Media,
Mention,
@@ -97,6 +99,7 @@ type StatusViewer = {
user: User | null;
actor: string | null;
followsByOwnerId: Map<string, boolean>;
remoteFollowsByActorId: Map<string, boolean>;
};
function parseRedirectUris(value: string): string[] {
@@ -477,10 +480,13 @@ export async function accountStatuses(request: Request, env: Env, accountId: str
const remote = await getActorByLocalId(env, accountId)
?? (accountId.startsWith("http://") || accountId.startsWith("https://") ? await resolveRemoteActor(env, accountId) : null);
if (remote) {
const viewer = await loadStatusViewer(request, env);
const fetchLimit = Math.min(limit * 4, 160);
const rows = await env.DB.prepare(
"SELECT * FROM cached_statuses WHERE actor = ? ORDER BY published DESC LIMIT ?"
).bind(remote.id, limit).all<CachedStatus>();
const items = await Promise.all(rows.results.map((row) => cachedStatusToMastodon(env, row)));
).bind(remote.id, fetchLimit).all<CachedStatus>();
const visibleRows = await filterCachedStatusesForViewer(env, rows.results, viewer);
const items = await Promise.all(visibleRows.slice(0, limit).map((row) => cachedStatusToMastodon(env, row)));
return json(items);
}
@@ -920,6 +926,8 @@ export async function homeTimeline(request: Request, env: Env): Promise<Response
const user = await requireUser(request, env);
const url = new URL(request.url);
const limit = clampLimit(url.searchParams.get("limit"), 20, 40);
const viewer = statusViewerForUser(env, user);
const cachedFetchLimit = Math.min(limit * 4, 160);
const localRows = await env.DB.prepare(
"SELECT * FROM statuses WHERE user_id = ? ORDER BY created_at DESC LIMIT ?"
@@ -930,10 +938,11 @@ export async function homeTimeline(request: Request, env: Env): Promise<Response
INNER JOIN outgoing_follows of ON of.target_actor = cs.actor
WHERE of.local_user_id = ? AND of.accepted = 1
ORDER BY cs.published DESC LIMIT ?`
).bind(user.id, limit).all<CachedStatus>();
).bind(user.id, cachedFetchLimit).all<CachedStatus>();
const localItems = await serializeStatuses(env, localRows.results, request);
const cachedItems = await Promise.all(cachedRows.results.map((row) => cachedStatusToMastodon(env, row)));
const visibleCachedRows = await filterCachedStatusesForViewer(env, cachedRows.results, viewer);
const cachedItems = await Promise.all(visibleCachedRows.slice(0, limit).map((row) => cachedStatusToMastodon(env, row)));
const merged = [...localItems, ...cachedItems].sort((a, b) => {
const at = String(a.created_at ?? "");
@@ -1219,18 +1228,20 @@ async function cachedStatusToMastodon(env: Env, row: CachedStatus): Promise<Reco
const cache = await resolveRemoteActor(env, row.actor);
const account = cache ? remoteAccountJson(cache) : { id: row.actor, acct: row.actor, username: row.actor };
const attachments = await listCachedStatusAttachments(env, row.id);
const mentions = parseCachedJson<CachedStatusMention>(row.mentions_json);
const tags = parseCachedJson<CachedStatusTag>(row.tags_json);
return {
id: row.object_id,
uri: row.object_id,
url: row.url,
account,
in_reply_to_id: null,
in_reply_to_id: row.in_reply_to,
in_reply_to_account_id: null,
content: row.content,
text: row.content,
created_at: row.published,
edited_at: null,
visibility: "public",
visibility: row.visibility,
language: row.language,
sensitive: Boolean(row.sensitive),
spoiler_text: row.summary,
@@ -1247,8 +1258,13 @@ async function cachedStatusToMastodon(env: Env, row: CachedStatus): Promise<Reco
description: att.description,
blurhash: null
})),
mentions: [],
tags: [],
mentions: mentions.map((mention) => ({
id: mention.actor,
username: mention.acct.replace(/^@/, "").split("@")[0],
acct: mention.acct.replace(/^@/, ""),
url: mention.url
})),
tags: tags.map((tag) => ({ name: tag.name, url: tag.url })),
emojis: [],
reblogs_count: 0,
favourites_count: 0,
@@ -1265,6 +1281,15 @@ async function cachedStatusToMastodon(env: Env, row: CachedStatus): Promise<Reco
};
}
function parseCachedJson<T>(value: string): T[] {
try {
const parsed = JSON.parse(value) as unknown;
return Array.isArray(parsed) ? parsed as T[] : [];
} catch {
return [];
}
}
async function statusJson(
env: Env,
status: Status,
@@ -1589,6 +1614,12 @@ async function loadStatusesByIds(env: Env, statusIds: string[]): Promise<Status[
return rows.results;
}
async function loadCachedStatusesByObjectIds(env: Env, objectIds: string[]): Promise<CachedStatus[]> {
if (objectIds.length === 0) return [];
const rows = await env.DB.prepare(`SELECT * FROM cached_statuses WHERE object_id IN (${placeholders(objectIds.length)})`).bind(...objectIds).all<CachedStatus>();
return rows.results;
}
async function loadMediaByStatusIds(env: Env, statusIds: string[]): Promise<Map<string, Media[]>> {
const grouped = new Map<string, Media[]>();
if (statusIds.length === 0) return grouped;
@@ -1666,10 +1697,18 @@ async function loadReplyCountByStatusIds(env: Env, statusIds: string[]): Promise
async function serializeNotifications(env: Env, notifications: Notification[], request: Request): Promise<Record<string, unknown>[]> {
if (notifications.length === 0) return [];
const statuses = await loadStatusesByIds(env, uniqueStrings(notifications.map((notification) => notification.status_id)));
const visibleStatuses = await filterStatusesForViewer(env, statuses, await loadStatusViewer(request, env));
const notificationStatusIds = uniqueStrings(notifications.map((notification) => notification.status_id));
const viewer = await loadStatusViewer(request, env);
const statuses = await loadStatusesByIds(env, notificationStatusIds);
const localStatusIds = new Set(statuses.map((status) => status.id));
const cachedStatuses = await loadCachedStatusesByObjectIds(env, notificationStatusIds.filter((statusId) => !localStatusIds.has(statusId)));
const visibleStatuses = await filterStatusesForViewer(env, statuses, viewer);
const visibleCachedStatuses = await filterCachedStatusesForViewer(env, cachedStatuses, viewer);
const serializedStatuses = await serializeStatuses(env, visibleStatuses, request);
const serializedStatusById = new Map(serializedStatuses.map((item) => [String(item.id), item]));
const serializedCachedStatuses = await Promise.all(visibleCachedStatuses.map((row) => cachedStatusToMastodon(env, row)));
const serializedStatusById = new Map(
[...serializedStatuses, ...serializedCachedStatuses].map((item) => [String(item.id), item])
);
const remoteActorIds = uniqueStrings(
notifications.map((notification) => notification.actor).filter((actorId) => !actorId.startsWith(baseUrl(env)))
@@ -1762,7 +1801,8 @@ function statusViewerForUser(env: Env, user: User | null): StatusViewer {
return {
user,
actor: user ? actorUrl(env, user) : null,
followsByOwnerId: new Map()
followsByOwnerId: new Map(),
remoteFollowsByActorId: new Map()
};
}
@@ -1786,6 +1826,14 @@ async function filterStatusesForViewer(env: Env, statuses: Status[], viewer: Sta
return visible;
}
async function filterCachedStatusesForViewer(env: Env, statuses: CachedStatus[], viewer: StatusViewer): Promise<CachedStatus[]> {
const visible: CachedStatus[] = [];
for (const status of statuses) {
if (await canViewerViewCachedStatus(env, status, viewer)) visible.push(status);
}
return visible;
}
async function canViewerViewStatus(env: Env, status: Status, viewer: StatusViewer): Promise<boolean> {
if (status.visibility === "public" || status.visibility === "unlisted") return true;
if (viewer.user?.id === status.user_id) return true;
@@ -1793,6 +1841,13 @@ async function canViewerViewStatus(env: Env, status: Status, viewer: StatusViewe
return false;
}
async function canViewerViewCachedStatus(env: Env, status: CachedStatus, viewer: StatusViewer): Promise<boolean> {
if (status.visibility === "public" || status.visibility === "unlisted") return true;
if (!viewer.actor) return false;
if (status.visibility === "private") return viewerFollowsRemoteActor(env, viewer, status.actor);
return parseCachedJson<string>(status.local_recipients_json).includes(viewer.actor);
}
async function viewerFollowsOwner(env: Env, viewer: StatusViewer, ownerUserId: string): Promise<boolean> {
if (!viewer.actor) return false;
const cached = viewer.followsByOwnerId.get(ownerUserId);
@@ -1805,6 +1860,18 @@ async function viewerFollowsOwner(env: Env, viewer: StatusViewer, ownerUserId: s
return follows;
}
async function viewerFollowsRemoteActor(env: Env, viewer: StatusViewer, actorId: string): Promise<boolean> {
if (!viewer.user) return false;
const cached = viewer.remoteFollowsByActorId.get(actorId);
if (cached !== undefined) return cached;
const row = await env.DB.prepare(
"SELECT 1 AS hit FROM outgoing_follows WHERE local_user_id = ? AND target_actor = ? AND accepted = 1 LIMIT 1"
).bind(viewer.user.id, actorId).first<{ hit: number }>();
const follows = Boolean(row?.hit);
viewer.remoteFollowsByActorId.set(actorId, follows);
return follows;
}
async function viewerUser(request: Request, env: Env): Promise<User | null> {
const auth = request.headers.get("authorization") ?? "";
const token = auth.match(/^Bearer\s+(.+)$/i)?.[1];