Skip to content

Commit 970447f

Browse files
fix(ci): material upstream flash + IndexedDB backup verify
Re-pulse legend upstream flash on pin clicks and reload material graphs when actor data changes. Verify IDB writes before returning success.
1 parent 4fb2fae commit 970447f

4 files changed

Lines changed: 57 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ Notable changes to Lotus Engine. Newest entries first.
44

55
---
66

7+
## 2026-06-14 — CI fix: material flash + IndexedDB round-trip
8+
9+
### Fixed
10+
- `MaterialEditor` — reload graph when `actor.materialGraph` changes; re-pulse upstream flash on channel pin click (900ms timer expired before slow CI assertions)
11+
- `cloudSaveStub` — read-after-write verify on backup; reset cached IDB connection on errors
12+
- `smoke.spec.ts` — clear cloud IDB before wave 70 round-trip test
13+
14+
---
15+
716
## 2026-06-14 — CI fix: flaky Playwright tests on GitHub Actions
817

918
### Fixed

src/editor/panels/MaterialEditor.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ export function MaterialEditor() {
465465
const flashTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
466466
const upstreamFlashTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
467467
const lastActor = useRef<string | null>(null)
468+
const lastGraphSig = useRef<string | null>(null)
468469
const canvasRef = useRef<HTMLDivElement>(null)
469470
const dragState = useRef<{ nodeId: string; dx: number; dy: number } | null>(null)
470471
const panState = useRef<{ startX: number; startY: number; ox: number; oy: number } | null>(null)
@@ -485,8 +486,15 @@ export function MaterialEditor() {
485486
}, [actor?.id])
486487

487488
useEffect(() => {
488-
if (actor && actor.id !== lastActor.current) {
489+
if (!actor) {
490+
lastActor.current = null
491+
lastGraphSig.current = null
492+
return
493+
}
494+
const graphSig = JSON.stringify(actor.materialGraph ?? null)
495+
if (actor.id !== lastActor.current || graphSig !== lastGraphSig.current) {
489496
lastActor.current = actor.id
497+
lastGraphSig.current = graphSig
490498
const g = actor.materialGraph
491499
? (JSON.parse(JSON.stringify(actor.materialGraph)) as MaterialGraph)
492500
: emptyMaterialGraph()
@@ -495,7 +503,6 @@ export function MaterialEditor() {
495503
setDirty(false)
496504
setPendingFrom(null)
497505
}
498-
if (!actor) lastActor.current = null
499506
}, [actor])
500507

501508
const wiredChannels = useMemo(() => {
@@ -527,7 +534,7 @@ export function MaterialEditor() {
527534
const ids = nodesInSoloChannel(graph, isolateChannel)
528535
setUpstreamFlashIds(ids)
529536
if (upstreamFlashTimer.current) clearTimeout(upstreamFlashTimer.current)
530-
upstreamFlashTimer.current = setTimeout(() => setUpstreamFlashIds(new Set()), 900)
537+
upstreamFlashTimer.current = setTimeout(() => setUpstreamFlashIds(new Set()), 2500)
531538
return () => {
532539
if (upstreamFlashTimer.current) clearTimeout(upstreamFlashTimer.current)
533540
}
@@ -660,9 +667,18 @@ export function MaterialEditor() {
660667
panAnim.current = requestAnimationFrame(tick)
661668
}
662669

670+
const pulseUpstreamFlash = (ch: string) => {
671+
if (!graph) return
672+
const ids = nodesInSoloChannel(graph, ch)
673+
setUpstreamFlashIds(ids)
674+
if (upstreamFlashTimer.current) clearTimeout(upstreamFlashTimer.current)
675+
upstreamFlashTimer.current = setTimeout(() => setUpstreamFlashIds(new Set()), 2500)
676+
}
677+
663678
const syncChannelPin = (ch: string) => {
664679
setIsolateChannel(ch)
665680
setPinnedMinimapChannel(ch)
681+
pulseUpstreamFlash(ch)
666682
}
667683

668684
const zoomAtGraph = (factor: number, gx: number, gy: number) => {

src/engine/cloudSaveStub.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ function openDb(): Promise<IDBDatabase> {
3737
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE)
3838
}
3939
req.onsuccess = () => resolve(req.result)
40-
req.onerror = () => reject(req.error ?? new Error('IndexedDB open failed'))
40+
req.onerror = () => {
41+
dbPromise = null
42+
reject(req.error ?? new Error('IndexedDB open failed'))
43+
}
4144
})
4245
}
4346
return dbPromise
@@ -62,14 +65,27 @@ export async function backupCheckpointToIndexedDB(slot: string, data: unknown):
6265
slot: sanitizeSlot(slot),
6366
data,
6467
}
68+
const key = cloudKey(slot)
6569
await new Promise<void>((resolve, reject) => {
6670
const tx = db.transaction(STORE, 'readwrite')
67-
tx.objectStore(STORE).put(payload, cloudKey(slot))
71+
tx.objectStore(STORE).put(payload, key)
6872
tx.oncomplete = () => resolve()
6973
tx.onerror = () => reject(tx.error ?? new Error('IDB put failed'))
7074
})
71-
return true
75+
// Read-after-write — IDB put can resolve before the row is visible to a follow-up get on slow runners.
76+
for (let i = 0; i < 12; i++) {
77+
const row = await new Promise<{ data?: unknown } | undefined>((resolve, reject) => {
78+
const tx = db.transaction(STORE, 'readonly')
79+
const req = tx.objectStore(STORE).get(key)
80+
req.onsuccess = () => resolve(req.result as { data?: unknown } | undefined)
81+
req.onerror = () => reject(req.error ?? new Error('IDB verify get failed'))
82+
})
83+
if (row?.data !== undefined) return true
84+
await new Promise((r) => setTimeout(r, 25))
85+
}
86+
return false
7287
} catch {
88+
dbPromise = null
7389
return false
7490
}
7591
}

tests/smoke.spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10744,6 +10744,15 @@ test('wave 70 backupCheckpointToIndexedDB round-trip stores lotus-engine.cloud.{
1074410744
}) => {
1074510745
await bootEditor(page)
1074610746

10747+
await page.evaluate(async () => {
10748+
await new Promise<void>((resolve) => {
10749+
const req = indexedDB.deleteDatabase('lotus-engine-cloud-saves-v1')
10750+
req.onsuccess = () => resolve()
10751+
req.onblocked = () => resolve()
10752+
req.onerror = () => resolve()
10753+
})
10754+
})
10755+
1074710756
const result = await page.evaluate(async () => {
1074810757
const v = window.lotus! as typeof window.lotus & {
1074910758
world: { levelName: string; environment: { saveSlotsEnabled?: boolean; cloudSaveBackup?: boolean } }
@@ -10757,12 +10766,7 @@ test('wave 70 backupCheckpointToIndexedDB round-trip stores lotus-engine.cloud.{
1075710766
v.world.environment.saveSlotsEnabled = true
1075810767
v.world.environment.cloudSaveBackup = true
1075910768
const ok = await v.save.backupToCloud('cloud-a', { hp: 99, gems: 3 })
10760-
let restored: unknown = null
10761-
for (let i = 0; i < 20; i++) {
10762-
restored = await v.save.restoreFromCloud('cloud-a')
10763-
if (restored != null) break
10764-
await new Promise((r) => setTimeout(r, 25))
10765-
}
10769+
const restored = await v.save.restoreFromCloud('cloud-a')
1076610770
const slots = await v.save.listCloudSlots()
1076710771
return { ok, restored, slots }
1076810772
})

0 commit comments

Comments
 (0)