Browse Source

feat(scene): add transition transaction with rollback flow

Made-with: Cursor
master
npmrun 2 weeks ago
parent
commit
0848c35c2e
  1. 108
      src/scene/SceneManager.ts
  2. 29
      src/scene/TransitionTransaction.ts
  3. 24
      tests/kernel/transition-transaction.test.ts

108
src/scene/SceneManager.ts

@ -3,6 +3,7 @@ import Game from "@/core/Game";
import { logger } from "@/core/Logger"; import { logger } from "@/core/Logger";
import { SceneType } from "@/enums/SceneType"; import { SceneType } from "@/enums/SceneType";
import { BaseScene } from "./BaseScene"; import { BaseScene } from "./BaseScene";
import { TransitionTransaction } from "./TransitionTransaction";
import type { IBaseScene } from "./types"; import type { IBaseScene } from "./types";
// Extend the Container interface to allow setting and accessing _isHolderLast // Extend the Container interface to allow setting and accessing _isHolderLast
@ -133,64 +134,71 @@ class SceneManager {
} }
const target = this.getSceneOrThrow(name); const target = this.getSceneOrThrow(name);
const transaction = new TransitionTransaction({
// 处理前一个场景 commit: async () => {
if (previous.type === SceneType.Normal) { // 处理前一个场景
if (options?.isHolderLast) { if (previous.type === SceneType.Normal) {
previous.stage.visible = false; if (options?.isHolderLast) {
previous.stage._isHolderLast = true; previous.stage.visible = false;
} else { previous.stage._isHolderLast = true;
// 销毁显示节点并卸载,但保留场景注册,便于之后再次 changeScene("welcome") 等 } else {
const oldStage = previous.stage; // 销毁显示节点并卸载,但保留场景注册,便于之后再次 changeScene("welcome") 等
oldStage.destroy({ children: true }); const oldStage = previous.stage;
this.game.stage.removeChild(oldStage); oldStage.destroy({ children: true });
if (previous._assetsLoaded) { this.game.stage.removeChild(oldStage);
await previous.unLoadBundle?.(); if (previous._assetsLoaded) {
previous._assetsLoaded = false; 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; this._currentScene = target;
// 处理目标场景 // 处理目标场景
if (target.type === SceneType.Normal) { if (target.type === SceneType.Normal) {
if (target.stage._isHolderLast) { if (target.stage._isHolderLast) {
target.stage.visible = true; target.stage.visible = true;
} else { } else {
if (!this.game.stage.children.includes(target.stage)) { if (!this.game.stage.children.includes(target.stage)) {
this.game.stage.addChild(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) { if (!target._assetsLoaded) {
await target.loadBundle?.(); await target.loadBundle?.();
target._assetsLoaded = true; target._assetsLoaded = true;
} }
if (!target._layoutDone) { if (!target._layoutDone) {
await target.layout?.(); await target.layout?.();
target._layoutDone = true; 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();
} }
/** 获取已注册场景 */ /** 获取已注册场景 */

29
src/scene/TransitionTransaction.ts

@ -0,0 +1,29 @@
type TransitionAction = () => Promise<void> | 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<void> {
try {
await this.prepare();
await this.commit();
} catch (error) {
await this.rollback();
throw error;
}
}
}

24
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();
});
});
Loading…
Cancel
Save