import Dexie, { Table } from 'dexie'; import { logger } from '@motajs/common'; import { IGlobalTrasaction, ISaveRead, ISaveSystem, ISaveSystemConfig } from './types'; import { ISaveableContent, SaveCompression } from '@user/data-base'; import { isNil } from 'lodash-es'; interface ISaveRecord { /** 存档 id */ readonly id: number; /** 存档压缩级别 */ readonly compression: SaveCompression; /** 存档内容 */ readonly data: Map; } interface IGlobalRecord { /** 全局存储的键名 */ readonly key: string; /** 全局存储的内容 */ readonly value: unknown; } export class GlobalTransaction implements IGlobalTrasaction { constructor(readonly table: Table) {} async get(key: string): Promise { const record = await this.table.get(key); return record!.value as T; } async set(key: string, value: unknown): Promise { await this.table.put({ key, value }); } } export class SaveSystem implements ISaveSystem { db!: Dexie; /** 当前的撤回栈 */ private readonly undoStack: ISaveRead[] = []; /** 当前的重做栈 */ private readonly redoStack: ISaveRead[] = []; /** 撤回栈与重做栈的最大长度 */ private stackSize: number = 20; /** 自动存档压缩级别 */ private autosaveLevel: SaveCompression = SaveCompression.LowCompression; /** 普通存档压缩级别 */ private commonSaveLevel: SaveCompression = SaveCompression.HighCompression; /** 普通存档容忍时长 */ private saveTimeTolerance: number = 100; /** 自动存档容忍时长 */ private autosaveTimeTolerance: number = 50; init(name: string) { this.db = new Dexie(name); this.db.version(1).stores({ saves: 'id', global: 'key' }); } config(config: Readonly>): void { if (!isNil(config.autosaveLevel)) { this.autosaveLevel = config.autosaveLevel; } if (!isNil(config.commonSaveLevel)) { this.commonSaveLevel = config.commonSaveLevel; } if (!isNil(config.saveTimeTolerance)) { this.saveTimeTolerance = config.saveTimeTolerance; } if (!isNil(config.autosaveTimeTolerance)) { this.autosaveTimeTolerance = config.autosaveTimeTolerance; } if (!isNil(config.autosaveStackSize)) { const size = config.autosaveStackSize; this.stackSize = size; if (this.undoStack.length > size) { this.undoStack.splice(0, this.undoStack.length - size); } if (this.redoStack.length > size) { this.redoStack.splice(0, this.redoStack.length - size); } } } undoAutosave( current: Map> ): ISaveRead | null { if (this.undoStack.length === 0) return null; const data = new Map(); for (const [key, content] of current) { data.set(key, content.saveState(this.autosaveLevel)); } this.redoStack.push({ compression: this.autosaveLevel, data }); if (this.redoStack.length > this.stackSize) { this.redoStack.splice(0, this.redoStack.length - this.stackSize); } return this.undoStack.pop()!; } redoAutosave( current: Map> ): ISaveRead | null { if (this.redoStack.length === 0) return null; const data = new Map(); for (const [key, content] of current) { data.set(key, content.saveState(this.autosaveLevel)); } this.undoStack.push({ compression: this.autosaveLevel, data }); if (this.undoStack.length > this.stackSize) { this.undoStack.splice(0, this.undoStack.length - this.stackSize); } return this.redoStack.pop()!; } getUndoStack(): ISaveRead[] { return this.undoStack.slice(); } getRedoStack(): ISaveRead[] { return this.redoStack.slice(); } autosave(state: Map>): void { const data = new Map(); for (const [key, content] of state) { data.set(key, content.saveState(this.autosaveLevel)); } this.undoStack.push({ compression: this.autosaveLevel, data }); this.redoStack.length = 0; if (this.undoStack.length > this.stackSize) { this.undoStack.splice(0, this.undoStack.length - this.stackSize); } } async saveAutosaveToDB(): Promise { if (this.undoStack.length === 0) return; const t0 = performance.now(); const top = this.undoStack[this.undoStack.length - 1]; const table = this.db.table('saves'); await table.put({ id: -1, compression: top.compression, data: top.data }); const t1 = performance.now(); if (t1 - t0 > this.autosaveTimeTolerance) { logger.warn( 115, (t1 - t0).toFixed(0), this.autosaveTimeTolerance.toString() ); } } async save( id: number, state: Map> ): Promise { const t0 = performance.now(); const data = new Map(); for (const [key, content] of state) { data.set(key, content.saveState(this.commonSaveLevel)); } const table = this.db.table('saves'); await table.put({ id, compression: this.commonSaveLevel, data }); await this.setGlobal('lastSlot', id); const t1 = performance.now(); if (t1 - t0 > this.saveTimeTolerance) { logger.warn( 114, (t1 - t0).toFixed(0), this.saveTimeTolerance.toString() ); } } async load(id: number): Promise { const table = this.db.table('saves'); const record = await table.get(id); if (record === undefined) return null; return { compression: record.compression, data: record.data }; } async deleteSave(id: number): Promise { const table = this.db.table('saves'); await table.delete(id); } async getLastSlot(): Promise { const value = await this.getGlobal('lastSlot'); return value ?? 0; } async getGlobal(key: string): Promise { const table = this.db.table('global'); const record = await table.get(key); if (!record) return null; else return record.value as T; } async setGlobal(key: string, value: unknown): Promise { const table = this.db.table('global'); await table.put({ key, value }); } async startGlobalTransaction( handle: (transaction: IGlobalTrasaction) => PromiseLike ): Promise { const globalTable = this.db.table('global'); return this.db.transaction('rw', globalTable, () => { return handle(new GlobalTransaction(globalTable)); }); } }