- EpochMap.luau: Update/Peek/Refresh/Sync/TrackFrom, EpochSet은 집합, 키 weak (state-epoch-plan §3).
- State.luau: InitState(module) → 인스턴스별 임플(implFor로 Source/Store에 전달). _emitDown은
스냅샷 후 sub:_receive(from)(H-163), _receive 규칙 1~3, 시딩 Sync/TrackFrom, 카운터 쌍(H-85),
Get의 Refresh 순회(값만), fn(self=리시버 lazy 핸들, previous?, ...deps), With pass-through,
Apply(fn | __apply 객체, H-158), _hold 강참조. Compute 결과 isModifier 가드, dep isState 검증.
- Source.luau: Set 동일값도 emit(H-68)/Emit/isModifier 가드, SourceBrand+EpochBrand.
- Store.luau: 그림자=store 자신, defaults isSource 화이트리스트 + RESERVED 가드(H-122/H-153),
Of(모듈의 Source를 호출 시점에)/Names.
- quad-types: StateData/State/Source/Store 타입(ty11 최종형), export type function CheckReservedKeys,
Quad에 Source/Store. Compute deps는 ...any — 타입팩 D...는 strict에서 기각(H-176, 스파이크 15 닫힘).
- spec.{epochmap,source,state,store}.luau, spec.init에 Source/Store. ROADMAP 단위 2 체크박스,
round11 §5 단위 2 확인 목록, 세션 원문·요약.
Co-authored-by: qwreey <me@qwreey.moe>
125 lines
4 KiB
Text
125 lines
4 KiB
Text
--[[ EpochMap 계약 — `.claude/base/state-epoch-plan.md` §3 "`EpochMap` — 컴포지션 가능한 부기 객체" ]]
|
|
|
|
local Quad = require("../src")
|
|
local EpochMap = require("../src/EpochMap")
|
|
local Brand = require("../src/Brand")
|
|
|
|
-- strict 모드에서 `collectgarbage`는 선언 안 된 전역 — 표준 luau CLI엔 있다(Roblox엔 없음)
|
|
local collectgarbage = (_G :: any).collectgarbage :: () -> ()
|
|
|
|
local function epoch(rev: number): any
|
|
local e = { Revision = rev }
|
|
Brand.EpochBrand:register(e)
|
|
return e
|
|
end
|
|
|
|
print("=== 1. Update — 읽고 비교하고 덮는다, 하나라도 달랐으면 true ===")
|
|
do
|
|
local m = EpochMap()
|
|
local a = epoch(1)
|
|
assert(m:Update(a) == true, "first sight of an epoch is a change")
|
|
assert(m:Update(a) == false, "same revision again is not")
|
|
a.Revision = 2
|
|
assert(m:Update(a) == true and m:Update(a) == false, "revision moved → true once, then absorbed")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 2. EpochSet — 집합({[Epoch]: true})을 받는다, 배열이 아니다 ===")
|
|
do
|
|
local m = EpochMap()
|
|
local a, b = epoch(1), epoch(1)
|
|
assert(m:Update({ [a] = true, [b] = true }) == true, "set: any unseen key → true")
|
|
assert(m:Update({ [a] = true, [b] = true }) == false, "set: all absorbed → false")
|
|
b.Revision = 5
|
|
assert(m:Update({ [a] = true, [b] = true }) == true, "set: one moved → true")
|
|
assert(m:Update(b) == false, "and it was written even though the answer was already fixed")
|
|
-- (배열 `{a, b}`는 계약 밖 — 집합이어야 한다. 문서가 경고한 그 함정은 여기서 고정하지 않는다.)
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 3. Peek — 비교만, 덮지 않음 ===")
|
|
do
|
|
local m = EpochMap()
|
|
local a = epoch(1)
|
|
assert(m:Peek(a) == true, "unseen → differs")
|
|
assert(m:Peek(a) == true, "still differs: Peek wrote nothing")
|
|
m:Update(a)
|
|
assert(m:Peek(a) == false, "absorbed by Update")
|
|
a.Revision = 2
|
|
assert(m:Peek({ [a] = true }) == true and m:Update(a) == true, "set form too; Update still sees the change")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 4. Sync — 읽지 않고 쓰기만, 반환값 없음 ===")
|
|
do
|
|
local m = EpochMap()
|
|
local a = epoch(3)
|
|
assert(select("#", m:Sync(a)) == 0, "Sync returns nothing")
|
|
assert(m:Update(a) == false, "after Sync the epoch is current")
|
|
local b = epoch(1)
|
|
m:Sync({ [b] = true })
|
|
assert(m:Update(b) == false, "set form")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 5. Refresh — 인자 없는 Update: 추적 중인 키 전부를 라이브로 ===")
|
|
do
|
|
local m = EpochMap()
|
|
local a, b = epoch(1), epoch(1)
|
|
m:Sync({ [a] = true, [b] = true })
|
|
assert(m:Refresh() == false, "nothing moved")
|
|
b.Revision = 2
|
|
assert(m:Refresh() == true, "b moved")
|
|
assert(m:Refresh() == false and m:Update(b) == false, "Refresh wrote the live revision")
|
|
assert(EpochMap():Refresh() == false, "empty map: nothing to walk → false")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 6. TrackFrom — other의 키를 넘겨받아 라이브 리비전으로, other는 안 건드림 ===")
|
|
do
|
|
local up = EpochMap()
|
|
local a = epoch(1)
|
|
up:Sync(a)
|
|
a.Revision = 7 -- up의 저장값(1)은 이제 낡았다
|
|
local down = EpochMap()
|
|
down:TrackFrom(up)
|
|
assert(down:Update(a) == false, "down was filled with the LIVE revision (7), not up's stored 1")
|
|
assert(up:Refresh() == true, "up itself was untouched — it still sees the move")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 7. 키는 weak — Epoch가 죽으면 항목이 사라짐 ===")
|
|
do
|
|
local m = EpochMap()
|
|
local function seed()
|
|
m:Sync(epoch(1))
|
|
end
|
|
seed()
|
|
collectgarbage()
|
|
collectgarbage()
|
|
assert(next((m :: any)._map) == nil, "dead epoch's entry is gone")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== 8. Source/Ref는 Epoch로서 그대로 들어간다 ===")
|
|
do
|
|
local m = EpochMap()
|
|
local src = Quad.Source(0)
|
|
local ref = Quad.Ref(0)
|
|
assert(m:Update({ [src] = true, [ref] = true } :: any) == true, "both are Epochs")
|
|
src:Set(1)
|
|
assert(m:Update(src) == true, "Source:Set bumps its revision")
|
|
ref:Set(1)
|
|
assert(m:Refresh() == true, "Ref:Set too")
|
|
print("PASS")
|
|
end
|
|
|
|
print()
|
|
print("=== ALL PASS ===")
|