--[[ Effect — a leaf consumer with several dependencies (`State`/`Source`/`Ref`) and a cleanup contract. `Effect(fn, ...deps)`; `fn(self) -> ...cleanup`. `.claude/base/effect-plan.md`, transcribed as-is: - "확정 구조": the strong owner is ALWAYS the Effect (`_deps` map); registrations at the deps are weak (`ref:WeakCallback`, internal `observer:WeakSubscribe()`); the single fire gate is `canExecute`. - "의사코드 — 생성자": deps validated once (`select("#")`, nil → error, non State/Source/Ref → error, duplicates ignored — `H-70`); one closure PER DEP KIND (`onRefFire(_, ref)` / `onStateFire(_, _, from)`, `H-107`); `fire`: `from == nil` (the internal Observer's install fire) is dropped, else `_epochs:Update(from)` → `Rerun` (`H-151`: the only place `_epochs` is written; no `_blocker`, `H-150`); `_epochs` seeded `Sync`/`TrackFrom` by `isEpoch`; install = `_rerunRequired = true` + `rawRerun(self, true)`. - `_bindDestroying(inst)` (guard `isRunning`; `_unbindDestroying` first; injected `onDestroying(inst, fn)`; replay once if `_rerunRequired`), `_unbindDestroying()` (idempotent), `_consumeCleanup()` (read → clear → `_rerunRequired = true` → run under `_cleanupRunning`, `H-160`). - `rawRerun(self, force)` + public `Rerun()`: re-entrancy is deferred (`_pending`), an unexecutable state HOLDS the request (`H-159`), the loop has no death judgement (`H-147`); errors leave `_running` / `_cleanupRunning` set — that Effect is dead, by contract. - "`EffectHandle:Subscribe()`": four entry points of its OWN (not Observer's bodies, `H-144`), first line `isRunning` guard (`H-147`), `resubscribeTail` replays a held change after registration, `Unsubscribe` consumes the cleanup. Assembly (`H-174`): `Effect.Init(module)` — gates (`module.canExecute` / `module.canBound`) and the injected `module.onDestroying` are read at call time; the registries come from `Observer.luau` (`H-99`), pulled in with `module:RunInit(Observer.Init)` (idempotent). ]] local Brand = require("./Brand") local EpochMap = require("./EpochMap") local ImplRegistry = require("./ImplRegistry") local Observer = require("./Observer") local EffectBrand = Brand.EffectBrand local isState = Brand.isState local isSource = Brand.isSource local isRef = Brand.isRef local isEpoch = Brand.isEpoch -- One implementation per quad instance (`H-174`); storage scheme in `ImplRegistry.luau`. local implsOf = ImplRegistry.implsOf local function createImpl(module: any) module:RunInit(Observer.Init) -- registries live there (`H-99`) local ObserverImpl = Observer.implFor(module) local Subscribed = ObserverImpl._Subscribed local WeakSubscribed = ObserverImpl._WeakSubscribed local Impl = {} Impl.__index = Impl -- ── local helpers, declared before their callers ───────────────── local function isRunning(self: any): boolean -- predicate only; `error` stays in each body (level 2) return self._running == true or self._cleanupRunning == true end -- The body. `force` means exactly one thing: "may be called ignoring -- canExecute" — the install from the constructor, not yet bound. local function rawRerun(self: any, force: boolean) if self._running then self._pending = true -- re-entered while running → deferred re-run return end if self._cleanupRunning or self._dying or (not force and not module.canExecute(self)) then self._rerunRequired = true -- `H-159`/`H-160`/`H-182`: unexecutable (cleanup running / dying / unbound / dead) return -- → HOLD, replayed once on the next bind/subscribe end self._running = true repeat self._pending = false self:_consumeCleanup() -- sets `_rerunRequired = true` self._rerunRequired = false -- the ONLY place the flag goes down: fn really runs -- `H-185` (user, 2026-08-31): the cleanup is ONE function — wrap several -- teardowns in one closure (`function() a() b() end`); extra returns are -- deliberately not collected (a list would widen the re-entry surface). self._cleanup = self.fn(self) until not self._pending self._running = false end -- After registration: replay a held change once (`H-144`; `Refresh` gone, `H-151`). local function resubscribeTail(self: any) if self._rerunRequired then self:Rerun() -- registered now, so the public gate passes end end -- ── hooks called by `bindLifetime` / `unbindLifetime` ───────────── -- `H-184`: `bindLifetime` asks this BEFORE committing the binding, so a refusal -- cannot leave a half-bound handle (bound, `canExecute` true, no Destroying -- connection). Level 3: past this method and `bindLifetime` to the user's call. function Impl._assertBindable(self: any) if isRunning(self) then error("Effect: cannot bind an Effect from inside its own fn or cleanup", 3) -- `H-147` (A) end end function Impl._bindDestroying(self: any, inst: any) -- `H-147`'s guard moved to `_assertBindable` (`H-184`) — `bindLifetime` is the -- only caller and has already asked it before committing the binding. self:_unbindDestroying() -- rebind (portal remount): drop the old connection first — idempotent self._dying = false -- `H-182`: rebinding a survivor of a Destroy wave re-arms it -- (1) leaf dies → cleanup exactly once. The one hook point (`LP-2`). self._destroyConn = module.onDestroying(inst, function() -- injected op, read at call time -- `H-182`: `canExecute` stays true for the rest of the Destroy wave (gcconn -- is cut last) — the flag closes that window so a dep change later in the -- wave HOLDS instead of re-running fn on the dying leaf. NOT `_destroyed` -- (Slot's word): a Slot never rebinds, this handle may — the bind above re-arms. self._dying = true self:_unbindDestroying() self:_consumeCleanup() end) -- (2) catch-up: a change held while unexecutable is replayed once. gcconn is -- connected by now, so the public Rerun's gate passes. if self._rerunRequired then self:Rerun() end end function Impl._unbindDestroying(self: any) if self._destroyConn then self._destroyConn:Disconnect() self._destroyConn = nil end -- Ref callbacks and the internal Observers stay (weakly registered; the gate -- silences them). `_cleanup` is NOT run here. end -- read → clear → run. `_cleanup`'s presence is not "installed" — the flag is. function Impl._consumeCleanup(self: any) local c: any = self._cleanup self._cleanup = nil self._rerunRequired = true -- consumed = must install again at the next chance if c ~= nil then self._cleanupRunning = true -- `H-160`: cleanup runs outside `_running` in two of its three sites (c :: () -> ())() self._cleanupRunning = false end end function Impl.Rerun(self: any): any -- public, no args — always gated rawRerun(self, false) return self end -- ── four entry points — EffectHandle's OWN bodies (`H-144` (b)) ──── function Impl.WeakSubscribe(self: any): any if isRunning(self) then error("Effect: cannot change subscription from inside fn or cleanup", 2) -- `H-147` end if not module.canBound(self) then error(if self.Subscribed then "Effect: already subscribed" else "Effect: already bound to an Instance", 2) end self.Subscribed = true self._dying = false -- `H-182`: every path that re-arms executability lowers it WeakSubscribed[self] = true resubscribeTail(self) return self end function Impl.Subscribe(self: any): any if isRunning(self) then error("Effect: cannot change subscription from inside fn or cleanup", 2) -- `H-147` end if not module.canBound(self) then error(if self.Subscribed then "Effect: already subscribed" else "Effect: already bound to an Instance", 2) end self.Subscribed = true self._dying = false -- `H-182` WeakSubscribed[self] = true Subscribed[self] = true -- the strong keep is up BEFORE the tail runs resubscribeTail(self) return self end function Impl.WeakUnsubscribe(self: any): any -- lenient (`H-133`) — does not touch the cleanup if isRunning(self) then error("Effect: cannot change subscription from inside fn or cleanup", 2) -- `H-147` end if Subscribed[self] ~= nil then error("Effect: subscribed strongly; use :Unsubscribe()", 2) end WeakSubscribed[self] = nil self.Subscribed = false return self end function Impl.Unsubscribe(self: any): any if isRunning(self) then error("Effect: cannot change subscription from inside fn or cleanup", 2) -- `H-147` end if Subscribed[self] == nil then -- gate FIRST: never strongly subscribed → error, cleanup untouched error("Effect: not subscribed strongly; use :WeakUnsubscribe()", 2) end Subscribed[self] = nil WeakSubscribed[self] = nil self.Subscribed = false -- blocks future re-runs self:_consumeCleanup() -- only when the gate passed: the last cleanup exactly once return self end -- ── constructor ────────────────────────────────────────────────── local function Effect(fn: any, ...: any): any if type(fn) ~= "function" then error("Effect: fn must be a function", 2) end local self = setmetatable({ fn = fn, _deps = {}, -- strong owner of every registration _epochs = EpochMap(), _cleanup = nil, _rerunRequired = false, _running = false, _pending = false, _cleanupRunning = false, _dying = false, -- `H-182`: inside a Destroy wave, after our Destroying ran _destroyConn = nil, Subscribed = false, }, Impl) EffectBrand:register(self) -- (0) deps validation — once, in the constructor. `select("#")` so a nil hole -- does not silently truncate the list. -- `H-186` (user-confirmed 2026-08-31): a dep from ANOTHER quad instance is UB — -- deliberately not checked (a guard here closes only half the hole: a foreign -- `bindLifetime` on our handle is unreachable from base). Documented, revisit at M5. local seen = {} for i = 1, select("#", ...) do local d = (select(i, ...)) if d == nil then error(`Effect: dep #{i} is nil`, 2) end if not (isState(d) or isSource(d) or isRef(d)) then error(`Effect: dep #{i} is not a State/Source/Ref`, 2) end seen[d] = true -- duplicates are silently ignored (dedup lives in `_deps`/`_epochs`) end -- (1) register deps — HERE, ONCE. One closure per dep kind (`H-107`). local function fire(from: any) if from == nil then return -- the internal Observer's install fire (no source) cannot `Update(nil)` end if self._epochs:Update(from) then -- `H-151`: the only place `_epochs` is written self:Rerun() -- unexecutable → rawRerun holds it (`H-159`) end end local function onRefFire(_value: any, ref: any) fire(ref) -- Ref: the 2nd arg is the source end local function onStateFire(_targetState: any, _observer: any, from: any) fire(from) -- Observer: the 3rd arg is the source end for d in seen do if isRef(d) then self._deps[d] = onRefFire -- strong owner = Effect d:WeakCallback(onRefFire) -- the Ref side is weak else local o = d:Observer(onStateFire) self._deps[d] = o -- strong owner = Effect o:WeakSubscribe() -- the global registry side is weak end -- seeding by "is it an Epoch" — Source/Ref are (`Sync`), a State hands over -- what it tracks (`_track` → `TrackFrom`, same polymorphic hook `_recompute` uses) if isEpoch(d) then self._epochs:Sync(d) else d:_track(self._epochs) end end -- (2) install — once, immediately; cannot be deferred to bind. self._rerunRequired = true rawRerun(self, true) return self end module.Effect = Effect return Impl end local function Init(module: any) implsOf(module).Effect = createImpl(module) end local function implFor(module: any) local impl = implsOf(module).Effect if impl == nil then error("Effect: Effect.Init(module) has not run for this quad instance", 1) end return impl end return { Init = Init, implFor = implFor, }