From 0848c35c2e61f57f5c5ff807b0906ef8385b11e8 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 26 Apr 2026 21:22:46 +0800 Subject: [PATCH] feat(scene): add transition transaction with rollback flow Made-with: Cursor --- src/scene/SceneManager.ts | 108 +++++++++++++++------------- src/scene/TransitionTransaction.ts | 29 ++++++++ tests/kernel/transition-transaction.test.ts | 24 +++++++ 3 files changed, 111 insertions(+), 50 deletions(-) create mode 100644 src/scene/TransitionTransaction.ts create mode 100644 tests/kernel/transition-transaction.test.ts diff --git a/src/scene/SceneManager.ts b/src/scene/SceneManager.ts index 5a40f6c..8d639af 100644 --- a/src/scene/SceneManager.ts +++ b/src/scene/SceneManager.ts @@ -3,6 +3,7 @@ import Game from "@/core/Game"; import { logger } from "@/core/Logger"; import { SceneType } from "@/enums/SceneType"; import { BaseScene } from "./BaseScene"; +import { TransitionTransaction } from "./TransitionTransaction"; import type { IBaseScene } from "./types"; // Extend the Container interface to allow setting and accessing _isHolderLast @@ -133,64 +134,71 @@ class SceneManager { } const target = this.getSceneOrThrow(name); - - // 处理前一个场景 - if (previous.type === SceneType.Normal) { - if (options?.isHolderLast) { - previous.stage.visible = false; - previous.stage._isHolderLast = true; - } else { - // 销毁显示节点并卸载,但保留场景注册,便于之后再次 changeScene("welcome") 等 - const oldStage = previous.stage; - oldStage.destroy({ children: true }); - this.game.stage.removeChild(oldStage); - if (previous._assetsLoaded) { - await previous.unLoadBundle?.(); - previous._assetsLoaded = false; + const transaction = new TransitionTransaction({ + commit: async () => { + // 处理前一个场景 + if (previous.type === SceneType.Normal) { + if (options?.isHolderLast) { + previous.stage.visible = false; + previous.stage._isHolderLast = true; + } else { + // 销毁显示节点并卸载,但保留场景注册,便于之后再次 changeScene("welcome") 等 + const oldStage = previous.stage; + oldStage.destroy({ children: true }); + this.game.stage.removeChild(oldStage); + if (previous._assetsLoaded) { + await previous.unLoadBundle?.(); + previous._assetsLoaded = false; + } + await previous.onUnLoad?.(); + const nextStage = new Container(); + nextStage.label = previous.name; + (previous as { stage: Container }).stage = nextStage; + previous._layoutDone = false; + } + } else if (previous.type === SceneType.Resident) { + previous.stage.visible = false; + await previous.onUnLoad?.(); } - await previous.onUnLoad?.(); - const nextStage = new Container(); - nextStage.label = previous.name; - (previous as { stage: Container }).stage = nextStage; - previous._layoutDone = false; - } - } else if (previous.type === SceneType.Resident) { - previous.stage.visible = false; - await previous.onUnLoad?.(); - } - this._currentScene = target; - - // 处理目标场景 - if (target.type === SceneType.Normal) { - if (target.stage._isHolderLast) { - target.stage.visible = true; - } else { - if (!this.game.stage.children.includes(target.stage)) { - this.game.stage.addChild(target.stage); + this._currentScene = target; + + // 处理目标场景 + if (target.type === SceneType.Normal) { + if (target.stage._isHolderLast) { + target.stage.visible = true; + } else { + if (!this.game.stage.children.includes(target.stage)) { + this.game.stage.addChild(target.stage); + } + } + } else if (target.type === SceneType.Resident) { + target.stage.visible = true; } - } - } else if (target.type === SceneType.Resident) { - target.stage.visible = true; - } - // 生命周期 - if (!target._assetsLoaded) { - await target.loadBundle?.(); - target._assetsLoaded = true; - } + // 生命周期 + if (!target._assetsLoaded) { + await target.loadBundle?.(); + target._assetsLoaded = true; + } - if (!target._layoutDone) { - await target.layout?.(); - target._layoutDone = true; - } + if (!target._layoutDone) { + await target.layout?.(); + target._layoutDone = true; + } - await target.onLoad?.(); + await target.onLoad?.(); - // 触发回调 - await this.emitStageChange(target, previous); + // 触发回调 + await this.emitStageChange(target, previous); + logger.debug(`SceneManager: changed from "${previous.name}" to "${name}"`); + }, + rollback: async () => { + this._currentScene = previous; + }, + }); - logger.debug(`SceneManager: changed from "${previous.name}" to "${name}"`); + await transaction.execute(); } /** 获取已注册场景 */ diff --git a/src/scene/TransitionTransaction.ts b/src/scene/TransitionTransaction.ts new file mode 100644 index 0000000..a894227 --- /dev/null +++ b/src/scene/TransitionTransaction.ts @@ -0,0 +1,29 @@ +type TransitionAction = () => Promise | void; + +export interface TransitionTransactionConfig { + prepare?: TransitionAction; + commit: TransitionAction; + rollback: TransitionAction; +} + +export class TransitionTransaction { + private readonly prepare: TransitionAction; + private readonly commit: TransitionAction; + private readonly rollback: TransitionAction; + + constructor(config: TransitionTransactionConfig) { + this.prepare = config.prepare ?? (() => {}); + this.commit = config.commit; + this.rollback = config.rollback; + } + + async execute(): Promise { + try { + await this.prepare(); + await this.commit(); + } catch (error) { + await this.rollback(); + throw error; + } + } +} diff --git a/tests/kernel/transition-transaction.test.ts b/tests/kernel/transition-transaction.test.ts new file mode 100644 index 0000000..6022d34 --- /dev/null +++ b/tests/kernel/transition-transaction.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it, vi } from "vitest"; +import { TransitionTransaction } from "@/scene/TransitionTransaction"; + +describe("TransitionTransaction", () => { + it("rolls back when prepare fails and never commits", async () => { + const prepareError = new Error("prepare failed"); + const prepare = vi.fn(async () => { + throw prepareError; + }); + const commit = vi.fn(async () => {}); + const rollback = vi.fn(async () => {}); + + const transaction = new TransitionTransaction({ + prepare, + commit, + rollback, + }); + + await expect(transaction.execute()).rejects.toThrowError("prepare failed"); + expect(prepare).toHaveBeenCalledTimes(1); + expect(rollback).toHaveBeenCalledTimes(1); + expect(commit).not.toHaveBeenCalled(); + }); +});