fix(07-11): #06-17-5 preserve flag field instances across loadState

- FlagSystem.loadState 按 key 复用现有 FlagCommonField 并用 fromStructured 原地写值
- 存档中不存在的字段一律删除(以存档为准)
- IFlagSystem.loadState jsDoc 补写同引用与删除语义
- 新增同实例保真、getOrInsert 复用、结构化值恢复、删除多余字段回归用例
- flag/saveLoad.test.ts 的「重建字段对象」断言按 Q1=A 裁决改为同实例保真
This commit is contained in:
unanmed 2026-09-17 13:50:09 +08:00
parent 153f4a490a
commit 9964326210
4 changed files with 90 additions and 8 deletions

View File

@ -64,15 +64,15 @@ describe('FlagSystem save and load round trips', () => {
expect(system.occupied('extra')).toBe(false);
});
// 验证 loadState 会重建字段对象并保留恢复值
it('rebuilds field objects on loadState', () => {
// 验证 loadState 在同实例上原地恢复字段值(#06-17-5)
it('keeps field objects on loadState', () => {
const system = new FlagSystem();
const before = system.insertField('score', 7);
const saved = system.saveState();
system.loadState(saved);
expect(system.getField('score')).not.toBe(before);
expect(system.getField('score')).toBe(before);
expect(system.getFieldValue<number>('score')).toBe(7);
});
});

View File

@ -169,3 +169,61 @@ describe('FlagSystem value accessors', () => {
expect(system.getFieldValue<number>('lives')).toBe(3);
});
});
describe('FlagSystem same-reference load (#06-17-5)', () => {
// 验证同 key 字段跨读档为同一实例,且数值恢复到存档点
it('keeps the same field instance and restores its value', () => {
const system = new modules.FlagSystem();
const before = system.getOrInsert('score', 5);
before.set(7);
const saved = system.saveState();
before.set(99);
system.loadState(saved);
expect(system.getField('score')).toBe(before);
expect(before.get()).toBe(7);
});
// 验证读档覆盖后 getOrInsert 仍返回此前持有的同一实例
it('reuses the held field instance through getOrInsert after load', () => {
const system = new modules.FlagSystem();
const before = system.getOrInsert('count', 1);
const saved = system.saveState();
before.set(99);
system.loadState(saved);
expect(system.getOrInsert('count', 42)).toBe(before);
expect(before.get()).toBe(1);
});
// 验证复杂字段值在原地复用实例上以存档克隆恢复
it('restores structured field values on the same instance', () => {
const system = new modules.FlagSystem();
const before = system.getOrInsert('nested', { list: [1, 2] });
const saved = system.saveState();
before.set({ list: [9] });
system.loadState(saved);
expect(system.getField('nested')).toBe(before);
expect(before.get()).toEqual({ list: [1, 2] });
});
// 验证存档中不存在的字段在读档后被删除(以存档为准)
it('deletes fields absent from the save', () => {
const system = new modules.FlagSystem();
system.setFieldValue('score', 7);
const kept = system.getField('score');
const saved = system.saveState();
system.setFieldValue('extra', 1);
system.loadState(saved);
expect(system.getField('score')).toBe(kept);
expect(system.occupied('extra')).toBe(false);
});
});

View File

@ -63,11 +63,26 @@ export class FlagSystem implements IFlagSystem {
}
loadState(state: IFlagSystemSave): void {
this.fieldMap.clear();
// 按 key 复用现有字段原地写值,使外部持有的引用跨读档仍然有效
for (const [key, data] of state.fields) {
const field = new FlagCommonField<unknown>(this, key, undefined);
field.fromStructured(data);
this.fieldMap.set(key, field);
const existing = this.fieldMap.get(key);
if (existing) {
existing.fromStructured(data);
} else {
const field = new FlagCommonField<unknown>(
this,
key,
undefined
);
field.fromStructured(data);
this.fieldMap.set(key, field);
}
}
// 以存档为准:存档中不存在的字段一律删除
for (const key of this.fieldMap.keys()) {
if (!state.fields.has(key)) {
this.fieldMap.delete(key);
}
}
}
}

View File

@ -1,6 +1,6 @@
//#region 字段
import { ISaveableContent } from '@user/data-common';
import { ISaveableContent, SaveCompression } from '@user/data-common';
export interface IFlagCommonField<T> {
/** 此字段所处的 Flag 系统 */
@ -133,6 +133,15 @@ export interface IFlagSystem extends ISaveableContent<IFlagSystemSave> {
* @param defaultValue 字段默认值
*/
getFieldValueDefaults<T>(field: PropertyKey, defaultValue: T): T;
/**
* 读取 Flag 系统存档。存档中已存在的字段会复用原实例并在其实例上原地写值,
* 因此外部通过 {@link getField} / {@link getOrInsert} 持有的字段引用跨读档仍然有效;
* 存档中不存在的字段一律删除,即读档结果以存档为准。
* @param state Flag 系统存档
* @param compression 压缩级别
*/
loadState(state: IFlagSystemSave, compression: SaveCompression): void;
}
//#endregion