/* * 지난 기록 채우기의 **판단** — 저장소를 모르는 순수 함수. * * 무엇을 받고 무엇을 거절하는지가 이 파일에 다 있다. 저장소를 아는 코드(`backfill-period-facts`)는 * 이 판단을 부르고 쓰기만 한다. 갈라 두는 이유는 이 저장소가 이미 쓰는 방식이다 — * 판단은 시험이 데이터베이스 없이 지킬 수 있어야 한다(§`event-type-split-shape`). */ /** 이 길이 받는 사실 — 마감된 구간을 말하는 것들. */ export const PERIOD_FACTS: readonly string[] = [ 'energy.usage.period', 'energy.generation.period', 'energy.generation.price', 'energy.tariff.basis', 'energy.bill' ] export interface BackfillJudgement { /** 저널에 넣어도 되는 것. */ keep: any[] /** 받지 않은 것과 그 이유 — 조용히 버리지 않는다. */ refused: { record: unknown; reason: string }[] } /** * 무엇을 채울 수 있나. * * 세 가지로 거절한다. * * 마감된 구간이 아니다 상태를 바꾸는 사실을 저널에만 넣으면 상태와 저널이 갈라진다 * 아직 오지 않은 시각 지난 기록이 아니다 * 보존보다 오래됐다 넣어도 다음 정리에서 지워진다 — 조용히 사라지느니 거절한다 */ export function judgeBackfill( envelopes: readonly any[], opts: { nowMs: number; retentionDays?: number } ): BackfillJudgement { const keep: any[] = [] const refused: { record: unknown; reason: string }[] = [] const horizonMs = opts.retentionDays === undefined ? undefined : opts.nowMs - opts.retentionDays * 86_400_000 for (const env of envelopes) { const type = String(env?.eventType ?? '') if (!PERIOD_FACTS.includes(type)) { refused.push({ record: env, reason: `${type} 는 마감된 구간 사실이 아니다 — 이 길은 저널에만 쓰므로, 상태를 바꾸는 사실을 여기로 넣으면 ` + '상태와 저널이 갈라진다. 라이브 문으로 보내라' }) continue } const atMs = Date.parse(String(env?.eventTime ?? '')) if (!Number.isFinite(atMs)) { refused.push({ record: env, reason: '사건 시각을 읽을 수 없다' }) continue } if (atMs > opts.nowMs) { refused.push({ record: env, reason: `아직 오지 않은 시각이다(${env?.eventTime}) — 지난 기록이 아니다` }) continue } if (horizonMs !== undefined && atMs < horizonMs) { refused.push({ record: env, reason: `보존 기간(${opts.retentionDays}일)보다 오래됐다(${env?.eventTime}) — 넣어도 다음 정리에서 지워진다. ` + '더 뒤까지 보려면 보존 기간을 늘려야 한다' }) continue } keep.push(env) } return { keep, refused } } /** * 이미 있는 것을 가려낸다 — **정체성은 내용에 있다**(커널 0.7.81). * * 저장소의 제약으로 막지 않는 이유는 드라이버가 다섯이고 충돌 처리가 제각각이기 때문이다. * 구간 단위로 한 번 읽어 가려내는 편이 이식성이 있고 값도 싸다. */ export function splitAlreadyThere(keep: readonly any[], existingIds: ReadonlySet): { fresh: any[]; skipped: number } { const fresh = keep.filter(e => typeof e?.eventId === 'string' && e.eventId && !existingIds.has(e.eventId)) return { fresh, skipped: keep.length - fresh.length } }