- H-198(사용자 안): 상류 스탬프(dep:_track)를 fn 직전으로 + Get을 수렴까지의 재시작 루프로 — 계약 강화: 모든 Get이 fn 도중 변경(재진입·게이트 유보)을 같은 호출 안에서 수렴시켜 항상 최신 반환. 매 패스 자기 dep Set은 UB. state-epoch-plan §4 세 절 + H-85 bullet 정정, spec.state 6 새 계약, spec.gate 10(리뷰의 영구 stale 재현 → 같은 Get에서 99 + flush 통지만) - H-186(b): 교차 인스턴스 값 혼용 UB 문서화 — architecture 13번 + content-map §4 22번(H-116 이웃), 코드 주석. M5 재검토 - H-205(a): Modifier 가드 level 2→3(직접 Get이면 유저 호출부), spec.state 12 단언 - H-208: Ref:Set 스냅샷을 table.clone + 집합 병합으로(사용자 — 더 싸고 dedup 공짜) - H-209: src 전 파일 pairs/ipairs → generalized iteration(사용자 — 최적화로 더 빠름; 메타테이블 있는 테이블의 raw 순회 실측 확인). 문서 의사코드 표기는 H-178과 같은 급으로 무변경 - H-211: Relate:SetWeak의 캐스트 없는 setmetatable 대입이 IDE(strict)에서 TypeError(플레인 luau-analyze는 솔버 차로 조용) → 로컬 주석 + :: any 경유 - §4 열린 문항 0, 코드 마커 0. 감사 2라운드 수렴(확실 1·의심 1 → 0) Co-authored-by: qwreey <me@qwreey.moe> Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG
118 lines
4.2 KiB
Text
118 lines
4.2 KiB
Text
--[[
|
|
Blocker — value-based emit deferral, as a POLICY on top of `state:Gate`.
|
|
`.claude/base/blocker-plan.md` "메커니즘 (확정)", `.claude/base/gate-plan.md`
|
|
5번 (`blocker:Policy(emit)`), as-is.
|
|
|
|
Blocker() -> blocker
|
|
blocker:On() -- IsBlocked = true, nothing else
|
|
blocker:Off() -- IsBlocked = false FIRST, then every registered handle with emit=true
|
|
blocker:OffWithoutEmit() -- same path with emit=false: the withheld batch is DISCARDED
|
|
blocker:IsOn() -> boolean -- thin read of `IsBlocked`
|
|
blocker:Policy(emit) -> onUpstreamEmit
|
|
-- this blocker's gate policy as a value; registers the
|
|
-- onunblock handle at THIS call (weak-key set)
|
|
state:Apply(blocker) -- == state:Gate(function(emit) return blocker:Policy(emit) end)
|
|
-- via the method-form `__apply` (`H-158`; old `state:Block` is gone)
|
|
|
|
Ownership (`H-63`): the handle set is weak-key; the strong owner of a handle
|
|
is the `onUpstreamEmit` closure the policy returns (upvalue), so a handle
|
|
lives exactly as long as its GateNode. A Blocker holding handles strongly
|
|
was rejected — it would pin every gated state and its upstream chain.
|
|
`Off`/`OffWithoutEmit` snapshot the set before walking (a flush can create
|
|
new gates mid-walk). `IsBlocked` is a plain boolean — nesting the same
|
|
Blocker is deliberately unsupported; make a new one per overlapping batch.
|
|
"HasBlockedEmit" has no field here: it IS `next(gate._withheld) ~= nil`, and
|
|
the gate's `emit(commit) -> boolean` is the only channel to it.
|
|
|
|
Dependency-free leaf (`Brand` only) — it never touches lifetime gates, so it
|
|
is shared across quad instances like `Ref`.
|
|
]]
|
|
|
|
local Brand = require("./Brand")
|
|
local QuadTypes = require("../roblox_packages/quad_types")
|
|
|
|
export type Blocker = QuadTypes.Blocker
|
|
|
|
local BlockerBrand = Brand.BlockerBrand
|
|
|
|
local WEAK_KEY_MT = { __mode = "k" }
|
|
|
|
local BlockerImpl = {}
|
|
BlockerImpl.__index = BlockerImpl
|
|
|
|
function BlockerImpl.IsOn(self: any): boolean
|
|
return self.IsBlocked
|
|
end
|
|
|
|
function BlockerImpl.On(self: any): any
|
|
self.IsBlocked = true
|
|
return self
|
|
end
|
|
|
|
-- Shared by Off/OffWithoutEmit — the only difference is the flag passed on.
|
|
local function runHandles(self: any, doEmit: boolean)
|
|
local snapshot: { (boolean) -> () } = {}
|
|
for handle in self._handles do -- `H-63` (3): snapshot, then walk
|
|
snapshot[#snapshot + 1] = handle
|
|
end
|
|
for _, handle in snapshot do
|
|
if self.IsBlocked then
|
|
-- `H-203` (a): a downstream fired by an earlier handle's flush re-blocked
|
|
-- this blocker — stop; the remaining handles keep their withheld batches
|
|
-- for the next Off. A half-flushed stop is fine by quad's contract
|
|
-- (user, 2026-08-31: "중간에서 멈춰도 quad 정의 상 문제는 없는 상태").
|
|
return
|
|
end
|
|
handle(doEmit)
|
|
end
|
|
end
|
|
|
|
function BlockerImpl.Off(self: any): any
|
|
self.IsBlocked = false -- first, so a flush that re-enters sees the blocker open
|
|
runHandles(self, true)
|
|
return self
|
|
end
|
|
|
|
function BlockerImpl.OffWithoutEmit(self: any): any
|
|
self.IsBlocked = false
|
|
runHandles(self, false)
|
|
return self
|
|
end
|
|
|
|
-- The gate policy, as a value. `emit` is the gate's flush: `emit()` propagates
|
|
-- the withheld batch, `emit(false)` discards it, both no-ops on an empty batch
|
|
-- (so the handle is idempotent for free — gate-plan 8번).
|
|
function BlockerImpl.Policy(self: any, emit: (boolean?) -> boolean): () -> ()
|
|
local function handle(doEmit: boolean)
|
|
if doEmit then
|
|
emit()
|
|
else
|
|
emit(false)
|
|
end
|
|
end
|
|
self._handles[handle] = true -- `H-63` (1): weak-key set
|
|
return function() -- onUpstreamEmit — `H-63` (2): THIS closure is the handle's strong owner
|
|
if self.IsBlocked then
|
|
return -- the gate already put the source into its withheld set; nothing to do
|
|
end
|
|
handle(true) -- open: pass through — going through `handle` is what keeps it alive (upvalue)
|
|
end
|
|
end
|
|
|
|
-- Applicative factory, method form (`H-158`): `state:Apply(blocker)`.
|
|
function BlockerImpl.__apply(self: any, state: any): any
|
|
return state:Gate(function(emit)
|
|
return self:Policy(emit)
|
|
end)
|
|
end
|
|
|
|
local function Blocker(): Blocker
|
|
local self = setmetatable({
|
|
IsBlocked = false,
|
|
_handles = setmetatable({}, WEAK_KEY_MT),
|
|
}, BlockerImpl)
|
|
BlockerBrand:register(self)
|
|
return (self :: any) :: Blocker
|
|
end
|
|
|
|
return Blocker
|