From 82bec76cd4e1fc8fb3fea0b7c6a60c02707f3caf Mon Sep 17 00:00:00 2001
From: taichanne30 <dev@taichan.site>
Date: Sat, 2 Mar 2024 15:18:40 +0900
Subject: [PATCH 1/6] =?UTF-8?q?fix(backend):=20DB=E3=83=95=E3=82=A9?=
 =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=83=90=E3=83=83=E3=82=AF=E6=9C=89=E5=8A=B9?=
 =?UTF-8?q?=E6=99=82=E3=80=81=E8=A4=87=E6=95=B0=E3=81=AEFTT=E3=82=BD?=
 =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=81=8B=E3=82=89=E5=8F=96=E5=BE=97=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3?=
 =?UTF-8?q?=E3=81=A7=E5=8F=96=E5=BE=97=E6=BC=8F=E3=82=8C=E3=81=8C=E8=B5=B7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E7=8F=BE=E8=B1=A1=E3=81=AE=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../backend/src/core/FanoutTimelineEndpointService.ts    | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index 9c239b4dfc..dab9f74f76 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -66,8 +66,15 @@ export class FanoutTimelineEndpointService {
 
 		const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
 
+		// 取得したredisResultのうち、2つ以上ソースがあり、1つでも空であればDBにフォールバックする
+		shouldFallbackToDb = ps.useDbFallback && (redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
+
+		// 取得したresultの中で最古のIDのうち、最も新しいものを取得
+		// shouldPrependがtrueの場合は最も新しいものを、falseの場合は最も古いものを取得
+		const thresholdId = shouldPrepend ? redisResult.map(ids => ids[ids.length - 1]).sort(idCompare)[0] : redisResult.map(ids => ids[0]).sort(idCompare)[0];
+
 		// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
-		const redisResultIds = Array.from(new Set(redisResult.flat(1)));
+		const redisResultIds = shouldFallbackToDb ? [] : Array.from(new Set(redisResult.flat(1))).filter(id => idCompare(id, thresholdId) === 1);
 
 		redisResultIds.sort(idCompare);
 		noteIds = redisResultIds.slice(0, ps.limit);

From 561567599180dc657b8e9de8c9e32fce3937cfd2 Mon Sep 17 00:00:00 2001
From: taichanne30 <dev@taichan.site>
Date: Sat, 2 Mar 2024 15:35:39 +0900
Subject: [PATCH 2/6] Update CHANGELOG.md

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d223db819a..1dc6e2664d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@
 - Fix: カスタム絵文字の画像読み込みに失敗した際はテキストではなくダミー画像を表示 #13487
 
 ### Server
--
+- Fix: FTT有効かつDBフォールバック有効時、STLのようにタイムラインのソースが複数だとFTTとDBのフォールバック間で取得されないノートがある問題
 
 ## 2024.3.0
 

From 4a8ffe20a7ebe064fa9e970fb417000672b7ac27 Mon Sep 17 00:00:00 2001
From: taichanne30 <dev@taichan.site>
Date: Thu, 7 Mar 2024 01:47:41 +0900
Subject: [PATCH 3/6] Fix timeline fetch when using sinceId

---
 packages/backend/src/core/FanoutTimelineEndpointService.ts | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index dab9f74f76..c3298ec5b9 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -70,14 +70,13 @@ export class FanoutTimelineEndpointService {
 		shouldFallbackToDb = ps.useDbFallback && (redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
 
 		// 取得したresultの中で最古のIDのうち、最も新しいものを取得
-		// shouldPrependがtrueの場合は最も新しいものを、falseの場合は最も古いものを取得
-		const thresholdId = shouldPrepend ? redisResult.map(ids => ids[ids.length - 1]).sort(idCompare)[0] : redisResult.map(ids => ids[0]).sort(idCompare)[0];
+		const thresholdId = redisResult.map(ids => ids[0]).sort()[0];
 
 		// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
-		const redisResultIds = shouldFallbackToDb ? [] : Array.from(new Set(redisResult.flat(1))).filter(id => idCompare(id, thresholdId) === 1);
+		const redisResultIds = shouldFallbackToDb ? [] : Array.from(new Set(redisResult.flat(1)));
 
 		redisResultIds.sort(idCompare);
-		noteIds = redisResultIds.slice(0, ps.limit);
+		noteIds = redisResultIds.filter(id => id >= thresholdId).slice(0, ps.limit);
 
 		shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
 

From 685fc2bd9d2283ac2a04894ec7e50adc1e4c3e5d Mon Sep 17 00:00:00 2001
From: taichanne30 <dev@taichan.site>
Date: Thu, 25 Jul 2024 14:30:10 +0900
Subject: [PATCH 4/6] =?UTF-8?q?Fix:=20shouldFallbackToDb=E3=81=8C=E3=81=99?=
 =?UTF-8?q?=E3=81=A7=E3=81=ABtrue=E3=81=AE=E5=A0=B4=E5=90=88=E3=81=AB?=
 =?UTF-8?q?=E3=81=9D=E3=82=8C=E3=81=8C=E7=84=A1=E8=A6=96=E3=81=95=E3=82=8C?=
 =?UTF-8?q?=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/src/core/FanoutTimelineEndpointService.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index ff8b22bde9..55ef7abf3c 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -75,7 +75,7 @@ export class FanoutTimelineEndpointService {
 		let noteIds = redisResultIds.filter(id => id >= thresholdId).slice(0, ps.limit);
 
 		const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
-		shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
+		shouldFallbackToDb ||= noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
 
 		if (!shouldFallbackToDb) {
 			let filter = ps.noteFilter ?? (_note => true);

From 3564bf5c66382b92a2ac7b23c300f8854058b700 Mon Sep 17 00:00:00 2001
From: taichanne30 <dev@taichan.site>
Date: Thu, 25 Jul 2024 14:37:09 +0900
Subject: [PATCH 5/6] Refactor: const naming

---
 packages/backend/src/core/FanoutTimelineEndpointService.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index 55ef7abf3c..6c874db9d1 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -67,12 +67,12 @@ export class FanoutTimelineEndpointService {
 		let shouldFallbackToDb = ps.useDbFallback && (redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
 
 		// 取得したresultの中で最古のIDのうち、最も新しいものを取得
-		const thresholdId = redisResult.map(ids => ids[0]).sort()[0];
+		const fttThresholdId = redisResult.map(ids => ids[0]).sort()[0];
 
 		// TODO: いい感じにgetMulti内でソート済だからuniqするときにredisResultが全てソート済なのを利用して再ソートを避けたい
 		const redisResultIds = shouldFallbackToDb ? [] : Array.from(new Set(redisResult.flat(1))).sort(idCompare);
 
-		let noteIds = redisResultIds.filter(id => id >= thresholdId).slice(0, ps.limit);
+		let noteIds = redisResultIds.filter(id => id >= fttThresholdId).slice(0, ps.limit);
 
 		const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1];
 		shouldFallbackToDb ||= noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;

From 48232ca57b0e8e5a1aa950527ae6ef3a3eb9d0d8 Mon Sep 17 00:00:00 2001
From: taichanne30 <dev@taichan.site>
Date: Thu, 25 Jul 2024 16:01:41 +0900
Subject: [PATCH 6/6] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BCTL?=
 =?UTF-8?q?=E3=81=A7=E3=81=AFFTT=E3=81=AE=E3=82=BD=E3=83=BC=E3=82=B9?=
 =?UTF-8?q?=E3=81=8C=E7=A9=BA=E3=81=AE=E9=9A=9B=E3=81=ABDB=E3=81=ABFallbac?=
 =?UTF-8?q?k=E3=81=97=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../backend/src/core/FanoutTimelineEndpointService.ts     | 8 +++++---
 packages/backend/src/server/api/endpoints/users/notes.ts  | 1 +
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index 6c874db9d1..4f513110b4 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -34,6 +34,7 @@ type TimelineOptions = {
 	excludeReplies?: boolean;
 	excludePureRenotes: boolean;
 	dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise<MiNote[]>,
+	preventEmptyTimelineDbFallback?: boolean;
 };
 
 @Injectable()
@@ -63,8 +64,9 @@ 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));
+		// オプション無効時、取得したredisResultのうち、2つ以上ソースがあり、1つでも空であればDBにフォールバックする
+		let shouldFallbackToDb = ps.useDbFallback &&
+			(ps.preventEmptyTimelineDbFallback !== true && redisResult.length > 1 && redisResult.some(ids => ids.length === 0));
 
 		// 取得したresultの中で最古のIDのうち、最も新しいものを取得
 		const fttThresholdId = redisResult.map(ids => ids[0]).sort()[0];
@@ -75,7 +77,7 @@ export class FanoutTimelineEndpointService {
 		let noteIds = redisResultIds.filter(id => id >= fttThresholdId).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);
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index cc76c12f1d..cbd14cf608 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -143,6 +143,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 					withFiles: ps.withFiles,
 					withRenotes: ps.withRenotes,
 				}, me),
+				preventEmptyTimelineDbFallback: true,
 			});
 
 			return timeline;