import { itemRefOf, resolveByDeclaration } from '../twin-model/item-ref.js' import type { ItemResolver } from './event-subjects.js' /* * **개체 식별자 → 선언된 품목** — 대상별 사건 목록에 품목 줄을 적기 위한 것. * * ── 왜 문자열을 자르지 않나 ───────────────────────────────────────────────── * 저장된 식별자에 품목과 로트가 붙어 있다. * * https://seunghwa.net/class/product-M0000438-lot-L2403090386 * * 여기서 품목을 꺼내려면 `-lot-` 이 로트를 가리킨다는 것을 알아야 하는데, 그 부호는 연결된 시스템마다 * 다르고 커널 계약이 정한 적이 없다. 한 시스템의 관습을 규칙으로 굳히면 이 코드가 그 시스템 위에 서게 * 된다(`item-ref.ts` 가 같은 판단을 적어 두었다). * * 그래서 **그 트윈의 선언과 견주어** 판정한다. 선언된 품목 중 이 식별자가 가리키는 것을 찾고, 후보가 * 둘 이상이면 답하지 않는다 — 짐작으로 이으면 남의 품목 이력에 이 사건이 섞인다. * * ── 왜 기억해 두나 ────────────────────────────────────────────────────────── * 사건마다 부르는 함수다(초당 130건). 같은 개체 식별자가 계속 되풀어 오므로, 한 번 판정한 것은 기억해 * 둔다. 트윈의 선언이 바뀌면 그 기억을 버려야 하므로 **선언의 개수와 첫 항목으로 세대**를 만든다 — * 선언이 바뀌면 세대가 달라져 다시 판정한다. */ /** 트윈마다 기억해 둔 판정 — 세대가 바뀌면 버린다. */ const memo = new Map; seen: Map }>() /** 한 트윈이 기억할 개체 식별자 수 상한 — 넘으면 비우고 다시 채운다(무한히 자라지 않게). */ const MEMO_LIMIT = 20_000 /** * 선언된 품목의 식별자들을 모은다 — 트윈 모델이 말하는 것만. * * `materialDefinitions` 가 이 트윈이 다루는 품목의 선언이다. 그 항목의 `id` 와, 연결된 시스템의 * 어휘로 이어 둔 값(`binding`)을 함께 후보로 둔다 — 개체 식별자는 후자를 앞부분으로 갖는다. */ function declaredItems(model: any): Map { const out = new Map() const defs = model?.productionSpec?.materialDefinitions ?? model?.materialDefinitions if (!Array.isArray(defs)) return out for (const d of defs) { const id = String(d?.id ?? '').trim() if (!id) continue out.set(id, id) const binding = d?.binding const bound = typeof binding === 'string' ? binding : binding?.epcClass ?? binding?.id const b = String(bound ?? '').trim() if (b) out.set(b, id) } return out } /** 선언이 바뀌었는지 값싸게 가리는 표식 — 개수와 첫·끝 항목이면 실제 변경을 잡는다. */ function generationOf(declared: Map): string { const keys = [...declared.keys()] return `${keys.length}:${keys[0] ?? ''}:${keys[keys.length - 1] ?? ''}` } /** * 그 트윈의 품목 판정 함수 — 선언이 없으면 `undefined`(그때는 품목 줄을 만들지 않는다). * * 선언을 모르는 트윈에서 품목을 지어내지 않는다. 개체 줄(`lot`)은 그대로 적히므로 이력 조회는 로트로 * 답할 수 있고, 품목 축만 비어 있다. 그 사실을 조회하는 쪽이 말한다. */ export function itemResolverFor(domainId: string, instanceId: string, model: any): ItemResolver | undefined { const declared = declaredItems(model) if (!declared.size) return undefined const key = `${domainId}:${instanceId}` const generation = generationOf(declared) let entry = memo.get(key) if (!entry || entry.generation !== generation) { entry = { generation, declared, seen: new Map() } memo.set(key, entry) } const state = entry return (epc: string) => { const hit = state.seen.get(epc) if (hit !== undefined) return hit /* 판정은 선언이 한다 — 후보가 둘 이상이면 `undefined` 를 내고, 그것을 「모른다」로 기억한다. */ const found = resolveByDeclaration(itemRefOf(epc), state.declared) ?? null if (state.seen.size >= MEMO_LIMIT) state.seen.clear() state.seen.set(epc, found) return found } } /** 시험이 쓰는 초기화 — 프로세스 안의 기억이므로 시험마다 비운다. */ export function resetItemResolverMemo(): void { memo.clear() }