enhance: ファンアウトタイムラインの空結果処理を改善し、検出可能なダミーIDとDBフォールバックロジックを導入
This commit is contained in:
parent
6c1bcd9a48
commit
7ad8861c64
|
|
@ -80,7 +80,19 @@ export class FanoutTimelineEndpointService {
|
|||
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
|
||||
|
||||
// 取得したredisResultのうち、2つ以上ソースがあり、1つでも空であればDBにフォールバックする
|
||||
let shouldFallbackToDb = ps.useDbFallback && (redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
|
||||
const trustedEmptyIndices = new Set<number>();
|
||||
for (let i = 0; i < redisResult.length; i++) {
|
||||
const ids = redisResult[i];
|
||||
const dummyIdIndex = ids.findIndex(id => this.idService.parse(id).date.getTime() === 1);
|
||||
if (dummyIdIndex !== -1) {
|
||||
ids.splice(dummyIdIndex, 1);
|
||||
if (ids.length === 0) {
|
||||
trustedEmptyIndices.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shouldFallbackToDb = ps.useDbFallback && (redisResult.length > 1 && redisResult.some((ids, i) => ids.length === 0 && !trustedEmptyIndices.has(i)));
|
||||
|
||||
// 取得したresultの中で最古のIDのうち、最も新しいものを取得
|
||||
// ids自体が空配列の場合、ids[ids.length - 1]はundefinedになるため、filterでnullを除外する
|
||||
|
|
@ -97,7 +109,7 @@ export class FanoutTimelineEndpointService {
|
|||
let noteIds = redisResultIds.slice(0, ps.limit);
|
||||
|
||||
const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
|
||||
shouldFallbackToDb ||= noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
||||
shouldFallbackToDb ||= ps.useDbFallback && (noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId);
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
let filter = ps.noteFilter ?? (_note => true) as NoteFilter;
|
||||
|
|
@ -219,17 +231,25 @@ export class FanoutTimelineEndpointService {
|
|||
|
||||
// RedisおよびDBが空の場合、次回以降の無駄なDBアクセスを防ぐためダミーIDを保存する
|
||||
const gotFromDb = await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
|
||||
if (
|
||||
redisResultIds.length === 0 &&
|
||||
ps.sinceId == null && ps.untilId == null &&
|
||||
gotFromDb.length === 0
|
||||
) {
|
||||
const dummyId = this.idService.gen();
|
||||
const canInject = (
|
||||
(redisResultIds.length === 0 && ps.sinceId == null && ps.untilId == null) &&
|
||||
(gotFromDb.length < ps.limit)
|
||||
);
|
||||
|
||||
if (canInject) {
|
||||
const dummyId = this.idService.gen(1); // 1 = Detectable Dummy Timestamp
|
||||
|
||||
Promise.all(ps.redisTimelines.map((tl, i) => {
|
||||
// 有効なソースかつ結果が空だった場合のみダミーを入れる
|
||||
if (redisResult[i] && redisResult[i].length === 0) {
|
||||
return this.fanoutTimelineService.injectDummyIfEmpty(tl, dummyId);
|
||||
let isEmpty = true;
|
||||
if (gotFromDb.length > 0) {
|
||||
isEmpty = !gotFromDb.some(n => this.accepts(tl, n));
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
return this.fanoutTimelineService.injectDummyIfEmpty(tl, dummyId);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}));
|
||||
|
|
@ -254,4 +274,32 @@ export class FanoutTimelineEndpointService {
|
|||
|
||||
return notes;
|
||||
}
|
||||
|
||||
private accepts(tl: FanoutTimelineName, note: MiNote): boolean {
|
||||
if (tl === 'localTimeline') {
|
||||
return !note.userHost && !note.replyId && note.visibility === 'public';
|
||||
} else if (tl === 'localTimelineWithFiles') {
|
||||
return !note.userHost && !note.replyId && note.visibility === 'public' && note.fileIds.length > 0;
|
||||
} else if (tl === 'localTimelineWithReplies') {
|
||||
return !note.userHost && note.replyId != null && note.visibility === 'public';
|
||||
} else if (tl.startsWith('localTimelineWithReplyTo:')) {
|
||||
const id = tl.split(':')[1];
|
||||
return !note.userHost && note.replyId != null && note.replyUserId === id;
|
||||
} else if (tl.startsWith('userTimeline:')) {
|
||||
const id = tl.split(':')[1];
|
||||
return note.userId === id && !note.replyId;
|
||||
} else if (tl.startsWith('userTimelineWithFiles:')) {
|
||||
const id = tl.split(':')[1];
|
||||
return note.userId === id && !note.replyId && note.fileIds.length > 0;
|
||||
} else if (tl.startsWith('userTimelineWithReplies:')) {
|
||||
const id = tl.split(':')[1];
|
||||
return note.userId === id && note.replyId != null;
|
||||
} else if (tl.startsWith('userTimelineWithChannel:')) {
|
||||
const id = tl.split(':')[1];
|
||||
return note.userId === id && note.channelId != null;
|
||||
} else {
|
||||
// TODO: homeTimeline系
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ export type FanoutTimelineName = (
|
|||
| `homeTimeline:${string}`
|
||||
| `homeTimelineWithFiles:${string}` // only notes with files are included
|
||||
// local timeline
|
||||
| `localTimeline` // replies are not included
|
||||
| `localTimelineWithFiles` // only non-reply notes with files are included
|
||||
| `localTimelineWithReplies` // only replies are included
|
||||
| 'localTimeline' // replies are not included
|
||||
| 'localTimelineWithFiles' // only non-reply notes with files are included
|
||||
| 'localTimelineWithReplies' // only replies are included
|
||||
| `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id.
|
||||
|
||||
// antenna
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ describe('FanoutTimelineEndpointService', () => {
|
|||
fanoutTimelineService.getMulti.mockResolvedValue([htlIds, ltlIds]);
|
||||
|
||||
// dbFallback spy
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const dbFallback = jest.fn((_untilId: string | null, _sinceId: string | null, _limit: number) => Promise.resolve([] as MiNote[]));
|
||||
|
||||
const ps = {
|
||||
|
|
@ -171,8 +170,6 @@ describe('FanoutTimelineEndpointService', () => {
|
|||
|
||||
const result = await service.getMiNotes(ps);
|
||||
|
||||
// With the fix, we should get note1 and note2.
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe(note1.id);
|
||||
expect(result[1].id).toBe(note2.id);
|
||||
|
|
@ -248,7 +245,6 @@ describe('FanoutTimelineEndpointService', () => {
|
|||
fanoutTimelineService.getMulti.mockResolvedValue(redisResult);
|
||||
|
||||
// Mock dbFallback to return empty array
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const dbFallback = jest.fn((_untilId: string | null, _sinceId: string | null, _limit: number) => Promise.resolve([] as MiNote[]));
|
||||
|
||||
const ps = {
|
||||
|
|
@ -283,7 +279,6 @@ describe('FanoutTimelineEndpointService', () => {
|
|||
|
||||
// Mock dbFallback (should be called to check for newer notes than the dummy ID)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const dbFallback = jest.fn((_untilId: string | null, _sinceId: string | null, _limit: number) => Promise.resolve([] as MiNote[]));
|
||||
|
||||
const ps = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue