Q4 EffectHandle 네 진입점 의사코드(Observer 것 재사용, Unsubscribe만 게이트 통과 뒤 cleanup) / Q5 M2 공통 기반에 Ref 최소형(+ Quad.Ref 필드) / Q6 WeakUnsubscribe 관대 명문화 / Q7 폐기 블록 archive/effect-internal-observer-cascade-reversed.md / Q8 InstanceChildHandler 부기(Parent → setLength(1), retractor는 Parent=nil → 해제) / Q9 문항 전제 정정 — Tween 절 스케치 hint==nil 줄은 복사 오류, retractor는 function() end / Q10 :List reconcile 재실행도 배치 Blocker(ownsGate — 네스팅 불가) / H-138 숏핸드 우선순위 > PropertyHandler, 충돌 방지는 UI 접두어 / H-139 New(name)(props)·Dispatch.drive 파이프라인 의사코드(bind-system-plan) — 쓰면서 빈 배열 파트 가드와 H-142 발견 / H-142 props에 Parent 금지(부모가 하는 일) — 순서 문제 소멸, D·Modifier 타입 제외 + PropertyHandler 거부(배선은 에이전트 선택으로 갈라 적음). H-129/H-131 정정. 감사 8라운드(5→3→4→5→3→1→3→1) — 1라운드가 drive 의사코드의 H-17 위반(post-pass 포함 전체 감쌈)을 잡음, 이후는 기록 문서 표기. /code-review high 10건 — 여섯 반영(그중 셋이 이 세션의 H-134 반영이 만든 것), 넷은 새 메커니즘이라 문항 H-143~H-146으로(question.md 최우선 절). doc-check ERROR 0. Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb Co-authored-by: qwreey <me@qwreey.moe>
5.3 KiB
[역전됨] EffectHandle의 내부 Observer 바인딩 세부 — _observers 배열 + bindLifetime/:Subscribe() cascade
역전일: 2026-08-25(7라운드
H-58/H-59). 대체된 곳:base/effect-plan.md의 "확정 구조 — 강한 주인은 항상Effect" 절과 "의사코드 — 생성자 /bindLifetime이 부르는 두 훅 /Rerun" 절 —bindLifetime/unbindLifetime은Effect핸들 하나에만 적용되고 내부 Observer로 cascade하지 않는다. dep 등록은 생성자에서 한 번만WeakSubscribe/WeakCallback으로 하고, 발화 여부는canExecute(handle)이 전담한다. 아래가 서술하는_observers배열/cascade/Subscribe순회는 전부_deps하나와_blocker로 대체됐다. 이 문단대로 짜면H-58의 중복Rerun이 되살아난다.왜
archive/로 왔나 (2026-08-27, 9라운드H-130, 사용자 결정 Q7-(b)): 이 블록은base/effect-plan.md안에 ⛔⛔ 폐기 배너를 단 채 남아 있었는데, 배너 아래 죽은 문단을 두 번이나 살아 있는 문장처럼 편집하는 사고가 났다(8라운드 2차 #1, 그리고 커밋9dd8213이 아래 40번째 줄 근처의 ".Subscribed는 구독 경로 전용"을 날짜 마커 없이 고친 것 — 옛 표기는 "전역:Subscribe()전용").conventions.md의 핸드오버 체크리스트 3번 ("뒤집힌 원문은archive/로 옮기고 포인터만 남길 것")이 정확히 이 실패 모드를 규정하고 있어 그대로 따랐다. 아래는 옮기는 시점의 원문 그대로(그 편집 흔적 포함)이고, 더 이상 갱신하지 않는다.
옮겨온 원문 (base/effect-plan.md, 2026-08-27 시점)
보강 — EffectHandle의 내부 Observer 바인딩 세부(2026-08-09 열한 번째
세션, 재확인 후 명시화):
⛔⛔ [2026-08-25 폐기, 7라운드
H-58/H-59] 이 문단 전체는 옛 모델이다. 위 "확정 구조 — 강한 주인은 항상Effect" 절이 정반대로 확정했다 —bindLifetime/unbindLifetime은Effect핸들 하나에만 적용되고 내부 Observer로 cascade하지 않는다. dep 등록은 생성자에서 한 번만WeakSubscribe/WeakCallback으로 하고, 발화 여부는canExecute(handle)이 전담한다. 아래가 서술하는_observers배열/cascade/Subscribe순회는 전부_deps하나와_blocker로 대체됐다(위 "필드 목록"). 아래는 히스토리로만 읽을 것 — 이 문단대로 짜면H-58의 중복Rerun이 되살아난다.
⚠️ [2026-08-24 6라운드 손 트레이싱 H-8, 2026-08-25 폐기] 이 문단 전체가 아직 "Observer 하나"
전제로 쓰여 있었다 — _observer(단수)를 _observers(배열)로 읽을 것.
아래 절이 확정한 Effect(fn, ...deps)(N-deps)와 정면으로 어긋났고, 그대로
구현하면 2번째 이후 dep의 Observer엔 canExecute 판정 근거가 아예 안 실려
그 Observer의 재실행이 통째로 죽는다 — 바로 이 문단 자신이 경고하는 실패
모드다. 필드를 배열로 바꾸고 cascade/Subscribe/Unsubscribe를 전부 순회로
고친다(새 결정 없음, 반영 누락). Ref dep은 Observer가 아니라 콜백이라 이
배열에 안 들어간다 — 그쪽 해제는 아래 H-7 문단이 소스.
EffectHandle은 내부 Observer를 필드로 강참조 —handle._observers[i] = observer(dep이 State/Source인 경우만 존재). 이건 GC 방지가 목적이 아니라 (그건 아래bindLifetime/gchold가 담당):Unsubscribe()/bindLifetimecascade가 이 필드를 통해 내부 Observer에 접근하기 위한 것.bindLifetime(inst, handle)은state가 있는 경우 내부 Observer도 같은inst로handle._observers전부에 대해bindLifetime(inst, observer)를 cascade해야 함 —Dispatch/Leaf.luau가 children 배열의EffectHandle을 매치해bindLifetime(inst, handle)을 부르는 시점(leaf 부착)과,:Subscribe()가handle을 전역 레지스트리에 등록하는 시점(아래) 둘 다 해당. 이유:canExecute(observer)가 보는 gcconn 참조는 그 Observer 자신이bindLifetime(inst, observer)될 때 그 Observer 쪽 릴레이션에 복사되는 것이라,EffectHandle만 바인드하고 내부 Observer는 안 하면 그 Observer에겐 판정 근거가 아예 없어서canExecute가 항상 거짓이 됨 (=재실행이 통째로 죽음). 같은 이유로unbindLifetime(handle)도 내부 Observer까지 같이 풀어야 대칭이 맞음. [정정, 2026-08-14 다섯 번째 세션] 이 항목이 원래 근거로 든 "canExecute가Subscribed필드 +inst의 gcconn을 함께 본다"는 틀렸음 —.Subscribed는 구독 경로 전용이고 leaf 경로와 무관(archive/canexecute-inst-arg-reversed.md). cascade가 필요하다는 결론은 그대로이고 오히려 근거가 더 직접적이 됨.:Subscribe()도 마찬가지로state가 있으면 내부 Observer를 같은 전역 강참조 레지스트리에 같이 등록(handle자신 +handle._observers전부, 또는handle._observers만으로 충분한지는 구현 세부 — 어느 쪽이든 "EffectHandle은 등록됐는데 내부 Observer는 등록 안 됨" 상태가 생기면 안 됨).