/* ============================================================================= * common_util_formula_doc_cache.ts * 수식 문서 연결 캐시(PLAN 802-3) — 서버 기록(요소 값 · 표 · 단가산출 문서) → `DocLinks` * 화면이 같은 계산기(`calcDoc`)로 네트워크 없이 셈 · 캐시에 없는 연결 = 빈 값(「불러오는 중」) + `missing` 표시 * → 화면이 뒤에서 미리 셈(서버 기록)을 받아 `merge` 뒤 다시 셈 * 파이썬 짝 = resources/master_data/scripts/master_doc_record.py(기록 꼴) · 화면 · DOM · fetch 없음 * ========================================================================== */ import type { FormulaDoc } from "./common_util_formula_doc"; import { Broken, Gap, plainText, type DocLinks } from "./common_util_formula_doc_calc"; import { tableValue, type DocTable } from "./common_util_formula_doc_table"; type Payload = Record; type Got = { 값?: unknown; 빈?: string; 끊김?: string }; export interface DocRecord { 요소?: (Got & { p: Payload })[]; 표?: Record; 문서?: Record; 이름?: Record; } export const LOADING = "불러오는 중"; function canon(v: unknown): unknown { if (typeof v === "number") return plainText(v); if (Array.isArray(v)) return v.map(canon); if (v && typeof v === "object") { const out: Record = {}; for (const k of Object.keys(v).sort()) out[k] = canon((v as Payload)[k]); return out; } return v; } const keyOf = (p: Payload): string => JSON.stringify(canon(p)); export interface DocCache { links: DocLinks; /** 서버 기록을 더함 */ merge: (rec: DocRecord | undefined) => void; /** 키 → 이름(연결 글) */ names: Map; /** 마지막 셈에서 캐시에 없던 연결이 있었나 — 읽으면 지움 */ takeMissing: () => boolean; /** 표 하나 넣음(고르기에서 받은 것) */ putTable: (ref: string, table: DocTable) => void; } export function createDocCache(): DocCache { const elements = new Map(); const tables = new Map(); const docs = new Map(); const names = new Map(); let missing = false; const miss = (): never => { missing = true; throw new Gap(LOADING); }; const links: DocLinks = { element(p) { if (!p.키 && p.재료 === undefined) throw new Broken("요소 고르지 않음"); // 짝 master_doc_link const got = elements.get(keyOf(p)); if (!got) return miss(); if (got.빈 !== undefined) throw new Gap(got.빈); if (got.끊김 !== undefined) throw new Broken(got.끊김); return got.값; }, table(p) { if (!p.표 || !p.칸) throw new Broken("표 · 칸 고르지 않음"); const ref = String(p.표); if (!tables.has(ref)) return miss(); const t = tables.get(ref); if (!t || t.끊김 !== undefined) throw new Broken(t?.끊김 ?? `「${ref}」 표 아님`); return tableValue(t, p); }, doc(key) { if (!key) return null; if (!docs.has(key)) return miss(); return docs.get(key) ?? null; }, }; return { links, names, merge(rec) { for (const one of rec?.요소 ?? []) { const { p, ...got } = one; elements.set(keyOf(p), got); } for (const [k, t] of Object.entries(rec?.표 ?? {})) tables.set(k, t); for (const [k, d] of Object.entries(rec?.문서 ?? {})) docs.set(k, d); for (const [k, n] of Object.entries(rec?.이름 ?? {})) names.set(k, n); }, takeMissing() { const was = missing; missing = false; return was; }, putTable(ref, table) { tables.set(ref, table); }, }; }