qa: 10라운드 후속 H-163/H-164 + EmitReceive — 전파 루프 계층 분리, Slot 재마운트 캐치업, emitFrom nil 계약
- EmitReceive: State:_emitDown은 sub:_receive(from)만, Observer:_receive가 canExecute 판정·_rerunRequired 홀드 (사용자 지시 — 계층간 지식 분리), State:Observer(fn) 생성자 순서(플래그 참 → fn 1회 → 내림 → _subs) - H-163 (a′): _listObserver 재마운트 바인드는 materializeSlotTree 꼬리(트리 확정 뒤)에서 재마운트일 때만, 홀드가 있었으면 reconcile 1회 (감사가 잡은 첫 마운트 이중 bind·reconcile 미실행 결함 정정) - H-164 (c): emitFrom == nil = 출처 없음(설치 또는 캐치업), from 보관 기각 - 감사 4→3→2→2(각도 교체, 마지막 둘은 표현), /code-review 8 반영분 포함 Co-authored-by: qwreey <me@qwreey.moe> Claude-Session: https://claude.ai/code/session_01546hjsYNLSMZdHdPyTZaGb
This commit is contained in:
parent
ecc6b0e538
commit
5e96cd6258
10 changed files with 143 additions and 54 deletions
File diff suppressed because one or more lines are too long
|
|
@ -2601,8 +2601,9 @@ end
|
||||||
캡처하므로, 예전처럼 `relate:SetStrong(inst,k,observer)`로 저장해뒀다가
|
캡처하므로, 예전처럼 `relate:SetStrong(inst,k,observer)`로 저장해뒀다가
|
||||||
나중에 `relate:GetStrong(inst,k)`로 다시 찾아올 필요가 없어짐(위
|
나중에 `relate:GetStrong(inst,k)`로 다시 찾아올 필요가 없어짐(위
|
||||||
"핸들러 계약"/"핸들러 내부 상태 저장" 절 참고).
|
"핸들러 계약"/"핸들러 내부 상태 저장" 절 참고).
|
||||||
- **핸들러가 직접 `canExecute`/liveness를 재구현할 필요 없음** — State의
|
- **핸들러가 직접 `canExecute`/liveness를 재구현할 필요 없음** — 발화 때마다
|
||||||
전파 루프가 발화 때마다 `canExecute(observer)`로 각 구독자를 게이팅하고,
|
Observer 자신의 `_receive`가 `canExecute(observer)`로 게이팅하고(**[2026-08-28
|
||||||
|
`EmitReceive`]** 옛 표현 "State의 전파 루프가"),
|
||||||
그 판정 근거(`inst` 생존)는 `bindLifetime`이 `observer` 쪽에 복사해둔
|
그 판정 근거(`inst` 생존)는 `bindLifetime`이 `observer` 쪽에 복사해둔
|
||||||
gcconn 참조가 제공함(`base/lifecycle-pattern.md`의
|
gcconn 참조가 제공함(`base/lifecycle-pattern.md`의
|
||||||
"`bindLifetime`/`canBound`/`canExecute`/`unbindLifetime`" 절).
|
"`bindLifetime`/`canBound`/`canExecute`/`unbindLifetime`" 절).
|
||||||
|
|
|
||||||
|
|
@ -539,8 +539,9 @@ end
|
||||||
있는 것은 "`Ref`에 콜백 해제 경로(`:Uncallback`)를 둔다"는 결론뿐이다).
|
있는 것은 "`Ref`에 콜백 해제 경로(`:Uncallback`)를 둔다"는 결론뿐이다).
|
||||||
|
|
||||||
**⭐ [2026-08-24 추가, 사용자 지적] 해제 경로만으로는 부족하다 — `Ref` 콜백도
|
**⭐ [2026-08-24 추가, 사용자 지적] 해제 경로만으로는 부족하다 — `Ref` 콜백도
|
||||||
발화 시점에 `canExecute`를 확인한다.** State/Source dep은 State의 전파 루프가
|
발화 시점에 `canExecute`를 확인한다.** State/Source dep은 Observer 자신의 `_receive`가
|
||||||
구독자마다 `canExecute(observer)`를 보고 죽은 것을 건너뛰는데(`base/source-state-plan.md`),
|
`canExecute(observer)`를 보고 죽은 것을 건너뛰는데(**[2026-08-28 `EmitReceive`]** 옛 표현은
|
||||||
|
"전파 루프가" — `base/source-state-plan.md`),
|
||||||
`Ref` 경로엔 그 게이트가 아예 없었다. 해제가 늦거나 누락되는 창이 실재한다 —
|
`Ref` 경로엔 그 게이트가 아예 없었다. 해제가 늦거나 누락되는 창이 실재한다 —
|
||||||
예컨대 `unbindLifetime`으로 조용히 끊긴 상태(포탈 언마운트)는 `Destroying`이
|
예컨대 `unbindLifetime`으로 조용히 끊긴 상태(포탈 언마운트)는 `Destroying`이
|
||||||
안 도는데도 `canExecute`가 거짓이다. **확정된 형태**: `Effect`가 거는 `Ref`
|
안 도는데도 `canExecute`가 거짓이다. **확정된 형태**: `Effect`가 거는 `Ref`
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,8 @@ function bindLifetime(inst, value)
|
||||||
-- `base/effect-plan.md`의 "확정 구조" 절이 소스.
|
-- `base/effect-plan.md`의 "확정 구조" 절이 소스.
|
||||||
if isObserver(value) and value._rerunRequired then
|
if isObserver(value) and value._rerunRequired then
|
||||||
-- ⭐ [2026-08-28 `H-159`] Observer도 대칭 — 묶이기 전(생성~바인드 사이)에 온 emit은
|
-- ⭐ [2026-08-28 `H-159`] Observer도 대칭 — 묶이기 전(생성~바인드 사이)에 온 emit은
|
||||||
-- 전파 루프가 `_rerunRequired`로 홀드해 두고(`source-state-plan.md` 전파 루프),
|
-- Observer 자신의 `_receive`가 `_rerunRequired`로 홀드해 두고(`source-state-plan.md`의
|
||||||
|
-- `Observer:_receive` — 전파 루프는 `EmitReceive`로만 본다),
|
||||||
-- 묶이는 순간 1회 발화(출처 없음 — 설치 발화와 같은 모양). Observer엔 epoch가
|
-- 묶이는 순간 1회 발화(출처 없음 — 설치 발화와 같은 모양). Observer엔 epoch가
|
||||||
-- 없으니 dedup은 없고 "놓친 게 있었다"만 기록된다.
|
-- 없으니 dedup은 없고 "놓친 게 있었다"만 기록된다.
|
||||||
value._rerunRequired = false
|
value._rerunRequired = false
|
||||||
|
|
@ -615,11 +616,11 @@ leaf냐"를 가르는 판별자.
|
||||||
**Observer 인스턴스 필드 목록 (2026-08-28 명문화)** — `fn`(콜백, `fn(targetState, self,
|
**Observer 인스턴스 필드 목록 (2026-08-28 명문화)** — `fn`(콜백, `fn(targetState, self,
|
||||||
emitFrom)`), `_state`(리시버 State — `_hold`로 강참조, `source-state-plan.md`),
|
emitFrom)`), `_state`(리시버 State — `_hold`로 강참조, `source-state-plan.md`),
|
||||||
**`.Subscribed`**(공개 플래그, 위 표), **`_rerunRequired`**(**[2026-08-28 10라운드
|
**`.Subscribed`**(공개 플래그, 위 표), **`_rerunRequired`**(**[2026-08-28 10라운드
|
||||||
`H-159`]** 묶이기 전에 온 emit을 전파 루프가 홀드 — `bindLifetime`/`Subscribe`/
|
`H-159`]** 묶이기 전에 온 emit을 자기 `_receive`가 홀드 — `bindLifetime`/`Subscribe`/
|
||||||
`WeakSubscribe`가 1회 발화. **거짓으로 시작**한다: `state:Observer(fn)`의 "등록 시점
|
`WeakSubscribe`가 1회 발화. Effect와 같은 뜻("`fn`이 돌아야 하는데 아직 안 돌았다"):
|
||||||
즉시 1회 실행"(`source-state-plan.md`)은 생성자가 **무조건** 하는 것이라 이 플래그와
|
생성 시 참 → `state:Observer(fn)` 생성자의 "등록 시점 즉시 1회 실행"이 돌면서 거짓 →
|
||||||
무관하고, 플래그는 그 뒤 ~ 묶이기 전 사이에 온 변경만 기록한다 — 감사 3라운드가
|
그 뒤 묶이기 전 사이에 온 변경이 다시 세운다), **`_receive(from)`**(`EmitReceive` —
|
||||||
"초기화가 없어 한 번도 안 돈다"로 오독할 수 있음을 짚어 명시). 레지스트리 두 테이블은 인스턴스 필드가 아니라
|
`source-state-plan.md`의 `_emitDown` 아래). 레지스트리 두 테이블은 인스턴스 필드가 아니라
|
||||||
`Observer.luau`의 모듈 로컬. Effect와 달리 epoch 맵·cleanup·재진입 플래그는 없다.
|
`Observer.luau`의 모듈 로컬. Effect와 달리 epoch 맵·cleanup·재진입 플래그는 없다.
|
||||||
|
|
||||||
**여전히 참인 것**: 자기 짝은 반드시 같이 지운다 — `:Unsubscribe()`가
|
**여전히 참인 것**: 자기 짝은 반드시 같이 지운다 — `:Unsubscribe()`가
|
||||||
|
|
@ -680,10 +681,12 @@ Destroy됐거나 `unbindLifetime`된 `value`는 `canBound`가 **참**이라
|
||||||
게이트를 통과함(다시 다른 `inst`에 걸 수 있음). 살아있는 바인딩만 막는
|
게이트를 통과함(다시 다른 `inst`에 걸 수 있음). 살아있는 바인딩만 막는
|
||||||
게 이 게이트의 의도.
|
게 이 게이트의 의도.
|
||||||
|
|
||||||
#### (4) 실제 호출부 — State 전파(`emit`)가 `canExecute`로 게이팅한다
|
#### (4) 실제 호출부 — Observer의 `_receive`가 `canExecute`로 게이팅한다
|
||||||
|
|
||||||
`canExecute`가 "어디서 불리는가"는 지금까지 어느 문서에도 코드로 없었음(위
|
`canExecute`가 "어디서 불리는가"는 지금까지 어느 문서에도 코드로 없었음(위
|
||||||
정정 배너 참고). 확정된 위치는 **State의 전파 루프**:
|
정정 배너 참고). 확정된 위치는 **`Observer:_receive(from)`**(**[2026-08-28]** State의
|
||||||
|
전파 루프는 구독자를 `EmitReceive`로만 보고 `sub:_receive(from)`을 부른다 —
|
||||||
|
`base/source-state-plan.md`의 `_emitDown`; 판정·홀드는 Observer 자신의 몫):
|
||||||
|
|
||||||
- State는 자기 구독자를 **weak-키로** 담는다 — 살려두는 책임은 State가
|
- State는 자기 구독자를 **weak-키로** 담는다 — 살려두는 책임은 State가
|
||||||
아니라 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있고, 어디에도
|
아니라 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있고, 어디에도
|
||||||
|
|
@ -692,9 +695,9 @@ Destroy됐거나 `unbindLifetime`된 `value`는 `canBound`가 **참**이라
|
||||||
클로저"가 아니라 **Observer 값**이다.** `bindLifetime(inst, observer)`가
|
클로저"가 아니라 **Observer 값**이다.** `bindLifetime(inst, observer)`가
|
||||||
Observer **값**을 키로 `BindData`에 gcconn을 복사하므로, 집합에 클로저를
|
Observer **값**을 키로 `BindData`에 gcconn을 복사하므로, 집합에 클로저를
|
||||||
담으면 identity가 달라 `canExecute`가 **항상 거짓**이 된다.
|
담으면 identity가 달라 `canExecute`가 **항상 거짓**이 된다.
|
||||||
- 발화 시 **Observer/Effect 구독자에 대해서만** `canExecute(observer)`를
|
- 발화 시 Observer 자신의 `_receive`가 `canExecute(self)`를 확인하고, 거짓이면
|
||||||
확인하고, 거짓이면 **그 구독자에게 `_rerunRequired`만 세우고 건너뜀**(**[2026-08-28
|
**`_rerunRequired`만 세우고 건너뜀**(**[2026-08-28 `H-159`]** 옛 "조용히 건너뜀" —
|
||||||
`H-159`]** 옛 "조용히 건너뜀" — 이제 묶일 때 1회 따라잡는다) — 죽은 `inst`를
|
이제 묶일 때 1회 따라잡는다) — 죽은 `inst`를
|
||||||
건드리는 시도가 일어나지 않게 막는 위 "해야 할 일은 딱 하나" 원칙의
|
건드리는 시도가 일어나지 않게 막는 위 "해야 할 일은 딱 하나" 원칙의
|
||||||
실제 구현 지점.
|
실제 구현 지점.
|
||||||
**⭐⭐ [2026-08-25 정정, 7라운드 `H-56`] 자식 State 노드는 이 게이트를
|
**⭐⭐ [2026-08-25 정정, 7라운드 `H-56`] 자식 State 노드는 이 게이트를
|
||||||
|
|
|
||||||
|
|
@ -1418,7 +1418,14 @@ function activateList(self, physicalTarget)
|
||||||
-- 죽는다(`base/lifecycle-pattern.md`의 `bindLifetime` 계약).
|
-- 죽는다(`base/lifecycle-pattern.md`의 `bindLifetime` 계약).
|
||||||
-- 언마운트 쪽(`unmountSlotTree`)은 이미 `if slot._listObserver then`로
|
-- 언마운트 쪽(`unmountSlotTree`)은 이미 `if slot._listObserver then`로
|
||||||
-- 방어돼 있었는데 이 재마운트 분기만 빠져 있었다.
|
-- 방어돼 있었는데 이 재마운트 분기만 빠져 있었다.
|
||||||
if self._listObserver then bindLifetime(physicalTarget, self._listObserver) end
|
-- ⭐ [2026-08-28 확정, 10라운드 `H-163` (a′)] `_listObserver`는 **여기서 묶지 않는다** —
|
||||||
|
-- `materializeSlotTree`의 꼬리(트리 확정 뒤)가 묶는다. 언마운트 사이의 emit은
|
||||||
|
-- `_receive`가 `_rerunRequired`로 홀드해 뒀는데, 트리가 미확정인 이 시점에 묶으면
|
||||||
|
-- `bindLifetime`의 홀드 발화가 reconcile을 동기 실행해 자리 이중 등록·중첩 Slot
|
||||||
|
-- `canBound` error가 난다. 사용자: *"슬롯트리가 미확정 상황에는 bind 안하고,
|
||||||
|
-- 확정될 때 bind 전에 rerun 끈다"*, 유실 대신 확정 뒤 1회 reconcile은 *"권고가
|
||||||
|
-- 맞는듯. get 자체가 안 나니까"*(재마운트 분기 자체는 reconcile을 안 돌린다 —
|
||||||
|
-- 감사 2라운드 지적). plain table `data`면 `_listObserver`가 `nil`이라 꼬리도 건너뛴다.
|
||||||
bindLifetime(physicalTarget, self._detachCleanup)
|
bindLifetime(physicalTarget, self._detachCleanup)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -2344,6 +2351,7 @@ local function materializeSlotTree(slot, physicalTarget, ownerKey, position)
|
||||||
-- 완주하면, 그 시점 `bk`는 언마운트 전 옛 부기(`Relate(slot)` 위에
|
-- 완주하면, 그 시점 `bk`는 언마운트 전 옛 부기(`Relate(slot)` 위에
|
||||||
-- 살아남는다)라 옛 `N`·옛 자식 목록으로 돈다. 이 Blocker는 이 Slot의 것이고
|
-- 살아남는다)라 옛 `N`·옛 자식 목록으로 돈다. 이 Blocker는 이 Slot의 것이고
|
||||||
-- `setOffsetSource`는 **부모 owner의** blocker를 보므로 간섭이 없다.
|
-- `setOffsetSource`는 **부모 owner의** blocker를 보므로 간섭이 없다.
|
||||||
|
slot._baseObserver._rerunRequired = false -- [2026-08-28 `H-163`] 위 `_listObserver`와 같은 이유
|
||||||
bindLifetime(physicalTarget, slot._baseObserver)
|
bindLifetime(physicalTarget, slot._baseObserver)
|
||||||
-- offset — activateList가 updateFn에 이 값을 넘겨야 하므로(C1). 여기서
|
-- offset — activateList가 updateFn에 이 값을 넘겨야 하므로(C1). 여기서
|
||||||
-- `slot.Offset`이 새 베이스로 `Set`되고, 위 관측자가 두 필드를 0으로 내린 뒤
|
-- `slot.Offset`이 새 베이스로 `Set`되고, 위 관측자가 두 필드를 0으로 내린 뒤
|
||||||
|
|
@ -2381,8 +2389,10 @@ local function materializeSlotTree(slot, physicalTarget, ownerKey, position)
|
||||||
-- `_elements` 안의 중첩 Slot들이 **다시 실체화되지 않는다** —
|
-- `_elements` 안의 중첩 Slot들이 **다시 실체화되지 않는다** —
|
||||||
-- `_physicalTarget`이 `nil`인 채 `_mounted`만 켜지고, `_baseObserver`가
|
-- `_physicalTarget`이 `nil`인 채 `_mounted`만 켜지고, `_baseObserver`가
|
||||||
-- **죽은 옛 target**에 매달린 채 남는다(그 가드 자신이 경고하는 실패 모드).
|
-- **죽은 옛 target**에 매달린 채 남는다(그 가드 자신이 경고하는 실패 모드).
|
||||||
|
local remountingList = slot._listed and slot._listActivated -- [2026-08-28 `H-163` (a′)] 꼬리가 본다 —
|
||||||
|
-- 아래 activateList가 플래그를 켜기 **전**에 잡는다
|
||||||
if slot._listed and not slot._listActivated then
|
if slot._listed and not slot._listActivated then
|
||||||
activateList(slot, physicalTarget) -- 최초 population — rawAdd가 자리마다 등록
|
activateList(slot, physicalTarget) -- 최초 population — rawAdd가 자리마다 등록(observer도 여기서 묶는다)
|
||||||
else
|
else
|
||||||
if slot._listed then
|
if slot._listed then
|
||||||
activateList(slot, physicalTarget) -- 재마운트: 앵커만 새 target으로
|
activateList(slot, physicalTarget) -- 재마운트: 앵커만 새 target으로
|
||||||
|
|
@ -2417,6 +2427,24 @@ local function materializeSlotTree(slot, physicalTarget, ownerKey, position)
|
||||||
if not bk.recomputeBlocker:IsOn() then
|
if not bk.recomputeBlocker:IsOn() then
|
||||||
recompute(slot, bk) -- 여기서 slot.Length가 최종값으로 확정
|
recompute(slot, bk) -- 여기서 slot.Length가 최종값으로 확정
|
||||||
end
|
end
|
||||||
|
-- ⭐ [2026-08-28 확정, 10라운드 `H-163` (a′)] 재마운트의 `_listObserver` 바인드는
|
||||||
|
-- **트리가 확정된 여기서** — `activateList` 재마운트 분기가 넘겨둔 일. 언마운트
|
||||||
|
-- 사이에 `data`가 바뀌었으면(`_receive`가 세운 `_rerunRequired`) 끄고 묶은 뒤
|
||||||
|
-- reconcile을 **정확히 1회** — 상류 `data`는 epoch가 최신이라 `Get` 한 번이면 된다.
|
||||||
|
-- `bindLifetime` 자신의 홀드 발화에 맡기지 않는 이유: 그건 gcconn 연결 직후
|
||||||
|
-- 묶이는 자리에서 돌아 순서를 이 꼬리에 못 맞춘다(`_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())
|
||||||
|
end
|
||||||
|
|
||||||
-- 자기 길이를 부모에게. 이제 **처음부터 최종값**이고(C6), 동시에
|
-- 자기 길이를 부모에게. 이제 **처음부터 최종값**이고(C6), 동시에
|
||||||
-- 어떤 Parent 대입보다도 먼저다(C7) — 단일 함수로는 둘을 동시에
|
-- 어떤 Parent 대입보다도 먼저다(C7) — 단일 함수로는 둘을 동시에
|
||||||
|
|
|
||||||
|
|
@ -251,16 +251,39 @@ function State:_emitDown(from)
|
||||||
local snap = {}
|
local snap = {}
|
||||||
for sub in self._subs do snap[#snap + 1] = sub end
|
for sub in self._subs do snap[#snap + 1] = sub end
|
||||||
for _, sub in ipairs(snap) do
|
for _, sub in ipairs(snap) do
|
||||||
if isState(sub) then -- 자식 노드
|
sub:_receive(from) -- ⭐ [2026-08-28 확정, `H-163` 대화] 구독자는 전부 **`EmitReceive`**
|
||||||
sub:_receive(from) -- state-epoch-plan.md §4 규칙 1~3
|
end -- (`:_receive(from)` 하나짜리 인터페이스 — `Epoch`과 같은 급).
|
||||||
elseif canExecute(sub) then -- Observer (Effect는 자기 내부 Observer로 여기 온다)
|
-- State 노드(`ComputeNode`/`GateNode`)는 §4 규칙, Observer는 아래
|
||||||
sub.fn(sub._state, sub, from) -- ⭐ (리시버 State, Observer 자신, 출처)
|
-- `Observer:_receive`. 한때 여기서 `isState`/`canExecute`로 갈라
|
||||||
else
|
-- Observer의 `fn`을 직접 부르고 `_rerunRequired`까지 세웠는데
|
||||||
sub._rerunRequired = true -- ⭐ [2026-08-28 `H-159`] 묶이기 전의 변경은 버리지 않고 홀드 —
|
-- **계층 간 지식이 섞여** 있었다 — 사용자: *"State:_emitDown 에
|
||||||
end -- `bindLifetime`/`Subscribe`가 1회 발화(`lifecycle-pattern.md`).
|
-- … 계층간 확인 구조가 있거든? 그냥 Epoch 처럼 EmitReceive 를
|
||||||
-- (Effect의 내부 Observer는 `WeakSubscribe`돼 있어 여기 안 옴 —
|
-- 만들고, 각 state 나 observer 측에서 해당 emit 을 처리하는 함수를
|
||||||
-- Effect 쪽 홀드는 `rawRerun`이 한다)
|
-- 만들어주는게 맞는듯. _rerunRequired 를 여기서 설정하는게
|
||||||
|
-- 문제가 되어보여(계층간 지식이 분리 안되어있음)."*
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Observer 쪽 `EmitReceive` 구현(`Observer.luau`). 판정과 홀드가 **여기** 산다.
|
||||||
|
function Observer:_receive(from)
|
||||||
|
if canExecute(self) then
|
||||||
|
self.fn(self._state, self, from) -- ⭐ (리시버 State, Observer 자신, 출처)
|
||||||
|
else
|
||||||
|
self._rerunRequired = true -- [2026-08-28 `H-159`] 묶이기 전의 변경은 홀드 — 묶일 때 1회
|
||||||
|
end -- (Effect의 내부 Observer도 이 경로 — `fire`가 `fn`이다)
|
||||||
|
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`(위).
|
||||||
|
|
||||||
|
-- `state:Observer(fn)` 생성자 — 순서가 계약이다(**[2026-08-28 `H-159`/`H-164`]**).
|
||||||
|
function State:Observer(fn)
|
||||||
|
local o = setmetatable({ fn = fn, _state = self, _rerunRequired = true }, Observer)
|
||||||
|
ObserverBrand:register(o)
|
||||||
|
o.fn(self, o, nil) -- (1) 등록 시점 즉시 1회 — 출처 없음(`nil`)
|
||||||
|
o._rerunRequired = false -- (2) 설치 발화가 플래그를 내린다(사용자 확인)
|
||||||
|
self._subs[o] = true -- (3) 그다음 구독자 집합에 — 순서를 뒤집으면 (1)이 자기
|
||||||
|
return o -- State를 Set할 때 그 emit이 자신의 _receive에 와 플래그가 선다
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1143,10 +1166,11 @@ retract/Destroy되면 자동으로 정리됨.
|
||||||
|
|
||||||
- **`fn`은 등록 시점에 즉시 1회 실행된다(2026-08-07 여섯 번째 세션,
|
- **`fn`은 등록 시점에 즉시 1회 실행된다(2026-08-07 여섯 번째 세션,
|
||||||
사용자 확정 — 이전까지 미명시였던 항목).** (**[2026-08-28 `H-159`]** 이 1회는
|
사용자 확정 — 이전까지 미명시였던 항목).** (**[2026-08-28 `H-159`]** 이 1회는
|
||||||
`state:Observer(fn)` 생성자가 무조건 하는 것이고, 같은 날 신설된 `_rerunRequired`
|
`state:Observer(fn)` 생성자가 무조건 하는 것(Effect 생성자와 같은 모양 —
|
||||||
홀드는 그 **뒤** ~ 묶이기 전 사이의 변경만 다룬다 — 별개다. **생성자 순서는 `fn`
|
`_rerunRequired = true`로 시작해 이 1회가 내린다)이고, 같은 날 신설된 홀드는
|
||||||
1회 실행 → `_subs` 삽입**(`/code-review` 지적으로 고정): 반대면 설치 발화가 자기
|
그 **뒤** ~ 묶이기 전 사이의 변경만 다룬다. **생성자 순서는 `fn` 1회 실행 →
|
||||||
State를 `Set`할 때 그 emit이 아직 안 묶인 자신에게 와 `_rerunRequired`가 서고 첫
|
`_subs` 삽입**(`/code-review` 지적으로 고정): 반대면 설치 발화가 자기 State를
|
||||||
|
`Set`할 때 그 emit이 아직 안 묶인 자신의 `_receive`에 와 플래그가 서고 첫
|
||||||
바인드에서 `fn`이 한 번 더 돈다. Effect의 내부
|
바인드에서 `fn`이 한 번 더 돈다. Effect의 내부
|
||||||
Observer가 이 설치 발화를 `from == nil`로 거르는 이유이기도 하다.) 근거: (1) 이미 채워진
|
Observer가 이 설치 발화를 `from == nil`로 거르는 이유이기도 하다.) 근거: (1) 이미 채워진
|
||||||
State를 나중에 구독하면 그 값을 반영하는 연산이 아예 한 번도 안
|
State를 나중에 구독하면 그 값을 반영하는 연산이 아예 한 번도 안
|
||||||
|
|
@ -1179,9 +1203,13 @@ retract/Destroy되면 자동으로 정리됨.
|
||||||
그건 통지가 아니라 설치라 출처가 존재하지 않는다. 그래서 `emitFrom`은
|
그건 통지가 아니라 설치라 출처가 존재하지 않는다. 그래서 `emitFrom`은
|
||||||
**옵셔널**이고, 이때만 `nil`이다(2026-08-21 커밋 전 `/code-review high`
|
**옵셔널**이고, 이때만 `nil`이다(2026-08-21 커밋 전 `/code-review high`
|
||||||
발견 — 한때 non-optional로 적혀 있었다). `fn`이 `emitFrom`을 실제로 쓰는
|
발견 — 한때 non-optional로 적혀 있었다). `fn`이 `emitFrom`을 실제로 쓰는
|
||||||
소비자라면 `nil`을 "설치 발화"로 분기해야 한다. **⚠️ [2026-08-28 판단 대기,
|
소비자라면 `nil`을 **"출처 없음 — 값을 읽어라"**로 다뤄야 한다. **[2026-08-28
|
||||||
`H-164`]** `H-159`의 Observer 홀드 발화(묶일 때 1회)도 지금 `nil`을 넘겨 이
|
`H-164` 확정]** `nil`은 설치 발화 **또는** 묶일 때의 캐치업(`H-159` 홀드 발화)
|
||||||
분기와 구분이 안 된다 — 갈래는 `-round10.md` §4.
|
둘 다다 — 둘 다 "특정 출처의 통지"가 아니라 "지금 값을 반영하라"라 같은 종류.
|
||||||
|
홀드된 출처를 보관해 넘기는 안은 기각(사용자: *"_rerunRequired 를 from 으로
|
||||||
|
저장하면 여러 홀드 변경이 오면 from 이 날아가지 않아? 애초에 from 을 저장할
|
||||||
|
이유가 왜 있어?"*). `nil`을 "초기화 전용"으로 분기하는 소비자 패턴은 계약이
|
||||||
|
아니다.
|
||||||
- **이건 "값을 안 실어주는 구독" 계약을 안 깬다** — 넘기는 건 값이 아니라
|
- **이건 "값을 안 실어주는 구독" 계약을 안 깬다** — 넘기는 건 값이 아니라
|
||||||
**핸들과 메타데이터**뿐이다.
|
**핸들과 메타데이터**뿐이다.
|
||||||
- 인자 없는 `state:Observer()`(항상 관측 유틸)도 그대로 성립한다 — 넘겨줄
|
- 인자 없는 `state:Observer()`(항상 관측 유틸)도 그대로 성립한다 — 넘겨줄
|
||||||
|
|
@ -1262,8 +1290,9 @@ override할 자리를 구조적으로 열어두는 것.)
|
||||||
동일한 재사용 — "canExecute 하나로 통일" 원칙, 새 메커니즘 발명 아님)
|
동일한 재사용 — "canExecute 하나로 통일" 원칙, 새 메커니즘 발명 아님)
|
||||||
— 발화 시점과 처리 시점 사이에 owning leaf가 이미 죽었으면 no-op.
|
— 발화 시점과 처리 시점 사이에 owning leaf가 이미 죽었으면 no-op.
|
||||||
**[명시화, 2026-08-14 다섯 번째 세션] 이 게이팅이 일어나는 자리는 State의
|
**[명시화, 2026-08-14 다섯 번째 세션] 이 게이팅이 일어나는 자리는 State의
|
||||||
전파 루프**다 — State는 구독자를 **weak로** 담고, 발화 시 각 구독자마다
|
전파 루프**다 — State는 구독자를 **weak로** 담고 `sub:_receive(from)`을 부르며, 발화
|
||||||
`canExecute(observer)`를 확인해 거짓이면 그 구독자만 건너뜀. 여기에
|
시 Observer 자신의 `_receive`가 `canExecute(observer)`를 확인해 거짓이면 홀드
|
||||||
|
(**[2026-08-28 `EmitReceive`]**). 여기에
|
||||||
`inst`가 없다는 사실이 `canExecute`가 `value` 하나만 받아야 하는
|
`inst`가 없다는 사실이 `canExecute`가 `value` 하나만 받아야 하는
|
||||||
이유(`base/lifecycle-pattern.md`의 "실제 호출부" 절, 옛 2-인자
|
이유(`base/lifecycle-pattern.md`의 "실제 호출부" 절, 옛 2-인자
|
||||||
시그니처의 역전 경위는 `archive/canexecute-inst-arg-reversed.md`).
|
시그니처의 역전 경위는 `archive/canexecute-inst-arg-reversed.md`).
|
||||||
|
|
@ -1357,7 +1386,7 @@ no-op. 한때 검토했던 "`isInit=false`면 허용, `isInit=true`+생존확인
|
||||||
세운다.** 즉 갈라지는 지점은 **레지스트리를 강하게 잡느냐뿐**이고,
|
세운다.** 즉 갈라지는 지점은 **레지스트리를 강하게 잡느냐뿐**이고,
|
||||||
`.Subscribed` 플래그는 **강·약 구독 경로 공용**이다. 이게 정해져 있지
|
`.Subscribed` 플래그는 **강·약 구독 경로 공용**이다. 이게 정해져 있지
|
||||||
않아서 두 해석이 각자 다른 확정 문장에 뿌리를 두고 있었고, 안 세우는
|
않아서 두 해석이 각자 다른 확정 문장에 뿌리를 두고 있었고, 안 세우는
|
||||||
쪽으로 읽으면 전파 루프의 `canExecute(sub)` 게이트가 항상 거짓이 되어
|
쪽으로 읽으면 `Observer:_receive`의 `canExecute(sub)` 게이트(**[2026-08-28 `EmitReceive`]** 옛 표현 "전파 루프의")가 항상 거짓이 되어
|
||||||
**`Effect`의 State dep 전량이 조용히 침묵**한다(`Effect`의 내부 Observer는
|
**`Effect`의 State dep 전량이 조용히 침묵**한다(`Effect`의 내부 Observer는
|
||||||
`WeakSubscribe`로만 등록되고, gcconn 경로는 핸들에만 있으므로 남는 판정
|
`WeakSubscribe`로만 등록되고, gcconn 경로는 핸들에만 있으므로 남는 판정
|
||||||
근거가 `.Subscribed`뿐이다). 사용자 원문 *"구현이 한 벌"*과도 이쪽이
|
근거가 `.Subscribed`뿐이다). 사용자 원문 *"구현이 한 벌"*과도 이쪽이
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
| `H-158` | `state:Block(blocker)` 슈가 잔존 (이 대화에서 나옴) | ✅ **확정 폐기** → `state:Apply(blocker)`, 필드 `__apply` |
|
| `H-158` | `state:Block(blocker)` 슈가 잔존 (이 대화에서 나옴) | ✅ **확정 폐기** → `state:Apply(blocker)`, 필드 `__apply` |
|
||||||
| `H-159` | 바인드 전 emit 캐치업 | ✅ **확정 — 사용자 제안 `_rerunRequired`(Gate식 홀드)**, `_installed` 흡수, Observer 대칭, `fire`는 `Update → Rerun`만 |
|
| `H-159` | 바인드 전 emit 캐치업 | ✅ **확정 — 사용자 제안 `_rerunRequired`(Gate식 홀드)**, `_installed` 흡수, Observer 대칭, `fire`는 `Update → Rerun`만 |
|
||||||
| `H-162` | `Void` no-op export (이 대화에서 나옴) | ✅ **확정** — quad-base export(잎 모듈 `Void.luau`), no-op 클로저 자리는 전부 `Void` |
|
| `H-162` | `Void` no-op export (이 대화에서 나옴) | ✅ **확정** — quad-base export(잎 모듈 `Void.luau`), no-op 클로저 자리는 전부 `Void` |
|
||||||
| `H-163`/`H-164` | `H-159` 반영분에 `/code-review high`가 낸 둘 | ⏳ 판단 대기 — Slot 내부 Observer × 홀드 발화 / 홀드 발화의 `emitFrom == nil` |
|
| `H-163` | Slot 내부 Observer × 홀드 발화 | ✅ **확정 (a′)** `_listObserver`는 트리 확정 뒤(`materializeSlotTree` 꼬리)에 끄고 묶고, 홀드가 있었으면 reconcile 1회; `_baseObserver`는 끄고 묶음 + **`EmitReceive` 인터페이스**(전파 루프 계층 분리) |
|
||||||
|
| `H-164` | 홀드 발화의 `emitFrom == nil` | ✅ **전제 정정 (c)** — `nil` = 출처 없음(설치 또는 캐치업), `from` 보관 기각 |
|
||||||
| `H-160` | `Destroying` 경로 cleanup `Rerun` | ✅ **확정 (a) → `H-159`로 정정**: `rawRerun`이 `_cleanupRunning`이면 **버리지 않고 `_rerunRequired`로 홀드** + "error 나면 그 Effect는 죽는다" 계약 |
|
| `H-160` | `Destroying` 경로 cleanup `Rerun` | ✅ **확정 (a) → `H-159`로 정정**: `rawRerun`이 `_cleanupRunning`이면 **버리지 않고 `_rerunRequired`로 홀드** + "error 나면 그 Effect는 죽는다" 계약 |
|
||||||
| `H-161` | M5 루트 부착·다중 스크립트 `Claim` | ✅ **확정 (a)** `Claim`을 M5 스코프로; §5-7 다중 스크립트는 미결 |
|
| `H-161` | M5 루트 부착·다중 스크립트 `Claim` | ✅ **확정 (a)** `Claim`을 M5 스코프로; §5-7 다중 스크립트는 미결 |
|
||||||
| `H-153` | Store 예약 이름 런타임 가드 | ✅ **확정 (a)** — 생성자·`Of(name)`에 예약 이름 검사(level 2), 그림자 = store 자신 (I) |
|
| `H-153` | Store 예약 이름 런타임 가드 | ✅ **확정 (a)** — 생성자·`Of(name)`에 예약 이름 검사(level 2), 그림자 = store 자신 (I) |
|
||||||
|
|
@ -365,3 +366,35 @@ session 후속 절) · 2 의미론(**`_rerunRequired` 상태 기계 자체는
|
||||||
stale 셋(`Blocker` 억제·"`fire` 첫 줄"·"죽은 핸들은 no-op") / `ROADMAP.md` M2 `Rerun`
|
stale 셋(`Blocker` 억제·"`fire` 첫 줄"·"죽은 핸들은 no-op") / `ROADMAP.md` M2 `Rerun`
|
||||||
no-op → 홀드 / `lifecycle-pattern.md`의 *조용히 건너뜀* → 홀드 / "M5 이후" 잔존 셋 /
|
no-op → 홀드 / `lifecycle-pattern.md`의 *조용히 건너뜀* → 홀드 / "M5 이후" 잔존 셋 /
|
||||||
followup 절 제목 포인터·개수.
|
followup 절 제목 포인터·개수.
|
||||||
|
|
||||||
|
## `H-163` — Slot은 재바인드 전에 내부 Observer의 홀드를 끈다 (a) + `EmitReceive` 인터페이스
|
||||||
|
|
||||||
|
**사용자 확정**: *"slot 의 canExecute 를 따라 이것도 쌓아두는 등의 작업을 할 필요는 안
|
||||||
|
보이는듯. 이건 단일 대상에 대해 observe 하는거라서, 상류 state 의 온전한 epochmap
|
||||||
|
처리를 받거든. _rerunRequired 를 직접 false 로 바꿔두고 bind 하는건 확실히 맞아보여.
|
||||||
|
… 그냥 슬롯트리가 미확정 상황에는 bind 안하고, 확정될 때 bind 전에 rerun 끈다는
|
||||||
|
괜찮은 생각."* — 처음엔 `activateList` 재마운트 분기에서 끄고 묶었는데 **감사
|
||||||
|
2라운드가 그 분기는 reconcile을 다시 돌리지 않는다**(앵커만 옮기고 return)고 잡아,
|
||||||
|
그대로면 언마운트 중 `data` 변경이 다음 `Set`까지 유실됐다. 갈래 (a′) 트리 확정 뒤
|
||||||
|
(`materializeSlotTree` 꼬리, `OffWithoutEmit`·`recompute` 뒤) 끄고 묶고 홀드가 있었으면
|
||||||
|
`reconcile(data:Get())` 1회 / (b′) 유실을 계약으로 → **사용자 확정 (a′)**: *"권고가
|
||||||
|
맞는듯. get 자체가 안 나니까"*. `_baseObserver`는 `materializeSlotTree` 머리에서 끄고
|
||||||
|
묶는다(그쪽 캐치업은 `setOffsetSource` 경로).
|
||||||
|
|
||||||
|
**같은 발언에서 나온 구조 결정 — `EmitReceive`**: *"State:_emitDown 에 관한 이야기인데,
|
||||||
|
여기 계층간 확인 구조가 있거든? 그냥 Epoch 처럼 EmitReceive 를 만들고, 각 state 나
|
||||||
|
observer 측에서 해당 emit 을 처리하는 함수를 만들어주는게 맞는듯. _rerunRequired 를
|
||||||
|
여기서 설정하는게 문제가 되어보여(계층간 지식이 분리 안되어있음)."* → 전파 루프는
|
||||||
|
`sub:_receive(from)` 한 줄, `Observer:_receive`가 `canExecute` 판정과 홀드를 자기 안에
|
||||||
|
둔다(`source-state-plan.md` `_emitDown` 아래, `lifecycle-pattern.md` (4)절, `ROADMAP.md`).
|
||||||
|
|
||||||
|
## `H-164` — 문항 전제 정정: `emitFrom == nil` = 출처 없음(설치 또는 캐치업), `from` 보관 기각 (c)
|
||||||
|
|
||||||
|
**사용자**: *"H164는 뭔가 이상함. 설치 발화 후 홀드된 변경이 무슨말인지 모르겠음. 아에
|
||||||
|
별도의 이야기일텐데? … _rerunRequired 를 from 으로 저장하면 여러 홀드 변경이 오면 from 이
|
||||||
|
날아가지 않아? 애초에 from 을 저장할 이유가 왜 있어?"* — 맞다. 홀드 발화는 출처 있는
|
||||||
|
통지가 아니라 "묶였으니 값을 읽어라"라 설치 발화와 같은 종류이고, `nil`을 "초기화
|
||||||
|
전용"으로 분기하는 소비자 패턴은 계약이 아니다. Observer의 `_rerunRequired`는 Effect와
|
||||||
|
같은 뜻으로 **생성 시 참 → 설치 발화가 내림 → 묶이기 전 변경이 다시 세움**(사용자
|
||||||
|
확인: *"설치 발화 이후엔 rerunRequired = false 되긴 해."*). 반영: `source-state-plan.md`
|
||||||
|
1171행 계약 문구.
|
||||||
|
|
|
||||||
|
|
@ -350,8 +350,8 @@ spurious 재발행은 `Source:Set`이 같은 값도 emit한다는 확정(`t18`
|
||||||
| **`H-159`** | **[2026-08-28 `/code-review`, 반영 뒤]** `H-151`이 잃은 캐치업 — 바인드 **전**에 온 emit(특히 `Ref`)은 다시 안 온다 | (a) `_bindDestroying`/`resubscribeTail`에 **"묶이는 시점 1회 `Refresh`"**만 되살림(emit 경로의 `_epochs` 갱신은 `H-151`대로 `Update`만) / (b) `Ref` dep만 바인드 시 `.Revision` 대조 / (c) 계약으로 두고 사용자에게 "`Ref`를 dep으로 쓰는 Effect는 그 leaf 뒤에 두라" 문서화 | **(a)** — `H-151`의 근거("다음 emit이 잡는다")가 `Ref`엔 성립하지 않는다; (a)는 `H-151`을 되돌리는 게 아니라 "emit 경로만 미룬다"는 계약과 양립(바인드는 emit 경로가 아님) |
|
| **`H-159`** | **[2026-08-28 `/code-review`, 반영 뒤]** `H-151`이 잃은 캐치업 — 바인드 **전**에 온 emit(특히 `Ref`)은 다시 안 온다 | (a) `_bindDestroying`/`resubscribeTail`에 **"묶이는 시점 1회 `Refresh`"**만 되살림(emit 경로의 `_epochs` 갱신은 `H-151`대로 `Update`만) / (b) `Ref` dep만 바인드 시 `.Revision` 대조 / (c) 계약으로 두고 사용자에게 "`Ref`를 dep으로 쓰는 Effect는 그 leaf 뒤에 두라" 문서화 | **(a)** — `H-151`의 근거("다음 emit이 잡는다")가 `Ref`엔 성립하지 않는다; (a)는 `H-151`을 되돌리는 게 아니라 "emit 경로만 미룬다"는 계약과 양립(바인드는 emit 경로가 아님) |
|
||||||
| **`H-160`** | leaf `Destroying` 콜백이 도는 cleanup 안의 `self:Rerun()`/`dep:Set()` — `canExecute`가 아직 참이라 죽는 inst에서 `fn`이 돌고 새 cleanup이 영구 고아 | (a) `rawRerun` 진입에서 `_cleanupRunning`이면 **버린다**(no-op) — "cleanup은 자기 생명주기를 못 바꾼다"의 `Rerun`판 / (b) `Destroying` 콜백이 `_consumeCleanup` **전에** `.Subscribed`류 표식으로 죽음을 먼저 세움(새 상태) / (c) UB 문서화 | **(a)** — 새 상태 없이 기존 플래그 하나로, `Unsubscribe` 경로와 같은 결과 |
|
| **`H-160`** | leaf `Destroying` 콜백이 도는 cleanup 안의 `self:Rerun()`/`dep:Set()` — `canExecute`가 아직 참이라 죽는 inst에서 `fn`이 돌고 새 cleanup이 영구 고아 | (a) `rawRerun` 진입에서 `_cleanupRunning`이면 **버린다**(no-op) — "cleanup은 자기 생명주기를 못 바꾼다"의 `Rerun`판 / (b) `Destroying` 콜백이 `_consumeCleanup` **전에** `.Subscribed`류 표식으로 죽음을 먼저 세움(새 상태) / (c) UB 문서화 | **(a)** — 새 상태 없이 기존 플래그 하나로, `Unsubscribe` 경로와 같은 결과 |
|
||||||
| **`H-161`** | `H-148` 이후 **M5에 승인된 루트 부착 경로가 없다** + 여러 스크립트가 같은 `PlayerGui`를 `Claim`하면 이중 claim error / 다중 quad UB라 `Claim`이 자기 동기 사례를 막는다 | (a) `Claim`을 **M5 스코프**로 당기고(프로바이더 마일스톤이라 자연스러움) `research/existing-mount-plan.md` §5-7·8 갈래를 같이 정한다 / (b) `Claim` 전까지 임시로 `H-146` 루트 예외(밖에서 `.Parent =`)를 M5 한정으로 되살림 / (c) 루트 컨테이너(부기 대상 아님)는 claim 없이 자식만 붙이는 얇은 표면 신설 | **(a)** — 임시 예외는 하루 만에 뒤집힌 것을 되살리는 것이고, (c)는 `Mount` 기각의 재개방. §5-7(다중 스크립트)은 `Claim`의 "전부 매핑" 계약이 **루트 컨테이너에는 안 맞는다**는 신호라 갈래를 그 문서에 적었다 |
|
| **`H-161`** | `H-148` 이후 **M5에 승인된 루트 부착 경로가 없다** + 여러 스크립트가 같은 `PlayerGui`를 `Claim`하면 이중 claim error / 다중 quad UB라 `Claim`이 자기 동기 사례를 막는다 | (a) `Claim`을 **M5 스코프**로 당기고(프로바이더 마일스톤이라 자연스러움) `research/existing-mount-plan.md` §5-7·8 갈래를 같이 정한다 / (b) `Claim` 전까지 임시로 `H-146` 루트 예외(밖에서 `.Parent =`)를 M5 한정으로 되살림 / (c) 루트 컨테이너(부기 대상 아님)는 claim 없이 자식만 붙이는 얇은 표면 신설 | **(a)** — 임시 예외는 하루 만에 뒤집힌 것을 되살리는 것이고, (c)는 `Mount` 기각의 재개방. §5-7(다중 스크립트)은 `Claim`의 "전부 매핑" 계약이 **루트 컨테이너에는 안 맞는다**는 신호라 갈래를 그 문서에 적었다 |
|
||||||
| **`H-163`** | **[2026-08-28 `/code-review`, `H-159` 반영 뒤]** Slot 내부 Observer(`_listObserver`·`_baseObserver`)에도 홀드 발화가 걸려 재마운트의 `bindLifetime`이 `materializeSlotTree` **도중** `reconcile`을 동기 실행 → 자리 이중 등록, 중첩 Slot이면 `canBound` error | (a) Slot이 자기 내부 Observer를 다시 묶기 전에 `_rerunRequired`를 **지운다**(재마운트 캐치업은 `activateList`가 이미 명시적으로 한다 — 이중) / (b) 홀드 발화를 사용자 Observer에만(내부 Observer는 브랜드로 구분 — 새 구분) / (c) 홀드 발화를 `bindLifetime` 안이 아니라 `materializeSlotTree` 끝(`blocker:OffWithoutEmit()` 뒤)으로 미룸 | **(a)** — 새 구분 없이 한 줄, "재마운트 캐치업의 주체는 Slot"이라는 기존 계약 그대로 |
|
| **`H-163`** ✅ (a) → **(a′)** | **[2026-08-28 `/code-review`, `H-159` 반영 뒤]** Slot 내부 Observer(`_listObserver`·`_baseObserver`)에도 홀드 발화가 걸려 재마운트의 `bindLifetime`이 `materializeSlotTree` **도중** `reconcile`을 동기 실행 → 자리 이중 등록, 중첩 Slot이면 `canBound` error | (a) Slot이 자기 내부 Observer를 다시 묶기 전에 `_rerunRequired`를 **지운다**(재마운트 캐치업은 `activateList`가 이미 명시적으로 한다 — 이중) / (b) 홀드 발화를 사용자 Observer에만(내부 Observer는 브랜드로 구분 — 새 구분) / (c) 홀드 발화를 `bindLifetime` 안이 아니라 `materializeSlotTree` 끝(`blocker:OffWithoutEmit()` 뒤)으로 미룸 | **(a) → (a′)** — (a)의 전제("재마운트 캐치업은 `activateList`가 이미 한다")는 감사 2라운드가 반증(그 분기는 앵커만 옮긴다) → 트리 확정 뒤 끄고 묶고 홀드가 있었으면 reconcile 1회. 소스는 `-round10-followup.md` |
|
||||||
| **`H-164`** | Observer 홀드 발화가 `emitFrom = nil`로 오면 계약("`nil` = 설치 발화")과 구분 불가 — `if emitFrom == nil then initOnly()`로 짠 소비자가 변경을 놓침 | (a) 홀드 시 **마지막 `from`을 보관**(`_rerunRequired = from`, 진리값으로 플래그 겸용)해 그것을 넘김 / (b) 전용 센티널(`HeldEmit`) / (c) 계약 문구만 "`nil` = 설치 **또는** 묶일 때 캐치업" | **(a)** — Observer는 dedup이 없어 "마지막 출처"가 곧 홀드의 내용; 단 필드가 불리언과 출처를 겸하는 게 원칙(한 필드 두 뜻)에 걸리면 (b) |
|
| **`H-164`** ✅ (c) — 문항 전제 정정 | Observer 홀드 발화가 `emitFrom = nil`로 오면 계약("`nil` = 설치 발화")과 구분 불가 — `if emitFrom == nil then initOnly()`로 짠 소비자가 변경을 놓침 | (a) 홀드 시 **마지막 `from`을 보관**(`_rerunRequired = from`, 진리값으로 플래그 겸용)해 그것을 넘김 / (b) 전용 센티널(`HeldEmit`) / (c) 계약 문구만 "`nil` = 설치 **또는** 묶일 때 캐치업" | **(c) — 문항 전제 정정**: 홀드 발화는 출처 있는 통지가 아니라 "묶였으니 값을 읽어라"라 설치 발화와 같은 종류 — `nil` = 출처 없음(설치 또는 캐치업). (a)의 `from` 보관은 사용자 기각(여러 홀드가 오면 앞 것이 날아감, 보관할 이유 없음). 소스는 `-round10-followup.md` |
|
||||||
|
|
||||||
갈래 없는 것(회신 불필요, 반영만): `H-152`(브랜드 등록 한 줄), `H-155`(ROADMAP 넷),
|
갈래 없는 것(회신 불필요, 반영만): `H-152`(브랜드 등록 한 줄), `H-155`(ROADMAP 넷),
|
||||||
`H-156`(`H-32` 문단), `H-157`(실측 완료 표기).
|
`H-156`(`H-32` 문단), `H-157`(실측 완료 표기).
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,6 @@
|
||||||
사용자와 대화형으로 전량 결정·반영됐습니다**(소스는
|
사용자와 대화형으로 전량 결정·반영됐습니다**(소스는
|
||||||
`qa-request/pre-implementation-handtrace-round10-followup.md`). 남은 건 하나 —
|
`qa-request/pre-implementation-handtrace-round10-followup.md`). 남은 건 하나 —
|
||||||
**M2 게이트 아님**:
|
**M2 게이트 아님**:
|
||||||
- **`H-163`/`H-164`** (`-round10.md` §4 표 마지막 둘, `H-159` 반영분에
|
|
||||||
`/code-review high`가 낸 것) — Slot 내부 Observer × 홀드 발화(권고 (a) Slot이
|
|
||||||
재바인드 전에 플래그를 지움) / Observer 홀드 발화의 `emitFrom == nil` 모호(권고
|
|
||||||
(a) 마지막 `from`을 넘김, 한 필드 두 뜻이 걸리면 (b) 센티널).
|
|
||||||
- **`research/existing-mount-plan.md` §5** — 루트/템플릿을 quad가 소유하는
|
- **`research/existing-mount-plan.md` §5** — 루트/템플릿을 quad가 소유하는
|
||||||
`Claim` + `D.Mapper`의 갈래들(루트 디스크립터 이름·물리 순서 계약·비루트
|
`Claim` + `D.Mapper`의 갈래들(루트 디스크립터 이름·물리 순서 계약·비루트
|
||||||
사용·debug 검사 범위·표면 이름 … — 개수는 그 문서 §5가 소스). 방향은 확정, M5
|
사용·debug 검사 범위·표면 이름 … — 개수는 그 문서 §5가 소스). 방향은 확정, M5
|
||||||
|
|
|
||||||
14
ROADMAP.md
14
ROADMAP.md
|
|
@ -410,8 +410,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
(없는 키를 그 자리에서 만들어 저장)는 **폐기**. 그래서 `defaults`가
|
(없는 키를 그 자리에서 만들어 저장)는 **폐기**. 그래서 `defaults`가
|
||||||
곧 선언 키 집합이고 `Names()`가 성립한다
|
곧 선언 키 집합이고 `Names()`가 성립한다
|
||||||
- [ ] **State 전파 루프 — 구독자는 weak, 발화마다 `canExecute` 게이팅**
|
- [ ] **State 전파 루프 — 구독자는 weak, 발화마다 `canExecute` 게이팅**
|
||||||
(2026-08-14 다섯 번째 세션 확정, `base/lifecycle-pattern.md`의 "실제
|
(2026-08-14 다섯 번째 세션 확정, `base/lifecycle-pattern.md`의 "실제 호출부" 절) —
|
||||||
호출부 — State 전파(`emit`)가 `canExecute`로 게이팅한다" 절) —
|
|
||||||
State는 구독자를 **weak-키로만** 담고, 살려두는
|
State는 구독자를 **weak-키로만** 담고, 살려두는
|
||||||
책임은 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있음
|
책임은 `gchold`(leaf) 또는 전역 `Subscribed` 테이블(전역)에 있음
|
||||||
(어디에도 안 묶인 Observer는 GC되어 목록에서 자연히 빠짐).
|
(어디에도 안 묶인 Observer는 GC되어 목록에서 자연히 빠짐).
|
||||||
|
|
@ -427,12 +426,11 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
노드의 생존은 `canExecute`가 아니라 같은 문서의 **`_hold`
|
노드의 생존은 `canExecute`가 아니라 같은 문서의 **`_hold`
|
||||||
불변식**(하류 → 상류 강함)이 책임진다.
|
불변식**(하류 → 상류 강함)이 책임진다.
|
||||||
```lua
|
```lua
|
||||||
if isState(sub) then sub:_receive(from) -- 자식 노드: 게이트 없음
|
sub:_receive(from) -- [2026-08-28 `EmitReceive`] 구독자 전부 같은 인터페이스 — State 노드는 §4 규칙,
|
||||||
elseif canExecute(sub) then -- Observer만 (Effect는 자기 내부 Observer로 온다)
|
-- Observer:_receive가 canExecute 판정·홀드(`_rerunRequired`)를 자기 안에서
|
||||||
sub.fn(sub._state, sub, from) -- [H-109] (리시버 State, Observer 자신, 출처)
|
|
||||||
else sub._rerunRequired = true -- [2026-08-28 `H-159`] 묶이기 전의 변경은 홀드 → 바인드/구독 시 1회
|
|
||||||
end
|
|
||||||
```
|
```
|
||||||
|
(한때 여기서 `isState`/`canExecute`로 갈라 Observer의 `fn`을 직접 불렀다 —
|
||||||
|
계층 지식이 섞여 사용자 지시로 인터페이스화, `base/source-state-plan.md`)
|
||||||
`canExecute`가 `inst`를 인자로 받을 수 없는 이유는 그대로다(State는
|
`canExecute`가 `inst`를 인자로 받을 수 없는 이유는 그대로다(State는
|
||||||
자기가 어느 Instance에 걸렸는지 모름). `state:Observer(fn)`의
|
자기가 어느 Instance에 걸렸는지 모름). `state:Observer(fn)`의
|
||||||
"등록 즉시 1회 실행"은 `bindLifetime` 이전에 동기적으로 일어나므로
|
"등록 즉시 1회 실행"은 `bindLifetime` 이전에 동기적으로 일어나므로
|
||||||
|
|
@ -1526,7 +1524,7 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
헬퍼 `isBoundAlive(value)` 하나(복사된 gcconn의 `.Connected` 또는
|
헬퍼 `isBoundAlive(value)` 하나(복사된 gcconn의 `.Connected` 또는
|
||||||
`.Subscribed`를 봄)를 공유하는 얇은 진입점 둘로 분리** — `bindLifetime`/
|
`.Subscribed`를 봄)를 공유하는 얇은 진입점 둘로 분리** — `bindLifetime`/
|
||||||
`Observer:Subscribe()`의 이중 바인딩 가드는 `canBound`, State emit
|
`Observer:Subscribe()`의 이중 바인딩 가드는 `canBound`, State emit
|
||||||
전파 루프만 `canExecute`.
|
`Observer:_receive`만 `canExecute`(**[2026-08-28 `EmitReceive`]** 옛 표현 "전파 루프만").
|
||||||
**저장은 전부 `SetWeak`**(`SetStrong` 아님 — gchold/gcconn은 아래 M5
|
**저장은 전부 `SetWeak`**(`SetStrong` 아님 — gchold/gcconn은 아래 M5
|
||||||
클로저↔`gchold[1]` 상호 참조로 이미 안전하게 살아있고, "다른 곳에서
|
클로저↔`gchold[1]` 상호 참조로 이미 안전하게 살아있고, "다른 곳에서
|
||||||
안전하게 유지되는 것은 항상 weak로 잡는다"가 일반 규칙).
|
안전하게 유지되는 것은 항상 weak로 잡는다"가 일반 규칙).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue