import { ModelId } from '../core/types'; import { OpConflict, OpOrigin, OpResult, PersistedOpBatch } from './op'; import { CommandStack } from './stack'; import { EditorState } from './types'; /** * 컨테이너 rev + layout/logical version의 라이브 캐시 — 모든 forward op의 base rev 단일 출처. * `fromPersisted`(세션 #2)가 마커 doc을 파싱해 `seed`로 주입하고, 호스트 응답이 drain된 시점에 확정. */ export declare class RevCache { private revs; layoutVersion: number; logicalVersion: number; /** 마커 doc 적재/재적재 시 캐시 초기화(세션 #2 fromPersisted 분기 파서가 호출). */ seed(snapshot: { revs?: Record; layoutVersion?: number; logicalVersion?: number; }): void; rev(id: ModelId): number; setRev(id: ModelId, v: number): void; deleteRev(id: ModelId): void; /** 진단·테스트용 읽기 스냅샷(내부 Map 노출 안 함). */ snapshot(): { revs: Record; layoutVersion: number; logicalVersion: number; }; } /** 호스트로 배치를 전송하는 함수(세션 #3 applyEntityDiagramOps 라우트에 연결, 테스트는 mock). */ export type OpSyncTransport = (batch: PersistedOpBatch) => Promise; export interface OpSyncOptions { diagramId: string; /** emitOps(state) 호출 대상 — controller의 reactive EditorState. */ state: EditorState; transport: OpSyncTransport; origin?: OpOrigin; /** coalesce 그룹 idle 디바운스(ms). 기본 300. */ debounceMs?: number; /** 멱등 키 생성기(주입 가능 — 테스트 결정성). 기본 newId. */ newClientOpId?: () => string; /** 충돌(또는 전송 실패)로 하드 프리즈 진입 시 — controller가 editable=false + reload UX 연동. */ onFreeze?: (conflict: OpConflict) => void; /** * 전송 거부(네트워크 등) 통지 — 진단용(프리즈는 별도 onFreeze로). * 재시도하는 경우 **시도마다** 발화한다(회복돼도 원인을 추적할 수 있어야 하므로). */ onError?: (err: unknown) => void; /** * 전송 실패 재시도 지연(ms) — 배열 길이가 곧 재시도 횟수. 기본 `[400, 1200]`(총 3회 시도). * `[]`이면 재시도 없이 즉시 프리즈. 같은 배치를 **같은 `clientOpId`로** 재전송하므로 호스트 * 멱등 ring이 중복 적용을 흡수한다(응답만 유실된 경우 직전 결과를 그대로 돌려받는다). */ retryDelaysMs?: number[]; } export declare class OpSyncAdapter { private readonly opts; private readonly cache; private readonly newClientOpId; private readonly debounceMs; private readonly retryDelaysMs; private readonly emissions; private open; private debounceHandle; private readonly queue; private sending; private _frozen; private stack; private settledWaiters; constructor(opts: OpSyncOptions); /** RevCache 시드(세션 #2 fromPersisted가 마커 파싱 후 호출). */ seed(snapshot: Parameters[0]): void; /** stack에 구독 — onCommand 알림을 받고, flush 경계에서 sealCoalesce를 호출하기 위해 참조 보관. 해제 함수 반환. */ attach(stack: CommandStack): () => void; get frozen(): boolean; /** 미전송 pending 그룹 존재 여부(controller dirty 보조 신호). */ get hasPending(): boolean; /** 진단·테스트용 캐시 스냅샷. */ cacheSnapshot(): ReturnType; /** 강제 flush(save·blur·beforeunload) — 열린 그룹을 즉시 영속에 내보내고 coalesce 경계 봉인. */ flush(): void; /** 큐가 비고 전송 중이 아니며 열린 그룹도 없을 때 resolve — 테스트 동기화용. */ whenSettled(): Promise; private handle; private flushOpen; private startDebounce; private resetDebounce; private cancelDebounce; private buildBatch; private enqueue; private pump; /** * ok 응답 확정 — 낙관 체인 유지가 핵심. 후속 pending 배치가 이 예측 위에 base를 쌓았으므로 * 큐가 완전히 빈 시점에만 서버 권위값으로 덮어쓴다(중간에 덮으면 후속 배치 base가 stale → 거짓 충돌). * ok ⟹ CAS 통과 ⟹ 서버 신 rev = 낙관 예측이므로, 미배수 시 캐시는 이미 정확(덮을 필요 없음). */ private reconcile; private freeze; private notifySettled; }