From ab952eea2447d4688e01b72afd23f74b0e568e9f Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 28 Aug 2026 15:55:51 +0900 Subject: [PATCH] =?UTF-8?q?qa:=20Observer:=5FcatchUp()=20=E2=80=94=20?= =?UTF-8?q?=EC=BA=90=EC=B9=98=EC=97=85=20=EB=B0=9C=ED=99=94=20=EB=84=A4=20?= =?UTF-8?q?=EA=B3=B3=20=EB=8B=A8=EC=9D=BC=ED=99=94=20(=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=99=95=EC=A0=95),=20Slot=20=EA=BC=AC=EB=A6=AC?= =?UTF-8?q?=EB=8A=94=20bindLifetime=EC=9D=98=20=5FcatchUp=EC=97=90=20?= =?UTF-8?q?=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qwreey Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb --- .claude/base/lifecycle-pattern.md | 21 +++++++------------ .claude/base/slot-plan.md | 12 ++++------- .claude/base/source-state-plan.md | 11 ++++++++++ ...plementation-handtrace-round10-followup.md | 10 +++++++++ 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.claude/base/lifecycle-pattern.md b/.claude/base/lifecycle-pattern.md index fdac442..2e557c2 100644 --- a/.claude/base/lifecycle-pattern.md +++ b/.claude/base/lifecycle-pattern.md @@ -352,14 +352,14 @@ function bindLifetime(inst, value) -- 순회에서 죽고, 피해 가도 **바인드마다 `Rerun`이 도는 `H-58`이 -- 되살아난다.** 발화 게이팅은 전부 `canExecute(handle)` 하나가 맡는다 — -- `base/effect-plan.md`의 "확정 구조" 절이 소스. - if isObserver(value) and value._rerunRequired then + if isObserver(value) then -- ⭐ [2026-08-28 `H-159`] Observer도 대칭 — 묶이기 전(생성~바인드 사이)에 온 emit은 -- Observer 자신의 `_receive`가 `_rerunRequired`로 홀드해 두고(`source-state-plan.md`의 -- `Observer:_receive` — 전파 루프는 `EmitReceive`로만 본다), -- 묶이는 순간 1회 발화(출처 없음 — 설치 발화와 같은 모양). Observer엔 epoch가 - -- 없으니 dedup은 없고 "놓친 게 있었다"만 기록된다. - value._rerunRequired = false - value.fn(value._state, value, nil) + -- 없으니 dedup은 없고 "놓친 게 있었다"만 기록된다. 본문은 `Observer:_catchUp` + -- (같은 문서) — `Subscribe`/`WeakSubscribe`와 같은 한 곳. + value:_catchUp() end if isEffect(value) then value:_bindDestroying(inst) -- Destroying 연결 + **홀드된 변경 캐치업 1회** @@ -483,10 +483,7 @@ function Observer:WeakSubscribe() end self.Subscribed = true -- ⭐ [H-111] 약한 쪽도 세운다 — 구독 경로 공용 플래그 WeakSubscribed[self] = true - if self._rerunRequired then -- [2026-08-28 `H-159`] 구독 전에 홀드된 변경 1회(바인드와 대칭) - self._rerunRequired = false - self.fn(self._state, self, nil) - end + self:_catchUp() -- [2026-08-28 `H-159`] 구독 전에 홀드된 변경 1회(바인드와 대칭) return self end @@ -524,10 +521,7 @@ function Observer:Subscribe() self.Subscribed = true WeakSubscribed[self] = true Subscribed[self] = true -- 강한 킵 하나만 더 - if self._rerunRequired then -- [2026-08-28 `H-159`] 위 `WeakSubscribe`와 같은 꼬리 - self._rerunRequired = false - self.fn(self._state, self, nil) - end + self:_catchUp() -- [2026-08-28 `H-159`] 위 `WeakSubscribe`와 같은 꼬리 return self end @@ -620,7 +614,8 @@ emitFrom)`), `_state`(리시버 State — `_hold`로 강참조, `source-state-pl `WeakSubscribe`가 1회 발화. Effect와 같은 뜻("`fn`이 돌아야 하는데 아직 안 돌았다"): 생성 시 참 → `state:Observer(fn)` 생성자의 "등록 시점 즉시 1회 실행"이 돌면서 거짓 → 그 뒤 묶이기 전 사이에 온 변경이 다시 세운다), **`_receive(from)`**(`EmitReceive` — -`source-state-plan.md`의 `_emitDown` 아래). 레지스트리 두 테이블은 인스턴스 필드가 아니라 +`source-state-plan.md`의 `_emitDown` 아래), **`_catchUp()`**(홀드가 있었으면 출처 없이 1회 — +`bindLifetime`·`Subscribe`·`WeakSubscribe`가 부름, 내부 메소드). 레지스트리 두 테이블은 인스턴스 필드가 아니라 `Observer.luau`의 모듈 로컬. Effect와 달리 epoch 맵·cleanup·재진입 플래그는 없다. **여전히 참인 것**: 자기 짝은 반드시 같이 지운다 — `:Unsubscribe()`가 diff --git a/.claude/base/slot-plan.md b/.claude/base/slot-plan.md index f094286..75bf1a7 100644 --- a/.claude/base/slot-plan.md +++ b/.claude/base/slot-plan.md @@ -2431,19 +2431,15 @@ local function materializeSlotTree(slot, physicalTarget, ownerKey, position) -- **트리가 확정된 여기서** — `activateList` 재마운트 분기가 넘겨둔 일. 언마운트 -- 사이에 `data`가 바뀌었으면(`_receive`가 세운 `_rerunRequired`) 끄고 묶은 뒤 -- reconcile을 **정확히 1회** — 상류 `data`는 epoch가 최신이라 `Get` 한 번이면 된다. - -- `bindLifetime` 자신의 홀드 발화에 맡기지 않는 이유: 그건 gcconn 연결 직후 - -- 묶이는 자리에서 돌아 순서를 이 꼬리에 못 맞춘다(`_baseObserver`는 위에서 끄고 - -- 묶었다 — 그쪽 캐치업은 `setOffsetSource` 경로가 한다). + -- bind 자체가 이 꼬리에서 일어나므로 `bindLifetime`의 `Observer:_catchUp()`이 그 + -- 1회를 맡는다 — 별도 끄기/호출 없음(`_baseObserver`는 위에서 끄고 묶었다 — 그쪽 + -- 캐치업은 `setOffsetSource` 경로가 한다). -- ⚠️ **재마운트일 때만** — 최초 population은 위 `activateList`의 fresh 경로가 이미 -- observer를 만들고 묶었다(감사 3라운드: 여기서 또 묶으면 `canBound` error). 늦은 -- `:List()`(이미 마운트된 Slot에 설치, `Slot:List`)는 이 함수를 안 거치므로 fresh -- 경로의 bind가 유일 — 그래서 fresh 경로의 bind를 여기로 옮길 수도 없다. if remountingList and slot._listObserver then - local listObserver = slot._listObserver - local held = listObserver._rerunRequired - listObserver._rerunRequired = false - bindLifetime(physicalTarget, listObserver) - if held then listObserver.fn(slot._listData, listObserver, nil) end -- == reconcile(data:Get()) + bindLifetime(physicalTarget, slot._listObserver) -- 안의 `_catchUp()`이 홀드 시 reconcile(data:Get()) 1회 end -- 자기 길이를 부모에게. 이제 **처음부터 최종값**이고(C6), 동시에 diff --git a/.claude/base/source-state-plan.md b/.claude/base/source-state-plan.md index adbb2ea..15f2d66 100644 --- a/.claude/base/source-state-plan.md +++ b/.claude/base/source-state-plan.md @@ -272,6 +272,17 @@ function Observer:_receive(from) end -- (Effect의 내부 Observer도 이 경로 — `fire`가 `fn`이다) end +-- ⭐ [2026-08-28 확정] 캐치업 — 묶이거나 구독될 때 홀드가 있었으면 1회. 출처 없음(`nil`)이라 +-- `_receive`가 아니라 별도 내부 메소드(사용자: *"observer 계열의 rerun 느낌이네. 외부적으로 +-- 쓸 일은 안 보여서"* — 공개 표면 아님). `bindLifetime`·`Subscribe`·`WeakSubscribe`가 +-- 부르는 유일한 자리 — 네 곳이 각자 세 줄을 반복하던 것을 여기로(감사 4라운드 지적). +function Observer:_catchUp() + if self._rerunRequired then + self._rerunRequired = false + self.fn(self._state, self, nil) + end +end + -- `EmitReceive` — 구독자 집합 `_subs`의 원소가 만족하는 인터페이스(`Epoch`처럼 구조적). type EmitReceive = { _receive: (self: any, from: Epoch | EpochSet) -> () } -- 구현: `ComputeNode`/`GateNode`(state-epoch-plan.md §4 규칙 1~3 / gate-plan.md 조립 절), `Observer`(위). diff --git a/.claude/qa-request/pre-implementation-handtrace-round10-followup.md b/.claude/qa-request/pre-implementation-handtrace-round10-followup.md index 57fcf7f..b07f2d8 100644 --- a/.claude/qa-request/pre-implementation-handtrace-round10-followup.md +++ b/.claude/qa-request/pre-implementation-handtrace-round10-followup.md @@ -398,3 +398,13 @@ observer 측에서 해당 emit 을 처리하는 함수를 만들어주는게 맞 같은 뜻으로 **생성 시 참 → 설치 발화가 내림 → 묶이기 전 변경이 다시 세움**(사용자 확인: *"설치 발화 이후엔 rerunRequired = false 되긴 해."*). 반영: `source-state-plan.md` 1171행 계약 문구. + +## 캐치업 발화의 단일화 — `Observer:_catchUp()` (감사 4라운드 → 사용자 확정) + +`bindLifetime`·`Observer:Subscribe`·`WeakSubscribe`·`materializeSlotTree` 꼬리 넷이 +"`_rerunRequired` 끄고 → `fn(state, self, nil)`"을 각자 반복하던 것을 내부 메소드 +하나로. **사용자**: *"a 나쁘지 않은것 같아. observer 계열의 rerun 느낌이네. 외부적으로 +쓸 일은 안 보여서, 그대로 구현하면 될것 같아."* `_receive(from)`(출처 있는 통지)와 +짝. Slot 꼬리는 bind 자체가 확정 뒤라 `bindLifetime` 안의 `_catchUp`이 reconcile 1회를 +맡는다 — 별도 끄기/호출 제거. 반영: `source-state-plan.md`(정의), `lifecycle-pattern.md` +셋 + 필드 목록, `slot-plan.md` 꼬리.