Add getAll+diff to syncGamesForMonth to skip no-op writes
- KBO에서 가져온 경기 일정을 기존 DB 데이터와 비교해 변경된 항목만 Firestore에 저장 - 변경 없는 경기의 반복 write를 제거해 월간 write 비용 대폭 감소
This commit is contained in:
parent
e76b752a41
commit
12f21803bc
@ -52,26 +52,63 @@ export function toGameDoc(year: number, g: ScheduleGame): Game | null {
|
|||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 새로 도출한 game doc이 기존 문서와 동일한지 비교한다(merge write가 no-op이 될지 판정).
|
||||||
|
*
|
||||||
|
* `merge: true`로 쓰므로 새 doc에 **포함된 필드만** 비교한다. 새 doc이 생략한 필드
|
||||||
|
* (예: scheduled 경기의 `winningTeamCode`)는 merge가 건드리지 않으므로 비교 대상에서 제외한다.
|
||||||
|
* `time`은 Timestamp이므로 millis로 비교한다.
|
||||||
|
*/
|
||||||
|
function gameDocUnchanged(existing: Partial<Game>, doc: Game): boolean {
|
||||||
|
for (const [key, value] of Object.entries(doc) as Array<[keyof Game, unknown]>) {
|
||||||
|
if (key === "time") {
|
||||||
|
const a = existing.time?.toMillis();
|
||||||
|
const b = (value as Timestamp).toMillis();
|
||||||
|
if (a !== b) return false;
|
||||||
|
} else if (existing[key] !== value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 연도·월의 KBO 경기 일정을 fetch하여 Firestore `games` 컬렉션에 upsert한다.
|
* 특정 연도·월의 KBO 경기 일정을 fetch하여 Firestore `games` 컬렉션에 upsert한다.
|
||||||
*
|
*
|
||||||
* - `merge: true`로 기존 문서의 수동 필드(예: 외부에서 채운 `winningTeamCode`)를 보존한다.
|
* - `merge: true`로 기존 문서의 수동 필드(예: 외부에서 채운 `winningTeamCode`)를 보존한다.
|
||||||
* - gameId가 없는 일정 엔트리는 스킵.
|
* - 사전 `getAll` read로 기존 값과 비교하여 **변경이 있는 문서만** write한다 —
|
||||||
|
* 대부분 불변인 일정 데이터를 매일 같은 값으로 덮어쓰는 무의미한 write를 줄인다.
|
||||||
|
* (값이 동일하면 `onGameCompleted` 트리거도 점화되지 않으므로 후속 영향 없음.)
|
||||||
|
* - gameId가 없는 일정 엔트리는 스킵. 더블헤더 등으로 gameId가 중복되면 마지막 값으로 합친다.
|
||||||
*
|
*
|
||||||
* @returns upsert한 문서 개수
|
* @returns 실제로 write가 일어난 문서 개수
|
||||||
*/
|
*/
|
||||||
export async function syncGamesForMonth(year: number, month: number): Promise<number> {
|
export async function syncGamesForMonth(year: number, month: number): Promise<number> {
|
||||||
const result = await fetchScheduleFromKbo({ year, month });
|
const result = await fetchScheduleFromKbo({ year, month });
|
||||||
const batch = firestore.batch();
|
|
||||||
let count = 0;
|
// gameId 기준 dedupe (마지막 엔트리 우선) — 순차 set의 "last wins"와 동일.
|
||||||
|
const byGameId = new Map<string, Game>();
|
||||||
for (const g of result.games) {
|
for (const g of result.games) {
|
||||||
const doc = toGameDoc(year, g);
|
const doc = toGameDoc(year, g);
|
||||||
if (!doc) continue;
|
if (!doc) continue;
|
||||||
batch.set(firestore.collection(COLLECTION).doc(g.gameId as string), doc, {
|
byGameId.set(g.gameId as string, doc);
|
||||||
merge: true,
|
|
||||||
});
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
|
if (byGameId.size === 0) return 0;
|
||||||
|
|
||||||
|
const entries = [...byGameId.entries()];
|
||||||
|
const refs = entries.map(([gameId]) => firestore.collection(COLLECTION).doc(gameId));
|
||||||
|
const snaps = await firestore.getAll(...refs);
|
||||||
|
|
||||||
|
const batch = firestore.batch();
|
||||||
|
let count = 0;
|
||||||
|
snaps.forEach((snap, i) => {
|
||||||
|
const [gameId, doc] = entries[i];
|
||||||
|
const existing = snap.exists ? (snap.data() as Partial<Game>) : null;
|
||||||
|
if (existing && gameDocUnchanged(existing, doc)) return;
|
||||||
|
batch.set(firestore.collection(COLLECTION).doc(gameId), doc, { merge: true });
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
|
||||||
if (count > 0) await batch.commit();
|
if (count > 0) await batch.commit();
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user