- State.luau: GateImpl(Impl 상속) — _receive(emit 맵 Peek, 규칙 3 삼킴, unfold 합류, 정책 호출), _flush(빈 배치 false → weak 스왑 → Sync(batch) → _emitDown(batch); emit(false) 버리기), Impl.Gate(setup 검증, newNode(..., GateImpl)로 StateBrand·시딩·_hold 공유, 반환 검증). - Blocker.luau(잎): On/Off/OffWithoutEmit(스냅샷 순회)/IsOn/Policy(weak-key 핸들, 강한 주인은 onUpstreamEmit 클로저)/__apply(메소드형 → state:Gate). init.luau·quad-types에 Blocker. - quad-types: State.Apply를 교집합 오버로드로(H-179 — 유니온은 필드 있는 객체를 못 받음, 스파이크 luau-test/done/26-*), GateEmit/GateSetup/Blocker 타입, State.Gate. - spec.gate 9절·spec.blocker 7절 ALL PASS, analyze 0. 스파이크 05 → done/(spec.state/effect 3번이 대체). - 문서: :Block 잔재 정정(H-180), typing-limits §1②·ROADMAP·round11·STATUS/README·세션·요약. Co-authored-by: qwreey <me@qwreey.moe>
221 lines
12 KiB
Text
221 lines
12 KiB
Text
--!strict
|
|
--[[
|
|
`Quad` 공개 타입 계약 — 구현 없음, 런타임 값은 없다시피 함(빈 테이블).
|
|
`quad-base`가 이 타입을 구현하고, `quad-roblox`/향후 백엔드·플러그인
|
|
패키지는 무거운 `quad-base` 전체 대신 이 패키지만 의존한다.
|
|
|
|
**왜 별도 패키지인가**: `QuadRoblox(Quad): QuadRoblox`처럼 quad-base
|
|
인스턴스를 **런타임에 주입받는** 패키지는, quad-base를 pesde
|
|
`[dependencies]`로 선언할 필요가 없다 — 실제 값은 호출자가 넘겨준다.
|
|
그런데 `dev_dependencies`로만 선언하면, quad-roblox가 게시된 뒤 다른
|
|
사람이 그 패키지를 설치할 땐 dev dependency가 전파되지 않아 그
|
|
require(타입만 쓰려는 목적이어도 런타임에 실행됨, 실측 확인됨)가 그
|
|
자리에서 못 찾고 크래시한다. 그래서 "항상 안전하게 실 의존성으로 둘 수
|
|
있을 만큼 작은 것"이 따로 필요하고, 그게 이 패키지다.
|
|
(`.claude/base/project-setup-plan.md`/`session/2026-08-19-07-*.md` 참고)
|
|
]]
|
|
|
|
local TypeVersionCheck = require("./luau_packages/type_version_check")
|
|
|
|
-- `Epoch` — 최소 인터페이스(`.claude/base/state-epoch-plan.md` §2). 판정은
|
|
-- identity + "직전과 다른 Revision"뿐, 순서 비교 없음. 런타임 판별은 `isEpoch`.
|
|
export type Epoch = { Revision: number }
|
|
export type EpochSet = { [Epoch]: true } -- 집합이지 배열이 아니다(게이트 배치가 그대로 넘어온다)
|
|
|
|
-- `Relate` — inst를 weak 키로 하는 릴레이션(`.claude/base/relate-plan.md` "API").
|
|
-- `inst`는 항상 weak, `Weak`/`Strong`은 value의 보관 방식만 가리킨다.
|
|
export type Relate = {
|
|
SetStrong: (self: Relate, inst: any, key: any, value: any) -> (),
|
|
GetStrong: (self: Relate, inst: any, key: any) -> any?,
|
|
SetWeak: (self: Relate, inst: any, key: any, value: any) -> (),
|
|
GetWeak: (self: Relate, inst: any, key: any) -> any?,
|
|
}
|
|
|
|
-- `Ref<T>` 최소형(`.claude/base/ref-plan.md`, M2 `H-128`). 콜백은 `fn(value, ref)` —
|
|
-- 두 번째 인자가 Ref 자신(= `Epoch`). `:Wait`는 M8. **단일 타입 파라미터
|
|
-- `Ref<T>(T)`** — nil이 들어올 수 있는 자리는 호출자가 `Ref<<T?>>(nil)`로 넓힌다
|
|
-- (`ref-plan.md` "제네릭 시그니처").
|
|
export type RefCallback<T> = (value: T, ref: Ref<T>) -> ()
|
|
export type Ref<T> = {
|
|
Value: T,
|
|
Revision: number,
|
|
Callbacks: { [RefCallback<T> | thread]: true }, -- 강한 해시맵 셋(+ M8 `:Wait` 대기자)
|
|
WeakCallbacks: { [RefCallback<T>]: true }, -- weak-key 테이블
|
|
Set: (self: Ref<T>, value: T) -> Ref<T>,
|
|
Callback: (self: Ref<T>, fn: RefCallback<T>) -> Ref<T>,
|
|
WeakCallback: (self: Ref<T>, fn: RefCallback<T>) -> Ref<T>,
|
|
Uncallback: (self: Ref<T>, fn: RefCallback<T>) -> Ref<T>,
|
|
}
|
|
|
|
-- ── 반응형 코어(M2 단위 2) ────────────────────────────────────────────
|
|
-- 선언 스타일은 `.claude/base/typing-limits.md` §1②의 "데이터부/메소드부" 쪼개기 —
|
|
-- 콜백 파라미터 무주석 추론이 사는 유일한 스타일(`audit/handtrace-round10-reference-impl/
|
|
-- spikes/ty11_store_final.luau`가 최종형 실측). `Compute`의 `-> State<U>` 반환은 §1①의
|
|
-- 알려진 한계라 **파생 State를 만드는 자리는 결과 타입을 명시 주석**할 것.
|
|
-- `fn(self, previous?, ...deps)` — 전부 lazy 핸들, 값은 `:Get()`으로만
|
|
-- (`.claude/base/source-state-plan.md` "self 인자도 lazy 핸들로 통일").
|
|
export type StateData<T> = { Get: (self: StateData<T>) -> T }
|
|
|
|
-- `Observer` — 값을 안 실어주는 leaf 구독(`source-state-plan.md` "`state:Observer(fn)`").
|
|
-- `fn(targetState, self, emitFrom?)`: 3번째가 출처(`Epoch` | 집합), 설치·캐치업 발화는 `nil`.
|
|
-- 네 진입점은 `lifecycle-pattern.md` (2) — Weak가 프리미티브, `.Subscribed`는 강·약 공용.
|
|
export type Observer = {
|
|
Subscribed: boolean,
|
|
Subscribe: (self: Observer) -> Observer,
|
|
WeakSubscribe: (self: Observer) -> Observer,
|
|
Unsubscribe: (self: Observer) -> Observer,
|
|
WeakUnsubscribe: (self: Observer) -> Observer,
|
|
}
|
|
export type ObserverFn<T> = (targetState: StateData<T>, self: Observer, emitFrom: (Epoch | EpochSet)?) -> ()
|
|
|
|
-- `EffectHandle` — `Effect(fn, ...deps)`의 핸들(`effect-plan.md`). `fn(self) -> ...cleanup`.
|
|
-- 네 진입점은 Observer와 같은 이름·같은 게이트이되 **본문은 자기 것**(`H-144`).
|
|
export type EffectHandle = {
|
|
Subscribed: boolean,
|
|
Rerun: (self: EffectHandle) -> EffectHandle,
|
|
Subscribe: (self: EffectHandle) -> EffectHandle,
|
|
WeakSubscribe: (self: EffectHandle) -> EffectHandle,
|
|
Unsubscribe: (self: EffectHandle) -> EffectHandle,
|
|
WeakUnsubscribe: (self: EffectHandle) -> EffectHandle,
|
|
}
|
|
export type EffectFn = (self: EffectHandle) -> ...(() -> ())
|
|
|
|
-- `state:Gate(setup)`의 정책 계약(`gate-plan.md` 2번): 바깥은 생성 시 1회, 반환 클로저는 상류
|
|
-- emit마다. `emit()`/`emit(true)` = 흡수 집합 flush, `emit(false)` = 버리기, 반환 = 쌓인 게 있었나.
|
|
export type GateEmit = (commit: boolean?) -> boolean
|
|
export type GateSetup = (emit: GateEmit) -> () -> ()
|
|
|
|
-- `Blocker` — `GateNode` 위의 정책(`blocker-plan.md`). `state:Apply(blocker)`가 `__apply`로 배선.
|
|
export type Blocker = {
|
|
IsBlocked: boolean,
|
|
IsOn: (self: Blocker) -> boolean,
|
|
On: (self: Blocker) -> Blocker,
|
|
Off: (self: Blocker) -> Blocker,
|
|
OffWithoutEmit: (self: Blocker) -> Blocker,
|
|
Policy: (self: Blocker, emit: GateEmit) -> () -> (),
|
|
-- `state:Apply(b)`는 gated `State<T>`(같은 T)를 돌려준다 — 호출부가 결과 타입을 명시
|
|
-- (`State<T>.Apply`의 객체 오버로드 주석 참고).
|
|
__apply: (self: any, state: any) -> any,
|
|
}
|
|
|
|
export type State<T> = StateData<T> & {
|
|
-- deps는 `...any`다 — 타입팩 `D...`로 위치 인자를 좁히는 형태는 strict에서 콜백 dep
|
|
-- 추론이 `{read Get: ...}`로 뒤틀려 정상 호출까지 막힌다(M2 단위 2 실측, 스파이크 15가
|
|
-- "미검증"으로 남겨둔 자리). 콜백 안에서 dep 파라미터에 주석을 달 것.
|
|
Compute: <U>(self: StateData<T>, fn: (self: StateData<T>, previous: U?, ...any) -> U, ...any) -> State<U>,
|
|
With: (self: StateData<T>, ...any) -> State<T>,
|
|
-- 애플리커티브 팩토리는 함수이거나 메소드형 `__apply`를 가진 객체(`H-94`/`H-158`).
|
|
-- **교집합 오버로드**로 선언한다(M2 단위 4 실측, `luau-test/done/26-*`): 유니온 하나로
|
|
-- 두면 `Blocker`처럼 필드가 더 있는 객체가 제네릭 `U` 자리에서 너비 서브타이핑을 못
|
|
-- 받아 `state:Apply(blocker)`가 strict에서 막힌다. 객체 쪽은 `any`를 돌려주므로 결과는
|
|
-- `local g: State<number> = s:Apply(b)`로 명시(§1①의 파생 State 명시 주석 관례와 같다).
|
|
Apply: (<U>(self: StateData<T>, factory: (State<T>) -> U) -> U) & ((self: StateData<T>, factory: { __apply: (self: any, state: any) -> any }) -> any),
|
|
-- 등록 즉시 1회 실행. 무인자면 "항상 관측" 유틸.
|
|
Observer: (self: StateData<T>, fn: ObserverFn<T>?) -> Observer,
|
|
-- 전파 경로에 GateNode 하나(값은 안 가린다, 통지만 유보). 같은 T.
|
|
Gate: (self: StateData<T>, setup: GateSetup) -> State<T>,
|
|
}
|
|
-- `Source`는 `State`를 구조적으로 만족하고 동시에 `Epoch`다(다중 태깅).
|
|
export type Source<T> = State<T> & {
|
|
Revision: number,
|
|
Set: (self: Source<T>, v: T) -> Source<T>,
|
|
Emit: (self: Source<T>) -> Source<T>,
|
|
}
|
|
|
|
-- 예약 키 진단 — `keyof<T>`(키 싱글톤 유니온)만 받는다(`H-112`). `error()`는 못 쓴다
|
|
-- (타입 함수 자체가 실패로 판정) — `print` + `types.never`. 목록은 quad-base
|
|
-- `Store.luau`의 `RESERVED` 테이블의 사본이라 이름을 바꿀 땐 둘을 같이.
|
|
-- `.claude/base/typing-limits.md` §0: 타입 함수는 **진단까지만**.
|
|
export type function CheckReservedKeys(keys: type)
|
|
local list = if keys:is("union") then keys:components() else { keys }
|
|
for _, k in list do
|
|
if k:is("singleton") then
|
|
local v = k:value()
|
|
if v == "Of" or v == "Names" or v == "__reservedCheck" then
|
|
print(`quad.Store: "{v}" is a reserved key`)
|
|
return types.never
|
|
end
|
|
end
|
|
end
|
|
return types.singleton(true)
|
|
end
|
|
-- `Store<T>` — `T`는 `{hp: Source<number>, …}` 그대로(평범한 레코드, 타입 함수 없음).
|
|
-- `__reservedCheck`는 팬텀(런타임 nil, 읽지 말 것, `Names()`에 안 들어감).
|
|
export type Store<T> = T & {
|
|
Of: <U>(self: any, name: string) -> Source<U>,
|
|
Names: (self: any) -> { string },
|
|
__reservedCheck: CheckReservedKeys<keyof<T>>,
|
|
}
|
|
|
|
export type Quad = {
|
|
Version: "0.0.0", -- quad-base/pesde.toml의 version과 항상 맞출 것
|
|
debug: boolean,
|
|
New: () -> Quad,
|
|
RunInit: (self: Quad, initFn: (Quad) -> any) -> (),
|
|
AddPlugin: <Self, P>(self: Self, pluginFn: (Self) -> P) -> Self & P,
|
|
|
|
-- M2 공통 기반이 얹는 탑레벨 값(`ROADMAP.md` M2 `H-80`). `Source`/`Store`/
|
|
-- `Effect`/`Blocker`는 각 단위에서 추가된다.
|
|
Relate: () -> Relate,
|
|
Void: (...any) -> (),
|
|
Ref: <T>(default: T) -> Ref<T>,
|
|
-- 단위 2. `State`는 런타임 생성자가 없다(파생 전용) — 타입만 위에서 export.
|
|
Source: <T>(v: T) -> Source<T>,
|
|
Store: <T>(defaults: T?) -> Store<T>,
|
|
-- 단위 3. deps는 State/Source/Ref(`H-70`), `fn`엔 안 넘어간다.
|
|
Effect: (fn: EffectFn, ...any) -> EffectHandle,
|
|
-- 단위 4.
|
|
Blocker: () -> Blocker,
|
|
|
|
-- 생명주기 4종 — quad-base는 인터페이스(에러 스텁)만, 백엔드가 주입
|
|
-- (`.claude/base/lifecycle-pattern.md`). `canBound(v) == not canExecute(v)`.
|
|
bindLifetime: (inst: any, value: any) -> (),
|
|
unbindLifetime: (value: any) -> (),
|
|
canBound: (value: any) -> boolean,
|
|
canExecute: (value: any) -> boolean,
|
|
-- 주입 엔진 op(`architecture.md` EngineOps) — `Effect._bindDestroying`이 부른다. 미주입이면 에러 스텁.
|
|
onDestroying: (inst: any, fn: () -> ()) -> { Connected: boolean, Disconnect: (self: any) -> () },
|
|
|
|
-- 브랜드 술어(`.claude/base/brand-plan.md`). 나머지(`isTag`/`isAttribute*`/
|
|
-- `isTween`/`isSlot`)는 그 타입의 마일스톤에서.
|
|
isEpoch: (x: any) -> boolean,
|
|
isSource: (x: any) -> boolean,
|
|
isState: (x: any) -> boolean,
|
|
isStore: (x: any) -> boolean,
|
|
isObserver: (x: any) -> boolean,
|
|
isEffect: (x: any) -> boolean,
|
|
isBlocker: (x: any) -> boolean,
|
|
isModifier: (x: any) -> boolean,
|
|
isRef: (x: any) -> boolean,
|
|
isPreRef: (x: any) -> boolean,
|
|
isPostRef: (x: any) -> boolean,
|
|
}
|
|
|
|
--[[
|
|
`CheckedQuad<T, Pattern>` — 주입된 값의 실제 타입 `T`가 `Pattern`
|
|
(글롭/캐럿 문자열, `type-version-check` 패키지 참고 — `"*"`/`"N^"`/정확값)에
|
|
맞는 `Version`을 갖는지 컴파일 타임에 확인. quad-base/quad-roblox
|
|
자신은 항상 같은 모노레포에서 같이 개발되므로 지금은 정확히 일치하는
|
|
`"0.0.0"` 패턴으로만 쓰지만(아래 사용법), quad-spring-roblox류
|
|
**독립적으로 게시되는 백엔드 플러그인**은 `"0.*.*"`처럼 느슨한 패턴을
|
|
직접 골라 쓸 수 있다 — 그래서 패턴을 이 타입의 파라미터로 열어둠.
|
|
|
|
**⚠️ 반드시 "가상 필드"로만 쓸 것 — `T`를 직접 패스스루하지 말 것.**
|
|
`TypeVersionCheck.CheckVersion<Actual, Pattern>` 자체가 이미 이 규칙을
|
|
지키게 설계돼 있다(트리비얼한 `true`만 반환, `T`를 절대 참조/재구성
|
|
안 함) — `type function`을 거친 값은 패스스루라도 이후 `AddPlugin`
|
|
같은 제네릭 self 메소드 체이닝이 조용히 깨진다는 게 실측 확인됐기
|
|
때문(`typing-limits.md` §6, `type-version-check/src/init.luau`
|
|
참고).
|
|
|
|
**사용법(필수 — 이 필드를 실제로 참조해야 체크가 평가된다)**:
|
|
```lua
|
|
local checked: QuadTypes.CheckedQuad<typeof(injectedQuad), "0.0.0"> = injectedQuad :: any
|
|
local _ = checked.__versionCheck -- ⚠️ 이 줄이 없으면 체크가 조용히 스킵됨(lazy 평가)
|
|
-- 이후 checked는 injectedQuad와 완전히 같은 타입(원본 그대로) —
|
|
-- AddPlugin 체이닝 등 뒤이은 제네릭 연산이 전부 안전하게 동작함
|
|
```
|
|
]]
|
|
export type CheckedQuad<T, Pattern> = T & { __versionCheck: TypeVersionCheck.CheckVersion<index<T, "Version">, Pattern> }
|
|
|
|
return {}
|