124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
/**
|
|
* Local-storage facade used by:
|
|
* - Level unlock state (req 17.1)
|
|
* - Floating control layout (req 17.2)
|
|
* - BGM / SFX volume (req 17.3, 16.4)
|
|
* - Tutorial completion flags (req 17.4)
|
|
* - Story-intro seen flag (req 17.5 / 19.5)
|
|
*
|
|
* Rationale for the thin facade:
|
|
* - The WeChat Mini Game runtime exposes `wx.setStorageSync` while the
|
|
* in-editor / browser preview exposes `sys.localStorage`. We isolate
|
|
* both behind a single `IStorageDriver` interface so that switching
|
|
* platforms is a single line.
|
|
* - On read, a failure never throws: it returns the provided default value
|
|
* (req 17.6 — "must not crash if local storage is unreadable").
|
|
* - All values go through JSON serialisation so that structured objects
|
|
* round-trip without callers having to remember to stringify.
|
|
*/
|
|
|
|
export interface IStorageDriver {
|
|
getItem(key: string): string | null;
|
|
setItem(key: string, value: string): void;
|
|
removeItem(key: string): void;
|
|
}
|
|
|
|
/** Selects the best available driver at runtime. */
|
|
function detectDriver(): IStorageDriver {
|
|
// 1. WeChat Mini Game global
|
|
const wxGlobal = (globalThis as any).wx;
|
|
if (wxGlobal && typeof wxGlobal.setStorageSync === 'function') {
|
|
return {
|
|
getItem(key) {
|
|
try {
|
|
const v = wxGlobal.getStorageSync(key);
|
|
return v === '' ? null : (v as string);
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
setItem(key, value) {
|
|
try {
|
|
wxGlobal.setStorageSync(key, value);
|
|
} catch {
|
|
// swallow — req 17.6
|
|
}
|
|
},
|
|
removeItem(key) {
|
|
try {
|
|
wxGlobal.removeStorageSync(key);
|
|
} catch {
|
|
// swallow
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
// 2. Browser localStorage
|
|
if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) {
|
|
const ls = (globalThis as any).localStorage as Storage;
|
|
return {
|
|
getItem: (k) => ls.getItem(k),
|
|
setItem: (k, v) => ls.setItem(k, v),
|
|
removeItem: (k) => ls.removeItem(k),
|
|
};
|
|
}
|
|
|
|
// 3. In-memory fallback (Jest, Node-only unit tests).
|
|
const mem = new Map<string, string>();
|
|
return {
|
|
getItem: (k) => (mem.has(k) ? (mem.get(k) as string) : null),
|
|
setItem: (k, v) => {
|
|
mem.set(k, v);
|
|
},
|
|
removeItem: (k) => {
|
|
mem.delete(k);
|
|
},
|
|
};
|
|
}
|
|
|
|
export class StorageMgr {
|
|
private driver: IStorageDriver;
|
|
|
|
constructor(driver?: IStorageDriver) {
|
|
this.driver = driver ?? detectDriver();
|
|
}
|
|
|
|
/**
|
|
* Read a JSON-serialisable value. Returns `defaultValue` if the key is
|
|
* missing, unparseable, or the underlying driver throws (req 17.6).
|
|
*/
|
|
public get<T>(key: string, defaultValue: T): T {
|
|
try {
|
|
const raw = this.driver.getItem(key);
|
|
if (raw == null) {
|
|
return defaultValue;
|
|
}
|
|
return JSON.parse(raw) as T;
|
|
} catch {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
/** Write a JSON-serialisable value. Silently ignores driver errors. */
|
|
public set<T>(key: string, value: T): void {
|
|
try {
|
|
this.driver.setItem(key, JSON.stringify(value));
|
|
} catch {
|
|
// req 17.6
|
|
}
|
|
}
|
|
|
|
public remove(key: string): void {
|
|
this.driver.removeItem(key);
|
|
}
|
|
|
|
/** Swap the driver at runtime. Used in unit tests and platform ports. */
|
|
public setDriver(driver: IStorageDriver): void {
|
|
this.driver = driver;
|
|
}
|
|
}
|
|
|
|
/** Shared project-wide storage manager. */
|
|
export const globalStorageMgr = new StorageMgr();
|