/* * 런타임 레지스트리 키 — **트윈의 정체성은 `(도메인, 인스턴스 id)` 다.** 순수. * * ── 왜 있나 (2026-08-06 테넌트 격리 전수 확인) ─────────────────────────────── * `TwinEngine.instances` 와 `recovered` 가 **`instanceId` 하나로** 키를 잡고 있었다. 그런데 DB 유일성은 * `[domain, instanceId]` 이고(`TwinInstance` 인덱스) 트윈의 정체성도 그쪽이다. 즉 런타임 맵만 **전역 * 이름 공간**이었다. * * **충돌은 이론이 아니다.** 레퍼런스 경로가 `instanceId` 로 **레퍼런스 소스 이름**을 쓴다 * (`sap-ewm-1710`). 벤더 시스템 이름은 테넌트가 **공유하는 어휘**라, 두 테넌트가 같은 시스템에 * 연결하면 같은 id 가 된다 — 드문 사고가 아니라 예상되는 경우다. * * 그래서 무엇이 어긋났나: * · `startLive` 의 조기 반환이 A 의 런타임을 B 에게 돌려주고 `ingestLive` 는 도메인을 확인하지 않아 * **B 의 라이브 이벤트가 A 의 커널에 적용**될 수 있었다. * · `recovered` 가 겹치면 웜스타트 씨앗이 **남의 스냅샷**이 되어 재고가 섞인다. * · `start`/`stop` 이 남의 런타임을 집으므로 B 는 "기동됨" 을 받고도 이후 전부 거부됐다. * * 이 부류는 **이미 한 번 물었다** — `twin-lifecycle-mutation.ts` 주석이 증언한다: "과거엔 * `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그." 그 자리만 고쳤다. * 그래서 이번에는 가드를 더 붙이지 않고 **키를 고친다** — 저수준 메서드가 `domainId` 를 받게 되는 것이 * 요점이다. "누구의 트윈인가" 를 말하지 않고는 만질 수 없다. * * ── 구분자 규약 ───────────────────────────────────────────────────────────── * `${domainId}:${instanceId}` — 이 저장소가 이미 쓰는 모양이다(`structureRevCache`·`measuredCache`). * 새 모양을 만들지 않는다. 파싱은 **첫 구분자**에서 자르므로 `instanceId` 에 `:` 가 들어 있어도 * 안전하다(레퍼런스 소스 이름에 들어갈 수 있다). 대신 `domainId` 에 구분자가 있으면 키가 모호해지므로 * **그때는 오류를 낸다** — 알리지 않고 잘못된 키를 만들면 그게 곧 이 결함의 재발이다. */ /** 도메인과 인스턴스 id 를 잇는 문자 — 저장소의 기존 캐시 키와 같은 모양. */ export const RUNTIME_KEY_SEP = ':' /** * 런타임 레지스트리 키. * * @throws 도메인 id 가 비었거나 구분자를 담고 있으면(키가 모호해진다). 인스턴스 id 가 비었으면. */ export function runtimeKey(domainId: string, instanceId: string): string { if (!domainId) throw new Error('runtimeKey: domainId is required — a twin runtime always belongs to a tenant') if (!instanceId) throw new Error('runtimeKey: instanceId is required') if (domainId.includes(RUNTIME_KEY_SEP)) { throw new Error(`runtimeKey: domainId must not contain "${RUNTIME_KEY_SEP}" (would make the key ambiguous)`) } return `${domainId}${RUNTIME_KEY_SEP}${instanceId}` } /** 키를 되돌린다 — 첫 구분자에서 자른다(인스턴스 id 안의 구분자는 보존). */ export function parseRuntimeKey(key: string): { domainId: string; instanceId: string } { const at = key.indexOf(RUNTIME_KEY_SEP) if (at <= 0 || at === key.length - 1) throw new Error(`parseRuntimeKey: "${key}" is not a runtime key`) return { domainId: key.slice(0, at), instanceId: key.slice(at + 1) } } /** 이 키가 이 테넌트의 것인가 — 레지스트리를 조회할 때 도메인으로 걸러낸다. */ export function isOfDomain(key: string, domainId: string): boolean { return !!domainId && key.startsWith(`${domainId}${RUNTIME_KEY_SEP}`) }